mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-06 07:12:12 +03:00
feat(chatgpt-web): support thinking_effort (Standard/Extended) for thinking-capable models (#1821)
Integrated into release/v3.7.6
This commit is contained in:
@@ -274,11 +274,17 @@ export class BaseExecutor {
|
||||
const cloned = { ...body } as Record<string, unknown>;
|
||||
|
||||
if (Array.isArray(cloned.tools)) {
|
||||
cloned.tools = cloned.tools.map((tool: any) => {
|
||||
if (tool?.function && typeof tool.function === "object") {
|
||||
const func = { ...tool.function };
|
||||
cloned.tools = cloned.tools.map((tool: unknown) => {
|
||||
if (
|
||||
tool &&
|
||||
typeof tool === "object" &&
|
||||
"function" in tool &&
|
||||
tool.function &&
|
||||
typeof tool.function === "object"
|
||||
) {
|
||||
const func = { ...(tool.function as Record<string, unknown>) };
|
||||
if (func.description === "") delete func.description;
|
||||
return { ...tool, function: func };
|
||||
return { ...(tool as Record<string, unknown>), function: func };
|
||||
}
|
||||
return tool;
|
||||
});
|
||||
|
||||
@@ -36,6 +36,7 @@ const SESSION_URL = `${CHATGPT_BASE}/api/auth/session`;
|
||||
const SENTINEL_PREPARE_URL = `${CHATGPT_BASE}/backend-api/sentinel/chat-requirements/prepare`;
|
||||
const SENTINEL_CR_URL = `${CHATGPT_BASE}/backend-api/sentinel/chat-requirements`;
|
||||
const CONV_URL = `${CHATGPT_BASE}/backend-api/f/conversation`;
|
||||
const USER_LAST_USED_MODEL_CONFIG_URL = `${CHATGPT_BASE}/backend-api/settings/user_last_used_model_config`;
|
||||
|
||||
const CHATGPT_USER_AGENT =
|
||||
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/147.0.0.0 Safari/537.36";
|
||||
@@ -92,6 +93,20 @@ const MODEL_MAP: Record<string, string> = {
|
||||
o3: "o3",
|
||||
};
|
||||
|
||||
/** Set of chatgpt.com slugs that the user_last_used_model_config endpoint
|
||||
* accepts a `thinking_effort` value for, derived from MODEL_MAP so adding a
|
||||
* new thinking entry there automatically extends this set. Includes the
|
||||
* abbreviated slug `gpt-5-4-t-mini` (no literal "thinking" substring) — the
|
||||
* reason this set exists at all rather than a substring match.
|
||||
*
|
||||
* Derived from MODEL_MAP keys (always dot-form) that contain "thinking" or
|
||||
* are the `o3` reasoning model; the values are the chatgpt.com-side slugs. */
|
||||
const THINKING_CAPABLE_SLUGS: ReadonlySet<string> = new Set(
|
||||
Object.entries(MODEL_MAP)
|
||||
.filter(([k]) => k.includes("thinking") || k === "o3")
|
||||
.map(([, v]) => v)
|
||||
);
|
||||
|
||||
// ─── Browser-like default headers ──────────────────────────────────────────
|
||||
|
||||
function browserHeaders(): Record<string, string> {
|
||||
@@ -413,6 +428,158 @@ async function runSessionWarmup(
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Thinking-effort preference (PATCH user_last_used_model_config) ────────
|
||||
// chatgpt.com has two thinking levels for its dedicated thinking-models:
|
||||
// • standard — default, faster
|
||||
// • extended — longer reasoning budget
|
||||
// The browser sets the level by PATCHing `/backend-api/settings/user_last_used_model_config`
|
||||
// once, then issues the conversation request — the conversation endpoint itself
|
||||
// has no `thinking_effort` field; the server reads the user's stored preference
|
||||
// at routing time. We mirror that handshake when an OpenAI-style request
|
||||
// includes `reasoning_effort` (or a direct `providerSpecificData.thinkingEffort`
|
||||
// override).
|
||||
//
|
||||
// Cached per (cookie, slug, effort): the preference persists server-side, so
|
||||
// re-PATCHing the same combination is wasted bytes. Refreshed on TTL expiry or
|
||||
// whenever the caller switches efforts.
|
||||
|
||||
const thinkingEffortCache = new Map<string, number>();
|
||||
const THINKING_EFFORT_TTL_MS = 5 * 60 * 1000;
|
||||
const THINKING_EFFORT_CACHE_MAX = 400;
|
||||
|
||||
/** chatgpt.com only exposes the thinking-effort toggle on dedicated thinking
|
||||
* models and the o-series. PATCHing for a non-thinking surface is a no-op
|
||||
* (the server accepts it but the routing-time read picks the wrong knob).
|
||||
*
|
||||
* Three branches because the input can arrive in three shapes:
|
||||
* 1. OmniRoute dot-form id (`gpt-5.4-thinking-mini`) — every thinking
|
||||
* variant carries the literal "thinking" substring here.
|
||||
* 2. Resolved chatgpt.com slug containing "thinking" (`gpt-5-5-thinking`).
|
||||
* 3. Resolved chatgpt.com slug that drops the substring under abbreviation
|
||||
* (`gpt-5-4-t-mini`). Looked up via THINKING_CAPABLE_SLUGS, which is
|
||||
* derived from MODEL_MAP itself so adding a new abbreviated thinking
|
||||
* mapping automatically extends the check.
|
||||
*
|
||||
* Branch 3 also catches the case where a caller passes the chatgpt.com slug
|
||||
* directly as the `model` field (no MODEL_MAP translation needed), which
|
||||
* would otherwise silently bypass the PATCH. */
|
||||
function isThinkingCapableModel(modelId: string, slug: string): boolean {
|
||||
return (
|
||||
modelId.includes("thinking") ||
|
||||
modelId === "o3" ||
|
||||
slug.includes("thinking") ||
|
||||
THINKING_CAPABLE_SLUGS.has(slug) ||
|
||||
THINKING_CAPABLE_SLUGS.has(modelId)
|
||||
);
|
||||
}
|
||||
|
||||
/** Map either a chatgpt.com-native value (`standard`/`extended`) 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
|
||||
*
|
||||
* `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. */
|
||||
function normalizeThinkingEffort(input: unknown): "standard" | "extended" | null {
|
||||
if (typeof input !== "string") return null;
|
||||
const v = input.trim().toLowerCase();
|
||||
if (v === "extended" || v === "high" || v === "xhigh") 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. */
|
||||
function resolveThinkingEffort(
|
||||
body: unknown,
|
||||
providerSpecificData: Record<string, unknown> | undefined
|
||||
): "standard" | "extended" | null {
|
||||
if (providerSpecificData && providerSpecificData.thinkingEffort !== undefined) {
|
||||
return normalizeThinkingEffort(providerSpecificData.thinkingEffort);
|
||||
}
|
||||
const b = (body as Record<string, unknown> | null) ?? null;
|
||||
if (!b) return null;
|
||||
const top = normalizeThinkingEffort(b.reasoning_effort);
|
||||
if (top) return top;
|
||||
const nested = (b.reasoning as Record<string, unknown> | undefined)?.effort;
|
||||
return normalizeThinkingEffort(nested);
|
||||
}
|
||||
|
||||
async function setUserThinkingEffort(
|
||||
modelSlug: string,
|
||||
effort: "standard" | "extended",
|
||||
accessToken: string,
|
||||
accountId: string | null,
|
||||
sessionId: string,
|
||||
deviceId: string,
|
||||
cookie: string,
|
||||
signal: AbortSignal | null | undefined,
|
||||
log:
|
||||
| {
|
||||
debug?: (tag: string, msg: string) => void;
|
||||
warn?: (tag: string, msg: string) => void;
|
||||
}
|
||||
| null
|
||||
| undefined
|
||||
): Promise<void> {
|
||||
const cacheKey = `${cookieKey(cookie)}:${modelSlug}:${effort}`;
|
||||
const now = Date.now();
|
||||
const last = thinkingEffortCache.get(cacheKey);
|
||||
if (last && now - last < THINKING_EFFORT_TTL_MS) {
|
||||
log?.debug?.("CGPT-WEB", `thinking_effort cached (${modelSlug}=${effort}) — skip PATCH`);
|
||||
return;
|
||||
}
|
||||
if (thinkingEffortCache.size >= THINKING_EFFORT_CACHE_MAX && !thinkingEffortCache.has(cacheKey)) {
|
||||
const first = thinkingEffortCache.keys().next().value;
|
||||
if (first) thinkingEffortCache.delete(first);
|
||||
}
|
||||
|
||||
const url =
|
||||
`${USER_LAST_USED_MODEL_CONFIG_URL}` +
|
||||
`?model_slug=${encodeURIComponent(modelSlug)}` +
|
||||
`&thinking_effort=${encodeURIComponent(effort)}`;
|
||||
const headers: Record<string, string> = {
|
||||
...browserHeaders(),
|
||||
...oaiHeaders(sessionId, deviceId),
|
||||
Accept: "application/json",
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
Cookie: buildSessionCookieHeader(cookie),
|
||||
Priority: "u=4",
|
||||
};
|
||||
if (accountId) headers["chatgpt-account-id"] = accountId;
|
||||
|
||||
try {
|
||||
const r = await tlsFetchChatGpt(url, {
|
||||
method: "PATCH",
|
||||
headers,
|
||||
timeoutMs: 15_000,
|
||||
signal,
|
||||
});
|
||||
if (r.status >= 400) {
|
||||
log?.warn?.(
|
||||
"CGPT-WEB",
|
||||
`thinking_effort PATCH ${r.status} for ${modelSlug}=${effort} (continuing)`
|
||||
);
|
||||
return;
|
||||
}
|
||||
thinkingEffortCache.set(cacheKey, now);
|
||||
log?.debug?.("CGPT-WEB", `thinking_effort PATCH OK (${modelSlug}=${effort})`);
|
||||
} catch (err) {
|
||||
log?.warn?.(
|
||||
"CGPT-WEB",
|
||||
`thinking_effort PATCH failed: ${err instanceof Error ? err.message : String(err)}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async function prepareChatRequirements(
|
||||
accessToken: string,
|
||||
accountId: string | null,
|
||||
@@ -2380,6 +2547,29 @@ export class ChatGptWebExecutor extends BaseExecutor {
|
||||
log
|
||||
);
|
||||
|
||||
// 2a''. Apply thinking_effort preference for thinking-capable models.
|
||||
// Mirrors what chatgpt.com's web UI does when the user toggles the
|
||||
// "Standard"/"Extended" thinking switch — PATCH the user-config endpoint
|
||||
// before issuing the conversation. The conversation request itself has
|
||||
// no `thinking_effort` field; the server reads the stored preference at
|
||||
// routing time. Best-effort: a failed PATCH falls back to whatever the
|
||||
// account's current preference is.
|
||||
const earlyModelSlug = MODEL_MAP[model] ?? model;
|
||||
const requestedEffort = resolveThinkingEffort(body, credentials.providerSpecificData);
|
||||
if (requestedEffort && isThinkingCapableModel(model, earlyModelSlug)) {
|
||||
await setUserThinkingEffort(
|
||||
earlyModelSlug,
|
||||
requestedEffort,
|
||||
tokenEntry.accessToken,
|
||||
tokenEntry.accountId,
|
||||
sessionId,
|
||||
deviceId,
|
||||
cookie,
|
||||
signal,
|
||||
log
|
||||
);
|
||||
}
|
||||
|
||||
// 2b. Sentinel chat-requirements
|
||||
let reqs: ChatRequirements;
|
||||
try {
|
||||
@@ -2656,6 +2846,7 @@ function stringToStream(text: string): ReadableStream<Uint8Array> {
|
||||
export function __resetChatGptWebCachesForTesting(): void {
|
||||
tokenCache.clear();
|
||||
warmupCache.clear();
|
||||
thinkingEffortCache.clear();
|
||||
deviceIdCache.clear();
|
||||
__resetChatGptImageCacheForTesting();
|
||||
dplCache = null;
|
||||
|
||||
@@ -64,11 +64,13 @@ function installMockFetch({
|
||||
fileDownload,
|
||||
attachmentDownload,
|
||||
signedDownload,
|
||||
userConfig,
|
||||
onSession,
|
||||
onSentinel,
|
||||
onConv,
|
||||
onFileDownload,
|
||||
onAttachmentDownload,
|
||||
onUserConfig,
|
||||
} = {}) {
|
||||
const calls = {
|
||||
session: 0,
|
||||
@@ -78,6 +80,9 @@ function installMockFetch({
|
||||
fileDownload: 0,
|
||||
attachmentDownload: 0,
|
||||
signedDownload: 0,
|
||||
userConfig: 0,
|
||||
userConfigUrls: [],
|
||||
userConfigMethods: [],
|
||||
urls: [],
|
||||
headers: [],
|
||||
bodies: [],
|
||||
@@ -128,6 +133,22 @@ function installMockFetch({
|
||||
};
|
||||
}
|
||||
|
||||
// /backend-api/settings/user_last_used_model_config?model_slug=...&thinking_effort=...
|
||||
// Match before sentinel since /settings/* is its own surface.
|
||||
if (u.includes("/backend-api/settings/user_last_used_model_config")) {
|
||||
calls.userConfig++;
|
||||
calls.userConfigUrls.push(u);
|
||||
calls.userConfigMethods.push((opts.method || "GET").toUpperCase());
|
||||
if (onUserConfig) onUserConfig(opts, u);
|
||||
const cfg = userConfig ?? { status: 200, body: { is_disabled: false } };
|
||||
return {
|
||||
status: cfg.status,
|
||||
headers: makeHeaders({ "Content-Type": "application/json" }),
|
||||
text: typeof cfg.body === "string" ? cfg.body : JSON.stringify(cfg.body || {}),
|
||||
body: null,
|
||||
};
|
||||
}
|
||||
|
||||
if (u.includes("/sentinel/chat-requirements")) {
|
||||
calls.sentinel++;
|
||||
if (onSentinel) onSentinel(opts);
|
||||
@@ -1120,6 +1141,248 @@ test("Executor MODEL_MAP: dot-form OmniRoute IDs translate to dash-form ChatGPT
|
||||
}
|
||||
});
|
||||
|
||||
// ─── thinking_effort PATCH user_last_used_model_config ─────────────────────
|
||||
|
||||
test("thinking_effort: high → PATCH user_last_used_model_config with extended", async () => {
|
||||
reset();
|
||||
const m = installMockFetch();
|
||||
try {
|
||||
const executor = new ChatGptWebExecutor();
|
||||
await executor.execute({
|
||||
model: "gpt-5.5-thinking",
|
||||
body: { messages: [{ role: "user", content: "hi" }], reasoning_effort: "high" },
|
||||
stream: false,
|
||||
credentials: { apiKey: "cookie-1" },
|
||||
signal: AbortSignal.timeout(10_000),
|
||||
log: null,
|
||||
});
|
||||
assert.equal(m.calls.userConfig, 1, "exactly one PATCH issued");
|
||||
assert.equal(m.calls.userConfigMethods[0], "PATCH");
|
||||
const u = m.calls.userConfigUrls[0];
|
||||
assert.match(u, /model_slug=gpt-5-5-thinking/);
|
||||
assert.match(u, /thinking_effort=extended/);
|
||||
} finally {
|
||||
m.restore();
|
||||
}
|
||||
});
|
||||
|
||||
test("thinking_effort: low/medium → PATCH with standard", async () => {
|
||||
for (const effort of ["low", "medium", "minimal"]) {
|
||||
reset();
|
||||
const m = installMockFetch();
|
||||
try {
|
||||
const executor = new ChatGptWebExecutor();
|
||||
await executor.execute({
|
||||
model: "gpt-5.4-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(m.calls.userConfig, 1, `effort=${effort} should issue exactly one PATCH`);
|
||||
assert.match(m.calls.userConfigUrls[0], /thinking_effort=standard/, `${effort} → standard`);
|
||||
assert.match(m.calls.userConfigUrls[0], /model_slug=gpt-5-4-thinking/);
|
||||
} finally {
|
||||
m.restore();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test("thinking_effort: instant model never triggers PATCH even with reasoning_effort", async () => {
|
||||
reset();
|
||||
const m = installMockFetch();
|
||||
try {
|
||||
const executor = new ChatGptWebExecutor();
|
||||
await executor.execute({
|
||||
model: "gpt-5.3-instant",
|
||||
body: { messages: [{ role: "user", content: "hi" }], reasoning_effort: "high" },
|
||||
stream: false,
|
||||
credentials: { apiKey: "cookie-instant" },
|
||||
signal: AbortSignal.timeout(10_000),
|
||||
log: null,
|
||||
});
|
||||
assert.equal(m.calls.userConfig, 0, "instant slug must not PATCH thinking_effort");
|
||||
} finally {
|
||||
m.restore();
|
||||
}
|
||||
});
|
||||
|
||||
test("thinking_effort: bare chatgpt.com slug (e.g. gpt-5-4-t-mini) passed as model still PATCHes", async () => {
|
||||
// Regression: the abbreviated dash-form slug "gpt-5-4-t-mini" doesn't
|
||||
// carry the literal "thinking" substring, and isn't a key in MODEL_MAP
|
||||
// (only its dot-form alias is), so a substring-only check would silently
|
||||
// skip the PATCH for callers that send the chatgpt.com slug directly.
|
||||
for (const bareSlug of ["gpt-5-4-t-mini", "gpt-5-5-thinking", "o3"]) {
|
||||
reset();
|
||||
const m = installMockFetch();
|
||||
try {
|
||||
const executor = new ChatGptWebExecutor();
|
||||
await executor.execute({
|
||||
model: bareSlug,
|
||||
body: { messages: [{ role: "user", content: "hi" }], reasoning_effort: "high" },
|
||||
stream: false,
|
||||
credentials: { apiKey: `cookie-bare-${bareSlug}` },
|
||||
signal: AbortSignal.timeout(10_000),
|
||||
log: null,
|
||||
});
|
||||
assert.equal(
|
||||
m.calls.userConfig,
|
||||
1,
|
||||
`bare slug ${bareSlug} must trigger thinking_effort PATCH`
|
||||
);
|
||||
assert.match(
|
||||
m.calls.userConfigUrls[0],
|
||||
new RegExp(`model_slug=${bareSlug.replace(/\./g, "\\.")}`)
|
||||
);
|
||||
} finally {
|
||||
m.restore();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test("thinking_effort: thinking model without reasoning_effort skips PATCH", async () => {
|
||||
reset();
|
||||
const m = installMockFetch();
|
||||
try {
|
||||
const executor = new ChatGptWebExecutor();
|
||||
await executor.execute({
|
||||
model: "gpt-5.5-thinking",
|
||||
body: { messages: [{ role: "user", content: "hi" }] },
|
||||
stream: false,
|
||||
credentials: { apiKey: "cookie-noeffort" },
|
||||
signal: AbortSignal.timeout(10_000),
|
||||
log: null,
|
||||
});
|
||||
assert.equal(m.calls.userConfig, 0, "no effort requested → no PATCH");
|
||||
} finally {
|
||||
m.restore();
|
||||
}
|
||||
});
|
||||
|
||||
test("thinking_effort: providerSpecificData.thinkingEffort=extended overrides body", async () => {
|
||||
reset();
|
||||
const m = installMockFetch();
|
||||
try {
|
||||
const executor = new ChatGptWebExecutor();
|
||||
await executor.execute({
|
||||
model: "gpt-5.4-thinking-mini",
|
||||
body: {
|
||||
messages: [{ role: "user", content: "hi" }],
|
||||
reasoning_effort: "low", // would normally map to standard
|
||||
},
|
||||
stream: false,
|
||||
credentials: {
|
||||
apiKey: "cookie-override",
|
||||
providerSpecificData: { thinkingEffort: "extended" },
|
||||
},
|
||||
signal: AbortSignal.timeout(10_000),
|
||||
log: null,
|
||||
});
|
||||
assert.equal(m.calls.userConfig, 1);
|
||||
assert.match(m.calls.userConfigUrls[0], /model_slug=gpt-5-4-t-mini/);
|
||||
assert.match(m.calls.userConfigUrls[0], /thinking_effort=extended/);
|
||||
} finally {
|
||||
m.restore();
|
||||
}
|
||||
});
|
||||
|
||||
test("thinking_effort: nested body.reasoning.effort=high → extended", async () => {
|
||||
reset();
|
||||
const m = installMockFetch();
|
||||
try {
|
||||
const executor = new ChatGptWebExecutor();
|
||||
await executor.execute({
|
||||
model: "gpt-5.2-thinking",
|
||||
body: {
|
||||
messages: [{ role: "user", content: "hi" }],
|
||||
reasoning: { effort: "high" },
|
||||
},
|
||||
stream: false,
|
||||
credentials: { apiKey: "cookie-nested" },
|
||||
signal: AbortSignal.timeout(10_000),
|
||||
log: null,
|
||||
});
|
||||
assert.equal(m.calls.userConfig, 1);
|
||||
assert.match(m.calls.userConfigUrls[0], /model_slug=gpt-5-2-thinking/);
|
||||
assert.match(m.calls.userConfigUrls[0], /thinking_effort=extended/);
|
||||
} finally {
|
||||
m.restore();
|
||||
}
|
||||
});
|
||||
|
||||
test("thinking_effort: cached per (cookie, slug, effort) — second identical call skips PATCH", async () => {
|
||||
reset();
|
||||
const m = installMockFetch();
|
||||
try {
|
||||
const executor = new ChatGptWebExecutor();
|
||||
const opts = {
|
||||
model: "gpt-5.5-thinking",
|
||||
body: { messages: [{ role: "user", content: "hi" }], reasoning_effort: "high" },
|
||||
stream: false,
|
||||
credentials: { apiKey: "cookie-cache" },
|
||||
signal: AbortSignal.timeout(10_000),
|
||||
log: null,
|
||||
};
|
||||
await executor.execute(opts);
|
||||
await executor.execute(opts);
|
||||
assert.equal(m.calls.userConfig, 1, "second identical request hits cache");
|
||||
} finally {
|
||||
m.restore();
|
||||
}
|
||||
});
|
||||
|
||||
test("thinking_effort: switching effort within TTL triggers a fresh PATCH", async () => {
|
||||
reset();
|
||||
const m = installMockFetch();
|
||||
try {
|
||||
const executor = new ChatGptWebExecutor();
|
||||
const base = {
|
||||
model: "gpt-5.5-thinking",
|
||||
stream: false,
|
||||
credentials: { apiKey: "cookie-switch" },
|
||||
signal: AbortSignal.timeout(10_000),
|
||||
log: null,
|
||||
};
|
||||
await executor.execute({
|
||||
...base,
|
||||
body: { messages: [{ role: "user", content: "hi" }], reasoning_effort: "high" },
|
||||
});
|
||||
await executor.execute({
|
||||
...base,
|
||||
body: { messages: [{ role: "user", content: "hi" }], reasoning_effort: "low" },
|
||||
});
|
||||
assert.equal(m.calls.userConfig, 2, "different effort key bypasses cache");
|
||||
assert.match(m.calls.userConfigUrls[0], /thinking_effort=extended/);
|
||||
assert.match(m.calls.userConfigUrls[1], /thinking_effort=standard/);
|
||||
} finally {
|
||||
m.restore();
|
||||
}
|
||||
});
|
||||
|
||||
test("thinking_effort: PATCH failure is non-fatal — conversation request still fires", async () => {
|
||||
reset();
|
||||
const m = installMockFetch({
|
||||
userConfig: { status: 500, body: { error: "boom" } },
|
||||
});
|
||||
try {
|
||||
const executor = new ChatGptWebExecutor();
|
||||
const result = await executor.execute({
|
||||
model: "gpt-5.5-thinking",
|
||||
body: { messages: [{ role: "user", content: "hi" }], reasoning_effort: "high" },
|
||||
stream: false,
|
||||
credentials: { apiKey: "cookie-fail" },
|
||||
signal: AbortSignal.timeout(10_000),
|
||||
log: null,
|
||||
});
|
||||
assert.equal(m.calls.userConfig, 1);
|
||||
assert.equal(m.calls.conv, 1, "conversation still issued despite settings PATCH 500");
|
||||
assert.equal(result.response.status, 200);
|
||||
} finally {
|
||||
m.restore();
|
||||
}
|
||||
});
|
||||
|
||||
test("Image registry: cgpt-web/gpt-5.3-instant routes to ChatGPT Web image handler", async () => {
|
||||
const { parseImageModel, getImageProvider } =
|
||||
await import("../../open-sse/config/imageRegistry.ts");
|
||||
|
||||
Reference in New Issue
Block a user