Skip to content

fix: emit tool-input-end before tool-call in multi-chunk and flush paths#415

Open
robert-j-y wants to merge 2 commits intomainfrom
devin/1770920678-fix-tool-input-end-streaming
Open

fix: emit tool-input-end before tool-call in multi-chunk and flush paths#415
robert-j-y wants to merge 2 commits intomainfrom
devin/1770920678-fix-tool-input-end-streaming

Conversation

@robert-j-y
Copy link
Contributor

@robert-j-y robert-j-y commented Feb 12, 2026

Description

Fixes #413. The multi-chunk tool call merge path and the flush path for unsent tool calls were missing tool-input-end events before emitting tool-call, diverging from the stream event protocol used by @ai-sdk/openai.

Before (multi-chunk path): tool-input-start → tool-input-delta… → tool-call (missing tool-input-end)
After (multi-chunk path): tool-input-start → tool-input-delta… → tool-input-end → tool-call

The flush path for unsent tool calls now also emits the full tool-input-start → tool-input-delta → tool-input-end sequence before tool-call when the tool call was never partially streamed. When partial deltas were already streamed (i.e. inputStarted is true), only tool-input-end is emitted to avoid duplicate deltas.

Validated by comparing line-by-line against the @ai-sdk/openai reference implementation (openai-chat-language-model.ts), which consistently emits tool-input-end before tool-call in both its single-chunk and multi-chunk paths.

Updates since last revision

  • Fixed flush path duplicate delta bug: The tool-input-delta emission in the flush path was unconditional, meaning partially-streamed tool calls (where inputStarted is true) would receive a duplicate delta with the full accumulated input on top of the deltas already sent during streaming. Now tool-input-delta is only emitted inside the if (!toolCall.inputStarted) guard.
  • Added unit test for flush edge case: New test should emit tool-input-end without duplicate delta when partially-streamed tool call is flushed covers the scenario where tool call arguments are streamed across multiple chunks but the JSON never completes, forcing the tool call through the flush path. Verifies only the streaming deltas are present (no duplicate from flush).

Key areas for review

  1. Flush path branching (src/chat/index.ts lines 1057–1069): When inputStarted is false, the full sequence (start, delta, end) is emitted. When true, only tool-input-end is emitted. Verify this correctly avoids duplicate deltas without dropping required events.
  2. E2e test models: The issue reported against openai/gpt-5.2; the e2e test uses openai/gpt-4.1-nano and openai/gpt-4.1-mini for availability/stability.

Checklist

  • I have run pnpm stylecheck and pnpm typecheck
  • I have run pnpm test and all tests pass
  • I have added tests for my changes (if applicable)
  • I have updated documentation (if applicable)

Changeset

  • I have run pnpm changeset to create a changeset file

Link to Devin run | Requested by @robert-j-y

Fixes #413. The multi-chunk tool call merge path and the flush path for
unsent tool calls were missing the tool-input-end event before tool-call,
diverging from the stream event protocol used by @ai-sdk/openai.

Changes:
- Multi-chunk merge path: add tool-input-end before tool-call
- Flush path: emit full tool-input-start/delta/end sequence for unsent tools
- Update existing test expectations to match corrected protocol
- Add e2e regression test for issue #413

Co-Authored-By: Robert Yeakel <robert.yeakel@openrouter.ai>
@robert-j-y robert-j-y requested a review from mattapperson March 7, 2026 04:43
Copy link
Contributor

@devin-ai-integration devin-ai-integration bot left a comment

Choose a reason for hiding this comment

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

Devin Review found 3 potential issues.

View 1 additional finding in Devin Review.

Open in Devin Review

Copy link
Contributor

Choose a reason for hiding this comment

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

🚩 Pre-existing: initial chunk arguments are not emitted as a delta in the multi-chunk path

When the first chunk of a tool call includes non-empty but non-parsable-JSON arguments (e.g., {"val), those arguments are stored in toolCall.function.arguments at line 889 but never emitted as a tool-input-delta. The tool-input-start and first tool-input-delta are only emitted on the second chunk (merge path, lines 968-987), which only includes the merge chunk's delta, not the initial arguments. In the existing test (src/chat/index.test.ts:1476), the initial chunk has arguments: "" (empty string), so this isn't visible. This is a pre-existing issue not introduced by this PR, but worth noting for correctness if providers ever send non-empty partial arguments in the first tool call chunk.

(Refers to lines 884-893)

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

… is true

Co-Authored-By: Robert Yeakel <robert.yeakel@openrouter.ai>
@robert-j-y
Copy link
Contributor Author

Addressing "Key areas for review"

1. Flush path branching (lines 1057–1069)

Verified correct. When inputStarted is false (tool call never partially streamed), the full sequence is emitted: tool-input-start → tool-input-delta → tool-input-end → tool-call. When inputStarted is true (partial deltas already streamed), only tool-input-end → tool-call is emitted — this avoids duplicate deltas while ensuring the tool-input-end event always fires before tool-call.

This matches @ai-sdk/openai behavior where tool-input-end is consistently emitted before tool-call in both single-chunk and multi-chunk paths. The dedicated test at line 1812 (should emit tool-input-end without duplicate delta when partially-streamed tool call is flushed) exercises the inputStarted === true flush edge case and confirms no duplicate deltas are emitted.

2. E2e test models

Acceptable. The issue was reported against openai/gpt-5.2, but the e2e tests use openai/gpt-4.1-nano and openai/gpt-4.1-mini for CI stability — these are widely available, cheaper, and the bug is not model-specific (it's a streaming event ordering issue in the SDK). The fix applies to all models equally since it's in the flush() and merge paths of the stream transformer.

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.

Tool call events are buffered until stream ends (flush) causing perceived streaming delay

1 participant