Repository navigation
fix: override finishReason to tool-calls for Gemini 3 thoughtSignature - #288
Conversation
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.
There was a problem hiding this comment.
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
doStreamflush 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.
| // 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); |
There was a problem hiding this comment.
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_callspresent in the responsereasoning_detailswithtype: ReasoningDetailType.Encryptedanddatapresentfinish_reason: 'stop'in the response- Assertion that the returned
finishReasonis'tool-calls'
There was a problem hiding this comment.
agree, this needs tests
| // 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'; | ||
| } |
There was a problem hiding this comment.
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_detailscontainingtype: ReasoningDetailType.Encryptedanddata finish_reason: 'stop'in the final chunk- Assertion that the finish event has
finishReason: 'tool-calls'
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.
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.
Summary
finish_reason='stop'but has tool calls and encrypted reasoning, override totool-callsso the SDK continuesProblem
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-callsin both:doGenerate(non-streaming)doStream(streaming) flush handler