feat(agents): ChatGPT payer at Start, statuses and "Using ChatGPT plan"

Merged
CH
5d ago
+16700-115

P25 of the native-harness rollout (task "Bringing Codex or Claude Code Max plans into Luke"). It extends P14 (#4334) to GPT models, and builds on P24 (#4330), P22 (#4302) and P18/P19.

What changed

  • Payer at Start for GPT models.
    • agentRuntimeChoice now honours the pick on the ChatGPT branch too. On a GPT non-fast model with a usable chatgpt_oauth row:
      • chatgpt_plan goes to Codex.
      • api_key / luke go to eve, recorded as api_key when the account saved an OpenAI key and luke otherwise.
      • With no pick, the row's for_agents decides.
      • A pick of the other provider's plan is no pick of this one's.
    • start_agent takes chatgpt_plan.
    • The draft tab's "Who pays" picker offers "ChatGPT plan" / "API key" / "Luke" for a GPT non-fast model. It is not offered when the sign-in is signed out, ineligible or unreadable.
  • Settings › Coding agents › Codex: a "Use my ChatGPT plan for coding agents" switch under a connected sign-in, using P14's PATCH.
  • Row statuses, written from the projection in the same transaction.
    • PROVIDER_KEY_STATUS gains limit_reached and ineligible.
    • account_provider_key gains a nullable limit_resets_at (additive migration). The service answers it as limitResetsAt.
    • codex-moments emits a provider-neutral NATIVE_MOMENT.PLAN, following P22's words:
      • verified on a completed turn.
      • ineligible on *user_not_eligible.
      • invalid on unauthorized / *invalid_user.
      • limit_reached on usageLimitExceeded / *usage_limit*.
    • It also reads account/rateLimits/updated, so a reached limit carries the reset time Codex heard.
    • The Fold turns the moment into NATIVE_WRITE.PLAN_STATUS. runner-events-app writes it with the new setPlanStatus to the row of the agent's recorded payer (PLAN_ROW maps claude_plan too, for P15), inside withNativeProjection's transaction.
    • An unreadable row is never overwritten. verified never lifts invalid, because a renewal's refusal outranks a turn that ran. An already-verified row isn't rewritten on every turn.
  • Refusals in words.
    • refreshChatGptAccess answers ineligible, and limit_reached while a named reset is still ahead.
    • Codex's launch maps these to PLAN_REFUSAL.INELIGIBLE / LIMIT_REACHED, and they surface as the new agent-plan-ineligible / agent-plan-limit-reached refusals, with words for the agent tools and the panel.
    • The agent waits and never switches payer.
  • Label. Under a Codex agent's composer: "Using ChatGPT plan · Manage usage". Manage usage opens https://chatgpt.com/settings/usage, following OpenAI's UI guidelines.
    • For the starter, whose plan it is, a plan that can't serve shows its status instead: "Usage limit reached · Resets …", signed out, not eligible, or unreadable.
    • The status is re-read when the agent's status changes.
    • The composer takes a generic payer slot, so P15's Claude label can use it.

Judgment calls

  • A limit with no reset named doesn't refuse the launch. Otherwise nothing would ever lift it short of reconnecting; the next turn fails again and re-marks it.
  • A reached limit offers the payer at Start (it resets); an ineligible account doesn't.
  • Teammates see the plain label: the plan's status is the starter's alone.
  • Coordinated with P15 in the task chat. LIMIT_REACHED, the reset column and the PLAN moment are shared, so whichever of us lands second does the small merge.

Screenshots

Synthetic data, rendered from the panel in a throwaway harness. The broken OpenAI mark is the harness's missing asset only.

Settings › Codex switch · Start picker on a GPT model:

Composer label, verified · limit reached (the starter's view):

Tests

  • runtime-sessions.test.ts:
    • The ChatGPT pick, the default on and off, the override recording api_key (OpenAI key) or luke on eve, and the Claude plan picked for a GPT model.
    • The refusal mapping now covers limit reached and ineligible.
  • runner-events-app.test.ts, with statuses driven from recorded notifications:
    • P16's recorded launch gives verified.
    • A usageLimitExceeded turn after account/rateLimits/updated gives limit_reached with that reset.
    • unauthorized / subscription_sharing_invalid_user gives invalid, and a later successful turn leaves invalid.
    • user_not_eligible gives ineligible.
    • An unrelated failure, or an agent a key pays for, changes nothing.
  • codex-sessions.test.ts:
    • A reached limit refuses the launch by name until its reset, then launches.
    • Ineligible refuses by name.
    • No runner starts while refused.
  • coder-agent-tools.test.ts: start_agent takes chatgpt_plan, and an unknown payer is refused.
  • payer-model.test.ts, draft-agent-tab.test.ts, chatgpt-plan-section.test.ts and plan-label-model.test.ts cover the panel side.

Checks

  • tsc for features, service, panel and database.
  • Biome and oxlint on the change, and the discarded-effect check.
  • Vitest over the touched suites (106 files, 828 tests, plus the native-events, codex-sessions and panel files above).
  • pnpm check:affected's parallel typecheck was OOM-killed (137) in this 8 GB sandbox, as it was for P14. CI runs the full check.

The developer waived Preview runs for this rollout, so this merges on green. The plan's live ChatGPT-plan task on Preview is the developer's, later.

Made with Luke


Devin Review