Skip to content

OpenCode Go sessionless requests still omit x-opencode-session in 2.49.0 (client probes fail with 400 or stall into a timeout) #4172

Description

@chiyan171

Client or integration

Claude Code

Provider or upstream service

OpenCode Go (opencode-go)

OpenCodex version

2.49.0 (also present in 2.48.0 and in the reviewed src/providers/opencode-go-transport.ts of 2.49.0)

Endpoint or capability

POST /v1/messages (Claude Code / Claude Desktop three-party profile), OpenCode Go session affinity on requests that carry no conversation identity. Measured below on /v1/messages; the same helper is shared with the native /v1/chat/completions and the Chat-to-Responses bridge, which pass the same header-derived lane, so a sessionless request on those ingresses branches identically (read from source, not separately measured).

Current behaviour

resolveOpenCodeGoTransport still returns the provider unchanged when no session lane is available:

// src/providers/opencode-go-transport.ts (2.49.0)
if (registryEntryForProviderDestination(provider)?.id !== "opencode-go") return provider;
if (!sessionLane) return provider;                       // <-- sessionless requests leave here
if (hasHeaderCaseInsensitive(provider.headers, OPENCODE_GO_SESSION_HEADER)) return provider;

The lane is derived only from client-supplied identity — sessionLaneIdFromRequest(req.headers) (x-codex-parent-thread-id, thread-id, session_id / session-id) or an inbound x-opencode-session. A minimal Claude request has none of those: Claude Desktop's model-availability probe, and any first request before per-conversation metadata exists, carry no metadata.user_id, no session_id, and no x-opencode-session. The upstream Go request is therefore sent with no x-opencode-session at all.

Console Go handles that inconsistently, and both outcomes are visible in production:

  • it returns 400 invalid_request_error, which Claude Desktop renders as "Configured model not available";
  • or it accepts the connection and stalls, exceeding the client's probe budget and getting cancelled, which renders as "Can't reach 127.0.0.1:10100 — the operation was aborted due to timeout".

Request-log evidence from one local instance (surface claude-desktop, provider opencode-go): 4 rows failed with exactly this missing-session error (2026-09-09 12:26:18, 17:41:17, 17:45:21, and 2026-09-10 09:03:29), and 6 rows ended 499 client_closed_request — the one at 2026-09-10 09:01:52 ran durationMs: 7524 before the client gave up. No missing-session row was recorded for any other provider on that surface, so the failure tracks the Go route and not the proxy or the network.

Note this is the remaining branch of #3945, not a regression of it. That fix correctly gives requests with identity a stable lane; requests with no identity at all still bypass the header entirely.

Expected behaviour

Every request routed to the canonical Go destination should carry x-opencode-session, so that no Go request is ever sent without it.

  • Requests that carry real conversation identity keep the existing stable, per-conversation value.
  • Requests with no identity should receive an isolated per-request value rather than being sent unheaderised or sharing one value. A per-request id keeps unrelated probes from being smeared into one conversation and does not become the "global static session id" that [Provider compatibility] Pi native Chat requests still omit OpenCode Go session affinity in 2.46.0 #3857 warns against.
  • An explicitly supplied x-opencode-session must keep winning, unchanged.

Minimal redacted request or reproduction

With an authenticated OpenCode Go provider configured in the local proxy (the proxy owns the key; this client sends no credential):

# A Claude-shaped request with no metadata.user_id, no session_id, no x-opencode-session.
curl -sS http://127.0.0.1:10100/v1/messages \
  -H 'Content-Type: application/json' \
  -H 'anthropic-version: 2023-06-01' \
  -d '{"model":"claude-opus-4-8-20261111","max_tokens":16,"messages":[{"role":"user","content":"ping"}]}'

claude-opus-4-8-20261111 is the local Claude Desktop alias assigned to opencode-go/deepseek-v4-flash; any alias mapped onto an opencode-go route reproduces the same path. Adding -H 'x-opencode-session: <uuid>' to the identical request returns 200, which isolates the missing header as the cause.

Actual response or error

Sessionless (fails):

HTTP 400
{"type":"error","error":{"type":"invalid_request_error","message":"Error from provider (Console Go): Request is missing x-opencode-session and cannot be routed efficiently. Please see https://opencode.ai/docs/go/#where-can-i-use-it"}}

Same request with an x-opencode-session header (succeeds):

HTTP 200
{"id":"msg_...","type":"message","role":"assistant","content":[{"type":"text","text":"OK"}],"model":"claude-opus-4-8-20261111","stop_reason":"end_turn"}

Intermittent stall variant observed at the client: probe cancelled after 7524 ms and reported as an unreachable gateway (client_closed_request / client_cancel, no upstream response body).

Upstream documentation

https://opencode.ai/docs/go/#where-can-i-use-it

Go does not publish a full header specification; the requirement is stated in the error body and was also communicated directly by updates@opencode.ai (see #3344).

Suggested mapping or implementation notes

The gap is one branch inside the shared helper, so a fix there covers all three ingress paths that call it (/v1/messages → Responses core, native /v1/chat/completions, and the Chat-to-Responses bridge) without touching any caller:

-import { createHash } from "node:crypto";
+import { createHash, randomUUID } from "node:crypto";
@@
-  if (!sessionLane) return provider;
   if (hasHeaderCaseInsensitive(provider.headers, OPENCODE_GO_SESSION_HEADER)) return provider;
@@
-      [OPENCODE_GO_SESSION_HEADER]: deriveOpenCodeGoSessionId(sessionLane),
+      // Sessionless probes still need affinity; isolate them rather than sharing a conversation.
+      [OPENCODE_GO_SESSION_HEADER]: deriveOpenCodeGoSessionId(sessionLane || randomUUID()),
@@

deriveOpenCodeGoSessionId already hashes its input to an opaque ocx_<32 hex> value, so a random lane stays opaque and provider-scoped. The one behaviour worth pinning in a regression test: two consecutive sessionless requests must not receive the same id, while a real lane must stay stable across repeats and an explicit inbound header must pass through untouched.

Additional context and attachments

Locally applied and verified on 2.49.0 with the diff above:

  • sessionless probe shape, 5 consecutive requests: 200, 200, 200, 200, 200; slowest 4.1 s (probe budget is roughly 7.5 s).
  • the reported alias plus 5 other Go routes returned 200 after the change; before it, the identical sessionless request returned the 400 above.
  • after the restart that picked up the change, the request log records 92 Go requests, all 200, and zero missing x-opencode-session rows.

Related: #3945 (fixed affinity when identity is present — this is the remaining sessionless branch), #3344, #3378, #3857.

Checks

  • I searched existing provider and compatibility issues.
  • The request and response were redacted.
  • The expected behaviour is based on an upstream specification or a concrete client requirement.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    providerProvider adapters, OpenAI-compat presets, upstream API quirksprovider-compatibilityProvider compatibility reports

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions