chatgpt-web-opencode
v0.9.5
Published
OpenCode plugin for ChatGPT web /backend-api/f/conversation (SSE).
Readme
chatgpt-web-opencode
OpenCode plugin that routes @ai-sdk/openai Responses streaming requests to ChatGPT web
/backend-api/f/conversation (prepare → Sentinel PoW → SSE), not Codex Responses WS. Streaming is
real and incremental: deltas emit live as ChatGPT SSE arrives, not buffered until EOF.
Provider id: chatgpt-web. Sibling to openai-ws-opencode
(different backend).
Requires Node.js 20+. No Bun toolchain.
Install
npm install -g chatgpt-web-opencode
chatgpt-web-opencode setup --global
opencode auth login chatgpt-webFrom GitHub:
npm install -g github:jaredboynton/chatgpt-web-opencode
chatgpt-web-opencode setup --globalLocal checkout:
cd ~/__devlocal/chatgpt-web
npm install && npm run build
chatgpt-web-opencode setup --global --plugin "chatgpt-web-opencode@file:$(pwd)"
opencode auth login chatgpt-websetup owns repair of OpenCode's chatgpt-web-opencode@latest cache. When that cache is
missing its package metadata or contains an older version than the setup package, setup moves it
aside to a timestamped .stale-<ISO timestamp> path so OpenCode can reinstall it. Use
--no-cache-repair to skip this step. OpenCode launch does not perform this plugin cache repair.
Models
| OpenCode id | Wire slug |
|-------------|-----------|
| gpt-5.6-luna-pro | gpt-5-6-luna-pro |
| gpt-5.6-terra-pro | gpt-5-6-terra-pro |
| gpt-5.6-sol-pro | gpt-5-6-sol-pro |
| gpt-5.5-pro | gpt-5-5-pro |
| gpt-5.5-thinking | gpt-5-5-thinking |
| gpt-5.5-instant | gpt-5-5-instant |
Variants map OpenCode reasoningEffort → ChatGPT thinking_effort
(medium→standard, high→extended, xhigh/max→max; thinking also supports low→min).
Auth
ChatGPT / Codex OAuth only (same Auth0 client as Codex CLI). Uses OpenCode’s auth store
for provider chatgpt-web. Codex bearer + Chrome-like browser headers are enough for the
text path (inert oai-did cookie). No ChatGPT session cookie / UMP envelope required.
Refresh is singleflight within the plugin process and guarded across processes by
$XDG_DATA_HOME/opencode/.chatgpt-web-refresh.lock (or the platform default XDG data home).
Under that lock it reloads auth, persists the issuer-rotated refresh token before using the new
access token, and rejects a response without a rotated token; it never falls back to the old token.
Wire notes
- Never sends
service_tier—priorityreturns HTTP 422 on/f/conversation. - Never sends a null
x-conduit-token— the header is omitted when conversation prepare returns no conduit token. - SSE-only (no Celsius WSS handoff).
- Multi-turn via adapter-owned
previous_response_id→{conversation_id, message_id}cache. - Function tools: OpenAI
tools[]+local_function_names(bare declared names first, thenapi_tool.call_toolwrapper last).tool_choiceis passed through on first and continuation turns (resolveToolChoice; defaults toauto). Tool results continue via encodedcall_cgw_*ids. Exactly one tool output is supported per turn; turns with more than onefunction_call_outputreturn HTTP 400 rather than dropping output. Hosted tools / images rejected. - Tool schemas over the ~24 KB body budget are progressively shrunk (mild 400-char trim → 200 →
strip param descriptions → skeleton) instead of dropped wholesale (
fitToolsToBudget). - Prompts with declared function tools get an imperative
<tool_availability>trailer (buildToolAvailabilityGuard) with a budgeted, model-visible compact schema roster. The trailer requires an immediate tool call and forbids "tools unavailable" narration — no single-call ceiling. If a workspace-action turn still returns prose without a call, the bridge issues one parented continuation retry withtool_choice: required; literal JSON from that retry is validated and converted into a local call so ChatGPT's remote sandbox cannot consume it. It never loops. Luna's reliable emission channels are harness-nativepythonandcontainer.exec/shell; those remap onto localpython3/bashvia declaredbash(remapNativeToolCall). Shell-shaped payloads accidentally emitted throughpython(for examplepwdorls -la) are healed to direct local bash instead of invalid Python. Declared OpenCode function names are re-advertised, but Luna often will not emit them directly. Tool bodies are buffered across SSE patches and emitted only after that tool message reports bothstatus: finished_successfullyandmetadata.is_complete: true; assistant handoff,[DONE], and EOF discard an incomplete call, so OpenCode never executes a partial function. Connector/plugin calls remain disabled.# CODE_MODEPython bodies may enter the CodeAct route. The upstream stream is cut only after a complete remapped call is emitted. Tools stay re-advertised on tool-result continuation turns. - If Sentinel returns Turnstile
dx, empty turnstile is tried first; hard fail if finalize rejects. - Conversation prepare and Sentinel admission run concurrently (
Promise.all). Finalized Sentinel requirements tokens are cached per account for 60 seconds and lazily re-solved on conversation HTTP 401/403; warm turns perform no PoW solves.
Runtime (OpenCode on Bun)
OpenCode often loads plugins under Bun. Bun’s TLS fingerprint is Cloudflare-blocked on
chatgpt.com, so this plugin runs the shipped bin/chatgpt-fetch.mjs --daemon helper with
your system node binary for ChatGPT-host requests. The persistent daemon multiplexes requests
over newline-delimited JSON frames and keeps a warm keep-alive connection pool, avoiding per-request
spawns and cold TLS. shutdownHelper() tears it down when the session or server is disposed.
Node-native hosts use fetch directly.
CHATGPT_WEB_FORCE_NODE_HELPER=1— always use the Node helperCHATGPT_WEB_FORCE_FETCH=1— always useglobalThis.fetch(will fail under Bun for ChatGPT)
Code mode
The chatgpt-web-opencode/code-mode export provides startCodeRun(): a single-use Node
worker_threads + node:vm CodeAct runtime with batched tools.* waves, per-run state, capped
artifacts, and local bash, read, write, and exact edit execution. node:vm supplies timeout
and crash isolation; it is not a security sandbox. Under a Bun/OpenCode host, the VM is moved into
the shipped bin/chatgpt-code-mode.mjs system-Node helper. Run the standalone vertical slice with:
npx tsx scripts/test-code-mode.tsWhen a Responses request declares code, a // CODE_MODE request or a model-emitted
code({script}) enters the runtime. Tool waves return as ordinary Responses function_call items
and resume through previous_response_id; set CHATGPT_WEB_CODE_MODE_LOCAL=1 to execute supported
tools inside the plugin instead. CHATGPT_WEB_CODE_MODE=1 also injects the synthetic code tool and
enables // CODE_MODE / #codemode shell-marker interception.
The runtime may apply compile-verified script salvages for a trailing main() (rewritten to
return await main()), expression auto-invocation, or backtick healing. When a heal fires,
startCodeRun() may set result.salvage; disable these salvages with
CHATGPT_WEB_CODE_SCRIPT_HEAL=0.
Environment knobs
| Variable | Purpose |
|---|---|
| CHATGPT_WEB_CODE_MODE | Enable code mode + synthetic code tool |
| CHATGPT_WEB_CODE_MODE_LOCAL | Execute supported tools locally instead of upstream |
| CHATGPT_WEB_CODE_TIMEOUT_MS | Per-run timeout |
| CHATGPT_WEB_CODE_MAX_WAVES | Max tool waves per run |
| CHATGPT_WEB_CODE_MAX_CALLS | Max calls per wave |
| CHATGPT_WEB_CODE_MAX_OUTPUT_BYTES | Output cap |
| CHATGPT_WEB_CODE_ARTIFACT_MAX_BYTES | Artifact size cap |
| CHATGPT_WEB_CODE_SCRIPT_HEAL | Script salvage/heal (0 = off) |
| CHATGPT_WEB_CODE_WAVE_IDLE_MS | Idle delay between waves |
| CHATGPT_WEB_CODE_EXEC | Local exec (0 = disabled) |
| CHATGPT_WEB_CODE_REGISTRY | Script registry (0 = disabled) |
| CHATGPT_WEB_CODE_REGISTRY_DIR | Registry directory override |
| CHATGPT_WEB_CODE_SKILLS | Skill store (0 = disabled) |
| CHATGPT_WEB_CODE_SKILLS_DIR | Skills directory override |
| CHATGPT_WEB_CODE_ANCHORS | Anchor store (0 = disabled; boot-latched: read once per process) |
| CHATGPT_WEB_CODE_WARP | Warp edits (0 = disabled; boot-latched: read once per process) |
| CHATGPT_WEB_CODE_POOL | Daemon worker pool (0 = single-shot) |
| CHATGPT_WEB_CODE_POLL | Explicit poll/monitor (0 = disabled) |
| CHATGPT_WEB_CODE_POLL_YIELD | Poll backoff yields exec permit via __codemode_sleep wave (0 = in-VM only) |
| CHATGPT_WEB_CODE_BRIDGED | Code-mode bridging (0 = off; reserved / not shipped) |
| CHATGPT_WEB_CODE_AUTO_MONITOR | Auto-monitor (0 = off; reserved / not shipped) |
| CHATGPT_WEB_CODE_POLL_AWAIT | Poll await (0 = off; reserved / not shipped) |
| CHATGPT_WEB_CODE_SKILL_LEARN | Skill learning (0 = off; reserved / not shipped; no learner shipped) |
Debugging
CHATGPT_WEB_DEBUG=1appends full conversation bodies and raw SSE chunks to~/.cache/chatgpt-web-opencode/debug.log. It does not capture HTTP headers, bearer tokens, cookies, or credentials. Payload content is otherwise unredacted; delete the log after use.
Limitations
- Not a public OpenAI API credential path.
- Function tools are supported; hosted tools / images are rejected.
fitToolsToBudgetprogressively shrinks oversizedtools[](400 → 200 → strip param descriptions → skeleton) and keeps tools visible on continuation turns. Live probes:npm run live:tools,npx tsx scripts/live-tool-loop.ts. - The ChatGPT web backend does not surface declared OpenCode tools to the model, so indirect tools
(
task,code,question,todowrite,skill) are reached via a__opencode_tool__JSON payload carried in a nativecontainer.execcall; the bridge extracts and remaps it. Evidence and ratchet harness:docs/task-tool-ratchet-evidence.md,scripts/probe-task-ratchet.ts.
Dev
npm install
npm run verify
npm run harness:contract
npm run harness:opencode
npm run harness:live
npm run harness:release
npm run harness:scanverify runs lint, typecheck, tests, and build. The contract/OpenCode harnesses and receipt scan are separate (npm run harness:contract, npm run harness:opencode, npm run harness:scan).
harness:contract runs the targeted contract tests. harness:opencode packs and installs the
current artifact at an explicit local file URL and uses deterministic isolated auth against its
loopback fake backend, so neither requires a live credential. harness:live packs and installs
the local artifact unless CHATGPT_WEB_PLUGIN_SPEC selects an installed package; it uses
OPENCODE_AUTH_JSON, or a host OpenCode auth store when that variable is absent, for real service
calls. harness:release requires HARNESS_RELEASE_EXECUTE=1, an authenticated npm session
(NODE_AUTH_TOKEN in CI), and live OpenCode auth. It publishes an unpublished immutable version
under the canary dist-tag and promotes those exact bytes to latest only after all gates pass.
OpenCode and live harness reports are written under artifacts/harness/ as report.json,
junit.xml, and report.md; run npm run harness:scan before retaining or uploading them. Exit classes
are 0 (pass), 2 (product/contract failure), 3 (external dependency, credential, or authorization
block), and 4 (harness/infrastructure failure). CI is .github/workflows/ci.yml; nightly live checks are
.github/workflows/nightly-live.yml; the manually dispatched canary release is
.github/workflows/canary-release.yml. Release performs registry, dist-tag, tag, and GitHub
release writes only when explicitly authorized; do not run it for ordinary verification.
Security
Never log bearer tokens, cookies, account/device/session IDs, or raw user content in tests. See SECURITY.md.
