Repository navigation
Streaming tool calls have inconsistent identity or complete prematurely #2271
Description
This issue tracks two streaming tool-call assembly failures: inconsistent identity in the Responses adapter, and premature completion of interleaved calls in the shared stream builder. Both fixes are included in #2272.
1. Responses delta identity
At develop commit 16d83270f8a7f25358ae0165466f14e70416c428, OpenAILLMClient.executeStreaming emits argument deltas with the Responses item ID, but completes the same tool call with its call ID.
Reproduce by selecting OpenAIResponsesParams() and feeding these tool-call events through the streaming transport, followed by the normal response.completed event:
{"type":"response.output_item.added","sequence_number":1,"output_index":0,"item":{"type":"function_call","id":"fc_1","call_id":"call_1","name":"lookup","arguments":"","status":"in_progress"}}
{"type":"response.function_call_arguments.delta","sequence_number":2,"output_index":0,"item_id":"fc_1","delta":"{}"}
{"type":"response.output_item.done","sequence_number":3,"output_index":0,"item":{"type":"function_call","id":"fc_1","call_id":"call_1","name":"lookup","arguments":"{}","status":"completed"}}Actual frames:
ToolCallDelta("fc_1", null, "{}", 0)
ToolCallComplete("call_1", "lookup", "{}", 0)Expected: the delta uses "call_1" and "lookup", so consumers can correlate it with the completed call. Both values are available in response.output_item.added.
OpenAI's streaming function-call documentation distinguishes the item's id from call_id and shows argument events referring to the item through item_id.
Related: #2111 and #2134 concern a late-arriving ID in DeepSeek streaming. Here both IDs are present from the first event; the Responses adapter uses the wrong one for deltas.
2. Interleaved calls complete before their arguments finish
On the same commit, this reproducer requires no provider or credentials:
import ai.koog.prompt.streaming.StreamFrame
import ai.koog.prompt.streaming.buildStreamFrameFlow
import kotlinx.coroutines.flow.toList
import kotlinx.coroutines.runBlocking
fun main() = runBlocking {
val calls = buildStreamFrameFlow {
emitToolCallDelta("a", "search", "{\"a\":", 0)
emitToolCallDelta("b", "search", "{\"b\":", 1)
emitToolCallDelta(args = "1}", index = 0)
emitToolCallDelta(args = "2}", index = 1)
emitEnd()
}.toList().filterIsInstance<StreamFrame.ToolCallComplete>()
check(calls == listOf(
StreamFrame.ToolCallComplete("a", "search", "{\"a\":1}", 0),
StreamFrame.ToolCallComplete("b", "search", "{\"b\":2}", 1)
)) { calls }
}Expected: two completed calls with their full arguments and original identities. Actual: four completions, containing {"a":, {"b":, 1} and 2} respectively; the last two also lose their IDs and names. The builder keeps one pending call and flushes it whenever the index changes.
Verified on unchanged develop and with #2134 applied unchanged. That PR fixes the late-ID case from #2111, but this interleaving reproduction still fails.
Fix tracking
- Responses adapter identity: fix(prompt): preserve streamed tool-call identity and arguments #2272.
- Shared builder interleaving, including late-arriving IDs: fix(prompt): preserve streamed tool-call identity and arguments #2272.
The same PR now covers both parts.
Activity
[-]Responses streaming uses different IDs for tool-call deltas and completion[/-][+]Streaming tool calls have inconsistent identity or complete prematurely[/+]