fix(code-mode): attach learner_id to compat-proxy calls and surface upstream 402 as the credit UX - #436
fix(code-mode): attach learner_id to compat-proxy calls and surface upstream 402 as the credit UX#436null-crafter wants to merge 5 commits into
Conversation
…pstream 402 as the credit UX
PR E2E - timed_out
No test report was produced (result: Re-trigger it yourself: comment Last completed run for this PR: report — still valid unless this PR has changed since. |
PR E2E - FAILEDFull report, traces and screenshots
|
PR E2E - FAILEDFull report, traces and screenshots
|
PR E2E - FAILEDFull report, traces and screenshots
|
PR E2E - FAILEDFull report, traces and screenshots
|
PR E2E - FAILEDFull report, traces and screenshots
|
PR E2E - OKFull report, traces and screenshots
|
Checklist
Changes
What
Two related fixes to the Code-mode (opencode) OpenAI-compat loopback proxy, so
desktop model calls are attributed to the signed-in learner and respect credit
exhaustion the same way normal chat does.
1.
learner_idquery param on every forwarded model callThe proxy now appends
learner_id=<username>to each request it forwards to…/api/ai-mentor/orgs/<tenant>/v1/*, so upstream usage lands on the rightlearner:
set_opencode_learnercommand, calledfrom an app-root hook (
hooks/use-opencode-learner.ts, mounted inproviders/index.tsx) — reactive to login/logout viauseUsername(); anempty username clears a stale learner. App-global by design: a desktop
install has exactly one signed-in user.
while a session is alive takes effect on its next call.
learner_idis stripped before the real one is appended (names are form-encoded — a
hostile username can't smuggle extra params). The proxy stays a credential
boundary: attribution comes from the app, never from the agent.
main.rs+lib.rs) and addedto the ACL (
permissions/default.tomlblock + default set,capabilities/default.json).2. Insufficient credit (402) forwarded and shown as the normal-chat credit UX
The backend is starting to answer 402 on
chat/completionsat credit 0.Without this change the agent reduces that to an opaque error string (generic
support toast) or swallows it entirely (silent empty turn).
end-to-end regression test proves status, body, and the auth swap), and now
also emits
opencode:payment-requiredto the webview with the parsed body.hooks/use-opencode-402.ts) listens and drives theexisting
handle402Error— insufficient-balance toast, then the StripeUpgradePackageModal/ admin billing redirect / contact-admin banner,exactly as the WebSocket chat path does. If the 402 body lacks the Stripe
pricing table, it's backfilled cache-first from the account-billing query
(
CreditBalancekeeps that cache warm).wasRecent402()suppressesthe generic chat error toast for the same failure — mirroring normal chat,
which returns before its errorHandler on 402.
tauri:devterminal (payload capped at 4 KiB; never in release — thepayload is the user's chat).
Also:
coding-mode-button.tsxnow caches one shared dynamic import of@tauri-apps/api/core— two concurrent first-time imports could race undervitest's module aliasing and hand the loser the unmocked module.
Testing
cargo test opencode_proxy— 10 tests, including URL-building(append/strip/encode
learner_id) and a live end-to-end 402 passthroughagainst a canned upstream.
burst dedupe, suppression window, non-Tauri no-op); full unit suite green
(405 files / 11k+ tests); coverage on changed files ≥95 % lines
(hooks at 100 % / 96.8 %).
Note for manual verification:
pnpm tauri:devloads the deployed frontend(
devUrl), so the frontend half needscargo tauri dev --config '{"build":{"devUrl":"http://127.0.0.1:3000"}}'against a local
pnpm dev, or a deploy. The Rust half is visible immediately(
learner set: <user>at load,← 402 POST …on a credit failure).🤖 Generated with Claude Code