Skip to content
Merged
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
43 changes: 41 additions & 2 deletions packages/opencode/src/plugin/codex.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Comment thread
anandgupta42 marked this conversation as resolved.
* 2026-08-31T19:00:00Z.
Comment thread
anandgupta42 marked this conversation as resolved.
* ``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([
Comment thread
coderabbitai[bot] marked this conversation as resolved.
"gpt-5.3-codex-spark",
"gpt-5.4",
"gpt-5.4-mini",
"gpt-5.5",
"gpt-5.6-luna",
"gpt-5.6-sol",
Expand Down
43 changes: 33 additions & 10 deletions packages/opencode/test/plugin/codex-allowlist.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [
Expand Down Expand Up @@ -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", () => {
Expand Down Expand Up @@ -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", () => {
Expand Down
Loading