Repository navigation
Description
Description
Severity: high — this can permanently brick a long-running session. Switching models/providers (or resuming an affected session) makes every subsequent request fail with HTTP 400, and the invalid data is already persisted in history, so the error keeps repeating. Ordinary users cannot recover without starting a new session or hand-editing storage.
Summary
Cross-provider conversation replay preserves a provider-native tool-call ID verbatim and sends it unchanged to an OpenAI-compatible /chat/completions endpoint. Vertex/Gemini (and gateways that embed a signature in the call ID, e.g. SAP AI Core) can produce IDs >1000 characters, while OpenAI-compatible providers enforce a short tool_call_id limit (official OpenAI = 40, others 64). The request is rejected:
Error: 400 {
"error": {
"code": "string_above_max_length",
"message": "Invalid 'messages[n].tool_calls[0].id': string too long. Expected a string with maximum length 64, but got a string with length 1262 instead.",
"param": "messages[n].tool_calls[0].id",
"type": "invalid_request_error"
}
}
A historical assistant message carries an ID shaped like vertex_tool_<uuid>__sig_<long opaque base64 signature>, and that full value is emitted as both messages[n].tool_calls[0].id and the matching messages[n+1].tool_call_id.
Root cause
There is no tool-call-ID length normalization anywhere in the request serialization path. Incoming IDs are stored verbatim and replayed verbatim regardless of the target API's constraints.
New LLM protocol (packages/llm):
- Inbound streaming preserves the provider ID as-is:
packages/llm/src/protocols/openai-chat.ts(step, tool delta →{ id: tool.id ?? undefined, ... }). - Outbound serialization emits it verbatim with no cap:
openai-chat.tslowerToolCall→id: part.id; tool result →tool_call_id: part.id.openai-responses.ts→call_id: part.idfor bothfunction_callandfunction_call_output.openai-compatible-chat.tsreusesOpenAIChat.protocolend-to-end, so every non-OpenAI gateway inherits the same unbounded behavior.
Legacy session path (packages/opencode):
packages/opencode/src/session/message-v2.tspassestoolCallId: part.callIDstraight to the AI SDK, which forwards it to the provider unchanged.
A repo-wide search finds no cap/repair/dedup for tool-call ID length (provider-error.ts only matches context-window errors). Locally generated IDs are short, so this only triggers for externally supplied provider IDs.
Why this hits real users
Gemini/Vertex thinking models and gateways encode a thoughtSignature into the call ID (observed via SAP AI Core as vertex_tool_<uuid>__sig_<signature>, ~1200+ chars). Once such IDs are in history, switching to any OpenAI-compatible target — or resuming the session — fails immediately and keeps failing.
Prior (unresolved) reports
This has surfaced repeatedly but was never root-caused or fixed; all were auto-closed as stale/not-planned, and the bot explicitly asks to reopen a fresh issue:
- [BUG]: Invalid 'messages[68].tool_calls[0].id': string too long. Expected a string with maximum length 40, but got a string with length 46 instead. #12653 — "
messages[68].tool_calls[0].id: string too long … max 40, got 46" after switching to gpt-5-mini / via Portkey gateway. - Invalid input[2].id: string too long when submitting via Github copilot (5.3 codex and 5.4) #17933 — "
input[2].id: string too long" via GitHub Copilot codex (Responses API), flagged as the same underlying issue.
Steps to reproduce
- Run a session on a Vertex/Gemini model (or a gateway such as SAP AI Core / Portkey) that returns signed/long tool-call IDs.
- Make at least one tool call so a long ID is persisted in history.
- Switch to an OpenAI-compatible model (e.g.
gpt-5*via official OpenAI, LiteLLM, or Copilot), or resume the session with that model. - Next request → HTTP 400
string_above_max_lengthontool_calls[].id; the session stays broken.
Expected behavior
Before serializing history for a target API, tool-call IDs must satisfy that target's constraints:
- When the target enforces a length limit, replace the oversized ID with a deterministic, collision-safe ≤limit ID.
- Rewrite both
tool_calls[].id/call_idand every matchingtool_call_id/function_call_output.call_idso pairing is preserved. - Preserve opaque provider-native IDs only where the target API supports them (keep native Vertex/Gemini replay intact).
Suggested regression coverage: replay a history containing a Vertex-style signed tool-call ID through an OpenAI Chat target with a short limit and assert every emitted ID is within the limit, each result ID is rewritten to match its call, multiple calls stay uniquely paired, and native Gemini replay still carries signatures.
Plugins
none
OpenCode version
dev @ 1.18.35 (github-v1.2.25-2143-gf17f13bdc0); also reported on 1.1.53 (#12653) and 1.2.27 (#17933)
Operating System
macOS (reproducible independent of OS)
Terminal
n/a
Activity
opencode-agent commentedon Oct 9, 2026
Thanks for the detailed report and root-cause analysis!
This was reported on OpenCode v1 (1.18.35), and the code paths you cite (
packages/llm,packages/opencode/src/session/message-v2.ts) are v1 code. v1 only gets fixes for critical bugs (data loss, security problems, or breakage with no workaround). There is a workaround here: switch back to the provider that produced the long IDs, or start a new session. So this is unlikely to be fixed in v1.Please try OpenCode v2 (
@opencode/cli, theopencodecommand), which has a separate provider and protocol implementation. If you can reproduce this on v2, for example by running a session on Vertex/Gemini or a gateway like SAP AI Core, then switching to an OpenAI-compatible model, please reply here with the v2 version and the error, and we'll look at it again. Please also reply if you think this is critical in a way I've missed.Related issues:
tool_calls[0].idstring-too-long error after switching models (v1, closed)input[n].idtoo long (v1, closed)ton618to commentedon Oct 9, 2026
V2 (
packages/ai) also affected — partial mitigation onlyChecked the
v2branch (v2.0.0-951, architecture ispackages/ai, notpackages/llm). It adds a conditional tool-call-ID normalization but still reproduces this bug for the exact #15056-style scenario. The pattern matches the known failure mode: normalization is conditional and leaves cross-provider / foreign IDs unchanged.Chat path —
packages/ai/src/protocols/openai-chat.tslowerMessagesbuilds atoolCallID(id)function (L580-590) that is applied consistently to bothtool_calls[].id(lowerToolCall, L356) andtool_call_id(lowerToolMessages, L512 / L522), so call/result pairing stays correct. But the length cap is whitelist-based:openai/azure/openai/-prefixed modelid.slice(0, 40)slice(0, 9)claude-matching model idopenai-compatible, ...)return idverbatimThe generic provider in
packages/ai/src/providers/openai-compatible.tsdefaultsproviderto"openai-compatible"(or a user string likelitellm), which is neitheropenai/azurenor anopenai/-prefixed model id, sotoolCallIDreturns the ID verbatim. Avertex_tool_<uuid>__sig_<...>ID (>1000 chars) therefore passes through unchanged → HTTP 400string_above_max_length, exactly the LiteLLM/gpt-5.6-solcase.Responses path —
packages/ai/src/protocols/open-responses.tsNo normalization at all:
call_id: part.idverbatim at L494-495 (function_call) and L757-758 (function_call_output). This path backsopenai-compatible-responses(e.g. GitHub Copilot codex), matching the older report #17933 (input[].idstring too long).No per-provider length flag
The model compatibility schema (
packages/ai/src/schema/options.tsL194-201,packages/schema/src/model.tsL77-83) has fields likerequireReasoning/requireAssistantAfterToolbut notoolCallIdLimit/usesOpenAIToolCallIdLimit. There is no way for an OpenAI-compatible gateway to declare its 64-char limit; the cap is hardcoded by provider-name/model-id string matching.Summary
packages/llm): no ID normalization anywhere → every provider affected.packages/ai): bounded only foropenai/azure/mistral;claudeand all other gateways pass through verbatim.packages/ai): no normalization at all.A proper fix should be the same on both branches and both protocol families: when the target API enforces a tool-call-ID length limit, deterministically rewrite oversized IDs to a valid, collision-safe form and rewrite every matching call/result ID — rather than a provider-name whitelist — and extend the same handling to the Responses path.
opencode-agent commentedon Oct 9, 2026
Thanks for checking v2. I confirmed this on the latest
v2(5cfe3b4), and v2.0.26 has the same code.I built a history with a ~1254-character
vertex_tool_<uuid>__sig_<...>tool-call ID and its tool result, then compiled requests for OpenAI-compatible targets inpackages/ai:litellm,openai-compatible, and openrouter withanthropic/claude-sonnet-4):tool_calls[].idandtool_call_idare both sent at the full 1254 characters.github-copilot):function_call.call_idandfunction_call_output.call_idare also sent unchanged.This matches your analysis. The chat path only shortens IDs for mistral-family models and for
openai/azure/openai/*models, and the Responses path doesn't shorten them at all. I didn't have provider credentials, so I couldn't send the request to a real endpoint and see the 400. But these lengths are far over the 40/64-character limits. The script below reproduces it from a v2 checkout.Related: #12653 and #17933 (earlier reports of the same error).
Reproduction script (repro-54146.ts)
Run it:
bun repro-54146.tsargszero commentedon Oct 10, 2026
I'd like to work on this.
I verified both gaps in the code on the current
v2tip (f714e5f68b), by reading the serialization path — not by hitting a live endpoint:packages/ai/src/protocols/openai-chat.tsbuilds a singletoolCallID(id)that is applied to bothtool_calls[].idandtool_call_id, so call/result pairing is already consistent. The length cap, however, is whitelist-based: mistral-family getsslice(0, 9),claudemodel IDs are sanitized with no cap,openai/azure/openai/*getslice(0, 40), and every other target returns the ID verbatim — which is exactly the reported case (litellm,openai-compatible, other gateways).packages/ai/src/protocols/open-responses.tsdoes no normalization at all:call_id: part.idis emitted verbatim for bothfunction_callandfunction_call_output.So the change I'd like to prepare is one bounded, collision-safe derivation applied whenever a target enforces an ID length limit, on the Chat and Responses paths alike, instead of a provider-name whitelist. Extending the whitelist would leave the reported setup broken.
If you'd rather keep the first change narrower — for example, only the Chat path, or a per-model compatibility flag instead of a derivation — say so and I'll scope it that way.
fleuryc-owkin commentedon Oct 13, 2026
Hit this today on v2.0.28 via Requesty — a second minting source beyond cross-provider replay. When Requesty doesn't return a tool-call ID it synthesizes
chatcmpl-tool-<32hex>(46 chars), and the stored tool-part ID becomes the composite<uuid>.chatcmpl-tool-<32hex>(83 chars), over the 64-char cap from the moment it is minted. A single-provider session with no model switch then fails on every replay:Invalid 'input[641].call_id': string too long(Responses path).Evidence from local storage (
session_message, v2.0.28): 34 over-long IDs in one session, all of that composite shape, with tool calls in the same assistant message sharing the<uuid>prefix. The planned serialization-time derivation will have to absorb these composites regardless; capping at the mint/join point would collapse both sources into one.