diff --git a/src/main/java/com/google/genai/ApiClient.java b/src/main/java/com/google/genai/ApiClient.java index 75760524bba..b42d0254c7a 100644 --- a/src/main/java/com/google/genai/ApiClient.java +++ b/src/main/java/com/google/genai/ApiClient.java @@ -60,6 +60,7 @@ public abstract class ApiClient implements AutoCloseable { // {x-version-update-start:google-genai:released} private static final String SDK_VERSION = "1.80.0"; // {x-version-update-end:google-genai:released} + public static final String GOOGLE_AI_API_DEFAULT_VERSION = "v1"; private static final Logger logger = Logger.getLogger(ApiClient.class.getName()); private static final ImmutableSet METHODS_WITH_BODY = @@ -769,8 +770,10 @@ static HttpOptions defaultHttpOptions( HttpOptions.builder().headers(defaultHeaders.build()); ImmutableMap defaultEnvironmentVariables = defaultEnvironmentVariables(); + String envApiVersion = defaultEnvironmentVariables.get("apiVersion"); if (vertexAI) { - defaultHttpOptionsBuilder.apiVersion("v1beta1"); + defaultHttpOptionsBuilder.apiVersion( + envApiVersion != null && !envApiVersion.isEmpty() ? envApiVersion : "v1beta1"); String defaultBaseUrl = vertexBaseUrl.orElseGet(() -> defaultEnvironmentVariables.get("vertexBaseUrl")); if (defaultBaseUrl != null) { @@ -791,7 +794,10 @@ static HttpOptions defaultHttpOptions( String.format("https://%s-aiplatform.googleapis.com", location.get())); } } else { - defaultHttpOptionsBuilder.apiVersion("v1beta"); + defaultHttpOptionsBuilder.apiVersion( + envApiVersion != null && !envApiVersion.isEmpty() + ? envApiVersion + : GOOGLE_AI_API_DEFAULT_VERSION); String defaultBaseUrl = geminiBaseUrl.orElseGet(() -> defaultEnvironmentVariables.get("geminiBaseUrl")); if (defaultBaseUrl != null) { @@ -909,6 +915,10 @@ public static ImmutableMap defaultEnvironmentVariables() { if (value != null && !value.isEmpty()) { mapBuilder.put("vertexBaseUrl", value); } + value = System.getenv("GOOGLE_GENAI_API_VERSION"); + if (value != null && !value.isEmpty()) { + mapBuilder.put("apiVersion", value); + } return mapBuilder.buildOrThrow(); } diff --git a/src/main/java/com/google/genai/AsyncLive.java b/src/main/java/com/google/genai/AsyncLive.java index e3eea728fff..58413197b78 100644 --- a/src/main/java/com/google/genai/AsyncLive.java +++ b/src/main/java/com/google/genai/AsyncLive.java @@ -113,7 +113,7 @@ private URI getWebSocketUri() { logger.warning( "Warning: Ephemeral token support is experimental and may change in future" + " versions."); - if (!apiClient.httpOptions().apiVersion().orElse("v1beta").equals("v1alpha")) { + if (!apiClient.httpOptions().apiVersion().orElse(ApiClient.GOOGLE_AI_API_DEFAULT_VERSION).equals("v1alpha")) { logger.warning( "Warning: The SDK's ephemeral token support is in v1alpha only. Please use client" + " = Client.builder().httpOptions(HttpOptions.builder().apiVersion(\"v1alpha\").build()).build()" @@ -126,7 +126,7 @@ private URI getWebSocketUri() { return new URI( String.format( "%s/ws/google.ai.generativelanguage.%s.GenerativeService.%s", - wsBaseUrl, apiClient.httpOptions().apiVersion().orElse("v1beta"), method)); + wsBaseUrl, apiClient.httpOptions().apiVersion().orElse(ApiClient.GOOGLE_AI_API_DEFAULT_VERSION), method)); } else { return new URI( String.format( diff --git a/src/main/java/com/google/genai/Client.java b/src/main/java/com/google/genai/Client.java index 40845ef4e75..accbc5388c6 100644 --- a/src/main/java/com/google/genai/Client.java +++ b/src/main/java/com/google/genai/Client.java @@ -143,6 +143,7 @@ public Async(ApiClient apiClient) { /** Builder for {@link Client}. */ public static class Builder { private Optional apiKey = Optional.empty(); + private Optional apiVersion = Optional.empty(); private Optional project = Optional.empty(); private Optional location = Optional.empty(); private Optional credentials = Optional.empty(); @@ -155,13 +156,25 @@ public static class Builder { /** Builds the {@link Client} instance. */ public Client build() { + Optional resolvedHttpOptions = this.httpOptions; + if (this.apiVersion.isPresent()) { + if (this.httpOptions.isPresent() && this.httpOptions.get().apiVersion().isPresent()) { + throw new IllegalArgumentException( + "apiVersion is set in both Client.Builder and HttpOptions. Please specify apiVersion" + + " in only one place."); + } + HttpOptions currentOptions = + this.httpOptions.orElseGet(() -> HttpOptions.builder().build()); + resolvedHttpOptions = + Optional.of(currentOptions.toBuilder().apiVersion(this.apiVersion.get()).build()); + } return new Client( apiKey, project, location, credentials, - httpOptions, + resolvedHttpOptions, clientOptions, vertexAI, enterprise, @@ -169,6 +182,14 @@ public Client build() { asyncRetryScheduler); } + /** Sets the API version for the client (e.g. "v1" or "v1beta"). */ + @CanIgnoreReturnValue + public Builder apiVersion(String apiVersion) { + checkNotNull(apiVersion, "apiVersion cannot be null"); + this.apiVersion = Optional.of(apiVersion); + return this; + } + /** Sets the API key for Gemini API. */ @CanIgnoreReturnValue public Builder apiKey(String apiKey) { diff --git a/src/main/java/com/google/genai/gaos/Files.java b/src/main/java/com/google/genai/gaos/Files.java index 878472a8611..b78118aee5c 100644 --- a/src/main/java/com/google/genai/gaos/Files.java +++ b/src/main/java/com/google/genai/gaos/Files.java @@ -452,7 +452,7 @@ String buildDownloadUrl(String environment, String path, Options options) { String baseUrl = this.sdkConfiguration.serverUrl(); String apiVersion = (String) this.sdkConfiguration.globals.getParam("pathParam", "api_version") .map(Object::toString) - .orElse(SDKConfiguration.OPENAPI_DOC_VERSION); + .orElse(com.google.genai.ApiClient.GOOGLE_AI_API_DEFAULT_VERSION); StringBuilder urlBuilder = new StringBuilder(); if (baseUrl.endsWith("/")) { urlBuilder.append(baseUrl.substring(0, baseUrl.length() - 1)); diff --git a/src/test/java/com/google/genai/ClientTest.java b/src/test/java/com/google/genai/ClientTest.java index 457105b64ac..b266c2d8be3 100644 --- a/src/test/java/com/google/genai/ClientTest.java +++ b/src/test/java/com/google/genai/ClientTest.java @@ -93,6 +93,41 @@ public void testInitClientFromBuilder_withCredentialsAndHttpOptions() { assertEquals("test-url", client.baseUrl().orElse(null)); } + @Test + public void testInitClientFromBuilder_withApiVersion() { + // Act + Client client = + Client.builder() + .apiKey(API_KEY) + .vertexAI(false) + .apiVersion("v1beta") + .build(); + + // Assert + assertEquals(API_KEY, client.apiKey()); + assertFalse(client.vertexAI()); + } + + @Test + public void testInitClientFromBuilder_withApiVersionConflict_throwsException() { + // Act + IllegalArgumentException exception = + assertThrows( + IllegalArgumentException.class, + () -> + Client.builder() + .apiKey(API_KEY) + .vertexAI(false) + .apiVersion("v1beta") + .httpOptions(HttpOptions.builder().apiVersion("v1").build()) + .build()); + + // Assert + assertEquals( + "apiVersion is set in both Client.Builder and HttpOptions. Please specify apiVersion in only one place.", + exception.getMessage()); + } + @Test public void testReplayClient_mldev() { // Act @@ -374,6 +409,71 @@ void defaultIsFalse_whenNoFlagsOrEnvsAreSet() { } } + @Test + void apiVersionEnvVar_isRespected_mldev() { + try (MockedStatic mocked = + Mockito.mockStatic(ApiClient.class, Mockito.CALLS_REAL_METHODS)) { + mocked + .when(ApiClient::defaultEnvironmentVariables) + .thenReturn(ImmutableMap.of("googleApiKey", "api-key", "apiVersion", "v1alpha")); + Client client = new Client(); + assertFalse(client.vertexAI()); + assertEquals(Optional.of("v1alpha"), client.models.apiClient.httpOptions().apiVersion()); + } + } + + @Test + void apiVersionEnvVar_isRespected_vertex() { + try (MockedStatic mocked = + Mockito.mockStatic(ApiClient.class, Mockito.CALLS_REAL_METHODS); + MockedStatic mockedCredentials = + Mockito.mockStatic(GoogleCredentials.class)) { + mockedCredentials.when(GoogleCredentials::getApplicationDefault).thenReturn(CREDENTIALS); + + mocked + .when(ApiClient::defaultEnvironmentVariables) + .thenReturn( + ImmutableMap.of( + "vertexAI", + "true", + "project", + "project", + "location", + "location", + "apiVersion", + "v1")); + Client client = new Client(); + assertTrue(client.vertexAI()); + assertEquals(Optional.of("v1"), client.models.apiClient.httpOptions().apiVersion()); + } + } + + @Test + void explicitApiVersion_overridesEnvVar_withoutThrowing() { + try (MockedStatic mocked = + Mockito.mockStatic(ApiClient.class, Mockito.CALLS_REAL_METHODS)) { + mocked + .when(ApiClient::defaultEnvironmentVariables) + .thenReturn(ImmutableMap.of("googleApiKey", "api-key", "apiVersion", "v1alpha")); + + Client clientWithBuilderApiVersion = + Client.builder().apiKey(API_KEY).vertexAI(false).apiVersion("v1").build(); + assertEquals( + Optional.of("v1"), + clientWithBuilderApiVersion.models.apiClient.httpOptions().apiVersion()); + + Client clientWithHttpOptionsApiVersion = + Client.builder() + .apiKey(API_KEY) + .vertexAI(false) + .httpOptions(HttpOptions.builder().apiVersion("v1beta").build()) + .build(); + assertEquals( + Optional.of("v1beta"), + clientWithHttpOptionsApiVersion.models.apiClient.httpOptions().apiVersion()); + } + } + static class TestApiClient extends ApiClient { TestApiClient(HttpOptions httpOptions) { super(Optional.of("dummy-api-key"), Optional.of(httpOptions), Optional.empty()); diff --git a/src/test/java/com/google/genai/CredentialsLifecycleTest.java b/src/test/java/com/google/genai/CredentialsLifecycleTest.java index 4f963e22830..e37f740dd02 100644 --- a/src/test/java/com/google/genai/CredentialsLifecycleTest.java +++ b/src/test/java/com/google/genai/CredentialsLifecycleTest.java @@ -146,7 +146,9 @@ public HttpResponse send(HttpRequest request) { } } - if ("GET".equals(method) && "/v1beta/credentials".equals(path)) { + if ("GET".equals(method) + && ("/" + ApiClient.GOOGLE_AI_API_DEFAULT_VERSION + "/credentials") + .equals(path)) { return createMockResponse( authorized, 200, @@ -275,17 +277,18 @@ public void testRoutesThroughGoogleGenAiClientForBearerOAuth2AndEnvVars() throws // 9. Delete credential ai.credentials.delete("cred_bearer_123"); + String v = ApiClient.GOOGLE_AI_API_DEFAULT_VERSION; assertEquals( Arrays.asList( - "POST /v1beta/credentials", - "POST /v1beta/credentials", - "POST /v1beta/credentials", - "GET /v1beta/credentials", - "GET /v1beta/credentials/cred_bearer_123", - "PATCH /v1beta/credentials/cred_bearer_123", - "PATCH /v1beta/credentials/cred_env_123", - "PATCH /v1beta/credentials/cred_oauth_123", - "DELETE /v1beta/credentials/cred_bearer_123"), + "POST /" + v + "/credentials", + "POST /" + v + "/credentials", + "POST /" + v + "/credentials", + "GET /" + v + "/credentials", + "GET /" + v + "/credentials/cred_bearer_123", + "PATCH /" + v + "/credentials/cred_bearer_123", + "PATCH /" + v + "/credentials/cred_env_123", + "PATCH /" + v + "/credentials/cred_oauth_123", + "DELETE /" + v + "/credentials/cred_bearer_123"), captured); // Verify request bodies @@ -370,11 +373,11 @@ public void testAsyncCredentialsLifecycle() throws Exception { assertEquals( Arrays.asList( - "POST /v1beta/credentials", - "GET /v1beta/credentials", - "GET /v1beta/credentials/cred_env_123", - "PATCH /v1beta/credentials/cred_env_123", - "DELETE /v1beta/credentials/cred_env_123"), + "POST /" + ApiClient.GOOGLE_AI_API_DEFAULT_VERSION + "/credentials", + "GET /" + ApiClient.GOOGLE_AI_API_DEFAULT_VERSION + "/credentials", + "GET /" + ApiClient.GOOGLE_AI_API_DEFAULT_VERSION + "/credentials/cred_env_123", + "PATCH /" + ApiClient.GOOGLE_AI_API_DEFAULT_VERSION + "/credentials/cred_env_123", + "DELETE /" + ApiClient.GOOGLE_AI_API_DEFAULT_VERSION + "/credentials/cred_env_123"), captured); } diff --git a/src/test/java/com/google/genai/GaosClientTest.java b/src/test/java/com/google/genai/GaosClientTest.java index 8af22f9e7fe..0af8f69504e 100644 --- a/src/test/java/com/google/genai/GaosClientTest.java +++ b/src/test/java/com/google/genai/GaosClientTest.java @@ -189,7 +189,11 @@ public CompletableFuture> sendAsync(HttpRequest reques assertNotNull(req); assertEquals("POST", req.method()); - URI expectedUri = URI.create("https://generativelanguage.googleapis.com/v1beta/interactions"); + URI expectedUri = + URI.create( + "https://generativelanguage.googleapis.com/" + + ApiClient.GOOGLE_AI_API_DEFAULT_VERSION + + "/interactions"); assertEquals(expectedUri, req.uri()); assertEquals("test-api-key", req.headers().firstValue("x-goog-api-key").orElse(null)); assertTrue(req.headers().firstValue("user-agent").orElse("").contains("google-genai-sdk/")); @@ -387,7 +391,8 @@ public CompletableFuture> sendAsync(HttpRequest reques String agentId = "test-agent-id"; String webhookId = "test-webhook-id"; - String expectedUrlPrefix = "https://generativelanguage.googleapis.com/v1beta"; + String expectedUrlPrefix = + "https://generativelanguage.googleapis.com/" + ApiClient.GOOGLE_AI_API_DEFAULT_VERSION; // 1. Test Get Agent client.agents.get(agentId); @@ -434,7 +439,8 @@ public CompletableFuture> sendAsync(HttpRequest reques String credentialId = "test-credential-id"; String triggerId = "test-trigger-id"; - String expectedUrlPrefix = "https://generativelanguage.googleapis.com/v1beta"; + String expectedUrlPrefix = + "https://generativelanguage.googleapis.com/" + ApiClient.GOOGLE_AI_API_DEFAULT_VERSION; // 1. Test Get Credential client.credentials.get(credentialId); diff --git a/src/test/java/com/google/genai/HttpApiClientTest.java b/src/test/java/com/google/genai/HttpApiClientTest.java index 6b9e7adcd44..127c6659232 100644 --- a/src/test/java/com/google/genai/HttpApiClientTest.java +++ b/src/test/java/com/google/genai/HttpApiClientTest.java @@ -208,7 +208,10 @@ public void testRequestGetMethodWithMldev() throws Exception { assertEquals("GET", capturedRequest.method()); assertEquals( - "https://generativelanguage.googleapis.com/v1beta/" + TEST_PATH, + "https://generativelanguage.googleapis.com/" + + ApiClient.GOOGLE_AI_API_DEFAULT_VERSION + + "/" + + TEST_PATH, capturedRequest.url().toString()); assertNotNull(capturedRequest.header("x-goog-api-key")); assertEquals(API_KEY, capturedRequest.header("x-goog-api-key")); @@ -232,7 +235,10 @@ public void testRequestDeleteMethodWithMldev() throws Exception { assertEquals("DELETE", capturedRequest.method()); assertEquals( - "https://generativelanguage.googleapis.com/v1beta/" + TEST_PATH, + "https://generativelanguage.googleapis.com/" + + ApiClient.GOOGLE_AI_API_DEFAULT_VERSION + + "/" + + TEST_PATH, capturedRequest.url().toString()); assertNotNull(capturedRequest.header("x-goog-api-key")); assertEquals(API_KEY, capturedRequest.header("x-goog-api-key")); diff --git a/src/test/java/com/google/genai/gaos/hooks/SdkHooksTest.java b/src/test/java/com/google/genai/gaos/hooks/SdkHooksTest.java index 39455f7ab39..ea9d3d720d0 100644 --- a/src/test/java/com/google/genai/gaos/hooks/SdkHooksTest.java +++ b/src/test/java/com/google/genai/gaos/hooks/SdkHooksTest.java @@ -54,7 +54,7 @@ private Hook.BeforeRequestContext createContext(Security security) { private HttpRequest createBaseRequest() { return HttpRequest.builder() .method("POST") - .uri(URI.create("https://generativelanguage.googleapis.com/v1beta/interactions")) + .uri(URI.create("https://generativelanguage.googleapis.com/v1/interactions")) .build(); }