Skip to content

Streaming tool calls have inconsistent identity or complete prematurely #2271

Open

Description

@kyboi

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

The same PR now covers both parts.

Activity

  1. changed the title [-]Responses streaming uses different IDs for tool-call deltas and completion[/-] [+]Streaming tool calls have inconsistent identity or complete prematurely[/+] on Sep 29, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions