fix: accept a provider id in auth login, and diagnose plan/account on a Codex 400 - #1181
fix: accept a provider id in auth login, and diagnose plan/account on a Codex 400#1181anandgupta42 wants to merge 2 commits into
Conversation
…on a Codex 400
Two auth-flow defects, both hit by a user tonight.
1. `auth login <provider>` misread the provider name as a URL.
The login positional was declared `[url]` and passed straight to
`fetch(`${url}/.well-known/opencode`)`, so `auth login openai` — the
natural thing to type, given `auth list` prints provider ids and
`auth logout [provider]` takes one — died with
"Failed to load auth provider metadata from openai: fetch() URL is invalid".
The positional is now `[target]` and routes on its shape: an http(s) URL
keeps the well-known behavior unchanged, anything else resolves as a
provider id through the same path `--provider` and the interactive picker
use. Routing lives in `resolveLoginTarget()` so the `instance` predicate
and the handler cannot disagree about which path an invocation takes. An
unmatched id now names the alternatives instead of just rejecting.
2. A wrong-account / insufficient-plan login was undiagnosable.
A ChatGPT OAuth credential on a free-plan account fails every Codex model
request with `{"detail":"The '<model>' model is not supported when using
Codex with a ChatGPT account."}` — which blames the model and never says
the real problem is which account was signed in with.
The Codex fetch wrapper now recognises that one 400 shape and rewrites it
into a message naming the plan and the truncated account id the request
actually used, plus the commands to switch accounts. `auth list` gained the
same annotation for OAuth credentials, so a wrong account is visible before
it costs a debugging session.
The claims come from `auth/oauth-claims.ts`, which decodes only the
non-secret `chatgpt_plan_type` / `chatgpt_account_id` claims (unverified —
diagnostics only, never an authorization decision). Nothing there returns,
logs, or embeds token material, the account id is truncated to eight
characters, and tests assert the rendered strings contain no segment of the
token. `parseJwtClaims` in the Codex plugin now delegates to the same
decoder so there is one implementation.
Model allowlists are untouched; originator/User-Agent headers are unchanged.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VqnuBDGkh1ZT65Ti7e6DHZ
There was a problem hiding this comment.
Claude Code Review
This repository is configured for manual code reviews. Comment @claude review for a one-time review, or @claude review always to subscribe this PR to a review on every future push.
Tip: disable this comment in your organization's Code Review settings.
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
|
👋 This PR was automatically closed by our quality checks. Common reasons:
If you believe this was a mistake, please open an issue explaining your intended contribution and a maintainer will help you. |
|
Warning Review limit reachedNext included review available in 36 minutes. View limit detailsLimit details: You’ve used all 4 included reviews currently available. You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository. Review configuration: ⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Plus Run ID: ⛔ Files ignored due to path filters (1)
📒 Files selected for processing (7)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
👋 This PR was automatically closed by our quality checks. Common reasons:
If you believe this was a mistake, please open an issue explaining your intended contribution and a maintainer will help you. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: bdd742cd43
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| const CODEX_PLAN_MISMATCH_PATTERN = /not supported when using Codex with a ChatGPT account/i | ||
|
|
||
| export function isCodexPlanMismatchBody(body: string | undefined): boolean { | ||
| return Boolean(body) && CODEX_PLAN_MISMATCH_PATTERN.test(body!) |
There was a problem hiding this comment.
Check the decoded plan before diagnosing an entitlement failure
When a valid Plus/Pro credential receives this wording because the requested model itself is unavailable or retired, this phrase-only matcher still rewrites the response as an account-plan failure. The resulting message can literally say the user is on the pro plan, then claim that Codex requires Plus or Pro and that the account cannot run it, directing them to log out instead of changing models. Gate the entitlement diagnosis on a known non-entitled plan, and preserve the original model error for entitled or unknown plans.
Useful? React with 👍 / 👎.
| const identity = result.type === "oauth" ? describeOAuthIdentity(result.access) : undefined | ||
| yield* Prompt.log.info(`${name} ${UI.Style.TEXT_DIM}${result.type}${identity ? ` (${identity})` : ""}`) |
There was a problem hiding this comment.
Fall back to the stored OAuth account ID
When an OpenAI credential gets its account ID from the ID token or the supported organizations[0].id fallback, login saves that value in result.accountId, but this annotation examines only the access-token claims and therefore omits the account entirely. The same omission affects the new mismatch diagnostic even though the request itself uses the stored accountId in ChatGPT-Account-Id, so affected multi-account credentials can still fail without identifying the account actually selected. Pass the stored account ID as a fallback when building both diagnostics.
Useful? React with 👍 / 👎.
| .catch(() => "") | ||
| const enriched = enrichCodexPlanMismatchBody(body, currentAuth.access) | ||
| if (enriched) { | ||
| const identity = extractOAuthIdentity(currentAuth.access) |
There was a problem hiding this comment.
SUGGESTION: Redundant extractOAuthIdentity call — the identity is computed twice for the same token
enrichCodexPlanMismatchBody already calls extractOAuthIdentity(accessToken) internally (via codexPlanMismatchMessage), so this second call re-decodes and re-parses the same JWT payload solely to log the plan/account. Consider having enrichCodexPlanMismatchBody return the identity alongside the message and reuse it here instead of recomputing.
Reply with @kilocode-bot fix it to have Kilo Code address this issue.
| * Decide which login path an invocation takes. A URL positional keeps the | ||
| * well-known behavior it always had; anything else is a provider id, so | ||
| * `auth login openai` works like `auth login --provider openai` rather than | ||
| * being handed to fetch(). An explicit `--provider` flag takes precedence over |
There was a problem hiding this comment.
SUGGESTION: Comment misdescribes precedence — a URL positional actually beats --provider
The JSDoc states "An explicit --provider flag takes precedence over the positional", but resolveLoginTarget checks isAuthProviderUrl(args.target) first, so a URL positional wins over --provider (as the a URL positional keeps the well-known path even alongside --provider test asserts). The actual precedence is: URL positional > --provider > non-URL positional. Update this comment (and the matching claim in the PR description) to match the implementation.
Reply with @kilocode-bot fix it to have Kilo Code address this issue.
Code Review SummaryStatus: 1 Issue Found | Recommendation: Address before merge Overview
Issue Details (click to expand)SUGGESTION
Files Reviewed (4 files)
Fix these issues in Kilo Cloud Previous Review Summary (commit bdd742c)Current summary above is authoritative. Previous snapshots are kept for context only. Previous review (commit bdd742c)Status: 2 Issues Found | Recommendation: Address before merge Overview
Issue Details (click to expand)SUGGESTION
Files Reviewed (8 files)
Reviewed by deepseek-v4-pro · Input: 43.9K · Output: 28.4K · Cached: 415.5K Review guidance: REVIEW.md from base branch |
There was a problem hiding this comment.
5 issues found across 8 files
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="packages/opencode/src/cli/cmd/providers.ts">
<violation number="1" location="packages/opencode/src/cli/cmd/providers.ts:312">
P2: Fall back to the stored OAuth account ID when the access token has no account claim. Otherwise `auth list` and the Codex mismatch diagnostic can omit the account that the request actually selected.</violation>
<violation number="2" location="packages/opencode/src/cli/cmd/providers.ts:313">
P2: When an OAuth token contains control characters in `chatgpt_plan_type`, this interpolation sends them to `@clack/prompts`, enabling newline or ANSI terminal injection in `auth list`. Strip control characters or whitelist the claims before rendering them.</violation>
</file>
<file name="packages/opencode/src/auth/oauth-claims.ts">
<violation number="1" location="packages/opencode/src/auth/oauth-claims.ts:106">
P2: This phrase-only matcher rewrites model-specific 400s for Plus/Pro or unknown-plan credentials as entitlement failures. Gate the rewrite on a known non-entitled plan, and preserve the original model error for entitled or unknown plans.</violation>
<violation number="2" location="packages/opencode/src/auth/oauth-claims.ts:116">
P2: When a claim or matched provider detail contains control characters, this helper emits them unchanged into terminal-facing output. Sanitize or allowlist plan/account values and strip and bound `originalDetail` before rendering `auth list` and Codex errors.
(Based on your team's feedback about preventing response-body leaks in errors.)</violation>
</file>
<file name="packages/opencode/src/plugin/codex.ts">
<violation number="1" location="packages/opencode/src/plugin/codex.ts:553">
P3: On every matching Codex 400, this decodes and parses `currentAuth.access` a second time after `enrichCodexPlanMismatchBody` already decoded it. Return the identity alongside the enriched message and reuse it for logging.</violation>
</file>
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
| * Build the actionable replacement for that 400. Names the account and plan the | ||
| * request actually used, then gives the exact commands to switch accounts. | ||
| */ | ||
| export function codexPlanMismatchMessage(identity: OAuthIdentity, originalDetail?: string): string { |
There was a problem hiding this comment.
P2: When a claim or matched provider detail contains control characters, this helper emits them unchanged into terminal-facing output. Sanitize or allowlist plan/account values and strip and bound originalDetail before rendering auth list and Codex errors.
(Based on your team's feedback about preventing response-body leaks in errors.)
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/opencode/src/auth/oauth-claims.ts, line 116:
<comment>When a claim or matched provider detail contains control characters, this helper emits them unchanged into terminal-facing output. Sanitize or allowlist plan/account values and strip and bound `originalDetail` before rendering `auth list` and Codex errors.
(Based on your team's feedback about preventing response-body leaks in errors.) </comment>
<file context>
@@ -0,0 +1,150 @@
+ * Build the actionable replacement for that 400. Names the account and plan the
+ * request actually used, then gives the exact commands to switch accounts.
+ */
+export function codexPlanMismatchMessage(identity: OAuthIdentity, originalDetail?: string): string {
+ const masked = maskAccountId(identity.accountId)
+ const who = masked ? `Signed in as ChatGPT account ${masked}` : "Signed in with a ChatGPT account"
</file context>
| // unexplained model error. Only non-secret claims are decoded, and the account id | ||
| // is truncated; the token itself is never rendered. | ||
| const identity = result.type === "oauth" ? describeOAuthIdentity(result.access) : undefined | ||
| yield* Prompt.log.info(`${name} ${UI.Style.TEXT_DIM}${result.type}${identity ? ` (${identity})` : ""}`) |
There was a problem hiding this comment.
P2: When an OAuth token contains control characters in chatgpt_plan_type, this interpolation sends them to @clack/prompts, enabling newline or ANSI terminal injection in auth list. Strip control characters or whitelist the claims before rendering them.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/opencode/src/cli/cmd/providers.ts, line 313:
<comment>When an OAuth token contains control characters in `chatgpt_plan_type`, this interpolation sends them to `@clack/prompts`, enabling newline or ANSI terminal injection in `auth list`. Strip control characters or whitelist the claims before rendering them.</comment>
<file context>
@@ -265,7 +305,13 @@ export const ProvidersListCommand = effectCmd({
+ // unexplained model error. Only non-secret claims are decoded, and the account id
+ // is truncated; the token itself is never rendered.
+ const identity = result.type === "oauth" ? describeOAuthIdentity(result.access) : undefined
+ yield* Prompt.log.info(`${name} ${UI.Style.TEXT_DIM}${result.type}${identity ? ` (${identity})` : ""}`)
+ // altimate_change end
}
</file context>
| yield* Prompt.log.info(`${name} ${UI.Style.TEXT_DIM}${result.type}${identity ? ` (${identity})` : ""}`) | |
| yield* Prompt.log.info(`${name} ${UI.Style.TEXT_DIM}${result.type}${identity ? ` (${identity.replace(/[\u0000-\u001f\u007f-\u009f]/g, "")})` : ""}`) |
| // so a wrong-account login is visible here instead of surfacing later as an | ||
| // unexplained model error. Only non-secret claims are decoded, and the account id | ||
| // is truncated; the token itself is never rendered. | ||
| const identity = result.type === "oauth" ? describeOAuthIdentity(result.access) : undefined |
There was a problem hiding this comment.
P2: Fall back to the stored OAuth account ID when the access token has no account claim. Otherwise auth list and the Codex mismatch diagnostic can omit the account that the request actually selected.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/opencode/src/cli/cmd/providers.ts, line 312:
<comment>Fall back to the stored OAuth account ID when the access token has no account claim. Otherwise `auth list` and the Codex mismatch diagnostic can omit the account that the request actually selected.</comment>
<file context>
@@ -265,7 +305,13 @@ export const ProvidersListCommand = effectCmd({
+ // so a wrong-account login is visible here instead of surfacing later as an
+ // unexplained model error. Only non-secret claims are decoded, and the account id
+ // is truncated; the token itself is never rendered.
+ const identity = result.type === "oauth" ? describeOAuthIdentity(result.access) : undefined
+ yield* Prompt.log.info(`${name} ${UI.Style.TEXT_DIM}${result.type}${identity ? ` (${identity})` : ""}`)
+ // altimate_change end
</file context>
| * Matching on the stable phrase rather than the whole sentence keeps this | ||
| * working as the quoted model id changes. | ||
| */ | ||
| const CODEX_PLAN_MISMATCH_PATTERN = /not supported when using Codex with a ChatGPT account/i |
There was a problem hiding this comment.
P2: This phrase-only matcher rewrites model-specific 400s for Plus/Pro or unknown-plan credentials as entitlement failures. Gate the rewrite on a known non-entitled plan, and preserve the original model error for entitled or unknown plans.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/opencode/src/auth/oauth-claims.ts, line 106:
<comment>This phrase-only matcher rewrites model-specific 400s for Plus/Pro or unknown-plan credentials as entitlement failures. Gate the rewrite on a known non-entitled plan, and preserve the original model error for entitled or unknown plans.</comment>
<file context>
@@ -0,0 +1,150 @@
+ * Matching on the stable phrase rather than the whole sentence keeps this
+ * working as the quoted model id changes.
+ */
+const CODEX_PLAN_MISMATCH_PATTERN = /not supported when using Codex with a ChatGPT account/i
+
+export function isCodexPlanMismatchBody(body: string | undefined): boolean {
</file context>
| .catch(() => "") | ||
| const enriched = enrichCodexPlanMismatchBody(body, currentAuth.access) | ||
| if (enriched) { | ||
| const identity = extractOAuthIdentity(currentAuth.access) |
There was a problem hiding this comment.
P3: On every matching Codex 400, this decodes and parses currentAuth.access a second time after enrichCodexPlanMismatchBody already decoded it. Return the identity alongside the enriched message and reuse it for logging.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/opencode/src/plugin/codex.ts, line 553:
<comment>On every matching Codex 400, this decodes and parses `currentAuth.access` a second time after `enrichCodexPlanMismatchBody` already decoded it. Return the identity alongside the enriched message and reuse it for logging.</comment>
<file context>
@@ -532,10 +532,39 @@ export async function CodexAuthPlugin(input: PluginInput): Promise<Hooks> {
+ .catch(() => "")
+ const enriched = enrichCodexPlanMismatchBody(body, currentAuth.access)
+ if (enriched) {
+ const identity = extractOAuthIdentity(currentAuth.access)
+ log.warn("codex request rejected for this account's plan", {
+ plan: identity.plan,
</file context>
Three review findings on the claim-based diagnostics: - Claims come from a token we deliberately do not verify and are rendered straight into a terminal, so sanitize them at the extraction boundary: printable ASCII only (no escape or control sequences) and capped at 64 characters. A claim that sanitizes to nothing is dropped. - An entitled account (plus/pro/team/business/enterprise) can hit the same 400 for a model that simply is not offered to ChatGPT-subscription accounts. Telling that user their plan is too low contradicts what they are paying for, so the message now branches: unentitled plans get the switch-accounts instructions, entitled ones are told the model — not the plan — is the problem, with a pointer to `auth list` in case the stored account is not the intended one. - Clarify in `resolveLoginTarget`'s doc comment that a URL positional always wins (it is the only route to the well-known path) and `--provider` beats the positional only when the positional is not a URL. Behavior unchanged; the previous wording read as if the flag always won. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VqnuBDGkh1ZT65Ti7e6DHZ
|
👋 This PR was automatically closed by our quality checks. Common reasons:
If you believe this was a mistake, please open an issue explaining your intended contribution and a maintainer will help you. |
2 similar comments
|
👋 This PR was automatically closed by our quality checks. Common reasons:
If you believe this was a mistake, please open an issue explaining your intended contribution and a maintainer will help you. |
|
👋 This PR was automatically closed by our quality checks. Common reasons:
If you believe this was a mistake, please open an issue explaining your intended contribution and a maintainer will help you. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 7d53631b73
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| * diagnostics only, never for an authorization decision. Returns undefined for | ||
| * anything that is not a three-segment token with a JSON object payload. | ||
| */ | ||
| export function decodeJwtClaims(token: string): Record<string, unknown> | undefined { |
There was a problem hiding this comment.
Add the required OAuthClaims self-reexport
This new module exposes only direct named exports and omits the package-required self-reexport, so its new consumers import individual helpers instead of the namespace projection mandated for modules in this package. Add the OAuthClaims self-reexport and consume the helpers through it.
AGENTS.md reference: packages/opencode/AGENTS.md:L17-L20
Useful? React with 👍 / 👎.
| return new Response(JSON.stringify({ detail: enriched }), { | ||
| status: response.status, | ||
| statusText: response.statusText, | ||
| headers: { "content-type": "application/json" }, | ||
| }) |
There was a problem hiding this comment.
Preserve upstream headers on enriched errors
When the matched 400 includes diagnostic headers such as a request identifier, constructing the replacement with only content-type discards every upstream header before the AI SDK can propagate them into the resulting API error. This makes these failures harder to correlate with provider logs; preserve the original response headers while replacing the body, adjusting entity-specific headers such as content-length as needed.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
1 issue found across 3 files (changes from recent commits).
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="packages/opencode/src/auth/oauth-claims.ts">
<violation number="1" location="packages/opencode/src/auth/oauth-claims.ts:143">
P3: The new entitled-plan branch of `codexPlanMismatchMessage` tells a user whose account may be the wrong one to check `altimate-code auth list`, but then omits the switch-account commands that the sibling unentitled branch in the same function provides (`altimate-code auth logout openai` / `altimate-code auth login openai`). If the diagnosis is "that account is not the one you meant to use", the user is left with a pointer to the list but no way to switch, which contradicts the PR's stated goal of showing switch-account commands. Add the same logout/login commands here so the guidance is actionable.</violation>
</file>
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
| `${who} on the \`${identity.plan}\` plan, which does include Codex — so this is the model, ` + | ||
| `not the plan. This model is not offered to ChatGPT-subscription accounts; pick a different ` + | ||
| `model, or use an OpenAI API key.\n` + | ||
| `If that account is not the one you meant to use, \`altimate-code auth list\` shows which is stored.` + |
There was a problem hiding this comment.
P3: The new entitled-plan branch of codexPlanMismatchMessage tells a user whose account may be the wrong one to check altimate-code auth list, but then omits the switch-account commands that the sibling unentitled branch in the same function provides (altimate-code auth logout openai / altimate-code auth login openai). If the diagnosis is "that account is not the one you meant to use", the user is left with a pointer to the list but no way to switch, which contradicts the PR's stated goal of showing switch-account commands. Add the same logout/login commands here so the guidance is actionable.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/opencode/src/auth/oauth-claims.ts, line 143:
<comment>The new entitled-plan branch of `codexPlanMismatchMessage` tells a user whose account may be the wrong one to check `altimate-code auth list`, but then omits the switch-account commands that the sibling unentitled branch in the same function provides (`altimate-code auth logout openai` / `altimate-code auth login openai`). If the diagnosis is "that account is not the one you meant to use", the user is left with a pointer to the list but no way to switch, which contradicts the PR's stated goal of showing switch-account commands. Add the same logout/login commands here so the guidance is actionable.</comment>
<file context>
@@ -109,13 +120,31 @@ export function isCodexPlanMismatchBody(body: string | undefined): boolean {
+ `${who} on the \`${identity.plan}\` plan, which does include Codex — so this is the model, ` +
+ `not the plan. This model is not offered to ChatGPT-subscription accounts; pick a different ` +
+ `model, or use an OpenAI API key.\n` +
+ `If that account is not the one you meant to use, \`altimate-code auth list\` shows which is stored.` +
+ provider
+ )
</file context>
Issue for this PR
Closes #1180
Type of change
What does this PR do?
Fixes two auth-flow defects a user hit tonight.
Defect 1 —
auth login <provider>misreads the provider name as a URL.The login positional was declared
[url](auth login [url]in--help) and its value went straight intofetch(\${url}/.well-known/opencode`). Typing the provider name is the natural thing to do —auth listprints provider ids andauth logout [provider]` accepts one — so the user gets a low-level fetch error that names no alternative.The positional is now
[target]and routes on shape rather than position: anhttp:/https:URL takes the well-known path exactly as before; anything else resolves as a provider id through the same code path--providerand the interactive picker already use, soauth login openailands on OpenAI's login methods. The decision is one exported function,resolveLoginTarget(), used by both the handler and the command'sinstancepredicate — those two previously each testedargs.urlseparately, and a URL must skip instance bootstrap (it may load remote config with the stale token) while a provider id must not, so having one function makes it impossible for them to disagree. An unmatched id now says what to do next instead of only rejecting:Defect 2 — a wrong-account / insufficient-plan login is undiagnosable.
When the stored ChatGPT OAuth credential belongs to a free-plan account, every Codex model request fails with:
The message blames the model; the actual problem is which account was signed in with, and nothing in the CLI said so. That cost an engineer an evening.
Two changes:
auth listannotates OAuth credentials the same way, e.g.OpenAI oauth (pro plan, account 4f3a1b2c…), so a wrong account is visible before it costs a debugging session.Both read
packages/opencode/src/auth/oauth-claims.ts, which decodes the JWT payload of the stored access token without verifying it — these claims drive human-facing diagnostics only, never an authorization decision. Onlychatgpt_plan_typeandchatgpt_account_idare read (each of which appears either top-level or inside thehttps://api.openai.com/authclaim bag, depending on which flow minted the token). The module never returns, logs, or embeds token or refresh-token material, and the account id is truncated to eight characters everywhere it is shown. Because the token is not verified and the values land in a terminal, claims are sanitized at the extraction boundary — printable ASCII only, capped at 64 characters — so a hostile claim cannot inject escape sequences or wallpaper the output.parseJwtClaimsin the Codex plugin now delegates to the same decoder so there is a single implementation.The message is plan-aware. An account on a plan that does include Codex (plus/pro/team/business/enterprise) can hit the same 400 for a model that simply is not offered to ChatGPT-subscription accounts; telling that user their plan is too low would contradict what they are paying for, so that case says the model, not the plan, is the problem, and points at
auth listin case the stored account is not the intended one.Not changed: the model allowlist (
OAUTH_ALLOWED_MODELSis untouched, andtest/plugin/codex-allowlist.test.tsstill passes unmodified), and theoriginator/User-Agentheaders we send.How did you verify your code works?
Automated (no network in any of it —
globalThis.fetchis stubbed and the tokens are synthetic JWT-shaped strings withalg: none):packages/opencode/test/auth/oauth-claims.test.ts(23 tests) — claim extraction from both locations, sanitization of control characters and over-long claims, masking, theauth listannotation, matching/not-matching 400 bodies, both branches of the enriched message (unentitled and entitled plans), and assertions that no rendered string contains any segment of the token or the full account id.packages/opencode/test/plugin/codex-plan-mismatch.test.ts(6 tests) — drives the real plugin loader's fetch wrapper: a free-plan 400 is rewritten with plan + masked account + the switch-account commands; a token with no readable claims still produces actionable text; other 400s and 200s pass through unchanged; requests still rewrite to the Codex endpoint.packages/opencode/test/cli/auth-login-target.test.ts(11 tests) — provider ids route to the provider path, real URLs (including trailing-slash stripping) still take the well-known path, no argument still opens the picker,--providerstill works and beats the positional, non-http schemes are rejected.test/plugin+test/cli→ 897 pass / 24 skip / 0 fail (includes the updated help snapshot and the unmodifiedcodex-allowlist.test.ts);test/auth→ all pass. One flake seen on an earlytest/pluginrun —loader-shared.test.ts"loads a file:// plugin function export" timed out at 5s — which passes on a clean checkout ofmainand on every subsequent run here; unrelated to this change.Manual, against the real CLI:
auth --helpnow showsauth login [target].auth login openai --method "Manually enter API Key"reaches OpenAI's API-key prompt instead of the fetch error (aborted at the prompt; nothing written).auth login https://auth.invalid-host-for-test.examplestill takes the well-known path (fails with the connection error, proving URL handling is intact).auth login definitely-not-a-providerprints the actionable message above.auth liston a real credential rendersOpenAI oauth (pro plan, account …).Gates:
bun run typecheckclean (13/13 tasks);bun run script/upstream/analyze.ts --markers --base main --strictclean, with every edit to an upstream-shared file wrapped inaltimate_changemarkers.Not verified: the enriched error cannot be exercised end-to-end without a real free-plan ChatGPT credential, which I do not have. The rewrite path is covered by unit tests that feed the exact 400 body through the real fetch wrapper, and the claim names were confirmed against a real (Pro) credential via
auth list, but nobody has watched a live free-plan account produce this message. The claim locations for a free-plan token are inferred from the same token layout, not observed.Reviewed with a second model. Its findings that fall inside this change were fixed in the second commit (claim sanitization, the contradictory message for entitled plans, an ambiguous doc comment). Two findings it raised are pre-existing and out of scope, and are left as-is rather than folded into a bug-fix PR:
refreshAccessTokenincludes up to 200 characters of the OAuth token-endpoint error body in its message, and the well-known login path accepts plainhttp://URLs and prints the URL verbatim.Screenshots / recordings
Terminal output is quoted inline above.
Checklist