diff --git a/packages/opencode/src/plugin/codex.ts b/packages/opencode/src/plugin/codex.ts index 519dd3ae0..5daf5a5f9 100644 --- a/packages/opencode/src/plugin/codex.ts +++ b/packages/opencode/src/plugin/codex.ts @@ -111,11 +111,50 @@ const OAUTH_POLLING_SAFETY_MARGIN_MS = 3000 * the mechanism is a small drop-in: fetch, keep ``visibility === "list"``, and * fall back to this set whenever the response is empty or the call fails. * + * RETIRED — ``gpt-5.4`` and ``gpt-5.4-mini`` are excluded as of + * 2026-08-31T19:00:00Z. + * ``openai/codex``'s own shipped catalog + * (codex-rs/models-manager/models.json) marks both ``visibility: "hide"`` with + * ``upgrade.retirement_at: "2026-08-31T19:00:00Z"`` and the migration text + * "GPT-5.4 is no longer available. Codex now uses GPT-5.6 Terra in place of + * GPT-5.4." The documented replacements — ``gpt-5.4`` -> ``gpt-5.6-terra``, + * ``gpt-5.4-mini`` -> ``gpt-5.6-luna`` — are both already in this set, so + * affected users have a working model without further change **provided their + * catalog is current**. This filter only ever deletes; it cannot add a model + * the catalog lacks, and NEITHER replacement was present in any pre-#1188 + * bundled catalog. That is why this change was sequenced behind #1188 rather + * than shipped on its own: on a cold cache it would have left shipped release + * binaries (which embedded the 2026-03-30 ``release.yml`` fixture) with + * ``gpt-5.3-codex-spark`` alone, down from three. + * + * #1188 has since merged (``5993471bad``), so release builds now embed a + * release-time models.dev catalog. Measured against one generated by the + * post-#1188 build path: 207 providers, 47 openai models, both ``gpt-5.6-terra`` + * and ``gpt-5.6-luna`` present. A subscription user on such a build goes from + * seven selectable models to five here, losing only the two retired ids and + * keeping every gpt-5.6 replacement. The gap this comment used to describe is + * closed. + * + * A source checkout is still served by the older committed + * ``models-snapshot.ts`` blob, where the same removal is four models down to two + * (``gpt-5.3-codex-spark`` and ``gpt-5.5``); that blob carries no gpt-5.6 entry + * and is refreshed by the next release build. + * + * This is a retirement from the ChatGPT-subscription picker, NOT an API + * deprecation: both ids still carry ``supported_in_api: true``, neither is on + * OpenAI's deprecations page, and models.dev does not mark either + * ``deprecated``. API-key auth is unaffected and follows a longer schedule — + * this filter only runs when ``auth.type === "oauth"`` (see the models loader + * below), so key-authenticated users keep both ids. + * + * Because models.dev keeps serving these entries (they are still live API + * models), leaving them here would NOT self-heal after the deadline: they + * would stay in the subscription picker and fail at request time with the same + * opaque 400 this allowlist exists to prevent. + * * Exported for unit-test coverage — see test/plugin/codex-allowlist.test.ts. */ export const OAUTH_ALLOWED_MODELS = new Set([ "gpt-5.3-codex-spark", - "gpt-5.4", - "gpt-5.4-mini", "gpt-5.5", "gpt-5.6-luna", "gpt-5.6-sol", diff --git a/packages/opencode/test/plugin/codex-allowlist.test.ts b/packages/opencode/test/plugin/codex-allowlist.test.ts index f5f477bd4..d6f76a124 100644 --- a/packages/opencode/test/plugin/codex-allowlist.test.ts +++ b/packages/opencode/test/plugin/codex-allowlist.test.ts @@ -46,15 +46,20 @@ import { describe, expect, test } from "bun:test" import { OAUTH_ALLOWED_MODELS, disallowedOAuthModelKeys, shouldAllowOAuthModel } from "../../src/plugin/codex" /** Verified HTTP 200 on a ChatGPT Pro subscription credential. */ -const VERIFIED_ACCEPTED = [ - "gpt-5.3-codex-spark", - "gpt-5.4", - "gpt-5.4-mini", - "gpt-5.5", - "gpt-5.6-luna", - "gpt-5.6-sol", - "gpt-5.6-terra", -] +const VERIFIED_ACCEPTED = ["gpt-5.3-codex-spark", "gpt-5.5", "gpt-5.6-luna", "gpt-5.6-sol", "gpt-5.6-terra"] + +/** Retired from the ChatGPT-subscription picker on 2026-08-31T19:00:00Z. + * + * Both probed HTTP 200 before the deadline, so they are not in + * VERIFIED_REJECTED — they stopped being offered rather than being refused. + * Source: `openai/codex`'s shipped codex-rs/models-manager/models.json marks + * both `visibility: "hide"` with `upgrade.retirement_at: "2026-08-31T19:00:00Z"`. + * Replacements (gpt-5.4 -> gpt-5.6-terra, gpt-5.4-mini -> gpt-5.6-luna) are in + * VERIFIED_ACCEPTED above. + * + * These are still live API models, so models.dev keeps them and the catalog + * will not drop them for us — the allowlist is what has to. */ +const RETIRED_FROM_SUBSCRIPTION = ["gpt-5.4", "gpt-5.4-mini"] /** Verified HTTP 400 "not supported when using Codex with a ChatGPT account". */ const VERIFIED_REJECTED = [ @@ -94,6 +99,23 @@ describe("OAUTH_ALLOWED_MODELS — verified subscription truth table", () => { // against the live endpoint first and land it in VERIFIED_ACCEPTED too. expect([...OAUTH_ALLOWED_MODELS].sort()).toEqual([...VERIFIED_ACCEPTED].sort()) }) + + test("ids retired from the subscription picker are not offered", () => { + // gpt-5.4 / gpt-5.4-mini retired 2026-08-31T19:00:00Z. They remain live API + // models, so models.dev still lists them and the catalog will not remove + // them for us — if they were still allowlisted they would sit in the + // subscription picker and fail at request time. + for (const id of RETIRED_FROM_SUBSCRIPTION) { + expect(OAUTH_ALLOWED_MODELS.has(id)).toBe(false) + expect(shouldAllowOAuthModel(id)).toBe(false) + } + }) + + test("each retired id's documented replacement is offered", () => { + // The point of removing them is that users land somewhere that works. + expect(shouldAllowOAuthModel("gpt-5.6-terra")).toBe(true) // replaces gpt-5.4 + expect(shouldAllowOAuthModel("gpt-5.6-luna")).toBe(true) // replaces gpt-5.4-mini + }) }) describe("shouldAllowOAuthModel — behavior of the filter itself", () => { @@ -177,10 +199,11 @@ describe("disallowedOAuthModelKeys — what the loader actually deletes", () => const models = { "gpt-5.6-sol": model(undefined), "gpt-5.6": model(undefined), + // gpt-5.4 is retired from the subscription picker, so it is deleted too. "gpt-5.4": { api: {} }, "gpt-5.2": { api: {} }, } - expect(disallowedOAuthModelKeys(models).sort()).toEqual(["gpt-5.2", "gpt-5.6"]) + expect(disallowedOAuthModelKeys(models).sort()).toEqual(["gpt-5.2", "gpt-5.4", "gpt-5.6"]) }) test("the catalog set resolves identically whether matched by key or api.id", () => {