From 3a7ea4077d4028d088495767059c67f0a2a743c2 Mon Sep 17 00:00:00 2001 From: chilung Date: Thu, 10 Sep 2026 08:22:51 +0000 Subject: [PATCH 1/2] fix(codex): retry warmup with gpt-5.6-luna on 400 and 404 for ChatGPT Free accounts (#4126) --- .../docs/fr/guides/codex-integration.md | 2 +- .../content/docs/guides/codex-integration.md | 2 +- .../docs/ja/guides/codex-integration.md | 2 +- .../docs/ko/guides/codex-integration.md | 2 +- .../docs/ru/guides/codex-integration.md | 2 +- .../docs/tr/guides/codex-integration.md | 2 +- .../docs/zh-cn/guides/codex-integration.md | 2 +- .../docs/zh-tw/guides/codex-integration.md | 2 +- src/codex/auth-api.ts | 8 ++- src/codex/warmup.ts | 14 ++++-- tests/codex-integration/warmup.test.ts | 50 +++++++++++++++++++ 11 files changed, 74 insertions(+), 14 deletions(-) diff --git a/docs-site/src/content/docs/fr/guides/codex-integration.md b/docs-site/src/content/docs/fr/guides/codex-integration.md index 5964e77137..bd8157e747 100644 --- a/docs-site/src/content/docs/fr/guides/codex-integration.md +++ b/docs-site/src/content/docs/fr/guides/codex-integration.md @@ -375,7 +375,7 @@ délégation v1/base/v2 et de ses mécanismes de repli. ## Préchauffage des comptes Codex -L’ajout ou la réauthentification vérifie normalement le compte avant son enregistrement par une petite requête attendant `response.completed`. Le modèle par défaut est `gpt-5.4-mini`, avec un essai sur `gpt-5.5` en cas de HTTP 400. Les erreurs publiques contiennent des catégories fixes, sans corps de réponse brut. +L’ajout ou la réauthentification vérifie normalement le compte avant son enregistrement par une petite requête attendant `response.completed`. Le modèle par défaut est `gpt-5.4-mini`, avec un essai sur `gpt-5.5` et `gpt-5.6-luna` en cas de HTTP 400 ou HTTP 404. Les erreurs publiques contiennent des catégories fixes, sans corps de réponse brut. Si la lecture authentifiée des quotas avec le nouveau jeton OAuth confirme un quota de 5 heures, hebdomadaire ou mensuel épuisé, le compte est enregistré sans appel au modèle et affiche **Validation en attente**. Il reste exclu du routage après un redémarrage ou un renouvellement du jeton. Après récupération du quota, actualisez les quotas : une lecture récente et complète avec de la capacité disponible permet une petite requête de validation. Seule sa réussite active le compte. Tout échec conserve la restriction. Les lectures passives ne déclenchent pas cette requête. Un quota inconnu à l’inscription conserve la vérification habituelle. diff --git a/docs-site/src/content/docs/guides/codex-integration.md b/docs-site/src/content/docs/guides/codex-integration.md index 6552b14365..397ef277c6 100644 --- a/docs-site/src/content/docs/guides/codex-integration.md +++ b/docs-site/src/content/docs/guides/codex-integration.md @@ -706,7 +706,7 @@ Catalog sync makes the selected sub-agent models available to Codex; see [Codex ## Codex account warmup -When a ChatGPT account is added or reauthenticated, OpenCodex normally verifies it before saving with a small streaming request to the Codex Responses backend. It waits for `response.completed`, defaults to `gpt-5.4-mini`, and retries with `gpt-5.5` on HTTP 400. Public errors contain fixed failure categories rather than raw upstream response bodies. +When a ChatGPT account is added or reauthenticated, OpenCodex normally verifies it before saving with a small streaming request to the Codex Responses backend. It waits for `response.completed`, defaults to `gpt-5.4-mini`, and retries with `gpt-5.5` and `gpt-5.6-luna` on HTTP 400 or HTTP 404. Public errors contain fixed failure categories rather than raw upstream response bodies. If the new OAuth credential's authenticated usage lookup confirms an exhausted 5-hour, weekly, or monthly quota, the account is saved without this model request and shows **Validation pending**. It cannot serve pool requests, even after a restart or token refresh. Once quota recovers, **Refresh quotas** finishes validation: a fresh, complete usage reading with headroom permits one small model request, and only a completed response enables the account. Failed or incomplete readings and failed validation preserve the restriction. Passive account polling does not trigger deferred validation. Unknown usage during initial registration retains the normal warmup gate. diff --git a/docs-site/src/content/docs/ja/guides/codex-integration.md b/docs-site/src/content/docs/ja/guides/codex-integration.md index ca5d1628d7..58181c0aba 100644 --- a/docs-site/src/content/docs/ja/guides/codex-integration.md +++ b/docs-site/src/content/docs/ja/guides/codex-integration.md @@ -241,7 +241,7 @@ ocx service install # persistent: auto-starts on login and respawns on crash ## Codex アカウントのウォームアップ -アカウントの追加・再認証では通常、保存前に小さなモデルリクエストで `response.completed` を確認します。既定モデルは `gpt-5.4-mini` で、HTTP 400 の場合は `gpt-5.5` で再試行します。公開エラーには固定の分類のみを表示し、生の応答本文は公開しません。 +アカウントの追加・再認証では通常、保存前に小さなモデルリクエストで `response.completed` を確認します。既定モデルは `gpt-5.4-mini` で、HTTP 400 または HTTP 404 の場合は `gpt-5.5` および `gpt-5.6-luna` で再試行します。公開エラーには固定の分類のみを表示し、生の応答本文は公開しません。 新しい OAuth トークンによる使用量取得で5時間・週次・月次の上限到達が確認された場合、モデルを呼ばずに保存し、**検証待ち**と表示します。再起動やトークン更新後も使用できません。上限回復後に使用量を更新すると、十分な空き容量を示す完全な最新情報を確認してから小さなモデルリクエストを送り、完了した場合のみ使用可能になります。取得や検証の失敗では待機状態を維持します。通常の状態ポーリングは検証リクエストを送りません。初回登録時の使用量が不明な場合は通常の検証が必要です。 diff --git a/docs-site/src/content/docs/ko/guides/codex-integration.md b/docs-site/src/content/docs/ko/guides/codex-integration.md index c92f789999..391a3e63fc 100644 --- a/docs-site/src/content/docs/ko/guides/codex-integration.md +++ b/docs-site/src/content/docs/ko/guides/codex-integration.md @@ -252,7 +252,7 @@ catalog sync는 선택된 서브에이전트 모델을 Codex가 쓸 수 있게 ## Codex 계정 워밍업 -ChatGPT 계정을 추가하거나 재인증할 때 OpenCodex는 일반적으로 저장 전에 작은 모델 요청으로 확인합니다. `gpt-5.4-mini`의 `response.completed`를 기다리며 HTTP 400이면 `gpt-5.5`로 재시도합니다. 오류에는 고정된 실패 분류만 표시하고 원본 응답 본문은 노출하지 않습니다. +ChatGPT 계정을 추가하거나 재인증할 때 OpenCodex는 일반적으로 저장 전에 작은 모델 요청으로 확인합니다. `gpt-5.4-mini`의 `response.completed`를 기다리며 HTTP 400 또는 HTTP 404이면 `gpt-5.5`와 `gpt-5.6-luna`로 재시도합니다. 오류에는 고정된 실패 분류만 표시하고 원본 응답 본문은 노출하지 않습니다. 새 OAuth 토큰으로 인증된 사용량 조회에서 5시간·주간·월간 한도 소진이 확인되면 모델 요청 없이 계정을 저장하고 **검증 대기**로 표시합니다. 재시작이나 토큰 갱신 후에도 요청에 사용되지 않습니다. 한도 회복 후 **사용량 새로고침**을 실행하면, 여유가 있는 완전한 최신 사용량을 확인한 뒤 작은 모델 요청을 보내고 완료 응답을 받아야 계정을 사용할 수 있습니다. 조회나 검증 실패 시 대기 상태를 유지합니다. 일반적인 화면 상태 조회는 이 모델 요청을 보내지 않습니다. 최초 등록 때 사용량이 불명확하면 기존 워밍업 검증이 필요합니다. diff --git a/docs-site/src/content/docs/ru/guides/codex-integration.md b/docs-site/src/content/docs/ru/guides/codex-integration.md index 120eb10b61..a22c347f2d 100644 --- a/docs-site/src/content/docs/ru/guides/codex-integration.md +++ b/docs-site/src/content/docs/ru/guides/codex-integration.md @@ -368,7 +368,7 @@ v1/base/v2 при делегировании и fallback — в ## Прогрев аккаунтов Codex -При добавлении или повторной аутентификации аккаунт обычно проверяется до сохранения небольшим запросом, ожидающим `response.completed`. По умолчанию используется `gpt-5.4-mini`, при HTTP 400 — повтор с `gpt-5.5`. Публичные ошибки содержат фиксированные категории без исходного тела ответа. +При добавлении или повторной аутентификации аккаунт обычно проверяется до сохранения небольшим запросом, ожидающим `response.completed`. По умолчанию используется `gpt-5.4-mini`, при HTTP 400 или HTTP 404 — повтор с `gpt-5.5` и `gpt-5.6-luna`. Публичные ошибки содержат фиксированные категории без исходного тела ответа. Если запрос квоты с новым OAuth-токеном подтверждает исчерпание пятичасовой, недельной или месячной квоты, аккаунт сохраняется без вызова модели со статусом **Ожидает проверки**. Перезапуск и обновление токена не включают маршрутизацию. После восстановления квоты обновите её: полные свежие данные с доступной ёмкостью разрешают небольшой проверочный запрос. Только успешное завершение включает аккаунт. Ошибки сохраняют ограничение. Пассивный опрос не отправляет такой запрос. Неизвестная квота при регистрации требует обычной проверки. diff --git a/docs-site/src/content/docs/tr/guides/codex-integration.md b/docs-site/src/content/docs/tr/guides/codex-integration.md index 61d2577e84..fcf4f8de66 100644 --- a/docs-site/src/content/docs/tr/guides/codex-integration.md +++ b/docs-site/src/content/docs/tr/guides/codex-integration.md @@ -425,7 +425,7 @@ Arayüzü](/tr/guides/sub-agent-surface/) sayfasına bakın. ## Codex hesap ısınması -Hesap ekleme veya yeniden kimlik doğrulama, normalde kaydetmeden önce `response.completed` bekleyen küçük bir model isteğiyle doğrulanır. Varsayılan model `gpt-5.4-mini` olup HTTP 400 durumunda `gpt-5.5` denenir. Genel hatalar ham yanıt gövdesi yerine sabit hata kategorilerini içerir. +Hesap ekleme veya yeniden kimlik doğrulama, normalde kaydetmeden önce `response.completed` bekleyen küçük bir model isteğiyle doğrulanır. Varsayılan model `gpt-5.4-mini` olup HTTP 400 veya HTTP 404 durumunda `gpt-5.5` ve `gpt-5.6-luna` denenir. Genel hatalar ham yanıt gövdesi yerine sabit hata kategorilerini içerir. Yeni OAuth belirteciyle yapılan kota sorgusu 5 saatlik, haftalık veya aylık kotanın tükendiğini doğrularsa hesap model çağrısı olmadan kaydedilir ve **Doğrulama bekleniyor** gösterilir. Yeniden başlatma veya belirteç yenileme yönlendirmeyi açmaz. Kota geri geldiğinde kotaları yenileyin: kullanılabilir kapasite gösteren eksiksiz güncel veri küçük bir doğrulama isteğine izin verir. Yalnızca tamamlanan yanıt hesabı etkinleştirir. Hatalarda kısıtlama korunur. Pasif sorgulama bu isteği göndermez. İlk kayıtta bilinmeyen kota normal doğrulamayı gerektirir. diff --git a/docs-site/src/content/docs/zh-cn/guides/codex-integration.md b/docs-site/src/content/docs/zh-cn/guides/codex-integration.md index f63ace3586..bfe76b0c72 100644 --- a/docs-site/src/content/docs/zh-cn/guides/codex-integration.md +++ b/docs-site/src/content/docs/zh-cn/guides/codex-integration.md @@ -312,7 +312,7 @@ fallback 行为,参见 [Sub-agent Surface](/guides/sub-agent-surface/)。 ## Codex 账号预热 -添加或重新认证账号时,通常会在保存前发送一个小型模型请求并等待 `response.completed`。默认使用 `gpt-5.4-mini`,HTTP 400 时改用 `gpt-5.5` 重试。公开错误仅包含固定分类,不包含原始响应正文。 +添加或重新认证账号时,通常会在保存前发送一个小型模型请求并等待 `response.completed`。默认使用 `gpt-5.4-mini`,HTTP 400 或 HTTP 404 时改用 `gpt-5.5` 与 `gpt-5.6-luna` 重试。公开错误仅包含固定分类,不包含原始响应正文。 如果新 OAuth 凭据的已认证用量查询确认5小时、每周或每月额度耗尽,则不调用模型而直接保存账号,显示**等待验证**。重启或刷新令牌也不会使其可用。额度恢复后刷新额度:只有完整的最新用量显示有余额,才会发送一个小型验证请求;请求完成后账号才可用于路由。查询或验证失败将保留等待状态。普通状态轮询不会发送该请求。初次注册时用量未知仍需常规预热验证。 diff --git a/docs-site/src/content/docs/zh-tw/guides/codex-integration.md b/docs-site/src/content/docs/zh-tw/guides/codex-integration.md index 999295cd39..708341e5bf 100644 --- a/docs-site/src/content/docs/zh-tw/guides/codex-integration.md +++ b/docs-site/src/content/docs/zh-tw/guides/codex-integration.md @@ -319,7 +319,7 @@ ocx service install # 常駐:登入時自動啟動,崩潰後自動重新 ## Codex 帳號預熱 -新增或重新驗證帳號時,通常會在儲存前傳送小型模型請求並等待 `response.completed`。預設使用 `gpt-5.4-mini`,HTTP 400 時改用 `gpt-5.5` 重試。公開錯誤僅包含固定分類,不包含原始回應本文。 +新增或重新驗證帳號時,通常會在儲存前傳送小型模型請求並等待 `response.completed`。預設使用 `gpt-5.4-mini`,HTTP 400 或 HTTP 404 時改用 `gpt-5.5` 與 `gpt-5.6-luna` 重試。公開錯誤僅包含固定分類,不包含原始回應本文。 若新 OAuth 憑證的已驗證用量查詢確認5小時、每週或每月額度耗盡,則不呼叫模型而直接儲存帳號,顯示**等待驗證**。重新啟動或更新權杖也不會使其可用。額度恢復後重新整理額度:只有完整的最新用量顯示有餘額,才會傳送小型驗證請求;請求完成後帳號才可用於路由。查詢或驗證失敗將保留等待狀態。一般狀態輪詢不會傳送該請求。首次註冊時用量未知仍需一般預熱驗證。 diff --git a/src/codex/auth-api.ts b/src/codex/auth-api.ts index 6994e2b2ac..e68e79b12c 100644 --- a/src/codex/auth-api.ts +++ b/src/codex/auth-api.ts @@ -116,7 +116,7 @@ import type { CodexQuotaRefreshOutcome } from "./quota-refresh-outcome"; import { getMainAccountHardLockStatus, type MainAccountHardLockStatus } from "./main-account-hard-lock"; import { observeMainReserveRevocation } from "./reserve-availability"; import { emailMaskingEnabled, projectEmail } from "../lib/privacy"; -import { codexWarmupFailureReason, warmCodexAccount } from "./warmup"; +import { CodexWarmupError, codexWarmupFailureReason, warmCodexAccount } from "./warmup"; export { maskEmail } from "../lib/privacy"; import type { CodexAccount, CodexAccountCredentials, OcxConfig } from "../types"; import type { CatalogDisposition } from "./convergence-types"; @@ -585,10 +585,14 @@ async function verifyCodexAccountWarmup( return { ok: true, validatedAt: Date.now() }; } catch (err) { const reason = codexWarmupFailureReason(err); + const isModelProvisioningError = err instanceof CodexWarmupError && (err.status === 400 || err.status === 404); + const error = isModelProvisioningError + ? "Codex account warmup failed. Verify account model access or provisioning and try again." + : "Codex account warmup failed. Reauthenticate the account and try again."; return { ok: false, response: jsonResponse({ - error: "Codex account warmup failed. Reauthenticate the account and try again.", + error, code: "codex_warmup_failed", reason, accountId, diff --git a/src/codex/warmup.ts b/src/codex/warmup.ts index 5af42490b4..eb1cb7fc15 100644 --- a/src/codex/warmup.ts +++ b/src/codex/warmup.ts @@ -28,7 +28,8 @@ export interface CodexWarmupOptions { const CODEX_RESPONSES_URL = "https://chatgpt.com/backend-api/codex/responses"; const DEFAULT_MODEL = "gpt-5.4-mini"; -const FALLBACK_MODELS = ["gpt-5.5"]; +const FALLBACK_MODELS = ["gpt-5.5", "gpt-5.6-luna"]; +const isRetryableWarmupStatus = (status?: number): boolean => status === 400 || status === 404; const DEFAULT_TIMEOUT_MS = 30_000; const MAX_TIMEOUT_MS = 0x7fff_ffff; const MAX_ERROR_BODY_BYTES = 2048; @@ -286,8 +287,8 @@ export async function warmCodexAccount(options: CodexWarmupOptions): Promise { expect(fetchMock).toHaveBeenCalledTimes(2); expect(parsedBodies.map(body => body.model)).toEqual(["gpt-5.4-mini", "gpt-5.5"]); }); + test("warmCodexAccount retries FALLBACK_MODELS on HTTP 404 and falls through to gpt-5.6-luna", async () => { + const parsedBodies: Record[] = []; + const fetchMock = mock(async (_input: RequestInfo | URL, init?: RequestInit) => { + const body = JSON.parse(String(init?.body)) as Record; + parsedBodies.push(body); + + if (body.model === "gpt-5.4-mini") { + return new Response(JSON.stringify({ detail: "model not found" }), { status: 404 }); + } + if (body.model === "gpt-5.5") { + return new Response(JSON.stringify({ detail: "model not supported for free tier" }), { status: 400 }); + } + if (body.model === "gpt-5.6-luna") { + return sseResponse(); + } + return new Response("unexpected model", { status: 500 }); + }); + const fetchSpy = spyOn(globalThis, "fetch").mockImplementation(fetchMock as unknown as typeof fetch); + + try { + await warmCodexAccount({ accessToken: "access-test", chatgptAccountId: "acct-test" }); + } finally { + fetchSpy.mockRestore(); + } + + expect(fetchMock).toHaveBeenCalledTimes(3); + expect(parsedBodies.map(body => body.model)).toEqual(["gpt-5.4-mini", "gpt-5.5", "gpt-5.6-luna"]); + }); + + test("warmCodexAccount does not retry on 401 and immediately fails", async () => { + const parsedBodies: Record[] = []; + const fetchMock = mock(async (_input: RequestInfo | URL, init?: RequestInit) => { + const body = JSON.parse(String(init?.body)) as Record; + parsedBodies.push(body); + return new Response(JSON.stringify({ detail: "unauthorized" }), { status: 401 }); + }); + const fetchSpy = spyOn(globalThis, "fetch").mockImplementation(fetchMock as unknown as typeof fetch); + + try { + await warmCodexAccount({ accessToken: "access-test", chatgptAccountId: "acct-test" }); + expect.unreachable("expected warmCodexAccount to throw"); + } catch (err) { + expect(err).toBeInstanceOf(CodexWarmupError); + expect((err as CodexWarmupError).status).toBe(401); + } finally { + fetchSpy.mockRestore(); + } + + expect(fetchMock).toHaveBeenCalledTimes(1); + }); }); From 9a06b6df0c181197a689b6dd2439eeafdd621472 Mon Sep 17 00:00:00 2001 From: chilung Date: Thu, 10 Sep 2026 08:25:37 +0000 Subject: [PATCH 2/2] fix(codex): revert unsponsored auth-api diagnostic message to keep surface minimal (#4126) --- src/codex/auth-api.ts | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/codex/auth-api.ts b/src/codex/auth-api.ts index e68e79b12c..6994e2b2ac 100644 --- a/src/codex/auth-api.ts +++ b/src/codex/auth-api.ts @@ -116,7 +116,7 @@ import type { CodexQuotaRefreshOutcome } from "./quota-refresh-outcome"; import { getMainAccountHardLockStatus, type MainAccountHardLockStatus } from "./main-account-hard-lock"; import { observeMainReserveRevocation } from "./reserve-availability"; import { emailMaskingEnabled, projectEmail } from "../lib/privacy"; -import { CodexWarmupError, codexWarmupFailureReason, warmCodexAccount } from "./warmup"; +import { codexWarmupFailureReason, warmCodexAccount } from "./warmup"; export { maskEmail } from "../lib/privacy"; import type { CodexAccount, CodexAccountCredentials, OcxConfig } from "../types"; import type { CatalogDisposition } from "./convergence-types"; @@ -585,14 +585,10 @@ async function verifyCodexAccountWarmup( return { ok: true, validatedAt: Date.now() }; } catch (err) { const reason = codexWarmupFailureReason(err); - const isModelProvisioningError = err instanceof CodexWarmupError && (err.status === 400 || err.status === 404); - const error = isModelProvisioningError - ? "Codex account warmup failed. Verify account model access or provisioning and try again." - : "Codex account warmup failed. Reauthenticate the account and try again."; return { ok: false, response: jsonResponse({ - error, + error: "Codex account warmup failed. Reauthenticate the account and try again.", code: "codex_warmup_failed", reason, accountId,