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

Closed

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

  1. Ingwannu commented on Sep 10, 2026

    @Ingwannu
    Owner

    Thanks for the redacted before/after comparison. I checked the current dev source at c15a98c: src/providers/opencode-go-transport.ts:31 still returns before adding affinity when sessionLane is absent. Both src/server/chat-completions.ts and src/server/responses/core.ts call this helper. That supports the missing-header failure you reported; I have not run a live Go account test.

    The fix should preserve these separate cases:

    • An explicitly configured session header remains unchanged.
    • A real conversation lane remains stable across turns.
    • A sessionless request gets an isolated fallback identity, allocated once for that admitted request and retained across retries/route reconstruction, rather than a global value or a fresh value on each helper invocation.

    That last point needs caller-level coverage before taking the proposed one-line UUID fallback: otherwise retrying the same request can silently change upstream affinity. Regression coverage should include two independent sessionless requests, retries of one request, real lanes, explicit-header precedence, and a non-Go destination remaining untouched.

    The explicit missing-session 400 plus your successful header-added control is useful evidence. The 499/timeouts alone do not establish the same cause, so I would keep that claim separate until a matched trace confirms it. This is the authenticated Go route, not the separate keyless free-tier restriction. Keeping this open for a narrowly scoped fix.

  2. lidge-jun commented on Sep 10, 2026

    @lidge-jun
    Owner

    리뷰 · 우선순위 71 / 80

    이 이슈는 OpenCode Go로 가는 요청 중 대화 정체성이 없는 요청이 아직도 x-opencode-session 없이 Upstream으로 나가는 버그입니다. 지금 dev HEAD(c15a98caa)의 src/providers/opencode-go-transport.ts resolveOpenCodeGoTransport는 이렇게 끝납니다. provider가 opencode-go가 아니면 그대로 반환, sessionLane이 없으면 그대로 반환, 이미 헤더가 있으면 그대로 반환, 있을 때만 deriveOpenCodeGoSessionId로 ocx_… 헤더를 붙입니다. 즉 #3945가 고친 “정체성이 있는 요청”의 레인은 살아 있고, 정체성이 전무인 요청은 의도적으로(또는 미완으로) 헤더 없이 나갑니다.

    피해는 추측이 아닙니다. Claude Desktop / Claude Code의 모델 가용성 probe와, 대화 메타데이터가 아직 없는 첫 요청은 x-codex-parent-thread-id·thread-id·session_id·x-opencode-session이 없습니다. 그러면 Console Go가 400 invalid_request_error(클라이언트엔 “Configured model not available”)를 주거나, 연결만 받고 프로브 예산 안에서 스톨하다 499/client_closed_request로 끊깁니다. 리포터는 로컬 로그에서 missing-session 실패 4건과 타임아웃성 499 6건을 같은 표면(claude-desktop + opencode-go)에서만 봤다고 적었습니다. 같은 요청에 x-opencode-session만 붙이면 200이 나와, 원인이 헤더 누락으로 분리됩니다. /v1/messages뿐 아니라 Chat·Chat-to-Responses 브리지도 같은 helper를 쓰므로 분기 자체는 공유됩니다.

    기대 동작도 #3857이 경고한 “전역 고정 세션 ID”를 피하면서 정리되어 있습니다. 진짜 대화 정체성이 있으면 지금처럼 안정적인 per-conversation 값, 정체성이 없으면 요청마다 고립된 일회성 값, 클라이언트가 명시한 x-opencode-session은 계속 최우선. 이 방향은 현재 dev의 conversation affinity 불변식(#3716·#3880·#3961·#3966 계열)과 맞닿아 있고, Go 경로의 남은 구멍으로 보는 게 맞습니다. provider-compatibility 라벨도 맞습니다. types/config 분할과는 무관합니다.

    라인 문제:

    src/providers/opencode-go-transport.ts 라인 31 - if (!sessionLane) return provider; 가 sessionless early-return의 본체입니다. 여기가 프로브/첫 요청이 헤더 없이 나가는 지점입니다.

    sessionLane 공급자(요청 헤더 파서) - 레인이 x-codex-parent-thread-id / thread-id / session_id / 인바운드 x-opencode-session에만 의존합니다. Claude Desktop probe는 이 집합이 비어 있습니다. transport만 고치고 파서를 안 보면, “빈 레인일 때 ephemeral id” 정책이 빠질 수 있습니다.

    deriveOpenCodeGoSessionId - per-request ephemeral 값을 넣을 때, 빈 문자열·고정 솔트만으로 모든 probe가 같은 ocx_…가 되면 #3857이 피하려던 전역 세션과 같아집니다. 요청마다 새 엔트로피(또는 요청 id)를 섞어야 합니다.

    회귀 테스트 - “레인 있음 → 안정 해시”, “레인 없음 → 헤더 존재 + 두 번 호출 시 값 다름”, “이미 헤더 있음 → 덮지 않음” 세 가지를 단위 테스트로 잠가야 합니다. 지금은 early-return이라 sessionless 긍정 케이스가 없습니다.

    메인테이너의 판단이 필요한 지점

    • sessionless에 ephemeral per-request id를 넣는 제품 결정을 받을지(이슈 제안과 동일), 아니면 Claude probe만 특별 처리할지
    • ephemeral id를 transport에서 만들지, 상위에서 lane을 합성해 내려보낼지
    • Claude Code: OpenCode Go requests lose session affinity through /v1/messages #3945 잔여로 라벨/마일스톤에 묶을지, 독립 버그로 둘지
    • Chat/Messages/Responses 세 ingress를 한 PR에서 같이 잠글지

    너의 추천
    높은 우선순위로 수정 PR을 받으세요. resolveOpenCodeGoTransport에서 sessionLane이 비면 provider를 그대로 두지 말고, 요청 단위 ephemeral lane으로 헤더를 붙이세요. 명시한 x-opencode-session은 계속 존중하고, 안정 레인 경로는 건드리지 마세요. 재현 curl이 본문에 있으니 그걸로 수동 확인 + 단위 테스트 세 케이스를 넣고, Claude Desktop probe가 400/timeout 없이 끝나게 하세요. types/config 분할과 무관하며 중복 이슈(#3945는 닫힘, 이건 잔여 가지)로 합치지 말고 이 이슈를 본체로 두세요.

    이 댓글은 grok-bot이 작성했습니다

  3. lidge-jun commented on Sep 10, 2026

    @lidge-jun
    Owner

    Field evidence that this is now an enforced upstream requirement rather than a warning, and that it is reaching users through OpenCodex.

    OpenCode Go emailed affected accounts before switching enforcement on. The text was posted in r/PiCodingAgent on 2026-09-04 ("Missing OpenCode Go Header", https://old.reddit.com/r/PiCodingAgent/comments/1w6x2e5/missing_opencode_go_header/):

    Some of your requests to OpenCode Go are missing an x-opencode-session header. If we don't have this we cannot properly optimize our service. Starting 09/06 requests missing this header may error.

    The listed offending user agent in that thread is the "JavaScript OpenAI client", and a commenter who instrumented the Pi agent's own before_provider_headers hook found the header present on the agent's own calls - consistent with the missing header coming from a proxy or plugin path rather than from the agent.

    An OpenCodex user hit the resulting error on 2026-09-10 while routing Grok 4.1 Flash through a Go destination and asked for it to be looked at: https://x.com/splaxi/status/2098034139349876885

    So the branch this issue describes - requests with no conversation identity leaving resolveOpenCodeGoTransport unheaderised - is past the grace period and now fails upstream by design. PR #4184 currently carries CHANGES_REQUESTED; that review is the blocking path.

  4. lidge-jun commented on Sep 10, 2026

    @lidge-jun
    Owner

    Fixed on dev.

    #4226 (9e75542ff) gives sessionless OpenCode Go requests an isolated session lane. The lane is allocated once per admitted request and carried across every boundary that rebuilds one, including the policy-fallback retry, so a hop to the next candidate does not split one turn into two Go conversations.

    Closing manually: pull requests here target dev, so GitHub does not auto-close linked issues.

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