mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-13 10:43:43 +03:00
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:
@@ -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
|
||||
@@ -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> {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
159
tests/unit/chatgpt-web-max-thinking-effort.test.ts
Normal file
159
tests/unit/chatgpt-web-max-thinking-effort.test.ts
Normal 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();
|
||||
}
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user