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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

---

Expand Down
2 changes: 1 addition & 1 deletion config/quality/file-size-baseline.json
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
4 changes: 4 additions & 0 deletions open-sse/executors/chatgpt-web.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
46 changes: 46 additions & 0 deletions tests/unit/chatgpt-web.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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]);
Comment on lines +1173 to +1174

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

If the executor fails before reaching the conversation endpoint, convIdx will be -1, causing m.calls.bodies[convIdx] to be undefined and JSON.parse to throw a cryptic SyntaxError. Adding an assertion to verify that the conversation endpoint was reached makes test failures much clearer and easier to debug.

      const convIdx = m.calls.urls.findIndex((u) => u.endsWith("/backend-api/f/conversation"));
      assert.notEqual(convIdx, -1, `Execution for ${omniId} did not reach the conversation endpoint`);
      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 () => {
Expand Down
Loading