diff --git a/README.md b/README.md index 2234197123a..09684689788 100644 --- a/README.md +++ b/README.md @@ -129,9 +129,16 @@ Client client = new Client(); ### API Selection -By default, the SDK uses the beta API endpoints provided by Google to support -preview features in the APIs. The stable API endpoints can be selected by -setting the API version to `v1`. +By default, the SDK uses: +- the stable `v1` API endpoints for the Gemini Developer API. +- and the beta `v1beta1` API endpoints for Gemini Enterprise Agent Platform. + +A different API version (for example `v1beta` or `v1alpha`) can be selected +with `HttpOptions` or with the `GOOGLE_GENAI_API_VERSION` environment variable. +An explicitly configured version in `HttpOptions` takes precedence over the +environment variable. Setting `GOOGLE_GENAI_API_VERSION=beta` selects the beta +version of whichever backend is in use: `v1beta` for the Gemini Developer API +and `v1beta1` for Gemini Enterprise Agent Platform. To set the API version use `HttpOptions`. For example, to set the API version to `v1` for Gemini Enterprise Agent Platform: diff --git a/src/main/java/com/google/genai/ApiClient.java b/src/main/java/com/google/genai/ApiClient.java index 75760524bba..0b529c1626b 100644 --- a/src/main/java/com/google/genai/ApiClient.java +++ b/src/main/java/com/google/genai/ApiClient.java @@ -60,6 +60,8 @@ 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} + 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 +771,12 @@ 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.equals("beta") + ? "v1beta1" + : envApiVersion); String defaultBaseUrl = vertexBaseUrl.orElseGet(() -> defaultEnvironmentVariables.get("vertexBaseUrl")); if (defaultBaseUrl != null) { @@ -791,7 +797,10 @@ static HttpOptions defaultHttpOptions( String.format("https://%s-aiplatform.googleapis.com", location.get())); } } else { - defaultHttpOptionsBuilder.apiVersion("v1beta"); + defaultHttpOptionsBuilder.apiVersion( + envApiVersion == null || envApiVersion.isEmpty() + ? GOOGLE_AI_API_DEFAULT_VERSION + : envApiVersion.equals("beta") ? "v1beta" : envApiVersion); String defaultBaseUrl = geminiBaseUrl.orElseGet(() -> defaultEnvironmentVariables.get("geminiBaseUrl")); if (defaultBaseUrl != null) { @@ -909,6 +918,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..197e8ea25aa 100644 --- a/src/main/java/com/google/genai/AsyncLive.java +++ b/src/main/java/com/google/genai/AsyncLive.java @@ -113,7 +113,11 @@ 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 +130,12 @@ 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/test/java/com/google/genai/AsyncLiveTest.java b/src/test/java/com/google/genai/AsyncLiveTest.java index 2d210268ca0..1b3c1576d0b 100644 --- a/src/test/java/com/google/genai/AsyncLiveTest.java +++ b/src/test/java/com/google/genai/AsyncLiveTest.java @@ -120,6 +120,25 @@ public void testGetWebSocketUri_GoogleAiEphemeralToken() throws Exception { assertTrue(uri.toString().contains("v1alpha")); } + @Test + public void testGetWebSocketUri_GoogleAiDefaultApiVersion() throws Exception { + when(apiClient.vertexAI()).thenReturn(false); + when(apiClient.apiKey()).thenReturn("api-key"); + when(apiClient.httpOptions()) + .thenReturn( + HttpOptions.builder().baseUrl("https://generativelanguage.googleapis.com").build()); + + Method getWebSocketUri = AsyncLive.class.getDeclaredMethod("getWebSocketUri"); + getWebSocketUri.setAccessible(true); + URI uri = (URI) getWebSocketUri.invoke(asyncLive); + + assertEquals( + "wss://generativelanguage.googleapis.com/ws/google.ai.generativelanguage." + + ApiClient.GOOGLE_AI_API_DEFAULT_VERSION + + ".GenerativeService.BidiGenerateContent", + uri.toString()); + } + @Test public void testGetWebSocketHeaders_GoogleAiEphemeralToken() throws Exception { when(apiClient.vertexAI()).thenReturn(false); diff --git a/src/test/java/com/google/genai/ClientTest.java b/src/test/java/com/google/genai/ClientTest.java index 457105b64ac..c8a4a7ee5c8 100644 --- a/src/test/java/com/google/genai/ClientTest.java +++ b/src/test/java/com/google/genai/ClientTest.java @@ -374,6 +374,182 @@ void defaultIsFalse_whenNoFlagsOrEnvsAreSet() { } } + @Test + void defaultApiVersion_perBackend_whenNoEnvVar() { + try (MockedStatic mocked = + Mockito.mockStatic(ApiClient.class, Mockito.CALLS_REAL_METHODS)) { + mocked.when(ApiClient::defaultEnvironmentVariables).thenReturn(ImmutableMap.of()); + Client mldevClient = Client.builder().apiKey(API_KEY).vertexAI(false).build(); + assertEquals( + Optional.of(ApiClient.GOOGLE_AI_API_DEFAULT_VERSION), + mldevClient.models.apiClient.httpOptions().apiVersion()); + + Client vertexClient = + Client.builder() + .project(PROJECT) + .location(LOCATION) + .credentials(CREDENTIALS) + .vertexAI(true) + .build(); + assertEquals( + Optional.of("v1beta1"), vertexClient.models.apiClient.httpOptions().apiVersion()); + } + } + + @Test + void apiVersionEnvVar_emptyString_fallsBackToDefault() { + try (MockedStatic mocked = + Mockito.mockStatic(ApiClient.class, Mockito.CALLS_REAL_METHODS)) { + mocked + .when(ApiClient::defaultEnvironmentVariables) + .thenReturn(ImmutableMap.of("googleApiKey", "api-key", "apiVersion", "")); + Client client = new Client(); + assertEquals( + Optional.of(ApiClient.GOOGLE_AI_API_DEFAULT_VERSION), + client.models.apiClient.httpOptions().apiVersion()); + + Client vertexClient = + Client.builder() + .project(PROJECT) + .location(LOCATION) + .credentials(CREDENTIALS) + .vertexAI(true) + .build(); + assertEquals( + Optional.of("v1beta1"), vertexClient.models.apiClient.httpOptions().apiVersion()); + } + } + + @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 apiVersionEnvVar_appliesToMldevAndVertex() { + try (MockedStatic mocked = + Mockito.mockStatic(ApiClient.class, Mockito.CALLS_REAL_METHODS)) { + mocked + .when(ApiClient::defaultEnvironmentVariables) + .thenReturn(ImmutableMap.of("apiVersion", "v1alpha")); + + Client mldevClient = Client.builder().apiKey(API_KEY).vertexAI(false).build(); + assertEquals( + Optional.of("v1alpha"), mldevClient.models.apiClient.httpOptions().apiVersion()); + + Client vertexClient = + Client.builder() + .project(PROJECT) + .location(LOCATION) + .credentials(CREDENTIALS) + .vertexAI(true) + .build(); + assertEquals( + Optional.of("v1alpha"), vertexClient.models.apiClient.httpOptions().apiVersion()); + + Client vertexClientWithExplicitVersion = + Client.builder() + .project(PROJECT) + .location(LOCATION) + .credentials(CREDENTIALS) + .vertexAI(true) + .httpOptions(HttpOptions.builder().apiVersion("v1").build()) + .build(); + assertEquals( + Optional.of("v1"), + vertexClientWithExplicitVersion.models.apiClient.httpOptions().apiVersion()); + } + } + + @Test + void betaApiVersionEnvVar_selectsBackendBetaVersion() { + try (MockedStatic mocked = + Mockito.mockStatic(ApiClient.class, Mockito.CALLS_REAL_METHODS)) { + mocked + .when(ApiClient::defaultEnvironmentVariables) + .thenReturn(ImmutableMap.of("apiVersion", "beta")); + + Client mldevClient = Client.builder().apiKey(API_KEY).vertexAI(false).build(); + assertEquals(Optional.of("v1beta"), mldevClient.models.apiClient.httpOptions().apiVersion()); + + Client vertexClient = + Client.builder() + .project(PROJECT) + .location(LOCATION) + .credentials(CREDENTIALS) + .vertexAI(true) + .build(); + assertEquals( + Optional.of("v1beta1"), vertexClient.models.apiClient.httpOptions().apiVersion()); + + Client vertexClientWithExplicitVersion = + Client.builder() + .project(PROJECT) + .location(LOCATION) + .credentials(CREDENTIALS) + .vertexAI(true) + .httpOptions(HttpOptions.builder().apiVersion("v1").build()) + .build(); + assertEquals( + Optional.of("v1"), + vertexClientWithExplicitVersion.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 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..2d5c16b1313 100644 --- a/src/test/java/com/google/genai/GaosClientTest.java +++ b/src/test/java/com/google/genai/GaosClientTest.java @@ -156,6 +156,44 @@ public CompletableFuture> sendAsync(HttpRequest reques req.headers().firstValue("x-goog-api-client").orElse(null)); } + @Test + public void testInteractionsUrl_gemini_explicitApiVersion() throws Exception { + Client client = + Client.builder() + .apiKey("test-api-key") + .vertexAI(false) + .httpOptions(com.google.genai.types.HttpOptions.builder().apiVersion("v1alpha").build()) + .build(); + + AtomicReference capturedRequest = new AtomicReference<>(); + HTTPClient mockClient = + new HTTPClient() { + @Override + public HttpResponse send(HttpRequest request) { + capturedRequest.set(request); + return createMockResponse(request, 200, "{\"status\": \"completed\"}"); + } + + @Override + public CompletableFuture> sendAsync(HttpRequest request) { + return CompletableFuture.completedFuture(send(request)); + } + }; + setMockGaosClient(client, mockClient); + + com.google.genai.gaos.models.interactions.CreateModelInteraction body = + com.google.genai.gaos.models.interactions.CreateModelInteraction.builder() + .model("gemini-2.5-flash") + .input(com.google.genai.gaos.models.interactions.InteractionsInput.of("test-input")) + .build(); + client.interactions.create(CreateInteractionRequestBody.of(body)); + + HttpRequest req = capturedRequest.get(); + assertNotNull(req); + assertEquals( + URI.create("https://generativelanguage.googleapis.com/v1alpha/interactions"), req.uri()); + } + @Test public void testInteractionsUrl_gemini() throws Exception { Client client = Client.builder().apiKey("test-api-key").vertexAI(false).build(); @@ -189,7 +227,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 +429,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 +477,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"));