diff --git a/models/spring-ai-google-genai/src/main/java/org/springframework/ai/google/genai/GoogleGenAiChatModel.java b/models/spring-ai-google-genai/src/main/java/org/springframework/ai/google/genai/GoogleGenAiChatModel.java index 629431a3aa4..fd2ceab0919 100644 --- a/models/spring-ai-google-genai/src/main/java/org/springframework/ai/google/genai/GoogleGenAiChatModel.java +++ b/models/spring-ai-google-genai/src/main/java/org/springframework/ai/google/genai/GoogleGenAiChatModel.java @@ -16,6 +16,7 @@ package org.springframework.ai.google.genai; +import com.google.genai.types.UrlContext; import java.net.URI; import java.util.ArrayList; import java.util.Collection; @@ -489,6 +490,8 @@ Prompt buildRequestPrompt(Prompt prompt) { this.defaultOptions.getSafetySettings())); requestOptions .setLabels(ModelOptionsUtils.mergeOption(runtimeOptions.getLabels(), this.defaultOptions.getLabels())); + requestOptions.setUrlContextEnabled(ModelOptionsUtils.mergeOption(runtimeOptions.getUrlContextEnabled(), + this.defaultOptions.getUrlContextEnabled())); } else { requestOptions.setInternalToolExecutionEnabled(this.defaultOptions.getInternalToolExecutionEnabled()); @@ -499,6 +502,7 @@ Prompt buildRequestPrompt(Prompt prompt) { requestOptions.setGoogleSearchRetrieval(this.defaultOptions.getGoogleSearchRetrieval()); requestOptions.setSafetySettings(this.defaultOptions.getSafetySettings()); requestOptions.setLabels(this.defaultOptions.getLabels()); + requestOptions.setUrlContextEnabled(this.defaultOptions.getUrlContextEnabled()); } ToolCallingChatOptions.validateToolCallbacks(requestOptions.getToolCallbacks()); @@ -749,6 +753,14 @@ GeminiRequest createGeminiRequest(Prompt prompt) { tools.add(googleSearchRetrievalTool); } + if (prompt.getOptions() instanceof GoogleGenAiChatOptions options && Boolean.TRUE.equals(options.getUrlContextEnabled())) { + final var urlContextTool = Tool.builder() + .urlContext(UrlContext.builder().build()) + .build(); + + tools.add(urlContextTool); + } + if (!CollectionUtils.isEmpty(tools)) { configBuilder.tools(tools); } diff --git a/models/spring-ai-google-genai/src/main/java/org/springframework/ai/google/genai/GoogleGenAiChatOptions.java b/models/spring-ai-google-genai/src/main/java/org/springframework/ai/google/genai/GoogleGenAiChatOptions.java index 215d2955e5c..5931c4e21b8 100644 --- a/models/spring-ai-google-genai/src/main/java/org/springframework/ai/google/genai/GoogleGenAiChatOptions.java +++ b/models/spring-ai-google-genai/src/main/java/org/springframework/ai/google/genai/GoogleGenAiChatOptions.java @@ -188,6 +188,12 @@ public class GoogleGenAiChatOptions implements ToolCallingChatOptions, Structure private Map labels = new HashMap<>(); // @formatter:on + /** + * Enable Google's UrlContext tool + */ + @JsonIgnore + private Boolean urlContextEnabled; + public static Builder builder() { return new Builder(); } @@ -218,6 +224,7 @@ public static GoogleGenAiChatOptions fromOptions(GoogleGenAiChatOptions fromOpti options.setUseCachedContent(fromOptions.getUseCachedContent()); options.setAutoCacheThreshold(fromOptions.getAutoCacheThreshold()); options.setAutoCacheTtl(fromOptions.getAutoCacheTtl()); + options.setUrlContextEnabled(fromOptions.getUrlContextEnabled()); return options; } @@ -459,6 +466,14 @@ public void setOutputSchema(String jsonSchemaText) { this.setResponseMimeType("application/json"); } + public Boolean getUrlContextEnabled() { + return this.urlContextEnabled; + } + + public void setUrlContextEnabled(Boolean urlContextEnabled) { + this.urlContextEnabled = urlContextEnabled; + } + @Override public boolean equals(Object o) { if (this == o) { @@ -481,7 +496,8 @@ public boolean equals(Object o) { && Objects.equals(this.toolNames, that.toolNames) && Objects.equals(this.safetySettings, that.safetySettings) && Objects.equals(this.internalToolExecutionEnabled, that.internalToolExecutionEnabled) - && Objects.equals(this.toolContext, that.toolContext) && Objects.equals(this.labels, that.labels); + && Objects.equals(this.toolContext, that.toolContext) && Objects.equals(this.labels, that.labels) + && Objects.equals(this.urlContextEnabled, that.urlContextEnabled); } @Override @@ -490,7 +506,7 @@ public int hashCode() { this.frequencyPenalty, this.presencePenalty, this.thinkingBudget, this.maxOutputTokens, this.model, this.responseMimeType, this.responseSchema, this.toolCallbacks, this.toolNames, this.googleSearchRetrieval, this.safetySettings, this.internalToolExecutionEnabled, this.toolContext, - this.labels); + this.labels, this.urlContextEnabled); } @Override @@ -502,7 +518,7 @@ public String toString() { + this.model + '\'' + ", responseMimeType='" + this.responseMimeType + '\'' + ", toolCallbacks=" + this.toolCallbacks + ", toolNames=" + this.toolNames + ", googleSearchRetrieval=" + this.googleSearchRetrieval + ", safetySettings=" + this.safetySettings + ", labels=" + this.labels - + '}'; + + ", urlContextEnabled=" + this.urlContextEnabled + '}'; } @Override @@ -671,6 +687,11 @@ public Builder autoCacheTtl(java.time.Duration autoCacheTtl) { return this; } + public Builder urlContextEnabled(boolean enabled) { + this.options.setUrlContextEnabled(enabled); + return this; + } + public GoogleGenAiChatOptions build() { return this.options; } diff --git a/models/spring-ai-google-genai/src/test/java/org/springframework/ai/google/genai/CreateGeminiRequestTests.java b/models/spring-ai-google-genai/src/test/java/org/springframework/ai/google/genai/CreateGeminiRequestTests.java index 4f7abbaceec..ee00605bd92 100644 --- a/models/spring-ai-google-genai/src/test/java/org/springframework/ai/google/genai/CreateGeminiRequestTests.java +++ b/models/spring-ai-google-genai/src/test/java/org/springframework/ai/google/genai/CreateGeminiRequestTests.java @@ -299,6 +299,26 @@ public void createRequestWithGenerationConfigOptions() { assertThat(request.config().responseMimeType().orElse("")).isEqualTo("application/json"); } + @Test + public void createRequestWithUrlContextToolEnabled() { + + var client = GoogleGenAiChatModel.builder() + .genAiClient(this.genAiClient) + .defaultOptions(GoogleGenAiChatOptions.builder().model("DEFAULT_MODEL").urlContextEnabled(true).build()) + .build(); + + GeminiRequest request = client + .createGeminiRequest(client.buildRequestPrompt(new Prompt("Test message content"))); + + assertThat(request.config().tools()).isPresent(); + assertThat(request.config().tools().get()).anySatisfy(tool -> assertThat(tool.urlContext()).isPresent()); + + request = client.createGeminiRequest(client.buildRequestPrompt( + new Prompt("Test message content", GoogleGenAiChatOptions.builder().urlContextEnabled(false).build()))); + + assertThat(request.config().tools()).isEmpty(); + } + @Test public void createRequestWithThinkingBudget() { diff --git a/models/spring-ai-google-genai/src/test/java/org/springframework/ai/google/genai/GoogleGenAiChatOptionsTest.java b/models/spring-ai-google-genai/src/test/java/org/springframework/ai/google/genai/GoogleGenAiChatOptionsTest.java index c3f78a2ab02..79f5fe229c4 100644 --- a/models/spring-ai-google-genai/src/test/java/org/springframework/ai/google/genai/GoogleGenAiChatOptionsTest.java +++ b/models/spring-ai-google-genai/src/test/java/org/springframework/ai/google/genai/GoogleGenAiChatOptionsTest.java @@ -167,4 +167,27 @@ public void testLabelsWithEmptyMap() { assertThat(options.getLabels()).isEmpty(); } + @Test + public void testUrlContextEnabledCopyAndEquality() { + GoogleGenAiChatOptions original = GoogleGenAiChatOptions.builder() + .model("test-model") + .urlContextEnabled(true) + .build(); + + GoogleGenAiChatOptions copy = original.copy(); + + assertThat(original.getUrlContextEnabled()).isTrue(); + assertThat(copy.getUrlContextEnabled()).isTrue(); + assertThat(copy).isEqualTo(original); + assertThat(copy).isNotSameAs(original); + assertThat(copy.toString()).contains("urlContextEnabled=true"); + + GoogleGenAiChatOptions different = GoogleGenAiChatOptions.builder() + .model("test-model") + .urlContextEnabled(false) + .build(); + + assertThat(original).isNotEqualTo(different); + assertThat(original.hashCode()).isNotEqualTo(different.hashCode()); + } }