Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/opencode/src/altimate/plugin/altimate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ export async function buildCliContext(machineIdPath?: string): Promise<string> {
// Instance.provide() (AsyncLocalStorage-propagated across awaits), so
// Config.get() resolves during an ordinary browser authorize(). The known
// exception is `altimate auth login <url>`, which deliberately skips
// instance bootstrap (ProvidersLoginCommand `instance: (args) => !args.url`);
// instance bootstrap (ProvidersLoginCommand `instance: (args) => !isAuthProviderUrl(args.target)`);
// on that path this fires. Fail CLOSED — omit the durable machine_id rather
// than transmit it for a user who may have opted out via config; a missed
// correlation beats leaking a stable identifier. Log so a low correlation
Expand Down
179 changes: 179 additions & 0 deletions packages/opencode/src/auth/oauth-claims.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
/**
* Non-secret identity claims carried by OAuth access tokens, plus the
* diagnostics built on top of them.
*
* Why this exists: a ChatGPT OAuth credential that belongs to a free-plan
* account fails every Codex model request with a 400 whose body only says the
* *model* is unsupported — it never says the real problem is WHICH account you
* signed in with. These helpers pull the two non-secret claims that answer
* that question (plan type + account id) so the CLI can say it out loud.
*
* SECURITY: nothing here may return, log, or embed the token, the refresh
* token, or any other credential material. Only `plan` and a TRUNCATED account
* id ever leave this module. Callers render the returned strings directly to
* the terminal, so treat every return value as user-visible output.
*/

/** Account ids are opaque identifiers, not secrets, but there is no reason to
* print one in full — the first segment is enough to tell two accounts apart. */
const ACCOUNT_ID_VISIBLE_CHARS = 8

/** The OpenAI-namespaced claim bag inside ChatGPT id/access tokens. */
const OPENAI_AUTH_CLAIM = "https://api.openai.com/auth"

export interface OAuthIdentity {
/** ChatGPT plan type claim, e.g. "free", "plus", "pro". */
plan?: string
/** Raw account id — mask with `maskAccountId` before displaying. */
accountId?: string
}

/**
* Decode the payload of a JWT-shaped token WITHOUT verifying its signature.
*
* Verification is deliberately skipped: these claims are used for human-facing
* 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 {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 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 👍 / 👎.

const parts = token.split(".")
if (parts.length !== 3) return undefined
try {
const decoded = JSON.parse(Buffer.from(parts[1], "base64url").toString())
if (!decoded || typeof decoded !== "object" || Array.isArray(decoded)) return undefined
return decoded as Record<string, unknown>
} catch {
return undefined
}
}

/** Claim values are rendered straight into a terminal and into error text, and
* the token is never signature-verified, so treat every claim as untrusted
* input: keep printable ASCII only (no control or escape sequences) and cap the
* length so a hostile claim cannot wallpaper the output. */
const MAX_CLAIM_LENGTH = 64

function sanitizeClaim(value: string): string | undefined {
const cleaned = value.replace(/[^\x20-\x7e]/g, "").slice(0, MAX_CLAIM_LENGTH)
return cleaned.length > 0 ? cleaned : undefined
}

function stringClaim(source: Record<string, unknown> | undefined, key: string): string | undefined {
const value = source?.[key]
return typeof value === "string" && value.length > 0 ? sanitizeClaim(value) : undefined
}

/**
* Pull the plan type and account id out of an OAuth access token. Both claims
* appear either at the top level or inside the OpenAI-namespaced claim bag,
* depending on which flow minted the token, so check both.
*/
export function extractOAuthIdentity(accessToken: string | undefined): OAuthIdentity {
if (!accessToken) return {}
const claims = decodeJwtClaims(accessToken)
if (!claims) return {}
const namespaced = claims[OPENAI_AUTH_CLAIM]
const nested =
namespaced && typeof namespaced === "object" && !Array.isArray(namespaced)
? (namespaced as Record<string, unknown>)
: undefined
return {
plan: stringClaim(claims, "chatgpt_plan_type") ?? stringClaim(nested, "chatgpt_plan_type"),
accountId: stringClaim(claims, "chatgpt_account_id") ?? stringClaim(nested, "chatgpt_account_id"),
}
}

/** Truncate an account id for display. */
export function maskAccountId(accountId: string | undefined): string | undefined {
if (!accountId) return undefined
if (accountId.length <= ACCOUNT_ID_VISIBLE_CHARS) return accountId
return accountId.slice(0, ACCOUNT_ID_VISIBLE_CHARS) + "…"
}

/**
* One-line summary of an OAuth credential for `auth list`, e.g.
* `free plan, account 4f3a1b2c…`. Returns undefined when the token carries
* neither claim (nothing useful to show, so show nothing).
*/
export function describeOAuthIdentity(accessToken: string | undefined): string | undefined {
const { plan, accountId } = extractOAuthIdentity(accessToken)
const masked = maskAccountId(accountId)
const parts = [plan ? `${plan} plan` : undefined, masked ? `account ${masked}` : undefined].filter((x): x is string =>
Boolean(x),
)
return parts.length > 0 ? parts.join(", ") : undefined
}

/**
* The 400 body OpenAI returns when a Codex request is made with a ChatGPT
* credential whose plan has no Codex entitlement:
*
* {"detail":"The 'gpt-5.6' model is not supported when using Codex with a
* ChatGPT account."}
*
* 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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>


export function isCodexPlanMismatchBody(body: string | undefined): boolean {
return Boolean(body) && CODEX_PLAN_MISMATCH_PATTERN.test(body!)
Comment on lines +117 to +120

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

}

/** Plans that do carry Codex access. On one of these the 400 is NOT an
* entitlement problem, so the message must not claim the plan is too low —
* it would contradict what the user is paying for. */
const CODEX_ENTITLED_PLANS = new Set(["plus", "pro", "team", "business", "enterprise"])

/**
* Build the actionable replacement for that 400. Names the account and plan the
* request actually used; on an unentitled plan it says to switch accounts, and
* on an entitled one it says the model, not the plan, is the problem.
*/
export function codexPlanMismatchMessage(identity: OAuthIdentity, originalDetail?: string): string {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.)

View Feedback

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>

const masked = maskAccountId(identity.accountId)
const who = masked ? `Signed in as ChatGPT account ${masked}` : "Signed in with a ChatGPT account"
const provider = originalDetail ? `\nProvider said: ${originalDetail}` : ""

if (identity.plan && CODEX_ENTITLED_PLANS.has(identity.plan.toLowerCase())) {
return (
`${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.` +

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>

provider
)
}

const plan = identity.plan
? ` on the \`${identity.plan}\` plan`
: " whose plan could not be read from the stored credential"
return (
`${who}${plan}. Codex models require a ChatGPT Plus or Pro plan, ` +
`so this account cannot run them.\n` +
`If you have a Plus/Pro account, switch to it:\n` +
` altimate-code auth logout openai\n` +
` altimate-code auth login openai\n` +
`Otherwise use an OpenAI API key instead of the ChatGPT subscription, ` +
`or pick a model from another provider.` +
provider
)
}

/**
* Given a raw 400 response body and the access token the request used, return
* an enriched `detail` string — or undefined when the body is not this failure
* (leave every other error untouched).
*/
export function enrichCodexPlanMismatchBody(body: string | undefined, accessToken: string | undefined) {
if (!isCodexPlanMismatchBody(body)) return undefined
let detail: string | undefined
try {
const parsed = JSON.parse(body!)
if (parsed && typeof parsed.detail === "string") detail = parsed.detail
} catch {
// Body was not JSON — the phrase still matched, so still enrich, just
// without quoting a `detail` field we could not parse out.
}
return codexPlanMismatchMessage(extractOAuthIdentity(accessToken), detail)
}
83 changes: 71 additions & 12 deletions packages/opencode/src/cli/cmd/providers.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import type { Argv } from "yargs"
import { Auth } from "../../auth"
// altimate_change start — surface OAuth plan/account in `auth list`
import { describeOAuthIdentity } from "../../auth/oauth-claims"
// altimate_change end
import { cmd } from "./cmd"
import { CliError, effectCmd, fail } from "../effect-cmd"
import { UI } from "../ui"
Expand Down Expand Up @@ -209,6 +212,45 @@ const handlePluginAuth = Effect.fn("Cli.providers.pluginAuth")(function* (
return false
})

// altimate_change start — `auth login <provider>` used to be fed straight into a
// fetch() for well-known auth metadata, so the natural `auth login openai` died on
// "fetch() URL is invalid". Only an actual http(s) URL takes the well-known path;
// anything else is treated as a provider id, exactly like `--provider`.
export function isAuthProviderUrl(value: string | undefined): value is string {
if (!value) return false
try {
const { protocol } = new URL(value)
return protocol === "http:" || protocol === "https:"
} catch {
return false
}
}

export type LoginTarget =
/** Self-hosted auth provider: fetch its `.well-known/opencode` metadata. */
| { kind: "url"; url: string }
/** A provider id/name — same path the interactive picker resolves to. */
| { kind: "provider"; provider: string }
/** Nothing specified: prompt with the provider list. */
| { kind: "picker" }

/**
* 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(). A URL positional always wins — it is the only way to
* reach the well-known path, and it is what pre-existing invocations pass. When
* the positional is not a URL, an explicit `--provider` flag takes precedence
* over it, following the usual flag-beats-positional convention.
*/
export function resolveLoginTarget(args: { target?: string; provider?: string }): LoginTarget {
if (isAuthProviderUrl(args.target)) return { kind: "url", url: args.target.replace(/\/+$/, "") }
const provider = args.provider ?? args.target
if (provider) return { kind: "provider", provider }
return { kind: "picker" }
}
// altimate_change end

export function resolvePluginProviders(input: {
hooks: Hooks[]
existingProviders: Record<string, unknown>
Expand Down Expand Up @@ -265,7 +307,13 @@ export const ProvidersListCommand = effectCmd({

for (const [providerID, result] of results) {
const name = database[providerID]?.name || providerID
yield* Prompt.log.info(`${name} ${UI.Style.TEXT_DIM}${result.type}`)
// altimate_change start — show which account/plan an OAuth credential belongs to,
// 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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>

yield* Prompt.log.info(`${name} ${UI.Style.TEXT_DIM}${result.type}${identity ? ` (${identity})` : ""}`)
Comment on lines +314 to +315

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
Suggested change
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, "")})` : ""}`)

// altimate_change end
}

yield* Prompt.outro(`${results.length} credentials`)
Expand Down Expand Up @@ -297,18 +345,19 @@ export const ProvidersListCommand = effectCmd({
})

export const ProvidersLoginCommand = effectCmd({
command: "login [url]",
// altimate_change start — the positional accepts a provider id as well as a URL
command: "login [target]",
describe: "log in to a provider",
// URL login skips instance bootstrap, which would load remote config with the stale token and crash before re-auth.
instance: (args) => !args.url,
// A provider id goes down the picker path instead, which needs the instance.
instance: (args) => resolveLoginTarget(args).kind !== "url",
builder: (yargs: Argv) =>
yargs
.positional("url", {
// altimate_change start — branding
describe: "altimate auth provider",
// altimate_change end
.positional("target", {
describe: "provider id (e.g. openai) or altimate auth provider URL",
type: "string",
})
// altimate_change end
.option("provider", {
alias: ["p"],
describe: "provider id or name to log in to (skips provider selection)",
Expand All @@ -324,8 +373,11 @@ export const ProvidersLoginCommand = effectCmd({

UI.empty()
yield* Prompt.intro("Add credential")
if (args.url) {
const url = args.url.replace(/\/+$/, "")
// altimate_change start — only real http(s) URLs take the well-known path
const target = resolveLoginTarget(args)
if (target.kind === "url") {
const url = target.url
// altimate_change end
const wellknown = (yield* cliTry(`Failed to load auth provider metadata from ${url}: `, () =>
fetch(`${url}/.well-known/opencode`).then((x) => x.json()),
)) as {
Expand Down Expand Up @@ -417,13 +469,20 @@ export const ProvidersLoginCommand = effectCmd({
]

let provider: string
if (args.provider) {
const input = args.provider
// altimate_change start — a non-URL positional is a provider id, same as --provider
if (target.kind === "provider") {
const input = target.provider
// altimate_change end
const byID = options.find((x) => x.value === input)
const byName = options.find((x) => x.label.toLowerCase() === input.toLowerCase())
const match = byID ?? byName
if (!match) {
return yield* fail(`Unknown provider "${input}"`)
// altimate_change start — say what to do next instead of just rejecting
return yield* fail(
`Unknown provider "${input}". Run \`altimate-code auth login\` with no arguments to pick from the list, ` +
`or pass a full auth provider URL including https://.`,
)
// altimate_change end
}
provider = match.value
} else {
Expand Down
45 changes: 37 additions & 8 deletions packages/opencode/src/plugin/codex.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ import type { Hooks, PluginInput } from "@opencode-ai/plugin"
import { Log } from "../util/log"
import { Installation } from "../installation"
import { Auth, OAUTH_DUMMY_KEY } from "../auth"
// altimate_change start — plan/account diagnostics for wrong-account logins
import { decodeJwtClaims, enrichCodexPlanMismatchBody, extractOAuthIdentity, maskAccountId } from "../auth/oauth-claims"
// altimate_change end
import os from "os"
import { ProviderTransform } from "@/provider/transform"
import { ModelID, ProviderID } from "@/provider/schema"
Expand Down Expand Up @@ -88,13 +91,10 @@ export interface IdTokenClaims {
}

export function parseJwtClaims(token: string): IdTokenClaims | undefined {
const parts = token.split(".")
if (parts.length !== 3) return undefined
try {
return JSON.parse(Buffer.from(parts[1], "base64url").toString())
} catch {
return undefined
}
// altimate_change start — one JWT decoder, shared with auth/oauth-claims which
// reads the same tokens for the plan/account diagnostics
return decodeJwtClaims(token) as IdTokenClaims | undefined
// altimate_change end
}

export function extractAccountIdFromClaims(claims: IdTokenClaims): string | undefined {
Expand Down Expand Up @@ -532,10 +532,39 @@ export async function CodexAuthPlugin(input: PluginInput): Promise<Hooks> {
? new URL(CODEX_API_ENDPOINT)
: parsed

return fetch(url, {
const response = await fetch(url, {
...init,
headers,
})

// altimate_change start — a wrong-account login is otherwise undiagnosable.
// A free-plan ChatGPT credential fails every Codex request with a 400 that
// blames the MODEL ("...is not supported when using Codex with a ChatGPT
// account"), never the account. Replace that body with one naming the plan
// and account the request actually used, plus the commands to switch.
// Only this one 400 shape is touched; every other response passes through.
if (response.status === 400) {
const body = await response
.clone()
.text()
.catch(() => "")
const enriched = enrichCodexPlanMismatchBody(body, currentAuth.access)
if (enriched) {
const identity = extractOAuthIdentity(currentAuth.access)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>

log.warn("codex request rejected for this account's plan", {
plan: identity.plan,
account: maskAccountId(identity.accountId),
})
return new Response(JSON.stringify({ detail: enriched }), {
status: response.status,
statusText: response.statusText,
headers: { "content-type": "application/json" },
})
Comment on lines +558 to +562

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

}
}
// altimate_change end

return response
},
}
},
Expand Down
Loading
Loading