cmux NEXT: offer Add Credits when the cmux model router refuses a turn
Leo, 2026-10-26: "if user picks a model they don't have api key, we offer them to add credits or add their own api key"
This PR ships the Add Credits… half of that. The Add API Key… half is split out to manaflow-ai/cmuxterm-hq#5411: no placement makes a key saved from the pane usable today, so a button for it would be a dead end (details below and in the issue).
What changed
A turn the cmux model router refused for want of credits is no longer a dead end.
- The refusal. The router answers 402
inference.plan_requiredorcredits.insufficient, and its Messages endpoint passes only the text on to Claude Code.- Before: that refusal read as a quota failure ("rate-limited") with Switch model.
- Now: it is its own failure kind,
needs-credits, from the router's words or its typed code. The cause line reads " needs cmux credits or your own API key for this model." (the router's own words). The card leads with Add Credits… (filled), then Retry and Switch model, on both refusals.
Why Add Credits… fixes both refusals. In the router, resolveCaller (backend/apps/api/src/inference/route.ts) sends every caller without a paid plan (!plan.paid) down the credits path: inference.plan_required when that caller has no credits either, and credits.insufficient when a credit-funded caller's balance is short for the call. A paid-plan caller never reaches either. So the paid plan lifts both refusals. (An earlier revision of this PR assumed credits.insufficient meant "already on a plan"; that was wrong, and the card no longer treats the two differently.)
Add Credits… runs palette.pro.upgrade, the one paid-plan path from 6b30f93777e (PaidPlanUpgrade: Checkout that returns to this app, else pricing), with source: inference. That source was declared for the model router's paywall but had no caller. No new pricing, no Stripe copy, no money flow. The app has no credit top-up (PLAN.md: top-ups are test mode only, refused on production), so this PR doesn't offer one.
The model picker keeps Log In alone. A signed-out local harness (Claude Code, Codex, Gemini) still lists only its own Log In in place of its models, as on base. An earlier revision also listed Add Credits… there, but cmux credits only help once the chat routes through the cmux model router: buying a plan doesn't sign the local harness in, add its key, or move the chat to the router, so after Checkout the tab would still be signed out with no models. That case is recorded on manaflow-ai/cmuxterm-hq#5411 with the key half.
Native gate. AgentPaneModel.runAction admits palette.pro.upgrade the same way it admits the existing connect actions: only on the person's fresh gesture in the pane. Its source comes from native code, never from the page. Settings > Accounts and every other app action stay out of the pane's reach.
Why no Add API Key… here (manaflow-ai/cmuxterm-hq#5411)
Two review findings:
- A key saved from the pane never reaches a built-in harness. acpmux spawns Claude Code, Codex and Gemini itself. Its per-session env (
_meta.acpmux.env) passes only allow-listed keys (session_env.rsALLOWED_KEYS), and_acpmux/harness/secret/setwrites a${keychain:…}reference only into user profile files; for the built-in profiles it edits nothing. The app'sProviderKeyStoreitem is read only by CodeRouter Connect. - The cmux model router has no BYOK path. It calls providers with server keys only (
INFERENCE_*_KEY, plans/cmux-next/paid-plan.md §2), so a person's key has nowhere to go there either.
The issue names the two ways to make it real: a key sheet whose key reaches the harness env through acpmux, or a router BYOK path.
Gallery (agent-pane.access-offer)
Variants: a refused turn with no credits (inference.plan_required), one short of credits (credits.insufficient), and a pane that cannot run app actions (no buttons, the cause line still names the way out). There are no arms: with one action there is no order to pick.
Before / after
No capture: the Swift side adds no native UI. It only lets the agent pane run the existing paid-plan Checkout (palette.pro.upgrade) on the person's click, and that Checkout surface is unchanged. The visible change is the agent pane webview, captured below.
Gallery entry agent-pane.access-offer, rendered in Chromium on Linux at the same fixture and size.
A turn the cmux model router refused for want of credits
| Light | Dark | |
|---|---|---|
| Before: "ran into a problem", Retry only | ![]() | ![]() |
After, either refusal (inference.plan_required or credits.insufficient; the two render the same card) | ![]() | ![]() |
Risk
Low.
- Webview: adds a failure kind and its card action. The model picker is unchanged from base. The
needs-creditsmatch is narrow ("cmux credits" or the router's two codes), so other quota failures read as before. - Native: widens
action.runby one id behind the existing gesture gate, with a fixed native argument.palette.pro.upgradekeeps its behavior when invoked without arguments (palette, menu, CLI). - Chief paths: none touched.
- Known limits: no Add API Key… yet (#5411 in cmuxterm-hq); no pick-time credit pre-check for the cmux router harness, so the turn offer shows on its first refused turn.
- Overlap: none. This PR no longer changes picker source (only a test in
signed-out-rail.test.tsx).
Tests
Red first, then green, at this scope:
- Webview, on Linux via cmux-ci:
test/access-offer.test.tsx(classification from text and typed code;ACCESS_ACTIONSis only Add Credits…; on both refusals the card is Add Credits…, Retry, Switch model with one filled button and no Add API Key…; no buttons without the pane's actions; no offer on other failures) andtest/signed-out-rail.test.tsx(a signed-out harness's picker tab lists its own Log In alone: no Add Credits…, no Add API Key…, even where the pane can run Add Credits…). Red at 52b68517d65 (3 fail: the old card offered Add API Key… and a "ran out / top-ups" line): run 41115963774. Green with the fix, plusturn-error-card,signed-out-harness,failure-copy-team-account,vp fmt(no changes) andbun run check(0 warnings, 0 errors): 41 pass, 0 fail, run 41116488039. Picker: red at 66a7ab8f445 (the tab listed "Add Credits…"): run 41120778276; green with the fix, the same suites andbun run check: 41 pass, 0 fail, run 41121779463, and at the head 45b5e314120 (format check, the same suites andbun run check): 41 pass, 0 fail, run 41122952653. After merging feat-cmux-next (head b8bfe52c90f): webview suites andbun run check41 pass, 0 fail, run 41125914318; SwiftAgentPaneAccessActionTests2/2,AgentPaneConnectActionTests14/14,PaidPlanUpgradeTests10/10. - Swift, on the Mac fleet (package-test-lane):
AgentPaneAccessActionTests: Add Credits… needs a gesture and passes the fixedsource: inference;accounts.showandaccounts.removestay refused even with a gesture. Red at 52b68517d65 (accounts.showstill reachable). Green at 96baf377207 withAgentPaneConnectActionTestsandPaidPlanUpgradeTests: 2/2, 13/13 and 10/10 passed (Xcode 26.6). - Strings: two new strings in all 21 languages (
needs_reviewoutside English).
Observed / Not run
- Observed: the red and green runs above, on fleet Linux (webview) and a fleet Mac (Swift), and the gallery stills of the turn card.
- Not run: no tagged app build and no live Checkout. The Swift change is a gate-list entry covered by
AgentPaneAccessActionTests; the last picker commit changes no Swift, and the Swift suites were rerun at the merged head.
Agent session: agent session
🤖 Generated with Claude Code



