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
Original file line number Diff line number Diff line change
@@ -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
4 changes: 2 additions & 2 deletions open-sse/executors/chatgpt-web.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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> {
Expand Down
31 changes: 17 additions & 14 deletions open-sse/executors/chatgpt-web/models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -62,35 +64,36 @@ 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";
}
return null;
}

/** 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);
}
Expand All @@ -104,7 +107,7 @@ export function resolveThinkingEffort(

export interface ResolvedChatGptModel {
slug: string;
effort: "standard" | "extended" | null;
effort: ChatGptThinkingEffort | null;
isPro: boolean;
}

Expand Down
159 changes: 159 additions & 0 deletions tests/unit/chatgpt-web-max-thinking-effort.test.ts
Original file line number Diff line number Diff line change
@@ -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();
}
});
}