From 8147a4df1faaeb5788d2cb4dfad7f103a67b3ac4 Mon Sep 17 00:00:00 2001 From: Ben Wachman Date: Wed, 28 Oct 2026 21:37:37 -0700 Subject: [PATCH 1/3] fix(auth): replace retired models in the ChatGPT model list OpenAI retired gpt-5.4, gpt-5.4-mini and gpt-5.5 for ChatGPT sign-in only; API-key sessions still serve them. The ChatGPT and API-key defaults therefore differ on purpose, so they no longer have to match. --- .../first-class-providers/src/providers.ts | 7 +++---- src/auth/codex/constants.ts | 18 ++++++++---------- src/config/codex-providers.test.ts | 4 ++-- src/provider/identity-divergence.test.ts | 14 -------------- 4 files changed, 13 insertions(+), 30 deletions(-) diff --git a/packages/first-class-providers/src/providers.ts b/packages/first-class-providers/src/providers.ts index 48406ce04..a42cd1e92 100644 --- a/packages/first-class-providers/src/providers.ts +++ b/packages/first-class-providers/src/providers.ts @@ -55,10 +55,9 @@ export const OPENAI_API_MAX_COMPLETION_TOKENS_MODELS: readonly string[] = [ * Tier A order: dual-path OpenAI, OAuth xAI, Go/Zen, Z.AI, big three, Custom. * * Canonical static registry for provider identity: base URLs, model lists, - * and defaults live here. The Codex/xAI OAuth fallbacks stay separate — they - * back live catalog calls, not static identity — but must agree with this - * registry; src/provider/identity-divergence.test.ts pins that. Do not add - * another edge list: reference this registry or the fallbacks. + * and defaults live here. The Codex/xAI OAuth model lists stay separate, since + * those sign-ins serve their own model sets and defaults. Do not add another + * edge list: reference this registry or those lists. */ export const FIRST_CLASS_PROVIDERS: readonly FirstClassProviderDef[] = [ { diff --git a/src/auth/codex/constants.ts b/src/auth/codex/constants.ts index b81fab440..7a3786e81 100644 --- a/src/auth/codex/constants.ts +++ b/src/auth/codex/constants.ts @@ -21,19 +21,17 @@ export const CODEX_CLIENT_VERSION = "0.50.0"; // the public Codex CLI originator. export const CODEX_ORIGINATOR = "codex_cli_rs"; -// Fallback model list, used only when the live catalog (GET /codex/models) is -// unavailable — rate-limited it returns an empty list. The backend rotates its -// serving set, so the live fetch is authoritative and these are just a -// current-generation default so the picker is never empty. The first entry -// doubles as the ChatGPT-OAuth default: it must stay the model shared with the -// OpenAI API-key path's default in FIRST_CLASS_PROVIDERS (gpt-5.4) so both -// auth paths serving OpenAI agree. identity-divergence.test.ts pins this. +// The models a ChatGPT sign-in can select, in the Codex backend's priority +// order (GET /codex/models); the first entry is the ChatGPT default. OpenAI +// retires models for ChatGPT sign-in on its own schedule, independently of the +// OpenAI API, so this list and its default do not follow the API-key path. export const CODEX_DEFAULT_MODELS = [ - "gpt-5.4", - "gpt-5.5", + "gpt-6.1-sol", "gpt-6-astra", + "gpt-6.1-terra", + "gpt-6-sol", + "gpt-6-luna", "gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna", - "gpt-5.4-mini", ] as const; diff --git a/src/config/codex-providers.test.ts b/src/config/codex-providers.test.ts index 27a3d4205..87226391f 100644 --- a/src/config/codex-providers.test.ts +++ b/src/config/codex-providers.test.ts @@ -22,8 +22,8 @@ describe("codex provider naming", () => { }); }); -// The default-model agreement is pinned without literals in -// provider/identity-divergence.test.ts — no second literal re-pin here. +// The projected models and default are pinned against CODEX_DEFAULT_MODELS in +// provider/identity-divergence.test.ts; no literal re-pin here. describe("codexProvidersAsSettings", () => { test("projects profiles into provider settings seeded with the access token", () => { const profiles: CodexProfile[] = [ diff --git a/src/provider/identity-divergence.test.ts b/src/provider/identity-divergence.test.ts index e8935ebaa..707f1977f 100644 --- a/src/provider/identity-divergence.test.ts +++ b/src/provider/identity-divergence.test.ts @@ -56,20 +56,6 @@ describe("provider identity divergence", () => { resetGoModelDiscoveryForTests(); }); - test("OpenAI API-key default and ChatGPT-OAuth fallback default agree", () => { - const apiPath = firstClassProviderById("openai")?.paths?.find( - (p) => p.id === "api", - ); - if (apiPath?.defaultModel === undefined) - throw new Error("Expected the OpenAI API path to declare a defaultModel"); - // Both auth paths serve OpenAI models, so the shared model must lead both - // lists — the fallback default is models[0] by construction. - const codexFallbacks: readonly string[] = CODEX_DEFAULT_MODELS; - const codexDefault: string | undefined = codexFallbacks[0]; - expect(codexDefault).toBe(apiPath.defaultModel); - expect(codexFallbacks).toContain(apiPath.defaultModel); - }); - test("OpenAI API-key entry is self-consistent", () => { const apiPath = firstClassProviderById("openai")?.paths?.find( (p) => p.id === "api", From 260d6d739e1fbfe8d5b5f0275f673353bf701b90 Mon Sep 17 00:00:00 2001 From: Ben Wachman Date: Thu, 29 Oct 2026 09:00:57 -0700 Subject: [PATCH 2/3] fix(provider): match ChatGPT reasoning efforts to the served catalog The gpt-6 and gpt-6.1 models offered fewer levels than the Codex backend serves (xhigh, or max for gpt-6-astra), while gpt-5.6-luna was offered ultra, which the backend does not serve. --- src/provider/reasoning-effort.test.ts | 56 +++++++++++++++++++-------- src/provider/reasoning-effort.ts | 34 +++++++++++----- 2 files changed, 63 insertions(+), 27 deletions(-) diff --git a/src/provider/reasoning-effort.test.ts b/src/provider/reasoning-effort.test.ts index 9e7c9a5fe..cf25c48f4 100644 --- a/src/provider/reasoning-effort.test.ts +++ b/src/provider/reasoning-effort.test.ts @@ -1,4 +1,5 @@ import { afterEach, describe, test, expect } from "bun:test"; +import { CODEX_DEFAULT_MODELS } from "../auth/codex/constants.js"; import { isReasoningEffort, normalizeProviderEfforts, @@ -54,7 +55,7 @@ describe("supportedEfforts", () => { ]); }); - test("gpt-6-astra takes low through max on both API and Codex paths", () => { + test("gpt-6-astra takes low through max on the API path", () => { expect(supportedEfforts("gpt-6-astra")).toEqual([ "low", "medium", @@ -62,24 +63,45 @@ describe("supportedEfforts", () => { "xhigh", "max", ]); - expect(supportedEfforts("gpt-6-astra", undefined, true)).toEqual([ - "low", - "medium", - "high", - "xhigh", - "max", - ]); }); - test("gpt-5.6 family additionally takes max and ultra on the codex backend", () => { - expect(supportedEfforts("gpt-5.6-sol", undefined, true)).toEqual([ - "low", - "medium", - "high", - "xhigh", - "max", - "ultra", - ]); + test("ChatGPT models the backend serves up to ultra offer max and ultra", () => { + for (const model of [ + "gpt-6.1-sol", + "gpt-6-astra", + "gpt-6.1-terra", + "gpt-6-sol", + "gpt-5.6-sol", + "gpt-5.6-terra", + ]) { + expect(supportedEfforts(model, undefined, true)).toEqual([ + "low", + "medium", + "high", + "xhigh", + "max", + "ultra", + ]); + } + }); + + test("ChatGPT Luna models stop at max", () => { + for (const model of ["gpt-6-luna", "gpt-5.6-luna"]) { + const efforts = supportedEfforts(model, undefined, true); + expect(efforts).toEqual(["low", "medium", "high", "xhigh", "max"]); + expect(efforts).not.toContain("ultra"); + } + }); + + test("every built-in ChatGPT model has an assigned effort ladder", () => { + // A model added to the fallback list without a ladder would silently + // stop at xhigh. A model the backend really serves only up to xhigh + // belongs here, so adding one is a decision rather than an accident. + const servedOnlyUpToXhigh: readonly string[] = []; + for (const model of CODEX_DEFAULT_MODELS) { + if (servedOnlyUpToXhigh.includes(model)) continue; + expect(supportedEfforts(model, undefined, true)).toContain("max"); + } }); test("the model name alone does not imply codex levels", () => { diff --git a/src/provider/reasoning-effort.ts b/src/provider/reasoning-effort.ts index e87e019d2..79ae76d06 100644 --- a/src/provider/reasoning-effort.ts +++ b/src/provider/reasoning-effort.ts @@ -56,7 +56,8 @@ const FULL_EFFORT_MODELS: readonly string[] = [ "gpt-5.1-codex-max", ]; -// Codex backend models take low/medium/high/xhigh — no `minimal`, no `none`. +// ChatGPT (Codex backend) models not listed below: low/medium/high/xhigh, +// no `minimal`, no `none`. const CODEX_EFFORTS: readonly ReasoningEffort[] = [ "low", "medium", @@ -64,11 +65,21 @@ const CODEX_EFFORTS: readonly ReasoningEffort[] = [ "xhigh", ]; -// gpt-5.6 family adds `max` and `ultra`; older Codex models do not support -// them. -const MAX_EFFORT_CODEX_MODELS: readonly string[] = [ +// Per-model levels the Codex backend lists (`supported_reasoning_levels` in +// GET /codex/models, client_version 0.165.0, 2026-10-28). Exact ids rather +// than a name rule: an id not seen yet gets the conservative fallback above +// instead of a guessed tier, because the adapter does not clamp and a level +// the backend does not serve breaks the turn. +const ULTRA_EFFORT_CODEX_MODELS: readonly string[] = [ + "gpt-6.1-sol", + "gpt-6-astra", + "gpt-6.1-terra", + "gpt-6-sol", "gpt-5.6-sol", "gpt-5.6-terra", +]; +const MAX_EFFORT_CODEX_MODELS: readonly string[] = [ + "gpt-6-luna", "gpt-5.6-luna", ]; @@ -96,8 +107,8 @@ function isMuseSparkModel(model: string): boolean { const GROK_46_AND_47_EFFORTS: readonly ReasoningEffort[] = CODEX_EFFORTS; const GROK_46_AND_47_MODELS: readonly string[] = ["grok-4.6", "grok-4.7"]; -// GPT-6 Astra accepts low–max on both surfaces; not ultra (gpt-5.6 -// Codex-only), minimal, or none. +// GPT-6 Astra on the API path accepts low–max; not ultra, minimal, or none. +// On ChatGPT sign-in it follows the served-levels lists above. const GPT6_ASTRA_EFFORTS: readonly ReasoningEffort[] = [ "low", "medium", @@ -152,14 +163,17 @@ export function supportedEfforts( if (providerEfforts.length > 0) { return [...providerEfforts]; } - if (model === "gpt-6-astra") { - return [...GPT6_ASTRA_EFFORTS]; - } if (isCodex) { + if (ULTRA_EFFORT_CODEX_MODELS.includes(model)) { + return [...CODEX_EFFORTS, "max", "ultra"]; + } return MAX_EFFORT_CODEX_MODELS.includes(model) - ? [...CODEX_EFFORTS, "max", "ultra"] + ? [...CODEX_EFFORTS, "max"] : [...CODEX_EFFORTS]; } + if (model === "gpt-6-astra") { + return [...GPT6_ASTRA_EFFORTS]; + } if (FULL_EFFORT_MODELS.includes(model)) { return ["none", ...DEFAULT_EFFORTS, "xhigh"]; } From d2c486f376a26bd723512e6333f9b9d5c1cc39b7 Mon Sep 17 00:00:00 2001 From: Ben Wachman Date: Sun, 1 Nov 2026 21:02:25 -0500 Subject: [PATCH 3/3] test(auth): pin the built-in ChatGPT model list to the served catalog Nothing pinned the list's order or its default, since the other tests read the constant, so a test now records what GET /codex/models listed for a ChatGPT Plus account on 2026-11-01. The comment says where the order comes from and that availability varies by plan and rollout, so it no longer implies every account is served this list. It also gives the reason the three models are left out: OpenAI retired them from Codex with ChatGPT sign-in, and API-key sessions are not affected. --- src/auth/codex/constants.test.ts | 27 +++++++++++++++++++++++++++ src/auth/codex/constants.ts | 11 +++++++---- 2 files changed, 34 insertions(+), 4 deletions(-) create mode 100644 src/auth/codex/constants.test.ts diff --git a/src/auth/codex/constants.test.ts b/src/auth/codex/constants.test.ts new file mode 100644 index 000000000..40f482485 --- /dev/null +++ b/src/auth/codex/constants.test.ts @@ -0,0 +1,27 @@ +import { describe, expect, test } from "bun:test"; + +import { CODEX_DEFAULT_MODELS } from "./constants.js"; + +describe("CODEX_DEFAULT_MODELS", () => { + // Recorded from GET /codex/models for a ChatGPT Plus account (client_version + // 0.165.0, 2026-11-01). Nothing else pins the order or the default: the other + // tests read the constant. + test("follows the order the Codex backend listed the models in", () => { + expect([...CODEX_DEFAULT_MODELS]).toEqual([ + "gpt-6.1-sol", + "gpt-6-astra", + "gpt-6.1-terra", + "gpt-6-sol", + "gpt-6-luna", + "gpt-5.6-sol", + "gpt-5.6-terra", + "gpt-5.6-luna", + ]); + }); + + test("leaves out the models OpenAI retired for ChatGPT sign-in", () => { + for (const retired of ["gpt-5.4", "gpt-5.4-mini", "gpt-5.5"]) { + expect(CODEX_DEFAULT_MODELS as readonly string[]).not.toContain(retired); + } + }); +}); diff --git a/src/auth/codex/constants.ts b/src/auth/codex/constants.ts index 7a3786e81..8381f17e9 100644 --- a/src/auth/codex/constants.ts +++ b/src/auth/codex/constants.ts @@ -21,10 +21,13 @@ export const CODEX_CLIENT_VERSION = "0.50.0"; // the public Codex CLI originator. export const CODEX_ORIGINATOR = "codex_cli_rs"; -// The models a ChatGPT sign-in can select, in the Codex backend's priority -// order (GET /codex/models); the first entry is the ChatGPT default. OpenAI -// retires models for ChatGPT sign-in on its own schedule, independently of the -// OpenAI API, so this list and its default do not follow the API-key path. +// The models a ChatGPT sign-in offers, in the order GET /codex/models listed +// them for a ChatGPT Plus account (client_version 0.165.0, 2026-11-01); the +// first entry is the ChatGPT default. Availability varies by plan and rollout, +// so other accounts may be served a different set. gpt-5.4, gpt-5.4-mini and +// gpt-5.5 are left out because OpenAI retired them from Codex with ChatGPT +// sign-in. API-key sessions are not affected, so the API-key path keeps its +// own list and default. export const CODEX_DEFAULT_MODELS = [ "gpt-6.1-sol", "gpt-6-astra",