Skip to content

fix(prompt): Don't start a new tool call on repeated tool call id#2052

Merged
Anastasiia Zarechneva (aozherelyeva) merged 2 commits into
JetBrains:developfrom
jewoodev:fix/2002-repeated-tool-call-id
Jun 1, 2026
Merged

fix(prompt): Don't start a new tool call on repeated tool call id#2052
Anastasiia Zarechneva (aozherelyeva) merged 2 commits into
JetBrains:developfrom
jewoodev:fix/2002-repeated-tool-call-id

Conversation

@jewoodev

@jewoodev Jewoo Shin (jewoodev) commented May 21, 2026

Copy link
Copy Markdown
Contributor

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:

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

After:

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.

`emitToolCallDelta()` started a new tool call whenever the chunk `id` was
non-null. Some OpenAI-compatible providers repeat the same `id` on every
streaming chunk, so a single tool call was fragmented into multiple early
`ToolCallComplete` frames.

Detect a new tool call only when a present `id`/`index` signal differs from
the pending call, matching the existing logic in `emitReasoningDelta`.

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.

Thank you for the fix! Looks good!

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Thank you!

@aozherelyeva Anastasiia Zarechneva (aozherelyeva) merged commit 581d6af into JetBrains:develop Jun 1, 2026
22 checks passed
@jewoodev

Copy link
Copy Markdown
Contributor Author

Thanks for the review and for landing the OpenTelemetry test fix on top to unblock CI — really appreciated.

@jewoodev Jewoo Shin (jewoodev) deleted the fix/2002-repeated-tool-call-id branch June 1, 2026 16:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

StreamFrameFlowBuilder.emitToolCallDelta() incorrectly treats every chunk with non-null id as a new tool call

3 participants