fix(chatgpt-web): map advertised gpt-5.5/5.4-pro/5.2-pro slugs to prevent silent model substitution (#4665) (#5010)

* fix(chatgpt-web): map advertised gpt-5.5/5.4-pro/5.2-pro slugs to prevent silent model substitution (#4665)

MODEL_MAP was missing the advertised catalog ids gpt-5.5, gpt-5.5-pro,
gpt-5.4-pro and gpt-5.2-pro, so MODEL_MAP[model] ?? model sent the dot-form
id verbatim to the ChatGPT backend-api, which silently rejected it and served
the default Plus model. Map each to its dash-form slug. gpt-4-5 is already
dash-form and falls through correctly, so it is intentionally left unmapped.

Extends the executor MODEL_MAP test with the four ids and adds a drift guard
asserting every advertised dot-form catalog id reaches the backend in dash-form
(never verbatim), guarding future catalog<->map drift.

file-size: tests/unit/chatgpt-web.test.ts frozen baseline 2809->2855 (+46) for
the added test cases and drift-guard test; executor source unchanged in baseline.

* docs(changelog): restore #3981/#5003 entries eaten by merge

---------

Co-authored-by: Diego Rodrigues de Sa e Souza <souzamiriamrodrigues790@gmail.com>
This commit is contained in:
Diego Rodrigues de Sa e Souza
2026-06-25 07:25:07 -03:00
committed by GitHub
parent 515bc3465f
commit 09d0d11ed5
4 changed files with 52 additions and 1 deletions

View File

@@ -16,6 +16,7 @@ _In development — bullets added per PR; finalized at release._
- **fix(dashboard):** on OAuth providers (e.g. GLM Coding), "Test all models" with auto-hide-failed now switches the model list to the "visible" filter after the run, so just-hidden failed models actually disappear on-screen — parity with the passthrough-provider path (#3610). Previously they were hidden in the DB but stayed visible under the "All" filter, so it looked like nothing was hidden. (#4887)
- **fix(pollinations):** stop forcing `jsonMode` on every request. Pollinations treats `jsonMode=true` as "the model MUST return JSON" and rejects (HTTP 400 "messages must contain the word 'json'") any normal chat request whose messages don't mention "json", so all non-JSON chat was broken. `jsonMode` is now only enabled when the caller actually requests JSON output (`response_format.type` of `json_object` or `json_schema`). (#3981)
- **fix(antigravity):** default `safetySettings` to all-OFF for parity with the native Gemini paths. The Antigravity (Google Cloud Code) request builder set `safetySettings: undefined`, which `JSON.stringify` drops — so no safety settings reached Google and its server-side defaults false-flagged benign technical prompts as `prohibited_content` (HTTP 200 + blocked body, which combo failover treats as terminal). Now honors a caller-supplied value and otherwise defaults to `DEFAULT_SAFETY_SETTINGS`, matching the claude-to-gemini / openai-to-gemini paths (#5003)
- **fix(chatgpt-web):** map the advertised `gpt-5.5`, `gpt-5.5-pro`, `gpt-5.4-pro` and `gpt-5.2-pro` catalog ids to their dash-form ChatGPT backend slugs. They were missing from `MODEL_MAP`, so the executor sent the dot-form id verbatim, which the ChatGPT backend silently ignored and served the default Plus model instead of the requested one. Adds a drift guard asserting no advertised dot-form id reaches the backend verbatim. (#4665)
---

View File

@@ -240,7 +240,7 @@
"tests/unit/cc-compatible-provider.test.ts": 1179,
"tests/unit/chatcore-sanitization.test.ts": 829,
"tests/unit/chatcore-translation-paths.test.ts": 2810,
"tests/unit/chatgpt-web.test.ts": 2809,
"tests/unit/chatgpt-web.test.ts": 2855,
"tests/unit/combo-routing-engine.test.ts": 3213,
"tests/unit/combo-strategy-fallbacks.test.ts": 880,
"tests/unit/db-core-init.test.ts": 867,

View File

@@ -82,9 +82,13 @@ const MODEL_MAP: Record<string, string> = {
"gpt-5.3-instant": "gpt-5-3-instant",
"gpt-5.3": "gpt-5-3",
"gpt-5.3-mini": "gpt-5-3-mini",
"gpt-5.5-pro": "gpt-5-5-pro",
"gpt-5.5-thinking": "gpt-5-5-thinking",
"gpt-5.5": "gpt-5-5",
"gpt-5.4-pro": "gpt-5-4-pro",
"gpt-5.4-thinking": "gpt-5-4-thinking",
"gpt-5.4-thinking-mini": "gpt-5-4-t-mini",
"gpt-5.2-pro": "gpt-5-2-pro",
"gpt-5.2-instant": "gpt-5-2-instant",
"gpt-5.2": "gpt-5-2",
"gpt-5.2-thinking": "gpt-5-2-thinking",

View File

@@ -1119,6 +1119,13 @@ test("Executor MODEL_MAP: dot-form OmniRoute IDs translate to dash-form ChatGPT
["gpt-5.4-thinking-mini", "gpt-5-4-t-mini"],
["gpt-5.2-thinking", "gpt-5-2-thinking"],
["o3", "o3"],
// Regression #4665: these advertised catalog ids were missing from
// MODEL_MAP and fell through as their dot-form slug verbatim, which the
// ChatGPT backend-api silently rejects → served the default Plus model.
["gpt-5.5", "gpt-5-5"],
["gpt-5.5-pro", "gpt-5-5-pro"],
["gpt-5.4-pro", "gpt-5-4-pro"],
["gpt-5.2-pro", "gpt-5-2-pro"],
];
for (const [omniId, expectedSlug] of cases) {
m.calls.urls.length = 0;
@@ -1141,6 +1148,45 @@ test("Executor MODEL_MAP: dot-form OmniRoute IDs translate to dash-form ChatGPT
}
});
test("MODEL_MAP drift guard: every advertised dot-form catalog id resolves to a dash-form backend slug (no verbatim fall-through) (#4665)", async () => {
reset();
const { getRegistryEntry } = await import("../../open-sse/config/providerRegistry.ts");
const ids = (getRegistryEntry("chatgpt-web")?.models || []).map((m) => m.id);
// Dot-form ids must never reach the ChatGPT backend verbatim — the backend
// only accepts dash-form slugs. (Ids that are already dash-form, e.g.
// "gpt-4-5", legitimately pass through unchanged and are exempt.)
const dotFormIds = ids.filter((id) => id.includes("."));
const m = installMockFetch();
try {
for (const omniId of dotFormIds) {
m.calls.urls.length = 0;
m.calls.bodies.length = 0;
const executor = new ChatGptWebExecutor();
await executor.execute({
model: omniId,
body: { messages: [{ role: "user", content: "hi" }] },
stream: false,
credentials: { apiKey: "test" },
signal: AbortSignal.timeout(10_000),
log: null,
});
const convIdx = m.calls.urls.findIndex((u) => u.endsWith("/backend-api/f/conversation"));
const body = JSON.parse(m.calls.bodies[convIdx]);
assert.ok(
!body.model.includes("."),
`${omniId} reached the backend as "${body.model}" (still dot-form) — missing MODEL_MAP entry causes silent model substitution`,
);
assert.notEqual(
body.model,
omniId,
`${omniId} fell through MODEL_MAP verbatim — add a dash-form mapping`,
);
}
} finally {
m.restore();
}
});
// ─── thinking_effort PATCH user_last_used_model_config ─────────────────────
test("thinking_effort: high → PATCH user_last_used_model_config with extended", async () => {