# 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`](https://github.com/jaredboynton/openai-ws-opencode)
(different backend).

Requires **Node.js 20+**. No Bun toolchain.

## Install

```sh
npm install -g chatgpt-web-opencode
chatgpt-web-opencode setup --global
opencode auth login chatgpt-web
```

From GitHub:

```sh
npm install -g github:jaredboynton/chatgpt-web-opencode
chatgpt-web-opencode setup --global
```

Local checkout:

```sh
cd ~/__devlocal/chatgpt-web
npm install && npm run build
chatgpt-web-opencode setup --global --plugin "chatgpt-web-opencode@file:$(pwd)"
opencode auth login chatgpt-web
```

`setup` 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`** — `priority` returns 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, then
  `api_tool.call_tool` wrapper last). `tool_choice` is passed through on first and continuation
  turns (`resolveToolChoice`; defaults to `auto`). Tool results continue via encoded `call_cgw_*`
  ids. Exactly one tool output is supported per turn; turns with more than one
  `function_call_output` return 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 with `tool_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-native `python` and `container.exec`/`shell`; those remap onto local `python3`/`bash`
   via declared `bash` (`remapNativeToolCall`). Shell-shaped payloads accidentally emitted through
   `python` (for example `pwd` or `ls -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 both `status: finished_successfully` and
   `metadata.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_MODE` Python 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 helper
- `CHATGPT_WEB_FORCE_FETCH=1` — always use `globalThis.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:

```sh
npx tsx scripts/test-code-mode.ts
```

When 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=1` appends 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. `fitToolsToBudget` progressively
  shrinks oversized `tools[]` (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 native `container.exec` call; the bridge extracts and remaps it. Evidence and
  ratchet harness: `docs/task-tool-ratchet-evidence.md`, `scripts/probe-task-ratchet.ts`.

## Dev

```sh
npm install
npm run verify
npm run harness:contract
npm run harness:opencode
npm run harness:live
npm run harness:release
npm run harness:scan
```

`verify` 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](SECURITY.md).
