Skip to content

fix(workers-ai-provider): close reasoning block before tool calls and text#455

Merged
threepointone merged 2 commits intocloudflare:mainfrom
ferdousbhai:fix/close-reasoning-before-tool-calls
Mar 25, 2026
Merged

fix(workers-ai-provider): close reasoning block before tool calls and text#455
threepointone merged 2 commits intocloudflare:mainfrom
ferdousbhai:fix/close-reasoning-before-tool-calls

Conversation

@ferdousbhai
Copy link
Contributor

Summary

When a reasoning model (e.g. Kimi K2.5 via @cf/moonshotai/kimi-k2.5) streams reasoning_content followed by tool_calls or text content, the workers-ai-provider never closes the reasoning block before emitting the new content type. This causes the AI SDK to misinterpret the stream structure, leading to silent failures where tool call results or text responses are lost.

Root Cause

In streaming.ts, the getMappedStream() transform emits reasoning-start and reasoning-delta events but only emits reasoning-end in flush() — after the entire stream completes. When the model transitions from reasoning to tool calls or text mid-stream, the reasoning block stays open, and downstream consumers (AI SDK's streamText) can't properly detect the content type transition.

The Fix

Close the active reasoning block (reasoning-end + null reasoningId) before emitting:

  • text deltas (text-start / text-delta) — both native and OpenAI format
  • tool call deltas (tool-input-start / tool-input-delta) — both native and OpenAI format

This matches the behavior already implemented in @ai-sdk/openai-compatible, which properly emits reasoning-end before transitioning to tool or text events.

Nulling reasoningId on transition prevents double-close in flush().

Reproduction

Observed with @cf/moonshotai/kimi-k2.5 on Workers AI:

  1. User sends a message that triggers tool use (e.g. document search)
  2. Model streams reasoning_content (thinking about the request)
  3. Model emits tool_calls (first tool call works correctly with finish_reason: "tool_calls")
  4. After tool result, model streams reasoning_content again, then tries another tool call
  5. Second tool call leaks into reasoning_content as raw tokens (<|tool_call_begin|>, <|tool_calls_section_end|>)
  6. finish_reason: "stop" with zero text content → user sees nothing

The structural fix ensures reasoning blocks are properly closed so the AI SDK can detect tool calls even when the model's output format is imperfect.

Related: vercel/ai#11409 (same Kimi K2 issue on AWS Bedrock)

Test Plan

  • Added test: reasoning block closes before tool calls (no double-close)
  • Added test: reasoning block closes before text content (no double-close)
  • All 215 existing tests pass
  • Verified flush() guard (if (reasoningId)) prevents double-close when already nulled

🤖 Generated with Claude Code

@changeset-bot
Copy link

changeset-bot bot commented Mar 22, 2026

🦋 Changeset detected

Latest commit: e02cdd2

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
workers-ai-provider Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@ferdousbhai ferdousbhai reopened this Mar 23, 2026
ferdousbhai and others added 2 commits March 25, 2026 08:30
… text

When a model (e.g. Kimi K2.5) streams reasoning_content followed by
tool_calls or text content, the reasoning block was never closed before
the new content type began. This caused the AI SDK to misinterpret the
stream structure, leading to silent failures where tool calls or text
responses were lost.

This matches the behavior already implemented in @ai-sdk/openai-compatible
(vercel/ai), which properly emits reasoning-end before transitioning to
tool-input-start or text-start events.

The fix applies to both native and OpenAI-compatible response formats.
Nulling reasoningId on transition prevents double-close in flush().

Fixes tool calling with reasoning models on Workers AI (observed with
@cf/moonshotai/kimi-k2.5 where multi-step tool loops would produce
reasoning-only responses with no visible text).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@threepointone threepointone force-pushed the fix/close-reasoning-before-tool-calls branch from 7650a6c to e02cdd2 Compare March 25, 2026 08:35
Copy link
Collaborator

@threepointone threepointone left a comment

Choose a reason for hiding this comment

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

good pr, thanks!

@pkg-pr-new
Copy link

pkg-pr-new bot commented Mar 25, 2026

Open in StackBlitz

npx https://pkg.pr.new/cloudflare/ai/ai-gateway-provider@455
npx https://pkg.pr.new/cloudflare/ai/@cloudflare/tanstack-ai@455
npx https://pkg.pr.new/cloudflare/ai/workers-ai-provider@455

commit: e02cdd2

@threepointone threepointone merged commit 06ce1b3 into cloudflare:main Mar 25, 2026
3 checks passed
@github-actions github-actions bot mentioned this pull request Mar 25, 2026
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.

2 participants