diff --git a/core/src/main/java/com/google/adk/sessions/ApiClient.java b/core/src/main/java/com/google/adk/sessions/ApiClient.java deleted file mode 100644 index 6bf69ee47..000000000 --- a/core/src/main/java/com/google/adk/sessions/ApiClient.java +++ /dev/null @@ -1,232 +0,0 @@ -/* - * 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. - */ - -package com.google.adk.sessions; - -import static com.google.common.base.Preconditions.checkNotNull; -import static com.google.common.base.StandardSystemProperty.JAVA_VERSION; - -import com.google.auth.oauth2.GoogleCredentials; -import com.google.common.base.Ascii; -import com.google.common.collect.ImmutableMap; -import com.google.genai.errors.GenAiIOException; -import com.google.genai.types.HttpOptions; -import java.io.IOException; -import java.time.Duration; -import java.util.Map; -import java.util.Optional; -import okhttp3.OkHttpClient; -import org.jspecify.annotations.Nullable; - -/** Interface for an API client which issues HTTP requests to the GenAI APIs. */ -abstract class ApiClient { - OkHttpClient httpClient; - // For Google AI APIs - final Optional apiKey; - // For Vertex AI APIs - final Optional project; - final Optional location; - final Optional credentials; - HttpOptions httpOptions; - final boolean vertexAI; - - /** Constructs an ApiClient for Google AI APIs. */ - ApiClient(Optional apiKey, Optional customHttpOptions) { - checkNotNull(apiKey, "API Key cannot be null"); - checkNotNull(customHttpOptions, "customHttpOptions cannot be null"); - - try { - this.apiKey = Optional.of(apiKey.orElseGet(() -> System.getenv("GOOGLE_API_KEY"))); - } catch (NullPointerException e) { - throw new IllegalArgumentException( - "API key must either be provided or set in the environment variable" + " GOOGLE_API_KEY.", - e); - } - - this.project = Optional.empty(); - this.location = Optional.empty(); - this.credentials = Optional.empty(); - this.vertexAI = false; - - this.httpOptions = defaultHttpOptions(/* vertexAI= */ false, this.location); - - if (customHttpOptions.isPresent()) { - applyHttpOptions(customHttpOptions.get()); - } - - this.httpClient = createHttpClient(httpOptions.timeout()); - } - - ApiClient( - Optional project, - Optional location, - Optional credentials, - Optional customHttpOptions) { - checkNotNull(project, "project cannot be null"); - checkNotNull(location, "location cannot be null"); - checkNotNull(credentials, "credentials cannot be null"); - checkNotNull(customHttpOptions, "customHttpOptions cannot be null"); - - try { - this.project = Optional.of(project.orElseGet(() -> System.getenv("GOOGLE_CLOUD_PROJECT"))); - } catch (NullPointerException e) { - throw new IllegalArgumentException( - "Project must either be provided or set in the environment variable" - + " GOOGLE_CLOUD_PROJECT.", - e); - } - if (this.project.get().isEmpty()) { - throw new IllegalArgumentException("Project must not be empty."); - } - - try { - this.location = Optional.of(location.orElse(System.getenv("GOOGLE_CLOUD_LOCATION"))); - } catch (NullPointerException e) { - throw new IllegalArgumentException( - "Location must either be provided or set in the environment variable" - + " GOOGLE_CLOUD_LOCATION.", - e); - } - if (this.location.get().isEmpty()) { - throw new IllegalArgumentException("Location must not be empty."); - } - - this.credentials = Optional.of(credentials.orElseGet(this::defaultCredentials)); - - this.httpOptions = defaultHttpOptions(/* vertexAI= */ true, this.location); - - if (customHttpOptions.isPresent()) { - applyHttpOptions(customHttpOptions.get()); - } - this.apiKey = Optional.empty(); - this.vertexAI = true; - this.httpClient = createHttpClient(httpOptions.timeout()); - } - - private OkHttpClient createHttpClient(Optional timeout) { - OkHttpClient.Builder builder = new OkHttpClient().newBuilder(); - if (timeout.isPresent()) { - builder.connectTimeout(Duration.ofMillis(timeout.get())); - } - return builder.build(); - } - - /** Sends a Http request given the http method, path, and request json string. */ - public abstract ApiResponse request(String httpMethod, String path, String requestJson); - - /** Returns the library version. */ - static String libraryVersion() { - // TODO: Automate revisions to the SDK library version. - String libraryLabel = "google-genai-sdk/0.1.0"; - String languageLabel = "gl-java/" + JAVA_VERSION.value(); - return libraryLabel + " " + languageLabel; - } - - /** Returns whether the client is using Vertex AI APIs. */ - public boolean vertexAI() { - return vertexAI; - } - - /** Returns the project ID for Vertex AI APIs. */ - public @Nullable String project() { - return project.orElse(null); - } - - /** Returns the location for Vertex AI APIs. */ - public @Nullable String location() { - return location.orElse(null); - } - - /** Returns the API key for Google AI APIs. */ - public @Nullable String apiKey() { - return apiKey.orElse(null); - } - - /** Returns the HttpClient for API calls. */ - OkHttpClient httpClient() { - return httpClient; - } - - private Optional> getTimeoutHeader(HttpOptions httpOptionsToApply) { - if (httpOptionsToApply.timeout().isPresent()) { - int timeoutInSeconds = (int) Math.ceil((double) httpOptionsToApply.timeout().get() / 1000.0); - // TODO(b/329147724): Document the usage of X-Server-Timeout header. - return Optional.of(ImmutableMap.of("X-Server-Timeout", Integer.toString(timeoutInSeconds))); - } - return Optional.empty(); - } - - private void applyHttpOptions(HttpOptions httpOptionsToApply) { - HttpOptions.Builder mergedHttpOptionsBuilder = this.httpOptions.toBuilder(); - if (httpOptionsToApply.baseUrl().isPresent()) { - mergedHttpOptionsBuilder.baseUrl(httpOptionsToApply.baseUrl().get()); - } - if (httpOptionsToApply.apiVersion().isPresent()) { - mergedHttpOptionsBuilder.apiVersion(httpOptionsToApply.apiVersion().get()); - } - if (httpOptionsToApply.timeout().isPresent()) { - mergedHttpOptionsBuilder.timeout(httpOptionsToApply.timeout().get()); - } - if (httpOptionsToApply.headers().isPresent()) { - ImmutableMap mergedHeaders = - ImmutableMap.builder() - .putAll(httpOptionsToApply.headers().orElse(ImmutableMap.of())) - .putAll(this.httpOptions.headers().orElse(ImmutableMap.of())) - .putAll(getTimeoutHeader(httpOptionsToApply).orElse(ImmutableMap.of())) - .buildOrThrow(); - mergedHttpOptionsBuilder.headers(mergedHeaders); - } - this.httpOptions = mergedHttpOptionsBuilder.build(); - } - - static HttpOptions defaultHttpOptions(boolean vertexAI, Optional location) { - ImmutableMap.Builder defaultHeaders = ImmutableMap.builder(); - defaultHeaders - .put("Content-Type", "application/json") - .put("user-agent", libraryVersion()) - .put("x-goog-api-client", libraryVersion()); - - HttpOptions.Builder defaultHttpOptionsBuilder = - HttpOptions.builder().headers(defaultHeaders.buildOrThrow()); - - if (vertexAI && location.isPresent()) { - defaultHttpOptionsBuilder - .baseUrl( - Ascii.equalsIgnoreCase(location.get(), "global") - ? "https://aiplatform.googleapis.com" - : String.format("https://%s-aiplatform.googleapis.com", location.get())) - .apiVersion("v1beta1"); - } else if (vertexAI && location.isEmpty()) { - throw new IllegalArgumentException("Location must be provided for Vertex AI APIs."); - } else { - defaultHttpOptionsBuilder - .baseUrl("https://generativelanguage.googleapis.com") - .apiVersion("v1beta"); - } - return defaultHttpOptionsBuilder.build(); - } - - GoogleCredentials defaultCredentials() { - try { - return GoogleCredentials.getApplicationDefault() - .createScoped("https://www.googleapis.com/auth/cloud-platform"); - } catch (IOException e) { - throw new GenAiIOException( - "Failed to get application default credentials, please explicitly provide credentials.", - e); - } - } -} diff --git a/core/src/main/java/com/google/adk/sessions/ApiResponse.java b/core/src/main/java/com/google/adk/sessions/ApiResponse.java deleted file mode 100644 index 018c173a4..000000000 --- a/core/src/main/java/com/google/adk/sessions/ApiResponse.java +++ /dev/null @@ -1,28 +0,0 @@ -/* - * 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. - */ - -package com.google.adk.sessions; - -import okhttp3.ResponseBody; - -/** The API response contains a response to a call to the GenAI APIs. */ -public abstract class ApiResponse implements AutoCloseable { - /** Gets the HttpEntity. */ - public abstract ResponseBody getResponseBody(); - - @Override - public abstract void close(); -} diff --git a/core/src/main/java/com/google/adk/sessions/HttpApiClient.java b/core/src/main/java/com/google/adk/sessions/HttpApiClient.java deleted file mode 100644 index bba39da89..000000000 --- a/core/src/main/java/com/google/adk/sessions/HttpApiClient.java +++ /dev/null @@ -1,129 +0,0 @@ -/* - * 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. - */ - -package com.google.adk.sessions; - -import com.google.auth.oauth2.GoogleCredentials; -import com.google.common.base.Ascii; -import com.google.common.collect.ImmutableMap; -import com.google.genai.errors.GenAiIOException; -import com.google.genai.types.HttpOptions; -import java.io.IOException; -import java.util.Map; -import java.util.Optional; -import okhttp3.MediaType; -import okhttp3.Request; -import okhttp3.RequestBody; -import okhttp3.Response; - -/** Base client for the HTTP APIs. */ -public class HttpApiClient extends ApiClient { - public static final MediaType MEDIA_TYPE_APPLICATION_JSON = - MediaType.parse("application/json; charset=utf-8"); - - /** Constructs an ApiClient for Google AI APIs. */ - HttpApiClient(Optional apiKey, Optional httpOptions) { - super(apiKey, httpOptions); - } - - /** Constructs an ApiClient for Vertex AI APIs. */ - HttpApiClient( - Optional project, - Optional location, - Optional credentials, - Optional httpOptions) { - super(project, location, credentials, httpOptions); - } - - /** Sends a Http request given the http method, path, and request json string. */ - @Override - public ApiResponse request(String httpMethod, String path, String requestJson) { - boolean queryBaseModel = - Ascii.equalsIgnoreCase(httpMethod, "GET") && path.startsWith("publishers/google/models/"); - if (this.vertexAI() && !path.startsWith("projects/") && !queryBaseModel) { - path = - String.format("projects/%s/locations/%s/", this.project.get(), this.location.get()) - + path; - } - String requestUrl = - String.format( - "%s/%s/%s", httpOptions.baseUrl().get(), httpOptions.apiVersion().get(), path); - - Request.Builder requestBuilder = new Request.Builder().url(requestUrl); - setHeaders(requestBuilder); - - if (Ascii.equalsIgnoreCase(httpMethod, "POST")) { - requestBuilder.post(RequestBody.create(MEDIA_TYPE_APPLICATION_JSON, requestJson)); - - } else if (Ascii.equalsIgnoreCase(httpMethod, "GET")) { - requestBuilder.get(); - } else if (Ascii.equalsIgnoreCase(httpMethod, "DELETE")) { - requestBuilder.delete(); - } else { - throw new IllegalArgumentException("Unsupported HTTP method: " + httpMethod); - } - return executeRequest(requestBuilder.build()); - } - - /** Sets the required headers (including auth) on the request object. */ - private void setHeaders(Request.Builder requestBuilder) { - for (Map.Entry header : - httpOptions.headers().orElse(ImmutableMap.of()).entrySet()) { - requestBuilder.header(header.getKey(), header.getValue()); - } - - if (apiKey.isPresent()) { - requestBuilder.header("x-goog-api-key", apiKey.get()); - } else { - GoogleCredentials cred = - credentials.orElseThrow(() -> new IllegalStateException("credentials is required")); - try { - cred.refreshIfExpired(); - } catch (IOException e) { - throw new GenAiIOException("Failed to refresh credentials.", e); - } - String accessToken; - try { - accessToken = cred.getAccessToken().getTokenValue(); - } catch (NullPointerException e) { - // For test cases where the access token is not available. - if (e.getMessage() - .contains( - "because the return value of" - + " \"com.google.auth.oauth2.GoogleCredentials.getAccessToken()\" is null")) { - accessToken = ""; - } else { - throw e; - } - } - requestBuilder.header("Authorization", "Bearer " + accessToken); - - if (cred.getQuotaProjectId() != null) { - requestBuilder.header("x-goog-user-project", cred.getQuotaProjectId()); - } - } - } - - /** Executes the given HTTP request. */ - private ApiResponse executeRequest(Request request) { - try { - Response response = httpClient.newCall(request).execute(); - return new HttpApiResponse(response); - } catch (IOException e) { - throw new GenAiIOException("Failed to execute HTTP request.", e); - } - } -} diff --git a/core/src/main/java/com/google/adk/sessions/HttpApiResponse.java b/core/src/main/java/com/google/adk/sessions/HttpApiResponse.java deleted file mode 100644 index aca5eeeae..000000000 --- a/core/src/main/java/com/google/adk/sessions/HttpApiResponse.java +++ /dev/null @@ -1,43 +0,0 @@ -/* - * 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. - */ - -package com.google.adk.sessions; - -import okhttp3.Response; -import okhttp3.ResponseBody; - -/** Wraps a real HTTP response to expose the methods needed by the GenAI SDK. */ -public final class HttpApiResponse extends ApiResponse { - - private final Response response; - - /** Constructs a HttpApiResponse instance with the response. */ - public HttpApiResponse(Response response) { - this.response = response; - } - - /** Returns the HttpEntity from the response. */ - @Override - public ResponseBody getResponseBody() { - return response.body(); - } - - /** Closes the Http response. */ - @Override - public void close() { - response.close(); - } -} diff --git a/core/src/main/java/com/google/adk/sessions/VertexAiClient.java b/core/src/main/java/com/google/adk/sessions/VertexAiClient.java index d35bbccae..c190bd3f7 100644 --- a/core/src/main/java/com/google/adk/sessions/VertexAiClient.java +++ b/core/src/main/java/com/google/adk/sessions/VertexAiClient.java @@ -8,6 +8,8 @@ import com.google.auth.oauth2.GoogleCredentials; import com.google.common.base.Splitter; import com.google.common.collect.Iterables; +import com.google.genai.ApiResponse; +import com.google.genai.HttpApiClient; import com.google.genai.types.HttpOptions; import io.reactivex.rxjava3.core.Completable; import io.reactivex.rxjava3.core.Maybe; @@ -38,7 +40,13 @@ final class VertexAiClient { VertexAiClient() { this.apiClient = - new HttpApiClient(Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty()); + new HttpApiClient( + Optional.empty(), + Optional.empty(), + Optional.empty(), + Optional.empty(), + Optional.empty(), + Optional.empty()); } VertexAiClient( @@ -47,7 +55,13 @@ final class VertexAiClient { Optional credentials, Optional httpOptions) { this.apiClient = - new HttpApiClient(Optional.of(project), Optional.of(location), credentials, httpOptions); + new HttpApiClient( + Optional.empty(), + Optional.of(project), + Optional.of(location), + credentials, + httpOptions, + Optional.empty()); } Maybe createSession( @@ -65,7 +79,6 @@ Maybe createSession( "POST", "reasoningEngines/" + reasoningEngineId + "/sessions", sessionJson)) .flatMapMaybe( apiResponse -> { - logger.debug("Create Session response {}", apiResponse.getResponseBody()); return getJsonResponse(apiResponse); }) .flatMap( @@ -119,7 +132,6 @@ Maybe listEvents(String reasoningEngineId, String sessionId) { "GET", "reasoningEngines/" + reasoningEngineId + "/sessions/" + sessionId + "/events", "") - .doOnSuccess(apiResponse -> logger.debug("List events response {}", apiResponse)) .flatMapMaybe(VertexAiClient::getJsonResponse); } @@ -144,7 +156,7 @@ Completable appendEvent(String reasoningEngineId, String sessionId, String event .flatMapCompletable( response -> { try (response) { - ResponseBody responseBody = response.getResponseBody(); + ResponseBody responseBody = response.getBody(); if (responseBody != null) { String responseString = responseBody.string(); if (responseString.contains("com.google.genai.errors.ClientException")) { @@ -166,7 +178,7 @@ Completable appendEvent(String reasoningEngineId, String sessionId, String event private Single performApiRequest(String method, String path, String body) { return Single.fromCallable( () -> { - return apiClient.request(method, path, body); + return apiClient.request(method, path, body, Optional.empty()); }); } @@ -178,11 +190,11 @@ private Single performApiRequest(String method, String path, String @Nullable private static Maybe getJsonResponse(ApiResponse apiResponse) { try { - if (apiResponse == null || apiResponse.getResponseBody() == null) { + if (apiResponse == null || apiResponse.getBody() == null) { return Maybe.empty(); } try { - ResponseBody responseBody = apiResponse.getResponseBody(); + ResponseBody responseBody = apiResponse.getBody(); String responseString = responseBody.string(); // Read body here if (responseString.isEmpty()) { return Maybe.empty(); diff --git a/core/src/main/java/com/google/adk/sessions/VertexAiSessionService.java b/core/src/main/java/com/google/adk/sessions/VertexAiSessionService.java index 7878daf22..b1cd309da 100644 --- a/core/src/main/java/com/google/adk/sessions/VertexAiSessionService.java +++ b/core/src/main/java/com/google/adk/sessions/VertexAiSessionService.java @@ -26,6 +26,7 @@ import com.google.auth.oauth2.GoogleCredentials; import com.google.common.base.Splitter; import com.google.common.collect.Iterables; +import com.google.genai.HttpApiClient; import com.google.genai.types.HttpOptions; import io.reactivex.rxjava3.core.Completable; import io.reactivex.rxjava3.core.Maybe; diff --git a/core/src/test/java/com/google/adk/sessions/MockApiAnswer.java b/core/src/test/java/com/google/adk/sessions/MockApiAnswer.java index 111b1dce3..ead555c37 100644 --- a/core/src/test/java/com/google/adk/sessions/MockApiAnswer.java +++ b/core/src/test/java/com/google/adk/sessions/MockApiAnswer.java @@ -4,6 +4,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.google.adk.JsonBaseModel; import com.google.adk.events.Event; +import com.google.genai.ApiResponse; import java.util.ArrayList; import java.util.HashMap; import java.util.List; @@ -12,6 +13,7 @@ import java.util.concurrent.ConcurrentMap; import java.util.regex.Matcher; import java.util.regex.Pattern; +import okhttp3.Headers; import okhttp3.MediaType; import okhttp3.ResponseBody; import org.mockito.invocation.InvocationOnMock; @@ -68,10 +70,15 @@ public ApiResponse answer(InvocationOnMock invocation) throws Throwable { private static ApiResponse responseWithBody(String body) { return new ApiResponse() { @Override - public ResponseBody getResponseBody() { + public ResponseBody getBody() { return ResponseBody.create(JSON_MEDIA_TYPE, body); } + @Override + public Headers getHeaders() { + return Headers.of(); + } + @Override public void close() {} }; diff --git a/core/src/test/java/com/google/adk/sessions/VertexAiSessionServiceTest.java b/core/src/test/java/com/google/adk/sessions/VertexAiSessionServiceTest.java index 36eab1d16..c0c2789eb 100644 --- a/core/src/test/java/com/google/adk/sessions/VertexAiSessionServiceTest.java +++ b/core/src/test/java/com/google/adk/sessions/VertexAiSessionServiceTest.java @@ -3,6 +3,7 @@ import static com.google.common.collect.ImmutableList.toImmutableList; import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertThrows; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.when; @@ -14,6 +15,7 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; +import com.google.genai.HttpApiClient; import com.google.genai.types.Content; import com.google.genai.types.Part; import io.reactivex.rxjava3.core.Single; @@ -161,7 +163,7 @@ public void setUp() throws Exception { MockitoAnnotations.openMocks(this); vertexAiSessionService = new VertexAiSessionService("test-project", "test-location", mockApiClient); - when(mockApiClient.request(anyString(), anyString(), anyString())) + when(mockApiClient.request(anyString(), anyString(), anyString(), any())) .thenAnswer(new MockApiAnswer(sessionMap, eventMap)); }