fix(chatgpt-web): preserve native max thinking effort (#10077)

* fix(chatgpt-web): preserve max thinking effort

* fix(chatgpt-web): allow native max effort

* test(chatgpt-web): cover native max effort

* docs(changelog): record ChatGPT Web max effort fix
This commit is contained in:
zannen7
2026-08-13 12:01:52 +08:00
committed by GitHub
parent fffeb14e40
commit cc41503c4a
4 changed files with 179 additions and 16 deletions

View File

@@ -0,0 +1 @@
- **fix(chatgpt-web):** Preserve native `max` thinking effort through ChatGPT Web routing ([#10077](https://github.com/diegosouzapw/OmniRoute/pull/10077)) — thanks @zannen7

View File

@@ -441,7 +441,7 @@ function configuredProPollIntervalMs(): number {
async function setUserThinkingEffort(
modelSlug: string,
effort: "standard" | "extended",
effort: "standard" | "extended" | "max",
accessToken: string,
accountId: string | null,
sessionId: string,
@@ -984,7 +984,7 @@ function buildConversationBody(
// chatgpt.com history. Disable Temporary Chat only when ChatGPT needs a
// durable image conversation (image generation/editing).
persistConversation: boolean;
thinkingEffort: "standard" | "extended" | null;
thinkingEffort: "standard" | "extended" | "max" | null;
continuation?: ChatGptImageConversationContext | null;
}
): Record<string, unknown> {

View File

@@ -25,7 +25,9 @@ export const MODEL_MAP: Record<string, string> = {
o3: "o3",
};
export const MODEL_FORCED_EFFORT: Record<string, "standard" | "extended"> = {
export type ChatGptThinkingEffort = "standard" | "extended" | "max";
export const MODEL_FORCED_EFFORT: Record<string, ChatGptThinkingEffort> = {
"gpt-5-6-pro": "standard",
"gpt-5.6-pro": "standard",
"gpt-5-5-pro": "standard",
@@ -62,20 +64,21 @@ export function isThinkingCapableModel(modelId: string, slug: string): boolean {
);
}
/** Map either a chatgpt.com-native value (`standard`/`extended`) or the
/** Map either a chatgpt.com-native value (`standard`/`extended`/`max`) or the
* OpenAI Chat Completions `reasoning_effort` field to the value the
* `user_last_used_model_config` endpoint expects.
*
* minimal | low | medium | standard → standard
* high | xhigh | extended → extended
* high | extended → extended
* xhigh | max → max
*
* `medium` collapses to `standard` because chatgpt.com only has two levels —
* there is no separate medium tier on the web product. Returns null for
* absent/unknown inputs. */
export function normalizeThinkingEffort(input: unknown): "standard" | "extended" | null {
* `xhigh` remains a compatibility alias for the highest ChatGPT Web tier.
* Returns null for absent/unknown inputs. */
export function normalizeThinkingEffort(input: unknown): ChatGptThinkingEffort | null {
if (typeof input !== "string") return null;
const v = input.trim().toLowerCase();
if (v === "extended" || v === "high" || v === "xhigh") return "extended";
if (v === "max" || v === "xhigh") return "max";
if (v === "extended" || v === "high") return "extended";
if (v === "standard" || v === "low" || v === "medium" || v === "minimal") {
return "standard";
}
@@ -83,14 +86,14 @@ export function normalizeThinkingEffort(input: unknown): "standard" | "extended"
}
/** Resolve the requested effort for this turn.
* Order: `providerSpecificData.thinkingEffort` (raw override, takes
* `standard`/`extended` directly) > `body.reasoning_effort` (top-level OpenAI
* Chat Completions field) > `body.reasoning.effort` (Responses-API nesting).
* Returns null when the caller did not request one. */
* Order: `providerSpecificData.thinkingEffort` (raw override, takes native
* `standard`/`extended`/`max` values) > `body.reasoning_effort` (top-level
* OpenAI Chat Completions field) > `body.reasoning.effort` (Responses-API
* nesting). Returns null when the caller did not request one. */
export function resolveThinkingEffort(
body: unknown,
providerSpecificData: Record<string, unknown> | undefined
): "standard" | "extended" | null {
): ChatGptThinkingEffort | null {
if (providerSpecificData && providerSpecificData.thinkingEffort !== undefined) {
return normalizeThinkingEffort(providerSpecificData.thinkingEffort);
}
@@ -104,7 +107,7 @@ export function resolveThinkingEffort(
export interface ResolvedChatGptModel {
slug: string;
effort: "standard" | "extended" | null;
effort: ChatGptThinkingEffort | null;
isPro: boolean;
}

View File

@@ -0,0 +1,159 @@
import test from "node:test";
import assert from "node:assert/strict";
import {
normalizeThinkingEffort,
resolveChatGptModel,
} from "../../open-sse/executors/chatgpt-web/models.ts";
import {
ChatGptWebExecutor,
__resetChatGptWebCachesForTesting,
} from "../../open-sse/executors/chatgpt-web.ts";
import { __setTlsFetchOverrideForTesting } from "../../open-sse/services/chatgptTlsClient.ts";
function mockResponse(status: number, body: unknown, contentType = "application/json") {
return {
status,
headers: new Headers({ "Content-Type": contentType }),
text: typeof body === "string" ? body : JSON.stringify(body),
body: null,
};
}
function installMockFetch() {
const calls: {
userConfigUrl: string | null;
userConfigMethod: string | null;
conversationBody: string | null;
} = {
userConfigUrl: null,
userConfigMethod: null,
conversationBody: null,
};
__setTlsFetchOverrideForTesting(async (url, opts = {}) => {
const u = String(url);
if (u === "https://chatgpt.com/" || u === "https://chatgpt.com") {
return mockResponse(
200,
'<html data-build="prod-test"><script src="https://cdn.oaistatic.com/test.js"></script></html>',
"text/html"
);
}
if (u.includes("/api/auth/session")) {
return mockResponse(200, {
accessToken: "jwt-test",
expires: new Date(Date.now() + 3_600_000).toISOString(),
user: { id: "user-test" },
});
}
if (u.includes("/backend-api/settings/user_last_used_model_config")) {
calls.userConfigUrl = u;
calls.userConfigMethod = (opts.method || "GET").toUpperCase();
return mockResponse(200, { is_disabled: false });
}
if (u.includes("/backend-api/sentinel/chat-requirements")) {
return mockResponse(200, {
token: "requirements-test",
proofofwork: { required: false },
});
}
if (u.endsWith("/backend-api/f/conversation")) {
calls.conversationBody = opts.body ?? null;
return mockResponse(
200,
[
`data: ${JSON.stringify({
conversation_id: "conv-test",
message: {
id: "msg-test",
author: { role: "assistant" },
content: { content_type: "text", parts: ["ok"] },
status: "finished_successfully",
},
})}`,
"",
"data: [DONE]",
"",
].join("\r\n"),
"text/event-stream"
);
}
// Browser-like warmup endpoints are best-effort. Returning a normal 200
// keeps this focused test independent from their response details.
return mockResponse(200, {});
});
return {
calls,
restore() {
__setTlsFetchOverrideForTesting(null);
},
};
}
test("ChatGPT Web thinking effort aliases map to the three native tiers", () => {
const cases = [
["minimal", "standard"],
["low", "standard"],
["medium", "standard"],
["standard", "standard"],
["high", "extended"],
["extended", "extended"],
["xhigh", "max"],
["max", "max"],
] as const;
for (const [input, expected] of cases) {
assert.equal(normalizeThinkingEffort(input), expected, input);
}
});
test("providerSpecificData can request native max with highest precedence", () => {
const resolved = resolveChatGptModel(
"gpt-5.6-thinking",
{ reasoning_effort: "low" },
{ thinkingEffort: "max" }
);
assert.equal(resolved.effort, "max");
});
for (const effort of ["xhigh", "max"] as const) {
test(`ChatGPT Web executor sends ${effort} as thinking_effort=max`, async () => {
__resetChatGptWebCachesForTesting();
const mock = installMockFetch();
try {
const executor = new ChatGptWebExecutor();
const result = await executor.execute({
model: "gpt-5.6-thinking",
body: {
messages: [{ role: "user", content: "hi" }],
reasoning_effort: effort,
},
stream: false,
credentials: { apiKey: `cookie-${effort}` },
signal: AbortSignal.timeout(10_000),
log: null,
});
assert.equal(result.response.status, 200);
assert.equal(mock.calls.userConfigMethod, "PATCH");
assert.ok(mock.calls.userConfigUrl);
const settingsUrl = new URL(mock.calls.userConfigUrl);
assert.equal(settingsUrl.searchParams.get("model_slug"), "gpt-5-6-thinking");
assert.equal(settingsUrl.searchParams.get("thinking_effort"), "max");
assert.ok(mock.calls.conversationBody);
const conversationBody = JSON.parse(mock.calls.conversationBody) as Record<string, unknown>;
assert.equal(conversationBody.thinking_effort, "max");
} finally {
mock.restore();
}
});
}