Skip to content

feat(core): enable the responses websocket by default - #48140

Merged
nexxeln merged 8 commits into
v2from
websocket-default
Sep 9, 2026
Merged

feat(core): enable the responses websocket by default#48140
nexxeln merged 8 commits into
v2from
websocket-default

Conversation

@nexxeln

@nexxeln nexxeln commented Sep 9, 2026

Copy link
Copy Markdown
Member

Summary

Turn the OpenAI Responses WebSocket channel on by default for every route that advertises responsesWebsockets (OpenAI, Azure, xAI), after two days of live verification with an API key and a ChatGPT Codex credential, and give it a config switch shaped like compaction.

Behaviour changes

  • Default on. providers.<id>.websocket: false (or per model) keeps a provider on HTTP; the model policy overrides the provider policy, and providers with http.request/http.response hooks stay on HTTP as before. The experimental OPENCODE_EXPERIMENTAL_<P>_RESPONSES_WEBSOCKET opt-in is gone: it defaulted to false, was never documented, and config now owns the policy, so there is one switch. Docs: "WebSocket transport" in providers.mdx, one line in config.mdx.
  • Ambiguous WebSocket deliveries retry. HTTP transport errors carry no delivery and always retry; WebSocket post-send failures were ambiguous and never did (fix(ai): harden websocket error contracts #40695 said "never replays after ambiguous delivery" — this reverses that), so a socket dying before the first frame was a hard step failure. Now only accepted and rejected deliveries are final. Pre-output and store: false; worst case is one duplicated request's tokens.
  • Connects are bounded and a failed upgrade is sticky. 10 s connect timeout (there was none), and a Session whose upgrade fails stays on HTTP for the rest of the process, the way the 1009 path already worked. Without this a network that refuses upgrades charges every step a failed connect. Trade-off, stated in the code: a transient blip also pins that Session until restart or move.
  • Codex stale continuations recover. Codex reports a stale previous_response_id as invalid_request_error with no code, so OpenResponsesContinuation never classified it retry-full. An unclassified InvalidRequest on an incremental send is now retried full, read from the canonical classification the base driver already produced — classified failures such as context overflow keep their runner-owned recovery (an earlier draft re-parsed the raw event and would have turned overflow into a wasted full resend; covered by a test).
  • Aborted handshakes keep an error listener. Node's ws reports an aborted handshake as an error event on the next tick; waitOpen removed its listeners before close(), so with the new timeout every hung connect on the Node build would have raised an uncaught exception. Reproduced with node v22 + ws 8.21 and fixed; Bun is unaffected (its ws shim is the native EventTarget socket), which is also why this has no Bun test.

Plumbing (feat(core): add the websocket provider policy): Config.Provider/Config.Model → config plugin → Provider.Info.websocket/Model.Info.websocketprojectModel (model.websocket ?? provider.websocket) → Resolved.websocket: boolean (default resolved once, at the resolver) → the gate in prepare, which is now one expression: webSocket: "session" && !hasHttpHooks && capability && policy. Capability ("the route can") stays plugin-owned; policy ("the user wants") is config-owned.

Builds on #47973 (drop the socket after error frames) and #47806/#47974.

Evidence

  • Soaks with the default on, API key (gpt-5.4-mini) and ChatGPT Codex (gpt-5.5): tool-heavy steps, interrupt mid-step, model switch, idle gaps, automatic provider compactions, kill -9 mid-turn + restart. Zero HTTP fallbacks, zero WARN/ERROR, every assistant message error-free, recall intact across compaction and restart. Earlier soaks covered 150 s / 400 s idle gaps (api.openai.com drops idle sockets at ~4.5 min, Codex kept one through 400 s; both reconnect transparently).
  • Config switch verified live on an isolated server: provider false → zero WebSocket activity; provider false + model true → WebSocket for that model; model false → zero.
  • Raw-socket probes: api.openai.com keeps a connection usable after error frames; Codex stops serving it (next request unanswered, 1006 ~2.7 s later) — the reason fix(core): close websocket after provider error frame #47973 drops the socket after every error.

Validation

  • New tests: ambiguous deliveries retry; a failed connect falls back and stays on HTTP (one attempt per Session, not per step); a hanging connect times out to HTTP under TestClock; incremental unclassified InvalidRequestretry-full, full → provider failure, incremental overflow keeps context-overflow; Azure default-on with the policy disabling it; config policy inheritance and model override through the catalog; resolver default websocket: true.
  • bun run test: core 5,292 passed / 35 skipped; @opencode/ai 1,290 passed. bun run generate in packages/client; typecheck clean for schema, ai, core, client, server, sdk, tui, session-ui.
  • Simplify + strict quality pass applied before opening for review (deleted the env switch and legacy fallback, single gate expression, non-optional Resolved.websocket, connect-failure handling as statements, canonical classification, de-duplicated tests and docs).

Not in this PR

@nexxeln
nexxeln merged commit bdc143c into v2 Sep 9, 2026
14 of 15 checks passed
@nexxeln
nexxeln deleted the websocket-default branch September 9, 2026 14:43
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.

1 participant