Skip to content

fix(providers): final regenerated stream must not re-call tools (empty chat answers)#5915

Merged
icecrasher321 merged 3 commits into
stagingfrom
fix/final-stream-tool-rechoice
Jul 24, 2026
Merged

fix(providers): final regenerated stream must not re-call tools (empty chat answers)#5915
icecrasher321 merged 3 commits into
stagingfrom
fix/final-stream-tool-rechoice

Conversation

@icecrasher321

@icecrasher321 icecrasher321 commented Jul 24, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Deployed chat with OpenAI + tools intermittently returned an empty answer ({"content": ""}) while thinking streamed fine. Root cause: after the silent tool loop settles, the final regenerated streaming request still carried tools with tool_choice: 'auto' — a reasoning model can re-decide to call a tool there, but streamed calls are never executed on this path, so the stream ends with a dead function_call, no text, and the callback clobbers the tool loop's settled answer with ''.
  • Fix on both providers with this regeneration pattern:
    • OpenAI Responses (+ Azure via shared core): final stream now sends tool_choice: 'none', and an empty stream keeps the tool loop's settled answer instead of clobbering it (with a warn log).
    • Gemini silent loop: final stream sends functionCallingConfig: NONE (unless the responseFormat branch already stripped tools), same settled-answer fallback.
  • Latent pre-PR race (tool_choice: 'auto' regeneration exists on old staging); reasoning models re-reasoning during the regeneration made it likely enough to hit in practice.
  • Also: settled tool chips for OpenAI. The silent loop records each executed call and prepends tool_call_start/tool_call_end pairs (name + status only) to the agent-events stream ahead of the regenerated answer, so opted-in chat/canvas render chips for OpenAI runs. No live in-progress chips (tools run before any stream exists); legacy runs without a sink are untouched. Docs updated.

Test plan

  • New regression test: OpenAI tool-loop run asserts the third (streaming) request body carries tool_choice: 'none', that settled chip events precede the answer, and that an empty stream preserves the settled answer in execution.output.content
  • vitest run providers/openai providers/gemini providers/google passing
  • tsc --noEmit clean; agent-stream-docs:check up to date
  • Manual: deployed chat "use Exa to search news" with a reasoning model streams the answer after tools and shows the Exa chip

…ls and clobbering the answer

After the silent tool loop settles, OpenAI Responses and Gemini re-issue a
streaming request purely to stream the answer as prose — but with tools still
attached and auto tool choice, a reasoning model can re-decide to call a tool
there. Streamed calls are never executed on this path, so the run ends with a
dead function call, an empty streamed answer, and the stream callback
clobbering the tool loop's settled text with '' (deployed chat rendered
{"content": ""}).

Force tool_choice 'none' / functionCallingConfig NONE on the regeneration and
keep the tool loop's settled answer whenever the stream ends without text.
@vercel

vercel Bot commented Jul 24, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
docs Skipped Skipped Jul 24, 2026 3:13am

Request Review

@cursor

cursor Bot commented Jul 24, 2026

Copy link
Copy Markdown

PR Summary

Medium Risk
Touches core agent streaming across many LLM providers; behavior changes are intentional but could affect edge cases in tool+stream flows. OpenAI agent-events consumers now see prepended tool chip events.

Overview
Fixes empty chat answers after a silent tool loop: the post-loop regenerated streaming request no longer allows the model to call tools again (those streamed calls are never executed), which could leave the stream with no text and wipe the settled answer.

Across Anthropic, Azure OpenAI chat, Bedrock, Cerebras, DeepSeek, Groq, Mistral, OpenRouter, xAI, and OpenAI Responses (shared core), final regeneration now pins tool_choice: 'none' (or Anthropic { type: 'none' }, Gemini functionCallingConfig: NONE). Stream completion uses streamedContent || content (or Gemini’s check answer) plus a warn log when the final stream has no text.

OpenAI Responses additionally records executed tools and prepends settled tool_call_start / tool_call_end pairs on the agent-events stream before the regenerated answer text. Docs now describe OpenAI’s silent loop and settled chips (no live in-progress chips).

Regression test covers OpenAI: third request uses tool_choice: 'none', chips precede the answer, empty SSE keeps the tool-loop content.

Reviewed by Cursor Bugbot for commit b6fb82c. Configure here.

@greptile-apps

greptile-apps Bot commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR prevents regenerated answer streams from issuing unhandled tool calls.

  • Pins final streaming regeneration to a no-tools mode across supported providers and preserves the settled tool-loop answer when regeneration yields no text.
  • Adds settled OpenAI tool lifecycle events ahead of the regenerated answer stream.
  • Adds OpenAI regression coverage and updates agent-event documentation.

Confidence Score: 5/5

The PR appears safe to merge, with no blocking failures remaining from the eligible follow-up review scope.

No blocking failure remains.

Important Files Changed

Filename Overview
apps/sim/providers/openai/core.ts Disables tool calls during final Responses API regeneration, preserves settled content, and prepends completed tool lifecycle events.
apps/sim/providers/openai/core.reasoning.test.ts Covers disabled tool choice, settled lifecycle event ordering, and empty-stream answer preservation.
apps/sim/providers/gemini/core.ts Disables function calling during final Gemini regeneration and retains the settled answer on an empty stream.
apps/sim/providers/anthropic/core.ts Applies prose-only final regeneration and settled-answer fallback to both Anthropic execution paths.
apps/sim/providers/azure-openai/index.ts Prevents final Azure OpenAI regeneration from issuing unexecuted tool calls and preserves settled content.
apps/sim/providers/bedrock/index.ts Preserves the settled tool-loop answer when Bedrock regeneration produces no text.
apps/docs/content/docs/en/workflows/deployment/agent-events.mdx Documents that OpenAI tool chips arrive settled before the streamed answer.

Sequence Diagram

sequenceDiagram
  participant Client
  participant Provider as Provider adapter
  participant Model
  participant Tool

  Client->>Provider: Streamed request with tools
  Provider->>Model: Non-streaming tool-loop request
  Model-->>Provider: Tool call
  Provider->>Tool: Execute call
  Tool-->>Provider: Tool result
  Provider->>Model: Continue tool loop
  Model-->>Provider: Settled answer
  Provider-->>Client: Settled tool start/end events
  Provider->>Model: Regenerate stream with tool calls disabled
  Model-->>Provider: Streamed prose
  Provider-->>Client: Answer events
  Note over Provider,Client: If regeneration has no text, retain the settled answer
Loading

Reviews (2): Last reviewed commit: "fix(providers): apply the regeneration g..." | Re-trigger Greptile

The silent Responses tool loop has no live stream while tools run, so opted-in
consumers saw no tool chips at all for OpenAI. The loop now records each
executed call and prepends settled tool_call_start/end pairs (name + status
only) to the agent-events stream ahead of the regenerated answer. Runs without
a sink never see these events, so legacy output is unchanged.
Audit of every silent tool loop for the same race fixed for OpenAI/Gemini
(final regenerated stream re-calls a tool that is never executed, ending with
an empty answer that clobbers the settled text):

- anthropic (both implementations): tool_choice {type:'none'} on the
  regeneration (tools must stay — history carries tool_use blocks) + keep the
  settled answer when the stream ends without text
- groq: was re-applying the ORIGINAL tool_choice, so forced-tool runs
  re-forced the tool on the regeneration — guaranteed dead call; now 'none'
  + fallback
- deepseek, mistral, cerebras, azure-openai (legacy chat path), openrouter,
  xai: 'auto' -> 'none' + fallback
- bedrock: fallback only — Bedrock's ToolChoice has no 'none' and toolConfig
  is required when history carries toolUse blocks

Already guarded (no change): meta, sakana, nvidia, vllm, litellm, baseten,
together, fireworks, kimi, zai, ollama.
@icecrasher321

Copy link
Copy Markdown
Collaborator Author

bugbot run

@icecrasher321

Copy link
Copy Markdown
Collaborator Author

@greptile

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit b6fb82c. Configure here.

@icecrasher321
icecrasher321 merged commit 7120cdc into staging Jul 24, 2026
20 checks passed
@icecrasher321
icecrasher321 deleted the fix/final-stream-tool-rechoice branch July 24, 2026 03:21
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.

1 participant