Skip to content

Commit 6f29fc8

Browse files
committed
- Changes to includeThoughts() after Gemini 3 Pro model release
- documented latest change - documented outstanding chapter on cached content support Signed-off-by: ddobrin <[email protected]>
1 parent 23c838e commit 6f29fc8

File tree

8 files changed

+393
-29
lines changed

8 files changed

+393
-29
lines changed

auto-configurations/models/spring-ai-autoconfigure-model-google-genai/src/test/java/org/springframework/ai/model/google/genai/autoconfigure/chat/GoogleGenAiPropertiesTests.java

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,25 @@ void extendedUsageMetadataDefaultBinding() {
131131
});
132132
}
133133

134+
@Test
135+
void includeThoughtsPropertiesBinding() {
136+
this.contextRunner.withPropertyValues("spring.ai.google.genai.chat.options.include-thoughts=true")
137+
.run(context -> {
138+
GoogleGenAiChatProperties chatProperties = context.getBean(GoogleGenAiChatProperties.class);
139+
assertThat(chatProperties.getOptions().getIncludeThoughts()).isTrue();
140+
});
141+
}
142+
143+
@Test
144+
void includeThoughtsDefaultBinding() {
145+
// Test that defaults are applied when not specified
146+
this.contextRunner.run(context -> {
147+
GoogleGenAiChatProperties chatProperties = context.getBean(GoogleGenAiChatProperties.class);
148+
// Should be null when not set
149+
assertThat(chatProperties.getOptions().getIncludeThoughts()).isNull();
150+
});
151+
}
152+
134153
@Configuration
135154
@EnableConfigurationProperties({ GoogleGenAiConnectionProperties.class, GoogleGenAiChatProperties.class,
136155
GoogleGenAiEmbeddingConnectionProperties.class })

models/spring-ai-google-genai/src/main/java/org/springframework/ai/google/genai/GoogleGenAiChatModel.java

Lines changed: 63 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -281,9 +281,30 @@ else if (message instanceof UserMessage userMessage) {
281281
}
282282
else if (message instanceof AssistantMessage assistantMessage) {
283283
List<Part> parts = new ArrayList<>();
284+
285+
// Check if there are thought signatures to restore
286+
List<byte[]> thoughtSignatures = null;
287+
if (assistantMessage.getMetadata() != null
288+
&& assistantMessage.getMetadata().containsKey("thoughtSignatures")) {
289+
Object signaturesObj = assistantMessage.getMetadata().get("thoughtSignatures");
290+
if (signaturesObj instanceof List) {
291+
thoughtSignatures = (List<byte[]>) signaturesObj;
292+
}
293+
}
294+
295+
// Add text part, potentially with thought signature
284296
if (StringUtils.hasText(assistantMessage.getText())) {
285-
parts.add(Part.fromText(assistantMessage.getText()));
297+
Part.Builder partBuilder = Part.builder().text(assistantMessage.getText());
298+
// If we have thought signatures, apply the first one to this text part
299+
if (thoughtSignatures != null && !thoughtSignatures.isEmpty()) {
300+
partBuilder.thoughtSignature(thoughtSignatures.get(0));
301+
// Remove the used signature
302+
thoughtSignatures = thoughtSignatures.subList(1, thoughtSignatures.size());
303+
}
304+
parts.add(partBuilder.build());
286305
}
306+
307+
// Add function call parts
287308
if (!CollectionUtils.isEmpty(assistantMessage.getToolCalls())) {
288309
parts.addAll(assistantMessage.getToolCalls()
289310
.stream()
@@ -295,6 +316,16 @@ else if (message instanceof AssistantMessage assistantMessage) {
295316
.build())
296317
.toList());
297318
}
319+
320+
// If there are remaining thought signatures without corresponding content,
321+
// we might need to add empty parts with thought signatures.
322+
// This handles the case where the model returned only thoughts without text.
323+
if (thoughtSignatures != null && !thoughtSignatures.isEmpty()) {
324+
for (byte[] signature : thoughtSignatures) {
325+
parts.add(Part.builder().thoughtSignature(signature).build());
326+
}
327+
}
328+
298329
return parts;
299330
}
300331
else if (message instanceof ToolResponseMessage toolResponseMessage) {
@@ -601,8 +632,22 @@ protected List<Generation> responseCandidateToGeneration(Candidate candidate) {
601632
int candidateIndex = candidate.index().orElse(0);
602633
FinishReason candidateFinishReason = candidate.finishReason().orElse(new FinishReason(FinishReason.Known.STOP));
603634

604-
Map<String, Object> messageMetadata = Map.of("candidateIndex", candidateIndex, "finishReason",
605-
candidateFinishReason);
635+
Map<String, Object> messageMetadata = new HashMap<>();
636+
messageMetadata.put("candidateIndex", candidateIndex);
637+
messageMetadata.put("finishReason", candidateFinishReason);
638+
639+
// Extract thought signatures from response parts if present
640+
if (candidate.content().isPresent() && candidate.content().get().parts().isPresent()) {
641+
List<Part> parts = candidate.content().get().parts().get();
642+
List<byte[]> thoughtSignatures = parts.stream()
643+
.filter(part -> part.thoughtSignature().isPresent())
644+
.map(part -> part.thoughtSignature().get())
645+
.toList();
646+
647+
if (!thoughtSignatures.isEmpty()) {
648+
messageMetadata.put("thoughtSignatures", thoughtSignatures);
649+
}
650+
}
606651

607652
ChatGenerationMetadata chatGenerationMetadata = ChatGenerationMetadata.builder()
608653
.finishReason(candidateFinishReason.toString())
@@ -713,10 +758,19 @@ GeminiRequest createGeminiRequest(Prompt prompt) {
713758
if (requestOptions.getPresencePenalty() != null) {
714759
configBuilder.presencePenalty(requestOptions.getPresencePenalty().floatValue());
715760
}
716-
if (requestOptions.getThinkingBudget() != null) {
717-
configBuilder
718-
.thinkingConfig(ThinkingConfig.builder().thinkingBudget(requestOptions.getThinkingBudget()).build());
761+
762+
// Build thinking config if either thinkingBudget or includeThoughts is set
763+
if (requestOptions.getThinkingBudget() != null || requestOptions.getIncludeThoughts() != null) {
764+
ThinkingConfig.Builder thinkingBuilder = ThinkingConfig.builder();
765+
if (requestOptions.getThinkingBudget() != null) {
766+
thinkingBuilder.thinkingBudget(requestOptions.getThinkingBudget());
767+
}
768+
if (requestOptions.getIncludeThoughts() != null) {
769+
thinkingBuilder.includeThoughts(requestOptions.getIncludeThoughts());
770+
}
771+
configBuilder.thinkingConfig(thinkingBuilder.build());
719772
}
773+
720774
if (requestOptions.getLabels() != null && !requestOptions.getLabels().isEmpty()) {
721775
configBuilder.labels(requestOptions.getLabels());
722776
}
@@ -1065,7 +1119,9 @@ public enum ChatModel implements ChatModelDescription {
10651119
* See: <a href=
10661120
* "https://cloud.google.com/vertex-ai/generative-ai/docs/models/gemini/2-5-flash-lite">gemini-2.5-flash-lite</a>
10671121
*/
1068-
GEMINI_2_5_FLASH_LIGHT("gemini-2.5-flash-lite");
1122+
GEMINI_2_5_FLASH_LIGHT("gemini-2.5-flash-lite"),
1123+
1124+
GEMINI_3_PRO_PREVIEW("gemini-3-pro-preview");
10691125

10701126
public final String value;
10711127

models/spring-ai-google-genai/src/main/java/org/springframework/ai/google/genai/GoogleGenAiChatOptions.java

Lines changed: 37 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,19 @@ public class GoogleGenAiChatOptions implements ToolCallingChatOptions {
113113
*/
114114
private @JsonProperty("thinkingBudget") Integer thinkingBudget;
115115

116+
/**
117+
* Optional. Whether to include thoughts in the response.
118+
* When true, thoughts are returned if the model supports them and thoughts are available.
119+
*
120+
* <p><strong>IMPORTANT:</strong> For Gemini 3 Pro with function calling,
121+
* this MUST be set to true to avoid validation errors. Thought signatures
122+
* are automatically propagated in multi-turn conversations to maintain context.
123+
*
124+
* <p>Note: Enabling thoughts increases token usage and API costs.
125+
* This is part of the thinkingConfig in GenerationConfig.
126+
*/
127+
private @JsonProperty("includeThoughts") Boolean includeThoughts;
128+
116129
/**
117130
* Optional. Whether to include extended usage metadata in responses.
118131
* When true, includes thinking tokens, cached content, tool-use tokens, and modality details.
@@ -206,6 +219,7 @@ public static GoogleGenAiChatOptions fromOptions(GoogleGenAiChatOptions fromOpti
206219
options.setInternalToolExecutionEnabled(fromOptions.getInternalToolExecutionEnabled());
207220
options.setToolContext(fromOptions.getToolContext());
208221
options.setThinkingBudget(fromOptions.getThinkingBudget());
222+
options.setIncludeThoughts(fromOptions.getIncludeThoughts());
209223
options.setLabels(fromOptions.getLabels());
210224
options.setIncludeExtendedUsageMetadata(fromOptions.getIncludeExtendedUsageMetadata());
211225
options.setCachedContentName(fromOptions.getCachedContentName());
@@ -357,6 +371,14 @@ public void setThinkingBudget(Integer thinkingBudget) {
357371
this.thinkingBudget = thinkingBudget;
358372
}
359373

374+
public Boolean getIncludeThoughts() {
375+
return this.includeThoughts;
376+
}
377+
378+
public void setIncludeThoughts(Boolean includeThoughts) {
379+
this.includeThoughts = includeThoughts;
380+
}
381+
360382
public Boolean getIncludeExtendedUsageMetadata() {
361383
return this.includeExtendedUsageMetadata;
362384
}
@@ -448,6 +470,7 @@ public boolean equals(Object o) {
448470
&& Objects.equals(this.frequencyPenalty, that.frequencyPenalty)
449471
&& Objects.equals(this.presencePenalty, that.presencePenalty)
450472
&& Objects.equals(this.thinkingBudget, that.thinkingBudget)
473+
&& Objects.equals(this.includeThoughts, that.includeThoughts)
451474
&& Objects.equals(this.maxOutputTokens, that.maxOutputTokens) && Objects.equals(this.model, that.model)
452475
&& Objects.equals(this.responseMimeType, that.responseMimeType)
453476
&& Objects.equals(this.toolCallbacks, that.toolCallbacks)
@@ -460,21 +483,22 @@ public boolean equals(Object o) {
460483
@Override
461484
public int hashCode() {
462485
return Objects.hash(this.stopSequences, this.temperature, this.topP, this.topK, this.candidateCount,
463-
this.frequencyPenalty, this.presencePenalty, this.thinkingBudget, this.maxOutputTokens, this.model,
464-
this.responseMimeType, this.toolCallbacks, this.toolNames, this.googleSearchRetrieval,
465-
this.safetySettings, this.internalToolExecutionEnabled, this.toolContext, this.labels);
486+
this.frequencyPenalty, this.presencePenalty, this.thinkingBudget, this.includeThoughts,
487+
this.maxOutputTokens, this.model, this.responseMimeType, this.toolCallbacks, this.toolNames,
488+
this.googleSearchRetrieval, this.safetySettings, this.internalToolExecutionEnabled, this.toolContext,
489+
this.labels);
466490
}
467491

468492
@Override
469493
public String toString() {
470494
return "GoogleGenAiChatOptions{" + "stopSequences=" + this.stopSequences + ", temperature=" + this.temperature
471495
+ ", topP=" + this.topP + ", topK=" + this.topK + ", frequencyPenalty=" + this.frequencyPenalty
472496
+ ", presencePenalty=" + this.presencePenalty + ", thinkingBudget=" + this.thinkingBudget
473-
+ ", candidateCount=" + this.candidateCount + ", maxOutputTokens=" + this.maxOutputTokens + ", model='"
474-
+ this.model + '\'' + ", responseMimeType='" + this.responseMimeType + '\'' + ", toolCallbacks="
475-
+ this.toolCallbacks + ", toolNames=" + this.toolNames + ", googleSearchRetrieval="
476-
+ this.googleSearchRetrieval + ", safetySettings=" + this.safetySettings + ", labels=" + this.labels
477-
+ '}';
497+
+ ", includeThoughts=" + this.includeThoughts + ", candidateCount=" + this.candidateCount
498+
+ ", maxOutputTokens=" + this.maxOutputTokens + ", model='" + this.model + '\'' + ", responseMimeType='"
499+
+ this.responseMimeType + '\'' + ", toolCallbacks=" + this.toolCallbacks + ", toolNames="
500+
+ this.toolNames + ", googleSearchRetrieval=" + this.googleSearchRetrieval + ", safetySettings="
501+
+ this.safetySettings + ", labels=" + this.labels + '}';
478502
}
479503

480504
@Override
@@ -602,6 +626,11 @@ public Builder thinkingBudget(Integer thinkingBudget) {
602626
return this;
603627
}
604628

629+
public Builder includeThoughts(Boolean includeThoughts) {
630+
this.options.setIncludeThoughts(includeThoughts);
631+
return this;
632+
}
633+
605634
public Builder includeExtendedUsageMetadata(Boolean includeExtendedUsageMetadata) {
606635
this.options.setIncludeExtendedUsageMetadata(includeExtendedUsageMetadata);
607636
return this;

models/spring-ai-google-genai/src/test/java/org/springframework/ai/google/genai/GoogleGenAiChatModelObservationApiKeyIT.java

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ void beforeEach() {
6464
void observationForChatOperation() {
6565

6666
var options = GoogleGenAiChatOptions.builder()
67-
.model(GoogleGenAiChatModel.ChatModel.GEMINI_2_0_FLASH.getValue())
67+
.model(GoogleGenAiChatModel.ChatModel.GEMINI_3_PRO_PREVIEW.getValue())
6868
.temperature(0.7)
6969
.stopSequences(List.of("this-is-the-end"))
7070
.maxOutputTokens(2048)
@@ -86,7 +86,7 @@ void observationForChatOperation() {
8686
void observationForStreamingOperation() {
8787

8888
var options = GoogleGenAiChatOptions.builder()
89-
.model(GoogleGenAiChatModel.ChatModel.GEMINI_2_0_FLASH.getValue())
89+
.model(GoogleGenAiChatModel.ChatModel.GEMINI_3_PRO_PREVIEW.getValue())
9090
.temperature(0.7)
9191
.stopSequences(List.of("this-is-the-end"))
9292
.maxOutputTokens(2048)
@@ -126,7 +126,7 @@ private void validate(ChatResponseMetadata responseMetadata) {
126126
AiProvider.GOOGLE_GENAI_AI.value())
127127
.hasLowCardinalityKeyValue(
128128
ChatModelObservationDocumentation.LowCardinalityKeyNames.REQUEST_MODEL.asString(),
129-
GoogleGenAiChatModel.ChatModel.GEMINI_2_0_FLASH.getValue())
129+
GoogleGenAiChatModel.ChatModel.GEMINI_3_PRO_PREVIEW.getValue())
130130
.hasHighCardinalityKeyValue(
131131
ChatModelObservationDocumentation.HighCardinalityKeyNames.REQUEST_MAX_TOKENS.asString(), "2048")
132132
.hasHighCardinalityKeyValue(
@@ -174,8 +174,9 @@ public GoogleGenAiChatModel vertexAiEmbedding(Client genAiClient, TestObservatio
174174
return GoogleGenAiChatModel.builder()
175175
.genAiClient(genAiClient)
176176
.observationRegistry(observationRegistry)
177-
.defaultOptions(
178-
GoogleGenAiChatOptions.builder().model(GoogleGenAiChatModel.ChatModel.GEMINI_2_0_FLASH).build())
177+
.defaultOptions(GoogleGenAiChatOptions.builder()
178+
.model(GoogleGenAiChatModel.ChatModel.GEMINI_3_PRO_PREVIEW)
179+
.build())
179180
.build();
180181
}
181182

models/spring-ai-google-genai/src/test/java/org/springframework/ai/google/genai/GoogleGenAiRetryTests.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ public void setUp() {
6161
GoogleGenAiChatOptions.builder()
6262
.temperature(0.7)
6363
.topP(1.0)
64-
.model(GoogleGenAiChatModel.ChatModel.GEMINI_2_0_FLASH.getValue())
64+
.model(GoogleGenAiChatModel.ChatModel.GEMINI_3_PRO_PREVIEW.getValue())
6565
.build(),
6666
this.retryTemplate);
6767

models/spring-ai-google-genai/src/test/java/org/springframework/ai/google/genai/tool/GoogleGenAiChatModelToolCallingIT.java

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,6 @@ public void functionCallTestInferredOpenApiSchema() {
104104
List<Message> messages = new ArrayList<>(List.of(userMessage));
105105

106106
var promptOptions = GoogleGenAiChatOptions.builder()
107-
.model(GoogleGenAiChatModel.ChatModel.GEMINI_2_0_FLASH)
108107
.toolCallbacks(List.of(
109108
FunctionToolCallback.builder("get_current_weather", new MockWeatherService())
110109
.description("Get the current weather in a given location.")
@@ -125,7 +124,7 @@ public void functionCallTestInferredOpenApiSchema() {
125124

126125
assertThat(chatResponse.getMetadata()).isNotNull();
127126
assertThat(chatResponse.getMetadata().getUsage()).isNotNull();
128-
assertThat(chatResponse.getMetadata().getUsage().getTotalTokens()).isGreaterThan(150).isLessThan(330);
127+
assertThat(chatResponse.getMetadata().getUsage().getTotalTokens()).isGreaterThan(150).isLessThan(500);
129128

130129
ChatResponse response2 = this.chatModel
131130
.call(new Prompt("What is the payment status for transaction 696?", promptOptions));
@@ -145,7 +144,6 @@ public void functionCallTestInferredOpenApiSchemaStream() {
145144
List<Message> messages = new ArrayList<>(List.of(userMessage));
146145

147146
var promptOptions = GoogleGenAiChatOptions.builder()
148-
.model(GoogleGenAiChatModel.ChatModel.GEMINI_2_0_FLASH)
149147
.toolCallbacks(List.of(FunctionToolCallback.builder("getCurrentWeather", new MockWeatherService())
150148
.description("Get the current weather in a given location")
151149
.inputType(MockWeatherService.Request.class)
@@ -178,7 +176,6 @@ public void functionCallUsageTestInferredOpenApiSchemaStreamFlash20() {
178176
List<Message> messages = new ArrayList<>(List.of(userMessage));
179177

180178
var promptOptions = GoogleGenAiChatOptions.builder()
181-
.model(GoogleGenAiChatModel.ChatModel.GEMINI_2_0_FLASH)
182179
.toolCallbacks(List.of(
183180
FunctionToolCallback.builder("get_current_weather", new MockWeatherService())
184181
.description("Get the current weather in a given location.")
@@ -200,7 +197,7 @@ public void functionCallUsageTestInferredOpenApiSchemaStreamFlash20() {
200197
assertThat(chatResponse).isNotNull();
201198
assertThat(chatResponse.getMetadata()).isNotNull();
202199
assertThat(chatResponse.getMetadata().getUsage()).isNotNull();
203-
assertThat(chatResponse.getMetadata().getUsage().getTotalTokens()).isGreaterThan(150).isLessThan(330);
200+
assertThat(chatResponse.getMetadata().getUsage().getTotalTokens()).isGreaterThan(150).isLessThan(500);
204201

205202
}
206203

@@ -271,7 +268,7 @@ public GoogleGenAiChatModel vertexAiEmbedding(Client genAiClient) {
271268
return GoogleGenAiChatModel.builder()
272269
.genAiClient(genAiClient)
273270
.defaultOptions(GoogleGenAiChatOptions.builder()
274-
.model(GoogleGenAiChatModel.ChatModel.GEMINI_2_0_FLASH)
271+
.model(GoogleGenAiChatModel.ChatModel.GEMINI_2_5_FLASH)
275272
.temperature(0.9)
276273
.build())
277274
.build();

pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -288,7 +288,7 @@
288288
<onnxruntime.version>1.19.2</onnxruntime.version>
289289
<oci-sdk-version>3.63.1</oci-sdk-version>
290290
<com.google.cloud.version>26.60.0</com.google.cloud.version>
291-
<com.google.genai.version>1.17.0</com.google.genai.version>
291+
<com.google.genai.version>1.28.0</com.google.genai.version>
292292
<ibm.sdk.version>9.20.0</ibm.sdk.version>
293293
<jsonschema.version>4.38.0</jsonschema.version>
294294
<swagger-annotations.version>2.2.30</swagger-annotations.version>

0 commit comments

Comments
 (0)