Skip to content

fix: process blank tool call ID in StreamFrameFlowBuilder.emitToolCallDelta#1915

Merged
Anastasiia Zarechneva (aozherelyeva) merged 1 commit into
developfrom
zarechneva/fix-emitToolCallDelta-blank-id
Apr 27, 2026
Merged

fix: process blank tool call ID in StreamFrameFlowBuilder.emitToolCallDelta#1915
Anastasiia Zarechneva (aozherelyeva) merged 1 commit into
developfrom
zarechneva/fix-emitToolCallDelta-blank-id

Conversation

@aozherelyeva

@aozherelyeva Anastasiia Zarechneva (aozherelyeva) commented Apr 27, 2026

Copy link
Copy Markdown
Contributor

Handle blank tool call IDs in emitToolCallDelta in StreamFrameFlowBuilder; add a corresponding test.

Closes #1900

@antoniibelyshev Antonii (antoniibelyshev) left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, thanks

@aozherelyeva Anastasiia Zarechneva (aozherelyeva) merged commit 510e6cf into develop Apr 27, 2026
23 checks passed
@aozherelyeva Anastasiia Zarechneva (aozherelyeva) deleted the zarechneva/fix-emitToolCallDelta-blank-id branch April 27, 2026 15:35
Anastasiia Zarechneva (aozherelyeva) added a commit that referenced this pull request Jun 1, 2026
)

Closes #2002.

### Summary

`StreamFrameFlowBuilder.emitToolCallDelta()` treated every chunk with a
non-null tool-call `id` as a new tool call. This fix treats a repeated
`id` as a continuation of the pending tool call, so OpenAI-compatible
providers that repeat the same `id` on each streaming chunk no longer
fragment one logical tool call into multiple premature
`ToolCallComplete` frames.

### Fix

A new tool call now begins only when a present `id` or `index` differs
from the pending call, instead of whenever `id` is non-null.

Before:

```kotlin
val new: PendingToolCall = if (sanitizedId != null || index != previous?.index) {
```

After:

```kotlin
val isNewToolCall =
    (sanitizedId != null && sanitizedId != previous?.id) ||
        (index != null && index != previous?.index)
val new: PendingToolCall = if (isNewToolCall) {
```

This follows the same id-change principle as `emitReasoningDelta`;
absent or blank ids still continue the pending call (the blank case
preserves the #1915 handling). The fix lives in the shared
`StreamFrameFlowBuilder` because all streaming clients (OpenAI,
OpenRouter, Mistral, Anthropic, DeepSeek, Ollama, Bedrock, Dashscope,
Google) emit tool-call deltas through it.

### Tests

Two regression tests in `StreamFrameFlowBuilderTest`:
`testEmitToolCallDeltaWithRepeatedIdAppendsToExisting` (three chunks
with the same `id` combine into one `ToolCallComplete`) and
`testEmitToolCallDeltaWithRepeatedIdSeparatesDistinctToolCalls`
(switching to a different `id` still starts a separate tool call).
`:prompt:prompt-model:jvmTest` (20 tests, 0 failures), `wasmJsNodeTest`,
and `ktlintCheck` all pass locally; the full `./gradlew build` (Android
and browser targets) is left to CI, since this contributor environment
lacks the Android SDK and a headless browser.

---------

Co-authored-by: Anastasiia.Zarechneva <Anastasiia.Zarechneva@jetbrains.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bugfix Something was fixed 🎉

Projects

None yet

Development

Successfully merging this pull request may close these issues.

emitToolCallDelta incorrectly treats empty string id as new tool call instead of continuation

2 participants