Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion core/src/main/java/com/google/adk/models/Gemini.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,13 @@
import static com.google.common.base.StandardSystemProperty.JAVA_VERSION;

import com.google.adk.Version;
import com.google.adk.utils.HttpUtils;
import com.google.common.collect.ImmutableMap;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.genai.Client;
import com.google.genai.ResponseStream;
import com.google.genai.types.Candidate;
import com.google.genai.types.ClientOptions;
import com.google.genai.types.Content;
import com.google.genai.types.FinishReason;
import com.google.genai.types.GenerateContentConfig;
Expand All @@ -37,6 +39,7 @@
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import okhttp3.OkHttpClient;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

Expand All @@ -50,6 +53,8 @@ public class Gemini extends BaseLlm {

private static final Logger logger = LoggerFactory.getLogger(Gemini.class);
private static final ImmutableMap<String, String> TRACKING_HEADERS;
private static final OkHttpClient SHARED_HTTP_CLIENT =
HttpUtils.createSharedHttpClient("GeminiApiClient");

static {
String frameworkLabel = "google-adk/" + Version.JAVA_ADK_VERSION;
Expand Down Expand Up @@ -88,6 +93,7 @@ public Gemini(String modelName, String apiKey) {
Client.builder()
.apiKey(apiKey)
.httpOptions(HttpOptions.builder().headers(TRACKING_HEADERS).build())
.clientOptions(ClientOptions.builder().customHttpClient(SHARED_HTTP_CLIENT).build())
.build();
}

Expand All @@ -101,7 +107,9 @@ public Gemini(String modelName, VertexCredentials vertexCredentials) {
super(modelName);
Objects.requireNonNull(vertexCredentials, "vertexCredentials cannot be null");
Client.Builder apiClientBuilder =
Client.builder().httpOptions(HttpOptions.builder().headers(TRACKING_HEADERS).build());
Client.builder()
.httpOptions(HttpOptions.builder().headers(TRACKING_HEADERS).build())
.clientOptions(ClientOptions.builder().customHttpClient(SHARED_HTTP_CLIENT).build());
vertexCredentials.project().ifPresent(apiClientBuilder::project);
vertexCredentials.location().ifPresent(apiClientBuilder::location);
vertexCredentials.credentials().ifPresent(apiClientBuilder::credentials);
Expand Down Expand Up @@ -200,6 +208,7 @@ public Gemini build() {
modelName,
Client.builder()
.httpOptions(HttpOptions.builder().headers(TRACKING_HEADERS).build())
.clientOptions(ClientOptions.builder().customHttpClient(SHARED_HTTP_CLIENT).build())
.build());
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import com.google.adk.JsonBaseModel;
import com.google.adk.models.LlmRequest;
import com.google.adk.models.LlmResponse;
import com.google.adk.utils.HttpUtils;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
Expand Down Expand Up @@ -74,7 +75,8 @@ public final class ChatCompletionsHttpClient {
* {@link ChatCompletionsHttpClient} instances. Each instance forks this client via {@link
* OkHttpClient#newBuilder()} to apply per-instance timeouts without leaking pools.
*/
private static final OkHttpClient SHARED_POOL_CLIENT = new OkHttpClient();
private static final OkHttpClient SHARED_POOL_CLIENT =
HttpUtils.createSharedHttpClient("ChatCompletionsHttpClient");

private final OkHttpClient client;
private final HttpUrl completionsUrl;
Expand Down
17 changes: 16 additions & 1 deletion core/src/main/java/com/google/adk/sessions/ApiClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

import static com.google.common.base.StandardSystemProperty.JAVA_VERSION;

import com.google.adk.utils.HttpUtils;
import com.google.auth.oauth2.GoogleCredentials;
import com.google.common.base.Ascii;
import com.google.common.base.Strings;
Expand Down Expand Up @@ -102,8 +103,22 @@ abstract class ApiClient {
this.httpClient = createHttpClient(httpOptions.timeout().orElse(null));
}

/**
* Shared OkHttpClient instance whose connection pool and thread dispatcher are reused across all
* {@link ApiClient} subclass instances.
*
* <p>Even though {@link ApiClient} is abstract, every instantiation of a concrete subclass (e.g.,
* GoogleAiClient) triggers this class's constructor. Making this static prevents a severe
* resource leak where every new LLM Agent would otherwise spin up a brand new thread pool and TCP
* connection pool. Instead, all clients share this pool and fork it via {@link
* OkHttpClient#newBuilder()}.
*/
private static final OkHttpClient SHARED_POOL_CLIENT =
HttpUtils.createSharedHttpClient("ApiClient");

private OkHttpClient createHttpClient(@Nullable Integer timeout) {
OkHttpClient.Builder builder = new OkHttpClient().newBuilder();
OkHttpClient.Builder builder = SHARED_POOL_CLIENT.newBuilder();

if (timeout != null) {
builder.connectTimeout(Duration.ofMillis(timeout));
}
Expand Down
62 changes: 62 additions & 0 deletions core/src/main/java/com/google/adk/utils/HttpUtils.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
/*
* 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
*
* http://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.utils;

import java.time.Duration;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadFactory;
import okhttp3.Dispatcher;
import okhttp3.OkHttpClient;

/** Utility class for common HTTP client configuration across the ADK. */
public final class HttpUtils {

private HttpUtils() {}

/**
* Configures a custom OkHttp dispatcher that uses daemon threads. By default, OkHttp uses
* non-daemon threads for its async call thread pool, which prevents the JVM from shutting down
* for 60 seconds (the default keep-alive) after the last streaming request completes.
*
* @param name The prefix name to use for the dispatcher threads.
* @return A pre-configured Dispatcher using daemon threads.
*/
private static Dispatcher createDaemonDispatcher(String name) {
ThreadFactory daemonThreadFactory =
r -> {
Thread t = new Thread(r, name + "-Dispatcher");
t.setDaemon(true);
return t;
};
return new Dispatcher(Executors.newCachedThreadPool(daemonThreadFactory));
}

/**
* Creates a shared OkHttpClient instance equipped with a daemon thread dispatcher.
*
* @param threadName The prefix name to use for the dispatcher threads.
* @return A pre-configured OkHttpClient.
*/
public static OkHttpClient createSharedHttpClient(String threadName) {
return new OkHttpClient.Builder()
.dispatcher(createDaemonDispatcher(threadName))
.connectTimeout(Duration.ofMinutes(5))
.readTimeout(Duration.ofMinutes(5))
.writeTimeout(Duration.ofMinutes(5))
.build();
}
}
29 changes: 29 additions & 0 deletions core/src/test/java/com/google/adk/utils/HttpUtilsTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.google.adk.utils;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;

import java.util.concurrent.ThreadFactory;
import org.junit.jupiter.api.Test;

public class HttpUtilsTest {

@Test
public void testSharedHttpClientDaemonThreads() {
ThreadFactory tf =
((java.util.concurrent.ThreadPoolExecutor)
HttpUtils.createSharedHttpClient("Test").dispatcher().executorService())
.getThreadFactory();
// Usually OkHttp uses a specific thread factory. We passed our own DaemonThreadFactory
Thread t = tf.newThread(() -> {});
assertTrue(t.isDaemon(), "HttpUtils thread factory should produce daemon threads");
}

@Test
public void testSharedHttpClientTimeouts() {
okhttp3.OkHttpClient client = HttpUtils.createSharedHttpClient("Test");
assertEquals(300000, client.connectTimeoutMillis());
assertEquals(300000, client.readTimeoutMillis());
assertEquals(300000, client.writeTimeoutMillis());
}
}