feat(desktop): the payer at Start and the account default

Merged
CH
5d ago
+16613-64

P14 of the native-harness rollout (task "Bringing Codex or Claude Code Max plans into Luke"). It builds on P11 (#4313, agentRuntimeChoice) and P13 (#4298, the Claude plan row).

What changed

  • Account default. A new account_provider_key.for_agents boolean NOT NULL DEFAULT true column. The migration is additive: one ADD COLUMN. Connecting a plan turns the default on. Replacing the token keeps the choice, because the upsert leaves the column alone. Disconnecting deletes the row, so connecting again turns it back on. The service answers it as forAgents on subscriptions[]. A new PATCH /api/account/provider-keys {provider, kind, forAgents} turns it, and kind must be a subscription kind. The PATCH goes through every layer: wire, contract, client, service handler, store, host act providerKeys.forAgents, and gateway.
  • Settings. Under the Claude plan row, once a plan is connected, a switch: "Use my Claude plan for coding agents".
  • Start sheet. The new-agent draft tab gets a "Who pays" picker beside the model chip. It covers Fork and Continue-in-draft too. It shows only when the account is in the cohort, a Claude plan is connected, and the model is Claude and not a fast variant. The choices are "Claude plan" and eve's payer. eve's payer is labelled "API key" when an Anthropic key is saved and "Luke" otherwise, because that is the key eve actually bills (account-keys.ts). The picker starts on the account default. A pick is sent as payer only for that one agent.
  • Recording. codingAgentStartRequestFields takes an optional payer, and start_agent takes one too, in the same schema style (claude_plan | api_key | luke). agentRuntimeChoice(userId, model, picked?) works like this:
    • When the plan is offered (cohort, Claude model, not fast, native layer, setup-token row), the pick wins; with no pick, the row's for_agents decides.
    • claude_plan goes to Claude Code. Anything else goes to eve and records api_key if an Anthropic key exists, luke otherwise.
    • When the plan isn't offered, the agent goes to eve with no payer recorded, whatever was picked. Nothing changes outside the cohort or for GPT models.
    • Continue, Fork, and start_agent follow the account default unless a payer is passed. The payer is written once on the agent, as before.

Judgment calls

  • The default is stored on the plan's own row rather than in account preferences. It is per account, and P25 gets the ChatGPT row for free. Approved by the orchestrator.
  • The panel's Start button (the split button with no draft) sends no payer, so the account default decides. The per-agent choice is in the draft tab.
  • A pick the offer no longer holds is dropped, so the default decides. Example: the API key was removed after the pick.

Screenshots

Synthetic data, rendered from the panel in a throwaway harness.

Settings switch · Start sheet · picker open:

Tests

  • runtime-sessions.test.ts:
    • The API key override records api_key on eve.
    • With no key, it records luke.
    • With the toggle off, it records api_key or luke.
    • With the toggle off and the plan picked, it records claude_plan on Claude Code.
    • Outside the cohort, picking the plan gives eve with no payer.
    • The existing case already covers default on giving claude_plan on Claude Code.
  • provider-key-store.test.ts: the default is on when connected, turned per account, kept across a replaced token, and back on after reconnecting.
  • provider-keys-procedures.test.ts: the PATCH changes only the bearer's account, a plan the account doesn't hold changes nothing, and api_key is refused.
  • coder-agent-tools.test.ts: start_agent takes payer, and an unknown payer starts nothing.
  • claude-plan-section.test.ts: the switch stands on, turning it off sends the change, and it is absent without a plan.
  • draft-agent-tab.test.ts:
    • The picker shows the default, and the default sends no payer.
    • A pick sends it.
    • With the toggle off, the picker stands on the API key.
    • Outside the cohort or on a GPT model there is no picker and no keys read.
  • payer-model.test.ts (new).

Checks

pnpm check:affected could not finish in the agent's 8 GB sandbox: @luke/service#typecheck was OOM-killed (exit 137) under turbo's parallel run, twice. I ran its parts one at a time instead, and all passed:

  • tsc for service, panel, host, features, database, gateway, web and desktop
  • biome on the changed files
  • check:migrations
  • repository-checks.sh
  • vitest over the touched packages: 288 files, 2739 tests

CI runs the full check.

The developer waived Preview runs for this rollout, so this PR merges on green.

Made with Luke


Devin Review