GoogleGenAiChatModel fails to recognize tool calls when combined with text or thinking parts
Bug description
The GoogleGenAiChatModel fails to detect and extract tool calls when a model response contains a mix of text (or thought) parts and functionCall parts. This is caused by a strict allMatch check in responseCandidateToGeneration. If a response contains both reasoning text and a function call, the function call is ignored, and the tool-calling loop breaks.
Environment Spring AI version: 1.1.2 Java 25 Model: Gemini 3 Flash (with Thinking enabled and includeThoughts(true))
Steps to reproduce
- Configure
GoogleGenAiChatModelwithGoogleGenAiChatOptionswherethinkingLevelis set toHIGHandincludeThoughts(true)andinternalToolExecutionEnabled(false) - Provide a prompt that triggers both reasoning and a tool call.
- The model returns a candidate with multiple parts: text (plan/thought) and functionCall.
- Observe that AssistantMessage.getToolCalls() is empty because
isFunctionCallevaluated to false due to theallMatchlogic.
Expected behavior
The model should identify and extract functionCall parts even if other content parts (like text or thought) are present in the same candidate. The resulting AssistantMessage should contain both the text content and the populated toolCalls list.
Minimal Complete Reproducible example
The issue is located in org.springframework.ai.google.genai.GoogleGenAiChatModel#responseCandidateToGeneration
// Current buggy logic:
boolean isFunctionCall = candidate.content().isPresent()
&& candidate.content().get().parts().isPresent()
&& candidate.content().get().parts().get().stream()
.allMatch(part -> part.functionCall().isPresent()); // FAILS on mixed partsIf candidate.content().get().parts() returns [TextPart("plan"), FunctionCallPart("tool")], isFunctionCall becomes false, and the function call is lost in the else block.
Example of gemini response with thoughts and toolCalls
{
"sdkHttpResponse": {
"headers": {
"date": "Mon, 16 Feb 2026 20:38:15 GMT",
"server": "scaffolding on HTTPServer2",
"x-content-type-options": "nosniff",
"x-xss-protection": "0",
"vary": "Referer",
"x-frame-options": "SAMEORIGIN",
"content-type": "application/json; charset=UTF-8",
"server-timing": "gfet4t7; dur=5293",
"alt-svc": "h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000"
}
},
"candidates": [
{
"content": {
"parts": [
{
"text": "Thoughts text of what model plans todo",
"thought": true
},
{
"functionCall": {
"args": {
"location": "Tokyo",
"unit": "C"
},
"name": "getCurrentWeather"
},
"thoughtSignature": "thoughtSignature bla bla"
},
{
"functionCall": {
"args": {
"location": "London",
"unit": "C"
},
"name": "getCurrentWeather"
}
}
],
"role": "model"
},
"finishReason": "STOP",
"index": 0
}
],
"modelVersion": "gemini-3-flash-preview",
"responseId": "fake responseId",
"usageMetadata": {
"candidatesTokenCount": 75,
"promptTokenCount": 24966,
"promptTokensDetails": [
{
"modality": "TEXT",
"tokenCount": 24966
}
],
"thoughtsTokenCount": 320,
"totalTokenCount": 25361
}
}Source: spring-projects/spring-ai