mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-21 22:52:19 +03:00
fix(chatgpt-web): refresh current model catalog (#10637)
Merged — locally validated (123/123 focused chatgpt-web tests, typecheck:core clean, file-size/changelog gates green). Thanks!
This commit is contained in:
@@ -16,9 +16,11 @@ import type { FreeModelBudget } from "./freeModelCatalog.ts";
|
||||
* rewrites file timestamps on every deploy, which would report a months-old
|
||||
* catalog as "updated today". Bump this whenever the entries below change.
|
||||
*/
|
||||
export const FREE_CATALOG_CURATED_AT = "2026-08-16";
|
||||
export const FREE_CATALOG_CURATED_AT = "2026-08-18";
|
||||
|
||||
export const FREE_MODEL_BUDGETS: FreeModelBudget[] = [
|
||||
{ provider: "chatgpt-web", modelId: "gpt-5.6-luna-free", displayName: "GPT-5.6 Luna (Free)", monthlyTokens: 0, creditTokens: 0, freeType: "recurring-uncapped", poolKey: "chatgpt-web-free", tos: "caution" },
|
||||
{ provider: "chatgpt-web", modelId: "gpt-5.6-luna-free-thinking", displayName: "GPT-5.6 Luna (Free, Think)", monthlyTokens: 0, creditTokens: 0, freeType: "recurring-uncapped", poolKey: "chatgpt-web-free", tos: "caution" },
|
||||
{ provider: "agentrouter", modelId: "claude-opus-4-8", displayName: "Claude Opus 4.8", monthlyTokens: 0, creditTokens: 200000000, freeType: "one-time-initial", poolKey: "agentrouter", tos: "caution" },
|
||||
{ provider: "agentrouter", modelId: "claude-opus-5", displayName: "Claude Opus 5", monthlyTokens: 0, creditTokens: 200000000, freeType: "one-time-initial", poolKey: "agentrouter", tos: "caution" },
|
||||
{ provider: "agentrouter", modelId: "gpt-5.6-sol", displayName: "GPT-5.6 Sol", monthlyTokens: 0, creditTokens: 200000000, freeType: "one-time-initial", poolKey: "agentrouter", tos: "caution" },
|
||||
|
||||
@@ -9,12 +9,83 @@ export const chatgpt_webProvider: RegistryEntry = {
|
||||
authType: "apikey",
|
||||
authHeader: "cookie",
|
||||
models: [
|
||||
{ id: "gpt-5.6-pro", name: "GPT-5.6 Pro", toolCalling: false }, // pro tier only, standard effort
|
||||
{ id: "gpt-5.6-thinking", name: "GPT-5.6 Thinking", toolCalling: false }, // plus, pro tier
|
||||
{ id: "gpt-5.5-pro-extended", name: "GPT-5.5 Pro Extended", toolCalling: false }, // pro tier only, extended effort
|
||||
{ id: "gpt-5.5-pro", name: "GPT-5.5 Pro", toolCalling: false }, // pro tier only, standard effort
|
||||
{ id: "gpt-5.5-thinking", name: "GPT-5.5 Thinking", toolCalling: false }, // plus, pro tier
|
||||
{ id: "gpt-5.5", name: "GPT-5.5 Instant", toolCalling: false }, // free, plus, pro tier
|
||||
{ id: "o3", name: "o3", toolCalling: false }, // plus ~ tier
|
||||
{
|
||||
id: "gpt-5.6-sol-pro",
|
||||
name: "GPT-5.6 Sol (Pro)",
|
||||
liveCatalogIds: ["gpt-5-6-pro"],
|
||||
toolCalling: false,
|
||||
},
|
||||
{
|
||||
id: "gpt-5.6-sol-xhigh",
|
||||
name: "GPT-5.6 Sol (Xhigh)",
|
||||
liveCatalogIds: ["gpt-5-6-thinking"],
|
||||
toolCalling: false,
|
||||
},
|
||||
{
|
||||
id: "gpt-5.6-sol-high",
|
||||
name: "GPT-5.6 Sol (High)",
|
||||
liveCatalogIds: ["gpt-5-6-thinking"],
|
||||
toolCalling: false,
|
||||
},
|
||||
{
|
||||
id: "gpt-5.6-sol-medium",
|
||||
name: "GPT-5.6 Sol (Medium)",
|
||||
liveCatalogIds: ["gpt-5-6-thinking"],
|
||||
toolCalling: false,
|
||||
},
|
||||
{
|
||||
id: "gpt-5.6-sol-instant",
|
||||
name: "GPT-5.6 Sol (Instant)",
|
||||
liveCatalogIds: ["gpt-5-6"],
|
||||
toolCalling: false,
|
||||
},
|
||||
{
|
||||
id: "gpt-5.6-luna-free-thinking",
|
||||
name: "GPT-5.6 Luna (Free, Think)",
|
||||
liveCatalogIds: ["gpt-5-6"],
|
||||
toolCalling: false,
|
||||
},
|
||||
{
|
||||
id: "gpt-5.6-luna-free",
|
||||
name: "GPT-5.6 Luna (Free)",
|
||||
liveCatalogIds: ["gpt-5-6"],
|
||||
toolCalling: false,
|
||||
},
|
||||
{
|
||||
id: "gpt-5.5-pro-extended",
|
||||
name: "GPT-5.5 (Pro Extended)",
|
||||
liveCatalogIds: ["gpt-5-5-pro"],
|
||||
toolCalling: false,
|
||||
},
|
||||
{
|
||||
id: "gpt-5.5-pro",
|
||||
name: "GPT-5.5 (Pro)",
|
||||
liveCatalogIds: ["gpt-5-5-pro"],
|
||||
toolCalling: false,
|
||||
},
|
||||
{
|
||||
id: "gpt-5.5-xhigh",
|
||||
name: "GPT-5.5 (Xhigh)",
|
||||
liveCatalogIds: ["gpt-5-5-thinking"],
|
||||
toolCalling: false,
|
||||
},
|
||||
{
|
||||
id: "gpt-5.5-high",
|
||||
name: "GPT-5.5 (High)",
|
||||
liveCatalogIds: ["gpt-5-5-thinking"],
|
||||
toolCalling: false,
|
||||
},
|
||||
{
|
||||
id: "gpt-5.5-medium",
|
||||
name: "GPT-5.5 (Medium)",
|
||||
liveCatalogIds: ["gpt-5-5-thinking"],
|
||||
toolCalling: false,
|
||||
},
|
||||
{
|
||||
id: "gpt-5.5-instant",
|
||||
name: "GPT-5.5 (Instant)",
|
||||
liveCatalogIds: ["gpt-5-5"],
|
||||
toolCalling: false,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
@@ -46,6 +46,12 @@ export interface RegistryModel {
|
||||
id: string;
|
||||
name: string;
|
||||
aliases?: readonly string[];
|
||||
/**
|
||||
* Upstream model IDs that prove this static model is live when the provider
|
||||
* has an authoritative synchronized catalog. Needed for curated IDs whose
|
||||
* public name differs from the ID sent to the upstream service.
|
||||
*/
|
||||
liveCatalogIds?: readonly string[];
|
||||
toolCalling?: boolean;
|
||||
supportsReasoning?: boolean;
|
||||
supportedThinkingEfforts?: readonly string[];
|
||||
|
||||
@@ -32,7 +32,11 @@ import {
|
||||
__resetChatGptImageCacheForTesting,
|
||||
type ChatGptImageConversationContext,
|
||||
} from "../services/chatgptImageCache.ts";
|
||||
import { isThinkingCapableModel, resolveChatGptModel } from "./chatgpt-web/models.ts";
|
||||
import {
|
||||
resolveChatGptModel,
|
||||
resolveChatGptSystemHints,
|
||||
type ChatGptThinkingEffort,
|
||||
} from "./chatgpt-web/models.ts";
|
||||
import { cleanChatGptText } from "./chatgpt-web/citations.ts";
|
||||
import { resumeChatGptHandoff, type FinalAssistantAnswer } from "./chatgpt-web/handoff.ts";
|
||||
|
||||
@@ -43,7 +47,6 @@ 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 DEFAULT_PRO_POLL_TIMEOUT_MS = 20 * 60_000;
|
||||
const DEFAULT_PRO_POLL_INTERVAL_MS = 4_000;
|
||||
|
||||
@@ -81,10 +84,8 @@ function deviceIdFor(cookie: string): string {
|
||||
return id;
|
||||
}
|
||||
|
||||
// OmniRoute model ID → ChatGPT internal slug. The public ChatGPT Web catalog
|
||||
// keeps OmniRoute's historical dot-form IDs (e.g. "gpt-5.5-pro"), while
|
||||
// ChatGPT's backend routes use dash-form slugs (e.g. "gpt-5-5-pro"). The slug
|
||||
// catalog comes from /backend-api/models on a logged-in account.
|
||||
// OmniRoute model IDs select a GPT-5.6 Sol performance lane. Captured browser
|
||||
// requests use one of `gpt-5-6`, `gpt-5-6-thinking`, or `gpt-5-6-pro`.
|
||||
|
||||
// ─── Browser-like default headers ──────────────────────────────────────────
|
||||
|
||||
@@ -408,25 +409,6 @@ 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;
|
||||
|
||||
function configuredProPollTimeoutMs(): number {
|
||||
const raw = Number(process.env.OMNIROUTE_CGPT_WEB_PRO_TIMEOUT_MS);
|
||||
if (!Number.isFinite(raw) || raw <= 0) return DEFAULT_PRO_POLL_TIMEOUT_MS;
|
||||
@@ -439,73 +421,6 @@ function configuredProPollIntervalMs(): number {
|
||||
return Math.floor(raw);
|
||||
}
|
||||
|
||||
async function setUserThinkingEffort(
|
||||
modelSlug: string,
|
||||
effort: "standard" | "extended" | "max",
|
||||
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,
|
||||
@@ -889,6 +804,7 @@ interface ChatGptMessage {
|
||||
id: string;
|
||||
author: { role: string };
|
||||
content: { content_type: "text"; parts: string[] };
|
||||
metadata?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -984,7 +900,8 @@ function buildConversationBody(
|
||||
// chatgpt.com history. Disable Temporary Chat only when ChatGPT needs a
|
||||
// durable image conversation (image generation/editing).
|
||||
persistConversation: boolean;
|
||||
thinkingEffort: "standard" | "extended" | "max" | null;
|
||||
thinkingEffort: ChatGptThinkingEffort | null;
|
||||
systemHints: readonly string[];
|
||||
continuation?: ChatGptImageConversationContext | null;
|
||||
}
|
||||
): Record<string, unknown> {
|
||||
@@ -1021,6 +938,8 @@ function buildConversationBody(
|
||||
});
|
||||
}
|
||||
|
||||
const systemHints = options.systemHints;
|
||||
|
||||
const currentUserContent = hasOpenWebUIImageContext(parsed)
|
||||
? "Briefly acknowledge the image result described in the system context. Do not generate, edit, or request another image."
|
||||
: parsed.currentMsg || "";
|
||||
@@ -1029,6 +948,7 @@ function buildConversationBody(
|
||||
id: randomUUID(),
|
||||
author: { role: "user" },
|
||||
content: { content_type: "text", parts: [currentUserContent] },
|
||||
...(systemHints.length > 0 ? { metadata: { system_hints: [...systemHints] } } : {}),
|
||||
});
|
||||
|
||||
return {
|
||||
@@ -1051,6 +971,7 @@ function buildConversationBody(
|
||||
supports_buffering: true,
|
||||
force_parallel_switch: "auto",
|
||||
paragen_cot_summary_display_override: "allow",
|
||||
...(systemHints.length > 0 ? { system_hints: [...systemHints] } : {}),
|
||||
...(options.thinkingEffort ? { thinking_effort: options.thinkingEffort } : {}),
|
||||
};
|
||||
}
|
||||
@@ -2920,24 +2841,6 @@ export class ChatGptWebExecutor extends BaseExecutor {
|
||||
log
|
||||
);
|
||||
|
||||
// 2a''. Apply thinking-effort preference for thinking models.
|
||||
// Dedicated thinking models mirror the browser's user-config PATCH;
|
||||
// GPT-5.5 Pro effort is sent with the conversation body.
|
||||
const requestedEffort = resolvedModel.effort;
|
||||
if (requestedEffort && isThinkingCapableModel(model, modelSlug)) {
|
||||
await setUserThinkingEffort(
|
||||
modelSlug,
|
||||
requestedEffort,
|
||||
tokenEntry.accessToken,
|
||||
tokenEntry.accountId,
|
||||
sessionId,
|
||||
deviceId,
|
||||
cookie,
|
||||
signal,
|
||||
log
|
||||
);
|
||||
}
|
||||
|
||||
// 2b. Sentinel chat-requirements
|
||||
let reqs: ChatRequirements;
|
||||
try {
|
||||
@@ -3019,7 +2922,7 @@ export class ChatGptWebExecutor extends BaseExecutor {
|
||||
}
|
||||
|
||||
// Toggle Temporary Chat off only when ChatGPT needs a durable image
|
||||
// conversation. Text requests, including GPT-5.5 Pro, stay temporary so
|
||||
// conversation. Text requests, including GPT-5.6 Sol Pro, stay temporary so
|
||||
// they do not show up in the user's chatgpt.com sidebar/history.
|
||||
const imageEdit = looksLikeImageEditRequest(parsed);
|
||||
const continuation = imageEdit ? parsed.latestImageContext : null;
|
||||
@@ -3033,13 +2936,14 @@ export class ChatGptWebExecutor extends BaseExecutor {
|
||||
: "Image-gen intent detected — disabling Temporary Chat for this turn"
|
||||
);
|
||||
} else if (resolvedModel.isPro) {
|
||||
log?.debug?.("CGPT-WEB", "GPT-5.5 Pro text request — keeping Temporary Chat enabled");
|
||||
log?.debug?.("CGPT-WEB", "GPT-5.6 Sol Pro text request — keeping Temporary Chat enabled");
|
||||
}
|
||||
|
||||
const parentMessageId = continuation?.parentMessageId ?? randomUUID();
|
||||
const cgptBody = buildConversationBody(parsed, modelSlug, parentMessageId, {
|
||||
persistConversation,
|
||||
thinkingEffort: requestedEffort,
|
||||
thinkingEffort: resolvedModel.effort,
|
||||
systemHints: resolveChatGptSystemHints(model),
|
||||
continuation,
|
||||
});
|
||||
|
||||
@@ -3230,7 +3134,6 @@ function stringToStream(text: string): ReadableStream<Uint8Array> {
|
||||
export function __resetChatGptWebCachesForTesting(): void {
|
||||
tokenCache.clear();
|
||||
warmupCache.clear();
|
||||
thinkingEffortCache.clear();
|
||||
deviceIdCache.clear();
|
||||
__resetChatGptImageCacheForTesting();
|
||||
dplCache = null;
|
||||
|
||||
@@ -3,106 +3,60 @@
|
||||
|
||||
export const MODEL_MAP: Record<string, string> = {
|
||||
// ChatGPT backend slugs are also accepted directly for power users / tests.
|
||||
"gpt-5-6-pro": "gpt-5-6-pro",
|
||||
"gpt-5-6": "gpt-5-6",
|
||||
"gpt-5-6-thinking": "gpt-5-6-thinking",
|
||||
"gpt-5-5-pro": "gpt-5-5-pro",
|
||||
"gpt-5-5-pro-extended": "gpt-5-5-pro",
|
||||
"gpt-5-5-thinking": "gpt-5-5-thinking",
|
||||
"gpt-5-6-pro": "gpt-5-6-pro",
|
||||
"gpt-5-5": "gpt-5-5",
|
||||
"gpt-5-3": "gpt-5-3",
|
||||
"gpt-5-3-mini": "gpt-5-3-mini",
|
||||
"gpt-5-5-thinking": "gpt-5-5-thinking",
|
||||
"gpt-5-5-pro": "gpt-5-5-pro",
|
||||
|
||||
// Public OmniRoute dot-form ids exposed by the provider catalog.
|
||||
"gpt-5.6-pro": "gpt-5-6-pro",
|
||||
"gpt-5.6-thinking": "gpt-5-6-thinking",
|
||||
// Free accounts leave Luna selection to ChatGPT's server-side auto router.
|
||||
"gpt-5.6-luna-free": "auto",
|
||||
"gpt-5.6-luna-free-thinking": "auto",
|
||||
|
||||
// Captured from a real ChatGPT v2 picker conversation. The visible
|
||||
// performance levels select distinct backend model/effort pairs.
|
||||
"gpt-5.6-sol-instant": "gpt-5-6",
|
||||
"gpt-5.6-sol-medium": "gpt-5-6-thinking",
|
||||
"gpt-5.6-sol-high": "gpt-5-6-thinking",
|
||||
"gpt-5.6-sol-xhigh": "gpt-5-6-thinking",
|
||||
"gpt-5.6-sol-pro": "gpt-5-6-pro",
|
||||
|
||||
"gpt-5.5-instant": "gpt-5-5",
|
||||
"gpt-5.5-medium": "gpt-5-5-thinking",
|
||||
"gpt-5.5-high": "gpt-5-5-thinking",
|
||||
"gpt-5.5-xhigh": "gpt-5-5-thinking",
|
||||
"gpt-5.5-pro": "gpt-5-5-pro",
|
||||
"gpt-5.5-pro-extended": "gpt-5-5-pro",
|
||||
"gpt-5.5-thinking": "gpt-5-5-thinking",
|
||||
// Compatibility alias for existing chatgpt-web image integrations. It is
|
||||
// intentionally absent from the provider's visible curated model list.
|
||||
"gpt-5.5": "gpt-5-5",
|
||||
"gpt-5.3-instant": "gpt-5-3-instant",
|
||||
"gpt-5.3": "gpt-5-3",
|
||||
"gpt-5.3-mini": "gpt-5-3-mini",
|
||||
o3: "o3",
|
||||
};
|
||||
|
||||
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",
|
||||
"gpt-5-5-pro-extended": "extended",
|
||||
export const MODEL_FORCED_EFFORT: Record<string, ChatGptThinkingEffort | null> = {
|
||||
"gpt-5.6-sol-instant": null,
|
||||
"gpt-5.6-sol-medium": "standard",
|
||||
"gpt-5.6-sol-high": "extended",
|
||||
"gpt-5.6-sol-xhigh": "max",
|
||||
"gpt-5.6-sol-pro": "standard",
|
||||
"gpt-5.5-instant": null,
|
||||
"gpt-5.5-medium": "standard",
|
||||
"gpt-5.5-high": "extended",
|
||||
"gpt-5.5-xhigh": "max",
|
||||
"gpt-5.5-pro": "standard",
|
||||
"gpt-5.5-pro-extended": "extended",
|
||||
};
|
||||
|
||||
/** 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.
|
||||
*
|
||||
* 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. */
|
||||
export const THINKING_CAPABLE_SLUGS: ReadonlySet<string> = new Set(
|
||||
Object.entries(MODEL_MAP)
|
||||
.filter(([k]) => k.includes("thinking") || k === "o3")
|
||||
.map(([, v]) => v)
|
||||
);
|
||||
const MODEL_SYSTEM_HINTS: Record<string, readonly string[]> = {
|
||||
// Captured from the Free-account Think toggle. ChatGPT sends this both at
|
||||
// the request root and on the user message metadata.
|
||||
"gpt-5.6-luna-free-thinking": ["reason"],
|
||||
};
|
||||
|
||||
/** 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).
|
||||
*
|
||||
* The lookup also catches callers that pass a chatgpt.com slug directly as
|
||||
* the `model` field without MODEL_MAP translation. */
|
||||
export 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`/`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 | extended → extended
|
||||
* xhigh | max → max
|
||||
*
|
||||
* `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 === "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 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
|
||||
): ChatGptThinkingEffort | 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);
|
||||
export function resolveChatGptSystemHints(model: string): string[] {
|
||||
return [...(MODEL_SYSTEM_HINTS[model] ?? [])];
|
||||
}
|
||||
|
||||
export interface ResolvedChatGptModel {
|
||||
@@ -113,12 +67,16 @@ export interface ResolvedChatGptModel {
|
||||
|
||||
export function resolveChatGptModel(
|
||||
model: string,
|
||||
body: unknown,
|
||||
providerSpecificData: Record<string, unknown> | undefined
|
||||
_body?: unknown,
|
||||
_providerSpecificData?: Record<string, unknown>
|
||||
): ResolvedChatGptModel {
|
||||
const slug = MODEL_MAP[model] ?? model;
|
||||
const forcedEffort = MODEL_FORCED_EFFORT[model] ?? null;
|
||||
const effort = forcedEffort ?? resolveThinkingEffort(body, providerSpecificData);
|
||||
const isPro = slug === "gpt-5-6-pro" || slug === "gpt-5-5-pro";
|
||||
const effort = MODEL_FORCED_EFFORT[model] ?? null;
|
||||
const isPro =
|
||||
model === "gpt-5.6-sol-pro" ||
|
||||
model === "gpt-5.5-pro" ||
|
||||
model === "gpt-5.5-pro-extended" ||
|
||||
slug === "gpt-5-6-pro" ||
|
||||
slug === "gpt-5-5-pro";
|
||||
return { slug, effort, isPro };
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
const TOOL_ONLY_SERVICE_KINDS = new Set<string>(["webSearch", "webFetch"]);
|
||||
|
||||
/** Providers whose registry catalog is the complete, intentional model list. */
|
||||
const CURATED_MODEL_ONLY_PROVIDERS = new Set<string>(["kimi-web", "zai-web"]);
|
||||
const CURATED_MODEL_ONLY_PROVIDERS = new Set<string>(["chatgpt-web", "kimi-web", "zai-web"]);
|
||||
|
||||
export function providerUsesCuratedModelsOnly(providerId: string): boolean {
|
||||
return CURATED_MODEL_ONLY_PROVIDERS.has(providerId.trim().toLowerCase());
|
||||
|
||||
@@ -206,6 +206,24 @@ function findSyncedModelMeta(models: unknown, modelId: string): any {
|
||||
return Array.isArray(models) ? models.find((model: any) => model.id === modelId) : undefined;
|
||||
}
|
||||
|
||||
function findLiveCatalogModelMeta(
|
||||
providerId: string,
|
||||
requestedModelId: string,
|
||||
resolvedModelId: string,
|
||||
syncedModels: unknown
|
||||
): any {
|
||||
const directMatch = findSyncedModelMeta(syncedModels, resolvedModelId);
|
||||
if (directMatch || !Array.isArray(syncedModels)) return directMatch;
|
||||
|
||||
const registryModel = findRegistryModel(providerId, requestedModelId);
|
||||
const liveCatalogIds = registryModel?.liveCatalogIds;
|
||||
if (!Array.isArray(liveCatalogIds) || liveCatalogIds.length === 0) return undefined;
|
||||
|
||||
return syncedModels.find(
|
||||
(model: any) => typeof model?.id === "string" && liveCatalogIds.includes(model.id)
|
||||
);
|
||||
}
|
||||
|
||||
function resolveRuntimeFormats(customMatch: any, syncedMatch: any): RuntimeModelMeta {
|
||||
const apiFormat =
|
||||
customMatch?.apiFormat === "responses" || syncedMatch?.apiFormat === "responses"
|
||||
@@ -228,8 +246,10 @@ function copySyncedThinkingMetadata(metadata: RuntimeModelMeta, syncedMatch: any
|
||||
// Only let a non-empty synced effort list override the static registry fallback;
|
||||
// an empty array from an incomplete synced discovery must not erase registry-declared
|
||||
// tiers (#9485 review).
|
||||
if (Array.isArray(syncedMatch?.supportedThinkingEfforts) &&
|
||||
syncedMatch.supportedThinkingEfforts.length > 0) {
|
||||
if (
|
||||
Array.isArray(syncedMatch?.supportedThinkingEfforts) &&
|
||||
syncedMatch.supportedThinkingEfforts.length > 0
|
||||
) {
|
||||
metadata.supportedThinkingEfforts = syncedMatch.supportedThinkingEfforts;
|
||||
}
|
||||
if (typeof syncedMatch?.defaultThinkingEffort === "string") {
|
||||
@@ -300,7 +320,12 @@ async function lookupModelMeta(
|
||||
// Custom models remain explicit operator overrides even when live discovery
|
||||
// is authoritative for the provider.
|
||||
const customMatch = findCustomModelMeta(customModels, resolvedModelId);
|
||||
const syncedMatch = findSyncedModelMeta(syncedModels, resolvedModelId);
|
||||
const syncedMatch = findLiveCatalogModelMeta(
|
||||
providerId,
|
||||
modelId,
|
||||
resolvedModelId,
|
||||
syncedModels
|
||||
);
|
||||
const registryMatch = findRegistryModel(providerId, resolvedModelId);
|
||||
const effortBaseModelId = getRegisteredProviderEffortBaseModelId(providerId, modelId);
|
||||
|
||||
|
||||
@@ -10,12 +10,10 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
const { ChatGptWebExecutor, __resetChatGptWebCachesForTesting } = await import(
|
||||
"../../open-sse/executors/chatgpt-web.ts"
|
||||
);
|
||||
const { __setTlsFetchOverrideForTesting } = await import(
|
||||
"../../open-sse/services/chatgptTlsClient.ts"
|
||||
);
|
||||
const { ChatGptWebExecutor, __resetChatGptWebCachesForTesting } =
|
||||
await import("../../open-sse/executors/chatgpt-web.ts");
|
||||
const { __setTlsFetchOverrideForTesting } =
|
||||
await import("../../open-sse/services/chatgptTlsClient.ts");
|
||||
|
||||
// ─── Minimal TLS-fetch mock ──────────────────────────────────────────────────
|
||||
// Tailored to the citation flow: root/DPL, session→accessToken, sentinel→token
|
||||
@@ -81,7 +79,7 @@ function installMockFetch({
|
||||
if (u.includes("/sentinel/chat-requirements")) {
|
||||
return json({ token: "req-token", proofofwork: { required: false } });
|
||||
}
|
||||
// /backend-api/conversation/<id> — detail poll used by GPT-5.5 Pro handoff.
|
||||
// /backend-api/conversation/<id> — detail poll used by GPT-5.6 Sol Pro handoff.
|
||||
if (conversationDetail) {
|
||||
const m1 = u.match(/\/backend-api\/conversation\/([^/?#]+)$/);
|
||||
if (m1) {
|
||||
@@ -171,7 +169,7 @@ test("Non-streaming: resolves ChatGPT web citation markers into markdown links",
|
||||
try {
|
||||
const executor = new ChatGptWebExecutor();
|
||||
const result = await executor.execute({
|
||||
model: "gpt-5.5-pro-extended",
|
||||
model: "gpt-5.6-sol-pro",
|
||||
body: { messages: [{ role: "user", content: "latest Tesla FSD in Australia" }] },
|
||||
stream: false,
|
||||
credentials: { apiKey: "test" },
|
||||
@@ -260,7 +258,7 @@ test("Streaming: buffers split ChatGPT citation markers until metadata can link
|
||||
try {
|
||||
const executor = new ChatGptWebExecutor();
|
||||
const result = await executor.execute({
|
||||
model: "gpt-5.5-pro-extended",
|
||||
model: "gpt-5.6-sol-pro",
|
||||
body: {
|
||||
messages: [{ role: "user", content: "latest Tesla FSD in Australia" }],
|
||||
stream: true,
|
||||
@@ -297,7 +295,7 @@ test("Streaming: buffers split ChatGPT citation markers until metadata can link
|
||||
}
|
||||
});
|
||||
|
||||
test("GPT-5.5 Pro non-streaming: stream_handoff polls conversation detail for final answer", async () => {
|
||||
test("GPT-5.6 Sol Pro non-streaming: stream_handoff polls conversation detail for final answer", async () => {
|
||||
__resetChatGptWebCachesForTesting();
|
||||
const citationMarker = "citeturn0search0";
|
||||
const m = installMockFetch({
|
||||
@@ -369,7 +367,7 @@ test("GPT-5.5 Pro non-streaming: stream_handoff polls conversation detail for fi
|
||||
try {
|
||||
const executor = new ChatGptWebExecutor();
|
||||
const result = await executor.execute({
|
||||
model: "gpt-5.5-pro-extended",
|
||||
model: "gpt-5.6-sol-pro",
|
||||
body: { messages: [{ role: "user", content: "hard problem" }] },
|
||||
stream: false,
|
||||
credentials: { apiKey: "cookie-pro-poll" },
|
||||
|
||||
@@ -164,8 +164,8 @@ function installHandoffMock(
|
||||
};
|
||||
}
|
||||
|
||||
test("ChatGPT Web Pro models resume Temporary Chat handoffs through native SSE", async (t) => {
|
||||
for (const model of ["gpt-5.6-pro", "gpt-5.5-pro", "gpt-5.5-pro-extended"]) {
|
||||
test("ChatGPT Web GPT-5.6 Sol Pro resumes Temporary Chat handoffs through native SSE", async (t) => {
|
||||
for (const model of ["gpt-5.6-sol-pro"]) {
|
||||
await t.test(model, async () => {
|
||||
__resetChatGptWebCachesForTesting();
|
||||
const expected = `RESUMED_${model}`;
|
||||
@@ -204,7 +204,7 @@ test("ChatGPT Web handoff retries the next resume offset after a 404", async ()
|
||||
try {
|
||||
const executor = new ChatGptWebExecutor();
|
||||
const result = await executor.execute({
|
||||
model: "gpt-5.6-pro",
|
||||
model: "gpt-5.6-sol-pro",
|
||||
body: { messages: [{ role: "user", content: "hard problem" }] },
|
||||
stream: false,
|
||||
credentials: { apiKey: "cookie-offset" },
|
||||
@@ -231,7 +231,7 @@ test("ChatGPT Web streaming appends the native resumed Pro answer", async () =>
|
||||
try {
|
||||
const executor = new ChatGptWebExecutor();
|
||||
const result = await executor.execute({
|
||||
model: "gpt-5.5-pro-extended",
|
||||
model: "gpt-5.6-sol-pro",
|
||||
body: { messages: [{ role: "user", content: "hard problem" }], stream: true },
|
||||
stream: true,
|
||||
credentials: { apiKey: "cookie-stream" },
|
||||
|
||||
@@ -2,158 +2,33 @@ import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
import {
|
||||
normalizeThinkingEffort,
|
||||
resolveChatGptModel,
|
||||
resolveChatGptSystemHints,
|
||||
} 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", () => {
|
||||
test("ChatGPT Web performance lanes use their native model and effort pairs", () => {
|
||||
const cases = [
|
||||
["minimal", "standard"],
|
||||
["low", "standard"],
|
||||
["medium", "standard"],
|
||||
["standard", "standard"],
|
||||
["high", "extended"],
|
||||
["extended", "extended"],
|
||||
["xhigh", "max"],
|
||||
["max", "max"],
|
||||
["gpt-5.6-luna-free", "auto", null, false],
|
||||
["gpt-5.6-luna-free-thinking", "auto", null, false],
|
||||
["gpt-5.6-sol-instant", "gpt-5-6", null, false],
|
||||
["gpt-5.6-sol-medium", "gpt-5-6-thinking", "standard", false],
|
||||
["gpt-5.6-sol-high", "gpt-5-6-thinking", "extended", false],
|
||||
["gpt-5.6-sol-xhigh", "gpt-5-6-thinking", "max", false],
|
||||
["gpt-5.6-sol-pro", "gpt-5-6-pro", "standard", true],
|
||||
["gpt-5.5-instant", "gpt-5-5", null, false],
|
||||
["gpt-5.5-medium", "gpt-5-5-thinking", "standard", false],
|
||||
["gpt-5.5-high", "gpt-5-5-thinking", "extended", false],
|
||||
["gpt-5.5-xhigh", "gpt-5-5-thinking", "max", false],
|
||||
["gpt-5.5-pro", "gpt-5-5-pro", "standard", true],
|
||||
["gpt-5.5-pro-extended", "gpt-5-5-pro", "extended", true],
|
||||
] as const;
|
||||
|
||||
for (const [input, expected] of cases) {
|
||||
assert.equal(normalizeThinkingEffort(input), expected, input);
|
||||
for (const [model, slug, effort, isPro] of cases) {
|
||||
assert.deepEqual(resolveChatGptModel(model), { slug, effort, isPro }, model);
|
||||
}
|
||||
});
|
||||
|
||||
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");
|
||||
test("ChatGPT Web Free Luna Think uses the captured reason system hint", () => {
|
||||
assert.deepEqual(resolveChatGptSystemHints("gpt-5.6-luna-free"), []);
|
||||
assert.deepEqual(resolveChatGptSystemHints("gpt-5.6-luna-free-thinking"), ["reason"]);
|
||||
});
|
||||
|
||||
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();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -5,16 +5,16 @@ import { fileURLToPath } from "node:url";
|
||||
import { dirname, join } from "node:path";
|
||||
|
||||
// Split-guard for the chatgpt-web model-mapping extraction.
|
||||
// The static model maps + pure thinking-effort resolvers live in the pure leaf
|
||||
// chatgpt-web/models.ts (no module state). Host imports the two it uses back.
|
||||
// The static model maps + pure model resolver live in the pure leaf
|
||||
// chatgpt-web/models.ts (no module state). Host imports it back.
|
||||
const HERE = dirname(fileURLToPath(import.meta.url));
|
||||
const EXE = join(HERE, "../../open-sse/executors");
|
||||
const HOST = join(EXE, "chatgpt-web.ts");
|
||||
const LEAF = join(EXE, "chatgpt-web/models.ts");
|
||||
|
||||
test("leaf hosts the model maps + resolvers and does not import the host", () => {
|
||||
test("leaf hosts the model maps + resolver and does not import the host", () => {
|
||||
const src = readFileSync(LEAF, "utf8");
|
||||
for (const sym of ["MODEL_MAP", "resolveChatGptModel", "resolveThinkingEffort"]) {
|
||||
for (const sym of ["MODEL_MAP", "MODEL_FORCED_EFFORT", "resolveChatGptModel"]) {
|
||||
assert.match(src, new RegExp(`export (const|function) ${sym}\\b`));
|
||||
}
|
||||
assert.doesNotMatch(src, /from "\.\.\/chatgpt-web\.ts"/);
|
||||
|
||||
@@ -8,15 +8,13 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
const { ChatGptWebExecutor, __resetChatGptWebCachesForTesting } = await import(
|
||||
"../../open-sse/executors/chatgpt-web.ts"
|
||||
);
|
||||
const { __setTlsFetchOverrideForTesting } = await import(
|
||||
"../../open-sse/services/chatgptTlsClient.ts"
|
||||
);
|
||||
const { ChatGptWebExecutor, __resetChatGptWebCachesForTesting } =
|
||||
await import("../../open-sse/executors/chatgpt-web.ts");
|
||||
const { __setTlsFetchOverrideForTesting } =
|
||||
await import("../../open-sse/services/chatgptTlsClient.ts");
|
||||
|
||||
// ─── Minimal TLS-fetch mock ──────────────────────────────────────────────────
|
||||
// Tailored to the tool-call flow (gpt-5.3-instant, non-thinking): root/DPL,
|
||||
// Tailored to the tool-call flow (gpt-5.5, non-thinking): root/DPL,
|
||||
// session→accessToken, sentinel→token (no PoW), conv→SSE. Warmup GETs fall
|
||||
// through to 404, which the executor tolerates.
|
||||
|
||||
@@ -68,7 +66,10 @@ function installMockFetch(convEvents: unknown[]) {
|
||||
body: null,
|
||||
});
|
||||
|
||||
if ((u === "https://chatgpt.com/" || u === "https://chatgpt.com") && (opts.method || "GET") === "GET") {
|
||||
if (
|
||||
(u === "https://chatgpt.com/" || u === "https://chatgpt.com") &&
|
||||
(opts.method || "GET") === "GET"
|
||||
) {
|
||||
return {
|
||||
status: 200,
|
||||
headers: makeHeaders({ "Content-Type": "text/html" }),
|
||||
@@ -127,7 +128,7 @@ const TOOL_CALL_TEXT = '<tool>{"name":"get_weather","arguments":{"location":"Tok
|
||||
|
||||
function baseOpts(extra: Record<string, unknown>) {
|
||||
return {
|
||||
model: "gpt-5.3-instant",
|
||||
model: "gpt-5.5",
|
||||
credentials: { apiKey: "test" },
|
||||
signal: AbortSignal.timeout(10_000),
|
||||
log: null,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// Tool contract serialization for chatgpt-web thinking models (#7679).
|
||||
// Tool contract serialization for ChatGPT Web performance models (#7679).
|
||||
//
|
||||
// GPT-5.6 Thinking via chatgpt-web ignores the injected `<tool>` pseudo-contract
|
||||
// GPT-5.6 Sol via chatgpt-web ignores the injected `<tool>` pseudo-contract
|
||||
// and replies in prose claiming tools are unavailable. This test covers the
|
||||
// nonce-bound serialization that clearly describes client-side tools and places
|
||||
// the full contract at the tail of the effective message list.
|
||||
@@ -161,7 +161,7 @@ test("parseToolCallsFromText returns null when hardened text has no tool blocks
|
||||
});
|
||||
|
||||
test("parseToolCallsFromText handles <tool> blocks line-boundary crossing in hardened text (#7679)", () => {
|
||||
// Some thinking models may emit the tool block adjacent to explanatory text
|
||||
// Some high-performance lanes may emit the tool block adjacent to explanatory text
|
||||
// with no preceding newline
|
||||
const text = [
|
||||
'I will use the weather tool. <tool>{"name":"get_weather","arguments":{"location":"Paris"}}</tool>',
|
||||
|
||||
@@ -81,13 +81,11 @@ type MockFetchOptions = {
|
||||
attachmentDownload?: MockTlsConfig;
|
||||
conversationDetail?: MockTlsConfig | MockTlsConfig[];
|
||||
signedDownload?: MockTlsConfig;
|
||||
userConfig?: MockTlsConfig;
|
||||
onSession?: (opts: TlsFetchOptions) => void;
|
||||
onSentinel?: (opts: TlsFetchOptions) => void;
|
||||
onConv?: (opts: TlsFetchOptions) => void;
|
||||
onFileDownload?: (opts: TlsFetchOptions, fileId: string) => void;
|
||||
onAttachmentDownload?: (opts: TlsFetchOptions, fileId: string) => void;
|
||||
onUserConfig?: (opts: TlsFetchOptions, url: string) => void;
|
||||
};
|
||||
|
||||
type MockFetchCalls = {
|
||||
@@ -99,9 +97,6 @@ type MockFetchCalls = {
|
||||
attachmentDownload: number;
|
||||
conversationDetail: number;
|
||||
signedDownload: number;
|
||||
userConfig: number;
|
||||
userConfigUrls: string[];
|
||||
userConfigMethods: string[];
|
||||
urls: string[];
|
||||
headers: Array<Record<string, string> | undefined>;
|
||||
bodies: Array<string | undefined>;
|
||||
@@ -118,13 +113,11 @@ function installMockFetch({
|
||||
attachmentDownload,
|
||||
conversationDetail,
|
||||
signedDownload,
|
||||
userConfig,
|
||||
onSession,
|
||||
onSentinel,
|
||||
onConv,
|
||||
onFileDownload,
|
||||
onAttachmentDownload,
|
||||
onUserConfig,
|
||||
}: MockFetchOptions = {}) {
|
||||
const calls: MockFetchCalls = {
|
||||
session: 0,
|
||||
@@ -135,9 +128,6 @@ function installMockFetch({
|
||||
attachmentDownload: 0,
|
||||
conversationDetail: 0,
|
||||
signedDownload: 0,
|
||||
userConfig: 0,
|
||||
userConfigUrls: [],
|
||||
userConfigMethods: [],
|
||||
urls: [],
|
||||
headers: [],
|
||||
bodies: [],
|
||||
@@ -188,22 +178,6 @@ 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);
|
||||
@@ -288,7 +262,7 @@ function installMockFetch({
|
||||
};
|
||||
}
|
||||
|
||||
// /backend-api/conversation/<id> — detail poll used by GPT-5.5 Pro handoff.
|
||||
// /backend-api/conversation/<id> — detail poll used by GPT-5.6 Sol Pro handoff.
|
||||
{
|
||||
const m1 = u.match(/\/backend-api\/conversation\/([^/?#]+)$/);
|
||||
if (m1) {
|
||||
@@ -480,7 +454,7 @@ test("Token exchange: cookie sent to /api/auth/session, accessToken used as Bear
|
||||
try {
|
||||
const executor = new ChatGptWebExecutor();
|
||||
await executor.execute({
|
||||
model: "gpt-5.3-instant",
|
||||
model: "gpt-5.5",
|
||||
body: { messages: [{ role: "user", content: "hi" }] },
|
||||
stream: false,
|
||||
credentials: { apiKey: "my-cookie-value" },
|
||||
@@ -518,7 +492,7 @@ test("Token cache: two calls within TTL only hit /api/auth/session once", async
|
||||
try {
|
||||
const executor = new ChatGptWebExecutor();
|
||||
const opts = {
|
||||
model: "gpt-5.3-instant",
|
||||
model: "gpt-5.5",
|
||||
body: { messages: [{ role: "user", content: "hi" }] },
|
||||
stream: false,
|
||||
credentials: { apiKey: "cookie-v1" },
|
||||
@@ -552,7 +526,7 @@ test("Refreshed cookie: surfaced via onCredentialsRefreshed callback", async ()
|
||||
let refreshed = null;
|
||||
const executor = new ChatGptWebExecutor();
|
||||
await executor.execute({
|
||||
model: "gpt-5.3-instant",
|
||||
model: "gpt-5.5",
|
||||
body: { messages: [{ role: "user", content: "hi" }] },
|
||||
stream: false,
|
||||
credentials: { apiKey: "old-cookie" },
|
||||
@@ -585,7 +559,7 @@ test("Sentinel: chat-requirements is hit before /backend-api/conversation", asyn
|
||||
try {
|
||||
const executor = new ChatGptWebExecutor();
|
||||
await executor.execute({
|
||||
model: "gpt-5.3-instant",
|
||||
model: "gpt-5.5",
|
||||
body: { messages: [{ role: "user", content: "hi" }] },
|
||||
stream: false,
|
||||
credentials: { apiKey: "test" },
|
||||
@@ -606,7 +580,7 @@ test("Sentinel: chat-requirements token forwarded on conv request", async () =>
|
||||
try {
|
||||
const executor = new ChatGptWebExecutor();
|
||||
await executor.execute({
|
||||
model: "gpt-5.3-instant",
|
||||
model: "gpt-5.5",
|
||||
body: { messages: [{ role: "user", content: "hi" }] },
|
||||
stream: false,
|
||||
credentials: { apiKey: "test" },
|
||||
@@ -635,7 +609,7 @@ test("PoW: when required, proof token is sent with valid prefix", async () => {
|
||||
try {
|
||||
const executor = new ChatGptWebExecutor();
|
||||
await executor.execute({
|
||||
model: "gpt-5.3-instant",
|
||||
model: "gpt-5.5",
|
||||
body: { messages: [{ role: "user", content: "hi" }] },
|
||||
stream: false,
|
||||
credentials: { apiKey: "test" },
|
||||
@@ -670,7 +644,7 @@ test("Turnstile: required flag does NOT block — conv endpoint accepts requests
|
||||
try {
|
||||
const executor = new ChatGptWebExecutor();
|
||||
const result = await executor.execute({
|
||||
model: "gpt-5.3-instant",
|
||||
model: "gpt-5.5",
|
||||
body: { messages: [{ role: "user", content: "hi" }] },
|
||||
stream: false,
|
||||
credentials: { apiKey: "test" },
|
||||
@@ -692,7 +666,7 @@ test("Non-streaming: returns OpenAI chat.completion JSON", async () => {
|
||||
try {
|
||||
const executor = new ChatGptWebExecutor();
|
||||
const result = await executor.execute({
|
||||
model: "gpt-5.3-instant",
|
||||
model: "gpt-5.5",
|
||||
body: { messages: [{ role: "user", content: "hi" }] },
|
||||
stream: false,
|
||||
credentials: { apiKey: "test" },
|
||||
@@ -743,7 +717,7 @@ test("Streaming: produces valid SSE chunks ending with [DONE]", async () => {
|
||||
try {
|
||||
const executor = new ChatGptWebExecutor();
|
||||
const result = await executor.execute({
|
||||
model: "gpt-5.3-instant",
|
||||
model: "gpt-5.5",
|
||||
body: { messages: [{ role: "user", content: "hi" }], stream: true },
|
||||
stream: true,
|
||||
credentials: { apiKey: "test" },
|
||||
@@ -807,7 +781,7 @@ test("Streaming: cumulative parts are diffed into non-overlapping deltas", async
|
||||
try {
|
||||
const executor = new ChatGptWebExecutor();
|
||||
const result = await executor.execute({
|
||||
model: "gpt-5.3-instant",
|
||||
model: "gpt-5.5",
|
||||
body: { messages: [{ role: "user", content: "hi" }], stream: true },
|
||||
stream: true,
|
||||
credentials: { apiKey: "test" },
|
||||
@@ -835,7 +809,7 @@ test("Streaming: cumulative parts are diffed into non-overlapping deltas", async
|
||||
}
|
||||
});
|
||||
|
||||
test("GPT-5.5 Pro streaming: preserves interim reasoning and appends final polled answer", async () => {
|
||||
test("GPT-5.6 Sol Pro streaming: preserves interim reasoning and appends final polled answer", async () => {
|
||||
reset();
|
||||
const m = installMockFetch({
|
||||
conv: {
|
||||
@@ -878,7 +852,7 @@ test("GPT-5.5 Pro streaming: preserves interim reasoning and appends final polle
|
||||
try {
|
||||
const executor = new ChatGptWebExecutor();
|
||||
const result = await executor.execute({
|
||||
model: "gpt-5.5-pro-extended",
|
||||
model: "gpt-5.6-sol-pro",
|
||||
body: { messages: [{ role: "user", content: "hard problem" }], stream: true },
|
||||
stream: true,
|
||||
credentials: { apiKey: "cookie-pro-stream" },
|
||||
@@ -907,7 +881,7 @@ test("Error: 401 on /api/auth/session returns 401 with re-paste hint", async ()
|
||||
try {
|
||||
const executor = new ChatGptWebExecutor();
|
||||
const result = await executor.execute({
|
||||
model: "gpt-5.3-instant",
|
||||
model: "gpt-5.5",
|
||||
body: { messages: [{ role: "user", content: "hi" }] },
|
||||
stream: false,
|
||||
credentials: { apiKey: "expired-cookie" },
|
||||
@@ -928,7 +902,7 @@ test("Error: 200 with no accessToken returns 401", async () => {
|
||||
try {
|
||||
const executor = new ChatGptWebExecutor();
|
||||
const result = await executor.execute({
|
||||
model: "gpt-5.3-instant",
|
||||
model: "gpt-5.5",
|
||||
body: { messages: [{ role: "user", content: "hi" }] },
|
||||
stream: false,
|
||||
credentials: { apiKey: "stale-cookie" },
|
||||
@@ -948,7 +922,7 @@ test("Error: 403 from sentinel returns 403 SENTINEL_BLOCKED", async () => {
|
||||
try {
|
||||
const executor = new ChatGptWebExecutor();
|
||||
const result = await executor.execute({
|
||||
model: "gpt-5.3-instant",
|
||||
model: "gpt-5.5",
|
||||
body: { messages: [{ role: "user", content: "hi" }] },
|
||||
stream: false,
|
||||
credentials: { apiKey: "test" },
|
||||
@@ -970,7 +944,7 @@ test("Error: 429 from conversation returns 429 with rate-limit message", async (
|
||||
try {
|
||||
const executor = new ChatGptWebExecutor();
|
||||
const result = await executor.execute({
|
||||
model: "gpt-5.3-instant",
|
||||
model: "gpt-5.5",
|
||||
body: { messages: [{ role: "user", content: "hi" }] },
|
||||
stream: false,
|
||||
credentials: { apiKey: "test" },
|
||||
@@ -991,7 +965,7 @@ test("Error: empty messages returns 400 without any fetch", async () => {
|
||||
try {
|
||||
const executor = new ChatGptWebExecutor();
|
||||
const result = await executor.execute({
|
||||
model: "gpt-5.3-instant",
|
||||
model: "gpt-5.5",
|
||||
body: { messages: [] },
|
||||
stream: false,
|
||||
credentials: { apiKey: "test" },
|
||||
@@ -1011,7 +985,7 @@ test("Error: missing apiKey returns 401 without any fetch", async () => {
|
||||
try {
|
||||
const executor = new ChatGptWebExecutor();
|
||||
const result = await executor.execute({
|
||||
model: "gpt-5.3-instant",
|
||||
model: "gpt-5.5",
|
||||
body: { messages: [{ role: "user", content: "hi" }] },
|
||||
stream: false,
|
||||
credentials: {},
|
||||
@@ -1033,7 +1007,7 @@ test("Cookie: bare value gets prepended with cookie name", async () => {
|
||||
try {
|
||||
const executor = new ChatGptWebExecutor();
|
||||
await executor.execute({
|
||||
model: "gpt-5.3-instant",
|
||||
model: "gpt-5.5",
|
||||
body: { messages: [{ role: "user", content: "hi" }] },
|
||||
stream: false,
|
||||
credentials: { apiKey: "rawValue" },
|
||||
@@ -1052,7 +1026,7 @@ test("Cookie: unchunked cookie line is passed through verbatim", async () => {
|
||||
try {
|
||||
const executor = new ChatGptWebExecutor();
|
||||
await executor.execute({
|
||||
model: "gpt-5.3-instant",
|
||||
model: "gpt-5.5",
|
||||
body: { messages: [{ role: "user", content: "hi" }] },
|
||||
stream: false,
|
||||
credentials: { apiKey: "__Secure-next-auth.session-token=actualvalue" },
|
||||
@@ -1071,7 +1045,7 @@ test("Cookie: chunked .0/.1 cookies are passed through verbatim (NextAuth reasse
|
||||
try {
|
||||
const executor = new ChatGptWebExecutor();
|
||||
await executor.execute({
|
||||
model: "gpt-5.3-instant",
|
||||
model: "gpt-5.5",
|
||||
body: { messages: [{ role: "user", content: "hi" }] },
|
||||
stream: false,
|
||||
credentials: {
|
||||
@@ -1096,7 +1070,7 @@ test("Cookie: 'Cookie: ' DevTools prefix is stripped", async () => {
|
||||
try {
|
||||
const executor = new ChatGptWebExecutor();
|
||||
await executor.execute({
|
||||
model: "gpt-5.3-instant",
|
||||
model: "gpt-5.5",
|
||||
body: { messages: [{ role: "user", content: "hi" }] },
|
||||
stream: false,
|
||||
credentials: {
|
||||
@@ -1127,7 +1101,7 @@ test("Session continuity: each call starts a fresh conversation (Temporary Chat
|
||||
try {
|
||||
const executor = new ChatGptWebExecutor();
|
||||
await executor.execute({
|
||||
model: "gpt-5.3-instant",
|
||||
model: "gpt-5.5",
|
||||
body: { messages: [{ role: "user", content: "First question" }] },
|
||||
stream: false,
|
||||
credentials: { apiKey: "test" },
|
||||
@@ -1135,7 +1109,7 @@ test("Session continuity: each call starts a fresh conversation (Temporary Chat
|
||||
log: null,
|
||||
});
|
||||
await executor.execute({
|
||||
model: "gpt-5.3-instant",
|
||||
model: "gpt-5.5",
|
||||
body: {
|
||||
messages: [
|
||||
{ role: "user", content: "First question" },
|
||||
@@ -1178,7 +1152,7 @@ test("Request: conversation POST has correct browser-like headers", async () =>
|
||||
try {
|
||||
const executor = new ChatGptWebExecutor();
|
||||
await executor.execute({
|
||||
model: "gpt-5.3-instant",
|
||||
model: "gpt-5.5",
|
||||
body: { messages: [{ role: "user", content: "hi" }] },
|
||||
stream: false,
|
||||
credentials: { apiKey: "test" },
|
||||
@@ -1204,7 +1178,7 @@ test("Request: payload has correct ChatGPT shape", async () => {
|
||||
try {
|
||||
const executor = new ChatGptWebExecutor();
|
||||
await executor.execute({
|
||||
model: "gpt-5.3-instant",
|
||||
model: "gpt-5.5",
|
||||
body: {
|
||||
messages: [
|
||||
{ role: "system", content: "Be concise" },
|
||||
@@ -1219,7 +1193,7 @@ test("Request: payload has correct ChatGPT shape", async () => {
|
||||
const convIdx = m.calls.urls.findIndex((u) => u.endsWith("/backend-api/f/conversation"));
|
||||
const body = JSON.parse(m.calls.bodies[convIdx]);
|
||||
assert.equal(body.action, "next");
|
||||
assert.equal(body.model, "gpt-5-3-instant");
|
||||
assert.equal(body.model, "gpt-5-5");
|
||||
// Plain text request → Temporary Chat stays ON. We disable it only for
|
||||
// image-gen prompts (see "Image gen: image-intent prompts" tests below).
|
||||
assert.equal(body.history_and_training_disabled, true);
|
||||
@@ -1245,26 +1219,23 @@ test("Provider registry: chatgpt-web exposes the current ChatGPT Web model catal
|
||||
assert.equal(entry.authHeader, "cookie");
|
||||
|
||||
const ids = (entry.models || []).map((m) => m.id);
|
||||
// Retired GPT-5.4 and older entries stay out of the advertised catalog.
|
||||
// Free accounts expose Luna with an optional Think toggle; paid accounts
|
||||
// expose five GPT-5.6 Sol performance lanes plus GPT-5.5.
|
||||
assert.deepEqual(ids, [
|
||||
"gpt-5.6-pro",
|
||||
"gpt-5.6-thinking",
|
||||
"gpt-5.6-sol-pro",
|
||||
"gpt-5.6-sol-xhigh",
|
||||
"gpt-5.6-sol-high",
|
||||
"gpt-5.6-sol-medium",
|
||||
"gpt-5.6-sol-instant",
|
||||
"gpt-5.6-luna-free-thinking",
|
||||
"gpt-5.6-luna-free",
|
||||
"gpt-5.5-pro-extended",
|
||||
"gpt-5.5-pro",
|
||||
"gpt-5.5-thinking",
|
||||
"gpt-5.5",
|
||||
"o3",
|
||||
"gpt-5.5-xhigh",
|
||||
"gpt-5.5-high",
|
||||
"gpt-5.5-medium",
|
||||
"gpt-5.5-instant",
|
||||
]);
|
||||
assert.equal(
|
||||
ids.some((id) => id.startsWith("gpt-5.4")),
|
||||
false
|
||||
);
|
||||
|
||||
const { MODEL_MAP } = await import("../../open-sse/executors/chatgpt-web/models.ts");
|
||||
assert.equal(
|
||||
Object.keys(MODEL_MAP).some((id) => id.startsWith("gpt-5.4") || id.startsWith("gpt-5-4")),
|
||||
false
|
||||
);
|
||||
});
|
||||
|
||||
test("Executor MODEL_MAP: OmniRoute IDs translate to ChatGPT backend slugs", async () => {
|
||||
@@ -1273,19 +1244,22 @@ test("Executor MODEL_MAP: OmniRoute IDs translate to ChatGPT backend slugs", asy
|
||||
try {
|
||||
const cases: Array<[string, string]> = [
|
||||
// Public catalog ids.
|
||||
["gpt-5.6-pro", "gpt-5-6-pro"],
|
||||
["gpt-5.6-thinking", "gpt-5-6-thinking"],
|
||||
["gpt-5.5-thinking", "gpt-5-5-thinking"],
|
||||
["gpt-5.5", "gpt-5-5"],
|
||||
["gpt-5.6-luna-free", "auto"],
|
||||
["gpt-5.6-luna-free-thinking", "auto"],
|
||||
["gpt-5.6-sol-instant", "gpt-5-6"],
|
||||
["gpt-5.6-sol-medium", "gpt-5-6-thinking"],
|
||||
["gpt-5.6-sol-high", "gpt-5-6-thinking"],
|
||||
["gpt-5.6-sol-xhigh", "gpt-5-6-thinking"],
|
||||
["gpt-5.6-sol-pro", "gpt-5-6-pro"],
|
||||
["gpt-5.5-instant", "gpt-5-5"],
|
||||
["gpt-5.5-medium", "gpt-5-5-thinking"],
|
||||
["gpt-5.5-high", "gpt-5-5-thinking"],
|
||||
["gpt-5.5-xhigh", "gpt-5-5-thinking"],
|
||||
["gpt-5.5-pro", "gpt-5-5-pro"],
|
||||
["gpt-5.5-pro-extended", "gpt-5-5-pro"],
|
||||
["o3", "o3"],
|
||||
// Backend dash-form slugs are still accepted for direct provider/model callers.
|
||||
["gpt-5-3", "gpt-5-3"],
|
||||
["gpt-5-5-thinking", "gpt-5-5-thinking"],
|
||||
["gpt-5-6-pro", "gpt-5-6-pro"],
|
||||
["gpt-5-5-pro", "gpt-5-5-pro"],
|
||||
["gpt-5-5-pro-extended", "gpt-5-5-pro"],
|
||||
["gpt-5-6", "gpt-5-6"],
|
||||
["gpt-5-5", "gpt-5-5"],
|
||||
];
|
||||
for (const [omniId, expectedSlug] of cases) {
|
||||
m.calls.urls.length = 0;
|
||||
@@ -1308,18 +1282,58 @@ test("Executor MODEL_MAP: OmniRoute IDs translate to ChatGPT backend slugs", asy
|
||||
}
|
||||
});
|
||||
|
||||
test("GPT-5.6 Luna Free Think sends the captured auto-router reason hints", async () => {
|
||||
reset();
|
||||
const m = installMockFetch();
|
||||
try {
|
||||
const executor = new ChatGptWebExecutor();
|
||||
for (const [model, expectedHints] of [
|
||||
["gpt-5.6-luna-free", undefined],
|
||||
["gpt-5.6-luna-free-thinking", ["reason"]],
|
||||
] as const) {
|
||||
m.calls.urls.length = 0;
|
||||
m.calls.bodies.length = 0;
|
||||
await executor.execute({
|
||||
model,
|
||||
body: { messages: [{ role: "user", content: "hi" }] },
|
||||
stream: false,
|
||||
credentials: { apiKey: "cookie-free-luna" },
|
||||
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]);
|
||||
const userMessage = body.messages.find(
|
||||
(message: { author?: { role?: string } }) => message.author?.role === "user"
|
||||
);
|
||||
|
||||
assert.equal(body.model, "auto");
|
||||
assert.deepEqual(body.system_hints, expectedHints);
|
||||
assert.deepEqual(userMessage?.metadata?.system_hints, expectedHints);
|
||||
}
|
||||
} finally {
|
||||
m.restore();
|
||||
}
|
||||
});
|
||||
|
||||
test("MODEL_MAP drift guard: every advertised catalog id reaches ChatGPT as a backend slug", async () => {
|
||||
reset();
|
||||
const { getRegistryEntry } = await import("../../open-sse/config/providerRegistry.ts");
|
||||
const ids = (getRegistryEntry("chatgpt-web")?.models || []).map((m) => m.id);
|
||||
const expectedSlugById: Record<string, string> = {
|
||||
"gpt-5.6-pro": "gpt-5-6-pro",
|
||||
"gpt-5.6-thinking": "gpt-5-6-thinking",
|
||||
"gpt-5.5-pro-extended": "gpt-5-5-pro",
|
||||
"gpt-5.6-luna-free": "auto",
|
||||
"gpt-5.6-luna-free-thinking": "auto",
|
||||
"gpt-5.6-sol-instant": "gpt-5-6",
|
||||
"gpt-5.6-sol-medium": "gpt-5-6-thinking",
|
||||
"gpt-5.6-sol-high": "gpt-5-6-thinking",
|
||||
"gpt-5.6-sol-xhigh": "gpt-5-6-thinking",
|
||||
"gpt-5.6-sol-pro": "gpt-5-6-pro",
|
||||
"gpt-5.5-instant": "gpt-5-5",
|
||||
"gpt-5.5-medium": "gpt-5-5-thinking",
|
||||
"gpt-5.5-high": "gpt-5-5-thinking",
|
||||
"gpt-5.5-xhigh": "gpt-5-5-thinking",
|
||||
"gpt-5.5-pro": "gpt-5-5-pro",
|
||||
"gpt-5.5-thinking": "gpt-5-5-thinking",
|
||||
"gpt-5.5": "gpt-5-5",
|
||||
o3: "o3",
|
||||
"gpt-5.5-pro-extended": "gpt-5-5-pro",
|
||||
};
|
||||
const m = installMockFetch();
|
||||
try {
|
||||
@@ -1348,15 +1362,15 @@ test("MODEL_MAP drift guard: every advertised catalog id reaches ChatGPT as a ba
|
||||
}
|
||||
});
|
||||
|
||||
// ─── thinking_effort PATCH user_last_used_model_config ─────────────────────
|
||||
// ─── GPT-5.6 Sol picker request contract ──────────────────────────────────
|
||||
|
||||
test("GPT-5.5 Pro Extended sends base slug with extended effort and Temporary Chat", async () => {
|
||||
test("GPT-5.6 Sol XHigh sends the captured thinking-model/max pair", async () => {
|
||||
reset();
|
||||
const m = installMockFetch();
|
||||
try {
|
||||
const executor = new ChatGptWebExecutor();
|
||||
const result = await executor.execute({
|
||||
model: "gpt-5.5-pro-extended",
|
||||
model: "gpt-5.6-sol-xhigh",
|
||||
body: { messages: [{ role: "user", content: "hi" }] },
|
||||
stream: false,
|
||||
credentials: { apiKey: "cookie-pro-extended" },
|
||||
@@ -1366,26 +1380,25 @@ test("GPT-5.5 Pro Extended sends base slug with extended effort and Temporary Ch
|
||||
assert.equal(result.response.status, 200);
|
||||
const convIdx = m.calls.urls.findIndex((u) => u.endsWith("/backend-api/f/conversation"));
|
||||
const body = JSON.parse(m.calls.bodies[convIdx]);
|
||||
assert.equal(body.model, "gpt-5-5-pro");
|
||||
assert.equal(body.thinking_effort, "extended");
|
||||
assert.equal(body.model, "gpt-5-6-thinking");
|
||||
assert.equal(body.thinking_effort, "max");
|
||||
assert.equal(body.history_and_training_disabled, true);
|
||||
assert.equal(
|
||||
m.calls.userConfig,
|
||||
0,
|
||||
"Pro effort is sent with the turn, not PATCHed as a thinking-model preference"
|
||||
assert.ok(
|
||||
!m.calls.urls.some((url) => url.includes("/settings/user_last_used_model_config")),
|
||||
"the captured browser request uses no settings PATCH"
|
||||
);
|
||||
} finally {
|
||||
m.restore();
|
||||
}
|
||||
});
|
||||
|
||||
test("GPT-5.5 Pro standard sends standard effort", async () => {
|
||||
test("GPT-5.6 Sol High sends the captured thinking-model/extended pair", async () => {
|
||||
reset();
|
||||
const m = installMockFetch();
|
||||
try {
|
||||
const executor = new ChatGptWebExecutor();
|
||||
await executor.execute({
|
||||
model: "gpt-5.5-pro",
|
||||
model: "gpt-5.6-sol-high",
|
||||
body: { messages: [{ role: "user", content: "hi" }] },
|
||||
stream: false,
|
||||
credentials: { apiKey: "cookie-pro-standard" },
|
||||
@@ -1394,21 +1407,21 @@ test("GPT-5.5 Pro standard sends standard effort", async () => {
|
||||
});
|
||||
const convIdx = m.calls.urls.findIndex((u) => u.endsWith("/backend-api/f/conversation"));
|
||||
const body = JSON.parse(m.calls.bodies[convIdx]);
|
||||
assert.equal(body.model, "gpt-5-5-pro");
|
||||
assert.equal(body.thinking_effort, "standard");
|
||||
assert.equal(body.model, "gpt-5-6-thinking");
|
||||
assert.equal(body.thinking_effort, "extended");
|
||||
assert.equal(body.history_and_training_disabled, true);
|
||||
} finally {
|
||||
m.restore();
|
||||
}
|
||||
});
|
||||
|
||||
test("GPT-5.5 Pro store:false keeps Temporary Chat enabled for background utility calls", async () => {
|
||||
test("GPT-5.6 Sol XHigh store:false keeps Temporary Chat enabled", async () => {
|
||||
reset();
|
||||
const m = installMockFetch();
|
||||
try {
|
||||
const executor = new ChatGptWebExecutor();
|
||||
const result = await executor.execute({
|
||||
model: "gpt-5.5-pro-extended",
|
||||
model: "gpt-5.6-sol-xhigh",
|
||||
body: {
|
||||
store: false,
|
||||
messages: [
|
||||
@@ -1424,8 +1437,8 @@ test("GPT-5.5 Pro store:false keeps Temporary Chat enabled for background utilit
|
||||
assert.equal(result.response.status, 200);
|
||||
const convIdx = m.calls.urls.findIndex((u) => u.endsWith("/backend-api/f/conversation"));
|
||||
const body = JSON.parse(m.calls.bodies[convIdx]);
|
||||
assert.equal(body.model, "gpt-5-5-pro");
|
||||
assert.equal(body.thinking_effort, "extended");
|
||||
assert.equal(body.model, "gpt-5-6-thinking");
|
||||
assert.equal(body.thinking_effort, "max");
|
||||
assert.equal(body.history_and_training_disabled, true);
|
||||
assert.equal(
|
||||
m.calls.conversationDetail,
|
||||
@@ -1437,242 +1450,6 @@ test("GPT-5.5 Pro store:false keeps Temporary Chat enabled for background utilit
|
||||
}
|
||||
});
|
||||
|
||||
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.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(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-6-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 thinking slugs still PATCH", async () => {
|
||||
for (const bareSlug of ["gpt-5-6-thinking", "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.ok(
|
||||
m.calls.userConfigUrls[0].includes(`model_slug=${bareSlug}`),
|
||||
`URL should contain model_slug=${bareSlug}`
|
||||
);
|
||||
} 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.6-thinking",
|
||||
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-6-thinking/);
|
||||
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.5-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-5-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.5 routes to ChatGPT Web image handler", async () => {
|
||||
const { parseImageModel, getImageProvider } =
|
||||
await import("../../open-sse/config/imageRegistry.ts");
|
||||
@@ -1709,7 +1486,7 @@ test("Cookie rotation: full DevTools blob keeps cf_clearance/__cf_bm/_cfuvid", a
|
||||
let refreshed = null;
|
||||
const executor = new ChatGptWebExecutor();
|
||||
await executor.execute({
|
||||
model: "gpt-5.3-instant",
|
||||
model: "gpt-5.5",
|
||||
body: { messages: [{ role: "user", content: "hi" }] },
|
||||
stream: false,
|
||||
credentials: {
|
||||
@@ -1764,7 +1541,7 @@ test("Cookie rotation: unchunked → chunked drops stale unchunked variant", asy
|
||||
let refreshed = null;
|
||||
const executor = new ChatGptWebExecutor();
|
||||
await executor.execute({
|
||||
model: "gpt-5.3-instant",
|
||||
model: "gpt-5.5",
|
||||
body: { messages: [{ role: "user", content: "hi" }] },
|
||||
stream: false,
|
||||
credentials: {
|
||||
@@ -1811,7 +1588,7 @@ test("Cookie rotation: chunked → unchunked drops stale chunks", async () => {
|
||||
let refreshed = null;
|
||||
const executor = new ChatGptWebExecutor();
|
||||
await executor.execute({
|
||||
model: "gpt-5.3-instant",
|
||||
model: "gpt-5.5",
|
||||
body: { messages: [{ role: "user", content: "hi" }] },
|
||||
stream: false,
|
||||
credentials: {
|
||||
@@ -1855,7 +1632,7 @@ test("Cookie rotation: returns null when Set-Cookie has no session-token", async
|
||||
let refreshed = null;
|
||||
const executor = new ChatGptWebExecutor();
|
||||
await executor.execute({
|
||||
model: "gpt-5.3-instant",
|
||||
model: "gpt-5.5",
|
||||
body: { messages: [{ role: "user", content: "hi" }] },
|
||||
stream: false,
|
||||
credentials: { apiKey: "cookie-v1" },
|
||||
@@ -1919,7 +1696,7 @@ test("Stream parser: echoed prior assistant turn is suppressed (streaming)", asy
|
||||
try {
|
||||
const executor = new ChatGptWebExecutor();
|
||||
const result = await executor.execute({
|
||||
model: "gpt-5.3-instant",
|
||||
model: "gpt-5.5",
|
||||
body: { messages: [{ role: "user", content: "hi" }], stream: true },
|
||||
stream: true,
|
||||
credentials: { apiKey: "test" },
|
||||
@@ -1979,7 +1756,7 @@ test("Stream parser: echoed prior assistant turn is suppressed (non-streaming)",
|
||||
try {
|
||||
const executor = new ChatGptWebExecutor();
|
||||
const result = await executor.execute({
|
||||
model: "gpt-5.3-instant",
|
||||
model: "gpt-5.5",
|
||||
body: { messages: [{ role: "user", content: "hi" }] },
|
||||
stream: false,
|
||||
credentials: { apiKey: "test" },
|
||||
@@ -2017,7 +1794,7 @@ test("Stream parser: instant single-event reply still surfaces via fallback", as
|
||||
try {
|
||||
const executor = new ChatGptWebExecutor();
|
||||
const result = await executor.execute({
|
||||
model: "gpt-5.3-instant",
|
||||
model: "gpt-5.5",
|
||||
body: { messages: [{ role: "user", content: "hi" }] },
|
||||
stream: false,
|
||||
credentials: { apiKey: "test" },
|
||||
@@ -2083,7 +1860,7 @@ test("Error: TlsClientUnavailableError returns 502 with TLS_UNAVAILABLE code", a
|
||||
try {
|
||||
const executor = new ChatGptWebExecutor();
|
||||
const result = await executor.execute({
|
||||
model: "gpt-5.3-instant",
|
||||
model: "gpt-5.5",
|
||||
body: { messages: [{ role: "user", content: "hi" }] },
|
||||
stream: false,
|
||||
credentials: { apiKey: "test" },
|
||||
@@ -2211,7 +1988,7 @@ test("Image gen: file-service:// pointer resolves to download URL and is appende
|
||||
try {
|
||||
const executor = new ChatGptWebExecutor();
|
||||
const result = await executor.execute({
|
||||
model: "gpt-5.3-instant",
|
||||
model: "gpt-5.5",
|
||||
body: { messages: [{ role: "user", content: "generate an image of a kitten" }] },
|
||||
stream: false,
|
||||
credentials: { apiKey: "test" },
|
||||
@@ -2245,7 +2022,7 @@ test("Image gen: file-service:// pointer is appended in streaming SSE", async ()
|
||||
try {
|
||||
const executor = new ChatGptWebExecutor();
|
||||
const result = await executor.execute({
|
||||
model: "gpt-5.3-instant",
|
||||
model: "gpt-5.5",
|
||||
body: { messages: [{ role: "user", content: "draw a kitten" }] },
|
||||
stream: true,
|
||||
credentials: { apiKey: "test" },
|
||||
@@ -2279,7 +2056,7 @@ test("Image gen: sediment:// pointer prefers /files/<id>/download over /attachme
|
||||
try {
|
||||
const executor = new ChatGptWebExecutor();
|
||||
const result = await executor.execute({
|
||||
model: "gpt-5.3-instant",
|
||||
model: "gpt-5.5",
|
||||
body: { messages: [{ role: "user", content: "make a kitten" }] },
|
||||
stream: false,
|
||||
credentials: { apiKey: "test" },
|
||||
@@ -2324,7 +2101,7 @@ test("Image gen: failed download URL is dropped silently — no broken markdown"
|
||||
try {
|
||||
const executor = new ChatGptWebExecutor();
|
||||
const result = await executor.execute({
|
||||
model: "gpt-5.3-instant",
|
||||
model: "gpt-5.5",
|
||||
body: { messages: [{ role: "user", content: "kitten" }] },
|
||||
stream: false,
|
||||
credentials: { apiKey: "test" },
|
||||
@@ -2348,7 +2125,7 @@ test("Image gen: image-intent prompt disables Temporary Chat", async () => {
|
||||
try {
|
||||
const executor = new ChatGptWebExecutor();
|
||||
await executor.execute({
|
||||
model: "gpt-5.3-instant",
|
||||
model: "gpt-5.5",
|
||||
body: { messages: [{ role: "user", content: "generate an image of a kitten" }] },
|
||||
stream: false,
|
||||
credentials: { apiKey: "test" },
|
||||
@@ -2369,7 +2146,7 @@ test("Image gen: text-only prompt keeps Temporary Chat ON", async () => {
|
||||
try {
|
||||
const executor = new ChatGptWebExecutor();
|
||||
await executor.execute({
|
||||
model: "gpt-5.3-instant",
|
||||
model: "gpt-5.5",
|
||||
body: { messages: [{ role: "user", content: "what is the capital of France?" }] },
|
||||
stream: false,
|
||||
credentials: { apiKey: "test" },
|
||||
@@ -2396,7 +2173,7 @@ test("Image gen: Open WebUI follow-up/title/tag tool prompts do NOT trigger imag
|
||||
try {
|
||||
const executor = new ChatGptWebExecutor();
|
||||
await executor.execute({
|
||||
model: "gpt-5.3-instant",
|
||||
model: "gpt-5.5",
|
||||
body: { messages: [{ role: "user", content: prompt }] },
|
||||
stream: false,
|
||||
credentials: { apiKey: "test" },
|
||||
@@ -2429,7 +2206,7 @@ test("Image gen: Open WebUI image-generation context suppresses duplicate chat i
|
||||
try {
|
||||
const executor = new ChatGptWebExecutor();
|
||||
await executor.execute({
|
||||
model: "gpt-5.3-instant",
|
||||
model: "gpt-5.5",
|
||||
body: {
|
||||
messages: [
|
||||
{ role: "system", content: context },
|
||||
@@ -2477,7 +2254,7 @@ test("Image gen: heuristic catches common phrasings", async () => {
|
||||
try {
|
||||
const executor = new ChatGptWebExecutor();
|
||||
await executor.execute({
|
||||
model: "gpt-5.3-instant",
|
||||
model: "gpt-5.5",
|
||||
body: { messages: [{ role: "user", content: phrase }] },
|
||||
stream: false,
|
||||
credentials: { apiKey: "test" },
|
||||
@@ -2598,7 +2375,7 @@ test("Image gen: signed URL bytes are cached and exposed via /v1/chatgpt-web/ima
|
||||
try {
|
||||
const executor = new ChatGptWebExecutor();
|
||||
const result = await executor.execute({
|
||||
model: "gpt-5.3-instant",
|
||||
model: "gpt-5.5",
|
||||
body: { messages: [{ role: "user", content: "draw kitten" }] },
|
||||
stream: false,
|
||||
credentials: { apiKey: "test" },
|
||||
@@ -2648,7 +2425,7 @@ test("Image gen: prior data: image URIs are stripped from history before upstrea
|
||||
const assistantMsg = `Sure, here you go:\n\n\n`;
|
||||
const executor = new ChatGptWebExecutor();
|
||||
await executor.execute({
|
||||
model: "gpt-5.3-instant",
|
||||
model: "gpt-5.5",
|
||||
body: {
|
||||
messages: [
|
||||
{ role: "user", content: "draw a kitten" },
|
||||
@@ -2686,7 +2463,7 @@ test("Image edit: cached OmniRoute image URL continues the saved ChatGPT convers
|
||||
try {
|
||||
const executor = new ChatGptWebExecutor();
|
||||
await executor.execute({
|
||||
model: "gpt-5.3-instant",
|
||||
model: "gpt-5.5",
|
||||
body: {
|
||||
messages: [
|
||||
{ role: "user", content: "draw a kitten" },
|
||||
@@ -2726,7 +2503,7 @@ test("Image edit: Open WebUI image context suppresses duplicate edit continuatio
|
||||
try {
|
||||
const executor = new ChatGptWebExecutor();
|
||||
await executor.execute({
|
||||
model: "gpt-5.3-instant",
|
||||
model: "gpt-5.5",
|
||||
body: {
|
||||
messages: [
|
||||
{
|
||||
@@ -2798,7 +2575,7 @@ test("Image gen: dedupes the same pointer across in-progress + finished events",
|
||||
try {
|
||||
const executor = new ChatGptWebExecutor();
|
||||
const result = await executor.execute({
|
||||
model: "gpt-5.3-instant",
|
||||
model: "gpt-5.5",
|
||||
body: { messages: [{ role: "user", content: "kitten" }] },
|
||||
stream: false,
|
||||
credentials: { apiKey: "test" },
|
||||
@@ -2829,7 +2606,7 @@ test("Image gen: bytes-fetch failure drops markdown (no signed-URL fallback)", a
|
||||
try {
|
||||
const executor = new ChatGptWebExecutor();
|
||||
const result = await executor.execute({
|
||||
model: "gpt-5.3-instant",
|
||||
model: "gpt-5.5",
|
||||
body: { messages: [{ role: "user", content: "draw a kitten" }] },
|
||||
stream: false,
|
||||
credentials: { apiKey: "test" },
|
||||
@@ -2903,7 +2680,7 @@ test("Image edit: file_0000XXXX (chatgpt-web edit result) falls back to /convers
|
||||
try {
|
||||
const executor = new ChatGptWebExecutor();
|
||||
const result = await executor.execute({
|
||||
model: "gpt-5.3-instant",
|
||||
model: "gpt-5.5",
|
||||
body: { messages: [{ role: "user", content: "now make it nighttime" }] },
|
||||
stream: false,
|
||||
credentials: { apiKey: "test" },
|
||||
@@ -2965,7 +2742,7 @@ test("Image gen: ChatGPT-internal tool_invoked metadata does NOT spuriously trig
|
||||
try {
|
||||
const executor = new ChatGptWebExecutor();
|
||||
const result = await executor.execute({
|
||||
model: "gpt-5.3-instant",
|
||||
model: "gpt-5.5",
|
||||
body: { messages: [{ role: "user", content: "limitations of gpt-4o-mini?" }] },
|
||||
stream: true,
|
||||
credentials: { apiKey: "test" },
|
||||
@@ -3015,7 +2792,7 @@ test("Image edit handler: bytes-hash match drives executor with cached conversat
|
||||
const { handleImageEdit } = await import("../../open-sse/handlers/imageGeneration.ts");
|
||||
const result = await handleImageEdit({
|
||||
provider: "chatgpt-web",
|
||||
model: "gpt-5.3-instant",
|
||||
model: "gpt-5.5",
|
||||
body: { prompt: "turn it to day time" },
|
||||
imageBytes: sourceBytes,
|
||||
credentials: { apiKey: "test" },
|
||||
@@ -3053,7 +2830,7 @@ test("Image edit handler: no cached match returns 400 (does not silently generat
|
||||
const foreignBytes = Buffer.from([0xff, 0xd8, 0xff, 0xe0, 0xde, 0xad, 0xbe, 0xef]);
|
||||
const result = await handleImageEdit({
|
||||
provider: "chatgpt-web",
|
||||
model: "gpt-5.3-instant",
|
||||
model: "gpt-5.5",
|
||||
body: { prompt: "turn it to day time" },
|
||||
imageBytes: foreignBytes,
|
||||
credentials: { apiKey: "test" },
|
||||
@@ -3082,7 +2859,7 @@ test("Image gen handler: n>4 is rejected before any upstream call", async () =>
|
||||
try {
|
||||
const { handleImageGeneration } = await import("../../open-sse/handlers/imageGeneration.ts");
|
||||
const result = await handleImageGeneration({
|
||||
body: { prompt: "draw a kitten", n: 5, model: "cgpt-web/gpt-5.3-instant" },
|
||||
body: { prompt: "draw a kitten", n: 5, model: "cgpt-web/gpt-5.5" },
|
||||
credentials: { apiKey: "test" },
|
||||
log: null,
|
||||
});
|
||||
|
||||
@@ -202,3 +202,45 @@ test("#8926: partial passthrough discovery remains non-authoritative", async ()
|
||||
["gpt-5.6-luna"]
|
||||
);
|
||||
});
|
||||
|
||||
test("ChatGPT Web curated variants require their mapped upstream live slug", async () => {
|
||||
const variants = new Map([
|
||||
["gpt-5.6-sol-pro", "gpt-5-6-pro"],
|
||||
["gpt-5.6-sol-xhigh", "gpt-5-6-thinking"],
|
||||
["gpt-5.6-sol-high", "gpt-5-6-thinking"],
|
||||
["gpt-5.6-sol-medium", "gpt-5-6-thinking"],
|
||||
["gpt-5.6-sol-instant", "gpt-5-6"],
|
||||
["gpt-5.6-luna-free-thinking", "gpt-5-6"],
|
||||
["gpt-5.6-luna-free", "gpt-5-6"],
|
||||
["gpt-5.5-pro-extended", "gpt-5-5-pro"],
|
||||
["gpt-5.5-pro", "gpt-5-5-pro"],
|
||||
["gpt-5.5-xhigh", "gpt-5-5-thinking"],
|
||||
["gpt-5.5-high", "gpt-5-5-thinking"],
|
||||
["gpt-5.5-medium", "gpt-5-5-thinking"],
|
||||
["gpt-5.5-instant", "gpt-5-5"],
|
||||
]);
|
||||
|
||||
await seedProviderCatalog(
|
||||
"chatgpt-web",
|
||||
"chatgpt-web-live-8926",
|
||||
Array.from(new Set(variants.values()))
|
||||
);
|
||||
|
||||
const catalog = await getActiveSyncedCatalog("chatgpt-web");
|
||||
assert.equal(catalog.authoritative, true);
|
||||
|
||||
for (const modelId of variants.keys()) {
|
||||
const resolved = await getModelInfo(`chatgpt-web/${modelId}`);
|
||||
assert.equal(resolved.provider, "chatgpt-web", modelId);
|
||||
assert.equal(resolved.model, modelId, modelId);
|
||||
}
|
||||
|
||||
await seedProviderCatalog("chatgpt-web", "chatgpt-web-live-8926", ["gpt-5-6"]);
|
||||
|
||||
const available = await getModelInfo("chatgpt-web/gpt-5.6-sol-instant");
|
||||
assert.equal(available.provider, "chatgpt-web");
|
||||
|
||||
const unavailable = await getModelInfo("chatgpt-web/gpt-5.6-sol-pro");
|
||||
assert.equal(unavailable.provider, null);
|
||||
assert.equal(unavailable.errorType, "model_not_found");
|
||||
});
|
||||
|
||||
@@ -33,8 +33,10 @@ describe("providerLacksModelListing (#5420)", () => {
|
||||
it("keeps curated web providers visible while disabling remote model import", () => {
|
||||
assert.equal(providerLacksModelListing("kimi-web", ["llm"]), false);
|
||||
assert.equal(providerLacksModelListing("zai-web", ["llm"]), false);
|
||||
assert.equal(providerLacksModelListing("chatgpt-web", ["llm"]), false);
|
||||
assert.equal(providerUsesCuratedModelsOnly("kimi-web"), true);
|
||||
assert.equal(providerUsesCuratedModelsOnly("zai-web"), true);
|
||||
assert.equal(providerUsesCuratedModelsOnly("chatgpt-web"), true);
|
||||
assert.equal(providerUsesCuratedModelsOnly("qwen-cloud"), false);
|
||||
assert.equal(providerUsesCuratedModelsOnly("kimi-coding"), false);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user