Skip to content

feat(providers): restore ChatGPT Web via clean-room browser transport - #12239

Merged
diegosouzapw merged 6 commits into
cheaper-inference:release/v3.8.51from
backryun:codex/restore-chatgpt-web-cleanroom
Sep 2, 2026
Merged

diegosouzapw merged 6 commits into
cheaper-inference:release/v3.8.51from
backryun:codex/restore-chatgpt-web-cleanroom

Conversation

@backryun

@backryun backryun commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Restore the retired chatgpt-web provider with an independently implemented, first-party browser transport.
  • Reconstruct authenticated ChatGPT browser sessions from the provider cookie, perform the current first-party handshake, and translate Delta V1 streaming responses into OmniRoute chat output.
  • Support the observed ChatGPT Pro and Free routes, including GPT-5.6 Luna Free and Free Thinking.
  • Pass image, text, PDF, DOCX, and other file attachments through the clean-room upload/turn flow without imposing an OmniRoute-side MIME allowlist.
  • Add migration 171 to seed the restored provider while preserving the separate native chatgpt-web-codex tool bridge.

Related Issues

  • No linked issue. This restores functionality removed when the previous implementation's provenance could not be verified.

Validation

Choose the change type and focused loop from the
Contribution Golden Path. The full unit suite,
Vitest, the 60% coverage gate, and the production build all run in CI on this PR (#8329):

  • Change type: provider / DB
  • Focused tests and category gates from the golden path
  • npm run lint — left to CI; focused lint/type contracts are covered by the checks below
  • Reconciled with the current active release base; focused checks rerun afterward
  • Production-code changes include new and updated automated tests in this PR

Focused results:

  • Clean-room provider and transport tests: 50/50 passed.
  • Clean-room branch combined with the v4.0.7 Codex connector branch: 92/92 passed.
  • npm run typecheck:core: passed on the combined tree.
  • npm run check:open-sse-typecheck: passed with zero live errors on the combined tree.
  • Live browser smoke checks returned exact sentinels for image, TXT, PDF, and DOCX attachments.
  • Live OmniRoute Codex v2 tool round-trip also passed through the separate Codex connector.
  • Current release/v3.8.51: 0 commits behind, 4 commits ahead; git merge-tree --write-tree succeeds without conflicts.

Tests Added Or Updated

Dedicated clean-room coverage:

  • tests/unit/chatgpt-web-browser-session-cleanroom.test.ts
  • tests/unit/chatgpt-web-cleanroom-provider.test.ts
  • tests/unit/chatgpt-web-delta-v1-cleanroom.test.ts
  • tests/unit/chatgpt-web-executor-adapter-cleanroom.test.ts
  • tests/unit/chatgpt-web-first-party-cleanroom.test.ts
  • tests/unit/chatgpt-web-handshake-handoff-cleanroom.test.ts
  • tests/unit/migration-171-restore-chatgpt-web-cleanroom.test.ts

Updated provider, executor, routing, image, session, and retirement contracts:

  • tests/snapshots/executors/executor-map.json
  • tests/unit/browserPool-proxy.test.ts
  • tests/unit/chatcore-executor-proxy.test.ts
  • tests/unit/chatgpt-web-image-handler-retirement.test.ts
  • tests/unit/chatgpt-web-management-retirement.test.ts
  • tests/unit/chatgpt-web-retirement.test.ts
  • tests/unit/chatgpt-web-runtime-block.test.ts
  • tests/unit/chatgpt-web-source-retirement.test.ts
  • tests/unit/combo-auto-candidate-expansion.test.ts
  • tests/unit/flat-rate-cost-5552.test.ts
  • tests/unit/guardrails/visionBridgeRouter.test.ts
  • tests/unit/hard-session-lease-bypass-inventory.test.ts
  • tests/unit/image-generation-route.test.ts
  • tests/unit/image-registry-gpt56.test.ts
  • tests/unit/image-routes-combo-edits-3214-3215.test.ts
  • tests/unit/keepalive-threshold.test.ts
  • tests/unit/messages-count-tokens-route.test.ts
  • tests/unit/model-listing-capability-5420.test.ts
  • tests/unit/provider-node-reserved-prefix.test.ts
  • tests/unit/resolve-web-provider-host.test.ts
  • tests/unit/session-leases-route.test.ts
  • tests/unit/token-health-check-webcookie.test.ts
  • tests/unit/tokenExtractionConfig.test.ts
  • tests/unit/virtual-auto-combo.test.ts

Coverage Notes

  • Browser handshake, session lifecycle, Delta V1 decoding, attachment normalization/upload, provider registration, executor dispatch, validation, migration behavior, and retirement-contract reversal all have focused regression coverage.
  • The compatibility run against the v4.0.7 connector branch verifies that restoring standard ChatGPT Web does not regress the separate native tool bridge.

Reviewer Notes

  • This is a clean-room implementation based only on observable first-party ChatGPT browser behavior and assets; it does not reintroduce the removed implementation.
  • chatgpt-web intentionally advertises toolCalling: false. Native PC/MCP tool access remains isolated in chatgpt-web-codex, where the outer Codex runtime owns sandbox and approval enforcement.
  • Migration 171 is idempotent and restores the provider seed without overwriting user-managed credentials.
  • The browser protocol is upstream-controlled and may drift; focused tests pin the currently observed handshake, routing, and Delta V1 contracts.

@backryun
backryun force-pushed the codex/restore-chatgpt-web-cleanroom branch 17 times, most recently from a21f125 to bb24dda Compare September 2, 2026 08:33
@backryun
backryun force-pushed the codex/restore-chatgpt-web-cleanroom branch from bb24dda to d15912e Compare September 2, 2026 11:49
…ore-chatgpt-web-cleanroom

Clean merge against the tip. The two chat chokepoints this PR grows (chat.ts +40,
chatCore.ts +30) are recorded in the file-size baseline with an annotation; no other
cap moves. The rebaseline is carried here rather than left in a validation worktree —
the mistake that put the 2026-09-02 waves base-red (cheaper-inference#12434).
@diegosouzapw
diegosouzapw merged commit 7ed8ada into cheaper-inference:release/v3.8.51 Sep 2, 2026
4 of 7 checks passed
@backryun
backryun deleted the codex/restore-chatgpt-web-cleanroom branch September 2, 2026 13:34
Raudbjorn added a commit to Raudbjorn/OmniRoute that referenced this pull request Sep 8, 2026
- Lazily import detectChromeExecutable in the validator, matching the
  chatgpt-web-codex sibling: a static import pulls in the vendor
  adapter's tiktoken WASM chain and crashes Turbopack dev bundling for
  every provider validation request.
- Require a positive integer within setTimeout's int32 delay range for
  the stream-open timeout override, superseding the earlier floor-based
  fix: a fractional value no longer silently disables the deadline, and
  a value beyond Node's timer range no longer fires almost immediately.
- Classify AbortError as 499 client_disconnected before status/message
  matching, so a cancelled turn or client disconnect never counts
  toward the provider breaker or an account cooldown.
- Classify the "Unsupported ChatGPT Session model" message (an
  obsolete/invalid model slug) as a terminal 400 route_unavailable
  instead of falling through to a retryable 502.
- Add the three ChatGPT Session input-error codes (no_user_message,
  unsupported_content_part, vision_unsupported) to the public error
  identifier allowlist so they survive buildErrorBody instead of being
  replaced with generic bad_request.
- Reject chatgpt-session and chatgpt-web-codex from the generic
  bulk-web-session import route: both read their encoded, browser-
  verified credential from apiKey, which that route leaves null,
  storing only an unusable raw cookie in providerSpecificData.
- Strengthen the dashboard-modal regression guard to pin the exact
  identifier chain (usesChatGptBrowserSessionCredentials → ternary
  condition → { version: 1, cookie } envelope) instead of only
  checking the predicate's name appears in the file.
- Correct the changelog fragment and provider doc: chatgpt-web itself
  was restored (cheaper-inference#12239); only its old cgpt-web alias remains retired.
  Also fix "MIT-noticed" -> "MIT-licensed" in the provider doc.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JhWg8Hn1HBhmoHM4a2JmJ5
Raudbjorn added a commit to Raudbjorn/OmniRoute that referenced this pull request Sep 8, 2026
* feat(providers): add chatgpt-session model route table

* feat(providers): translate OpenAI messages for chatgpt-session

* feat(providers): classify chatgpt-session failures onto the router contract

* fix(providers): give TimeoutError name precedence over message matching in chatgpt-session error classification

* feat(providers): bridge chatgpt-session adapter events to OpenAI payloads

* fix(providers): surface mid-stream chatgpt-session errors as structured SSE error chunks

- Route mid-stream error events through formatTranslatedStreamError instead of
  silently closing with finish_reason:stop, so a truncated turn is distinguishable
  from a completed one (AGENTS.md Hard Rule #6).
- Sanitize the message on the pre-stream error verdict as defense in depth.
- Cover the previously-untested gating paths: terminal event as the first
  meaningful event, source ending without a terminal event, empty source,
  heartbeat-only source, and incomplete with endTurn:true.

* feat(providers): add the chatgpt-session executor host

* fix(providers): preserve chatgpt-session stream error classification and guarantee queue close

Keep the bridge's status/code authoritative on the streaming error branch instead of
re-deriving them from the message alone, which downgraded a name-classified 400 into a
breaker-tripping 502. Also guarantee events.close() runs even when the storage-state
persist path or a caller-supplied logger throws, so neither consumer can hang.

* feat(providers): register the chatgpt-session provider and executor

* feat(providers): wire chatgpt-session into the dashboard and validation

* fix(providers): route chatgpt-session through the shared browser-session credential lifecycle

Both /api/providers routes gated the raw-cookie -> verified-storage-state
finalize step on a hardcoded chatgpt-web-codex check, so chatgpt-session
connections never had their pasted cookie replaced, never released the
temporary validation directory, and leaked validationId into persisted
providerSpecificData. Introduce usesChatGptBrowserSessionCredentials() as
the single source of truth for both provider ids and use it in place of
the hardcoded equality checks. Also replace a brittle source-regex test
with one that proves the validation dispatch actually resolves.

* docs(providers): document the chatgpt-session provider

* docs(changelog): add chatgpt-session provider entry

* test: bump RESERVED_PREFIX_COUNT ratchet to 402 for chatgpt-session provider

The chatgpt-session provider adds two distinct reserved prefixes (id
chatgpt-session, alias cgpt-session), moving the shared reserved-prefix
set from 400 to 402 members. Adds explicit membership assertions for
both new prefixes alongside the count.

* fix(sse): drop the Codex read-only banner and align chatgpt-session failover

The vendored adapter emits its "local Codex computer is unavailable" warning as a
commentary-phase text_delta on every fresh turn, because this provider pins
localToolsEnabled: false. The bridge treated every text_delta alike and let
assistant_boundary fall through to `default`, so 100% of answers were prefixed with the
banner. Commentary-phase deltas are now dropped on both the streaming and the buffered
path (not rerouted to reasoning_content — it is transport chatter, not model reasoning),
and assistant_boundary gets an explicit ignoring case.

Same commit aligns the streaming gate with the buffered failover: the stream no longer
commits a 200 on a heartbeat, an assistant boundary, a commentary delta, an empty text
delta or a thinking delta, so `thinking_delta` followed by `error{429}` now returns HTTP
429 on both paths instead of a 200 stream carrying an in-band error. Reasoning that
arrives while the gate is closed is buffered and replayed once the gate opens.

* fix(sse): trust the adapter's explicit status over message patterns

CONTRACT CHANGE. classifyChatGptSessionError put message-pattern matching ahead of an
explicit numeric status, so an adapter error carrying status 503 whose prose mentioned
signing in was classified 401 session_expired — marking a healthy account's credentials
expired and pulling it out of rotation. The vendor documents the field as "Authoritative
upstream/proxy status when known; avoids message-based classification"
(vendor/codex-chatgpt-web/types.ts).

Explicit status now runs third (after ChatGptSessionInputError and TimeoutError, both of
which are stronger signals), uses the event's own code when present, and attaches
fallbackHint: "connection_cooldown" for 503/429 so a genuine outage cools one connection
instead of tripping the whole-provider breaker. Message matching is unchanged and still
essential — errors thrown by the executor itself carry no status.

ChatGptSessionStreamOpen's error arm now carries fallbackHint, so the executor uses the
bridge's classification directly instead of re-classifying the sanitized message.

The test "a matching message wins over an explicit upstream status" is deliberately
inverted and renamed; its comment records why.

* fix(dashboard): encode chatgpt-session credentials with the shared predicate

Both connection modals wrapped the pasted Cookie header in the {version, cookie,
runtimeKey} envelope only when provider === "chatgpt-web-codex", but the create and
update routes send every provider accepted by usesChatGptBrowserSessionCredentials()
through finalizeValidatedChatGptWebCodexSecrets, whose first statement is JSON.parse. A
chatgpt-session save therefore always failed with 400 "Unexpected token '_',
"__Secure-n"... is not valid JSON" — the dashboard could never create or update a
connection.

Both modals now key the envelope off the same predicate the routes use, so client and
server cannot drift again, and omit runtimeKey when empty (this provider never has one).

The two failure paths returned error.message raw, echoing the first characters of the
pasted credential; they now go through sanitizeErrorMessage, and their untranslated
German fallback is replaced with provider-neutral English.

* fix(sse): reject unsupported chatgpt-session content parts instead of dropping them

textFromContent threw for image parts but silently skipped everything else, so a `file`
or `input_audio` part vanished and the model answered about content it never received.
Any part that is neither a text part nor an already-handled image part now throws
ChatGptSessionInputError with code "unsupported_content_part" (a terminal 400).

* docs(providers): record the adapter task-framing limitation for chatgpt-session

Turns are relayed through the vendored adapter's task-framing prompt, which API clients
never see; note that a tool turn may be answered with a local-tool refusal instead of a
tool block, explicitly flagged as unconfirmed pending live validation.

The markdown tables are reflowed by the lint-staged Prettier hook.

* fix(sse): accept assistant refusal content parts in chatgpt-session

A `{ type: "refusal", refusal: "…" }` part is legal inside an assistant
message in the chat-completions spec, so a client replaying its own
conversation history sends it back. The unsupported-part guard rejected it
with a 400 and failed the whole request.

Fold a refusal part into the flattened content the way a text part is folded,
reading its `refusal` field. A refusal part whose payload is missing or is not
a string still falls through to the existing rejection — it is never dropped
in silence — and every other unrecognised part keeps throwing as before.

* fix(sse): bound the chatgpt-session stream-open gate with a deadline

The gate withholds the HTTP response until the first event that also counts
as committed output on the buffered path, so both paths agree on failover
status. While it is closed nothing reaches the client — not even a keepalive,
since the first byte commits the 200 — and a turn here runs in a real browser
that can think for a long time, so a client with an idle timeout could hang up
on a perfectly healthy turn.

Race the gate against CHATGPT_SESSION_STREAM_OPEN_TIMEOUT_MS (30s, overridable
per call through the new options argument). A committing event or an error
still wins exactly as before; only when the deadline elapses with neither does
the stream open anyway and keep consuming the same iterator, replaying the
buffered reasoning after the role chunk so nothing is lost or reordered. The
in-flight next() the deadline outran is carried into the stream body instead of
being abandoned, otherwise its event would vanish behind a second next(). The
timer is unref'd and cleared on every exit path.

Every failure that needs a real HTTP status — no browser, missing or expired
credentials, rate limiting, an incompatible route — surfaces within seconds,
far inside the window.

* feat(executors): make chatgpt-session stream-open timeout tunable

Add OMNIROUTE_CHATGPT_SESSION_STREAM_OPEN_TIMEOUT_MS to override the
30s CHATGPT_SESSION_STREAM_OPEN_TIMEOUT_MS default, mirroring
resolveDirectHeadersTimeoutMs's validation. Resolved per request in
the executor so a changed env var takes effect without a restart.

* fix(sse): synthesize the native Codex turn envelope for chatgpt-session

The executor passed the OpenAI chat-completions body through as `_rawBody`, but the
vendored browser adapter reads `_rawBody` as a native Codex Responses body and demands
turn identity from it. Every request therefore failed with "ChatGPT web requires native
Codex turn_id metadata for browser-session replay" before any browser work started; no
unit test caught it because they all mock the adapter call.

`buildParsedRequest` now builds the envelope itself: a fresh thread/turn id pair per
request (this provider serves stateless chat completions, so each request genuinely is
its own turn), the turn metadata as the JSON string the real client sends, an `input`
array mirroring the parsed messages in Responses item shape, and the current-turn
passthrough marker on the last user item only. The `rawBody` parameter is gone so no
caller can reintroduce the passthrough.

Verified against the real adapter through the production code path: the turn now reaches
stage=browser_page and fails only on the missing login state.

* fix(chatgpt-session): address PR review threads on combo routing and cleanup

- Phrase Sol/Pro route-mismatch errors as account-scoped model access
  denials so combo routing rotates to another connection instead of
  stopping (account-fallback's MODEL_ACCESS_DENIED_PATTERNS).
- Carry the classified fallbackHint through the buffered-completion
  path and set X-Omni-Fallback-Hint on non-streaming responses.
- Keep a trailing adapter error authoritative on the buffered path even
  when partial text was already produced — the HTTP status hasn't
  committed yet, unlike streaming.
- Reject a stream-open timeout override that floors to zero instead of
  silently disabling the deadline.
- Honor the connection_cooldown fallback hint on 429, not just 503, so
  a ChatGPT usage-limit response cools the one connection instead of
  tripping the whole-provider circuit breaker.
- Add bounded cleanup for abandoned browser-verification state: a
  validation that succeeds but is never finalized no longer retains
  live session cookies on disk indefinitely.
- Fix a stale README TOC anchor.

Also resolves two provider-registry duplicates (chatgpt-web-codex
import/map entry, WEB_COOKIE_PROVIDERS entry) left behind by a prior
merge of origin/dev that was textually clean but semantically wrong.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JhWg8Hn1HBhmoHM4a2JmJ5

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* fix(chatgpt-session): address second wave of PR review threads

- Lazily import detectChromeExecutable in the validator, matching the
  chatgpt-web-codex sibling: a static import pulls in the vendor
  adapter's tiktoken WASM chain and crashes Turbopack dev bundling for
  every provider validation request.
- Require a positive integer within setTimeout's int32 delay range for
  the stream-open timeout override, superseding the earlier floor-based
  fix: a fractional value no longer silently disables the deadline, and
  a value beyond Node's timer range no longer fires almost immediately.
- Classify AbortError as 499 client_disconnected before status/message
  matching, so a cancelled turn or client disconnect never counts
  toward the provider breaker or an account cooldown.
- Classify the "Unsupported ChatGPT Session model" message (an
  obsolete/invalid model slug) as a terminal 400 route_unavailable
  instead of falling through to a retryable 502.
- Add the three ChatGPT Session input-error codes (no_user_message,
  unsupported_content_part, vision_unsupported) to the public error
  identifier allowlist so they survive buildErrorBody instead of being
  replaced with generic bad_request.
- Reject chatgpt-session and chatgpt-web-codex from the generic
  bulk-web-session import route: both read their encoded, browser-
  verified credential from apiKey, which that route leaves null,
  storing only an unusable raw cookie in providerSpecificData.
- Strengthen the dashboard-modal regression guard to pin the exact
  identifier chain (usesChatGptBrowserSessionCredentials → ternary
  condition → { version: 1, cookie } envelope) instead of only
  checking the predicate's name appears in the file.
- Correct the changelog fragment and provider doc: chatgpt-web itself
  was restored (cheaper-inference#12239); only its old cgpt-web alias remains retired.
  Also fix "MIT-noticed" -> "MIT-licensed" in the provider doc.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JhWg8Hn1HBhmoHM4a2JmJ5

---------

Co-authored-by: Markus Hartung <diegosouzapw@users.noreply.github.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
muhamadgalihsaputra pushed a commit to niyatna/NiyatnaRoute that referenced this pull request Sep 27, 2026
…cheaper-inference#12239)

Restores ChatGPT Web on a clean-room browser transport, merged on the operator's explicit decision.

Worth stating precisely, because this touches a provenance decision: the PR does not revert cheaper-inference#11754. It narrows RETIRED_COMMON_CHATGPT_WEB_PROVIDER_IDS to the single GPL-derived alias cgpt-web and registers chatgpt-web as a separate clean-room id. The old implementation stays retired and blocked; the retirement machinery, its error code and its 410 contract are untouched. All four retirement suites agree with that distinction and pass unchanged.

The 44 protected agent-instruction surfaces this PR touches (AGENTS.md, llm.txt and its 42 mirrors, README) were verified rather than trusted: masking digits and comparing the removed and added line sets gives 264 lines on each side, identical — every change is a provider-count substitution, with no sentence added, removed or reworded.

Reconciled on merge: clean against the tip, with the two chat chokepoints this PR grows (src/sse/handlers/chat.ts +40, open-sse/handlers/chatCore.ts +30) recorded in the file-size baseline under an annotation. Verified that exactly those two caps move and nothing else, so the cheaper-inference#12411 ratchet holds. The rebaseline is carried on this branch rather than left in a validation worktree — the propagation mistake that put the 2026-09-02 merge waves base-red in cheaper-inference#12434.

Verified: 504/504 across the PR's 44 test files plus all four chatgpt-web retirement suites, check:provider-consistency OK (272 REGISTRY entries, 355 canonical providers), check-file-size OK, and every changed TypeScript file parses.

Thanks @backryun — separating the clean-room id from the retired alias, instead of reopening the old one, is what made this reviewable.
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