Skip to content

fix(code-mode): attach learner_id to compat-proxy calls and surface upstream 402 as the credit UX - #436

Open
null-crafter wants to merge 5 commits into
mainfrom
fix/attach-learner-id-qp-openai-compat
Open

fix(code-mode): attach learner_id to compat-proxy calls and surface upstream 402 as the credit UX#436
null-crafter wants to merge 5 commits into
mainfrom
fix/attach-learner-id-qp-openai-compat

Conversation

@null-crafter

Copy link
Copy Markdown
Contributor

Checklist

  • Tests were added/updated according to the feature/bugfix/change made
  • Version was rolled according to semver requirements
  • API endpoints openapi schema was updated if applicable

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_id query param on every forwarded model call

The proxy now appends learner_id=<username> to each request it forwards to
…/api/ai-mentor/orgs/<tenant>/v1/*, so upstream usage lands on the right
learner:

  • The username reaches Rust via a new set_opencode_learner command, called
    from an app-root hook (hooks/use-opencode-learner.ts, mounted in
    providers/index.tsx) — reactive to login/logout via useUsername(); an
    empty username clears a stale learner. App-global by design: a desktop
    install has exactly one signed-in user.
  • Read per request (not captured at session spawn), so a login that lands
    while a session is alive takes effect on its next call.
  • The agent controls its own query string, so any agent-supplied learner_id
    is 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.
  • New command registered in both entrypoints (main.rs + lib.rs) and added
    to the ACL (permissions/default.toml block + 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/completions at credit 0.
Without this change the agent reduces that to an opaque error string (generic
support toast) or swallows it entirely (silent empty turn).

  • The proxy still relays the 402 + JSON body to the agent verbatim (new
    end-to-end regression test proves status, body, and the auth swap), and now
    also emits opencode:payment-required to the webview with the parsed body.
  • A new app-root hook (hooks/use-opencode-402.ts) listens and drives the
    existing handle402Error — insufficient-balance toast, then the Stripe
    UpgradePackageModal / 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
    (CreditBalance keeps that cache warm).
  • Retry bursts collapse into one toast+dialog, and wasRecent402() suppresses
    the generic chat error toast for the same failure — mirroring normal chat,
    which returns before its errorHandler on 402.
  • Debug builds also log each forwarded request and its response status to the
    tauri:dev terminal (payload capped at 4 KiB; never in release — the
    payload is the user's chat).

Also: coding-mode-button.tsx now caches one shared dynamic import of
@tauri-apps/api/core — two concurrent first-time imports could race under
vitest's module aliasing and hand the loser the unmocked module.

Testing

  • Rust: cargo test opencode_proxy — 10 tests, including URL-building
    (append/strip/encode learner_id) and a live end-to-end 402 passthrough
    against a canned upstream.
  • Vitest: new suites for both hooks (learner sync 5, 402 UX 8 — backfill,
    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 %).
  • Typecheck, ESLint, Prettier, rustfmt, and both e2e journey-coverage checks
    Note for manual verification: pnpm tauri:dev loads the deployed frontend
    (devUrl), so the frontend half needs
    cargo 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

@null-crafter
null-crafter requested a review from mamigot as a code owner August 11, 2026 18:50
@null-crafter null-crafter added bug Something isn't working run-tests Trigger PR validation pipeline labels Aug 11, 2026
@github-actions

github-actions Bot commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

PR E2E - timed_out

result timed_out
central run 31819862077
commit deb84d18

No test report was produced (result: timed_out). The run did not finish a suite — usually because another PR's run took the shared environment, a newer commit/label superseded this one, or it was abandoned before completing. This says nothing about your code.

Re-trigger it yourself: comment /retest (or /retest <spec> to re-run a single file in minutes). /status shows the queue. (Avoid Re-run jobs — that replays the old merge ref instead of recomputing it against current main.)

Last completed run for this PR: report — still valid unless this PR has changed since.

@github-actions

Copy link
Copy Markdown
Contributor

PR E2E - FAILED

Full report, traces and screenshots

result FAILED
central run 31529481313
commit 8a232696
suite mentor - mode full - env stg1
tested against prod release 1.126.0 (env was synced to it first)
failed 2
new failures 2

@github-actions

Copy link
Copy Markdown
Contributor

PR E2E - FAILED

Full report, traces and screenshots

result FAILED
central run 31602594289
commit 10047a83
suite mentor - mode full - env stg1
tested against prod release 1.126.0 (env was synced to it first)
failed 3
new failures 3

@null-crafter null-crafter added run-tests Trigger PR validation pipeline and removed run-tests Trigger PR validation pipeline labels Aug 12, 2026
@github-actions

Copy link
Copy Markdown
Contributor

PR E2E - FAILED

Full report, traces and screenshots

result FAILED
central run 31612998291
commit 10047a83
suite mentor - mode full - env stg1
tested against prod release 1.126.0 (env was synced to it first)
failed 3
new failures 3

@github-actions

Copy link
Copy Markdown
Contributor

PR E2E - FAILED

Full report, traces and screenshots

result FAILED
central run 31711765603
commit deb84d18
suite mentor - mode full - env stg1
tested against prod release 1.126.0 (env was synced to it first)
failed 1
new failures 1

@null-crafter null-crafter added run-tests Trigger PR validation pipeline and removed run-tests Trigger PR validation pipeline bug Something isn't working labels Aug 14, 2026
@github-actions

Copy link
Copy Markdown
Contributor

PR E2E - FAILED

Full report, traces and screenshots

result FAILED
central run 32058342041
commit deb84d18
suite mentor - mode full - env stg1
tested against prod release 1.126.0 (env was synced to it first)
failed 4
new failures 3

@github-actions

Copy link
Copy Markdown
Contributor

PR E2E - OK

Full report, traces and screenshots

result OK
central run 32145804870
commit 51c4104e
suite mentor - mode full - env stg1
tested against prod release 1.126.0 (env was synced to it first)
failed 0
new failures 0

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

run-tests Trigger PR validation pipeline

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants