Description
OpenAI-compatible streaming tool calls can fail when a provider/model sends the first tool_calls delta with an id and function.arguments, but without function.name, and then sends function.name in a later delta.
OpenCode currently treats the first partial tool-call delta as invalid and throws:
AI_InvalidResponseDataError: Expected 'function.name' to be a string.
This appears to be a streaming parser robustness issue. OpenAI-compatible streaming responses may split tool-call fields across deltas, so a partial chunk should be accumulated until enough data exists to emit the tool call.
This is similar in spirit to vercel/ai#6687, where provider-specific OpenAI-compatible tool-call chunks were valid after accumulation but failed because the parser expected a more complete shape too early.
Expected Behavior
The following streamed chunks should be accumulated into a single valid tool call:
{
"id": "call_123",
"type": "function",
"function": {
"name": "bash",
"arguments": "{\"command\": \"ls -la\"}"
}
}
OpenCode should execute the bash tool after the function.name and complete JSON arguments have been received.
Actual Behavior
OpenCode throws before the later chunk containing function.name is processed:
AI_InvalidResponseDataError: Expected 'function.name' to be a string.
Minimal Reproduction
This mock OpenAI-compatible SSE stream reproduces the chunk ordering. The key part is the second chunk: it contains id, type, and function.arguments, but no function.name. The third chunk then provides function.name.
const chunks = [
`data: {"choices":[{"finish_reason":null,"index":0,"delta":{"role":"assistant","content":null}}]}`,
`data: {"choices":[{"finish_reason":null,"index":0,"delta":{"tool_calls":[{"index":0,"id":"call_123","type":"function","function":{"arguments":""}}]}}]}`,
`data: {"choices":[{"finish_reason":null,"index":0,"delta":{"tool_calls":[{"index":0,"id":"call_123","type":"function","function":{"name":"bash","arguments":"{"}}]}}]}`,
`data: {"choices":[{"finish_reason":null,"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"\\u0022command\\u0022"}}]}}]}`,
`data: {"choices":[{"finish_reason":null,"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":": \\u0022ls -la\\u0022}"}}]}}]}`,
`data: {"choices":[{"finish_reason":"tool_calls","index":0,"delta":{}}]}`,
`data: [DONE]`,
]
const stream = new ReadableStream({
start(controller) {
for (const chunk of chunks) {
controller.enqueue(new TextEncoder().encode(`${chunk}\n\n`))
}
controller.close()
},
})
const response = new Response(stream, {
status: 200,
headers: { "Content-Type": "text/event-stream" },
})
A parser that consumes this response should delay validation/emission of the tool call until function.name has arrived and the accumulated arguments are complete.
Raw SSE Stream
data: {"choices":[{"finish_reason":null,"index":0,"delta":{"role":"assistant","content":null}}]}
data: {"choices":[{"finish_reason":null,"index":0,"delta":{"tool_calls":[{"index":0,"id":"call_123","type":"function","function":{"arguments":""}}]}}]}
data: {"choices":[{"finish_reason":null,"index":0,"delta":{"tool_calls":[{"index":0,"id":"call_123","type":"function","function":{"name":"bash","arguments":"{"}}]}}]}
data: {"choices":[{"finish_reason":null,"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"\\u0022command\\u0022"}}]}}]}
data: {"choices":[{"finish_reason":null,"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":": \\u0022ls -la\\u0022}"}}]}}]}
data: {"choices":[{"finish_reason":"tool_calls","index":0,"delta":{}}]}
data: [DONE]
Suggested Fix Direction
Accumulate partial tool-call deltas by index/id and only emit/validate the tool call once function.name is available. If the stream finishes and a pending tool call still has no function.name, then it should remain an invalid response.
Related
Description
OpenAI-compatible streaming tool calls can fail when a provider/model sends the first
tool_callsdelta with anidandfunction.arguments, but withoutfunction.name, and then sendsfunction.namein a later delta.OpenCode currently treats the first partial tool-call delta as invalid and throws:
This appears to be a streaming parser robustness issue. OpenAI-compatible streaming responses may split tool-call fields across deltas, so a partial chunk should be accumulated until enough data exists to emit the tool call.
This is similar in spirit to vercel/ai#6687, where provider-specific OpenAI-compatible tool-call chunks were valid after accumulation but failed because the parser expected a more complete shape too early.
Expected Behavior
The following streamed chunks should be accumulated into a single valid tool call:
{ "id": "call_123", "type": "function", "function": { "name": "bash", "arguments": "{\"command\": \"ls -la\"}" } }OpenCode should execute the
bashtool after thefunction.nameand complete JSON arguments have been received.Actual Behavior
OpenCode throws before the later chunk containing
function.nameis processed:Minimal Reproduction
This mock OpenAI-compatible SSE stream reproduces the chunk ordering. The key part is the second chunk: it contains
id,type, andfunction.arguments, but nofunction.name. The third chunk then providesfunction.name.A parser that consumes this response should delay validation/emission of the tool call until
function.namehas arrived and the accumulated arguments are complete.Raw SSE Stream
data: {"choices":[{"finish_reason":null,"index":0,"delta":{"role":"assistant","content":null}}]} data: {"choices":[{"finish_reason":null,"index":0,"delta":{"tool_calls":[{"index":0,"id":"call_123","type":"function","function":{"arguments":""}}]}}]} data: {"choices":[{"finish_reason":null,"index":0,"delta":{"tool_calls":[{"index":0,"id":"call_123","type":"function","function":{"name":"bash","arguments":"{"}}]}}]} data: {"choices":[{"finish_reason":null,"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"\\u0022command\\u0022"}}]}}]} data: {"choices":[{"finish_reason":null,"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":": \\u0022ls -la\\u0022}"}}]}}]} data: {"choices":[{"finish_reason":"tool_calls","index":0,"delta":{}}]} data: [DONE]Suggested Fix Direction
Accumulate partial tool-call deltas by
index/idand only emit/validate the tool call oncefunction.nameis available. If the stream finishes and a pending tool call still has nofunction.name, then it should remain an invalid response.Related