Skip to content

fix: override finishReason to tool-calls for Gemini 3 thoughtSignature - #288

Merged
subtleGradient merged 2 commits into
mainfrom
tom/gemini3-thoughtsig-fix-v1
Dec 9, 2025
Merged

subtleGradient merged 2 commits into
mainfrom
tom/gemini3-thoughtsig-fix-v1

Conversation

@subtleGradient

Copy link
Copy Markdown
Contributor

Summary

  • Fixes Gemini 3 tool-call conversations stopping prematurely when thoughtSignature (encrypted reasoning) is present
  • When the model returns finish_reason='stop' but has tool calls and encrypted reasoning, override to tool-calls so the SDK continues

Problem

Gemini 3 with thinking enabled returns finish_reason: "stop" when making tool calls with encrypted reasoning (thoughtSignature). This causes AI SDK consumers to not continue the conversation after tool execution.

Solution

Detect the pattern (tool calls + encrypted reasoning + stop) and override finishReason to tool-calls in both:

  • doGenerate (non-streaming)
  • doStream (streaming) flush handler

When Gemini 3 makes tool calls with encrypted reasoning (thoughtSignature),
it returns finish_reason='stop' instead of 'tool_calls'. This causes the
AI SDK to not continue the conversation after tool execution.

This fix detects when there are:
- Tool calls present
- Encrypted reasoning details (thoughtSignature)
- finish_reason is 'stop'

And overrides the finish reason to 'tool-calls' so the conversation continues.

Applied to both doGenerate (non-streaming) and doStream (streaming) paths.
Copilot AI review requested due to automatic review settings December 8, 2025 14:14

Copilot AI left a comment

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.

Pull request overview

This PR fixes a critical issue with Gemini 3 models that use thinking mode (thoughtSignature/encrypted reasoning). When these models return tool calls alongside encrypted reasoning, they incorrectly signal finish_reason: "stop" instead of finish_reason: "tool-calls", causing the AI SDK to prematurely end the conversation instead of continuing after tool execution.

Key Changes:

  • Added finishReason override logic in doGenerate (non-streaming path) to detect the problematic pattern (tool calls + encrypted reasoning + stop) and override to 'tool-calls'
  • Added equivalent finishReason override logic in doStream flush handler (streaming path) to ensure consistent behavior
  • Both implementations use the same detection criteria: presence of tool calls, presence of encrypted reasoning with data, and finish_reason='stop'

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/chat/index.ts
Comment on lines +429 to +443
// Fix for Gemini 3 thoughtSignature: when there are tool calls with encrypted
// reasoning (thoughtSignature), the model returns 'stop' but expects continuation.
// Override to 'tool-calls' so the SDK knows to continue the conversation.
const hasToolCalls = choice.message.tool_calls && choice.message.tool_calls.length > 0;
const hasEncryptedReasoning = reasoningDetails.some(
(d) => d.type === ReasoningDetailType.Encrypted && d.data
);
const shouldOverrideFinishReason =
hasToolCalls &&
hasEncryptedReasoning &&
choice.finish_reason === 'stop';

const effectiveFinishReason = shouldOverrideFinishReason
? 'tool-calls'
: mapOpenRouterFinishReason(choice.finish_reason);

Copilot AI Dec 8, 2025

Copy link

Choose a reason for hiding this comment

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

The new finishReason override logic for Gemini 3 thoughtSignature lacks unit test coverage. While there are existing tests for tool_calls and reasoning_details separately, there's no test that verifies the specific scenario being fixed: when a model returns tool_calls with encrypted reasoning and finish_reason='stop', the SDK should override it to 'tool-calls'.

Consider adding a test in src/chat/index.test.ts similar to the existing 'should extract reasoning content from reasoning_details' test, but with:

  • tool_calls present in the response
  • reasoning_details with type: ReasoningDetailType.Encrypted and data present
  • finish_reason: 'stop' in the response
  • Assertion that the returned finishReason is 'tool-calls'

Copilot uses AI. Check for mistakes.

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.

agree, this needs tests

Comment thread src/chat/index.ts
Comment on lines +971 to +980
// Fix for Gemini 3 thoughtSignature: when there are tool calls with encrypted
// reasoning (thoughtSignature), the model returns 'stop' but expects continuation.
// Override to 'tool-calls' so the SDK knows to continue the conversation.
const hasToolCalls = toolCalls.length > 0;
const hasEncryptedReasoning = accumulatedReasoningDetails.some(
(d) => d.type === ReasoningDetailType.Encrypted && d.data
);
if (hasToolCalls && hasEncryptedReasoning && finishReason === 'stop') {
finishReason = 'tool-calls';
}

Copilot AI Dec 8, 2025

Copy link

Choose a reason for hiding this comment

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

The streaming finishReason override logic for Gemini 3 thoughtSignature lacks unit test coverage. Consider adding a test in src/chat/index.test.ts in the doStream section, similar to the existing 'should stream tool deltas' test, but with:

  • Tool call chunks in the stream
  • A chunk with reasoning_details containing type: ReasoningDetailType.Encrypted and data
  • finish_reason: 'stop' in the final chunk
  • Assertion that the finish event has finishReason: 'tool-calls'

Copilot uses AI. Check for mistakes.

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.

same

When Gemini 3 makes tool calls with encrypted reasoning (thoughtSignature),
it returns finish_reason='stop' instead of 'tool_calls'. This causes the
AI SDK to not continue the conversation after tool execution.

This fix detects when there are:
- Tool calls present
- Encrypted reasoning details (thoughtSignature)
- finish_reason is 'stop'

And overrides the finish reason to 'tool-calls' so the conversation continues.

Applied to both doGenerate (non-streaming) and doStream (streaming) paths.
@subtleGradient subtleGradient self-assigned this Dec 9, 2025
@subtleGradient
subtleGradient merged commit 0caf9f1 into main Dec 9, 2025
3 checks passed
@subtleGradient
subtleGradient deleted the tom/gemini3-thoughtsig-fix-v1 branch December 9, 2025 23:59
@github-actions github-actions Bot mentioned this pull request Dec 9, 2025
kesavan-byte pushed a commit to osm-API/ai-sdk-provider that referenced this pull request Feb 13, 2026
OpenRouterTeam#288)

* fix: override finishReason to tool-calls for Gemini 3 thoughtSignature

When Gemini 3 makes tool calls with encrypted reasoning (thoughtSignature),
it returns finish_reason='stop' instead of 'tool_calls'. This causes the
AI SDK to not continue the conversation after tool execution.

This fix detects when there are:
- Tool calls present
- Encrypted reasoning details (thoughtSignature)
- finish_reason is 'stop'

And overrides the finish reason to 'tool-calls' so the conversation continues.

Applied to both doGenerate (non-streaming) and doStream (streaming) paths.

* fix: override finishReason to tool-calls for Gemini 3 thoughtSignature

When Gemini 3 makes tool calls with encrypted reasoning (thoughtSignature),
it returns finish_reason='stop' instead of 'tool_calls'. This causes the
AI SDK to not continue the conversation after tool execution.

This fix detects when there are:
- Tool calls present
- Encrypted reasoning details (thoughtSignature)
- finish_reason is 'stop'

And overrides the finish reason to 'tool-calls' so the conversation continues.

Applied to both doGenerate (non-streaming) and doStream (streaming) paths.
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.

3 participants