Skip to content

feat: add ChatGPT Web (Plus/Pro) session provider - #1593

Closed
payne0420 wants to merge 3 commits into
diegosouzapw:release/v3.7.0from
payne0420:feat/chatgpt-web-provider
Closed

payne0420 wants to merge 3 commits into
diegosouzapw:release/v3.7.0from
payne0420:feat/chatgpt-web-provider

Conversation

@payne0420

Copy link
Copy Markdown
Contributor

Summary

Adds a new ChatGPT Web (Plus/Pro) session 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.

What it takes to look like chatgpt.com (the hard part)

  1. TLS fingerprint impersonation — Cloudflare on chatgpt.com pins cf_clearance to JA3/JA4 + HTTP/2 SETTINGS frame. Plain Node Undici fetch always returns cf-mitigated: challenge regardless of cookies. We use 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.
  2. DPL warmup — GET / to scrape data-build + a real <script src> URL (positions 5/6 of the prekey). Without these, Sentinel's prekey check tags us as a bot.
  3. 18-element browser-fingerprint prekey — matches chat2api / openai-sentinel shape, including the U+2212 MINUS SIGN in webdriver−false. A thin [ua, ts] shape is auto-escalated to mandatory Turnstile.
  4. Two-stage Sentinel handshake — POST /sentinel/chat-requirements/prepare returns prepare_token; POST /sentinel/chat-requirements (with that token) returns the real chat-requirements-token + PoW seed/diff. Sending only the prepare result returns a 403 "Unusual activity" response.
  5. Browser-like session warmup — GET /backend-api/me, /conversations, /models (cached 60s). Sentinel scores session warmth.
  6. SHA3-512 PoW solver — same algorithm as chat2api, reuses the prekey config shape with the server-provided seed/diff.
  7. turnstile.required: true is advisory — the conversation 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.
  8. SSE parser quirks — chatgpt.com echoes prior assistant turns at the start of the stream with status: finished_successfully before sending the new turn. The parser tracks message_id, resets the accumulator on a new turn, and does NOT break on the first finished_successfully.
  9. Conversation continuity disabled — history_and_training_disabled: true (Temporary Chat) makes conversation_ids expire too fast to reuse; reusing returned 404. Each request now sends conversation_id: null and replays full history (which is what Open WebUI / OpenAI-API-style clients send anyway).
  10. entity["..."] markup stripping — chatgpt.com embeds internal entity chips like entity["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:

  • 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: "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, _puid travel along — without cf_clearance, Cloudflare blocks the request before NextAuth ever sees it.

Files changed

  • Created: open-sse/executors/chatgpt-web.ts (~1400 lines)
  • Created: open-sse/services/chatgptTlsClient.ts (~300 lines — TLS-impersonating fetch wrapper)
  • Created: tests/unit/chatgpt-web.test.ts (27 tests, all passing)
  • Modified: open-sse/executors/index.ts (+4 — register chatgpt-web + cgpt-web alias)
  • Modified: open-sse/config/providerRegistry.ts (+11 — registry entry, model gpt-5.3-instant)
  • Modified: src/shared/constants/providers.ts (+10 — WEB_COOKIE_PROVIDERS UI metadata)
  • Modified: src/lib/providers/validation.ts (+113 — validateChatGptWebProvider with Cloudflare-block detection)
  • Modified: next.config.mjs (+3 — mark tls-client-node, koffi, tough-cookie as serverExternalPackages so Turbopack doesn't try to bundle the native .so)
  • Modified: package.json / package-lock.json — adds tls-client-node dependency

Models

OmniRoute Model ChatGPT slug
gpt-5.3-instant gpt-5-3

(Easy to add more — the registry takes a list, executor MODEL_MAP is one line each.)

Test results

✔ ChatGptWebExecutor is registered in executor index
✔ ChatGptWebExecutor alias resolves to same type
✔ ChatGptWebExecutor sets correct provider name
✔ Token exchange: cookie sent to /api/auth/session, accessToken used as Bearer on later calls
✔ Token cache: two calls within TTL only hit /api/auth/session once
✔ Refreshed cookie: surfaced via onCredentialsRefreshed callback
✔ Sentinel: chat-requirements is hit before /backend-api/conversation
✔ Sentinel: chat-requirements token forwarded on conv request
✔ PoW: when required, proof token is sent with valid prefix
✔ Turnstile: required flag does NOT block — conv endpoint accepts requests
✔ Non-streaming: returns OpenAI chat.completion JSON
✔ Streaming: produces valid SSE chunks ending with [DONE]
✔ Streaming: cumulative parts are diffed into non-overlapping deltas
✔ Error: 401 on /api/auth/session returns 401 with re-paste hint
✔ Error: 200 with no accessToken returns 401
✔ Error: 403 from sentinel returns 403 SENTINEL_BLOCKED
✔ Error: 429 from conversation returns 429 with rate-limit message
✔ Error: empty messages returns 400 without any fetch
✔ Error: missing apiKey returns 401 without any fetch
✔ Cookie: bare value gets prepended with cookie name
✔ Cookie: unchunked cookie line is passed through verbatim
✔ Cookie: chunked .0/.1 cookies are passed through verbatim (NextAuth reassembles)
✔ Cookie: 'Cookie: ' DevTools prefix is stripped
✔ Session continuity: each call starts a fresh conversation (Temporary Chat mode)
✔ Request: conversation POST has correct browser-like headers
✔ Request: payload has correct ChatGPT shape
✔ Provider registry: chatgpt-web is registered with gpt-5.3-instant model
ℹ tests 27 | pass 27 | fail 0

Test plan

  • npm run typecheck:core passes
  • npm run lint adds 0 new warnings
  • node --import tsx/esm --test tests/unit/chatgpt-web.test.ts — 27/27 pass
  • Existing perplexity-web / grok-web test suites unaffected (38/38 pass)
  • Manually verified end-to-end with a real Plus subscription:
    • Single-turn chat.completion
    • Multi-turn (memory preserved across turns)
    • Streaming (stream: true) with content deltas + [DONE]
    • Open WebUI-style sequential growing-history conversation
    • Validator returns Valid for a fresh full-cookie paste
    • Validator returns clear "paste cf_clearance" error when only the session-token chunks are pasted

Caveats

  • TOS: cookie-based session use sits in the same gray zone as the existing perplexity-web and grok-web providers. Not a sanctioned API path.
  • Maintenance: OpenAI rotates Sentinel logic every ~6–10 weeks. The 18-element prekey, the OAI-Client-Version constant, and tls-client-node's upstream Firefox profile are pin points that may need refresh.
  • Account risk: same as the other web-cookie providers. Heavy automated use may attract attention.

References

🤖 Generated with Claude Code

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>
@payne0420
payne0420 requested a review from diegosouzapw as a code owner April 25, 2026 13:10

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +573 to +581
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}`;
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

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.

Comment on lines +592 to +602
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}`;
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

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.

Comment thread open-sse/executors/chatgpt-web.ts Outdated
const OAI_CLIENT_BUILD_NUMBER = "6128297";

// Stable per-process device ID (matches the browser's persistent oai-did cookie behaviour).
const DEVICE_ID = randomUUID();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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.

Comment thread open-sse/executors/chatgpt-web.ts Outdated
Comment on lines +132 to +184
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);
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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.

Comment thread open-sse/executors/chatgpt-web.ts Outdated
// 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)}`);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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.

Suggested change
console.log(`[CGPT-WEB] conv ${status}: ${(response.text || "").slice(0, 400)}`);
log?.info?.("CGPT-WEB", "conv " + status + ": " + (response.text || "").slice(0, 400));

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment on lines +1310 to +1315
response = await tlsFetchChatGpt(CONV_URL, {
method: "POST",
headers,
body: JSON.stringify(cgptBody),
timeoutMs: 120_000, // generations can take a while
signal,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 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 👍 / 👎.

Comment on lines +319 to +321
const last = warmupCache.get(key);
if (last && now - last < WARMUP_TTL_MS) return;
warmupCache.set(key, now);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Comment on lines +148 to +156
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,
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Payne and others added 2 commits April 25, 2026 13:32
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>
@payne0420

Copy link
Copy Markdown
Contributor Author

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 async and await yieldToEventLoop() (a setImmediate micro-yield) every 1000 iterations — roughly every ~5ms of compute, enough that concurrent I/O and other requests still get scheduled. Wall time is essentially unchanged; what improves is fairness under concurrent load. Worker-thread offload is straightforward to add later if profiling shows we need true parallelism.

#6 — Real upstream streaming. The conversation call now passes stream: true through to tlsFetchChatGpt when the caller asked for streaming. The TLS client uses tls-client-node's streamOutputPath primitive to write the body to a temp file as it arrives, and tailFile() exposes that as a ReadableStream<Uint8Array> — clients see chunks in real time instead of one buffered burst at the end of a long generation. We also peek the first 256 bytes; if they look like a JSON error envelope ({...}) we wait for the full body and surface as non-streaming so error messages don't get lost in the SSE parser.

Medium priority

#3 — Per-cookie device id. DEVICE_ID was a single process-wide UUID; now deviceIdFor(cookie) returns a UUID-shaped value derived from SHA-256(cookie) — stable across requests for one connection, distinct across cookies. Cache bounded at 200 entries with FIFO eviction.

#4 — Dead convCache code removed. The conv-cache trio (convLookup/convStore/convCache, ~70 LOC) was unused since conversationId is hard-pinned to null (Temporary Chat conversation_ids 404 on reuse). Deleted; the comment explains why we don't persist.

#5 — console.log → log?.warn?.(...). Done; the conv 4xx body now goes through the executor's logger so it respects the application's logging configuration.

#7 — warmupCache unbounded. Now bounded at 200 entries with FIFO eviction (Map iteration order = insertion order, so deleting the first key drops the oldest entry).

#8 — Abort signals. tlsFetchChatGpt now checks options.signal?.aborted before issuing the upstream call, after the call returns, and tailFile listens for signal.abort to stop tailing the temp file. tls-client-node's koffi binding can't cancel an in-flight request mid-call (the binary call is opaque), but we no longer process or re-emit a response that the caller has already given up on.

1dd7ec9 — Multi-turn fix found via Open WebUI testing

A user reported in real Open WebUI testing that responses across turns concatenated:

user "test 1"                            → "1"
user "test 2"                            → "12"   (should be "2")
user "test 3. reply only with 3"         → "1123" (should be "3")

Root cause: when sending prior turns as separate assistant-role entries in /backend-api/f/conversation's messages array, ChatGPT's web API (action: "next") treats them as in-progress messages the model can extend rather than as completed turns — so the new generation literally appends to the most recent assistant message.

Fix: fold full history into the system message as plain text, send only the current user query as a single new turn. Verified:

Turn 1 → "1"
Turn 2 → "2"
Turn 3 → "3"

"My favorite color is teal" / "Got it" / "What color?"  →  "Teal"
"Remember 77" / "OK" / "What number?"  →  "77"  (streaming, real-time chunks)

Tests

All 27 chatgpt-web tests still pass. Updated several to find calls by URL via findIndex (since adding the warmup sequence and two-stage Sentinel handshake shifted positional offsets), and updated two body-shape tests to match the new single-user-message + history-in-system-message structure.

@diegosouzapw
diegosouzapw changed the base branch from main to release/v3.7.0 April 25, 2026 13:59
@diegosouzapw

Copy link
Copy Markdown
Owner

Manually merged during stabilization of release/v3.7.0

@payne0420

Copy link
Copy Markdown
Contributor Author

I have one more commit incoming @diegosouzapw, I'll open a new PR against the release/v3.7.0 branch

diegosouzapw pushed a commit that referenced this pull request Apr 25, 2026
Integrated into release/v3.7.0. Thank you for the contribution @trader-payne!
@diegosouzapw diegosouzapw mentioned this pull request Apr 25, 2026
payne0420 added a commit to payne0420/OmniRoute that referenced this pull request Apr 29, 2026
… 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>
This was referenced Apr 30, 2026
Poid-ZA pushed a commit to Poid-ZA/OmniRoute that referenced this pull request Aug 5, 2026
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>
Poid-ZA pushed a commit to Poid-ZA/OmniRoute that referenced this pull request Aug 5, 2026
Poid-ZA pushed a commit to Poid-ZA/OmniRoute that referenced this pull request Aug 5, 2026
…w#1596)

Integrated into release/v3.7.0. Thank you for the contribution @trader-payne!
muhamadgalihsaputra pushed a commit to niyatna/NiyatnaRoute that referenced this pull request Sep 27, 2026
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>
muhamadgalihsaputra pushed a commit to niyatna/NiyatnaRoute that referenced this pull request Sep 27, 2026
muhamadgalihsaputra pushed a commit to niyatna/NiyatnaRoute that referenced this pull request Sep 27, 2026
…w#1596)

Integrated into release/v3.7.0. Thank you for the contribution @trader-payne!
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants