From 8f9ff44f270cec369c3546290300d565e89bca0f Mon Sep 17 00:00:00 2001 From: semihisikman Date: Mon, 28 Sep 2026 12:42:24 +0700 Subject: [PATCH] fix(codex): remove ghost models, add gpt-daybreak/reserve, route gpt-5.x/6.x bare slugs to codex (#4418) --- open-sse/providers/registry/codex.js | 13 +- open-sse/services/model.js | 13 +- .../unit/codex-registry-model-routing.test.js | 135 ++++++++++++++++++ 3 files changed, 153 insertions(+), 8 deletions(-) create mode 100644 tests/unit/codex-registry-model-routing.test.js diff --git a/open-sse/providers/registry/codex.js b/open-sse/providers/registry/codex.js index c7e192e9..bfa30e3e 100644 --- a/open-sse/providers/registry/codex.js +++ b/open-sse/providers/registry/codex.js @@ -63,12 +63,11 @@ export default { { id: "gpt-5.6-luna-review", name: "GPT 5.6 Luna Review", upstreamModelId: "gpt-5.6-luna", quotaFamily: "review" }, { id: "gpt-5.5", name: "GPT 5.5" }, { id: "gpt-5.5-review", name: "GPT 5.5 Review", upstreamModelId: "gpt-5.5", quotaFamily: "review" }, - { id: "gpt-5.4", name: "GPT 5.4" }, - { id: "gpt-5.4-review", name: "GPT 5.4 Review", upstreamModelId: "gpt-5.4", quotaFamily: "review" }, - { id: "gpt-5.4-mini", name: "GPT 5.4 Mini" }, - { id: "gpt-5.4-mini-review", name: "GPT 5.4 Mini Review", upstreamModelId: "gpt-5.4-mini", quotaFamily: "review" }, - { id: "gpt-5.3-codex-spark", name: "GPT 5.3 Codex Spark" }, - { id: "gpt-5.3-codex-spark-review", name: "GPT 5.3 Codex Spark Review", upstreamModelId: "gpt-5.3-codex-spark", quotaFamily: "review" }, + // gpt-5.4 / gpt-5.4-mini / gpt-5.3-codex-spark removed: absent from backend-api/codex/models + // for ChatGPT Plus/Pro accounts and return HTTP 400 "model is not supported" (#4202). + // gpt-daybreak-blue-latest and gpt-reserve added: confirmed live via backend-api/codex/models (#4202). + { id: "gpt-daybreak-blue-latest", name: "GPT Daybreak Blue" }, + { id: "gpt-reserve", name: "GPT Reserve" }, // Codex CLI's auto-review virtual model. Unlike the "-review" variants above it is not derived // from a base model, so it is forwarded verbatim instead of having "-review" stripped (#1398). { id: "codex-auto-review", name: "Codex Auto Review", upstreamModelId: "codex-auto-review", quotaFamily: "review" }, @@ -81,7 +80,7 @@ export default { { id: "gpt-5.6-terra-image", name: "GPT 5.6 Terra Image", capabilities: ["text2img","edit"], params: ["size","quality","background","image_detail","output_format"], kind: "image" }, { id: "gpt-5.6-luna-image", name: "GPT 5.6 Luna Image", capabilities: ["text2img","edit"], params: ["size","quality","background","image_detail","output_format"], kind: "image" }, { id: "gpt-5.5-image", name: "GPT 5.5 Image", capabilities: ["text2img","edit"], params: ["size","quality","background","image_detail","output_format"], kind: "image" }, - { id: "gpt-5.4-image", name: "GPT 5.4 Image", capabilities: ["text2img","edit"], params: ["size","quality","background","image_detail","output_format"], kind: "image" }, + // gpt-5.4-image removed alongside gpt-5.4 (both are dead on the backend) (#4202). { id: "gpt-5.3-image", name: "GPT 5.3 Image", capabilities: ["text2img","edit"], params: ["size","quality","background","image_detail","output_format"], kind: "image" }, ], serviceKinds: ["llm","image"], diff --git a/open-sse/services/model.js b/open-sse/services/model.js index 15c50195..7715b586 100644 --- a/open-sse/services/model.js +++ b/open-sse/services/model.js @@ -124,8 +124,19 @@ export async function getModelInfoCore(modelStr, aliasesOrGetter) { // Config-driven prefix → provider inference (first match wins, fallback "openai"). const MODEL_PREFIX_PROVIDERS = [ - // Codex CLI sends this bare virtual model for auto-review — keep it on OAuth Codex (#1398). + // Codex CLI sends this bare virtual model for auto-review - keep it on OAuth Codex (#1398). [/^codex-auto-review$/, "codex"], + // Codex-only GPT model slugs: present in backend-api/codex/models but not on the + // OpenAI API. Without these rules a bare model id (e.g. "gpt-5.6-terra" from the + // Codex CLI /model picker) resolves to provider "openai", which 404s for users that + // only have a Codex OAuth account and no OpenAI API key (#4405). + // Ranges covered: gpt-5.x, gpt-6.x, gpt-daybreak-*, gpt-reserve* — all are + // Codex-backend models. Plain "gpt-4*" / "gpt-3.5*" / "gpt-4o*" fall through to + // the generic gpt-* → openai rule below. + [/^gpt-[56]\./, "codex"], + [/^gpt-6-/, "codex"], + [/^gpt-daybreak-/, "codex"], + [/^gpt-reserve/, "codex"], [/^claude-/, "anthropic"], [/^gemini-/, "gemini"], [/^gpt-/, "openai"], diff --git a/tests/unit/codex-registry-model-routing.test.js b/tests/unit/codex-registry-model-routing.test.js new file mode 100644 index 00000000..aaa52371 --- /dev/null +++ b/tests/unit/codex-registry-model-routing.test.js @@ -0,0 +1,135 @@ +/** + * Tests for #4202 and #4405 + * + * #4202 — Codex registry has ghost models (always HTTP 400) and is missing + * gpt-daybreak-blue-latest / gpt-reserve. + * Fix: remove gpt-5.4/mini/spark entries + gpt-5.4-image; add gpt-daybreak-blue-latest and gpt-reserve. + * + * #4405 — Bare Codex model slugs (e.g. gpt-5.6-terra from the CLI /model picker) + * routed to provider "openai" instead of "codex", causing 404 for users without + * an OpenAI API key connection. + * Fix: add codex-specific gpt-5.x / gpt-6.x / gpt-daybreak-* / gpt-reserve* rules + * to MODEL_PREFIX_PROVIDERS before the generic gpt-* → openai rule. + */ + +import { describe, it, expect } from "vitest"; +import fs from "fs"; +import path from "path"; + +// ── #4202 Registry checks ──────────────────────────────────────────────────── + +const codexSrc = fs.readFileSync( + path.resolve("../open-sse/providers/registry/codex.js"), + "utf-8" +); + +describe("Codex registry — ghost models removed (#4202)", () => { + it("gpt-5.4 is removed", () => { + // Match only a standalone entry, not inside gpt-5.4-mini or gpt-5.45 etc. + expect(codexSrc).not.toMatch(/id:\s*"gpt-5\.4"/); + }); + + it("gpt-5.4-mini is removed", () => { + expect(codexSrc).not.toMatch(/id:\s*"gpt-5\.4-mini"/); + }); + + it("gpt-5.3-codex-spark is removed", () => { + expect(codexSrc).not.toMatch(/id:\s*"gpt-5\.3-codex-spark"/); + }); + + it("gpt-5.4-image is removed", () => { + expect(codexSrc).not.toMatch(/id:\s*"gpt-5\.4-image"/); + }); +}); + +describe("Codex registry — new models added (#4202)", () => { + it("gpt-daybreak-blue-latest is present", () => { + expect(codexSrc).toContain('"gpt-daybreak-blue-latest"'); + }); + + it("gpt-reserve is present", () => { + expect(codexSrc).toContain('"gpt-reserve"'); + }); +}); + +describe("Codex registry — still-live models kept (#4202 regression guard)", () => { + it("gpt-5.5 still present", () => { + expect(codexSrc).toContain('"gpt-5.5"'); + }); + + it("gpt-5.6-terra still present", () => { + expect(codexSrc).toContain('"gpt-5.6-terra"'); + }); + + it("gpt-6-astra still present", () => { + expect(codexSrc).toContain('"gpt-6-astra"'); + }); +}); + +// ── #4405 Model prefix routing ─────────────────────────────────────────────── +// Replicate MODEL_PREFIX_PROVIDERS logic from open-sse/services/model.js + +const MODEL_PREFIX_PROVIDERS = [ + [/^codex-auto-review$/, "codex"], + [/^gpt-[56]\./, "codex"], + [/^gpt-6-/, "codex"], + [/^gpt-daybreak-/, "codex"], + [/^gpt-reserve/, "codex"], + [/^claude-/, "anthropic"], + [/^gemini-/, "gemini"], + [/^gpt-/, "openai"], + [/^o[134]/, "openai"], + [/^deepseek-/, "openrouter"], +]; + +function inferProvider(modelName) { + if (!modelName) return "openai"; + const m = modelName.toLowerCase(); + return MODEL_PREFIX_PROVIDERS.find(([re]) => re.test(m))?.[1] || "openai"; +} + +describe("inferProviderFromModelName — Codex gpt-* models route to codex (#4405)", () => { + it("gpt-5.6-terra → codex", () => { + expect(inferProvider("gpt-5.6-terra")).toBe("codex"); + }); + + it("gpt-5.6-sol → codex", () => { + expect(inferProvider("gpt-5.6-sol")).toBe("codex"); + }); + + it("gpt-5.5 → codex", () => { + expect(inferProvider("gpt-5.5")).toBe("codex"); + }); + + it("gpt-6-astra → codex", () => { + expect(inferProvider("gpt-6-astra")).toBe("codex"); + }); + + it("gpt-daybreak-blue-latest → codex", () => { + expect(inferProvider("gpt-daybreak-blue-latest")).toBe("codex"); + }); + + it("gpt-reserve → codex", () => { + expect(inferProvider("gpt-reserve")).toBe("codex"); + }); + + it("gpt-4o → openai (standard model not affected)", () => { + expect(inferProvider("gpt-4o")).toBe("openai"); + }); + + it("gpt-4-turbo → openai (standard model not affected)", () => { + expect(inferProvider("gpt-4-turbo")).toBe("openai"); + }); + + it("gpt-3.5-turbo → openai (standard model not affected)", () => { + expect(inferProvider("gpt-3.5-turbo")).toBe("openai"); + }); + + it("claude-opus-5 → anthropic (regression guard)", () => { + expect(inferProvider("claude-opus-5")).toBe("anthropic"); + }); + + it("codex-auto-review → codex (regression guard)", () => { + expect(inferProvider("codex-auto-review")).toBe("codex"); + }); +}); \ No newline at end of file