Skip to content

Commit ab0eb09

Browse files
committed
fix(inference): treat Codex 404s as expired credentials
1 parent 8ce9dcf commit ab0eb09

2 files changed

Lines changed: 65 additions & 5 deletions

File tree

‎src/inference-error-message.ts‎

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -146,14 +146,21 @@ export function terminalProviderFailureMessage(
146146
? diagnostic
147147
: `${diagnostic}.`;
148148
const guidance = terminalProviderFailureGuidance(error, category);
149-
return `${label} Provider failed (${category}): ${diagnosticSentence} ${guidance}`;
149+
const tail = guidance.length > 0 ? ` ${guidance}` : "";
150+
return `${label} Provider failed (${category}): ${diagnosticSentence}${tail}`;
150151
}
151152

152153
function terminalProviderFailureGuidance(
153154
error: InferenceErrorLike,
154155
category: string,
155156
): string {
156-
if (category === "credential_failure") return CREDENTIAL_FAILURE_USER_MESSAGE;
157+
if (category === "credential_failure") {
158+
// Normalized credential failures already carry the re-login hint in the
159+
// diagnostic (e.g. Codex profile copy); repeating it reads as a stutter.
160+
return /log in again|sign in again/i.test(error.message ?? "")
161+
? ""
162+
: CREDENTIAL_FAILURE_USER_MESSAGE;
163+
}
157164
if (category === "context_overflow") return "Try /clear to start fresh.";
158165
// A 429 that survived the harness's paced retries is a wait-it-out rate
159166
// limit, not a generic flake: say so instead of the bare "Try again."
@@ -180,7 +187,9 @@ function terminalProviderFailureSummary(
180187
): string {
181188
const label = terminalProviderFailureLabel(providerId, displayLabel);
182189
const category = terminalProviderFailureCategory(error);
183-
return `${label} Provider failed (${category}). ${terminalProviderFailureGuidance(error, category)}`;
190+
const guidance = terminalProviderFailureGuidance(error, category);
191+
const tail = guidance.length > 0 ? ` ${guidance}` : "";
192+
return `${label} Provider failed (${category}).${tail}`;
184193
}
185194

186195
export type ResolvedProviderFailureError = Error & {

‎src/inference-gateway-error.ts‎

Lines changed: 53 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -519,13 +519,61 @@ function normalizeCodexUsageLimitError(
519519
};
520520
}
521521

522+
/**
523+
* Body/message markers that mean a Codex 404 names an unknown model rather
524+
* than rejecting the credential. The regex below covers the same phrasing
525+
* when a model name sits between "model" and "does not exist".
526+
*/
527+
const CODEX_UNKNOWN_MODEL_MARKERS = [
528+
"model_not_found",
529+
"unknown model",
530+
] as const;
531+
532+
function hasCodexUnknownModelMarker(error: InferenceErrorLike): boolean {
533+
const parts = [error.message ?? "", stringFromRaw(error.raw)];
534+
if (combinedTextIncludesMarker(parts, CODEX_UNKNOWN_MODEL_MARKERS))
535+
return true;
536+
return parts.some((part) =>
537+
/model\b[^.!?]{0,60}\b(?:does not exist|not found)\b/i.test(part),
538+
);
539+
}
540+
541+
/**
542+
* Codex answers unauthenticated requests with 426/404, so a fatal 404 in a
543+
* known-Codex context is an expired, invalid, or missing credential — not a
544+
* bad model name. The re-login copy matches the CodexAuthError shape so the
545+
* TUI names the affected profile through its existing auth matchers. Only an
546+
* explicit unknown-model marker keeps the fatal switch-models path.
547+
*/
548+
function normalizeCodexCredential404Error(
549+
error: InferenceErrorWithGoContext,
550+
): InferenceError {
551+
if (error.category !== "fatal") return error;
552+
if (error.statusCode !== 404) return error;
553+
const providerId = error.providerId;
554+
if (providerId === undefined || !isCodexProviderName(providerId))
555+
return error;
556+
if (hasCodexUnknownModelMarker(error)) return error;
557+
const profile = codexProfileFromProviderName(providerId) ?? providerId;
558+
return {
559+
category: "credential_failure",
560+
message: `Codex profile "${profile}" is not authorized. Log in again.`,
561+
statusCode: 404,
562+
...(error.raw !== undefined ? { raw: error.raw } : {}),
563+
...(error.retryAfterMs !== undefined
564+
? { retryAfterMs: error.retryAfterMs }
565+
: {}),
566+
};
567+
}
568+
522569
/**
523570
* Reclassify gateway overload errors so the default retry policy treats them as
524571
* transient instead of aborting on protocol_mismatch. Also normalizes OpenCode
525572
* Go quota/rate-limit shapes (including HTTP 400 mis-status), known-xAI short
526573
* 429s, attributable xAI capacity protocol_mismatch, Codex usage limits
527-
* (nested detail.error with resets_in_seconds), and known-Codex short 429s that
528-
* are not usage_limit_reached.
574+
* (nested detail.error with resets_in_seconds), known-Codex short 429s that
575+
* are not usage_limit_reached, and known-Codex credential 404s that do not
576+
* name an unknown model.
529577
*/
530578
export function normalizeInferenceErrorForRetry(
531579
error: InferenceErrorWithGoContext,
@@ -545,6 +593,9 @@ export function normalizeInferenceErrorForRetry(
545593
const codexRateLimit = normalizeCodexRateLimitError(error);
546594
if (codexRateLimit !== error) return codexRateLimit;
547595

596+
const codexCredential = normalizeCodexCredential404Error(error);
597+
if (codexCredential !== error) return codexCredential;
598+
548599
if (!isGatewayOverloadInferenceError(error)) return error;
549600
if (error.category === "retryable" || error.category === "timeout")
550601
return error;

0 commit comments

Comments
 (0)