diff --git a/core/src/main/java/com/google/adk/config/AdkConfiguration.java b/core/src/main/java/com/google/adk/config/AdkConfiguration.java
new file mode 100644
index 000000000..946fb30e2
--- /dev/null
+++ b/core/src/main/java/com/google/adk/config/AdkConfiguration.java
@@ -0,0 +1,104 @@
+/*
+ * 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.config;
+
+import java.util.Optional;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+
+/**
+ * Central configuration provider for the ADK SDK.
+ *
+ *
Resolves configuration values using the following fallback chain (highest priority first):
+ *
+ *
+ * - Programmatic values set via {@link #set(String, String)}.
+ *
- JVM system properties ({@link System#getProperty(String)}).
+ *
- Operating system environment variables ({@link System#getenv(String)}) — preserved for
+ * backward compatibility.
+ *
+ *
+ * This abstraction allows applications (e.g. Spring Boot) to inject configuration that
+ * originates from {@code application.yaml}, secret managers, or any other source, without relying
+ * on the immutable OS environment.
+ */
+public final class AdkConfiguration {
+
+ private static final ConcurrentMap OVERRIDES = new ConcurrentHashMap<>();
+
+ private AdkConfiguration() {}
+
+ /**
+ * Programmatically sets a configuration value. Takes precedence over system properties and
+ * environment variables.
+ *
+ * @param key the configuration key (typically the same name as the legacy environment variable)
+ * @param value the value to associate with the key; must not be {@code null}. Use {@link
+ * #clear(String)} to remove an entry.
+ */
+ public static void set(String key, String value) {
+ if (key == null) {
+ throw new IllegalArgumentException("key must not be null");
+ }
+ if (value == null) {
+ throw new IllegalArgumentException("value must not be null; use clear(key) to remove");
+ }
+ OVERRIDES.put(key, value);
+ }
+
+ /** Removes a programmatic override for the given key, if any. */
+ public static void clear(String key) {
+ if (key != null) {
+ OVERRIDES.remove(key);
+ }
+ }
+
+ /** Removes all programmatic overrides. Primarily intended for tests. */
+ public static void clearAll() {
+ OVERRIDES.clear();
+ }
+
+ /**
+ * Resolves a configuration value using the fallback chain described in the class javadoc.
+ *
+ * @param key the configuration key
+ * @return an {@link Optional} containing the resolved value, or empty if no source provides one
+ */
+ public static Optional get(String key) {
+ if (key == null) {
+ return Optional.empty();
+ }
+ String override = OVERRIDES.get(key);
+ if (override != null) {
+ return Optional.of(override);
+ }
+ String systemProperty = System.getProperty(key);
+ if (systemProperty != null) {
+ return Optional.of(systemProperty);
+ }
+ return Optional.ofNullable(System.getenv(key));
+ }
+
+ /**
+ * Resolves a configuration value, returning the provided default if no source provides one.
+ *
+ * @param key the configuration key
+ * @param defaultValue value to return when the key cannot be resolved
+ */
+ public static String getOrDefault(String key, String defaultValue) {
+ return get(key).orElse(defaultValue);
+ }
+}
diff --git a/core/src/main/java/com/google/adk/models/ApigeeLlm.java b/core/src/main/java/com/google/adk/models/ApigeeLlm.java
index 088e0af76..db2205322 100644
--- a/core/src/main/java/com/google/adk/models/ApigeeLlm.java
+++ b/core/src/main/java/com/google/adk/models/ApigeeLlm.java
@@ -19,6 +19,7 @@
import static com.google.common.base.Strings.isNullOrEmpty;
import com.google.adk.Version;
+import com.google.adk.config.AdkConfiguration;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableMap;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
@@ -73,7 +74,7 @@ private ApigeeLlm(String modelName, String proxyUrl, Map customH
String effectiveProxyUrl = proxyUrl;
if (isNullOrEmpty(effectiveProxyUrl)) {
- effectiveProxyUrl = System.getenv(APIGEE_PROXY_URL_ENV_VARIABLE_NAME);
+ effectiveProxyUrl = AdkConfiguration.get(APIGEE_PROXY_URL_ENV_VARIABLE_NAME).orElse(null);
}
if (isNullOrEmpty(effectiveProxyUrl)) {
throw new IllegalArgumentException(
@@ -306,7 +307,7 @@ private static boolean validateModelString(String model) {
}
private static boolean isEnvEnabled(String envVarName) {
- String value = System.getenv(envVarName);
+ String value = AdkConfiguration.get(envVarName).orElse(null);
return Boolean.parseBoolean(value) || Objects.equals(value, "1");
}
diff --git a/core/src/main/java/com/google/adk/sessions/ApiClient.java b/core/src/main/java/com/google/adk/sessions/ApiClient.java
index 1b0485dd2..75ad98678 100644
--- a/core/src/main/java/com/google/adk/sessions/ApiClient.java
+++ b/core/src/main/java/com/google/adk/sessions/ApiClient.java
@@ -18,6 +18,7 @@
import static com.google.common.base.StandardSystemProperty.JAVA_VERSION;
+import com.google.adk.config.AdkConfiguration;
import com.google.auth.oauth2.GoogleCredentials;
import com.google.common.base.Ascii;
import com.google.common.base.Strings;
@@ -46,7 +47,7 @@ abstract class ApiClient {
/** Constructs an ApiClient for Google AI APIs. */
ApiClient(@Nullable String apiKey, @Nullable HttpOptions customHttpOptions) {
- this.apiKey = apiKey != null ? apiKey : System.getenv("GOOGLE_API_KEY");
+ this.apiKey = apiKey != null ? apiKey : AdkConfiguration.get("GOOGLE_API_KEY").orElse(null);
if (Strings.isNullOrEmpty(this.apiKey)) {
throw new IllegalArgumentException(
@@ -74,7 +75,8 @@ abstract class ApiClient {
@Nullable GoogleCredentials credentials,
@Nullable HttpOptions customHttpOptions) {
- this.project = project != null ? project : System.getenv("GOOGLE_CLOUD_PROJECT");
+ this.project =
+ project != null ? project : AdkConfiguration.get("GOOGLE_CLOUD_PROJECT").orElse(null);
if (Strings.isNullOrEmpty(this.project)) {
throw new IllegalArgumentException(
@@ -82,7 +84,8 @@ abstract class ApiClient {
+ " GOOGLE_CLOUD_PROJECT.");
}
- this.location = location != null ? location : System.getenv("GOOGLE_CLOUD_LOCATION");
+ this.location =
+ location != null ? location : AdkConfiguration.get("GOOGLE_CLOUD_LOCATION").orElse(null);
if (Strings.isNullOrEmpty(this.location)) {
throw new IllegalArgumentException(
diff --git a/core/src/main/java/com/google/adk/tools/retrieval/VertexAiRagRetrieval.java b/core/src/main/java/com/google/adk/tools/retrieval/VertexAiRagRetrieval.java
index a2720aae5..0ac28cc20 100644
--- a/core/src/main/java/com/google/adk/tools/retrieval/VertexAiRagRetrieval.java
+++ b/core/src/main/java/com/google/adk/tools/retrieval/VertexAiRagRetrieval.java
@@ -18,6 +18,7 @@
import static com.google.common.collect.ImmutableList.toImmutableList;
+import com.google.adk.config.AdkConfiguration;
import com.google.adk.models.LlmRequest;
import com.google.adk.tools.ToolContext;
import com.google.adk.utils.ModelNameUtils;
@@ -106,7 +107,8 @@ public Completable processLlmRequest(
LlmRequest.Builder llmRequestBuilder, ToolContext toolContext) {
LlmRequest llmRequest = llmRequestBuilder.build();
// Use Gemini built-in Vertex AI RAG tool for Gemini models when using Vertex AI API Model
- boolean useVertexAi = Boolean.parseBoolean(System.getenv("GOOGLE_GENAI_USE_VERTEXAI"));
+ boolean useVertexAi =
+ Boolean.parseBoolean(AdkConfiguration.getOrDefault("GOOGLE_GENAI_USE_VERTEXAI", "false"));
if (useVertexAi && llmRequest.model().filter(ModelNameUtils::isGeminiModel).isPresent()) {
GenerateContentConfig config =
llmRequest.config().orElseGet(() -> GenerateContentConfig.builder().build());
diff --git a/core/src/test/java/com/google/adk/config/AdkConfigurationTest.java b/core/src/test/java/com/google/adk/config/AdkConfigurationTest.java
new file mode 100644
index 000000000..fe721b3b7
--- /dev/null
+++ b/core/src/test/java/com/google/adk/config/AdkConfigurationTest.java
@@ -0,0 +1,88 @@
+/*
+ * 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.config;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+public class AdkConfigurationTest {
+
+ private static final String KEY = "ADK_CONFIG_TEST_KEY";
+
+ @BeforeEach
+ void setUp() {
+ AdkConfiguration.clearAll();
+ System.clearProperty(KEY);
+ }
+
+ @AfterEach
+ void tearDown() {
+ AdkConfiguration.clearAll();
+ System.clearProperty(KEY);
+ }
+
+ @Test
+ void get_returnsEmpty_whenNoSourceProvidesValue() {
+ assertThat(AdkConfiguration.get(KEY)).isEmpty();
+ }
+
+ @Test
+ void get_returnsSystemProperty_whenSet() {
+ System.setProperty(KEY, "from-sysprop");
+ assertThat(AdkConfiguration.get(KEY)).hasValue("from-sysprop");
+ }
+
+ @Test
+ void set_takesPrecedenceOverSystemProperty() {
+ System.setProperty(KEY, "from-sysprop");
+ AdkConfiguration.set(KEY, "from-programmatic");
+ assertThat(AdkConfiguration.get(KEY)).hasValue("from-programmatic");
+ }
+
+ @Test
+ void clear_removesProgrammaticOverride_andFallsBackToSystemProperty() {
+ System.setProperty(KEY, "from-sysprop");
+ AdkConfiguration.set(KEY, "from-programmatic");
+ AdkConfiguration.clear(KEY);
+ assertThat(AdkConfiguration.get(KEY)).hasValue("from-sysprop");
+ }
+
+ @Test
+ void getOrDefault_returnsDefault_whenUnset() {
+ assertThat(AdkConfiguration.getOrDefault(KEY, "fallback")).isEqualTo("fallback");
+ }
+
+ @Test
+ void getOrDefault_returnsResolvedValue_whenSet() {
+ AdkConfiguration.set(KEY, "resolved");
+ assertThat(AdkConfiguration.getOrDefault(KEY, "fallback")).isEqualTo("resolved");
+ }
+
+ @Test
+ void set_throws_onNullKeyOrValue() {
+ assertThrows(IllegalArgumentException.class, () -> AdkConfiguration.set(null, "v"));
+ assertThrows(IllegalArgumentException.class, () -> AdkConfiguration.set(KEY, null));
+ }
+
+ @Test
+ void get_returnsEmpty_forNullKey() {
+ assertThat(AdkConfiguration.get(null)).isEmpty();
+ }
+}