Conversation
Adds a new chatgpt-web provider that routes through chatgpt.com's internal backend-api using a Plus/Pro subscription session cookie, enabling access to GPT-5.x models without an OpenAI API key. Heavier than perplexity-web/grok-web because chatgpt.com layers more bot protection — this PR builds out the full pipeline needed to look like a real browser session. ## New executor: open-sse/executors/chatgpt-web.ts Auth/request pipeline (per chat completion): 1. exchangeSession() GET /api/auth/session cookie -> JWT (cached ~5min) 2. fetchDpl() GET / scrape data-build + script src 3. runSessionWarmup() GET /backend-api/me, /conversations, /models 4. POST /sentinel/chat-requirements/prepare -> prepare_token 5. POST /sentinel/chat-requirements -> chat-requirements-token + PoW seed/diff 6. solveProofOfWork() SHA3-512 loop -> "gAAAAAB..." sentinel proof token 7. POST /backend-api/f/conversation with all sentinel headers 8. parse SSE stream -> OpenAI chat.completion[.chunk] format Notable details: - 18-element prekey config matching chat2api/openai-sentinel (browser fingerprint values, U+2212 MINUS SIGN in `webdriver−false`). Thin shapes get escalated to mandatory Turnstile. - Two-stage Sentinel handshake (/prepare + /chat-requirements) — sending only the prepare result returns a 403 "Unusual activity" response. - `turnstile.required: true` from Sentinel is treated as advisory; the conv endpoint accepts requests without a Turnstile token as long as PoW + chat- requirements-token are valid. Optional bring-your-own Turnstile via `providerSpecificData.turnstileToken` for accounts that hard-require it. - SSE parser tracks message_id and resets the accumulator on a new turn — chatgpt.com echoes prior assistant messages (with status finished_successfully) before sending the new turn. - entity["...","value", ...] internal markup stripped from output (browser renders these client-side). - Conversation-continuity cache disabled by default: we send history_and_training_disabled: true (Temporary Chat mode) and those conversation_ids expire too fast to reuse — re-using returned 404. Each request now sends conversation_id: null and replays full history, matching what Open WebUI and OpenAI-API-style clients send anyway. ## TLS impersonation: open-sse/services/chatgptTlsClient.ts ChatGPT's Cloudflare config pins cf_clearance to JA3/JA4 TLS fingerprint + HTTP/2 SETTINGS frame. Plain Node Undici fetch always returns cf-mitigated: challenge regardless of cookies. The wrapper module loads `tls-client-node` (Firefox 148 fingerprint) in native runtime mode (.so via koffi) — managed mode spawns a sidecar that conflicts with OmniRoute's global fetch proxy patch. - Lazy singleton TLSClient with process exit hooks - Streaming-capable (file tail) and non-streaming modes - Test injection point: __setTlsFetchOverrideForTesting() lets unit tests mock the client without touching globalThis.fetch ## Provider wiring - open-sse/executors/index.ts — register ChatGptWebExecutor with cgpt-web alias - open-sse/config/providerRegistry.ts — registry entry, format=openai, authHeader=cookie, model gpt-5.3-instant - src/shared/constants/providers.ts — WEB_COOKIE_PROVIDERS UI metadata (icon, color, authHint) - src/lib/providers/validation.ts — validateChatGptWebProvider hits /api/auth/session via the TLS client, detects cf-mitigated/HTML responses and returns a clear "paste full Cookie line" hint instead of a generic "Invalid" - next.config.mjs — mark tls-client-node, koffi, tough-cookie as external packages (Turbopack can't bundle the native .so) ## Cookie format Validator and executor accept any of: - bare value: "eyJhbGc..." - unchunked cookie line: "__Secure-next-auth.session-token=eyJ..." - chunked cookie line: "__Secure-next-auth.session-token.0=...; __Secure-next-auth.session-token.1=..." - full DevTools Cookie header line: "Cookie: __Secure-next-auth.session-token.0=...; cf_clearance=...; ..." NextAuth chunks the JWE when it exceeds 4KB; chunked cookies pass through verbatim (NextAuth reassembles server-side). Recommend pasting the full DevTools Cookie line so cf_clearance, __cf_bm, _cfuvid, _puid travel along — without cf_clearance, Cloudflare blocks the request before NextAuth sees it. ## Tests tests/unit/chatgpt-web.test.ts — 27 tests, all passing: - Registration + alias resolution - Token exchange (cookie -> Bearer flow) - Token cache TTL - Refreshed cookie surfaced via onCredentialsRefreshed callback - Sentinel call ordering (session -> prepare -> chat-requirements -> conv) - Sentinel chat-requirements-token forwarded on conv request - PoW token has gAAAAAB prefix - Turnstile.required: true does NOT block conv (passes through) - Non-streaming chat.completion JSON - Streaming SSE chunks ending with [DONE] - Cumulative-parts diffing yields non-overlapping deltas - Errors: 401 session, 403 sentinel, 429 conv rate-limit - Empty messages -> 400 without any fetch - Missing apiKey -> 401 without any fetch - Cookie format: bare value, unchunked, chunked, "Cookie: ..." DevTools line - Conversation continuity: each call starts a fresh conversation - Browser-like headers on conv POST (UA, Origin, Sec-Fetch-Site, Accept) - Payload shape (action, model=gpt-5-3, history_and_training_disabled) - Provider registry contains chatgpt-web with gpt-5.3-instant model Verification: typecheck:core clean, lint clean (no new warnings), end-to-end manually verified across single-turn, multi-turn (memory preserved), streaming, and Open WebUI-style sequential growing-history flows. ## References - bogdanfinn/tls-client (Go) — TLS impersonation upstream - fatihkabakk/tls-client-node — Node bindings - lanqian528/chat2api — Sentinel/PoW/prekey reference impl (Python) - leetanshaj/openai-sentinel — Prekey config + SHA3-512 solver Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Code Review
This pull request introduces a new chatgpt-web provider that allows routing requests through the internal ChatGPT web API using session cookies. It includes a specialized ChatGptWebExecutor that handles session token exchange, browser-like warmup sequences, and the solving of Sentinel proof-of-work challenges. To bypass Cloudflare's TLS fingerprinting, a new chatgptTlsClient service was added, which utilizes the tls-client-node library to impersonate a Firefox handshake. Feedback focuses on performance issues caused by synchronous proof-of-work loops on the main event loop, the use of a global device ID instead of per-account identifiers, and the presence of unused caching logic and console.log statements in production code.
| for (let i = 0; i < 100_000; i++) { | ||
| cfg[3] = i; | ||
| const json = JSON.stringify(cfg); | ||
| const b64 = Buffer.from(json).toString("base64"); | ||
| const hash = createHash("sha3-512").update(b64).digest("hex"); | ||
| if (hash.slice(0, target.length) <= target) { | ||
| return `gAAAAAC${b64}`; | ||
| } | ||
| } |
There was a problem hiding this comment.
This Proof-of-Work (PoW) solver loop is synchronous and computationally intensive (performing up to 100,000 SHA3-512 hashes). Running this on the main Node.js event loop will block the entire process, preventing it from handling other concurrent requests or I/O operations. This is a significant performance bottleneck for a server-side application. Consider offloading this calculation to a Worker thread or using an asynchronous implementation that yields control periodically.
| for (let i = 0; i < maxIter; i++) { | ||
| cfg[3] = i; | ||
| const json = JSON.stringify(cfg); | ||
| const b64 = Buffer.from(json).toString("base64"); | ||
| const hash = createHash("sha3-512") | ||
| .update(seed + b64) | ||
| .digest("hex"); | ||
| if (target && hash.slice(0, target.length) <= target) { | ||
| return `gAAAAAB${b64}`; | ||
| } | ||
| } |
There was a problem hiding this comment.
Similar to the prekey PoW, this loop performs up to 500,000 synchronous SHA3-512 hashes on the main event loop. This will cause the Node.js process to hang for several seconds during each request that requires PoW, severely impacting the scalability and responsiveness of the service. This logic should be offloaded to a background worker.
| const OAI_CLIENT_BUILD_NUMBER = "6128297"; | ||
|
|
||
| // Stable per-process device ID (matches the browser's persistent oai-did cookie behaviour). | ||
| const DEVICE_ID = randomUUID(); |
There was a problem hiding this comment.
Using a single, global DEVICE_ID for all requests across different accounts increases the risk of being flagged by OpenAI's security systems. In a real browser, this ID is persistent per user/session. To better mimic browser behavior and improve account isolation, consider deriving a stable device ID from the user's session token (e.g., by hashing the cookie) so that each account has its own unique, persistent identifier.
| const convCache = new Map<string, ConvEntry>(); | ||
|
|
||
| function historyKey(history: Array<{ role: string; content: string }>): string { | ||
| const parts = history.map((h) => `${h.role}:${h.content}`).join("\n"); | ||
| let hash = 0x811c9dc5; | ||
| for (let i = 0; i < parts.length; i++) { | ||
| hash ^= parts.charCodeAt(i); | ||
| hash = (hash * 0x01000193) >>> 0; | ||
| } | ||
| return hash.toString(16).padStart(8, "0"); | ||
| } | ||
|
|
||
| function convLookup( | ||
| history: Array<{ role: string; content: string }> | ||
| ): { conversationId: string; lastMessageId: string } | null { | ||
| if (history.length === 0) return null; | ||
| const key = historyKey(history); | ||
| const entry = convCache.get(key); | ||
| if (!entry) return null; | ||
| if (Date.now() - entry.ts > CONV_TTL_MS) { | ||
| convCache.delete(key); | ||
| return null; | ||
| } | ||
| return { conversationId: entry.conversationId, lastMessageId: entry.lastMessageId }; | ||
| } | ||
|
|
||
| function convStore( | ||
| history: Array<{ role: string; content: string }>, | ||
| currentMsg: string, | ||
| responseText: string, | ||
| conversationId: string, | ||
| lastMessageId: string | ||
| ): void { | ||
| if (!conversationId || !lastMessageId) return; | ||
| const full = [ | ||
| ...history, | ||
| { role: "user", content: currentMsg }, | ||
| { role: "assistant", content: responseText }, | ||
| ]; | ||
| const key = historyKey(full); | ||
| convCache.set(key, { conversationId, lastMessageId, ts: Date.now() }); | ||
| if (convCache.size > CONV_MAX) { | ||
| let oldestKey: string | null = null; | ||
| let oldestTs = Infinity; | ||
| for (const [k, v] of convCache) { | ||
| if (v.ts < oldestTs) { | ||
| oldestTs = v.ts; | ||
| oldestKey = k; | ||
| } | ||
| } | ||
| if (oldestKey) convCache.delete(oldestKey); | ||
| } | ||
| } |
There was a problem hiding this comment.
The conversation continuity cache (convCache) and its associated logic (convLookup, convStore) are implemented but currently unused, as conversationId is hardcoded to null in the execute method (line 1282). This adds unnecessary memory overhead and complexity. If persistent chats are intentionally disabled, this dead code should be removed to keep the codebase clean.
| // Always log the upstream body on 4xx/5xx — error responses are small | ||
| // and the upstream message is much more useful than our wrapper. | ||
|
|
||
| console.log(`[CGPT-WEB] conv ${status}: ${(response.text || "").slice(0, 400)}`); |
There was a problem hiding this comment.
Avoid using console.log in production code. Use the provided log object (e.g., log.info or log.warn) to ensure that logs are handled according to the application's logging configuration.
| console.log(`[CGPT-WEB] conv ${status}: ${(response.text || "").slice(0, 400)}`); | |
| log?.info?.("CGPT-WEB", "conv " + status + ": " + (response.text || "").slice(0, 400)); |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 299793e788
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| response = await tlsFetchChatGpt(CONV_URL, { | ||
| method: "POST", | ||
| headers, | ||
| body: JSON.stringify(cgptBody), | ||
| timeoutMs: 120_000, // generations can take a while | ||
| signal, |
There was a problem hiding this comment.
Enable true upstream streaming for stream requests
When stream requests are handled, the conversation call is still made without stream: true, so tlsFetchChatGpt returns only after the full SSE response has completed. That means clients get buffered chunks all at once instead of real-time token streaming, which defeats the stream=true contract and can trigger downstream timeouts on long generations.
Useful? React with 👍 / 👎.
| const last = warmupCache.get(key); | ||
| if (last && now - last < WARMUP_TTL_MS) return; | ||
| warmupCache.set(key, now); |
There was a problem hiding this comment.
Bound session warmup cache growth
warmupCache is written on every new (cookie, accessToken) pair but never pruned or size-limited, so active accounts that rotate tokens will keep adding entries for the process lifetime. In a long-running multi-user deployment this causes unbounded memory growth in the chatgpt-web hot path.
Useful? React with 👍 / 👎.
| const requestOptions: Record<string, unknown> = { | ||
| method: options.method || "GET", | ||
| headers: options.headers || {}, | ||
| body: options.body, | ||
| tlsClientIdentifier: CHATGPT_PROFILE, | ||
| timeoutMilliseconds: options.timeoutMs ?? DEFAULT_TIMEOUT_MS, | ||
| followRedirects: true, | ||
| withRandomTLSExtensionOrder: true, | ||
| }; |
There was a problem hiding this comment.
Honor abort signals in TLS fetch requests
TlsFetchOptions accepts signal, and callers pass request abort signals, but tlsFetchChatGpt never checks or propagates it. As a result, canceled client requests continue running upstream TLS calls, which wastes resources and can leave long chatgpt-web requests running after the client disconnects.
Useful? React with 👍 / 👎.
Round of fixes addressing the gemini-code-assist and chatgpt-codex review comments on the initial PR. ## High priority - **PoW solver no longer blocks the event loop** (gemini #1, #2). The 100k prekey solver and 500k proof-of-work solver were synchronous SHA3-512 loops that pinned a CPU core for tens to hundreds of milliseconds per request. Both are now async and `await`-yield to the event loop every 1000 iterations via setImmediate, so concurrent requests and I/O still get scheduled. Wall time is approximately the same; what changes is fairness, not throughput. - **Real upstream streaming for stream=true requests** (codex diegosouzapw#6). The conv call now passes `stream: true` through to the TLS client when the caller asked for streaming. The TLS client uses tls-client-node's streamOutputPath primitive to write the response body to a temp file as it arrives, and we tail that file as a ReadableStream so clients see chunks in real time instead of getting one buffered burst at the end. Also peeks the first 256 bytes — if the response starts with `{` it's almost certainly a JSON error envelope, so we wait for the full body and surface as a non-streaming error response. ## Medium priority - **Per-cookie device id** (gemini diegosouzapw#3). Replaced the single process-wide DEVICE_ID with a per-cookie SHA-256-derived UUID that's stable across requests for one connection but unique per cookie. This matches how the browser's persistent oai-did cookie behaves and avoids cross-account fingerprint sharing. Cache is bounded to 200 entries with FIFO eviction. - **Removed dead conv-cache code** (gemini diegosouzapw#4). The convCache / convLookup / convStore trio (~70 LOC) was unused — conversationId is hard-pinned to null because Temporary Chat conversation_ids 404 on reuse. Deleted entirely; the comment explains why we don't persist. - **No more console.log in the conv 4xx path** (gemini diegosouzapw#5). Replaced with log?.warn so it respects the application's logging configuration. - **Bound the warmup cache** (codex diegosouzapw#7). The (cookie, accessToken) -> timestamp map was unbounded; long-running multi-user deployments with rotating tokens would grow it forever. Now capped at 200 entries with FIFO eviction (Map iteration order = insertion order). - **Honor abort signals in TLS fetch** (codex diegosouzapw#8). tlsFetchChatGpt now checks options.signal before issuing the upstream call, after the call returns, and the streaming body listens for abort to stop tailing the temp file. tls-client-node's koffi binding can't cancel an in-flight request mid-call, but we no longer process / re-emit a response that the caller has already given up on. ## Tests All 27 chatgpt-web tests still pass; updated several to find calls by URL via findIndex rather than hardcoded indices, since the warmup sequence (/me, /conversations, /models) and two-stage Sentinel (prepare + chat-requirements) shifted positional offsets. Manually verified end-to-end: - Non-streaming completions - Streaming completions (real-time chunks; SSE [DONE] terminator) - Multi-turn with full history each turn (memory preserved correctly) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Reported by user testing in Open WebUI: across three turns
user "test 1" -> "1"
user "test 2" -> "12" (should be "2")
user "test 3. reply only with 3" -> "1123" (should be "3")
The model was literally APPENDING prior assistant outputs into the new
generation instead of producing a fresh response. Root cause: when
sending each prior turn as a separate `assistant`-role entry in
`/backend-api/f/conversation`'s `messages` array, ChatGPT's web API
("action: next") treats those as in-progress messages the model can
continue rather than as completed turns. So the new generation extends
the most recent assistant message.
Fix: don't replay prior turns as separate messages. Instead fold the
full history into the system message as plain text and send only the
current user query as a single new turn. Verified end-to-end:
Turn 1 -> "1"
Turn 2 -> "2"
Turn 3 -> "3"
user "favorite color is teal" / assistant "Got it" / user "what color?"
-> "Teal" (memory still preserved through the system-message channel)
Streaming + multi-turn -> correct, real-time chunks
Updated two unit tests that previously asserted history items showed up
as separate `user`/`assistant` messages in the request — they now check
for the single-user-message + history-in-system-message shape.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
Thanks for the careful review. Pushed two follow-up commits. fc80a98 — Review feedback (all 8 comments)High priority#1, #2 — PoW solver blocking the event loop. Both the prekey solver (100k iters) and the proof-of-work solver (500k iters) are now #6 — Real upstream streaming. The conversation call now passes Medium priority#3 — Per-cookie device id. #4 — Dead #5 — #7 — #8 — Abort signals. 1dd7ec9 — Multi-turn fix found via Open WebUI testingA user reported in real Open WebUI testing that responses across turns concatenated: Root cause: when sending prior turns as separate Fix: fold full history into the system message as plain text, send only the current user query as a single new turn. Verified: TestsAll 27 chatgpt-web tests still pass. Updated several to find calls by URL via |
|
Manually merged during stabilization of release/v3.7.0 |
|
I have one more commit incoming @diegosouzapw, I'll open a new PR against the release/v3.7.0 branch |
Integrated into release/v3.7.0. Thank you for the contribution @trader-payne!
… Plus tier Two changes that together close the gap left when the original ChatGPT Web feature PR (diegosouzapw#1593) was integrated via merge commit 50097b1. 1. Restore validateChatGptWebProvider in src/lib/providers/validation.ts plus its "chatgpt-web" entry in SPECIALTY_VALIDATORS. The function was authored on feat/chatgpt-web-provider but lost when the merge resolved a conflict against an older validation.ts. Without it, the chatgpt-web account form falls through to the generic OpenAI validator, which 401s on /backend-api/conversation/models. The restored implementation accepts bare values, single pairs, chunked tokens, and full DevTools cookie blobs; uses tlsFetchChatGpt for Cloudflare-bypassing TLS impersonation; and surfaces specific errors for cf-mitigated challenges, expired sessions, missing accessToken, and TLS-client unavailability. Eight tests cover the success path and each error branch. 2. Expand the chatgpt-web model catalog from a single entry to the 13 chat models a Plus account actually has access to (verified against /backend-api/models). Adds instant / thinking / mini variants for 5.2 / 5.3 / 5.4 / 5.5, plus gpt-5, gpt-5.1, gpt-5-mini, and o3. The MODEL_MAP now translates dot-form OmniRoute IDs (e.g. "gpt-5.4-thinking-mini") to ChatGPT's exact dash-form slugs ("gpt-5-4-t-mini"). The previous default "gpt-5.3-instant" mapping pointed at the auto-routing base "gpt-5-3"; it now points at the explicit "gpt-5-3-instant" slug, matching what the UI's last-model-config cookie shows. Excludes "research" and "agent-mode" — those are specialty surfaces, not regular chat models. Tests: 118/118 pass. Verified live via /api/providers/validate and /v1/chat/completions on the running container. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Round of fixes addressing the gemini-code-assist and chatgpt-codex review comments on the initial PR. ## High priority - **PoW solver no longer blocks the event loop** (gemini diegosouzapw#1, diegosouzapw#2). The 100k prekey solver and 500k proof-of-work solver were synchronous SHA3-512 loops that pinned a CPU core for tens to hundreds of milliseconds per request. Both are now async and `await`-yield to the event loop every 1000 iterations via setImmediate, so concurrent requests and I/O still get scheduled. Wall time is approximately the same; what changes is fairness, not throughput. - **Real upstream streaming for stream=true requests** (codex diegosouzapw#6). The conv call now passes `stream: true` through to the TLS client when the caller asked for streaming. The TLS client uses tls-client-node's streamOutputPath primitive to write the response body to a temp file as it arrives, and we tail that file as a ReadableStream so clients see chunks in real time instead of getting one buffered burst at the end. Also peeks the first 256 bytes — if the response starts with `{` it's almost certainly a JSON error envelope, so we wait for the full body and surface as a non-streaming error response. ## Medium priority - **Per-cookie device id** (gemini diegosouzapw#3). Replaced the single process-wide DEVICE_ID with a per-cookie SHA-256-derived UUID that's stable across requests for one connection but unique per cookie. This matches how the browser's persistent oai-did cookie behaves and avoids cross-account fingerprint sharing. Cache is bounded to 200 entries with FIFO eviction. - **Removed dead conv-cache code** (gemini diegosouzapw#4). The convCache / convLookup / convStore trio (~70 LOC) was unused — conversationId is hard-pinned to null because Temporary Chat conversation_ids 404 on reuse. Deleted entirely; the comment explains why we don't persist. - **No more console.log in the conv 4xx path** (gemini diegosouzapw#5). Replaced with log?.warn so it respects the application's logging configuration. - **Bound the warmup cache** (codex diegosouzapw#7). The (cookie, accessToken) -> timestamp map was unbounded; long-running multi-user deployments with rotating tokens would grow it forever. Now capped at 200 entries with FIFO eviction (Map iteration order = insertion order). - **Honor abort signals in TLS fetch** (codex diegosouzapw#8). tlsFetchChatGpt now checks options.signal before issuing the upstream call, after the call returns, and the streaming body listens for abort to stop tailing the temp file. tls-client-node's koffi binding can't cancel an in-flight request mid-call, but we no longer process / re-emit a response that the caller has already given up on. ## Tests All 27 chatgpt-web tests still pass; updated several to find calls by URL via findIndex rather than hardcoded indices, since the warmup sequence (/me, /conversations, /models) and two-stage Sentinel (prepare + chat-requirements) shifted positional offsets. Manually verified end-to-end: - Non-streaming completions - Streaming completions (real-time chunks; SSE [DONE] terminator) - Multi-turn with full history each turn (memory preserved correctly) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…w#1596) Integrated into release/v3.7.0. Thank you for the contribution @trader-payne!
Round of fixes addressing the gemini-code-assist and chatgpt-codex review comments on the initial PR. ## High priority - **PoW solver no longer blocks the event loop** (gemini diegosouzapw#1, diegosouzapw#2). The 100k prekey solver and 500k proof-of-work solver were synchronous SHA3-512 loops that pinned a CPU core for tens to hundreds of milliseconds per request. Both are now async and `await`-yield to the event loop every 1000 iterations via setImmediate, so concurrent requests and I/O still get scheduled. Wall time is approximately the same; what changes is fairness, not throughput. - **Real upstream streaming for stream=true requests** (codex diegosouzapw#6). The conv call now passes `stream: true` through to the TLS client when the caller asked for streaming. The TLS client uses tls-client-node's streamOutputPath primitive to write the response body to a temp file as it arrives, and we tail that file as a ReadableStream so clients see chunks in real time instead of getting one buffered burst at the end. Also peeks the first 256 bytes — if the response starts with `{` it's almost certainly a JSON error envelope, so we wait for the full body and surface as a non-streaming error response. ## Medium priority - **Per-cookie device id** (gemini diegosouzapw#3). Replaced the single process-wide DEVICE_ID with a per-cookie SHA-256-derived UUID that's stable across requests for one connection but unique per cookie. This matches how the browser's persistent oai-did cookie behaves and avoids cross-account fingerprint sharing. Cache is bounded to 200 entries with FIFO eviction. - **Removed dead conv-cache code** (gemini diegosouzapw#4). The convCache / convLookup / convStore trio (~70 LOC) was unused — conversationId is hard-pinned to null because Temporary Chat conversation_ids 404 on reuse. Deleted entirely; the comment explains why we don't persist. - **No more console.log in the conv 4xx path** (gemini diegosouzapw#5). Replaced with log?.warn so it respects the application's logging configuration. - **Bound the warmup cache** (codex diegosouzapw#7). The (cookie, accessToken) -> timestamp map was unbounded; long-running multi-user deployments with rotating tokens would grow it forever. Now capped at 200 entries with FIFO eviction (Map iteration order = insertion order). - **Honor abort signals in TLS fetch** (codex diegosouzapw#8). tlsFetchChatGpt now checks options.signal before issuing the upstream call, after the call returns, and the streaming body listens for abort to stop tailing the temp file. tls-client-node's koffi binding can't cancel an in-flight request mid-call, but we no longer process / re-emit a response that the caller has already given up on. ## Tests All 27 chatgpt-web tests still pass; updated several to find calls by URL via findIndex rather than hardcoded indices, since the warmup sequence (/me, /conversations, /models) and two-stage Sentinel (prepare + chat-requirements) shifted positional offsets. Manually verified end-to-end: - Non-streaming completions - Streaming completions (real-time chunks; SSE [DONE] terminator) - Multi-turn with full history each turn (memory preserved correctly) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…w#1596) Integrated into release/v3.7.0. Thank you for the contribution @trader-payne!
Summary
Adds a new ChatGPT Web (Plus/Pro) session provider that routes through
chatgpt.com's internalbackend-apiusing a Plus/Pro subscription session cookie, enabling access to GPT-5.x models without an OpenAI API key.Heavier than
perplexity-web/grok-webbecause chatgpt.com layers more bot protection — this PR builds out the full pipeline needed to look like a real browser session.What it takes to look like chatgpt.com (the hard part)
cf_clearanceto JA3/JA4 + HTTP/2 SETTINGS frame. Plain Node Undici fetch always returnscf-mitigated: challengeregardless of cookies. We usetls-client-node(Firefox 148 fingerprint) in native runtime mode (.sovia koffi) — managed mode spawns a sidecar that conflicts with OmniRoute's global fetch proxy patch.GET /to scrapedata-build+ a real<script src>URL (positions 5/6 of the prekey). Without these, Sentinel's prekey check tags us as a bot.chat2api/openai-sentinelshape, including the U+2212 MINUS SIGN inwebdriver−false. A thin[ua, ts]shape is auto-escalated to mandatory Turnstile.POST /sentinel/chat-requirements/preparereturnsprepare_token;POST /sentinel/chat-requirements(with that token) returns the realchat-requirements-token+ PoW seed/diff. Sending only the prepare result returns a 403 "Unusual activity" response.GET /backend-api/me,/conversations,/models(cached 60s). Sentinel scores session warmth.chat2api, reuses the prekey config shape with the server-provided seed/diff.turnstile.required: trueis advisory — the conversation endpoint accepts requests without a Turnstile token as long as PoW + chat-requirements-token are valid. Optional bring-your-own Turnstile viaproviderSpecificData.turnstileTokenfor accounts that hard-require it.status: finished_successfullybefore sending the new turn. The parser tracksmessage_id, resets the accumulator on a new turn, and does NOT break on the firstfinished_successfully.history_and_training_disabled: true(Temporary Chat) makes conversation_ids expire too fast to reuse; reusing returned 404. Each request now sendsconversation_id: nulland replays full history (which is what Open WebUI / OpenAI-API-style clients send anyway).entity["..."]markup stripping — chatgpt.com embeds internal entity chips likeentity["city","Paris","capital of France"]that the browser renders client-side via JS. Stripped to plain text.Auth — cookie format
Validator and executor accept any of:
"eyJhbGc...""__Secure-next-auth.session-token=eyJ...""__Secure-next-auth.session-token.0=...; __Secure-next-auth.session-token.1=...""Cookie: __Secure-next-auth.session-token.0=...; cf_clearance=...; __cf_bm=...; ..."NextAuth chunks the JWE when it exceeds 4KB; chunked cookies pass through verbatim (NextAuth reassembles server-side). Recommended: paste the full DevTools Cookie line so
cf_clearance,__cf_bm,_cfuvid,_puidtravel along — withoutcf_clearance, Cloudflare blocks the request before NextAuth ever sees it.Files changed
open-sse/executors/chatgpt-web.ts(~1400 lines)open-sse/services/chatgptTlsClient.ts(~300 lines — TLS-impersonating fetch wrapper)tests/unit/chatgpt-web.test.ts(27 tests, all passing)open-sse/executors/index.ts(+4 — registerchatgpt-web+cgpt-webalias)open-sse/config/providerRegistry.ts(+11 — registry entry, modelgpt-5.3-instant)src/shared/constants/providers.ts(+10 —WEB_COOKIE_PROVIDERSUI metadata)src/lib/providers/validation.ts(+113 —validateChatGptWebProviderwith Cloudflare-block detection)next.config.mjs(+3 — marktls-client-node,koffi,tough-cookieasserverExternalPackagesso Turbopack doesn't try to bundle the native.so)package.json/package-lock.json— addstls-client-nodedependencyModels
gpt-5.3-instantgpt-5-3(Easy to add more — the registry takes a list, executor
MODEL_MAPis one line each.)Test results
Test plan
npm run typecheck:corepassesnpm run lintadds 0 new warningsnode --import tsx/esm --test tests/unit/chatgpt-web.test.ts— 27/27 passchat.completionstream: true) with content deltas +[DONE]Caveats
perplexity-webandgrok-webproviders. Not a sanctioned API path.OAI-Client-Versionconstant, andtls-client-node's upstream Firefox profile are pin points that may need refresh.References
🤖 Generated with Claude Code