Fix empty tool call ID causing API errors when switching providers
close #1279
Problem
Some providers (e.g. Gemini) don't generate tool call IDs, resulting in empty callID being stored. When switching to providers that require tool call IDs (e.g. Anthropic, OpenAI), this causes API errors with "id": "" in tool_use blocks.
Solution
- Generate fallback ulid() when value.id is empty from stream events
- Make toModelMessage async to fix empty callID in existing stored data
- Update storage with generated ID when empty callID is found
Test plan
- Use Gemini model to create tool calls in a session
- Switch to Anthropic/OpenAI model and continue the session
- Verify no API errors about empty tool call IDs
@9j are you sure this fixes the issue? I can't get google to not generate ids
ahh I see ur message it was specifically with the invalid tool
async experimental_repairToolCall(input) {
const lower = input.toolCall.toolName.toLowerCase()
if (lower !== input.toolCall.toolName && tools[lower]) {
log.info("repairing tool call", {
tool: input.toolCall.toolName,
repaired: lower,
})
return {
...input.toolCall,
toolName: lower,
}
}
return {
...input.toolCall,
input: JSON.stringify({
tool: input.toolCall.toolName,
error: input.error.message,
}),
toolName: "invalid",
}
},
I believe, the correct fix is here, all the other changes you made can be undone, just make sure that the id is set here
@9j are you sure this fixes the issue? I can't get google to not generate ids
https://github.com/sst/opencode/issues/1279#issuecomment-3563085200
There are cases like this. And when I built and tested with the version of the code I wrote, this problem was resolved.
async experimental_repairToolCall(input) { const lower = input.toolCall.toolName.toLowerCase() if (lower !== input.toolCall.toolName && tools[lower]) { log.info("repairing tool call", { tool: input.toolCall.toolName, repaired: lower, }) return { ...input.toolCall, toolName: lower, } } return { ...input.toolCall, input: JSON.stringify({ tool: input.toolCall.toolName, error: input.error.message, }), toolName: "invalid", } },I believe, the correct fix is here, all the other changes you made can be undone, just make sure that the id is set here
-
experimental_repairToolCall is only invoked when a tool call fails.
- An empty ID is still problematic for normal tool calls.
- Even a normal tool call from providers like Gemini can have an empty ID.
-
Issue with already saved data
- Parts that have previously been saved with an empty callID are still problematic.
Yeah can you show me a tool call from gemini that has no id? Your example didnt have that but it did have the invalid tool missing an id
Yeah we can do the other thing repair the session too but I think this is an edge case when the model calls a tool that doesnt exist
But maybe it happens more often with gemini can u show an example?
Yeah can you show me a tool call from gemini that has no id? Your example didnt have that but it did have the invalid tool missing an id
Yeah we can do the other thing repair the session too but I think this is an edge case when the model calls a tool that doesnt exist
But maybe it happens more often with gemini can u show an example?
I experienced this issue when switching from Gemini 3 Pro to Claude. Here's what I found:
Evidence from my storage:
// Message using zenmux/google/gemini-3-pro-preview
{
"id": "prt_aa63f171d0016pLk11oWQ8QG8D",
"callID": "", // ← Empty callID from Gemini
"tool": "list" // ← Normal tool call, not invalid
}
You're right that the invalid tool case is when the model calls a non-existent tool. But I also found multiple cases where Gemini 3 Pro generates empty callIDs for normal tool calls (list, edit, bash, etc.).
Not sure if this is model-specific or provider-specific, but all empty callIDs in my storage came from the same session where I used Gemini 3 Pro and then switched to Claude. The empty callIDs caused API errors when continuing the conversation with Claude.
ah okay makes sense, just wanted to avoid code if we could help it, I guess if u can resolve the conflicts this is basically good to go
@rekram1-node done!
@rekram1-node done again
ill merge in the morning thanks for your work on this
@rekram1-node Just a friendly bump! 👋 Let me know if there's anything else needed from my side before merging.
Hm yeah still haven't had any reports of this issue, nor can I replicate.
Can you share a session w/ the issue plz?
opencode export > session.json