From d3a111e582248a5dd503ffb64aaa095e6481d0ea Mon Sep 17 00:00:00 2001 From: Jaycee Li Date: Mon, 12 May 2025 12:50:24 -0700 Subject: [PATCH] feat: enable request level HttpOptions for LiveConnectConfig PiperOrigin-RevId: 757875770 --- .../examples/RequestLevelHttpOptions.java | 77 +++++++++++++++++++ src/main/java/com/google/genai/ApiClient.java | 20 +++-- .../java/com/google/genai/HttpApiClient.java | 19 +++-- src/main/java/com/google/genai/Models.java | 64 ++++++++++++--- .../java/com/google/genai/Operations.java | 15 +++- .../com/google/genai/ReplayApiClient.java | 3 +- .../genai/types/ComputeTokensConfig.java | 9 +++ .../google/genai/types/CountTokensConfig.java | 7 ++ .../genai/types/DownloadFileConfig.java | 63 +++++++++++++++ .../google/genai/types/EditImageConfig.java | 7 ++ .../genai/types/EmbedContentConfig.java | 7 ++ .../types/FetchPredictOperationConfig.java | 9 +++ .../genai/types/GenerateContentConfig.java | 7 ++ .../genai/types/GenerateImagesConfig.java | 7 ++ .../genai/types/GenerateVideosConfig.java | 7 ++ .../genai/types/GetOperationConfig.java | 9 +++ .../google/genai/types/LiveConnectConfig.java | 7 ++ .../google/genai/types/UploadFileConfig.java | 7 ++ .../genai/types/UpscaleImageAPIConfig.java | 7 ++ .../genai/types/UpscaleImageConfig.java | 7 ++ src/test/java/com/google/genai/ChatTest.java | 8 +- .../com/google/genai/DefaultValuesTest.java | 7 +- .../genai/ForwardCompatibilityTest.java | 14 +--- .../com/google/genai/HttpApiClientTest.java | 35 ++++++++- 24 files changed, 379 insertions(+), 43 deletions(-) create mode 100644 examples/src/main/java/com/google/genai/examples/RequestLevelHttpOptions.java create mode 100644 src/main/java/com/google/genai/types/DownloadFileConfig.java diff --git a/examples/src/main/java/com/google/genai/examples/RequestLevelHttpOptions.java b/examples/src/main/java/com/google/genai/examples/RequestLevelHttpOptions.java new file mode 100644 index 00000000000..1473a9ee9d7 --- /dev/null +++ b/examples/src/main/java/com/google/genai/examples/RequestLevelHttpOptions.java @@ -0,0 +1,77 @@ +/* + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Usage: + * + *

1a. If you are using Vertex AI, setup ADC to get credentials: + * https://cloud.google.com/docs/authentication/provide-credentials-adc#google-idp + * + *

Then set Project, Location, and USE_VERTEXAI flag as environment variables: + * + *

export GOOGLE_CLOUD_PROJECT=YOUR_PROJECT + * + *

export GOOGLE_CLOUD_LOCATION=YOUR_LOCATION + * + *

1b. If you are using Gemini Developer AI, set an API key environment variable. You can find a + * list of available API keys here: https://aistudio.google.com/app/apikey + * + *

export GOOGLE_API_KEY=YOUR_API_KEY + * + *

2. Compile the java package and run the sample code. + * + *

mvn clean compile + * + *

mvn exec:java -Dexec.mainClass="com.google.genai.examples.RequestLevelHttpOptions" + */ +package com.google.genai.examples; + +import com.google.common.collect.ImmutableMap; +import com.google.genai.Client; +import com.google.genai.types.GenerateContentConfig; +import com.google.genai.types.GenerateContentResponse; +import com.google.genai.types.HttpOptions; + +/** An example of setting http options at request level. */ +public class RequestLevelHttpOptions { + public static void main(String[] args) { + // Instantiate the client. The client by default uses the Gemini Developer API. It gets the API + // key from the environment variable `GOOGLE_API_KEY`. Vertex AI API can be used by setting the + // environment variables `GOOGLE_CLOUD_LOCATION` and `GOOGLE_CLOUD_PROJECT`, as well as setting + // `GOOGLE_GENAI_USE_VERTEXAI` to "true". + // + // Do not set http options at client level. + Client client = new Client(); + + if (client.vertexAI()) { + System.out.println("Using Vertex AI"); + } else { + System.out.println("Using Gemini Developer API"); + } + + // Set a customized header per request config. + GenerateContentConfig config = + GenerateContentConfig.builder() + .httpOptions( + HttpOptions.builder().headers(ImmutableMap.of("my-header", "my-value")).build()) + .build(); + + GenerateContentResponse response = + client.models.generateContent("gemini-2.0-flash-001", "Tell me the history of LLM", config); + + System.out.println("Response: " + response.text()); + } +} diff --git a/src/main/java/com/google/genai/ApiClient.java b/src/main/java/com/google/genai/ApiClient.java index 4e9c7c72a70..761e690b1e4 100644 --- a/src/main/java/com/google/genai/ApiClient.java +++ b/src/main/java/com/google/genai/ApiClient.java @@ -64,7 +64,7 @@ abstract class ApiClient { this.httpOptions = defaultHttpOptions(/* vertexAI= */ false, this.location); if (customHttpOptions.isPresent()) { - applyHttpOptions(customHttpOptions.get()); + this.httpOptions = applyHttpOptions(customHttpOptions.get()); } this.httpClient = createHttpClient(httpOptions.timeout()); @@ -109,7 +109,7 @@ abstract class ApiClient { this.httpOptions = defaultHttpOptions(/* vertexAI= */ true, this.location); if (customHttpOptions.isPresent()) { - applyHttpOptions(customHttpOptions.get()); + this.httpOptions = applyHttpOptions(customHttpOptions.get()); } this.apiKey = Optional.empty(); this.vertexAI = true; @@ -128,7 +128,8 @@ private CloseableHttpClient createHttpClient(Optional timeout) { } /** Sends a Http request given the http method, path, and request json string. */ - public abstract ApiResponse request(String httpMethod, String path, String requestJson); + public abstract ApiResponse request( + String httpMethod, String path, String requestJson, HttpOptions httpOptions); /** Returns the library version. */ static String libraryVersion() { @@ -172,7 +173,16 @@ private Optional> getTimeoutHeader(HttpOptions httpOptionsTo return Optional.empty(); } - private void applyHttpOptions(HttpOptions httpOptionsToApply) { + /** + * Applies the http options to the client's http options. + * + * @param httpOptionsToApply the http options to apply + * @return the merged http options + */ + HttpOptions applyHttpOptions(HttpOptions httpOptionsToApply) { + if (httpOptionsToApply == null) { + return this.httpOptions; + } HttpOptions.Builder mergedHttpOptionsBuilder = this.httpOptions.toBuilder(); if (httpOptionsToApply.baseUrl().isPresent()) { mergedHttpOptionsBuilder.baseUrl(httpOptionsToApply.baseUrl().get()); @@ -192,7 +202,7 @@ private void applyHttpOptions(HttpOptions httpOptionsToApply) { .build(); mergedHttpOptionsBuilder.headers(mergedHeaders); } - this.httpOptions = mergedHttpOptionsBuilder.build(); + return mergedHttpOptionsBuilder.build(); } static HttpOptions defaultHttpOptions(boolean vertexAI, Optional location) { diff --git a/src/main/java/com/google/genai/HttpApiClient.java b/src/main/java/com/google/genai/HttpApiClient.java index f1363f05650..d4ac3be8817 100644 --- a/src/main/java/com/google/genai/HttpApiClient.java +++ b/src/main/java/com/google/genai/HttpApiClient.java @@ -49,9 +49,11 @@ public class HttpApiClient extends ApiClient { super(project, location, credentials, httpOptions); } - /** Sends a Http request given the http method, path, and request json string. */ + /** Sends a Http request given the http method, path, request json string, and http options. */ @Override - public HttpApiResponse request(String httpMethod, String path, String requestJson) { + public HttpApiResponse request( + String httpMethod, String path, String requestJson, HttpOptions httpOptions) { + HttpOptions requestHttpOptions = applyHttpOptions(httpOptions); boolean queryBaseModel = httpMethod.equalsIgnoreCase("GET") && path.startsWith("publishers/google/models/"); if (this.vertexAI() && !path.startsWith("projects/") && !queryBaseModel) { @@ -61,20 +63,21 @@ public HttpApiResponse request(String httpMethod, String path, String requestJso } String requestUrl = String.format( - "%s/%s/%s", httpOptions.baseUrl().get(), httpOptions.apiVersion().get(), path); + "%s/%s/%s", + requestHttpOptions.baseUrl().get(), requestHttpOptions.apiVersion().get(), path); if (httpMethod.equalsIgnoreCase("POST")) { HttpPost httpPost = new HttpPost(requestUrl); - setHeaders(httpPost); + setHeaders(httpPost, requestHttpOptions); httpPost.setEntity(new StringEntity(requestJson, ContentType.APPLICATION_JSON)); return executeRequest(httpPost); } else if (httpMethod.equalsIgnoreCase("GET")) { HttpGet httpGet = new HttpGet(requestUrl); - setHeaders(httpGet); + setHeaders(httpGet, requestHttpOptions); return executeRequest(httpGet); } else if (httpMethod.equalsIgnoreCase("DELETE")) { HttpDelete httpDelete = new HttpDelete(requestUrl); - setHeaders(httpDelete); + setHeaders(httpDelete, requestHttpOptions); return executeRequest(httpDelete); } else { throw new IllegalArgumentException("Unsupported HTTP method: " + httpMethod); @@ -82,9 +85,9 @@ public HttpApiResponse request(String httpMethod, String path, String requestJso } /** Sets the required headers (including auth) on the request object. */ - private void setHeaders(HttpRequestBase request) { + private void setHeaders(HttpRequestBase request, HttpOptions requestHttpOptions) { for (Map.Entry header : - httpOptions.headers().orElse(ImmutableMap.of()).entrySet()) { + requestHttpOptions.headers().orElse(ImmutableMap.of()).entrySet()) { request.setHeader(header.getKey(), header.getValue()); } diff --git a/src/main/java/com/google/genai/Models.java b/src/main/java/com/google/genai/Models.java index 82066b3adeb..5c094ddce56 100644 --- a/src/main/java/com/google/genai/Models.java +++ b/src/main/java/com/google/genai/Models.java @@ -47,6 +47,7 @@ import com.google.genai.types.GenerateVideosOperation; import com.google.genai.types.GenerateVideosParameters; import com.google.genai.types.GeneratedImage; +import com.google.genai.types.HttpOptions; import com.google.genai.types.Image; import com.google.genai.types.Part; import com.google.genai.types.ReferenceImage; @@ -4116,8 +4117,13 @@ private GenerateContentResponse privateGenerateContent( // TODO: Remove the hack that removes config. body.remove("config"); + HttpOptions httpOptions = null; + if (config != null) { + httpOptions = config.httpOptions().orElse(null); + } + try (ApiResponse response = - this.apiClient.request("post", path, JsonSerializable.toJsonString(body))) { + this.apiClient.request("post", path, JsonSerializable.toJsonString(body), httpOptions)) { HttpEntity entity = response.getEntity(); String responseString; try { @@ -4167,8 +4173,13 @@ private ResponseStream privateGenerateContentStream( // TODO: Remove the hack that removes config. body.remove("config"); + HttpOptions httpOptions = null; + if (config != null) { + httpOptions = config.httpOptions().orElse(null); + } + ApiResponse response = - this.apiClient.request("post", path, JsonSerializable.toJsonString(body)); + this.apiClient.request("post", path, JsonSerializable.toJsonString(body), httpOptions); String converterName; if (this.apiClient.vertexAI()) { converterName = "generateContentResponseFromVertex"; @@ -4211,8 +4222,13 @@ private EmbedContentResponse privateEmbedContent( // TODO: Remove the hack that removes config. body.remove("config"); + HttpOptions httpOptions = null; + if (config != null) { + httpOptions = config.httpOptions().orElse(null); + } + try (ApiResponse response = - this.apiClient.request("post", path, JsonSerializable.toJsonString(body))) { + this.apiClient.request("post", path, JsonSerializable.toJsonString(body), httpOptions)) { HttpEntity entity = response.getEntity(); String responseString; try { @@ -4262,8 +4278,13 @@ private GenerateImagesResponse privateGenerateImages( // TODO: Remove the hack that removes config. body.remove("config"); + HttpOptions httpOptions = null; + if (config != null) { + httpOptions = config.httpOptions().orElse(null); + } + try (ApiResponse response = - this.apiClient.request("post", path, JsonSerializable.toJsonString(body))) { + this.apiClient.request("post", path, JsonSerializable.toJsonString(body), httpOptions)) { HttpEntity entity = response.getEntity(); String responseString; try { @@ -4319,8 +4340,13 @@ private EditImageResponse privateEditImage( // TODO: Remove the hack that removes config. body.remove("config"); + HttpOptions httpOptions = null; + if (config != null) { + httpOptions = config.httpOptions().orElse(null); + } + try (ApiResponse response = - this.apiClient.request("post", path, JsonSerializable.toJsonString(body))) { + this.apiClient.request("post", path, JsonSerializable.toJsonString(body), httpOptions)) { HttpEntity entity = response.getEntity(); String responseString; try { @@ -4374,8 +4400,13 @@ private UpscaleImageResponse privateUpscaleImage( // TODO: Remove the hack that removes config. body.remove("config"); + HttpOptions httpOptions = null; + if (config != null) { + httpOptions = config.httpOptions().orElse(null); + } + try (ApiResponse response = - this.apiClient.request("post", path, JsonSerializable.toJsonString(body))) { + this.apiClient.request("post", path, JsonSerializable.toJsonString(body), httpOptions)) { HttpEntity entity = response.getEntity(); String responseString; try { @@ -4426,8 +4457,13 @@ public CountTokensResponse countTokens( // TODO: Remove the hack that removes config. body.remove("config"); + HttpOptions httpOptions = null; + if (config != null) { + httpOptions = config.httpOptions().orElse(null); + } + try (ApiResponse response = - this.apiClient.request("post", path, JsonSerializable.toJsonString(body))) { + this.apiClient.request("post", path, JsonSerializable.toJsonString(body), httpOptions)) { HttpEntity entity = response.getEntity(); String responseString; try { @@ -4477,8 +4513,13 @@ public ComputeTokensResponse computeTokens( // TODO: Remove the hack that removes config. body.remove("config"); + HttpOptions httpOptions = null; + if (config != null) { + httpOptions = config.httpOptions().orElse(null); + } + try (ApiResponse response = - this.apiClient.request("post", path, JsonSerializable.toJsonString(body))) { + this.apiClient.request("post", path, JsonSerializable.toJsonString(body), httpOptions)) { HttpEntity entity = response.getEntity(); String responseString; try { @@ -4545,8 +4586,13 @@ public GenerateVideosOperation generateVideos( // TODO: Remove the hack that removes config. body.remove("config"); + HttpOptions httpOptions = null; + if (config != null) { + httpOptions = config.httpOptions().orElse(null); + } + try (ApiResponse response = - this.apiClient.request("post", path, JsonSerializable.toJsonString(body))) { + this.apiClient.request("post", path, JsonSerializable.toJsonString(body), httpOptions)) { HttpEntity entity = response.getEntity(); String responseString; try { diff --git a/src/main/java/com/google/genai/Operations.java b/src/main/java/com/google/genai/Operations.java index 9cdaef0ad04..e739eaf9311 100644 --- a/src/main/java/com/google/genai/Operations.java +++ b/src/main/java/com/google/genai/Operations.java @@ -28,6 +28,7 @@ import com.google.genai.types.GenerateVideosOperation; import com.google.genai.types.GetOperationConfig; import com.google.genai.types.GetOperationParameters; +import com.google.genai.types.HttpOptions; import java.io.IOException; import org.apache.http.HttpEntity; import org.apache.http.util.EntityUtils; @@ -402,8 +403,13 @@ private GenerateVideosOperation privateGetVideosOperation( // TODO: Remove the hack that removes config. body.remove("config"); + HttpOptions httpOptions = null; + if (config != null) { + httpOptions = config.httpOptions().orElse(null); + } + try (ApiResponse response = - this.apiClient.request("get", path, JsonSerializable.toJsonString(body))) { + this.apiClient.request("get", path, JsonSerializable.toJsonString(body), httpOptions)) { HttpEntity entity = response.getEntity(); String responseString; try { @@ -454,8 +460,13 @@ private GenerateVideosOperation privateFetchPredictVideosOperation( // TODO: Remove the hack that removes config. body.remove("config"); + HttpOptions httpOptions = null; + if (config != null) { + httpOptions = config.httpOptions().orElse(null); + } + try (ApiResponse response = - this.apiClient.request("post", path, JsonSerializable.toJsonString(body))) { + this.apiClient.request("post", path, JsonSerializable.toJsonString(body), httpOptions)) { HttpEntity entity = response.getEntity(); String responseString; try { diff --git a/src/main/java/com/google/genai/ReplayApiClient.java b/src/main/java/com/google/genai/ReplayApiClient.java index db8b39dbb2b..bb5ddee8151 100644 --- a/src/main/java/com/google/genai/ReplayApiClient.java +++ b/src/main/java/com/google/genai/ReplayApiClient.java @@ -121,7 +121,8 @@ void initializeReplaySession(String replayId) { /** Sends a Http Post request given the path and request json string. */ @SuppressWarnings("unchecked") @Override - public ApiResponse request(String httpMethod, String path, String requestJson) { + public ApiResponse request( + String httpMethod, String path, String requestJson, HttpOptions httpOptions) { if (this.clientMode.equals("replay") || this.clientMode.equals("auto")) { System.out.println(" === Using replay for ID: " + this.replayId); List interactions = Arrays.asList(this.replaySession.get("interactions")); diff --git a/src/main/java/com/google/genai/types/ComputeTokensConfig.java b/src/main/java/com/google/genai/types/ComputeTokensConfig.java index 8f9aebf57f6..9b6826bd4ad 100644 --- a/src/main/java/com/google/genai/types/ComputeTokensConfig.java +++ b/src/main/java/com/google/genai/types/ComputeTokensConfig.java @@ -19,14 +19,20 @@ package com.google.genai.types; import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.google.auto.value.AutoValue; import com.google.genai.JsonSerializable; +import java.util.Optional; /** Optional parameters for computing tokens. */ @AutoValue @JsonDeserialize(builder = ComputeTokensConfig.Builder.class) public abstract class ComputeTokensConfig extends JsonSerializable { + /** Used to override HTTP request options. */ + @JsonProperty("httpOptions") + public abstract Optional httpOptions(); + /** Instantiates a builder for ComputeTokensConfig. */ public static Builder builder() { return new AutoValue_ComputeTokensConfig.Builder(); @@ -44,6 +50,9 @@ private static Builder create() { return new AutoValue_ComputeTokensConfig.Builder(); } + @JsonProperty("httpOptions") + public abstract Builder httpOptions(HttpOptions httpOptions); + public abstract ComputeTokensConfig build(); } diff --git a/src/main/java/com/google/genai/types/CountTokensConfig.java b/src/main/java/com/google/genai/types/CountTokensConfig.java index 671780f25d2..0dd8fe93f6e 100644 --- a/src/main/java/com/google/genai/types/CountTokensConfig.java +++ b/src/main/java/com/google/genai/types/CountTokensConfig.java @@ -30,6 +30,10 @@ @AutoValue @JsonDeserialize(builder = CountTokensConfig.Builder.class) public abstract class CountTokensConfig extends JsonSerializable { + /** Used to override HTTP request options. */ + @JsonProperty("httpOptions") + public abstract Optional httpOptions(); + /** Instructions for the model to steer it toward better performance. */ @JsonProperty("systemInstruction") public abstract Optional systemInstruction(); @@ -65,6 +69,9 @@ private static Builder create() { return new AutoValue_CountTokensConfig.Builder(); } + @JsonProperty("httpOptions") + public abstract Builder httpOptions(HttpOptions httpOptions); + @JsonProperty("systemInstruction") public abstract Builder systemInstruction(Content systemInstruction); diff --git a/src/main/java/com/google/genai/types/DownloadFileConfig.java b/src/main/java/com/google/genai/types/DownloadFileConfig.java new file mode 100644 index 00000000000..8ecf6384d16 --- /dev/null +++ b/src/main/java/com/google/genai/types/DownloadFileConfig.java @@ -0,0 +1,63 @@ +/* + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// Auto-generated code. Do not edit. + +package com.google.genai.types; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.google.auto.value.AutoValue; +import com.google.genai.JsonSerializable; +import java.util.Optional; + +/** Used to override the default configuration. */ +@AutoValue +@JsonDeserialize(builder = DownloadFileConfig.Builder.class) +public abstract class DownloadFileConfig extends JsonSerializable { + /** Used to override HTTP request options. */ + @JsonProperty("httpOptions") + public abstract Optional httpOptions(); + + /** Instantiates a builder for DownloadFileConfig. */ + public static Builder builder() { + return new AutoValue_DownloadFileConfig.Builder(); + } + + /** Creates a builder with the same values as this instance. */ + public abstract Builder toBuilder(); + + /** Builder for DownloadFileConfig. */ + @AutoValue.Builder + public abstract static class Builder { + /** For internal usage. Please use `DownloadFileConfig.builder()` for instantiation. */ + @JsonCreator + private static Builder create() { + return new AutoValue_DownloadFileConfig.Builder(); + } + + @JsonProperty("httpOptions") + public abstract Builder httpOptions(HttpOptions httpOptions); + + public abstract DownloadFileConfig build(); + } + + /** Deserializes a JSON string to a DownloadFileConfig object. */ + public static DownloadFileConfig fromJson(String jsonString) { + return JsonSerializable.fromJsonString(jsonString, DownloadFileConfig.class); + } +} diff --git a/src/main/java/com/google/genai/types/EditImageConfig.java b/src/main/java/com/google/genai/types/EditImageConfig.java index 3085aba9de7..d468ab17601 100644 --- a/src/main/java/com/google/genai/types/EditImageConfig.java +++ b/src/main/java/com/google/genai/types/EditImageConfig.java @@ -30,6 +30,10 @@ @AutoValue @JsonDeserialize(builder = EditImageConfig.Builder.class) public abstract class EditImageConfig extends JsonSerializable { + /** Used to override HTTP request options. */ + @JsonProperty("httpOptions") + public abstract Optional httpOptions(); + /** Cloud Storage URI used to store the generated images. */ @JsonProperty("outputGcsUri") public abstract Optional outputGcsUri(); @@ -121,6 +125,9 @@ private static Builder create() { return new AutoValue_EditImageConfig.Builder(); } + @JsonProperty("httpOptions") + public abstract Builder httpOptions(HttpOptions httpOptions); + @JsonProperty("outputGcsUri") public abstract Builder outputGcsUri(String outputGcsUri); diff --git a/src/main/java/com/google/genai/types/EmbedContentConfig.java b/src/main/java/com/google/genai/types/EmbedContentConfig.java index eec61c958b1..76d8fab5fbb 100644 --- a/src/main/java/com/google/genai/types/EmbedContentConfig.java +++ b/src/main/java/com/google/genai/types/EmbedContentConfig.java @@ -29,6 +29,10 @@ @AutoValue @JsonDeserialize(builder = EmbedContentConfig.Builder.class) public abstract class EmbedContentConfig extends JsonSerializable { + /** Used to override HTTP request options. */ + @JsonProperty("httpOptions") + public abstract Optional httpOptions(); + /** Type of task for which the embedding will be used. */ @JsonProperty("taskType") public abstract Optional taskType(); @@ -74,6 +78,9 @@ private static Builder create() { return new AutoValue_EmbedContentConfig.Builder(); } + @JsonProperty("httpOptions") + public abstract Builder httpOptions(HttpOptions httpOptions); + @JsonProperty("taskType") public abstract Builder taskType(String taskType); diff --git a/src/main/java/com/google/genai/types/FetchPredictOperationConfig.java b/src/main/java/com/google/genai/types/FetchPredictOperationConfig.java index 10b9dafb8c9..66c1f5235c3 100644 --- a/src/main/java/com/google/genai/types/FetchPredictOperationConfig.java +++ b/src/main/java/com/google/genai/types/FetchPredictOperationConfig.java @@ -19,14 +19,20 @@ package com.google.genai.types; import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.google.auto.value.AutoValue; import com.google.genai.JsonSerializable; +import java.util.Optional; /** None */ @AutoValue @JsonDeserialize(builder = FetchPredictOperationConfig.Builder.class) public abstract class FetchPredictOperationConfig extends JsonSerializable { + /** Used to override HTTP request options. */ + @JsonProperty("httpOptions") + public abstract Optional httpOptions(); + /** Instantiates a builder for FetchPredictOperationConfig. */ public static Builder builder() { return new AutoValue_FetchPredictOperationConfig.Builder(); @@ -44,6 +50,9 @@ private static Builder create() { return new AutoValue_FetchPredictOperationConfig.Builder(); } + @JsonProperty("httpOptions") + public abstract Builder httpOptions(HttpOptions httpOptions); + public abstract FetchPredictOperationConfig build(); } diff --git a/src/main/java/com/google/genai/types/GenerateContentConfig.java b/src/main/java/com/google/genai/types/GenerateContentConfig.java index 89fba741bab..d5e96dfac56 100644 --- a/src/main/java/com/google/genai/types/GenerateContentConfig.java +++ b/src/main/java/com/google/genai/types/GenerateContentConfig.java @@ -37,6 +37,10 @@ @AutoValue @JsonDeserialize(builder = GenerateContentConfig.Builder.class) public abstract class GenerateContentConfig extends JsonSerializable { + /** Used to override HTTP request options. */ + @JsonProperty("httpOptions") + public abstract Optional httpOptions(); + /** * Instructions for the model to steer it toward better performance. For example, "Answer as * concisely as possible" or "Don't use technical terms in your response". @@ -205,6 +209,9 @@ private static Builder create() { return new AutoValue_GenerateContentConfig.Builder(); } + @JsonProperty("httpOptions") + public abstract Builder httpOptions(HttpOptions httpOptions); + @JsonProperty("systemInstruction") public abstract Builder systemInstruction(Content systemInstruction); diff --git a/src/main/java/com/google/genai/types/GenerateImagesConfig.java b/src/main/java/com/google/genai/types/GenerateImagesConfig.java index 7eddb35bc71..849a60e4c12 100644 --- a/src/main/java/com/google/genai/types/GenerateImagesConfig.java +++ b/src/main/java/com/google/genai/types/GenerateImagesConfig.java @@ -30,6 +30,10 @@ @AutoValue @JsonDeserialize(builder = GenerateImagesConfig.Builder.class) public abstract class GenerateImagesConfig extends JsonSerializable { + /** Used to override HTTP request options. */ + @JsonProperty("httpOptions") + public abstract Optional httpOptions(); + /** Cloud Storage URI used to store the generated images. */ @JsonProperty("outputGcsUri") public abstract Optional outputGcsUri(); @@ -118,6 +122,9 @@ private static Builder create() { return new AutoValue_GenerateImagesConfig.Builder(); } + @JsonProperty("httpOptions") + public abstract Builder httpOptions(HttpOptions httpOptions); + @JsonProperty("outputGcsUri") public abstract Builder outputGcsUri(String outputGcsUri); diff --git a/src/main/java/com/google/genai/types/GenerateVideosConfig.java b/src/main/java/com/google/genai/types/GenerateVideosConfig.java index afb4f9c4933..a536fe3fcae 100644 --- a/src/main/java/com/google/genai/types/GenerateVideosConfig.java +++ b/src/main/java/com/google/genai/types/GenerateVideosConfig.java @@ -29,6 +29,10 @@ @AutoValue @JsonDeserialize(builder = GenerateVideosConfig.Builder.class) public abstract class GenerateVideosConfig extends JsonSerializable { + /** Used to override HTTP request options. */ + @JsonProperty("httpOptions") + public abstract Optional httpOptions(); + /** Number of output videos. */ @JsonProperty("numberOfVideos") public abstract Optional numberOfVideos(); @@ -102,6 +106,9 @@ private static Builder create() { return new AutoValue_GenerateVideosConfig.Builder(); } + @JsonProperty("httpOptions") + public abstract Builder httpOptions(HttpOptions httpOptions); + @JsonProperty("numberOfVideos") public abstract Builder numberOfVideos(Integer numberOfVideos); diff --git a/src/main/java/com/google/genai/types/GetOperationConfig.java b/src/main/java/com/google/genai/types/GetOperationConfig.java index 21a24b75e33..aa62b6e429c 100644 --- a/src/main/java/com/google/genai/types/GetOperationConfig.java +++ b/src/main/java/com/google/genai/types/GetOperationConfig.java @@ -19,14 +19,20 @@ package com.google.genai.types; import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.google.auto.value.AutoValue; import com.google.genai.JsonSerializable; +import java.util.Optional; /** None */ @AutoValue @JsonDeserialize(builder = GetOperationConfig.Builder.class) public abstract class GetOperationConfig extends JsonSerializable { + /** Used to override HTTP request options. */ + @JsonProperty("httpOptions") + public abstract Optional httpOptions(); + /** Instantiates a builder for GetOperationConfig. */ public static Builder builder() { return new AutoValue_GetOperationConfig.Builder(); @@ -44,6 +50,9 @@ private static Builder create() { return new AutoValue_GetOperationConfig.Builder(); } + @JsonProperty("httpOptions") + public abstract Builder httpOptions(HttpOptions httpOptions); + public abstract GetOperationConfig build(); } diff --git a/src/main/java/com/google/genai/types/LiveConnectConfig.java b/src/main/java/com/google/genai/types/LiveConnectConfig.java index 4cd49b2b001..7542f0afdbd 100644 --- a/src/main/java/com/google/genai/types/LiveConnectConfig.java +++ b/src/main/java/com/google/genai/types/LiveConnectConfig.java @@ -34,6 +34,10 @@ @AutoValue @JsonDeserialize(builder = LiveConnectConfig.Builder.class) public abstract class LiveConnectConfig extends JsonSerializable { + /** Used to override HTTP request options. */ + @JsonProperty("httpOptions") + public abstract Optional httpOptions(); + /** * The requested modalities of the response. Represents the set of modalities that the model can * return. Defaults to AUDIO if not specified. @@ -148,6 +152,9 @@ private static Builder create() { return new AutoValue_LiveConnectConfig.Builder(); } + @JsonProperty("httpOptions") + public abstract Builder httpOptions(HttpOptions httpOptions); + @JsonProperty("responseModalities") public abstract Builder responseModalities(List responseModalities); diff --git a/src/main/java/com/google/genai/types/UploadFileConfig.java b/src/main/java/com/google/genai/types/UploadFileConfig.java index 20a68aa4cca..2a5b67eb187 100644 --- a/src/main/java/com/google/genai/types/UploadFileConfig.java +++ b/src/main/java/com/google/genai/types/UploadFileConfig.java @@ -29,6 +29,10 @@ @AutoValue @JsonDeserialize(builder = UploadFileConfig.Builder.class) public abstract class UploadFileConfig extends JsonSerializable { + /** Used to override HTTP request options. */ + @JsonProperty("httpOptions") + public abstract Optional httpOptions(); + /** * The name of the file in the destination (e.g., 'files/sample-image'. If not provided one will * be generated. @@ -64,6 +68,9 @@ private static Builder create() { return new AutoValue_UploadFileConfig.Builder(); } + @JsonProperty("httpOptions") + public abstract Builder httpOptions(HttpOptions httpOptions); + @JsonProperty("name") public abstract Builder name(String name); diff --git a/src/main/java/com/google/genai/types/UpscaleImageAPIConfig.java b/src/main/java/com/google/genai/types/UpscaleImageAPIConfig.java index eabd4671f28..d6e6cc587db 100644 --- a/src/main/java/com/google/genai/types/UpscaleImageAPIConfig.java +++ b/src/main/java/com/google/genai/types/UpscaleImageAPIConfig.java @@ -36,6 +36,10 @@ @InternalApi @JsonDeserialize(builder = UpscaleImageAPIConfig.Builder.class) public abstract class UpscaleImageAPIConfig extends JsonSerializable { + /** Used to override HTTP request options. */ + @JsonProperty("httpOptions") + public abstract Optional httpOptions(); + /** Whether to include a reason for filtered-out images in the response. */ @JsonProperty("includeRaiReason") public abstract Optional includeRaiReason(); @@ -73,6 +77,9 @@ private static Builder create() { return new AutoValue_UpscaleImageAPIConfig.Builder(); } + @JsonProperty("httpOptions") + public abstract Builder httpOptions(HttpOptions httpOptions); + @JsonProperty("includeRaiReason") public abstract Builder includeRaiReason(boolean includeRaiReason); diff --git a/src/main/java/com/google/genai/types/UpscaleImageConfig.java b/src/main/java/com/google/genai/types/UpscaleImageConfig.java index ec40ffa5efe..c7e5396f8f6 100644 --- a/src/main/java/com/google/genai/types/UpscaleImageConfig.java +++ b/src/main/java/com/google/genai/types/UpscaleImageConfig.java @@ -34,6 +34,10 @@ @AutoValue @JsonDeserialize(builder = UpscaleImageConfig.Builder.class) public abstract class UpscaleImageConfig extends JsonSerializable { + /** Used to override HTTP request options. */ + @JsonProperty("httpOptions") + public abstract Optional httpOptions(); + /** Whether to include a reason for filtered-out images in the response. */ @JsonProperty("includeRaiReason") public abstract Optional includeRaiReason(); @@ -63,6 +67,9 @@ private static Builder create() { return new AutoValue_UpscaleImageConfig.Builder(); } + @JsonProperty("httpOptions") + public abstract Builder httpOptions(HttpOptions httpOptions); + @JsonProperty("includeRaiReason") public abstract Builder includeRaiReason(boolean includeRaiReason); diff --git a/src/test/java/com/google/genai/ChatTest.java b/src/test/java/com/google/genai/ChatTest.java index c995b2f5511..020ff1ed1f4 100644 --- a/src/test/java/com/google/genai/ChatTest.java +++ b/src/test/java/com/google/genai/ChatTest.java @@ -18,6 +18,7 @@ import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.when; @@ -142,7 +143,8 @@ public class ChatTest { void setUp() { mockedClient = Mockito.mock(ApiClient.class); mockedResponse = Mockito.mock(ApiResponse.class); - when(mockedClient.request(anyString(), anyString(), anyString())).thenReturn(mockedResponse); + when(mockedClient.request(anyString(), anyString(), anyString(), any())) + .thenReturn(mockedResponse); mockedEntity = Mockito.mock(HttpEntity.class); client = Client.builder().build(); @@ -433,7 +435,7 @@ public void testIterateOverResponseStream() throws Exception { when(mockedEntity1.getContent()).thenReturn(inputStream1); when(mockedEntity2.getContent()).thenReturn(inputStream2); when(mockedEntity3.getContent()).thenReturn(inputStream3); - when(mockedClient.request(anyString(), anyString(), anyString())) + when(mockedClient.request(anyString(), anyString(), anyString(), any())) .thenReturn(mockedResponse1, mockedResponse2, mockedResponse3); assert chatSession.getHistory(false).size() == 0; @@ -519,7 +521,7 @@ public void testThrowsIfStreamResponseIsNotConsumed() throws Exception { when(mockedResponse2.getEntity()).thenReturn(mockedEntity2); when(mockedEntity1.getContent()).thenReturn(inputStream1); when(mockedEntity2.getContent()).thenReturn(inputStream2); - when(mockedClient.request(anyString(), anyString(), anyString())) + when(mockedClient.request(anyString(), anyString(), anyString(), any())) .thenReturn(mockedResponse1, mockedResponse2); assert chatSession.getHistory(false).size() == 0; diff --git a/src/test/java/com/google/genai/DefaultValuesTest.java b/src/test/java/com/google/genai/DefaultValuesTest.java index f52827fc737..7e5a0e7a980 100644 --- a/src/test/java/com/google/genai/DefaultValuesTest.java +++ b/src/test/java/com/google/genai/DefaultValuesTest.java @@ -17,6 +17,7 @@ package com.google.genai; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -37,7 +38,8 @@ public void testDefaultValues() throws Exception { // Mocks and test setup. ApiClient httpClientSpy = Mockito.spy(Mockito.mock(ApiClient.class)); ApiResponse mockedResponse = Mockito.mock(ApiResponse.class); - when(httpClientSpy.request(anyString(), anyString(), anyString())).thenReturn(mockedResponse); + when(httpClientSpy.request(anyString(), anyString(), anyString(), any())) + .thenReturn(mockedResponse); HttpEntity mockedEntity = Mockito.mock(HttpEntity.class); GenerateContentResponse returnResponse = GenerateContentResponse.builder().build(); StringEntity content = new StringEntity(returnResponse.toJson()); @@ -55,7 +57,8 @@ public void testDefaultValues() throws Exception { ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(String.class); verify(httpClientSpy) - .request(argumentCaptor.capture(), argumentCaptor.capture(), argumentCaptor.capture()); + .request( + argumentCaptor.capture(), argumentCaptor.capture(), argumentCaptor.capture(), any()); GenerateContentConfig spiedConfig = GenerateContentConfig.fromJson(argumentCaptor.getAllValues().get(2)); diff --git a/src/test/java/com/google/genai/ForwardCompatibilityTest.java b/src/test/java/com/google/genai/ForwardCompatibilityTest.java index 95887f57fb0..329897349fd 100644 --- a/src/test/java/com/google/genai/ForwardCompatibilityTest.java +++ b/src/test/java/com/google/genai/ForwardCompatibilityTest.java @@ -18,6 +18,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -57,7 +58,8 @@ public class ForwardCompatibilityTest { void setUp() { mockedClient = Mockito.mock(ApiClient.class); mockedResponse = Mockito.mock(ApiResponse.class); - when(mockedClient.request(anyString(), anyString(), anyString())).thenReturn(mockedResponse); + when(mockedClient.request(anyString(), anyString(), anyString(), any())) + .thenReturn(mockedResponse); mockedEntity = Mockito.mock(HttpEntity.class); returnResponse = GenerateContentResponse.builder().build(); @@ -69,13 +71,6 @@ void setUp() { @Test public void testForwardCompatibility() throws Exception { // Mocks and test setup. - ApiClient mockedClient = Mockito.mock(ApiClient.class); - ApiResponse mockedResponse = Mockito.mock(ApiResponse.class); - when(mockedClient.request(anyString(), anyString(), anyString())).thenReturn(mockedResponse); - HttpEntity mockedEntity = Mockito.mock(HttpEntity.class); - GenerateContentResponse returnResponse = GenerateContentResponse.builder().build(); - - ObjectMapper objectMapper = new ObjectMapper(); JsonNode rootNode = objectMapper.readTree(returnResponse.toJson()); ObjectNode objectNode = (ObjectNode) rootNode; objectNode.put("unknownFieldToTestForwardCompatibility", "Hello World!"); @@ -84,7 +79,6 @@ public void testForwardCompatibility() throws Exception { StringEntity content = new StringEntity(jsonString); when(mockedResponse.getEntity()).thenReturn(content); - Client client = Client.builder().build(); // Make the apiClient field public so that it can be spied on in the tests. This is a // workaround for the fact that the ApiClient is a final class and cannot be spied on directly. Field apiClientField = Models.class.getDeclaredField("apiClient"); @@ -251,7 +245,7 @@ public void testEnumSendingUnknownSafetysettings() throws Exception { client.models.generateContent("gemini-2.0-flash-exp", "What is your name?", config); ArgumentCaptor stringCaptor = ArgumentCaptor.forClass(String.class); - verify(mockedClient).request(anyString(), anyString(), stringCaptor.capture()); + verify(mockedClient).request(anyString(), anyString(), stringCaptor.capture(), any()); assertEquals( "{\"contents\":[{\"parts\":[{\"text\":\"What is your name?\"}],\"role\":\"user\"}]," + "\"safetySettings\":[{\"threshold\":\"NEW_UNKNOWN_VALUE\"}],\"generationConfig\":{}}", diff --git a/src/test/java/com/google/genai/HttpApiClientTest.java b/src/test/java/com/google/genai/HttpApiClientTest.java index c36d69ff26c..7a9ce4ef876 100644 --- a/src/test/java/com/google/genai/HttpApiClientTest.java +++ b/src/test/java/com/google/genai/HttpApiClientTest.java @@ -67,6 +67,12 @@ public class HttpApiClientTest { HttpApiClient.defaultHttpOptions(false, Optional.empty()); private static final HttpOptions defaultHttpOptionsVertex = HttpApiClient.defaultHttpOptions(true, Optional.of(LOCATION)); + private static final HttpOptions REQUEST_HTTP_OPTIONS = + HttpOptions.builder() + .baseUrl("test-url") + .apiVersion("test-api-version") + .headers(ImmutableMap.of("test", "header")) + .build(); private static final String TEST_PATH = "test-path"; private static final String TEST_REQUEST_JSON = "{\"test\": \"request-json\"}"; @@ -94,7 +100,7 @@ public void testRequestPostMethodWithVertexAI() throws Exception { setMockClient(client); // Act - client.request("POST", TEST_PATH, TEST_REQUEST_JSON); + client.request("POST", TEST_PATH, TEST_REQUEST_JSON, null); // Assert ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(HttpRequestBase.class); @@ -127,7 +133,7 @@ public void testRequestGetMethodWithMldev() throws Exception { setMockClient(client); // Act - client.request("GET", TEST_PATH, null); + client.request("GET", TEST_PATH, null, null); // Assert ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(HttpRequestBase.class); @@ -152,7 +158,7 @@ public void testRequestDeleteMethodWithMldev() throws Exception { setMockClient(client); // Act - client.request("DELETE", TEST_PATH, null); + client.request("DELETE", TEST_PATH, null, null); // Assert ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(HttpRequestBase.class); @@ -170,6 +176,29 @@ public void testRequestDeleteMethodWithMldev() throws Exception { assertNull(capturedRequest.getFirstHeader("Authorization")); } + @Test + public void testRequestWithHttpOptions() throws Exception { + // Arrange + HttpApiClient client = new HttpApiClient(Optional.of(API_KEY), Optional.empty()); + setMockClient(client); + + // Act + client.request("POST", TEST_PATH, TEST_REQUEST_JSON, REQUEST_HTTP_OPTIONS); + + // Assert + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(HttpRequestBase.class); + verify(mockHttpClient).execute(requestCaptor.capture()); + HttpRequestBase capturedRequest = requestCaptor.getValue(); + + assertTrue(capturedRequest instanceof HttpPost); + assertEquals("POST", capturedRequest.getMethod()); + // The request URL is set by the request-level http options. + assertEquals("test-url/test-api-version/" + TEST_PATH, capturedRequest.getURI().toString()); + Header authHeader = capturedRequest.getFirstHeader("Authorization"); + // Request should have the header set by the request-level http options. + assertEquals("header", capturedRequest.getFirstHeader("test").getValue()); + } + @Test public void testInitHttpClientMldev() throws Exception { HttpOptions httpOptions =