|
| 1 | +import { BaseExecutor, type ExecuteInput, type ProviderCredentials } from "../base.ts"; |
| 2 | +import { OMNIROUTE_VERSION } from "@/shared/constants/version.ts"; |
| 3 | +import { getProxyForAccount } from "../../utils/proxyFallback.ts"; |
| 4 | +import { HttpsProxyAgent } from "https-proxy-agent"; |
| 5 | +import crypto from "node:crypto"; |
| 6 | +import { createHash } from "node:crypto"; |
| 7 | +import { saveCallLog } from "@/lib/usage/callLogArtifacts.ts"; |
| 8 | +import { streamWithTimeout } from "../../utils/stream.ts"; |
| 9 | +import { ANTIGRAVITY_CONFIG } from "../../config/errorConfig.ts"; |
| 10 | +import { |
| 11 | + storeChatGptImage, |
| 12 | + getChatGptImageConversationContext, |
| 13 | + __resetChatGptImageCacheForTesting, |
| 14 | + type ChatGptImageConversationContext, |
| 15 | +} from "../../services/chatgptImageCache.ts"; |
| 16 | + |
| 17 | +import { thinkingEffortCache } from "./thinking.ts"; |
| 18 | +import { tokenCache, cookieKey } from "./session.ts"; |
| 19 | +import { warmupCache } from "./warmup.ts"; |
| 20 | +import { dplCache } from "./sentinel.ts"; |
| 21 | + |
| 22 | +// ─── Constants ────────────────────────────────────────────────────────────── |
| 23 | + |
| 24 | +export const CHATGPT_BASE = "https://chatgpt.com"; |
| 25 | + |
| 26 | +export const SESSION_URL = `${CHATGPT_BASE}/api/auth/session`; |
| 27 | + |
| 28 | +export const CONV_URL = `${CHATGPT_BASE}/backend-api/f/conversation`; |
| 29 | + |
| 30 | +export const CHATGPT_USER_AGENT = |
| 31 | + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:148.0) Gecko/20100101 Firefox/148.0"; |
| 32 | + |
| 33 | +// Captured from a real chatgpt.com browser session (April 2026). |
| 34 | +export const OAI_CLIENT_VERSION = "prod-81e0c5cdf6140e8c5db714d613337f4aeab94029"; |
| 35 | + |
| 36 | +export const OAI_CLIENT_BUILD_NUMBER = "6128297"; |
| 37 | + |
| 38 | +// Per-cookie device ID. The browser stores a persistent `oai-did` cookie that |
| 39 | +// uniquely identifies the device for OpenAI's risk model — we derive a stable |
| 40 | +// UUID from a hash of the session cookie so that each account/connection gets |
| 41 | +// its own device id, but it doesn't change between requests. |
| 42 | +export const deviceIdCache = new Map<string, string>(); |
| 43 | + |
| 44 | +export function deviceIdFor(cookie: string): string { |
| 45 | + const key = cookieKey(cookie); |
| 46 | + let id = deviceIdCache.get(key); |
| 47 | + if (!id) { |
| 48 | + // Synthesize a UUID v4-shaped string from a SHA-256 of the cookie. Stable, |
| 49 | + // deterministic per cookie, no PII (the cookie's already secret). |
| 50 | + // Not a password hash — SHA-256 is used to derive a stable UUID from the |
| 51 | + // session cookie for device-id fingerprinting. The output is a cache key. |
| 52 | + const h = createHash("sha256").update(cookie).digest("hex"); // lgtm[js/insufficient-password-hash] |
| 53 | + id = |
| 54 | + `${h.slice(0, 8)}-${h.slice(8, 12)}-4${h.slice(13, 16)}-` + |
| 55 | + `${((parseInt(h.slice(16, 17), 16) & 0x3) | 0x8).toString(16)}${h.slice(17, 20)}-` + |
| 56 | + h.slice(20, 32); |
| 57 | + if (deviceIdCache.size >= 200) { |
| 58 | + const first = deviceIdCache.keys().next().value; |
| 59 | + if (first) deviceIdCache.delete(first); |
| 60 | + } |
| 61 | + deviceIdCache.set(key, id); |
| 62 | + } |
| 63 | + return id; |
| 64 | +} |
| 65 | + |
| 66 | +// OmniRoute model ID → ChatGPT internal slug. OmniRoute uses dot-form IDs |
| 67 | +// (e.g. "gpt-5.3-instant"), ChatGPT's web routes use dash-form |
| 68 | +// (e.g. "gpt-5-3-instant"). The slug catalog comes from |
| 69 | +// /backend-api/models on a logged-in account; "gpt-5-4-t-mini" is ChatGPT's |
| 70 | +// abbreviated slug for "GPT-5.4 Thinking Mini". |
| 71 | +export const MODEL_MAP: Record<string, string> = { |
| 72 | + "gpt-5.3-instant": "gpt-5-3-instant", |
| 73 | + "gpt-5.3": "gpt-5-3", |
| 74 | + "gpt-5.3-mini": "gpt-5-3-mini", |
| 75 | + "gpt-5.5-thinking": "gpt-5-5-thinking", |
| 76 | + "gpt-5.4-thinking": "gpt-5-4-thinking", |
| 77 | + "gpt-5.4-thinking-mini": "gpt-5-4-t-mini", |
| 78 | + "gpt-5.2-instant": "gpt-5-2-instant", |
| 79 | + "gpt-5.2": "gpt-5-2", |
| 80 | + "gpt-5.2-thinking": "gpt-5-2-thinking", |
| 81 | + "gpt-5.1": "gpt-5-1", |
| 82 | + "gpt-5": "gpt-5", |
| 83 | + "gpt-5-mini": "gpt-5-mini", |
| 84 | + o3: "o3", |
| 85 | +}; |
| 86 | + |
| 87 | +/** Set of chatgpt.com slugs that the user_last_used_model_config endpoint |
| 88 | + * accepts a `thinking_effort` value for, derived from MODEL_MAP so adding a |
| 89 | + * new thinking entry there automatically extends this set. Includes the |
| 90 | + * abbreviated slug `gpt-5-4-t-mini` (no literal "thinking" substring) — the |
| 91 | + * reason this set exists at all rather than a substring match. |
| 92 | + * |
| 93 | + * Derived from MODEL_MAP keys (always dot-form) that contain "thinking" or |
| 94 | + * are the `o3` reasoning model; the values are the chatgpt.com-side slugs. */ |
| 95 | +export const THINKING_CAPABLE_SLUGS: ReadonlySet<string> = new Set( |
| 96 | + Object.entries(MODEL_MAP) |
| 97 | + .filter(([k]) => k.includes("thinking") || k === "o3") |
| 98 | + .map(([, v]) => v) |
| 99 | +); |
| 100 | + |
| 101 | +// ─── Browser-like default headers ────────────────────────────────────────── |
| 102 | + |
| 103 | +export function browserHeaders(): Record<string, string> { |
| 104 | + return { |
| 105 | + Accept: "*/*", |
| 106 | + "Accept-Language": "en-US,en;q=0.9", |
| 107 | + "Cache-Control": "no-cache", |
| 108 | + Origin: CHATGPT_BASE, |
| 109 | + Pragma: "no-cache", |
| 110 | + Referer: `${CHATGPT_BASE}/`, |
| 111 | + "Sec-Fetch-Dest": "empty", |
| 112 | + "Sec-Fetch-Mode": "cors", |
| 113 | + "Sec-Fetch-Site": "same-origin", |
| 114 | + "User-Agent": CHATGPT_USER_AGENT, |
| 115 | + }; |
| 116 | +} |
| 117 | + |
| 118 | +/** Headers ChatGPT's web client sends on backend-api requests. */ |
| 119 | +export function oaiHeaders(sessionId: string, deviceId: string): Record<string, string> { |
| 120 | + return { |
| 121 | + "OAI-Language": "en-US", |
| 122 | + "OAI-Device-Id": deviceId, |
| 123 | + "OAI-Client-Version": OAI_CLIENT_VERSION, |
| 124 | + "OAI-Client-Build-Number": OAI_CLIENT_BUILD_NUMBER, |
| 125 | + "OAI-Session-Id": sessionId, |
| 126 | + }; |
| 127 | +} |
| 128 | + |
| 129 | +// Strip ChatGPT's internal entity markup. The browser renders these as proper |
| 130 | +// inline citations / chips via JS; for a plain text completion we just want |
| 131 | +// the human-readable form. |
| 132 | +// entity["city","Paris","capital of France"] → Paris |
| 133 | +// entity["…","value", …] → value |
| 134 | +export const ENTITY_RE = /entity\["[^"]*","([^"]*)"[^\]]*\]/g; |
| 135 | + |
| 136 | +// Test-only: clear caches between tests |
| 137 | +export function __resetChatGptWebCachesForTesting(): void { |
| 138 | + tokenCache.clear(); |
| 139 | + warmupCache.clear(); |
| 140 | + thinkingEffortCache.clear(); |
| 141 | + deviceIdCache.clear(); |
| 142 | + __resetChatGptImageCacheForTesting(); |
| 143 | + dplCache = null; |
| 144 | +} |
0 commit comments