OpenCode MissingSessionID: the x-opencode-session fix
OpenCode Go now returns HTTP 400 MissingSessionID for any request without an x-opencode-session header. What changed, why proxies broke, and the fix.
If you use OpenCode Go through any kind of proxy, gateway, or router, you may have woken up to every single request failing with the same wall of red:
HTTP 400: {
"type": "error",
"error": {
"type": "MissingSessionID",
"message": "Error from provider (Console Go): Request is missing x-opencode-session and cannot be routed efficiently."
}
}Nothing about your setup changed. Your API key is fine. Your subscription is active. Every model
under ocg/ — glm-5.2, kimi-k3, minimax-m3, all of them — fails identically.
This is a provider-side change, and it is worth understanding properly, because the same class of change will happen again with a different provider.
What actually changed
OpenCode began requiring an x-opencode-session header on every request to the Go endpoint.
Requests without it are rejected before they reach a model.
The header was not new. It has been part of the OpenCode client's behaviour for a while — the official client sends one, so the official client never noticed. What changed is that the header went from optional to mandatory. The moment enforcement switched on, every integration that had not been sending it broke at once.
That is the important detail for anyone debugging this: there is no gradual failure signal. It is not a rate limit that ramps, or a model that degrades. It is a hard 400 on the first request after enforcement began, and on every request after that.
Why the error message is a little misleading
The message says the request "cannot be routed efficiently", which reads like a performance warning rather than a hard requirement. It is not a warning. The request is refused.
The word "efficiently" is a clue to the purpose of the header, though, and it is worth knowing because it tells you what a correct value looks like.
What the header is actually for
x-opencode-session identifies a conversation. It is not authentication — your API key already
does that. It is a routing and caching hint.
When a provider knows that twenty requests belong to one ongoing conversation, it can:
- route them to the same backend node, so the KV cache for that conversation stays warm
- avoid re-processing a long prompt prefix it has already seen
- attribute usage coherently across a multi-turn session
That is why the value has to be stable across the turns of one conversation and different between separate conversations. A random value per request satisfies the presence check and defeats the entire purpose — you get the header accepted and the cache benefit thrown away.
The value itself is opaque. OpenCode does not parse meaning out of it. The shape the official
tooling uses is ses_ followed by 32 lowercase hex characters.
Fixing it in kRouter
If you are on kRouter, upgrade:
npm i -g @sifxprime/krouter@latestAnything from v0.5.151 onward sends the header. Nothing else to configure, no new setting, no key to paste.
A few details of how it behaves, because they matter if you are debugging:
- The value is stable for the life of a conversation, so OpenCode's routing and prompt caching actually work rather than merely being satisfied.
- If your client already speaks OpenCode's protocol and sends its own
x-opencode-session, kRouter passes yours through untouched. Overwriting it would split one conversation into two upstream sessions and lose exactly the cache locality the header exists to provide. - The value is namespaced per client tool, so two different tools that both call their thread
"1"do not collide into a single upstream session. - It is applied to every OpenCode Go transport — the OpenAI-style
/chat/completionsendpoint, the Anthropic-style/messagesendpoint that the MiniMax models use, and, since v0.5.163, the/responsesendpoint described below.
Fixing it in your own client
If you are talking to OpenCode Go directly rather than through a router, add the header yourself.
const SESSION_PREFIX = "ses_";
// Derive a stable id from something that identifies the conversation — a thread id,
// a chat id, whatever your app already has. Hashing keeps it opaque and fixed-width.
function sessionIdFor(conversationId) {
const digest = crypto
.createHash("sha256")
.update(String(conversationId))
.digest("hex")
.slice(0, 32);
return SESSION_PREFIX + digest;
}
await fetch("https://opencode.ai/zen/go/v1/chat/completions", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${apiKey}`,
"x-opencode-session": sessionIdFor(thread.id),
},
body: JSON.stringify({ model: "glm-5.2", messages, stream: true }),
});Three things to get right:
- Same conversation, same value. Derive it from a conversation identifier, not from
Date.now()or a fresh UUID per call. - Different conversations, different values. If every user shares one hardcoded string, you have handed the provider a single enormous session and the cache behaviour will be worse than sending nothing coherent at all.
- Do not put anything sensitive in it. It is a header sent on every request. Hash whatever you derive it from rather than passing a raw email, user id, or path.
A second OpenCode Go failure: Responses-only models (v0.5.163)
The session header was not the last contract change. OpenCode serves seven Go models only on its
Responses API: grok-4.5, grok-4.6, grok-4.7, gpt-5.6-luna, gpt-6-luna,
muse-spark-1.2-contributor and muse-spark-1.3-contributor. Before v0.5.163, kRouter sent them to
/chat/completions, and OpenCode answered ModelProtocolUnsupported.
The worse part was the status code. OpenCode reports that error as HTTP 401, and a 401 reads like a bad credential, so kRouter put the whole OpenCode Go account on cooldown. One request to a Grok model broke every other model on the same account until the cooldown ran out.
v0.5.163 fixes both:
- The seven models go to OpenCode's
/responsesendpoint, and kRouter converts to and from the Responses API itself, so Claude Code, Codex and plain OpenAI clients all work with them. - The "not supported" error is reported as a 400, so it no longer cools down the account.
- Every model on the OpenCode Go provider page now has a Protocol control (Auto, Chat Completions, Messages or Responses). If OpenCode moves a model to a different endpoint again, you can follow it the same day instead of waiting for a release.
The same release also filled in the model list: 43 OpenCode Go models, up from 11. The three-protocols guide covers which model uses which endpoint and how to pick one.
What to check if it still fails after the fix
If you are on a current version and still seeing MissingSessionID, work through these in order:
- Confirm the version actually running.
krouter --versionshould report 0.5.151 or newer. A globally installed CLI and a locally installed one can disagree;npm i @sifxprime/krouterwithout-ginstalls into the current folder and does not update thekrouteron your PATH. - Check which endpoint is failing. If only one model fails and the rest work, the problem is more likely to be that model's transport than the header.
- Look at the actual outgoing request, not the config. A proxy in front of your router can strip unknown headers; that is uncommon but it does happen with corporate egress proxies.
The broader lesson
A provider turned an optional header into a required one with no deprecation window, and then the same provider moved seven models to a different endpoint. It will not be the last time.
If you maintain an integration against a provider API, the defensive posture is:
- Send what the official client sends, even when the API does not currently require it. The official client is the de facto specification, and anything it sends is a candidate for becoming mandatory.
- Treat a 400 with an unfamiliar error type as a contract change, not as a bug in your request
construction.
MissingSessionIDis not a validation error about your payload; it is the provider telling you the contract moved. - Do not assume silence means stability. A header that has been optional for a year can become required on a Tuesday.
Related reading
- Anthropic rate limits, in depth — how the other major provider signals limits, and what its error shapes actually mean
- The cheapest LLMs for coding in 2026 — where OpenCode Go's bundled models sit against everything else on price
- OpenCode Go's three protocols — Chat Completions, Messages and Responses, and how to pick one per model
- Responses API vs Chat Completions — why the wire format matters for proxies
Common questions
Is MissingSessionID a problem with my API key?
No. Authentication is handled by your Authorization header, and an auth failure returns 401 or
403, not 400. MissingSessionID means the request authenticated correctly and was then rejected
for a missing routing header.
Can I just send a random value for x-opencode-session?
It will pass the presence check, but you should not. The header exists so the provider can route a conversation's turns to the same warm cache. A fresh value per request makes every turn look like a brand-new conversation, so you lose prompt-cache reuse and pay full price for the prefix each time.
Does this affect every OpenCode Go model?
Yes. The check happens before model routing, so glm-5.2, kimi-k3, kimi-k2.7-code,
minimax-m3 and the rest all return the same 400.
Which kRouter version fixes it?
v0.5.151 and newer send the session header. v0.5.163 also fixes the seven Responses-only models
and stops their "not supported" error from cooling down the account. Upgrade with
npm i -g @sifxprime/krouter@latest.
Does kRouter overwrite a session header I send myself?
No. If your client sends a valid x-opencode-session, kRouter forwards it unchanged and does not
substitute its own.
Why did one Grok request break every other OpenCode Go model?
Before v0.5.163, a Responses-only model sent to the wrong endpoint got ModelProtocolUnsupported
back as HTTP 401. kRouter took the 401 as a bad account and cooled the whole account down. From
v0.5.163 the request goes to the right endpoint, and the error, if it happens, is a 400 that does
not touch the account.
Published by Kodelyth, the team that builds kRouter. Posts are drafted with AI assistance and reviewed by a person before they go out. kRouter is free and MIT licensed.
Install kRouterRelated posts
- Fix an errorClaude Code 529 Overloaded: who sent it and how to fail overA 529 is not your quota: the model is out of capacity, and Claude Code has already retried. How to tell who sent it, and how to keep working.
- Fix an errorClaude Code context window with non-Claude models: the fixClaude Code assumes 200K for a model it doesn't recognize, so it compacts too early or overruns the real limit. How to declare the real window.
- Fix an errorClaude Code weekly limit reached: keep working until resetClaude Code says you've hit your weekly limit. What the caps count, what does not help, and three ways to keep working until the reset.
Relevant docs
- TroubleshootingFixes for common kRouter problems: banned or rate-limited accounts, OAuth sign-ins, MITM certificates, localhost and Cursor, Docker logins and the CLI.
- Error referenceWhat the common AI provider errors mean, why they happen, and how to get past them.
- Combos & fallbackPut several models behind one name. kRouter falls back from one to the next, rotates them, or asks a panel of models and merges the answers.