Skip to content

Commit 63a92ce

Browse files
damianmomotgooglecopybara-github
authored andcommitted
chore: send one ADK tag and one Java tag on every Gemini request
PiperOrigin-RevId: 1002229130
1 parent 28f2606 commit 63a92ce

7 files changed

Lines changed: 614 additions & 60 deletions

File tree

‎core/pom.xml‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -319,6 +319,20 @@
319319
</environmentVariables>
320320
</configuration>
321321
</execution>
322+
<execution>
323+
<id>gemini-tracking-headers</id>
324+
<goals>
325+
<goal>test</goal>
326+
</goals>
327+
<configuration>
328+
<test>GeminiTrackingHeadersTest#generateContent_adkBuiltRegistryClient_sendsTrackingHeaders, GeminiTrackingHeadersTest#generateContent_adkBuiltVertexCredentialsClient_sendsTrackingHeaders</test>
329+
<!-- these tests read the API key from the environment -->
330+
<environmentVariables>
331+
<GOOGLE_API_KEY>api-key</GOOGLE_API_KEY>
332+
<GOOGLE_GENAI_USE_VERTEXAI>false</GOOGLE_GENAI_USE_VERTEXAI>
333+
</environmentVariables>
334+
</configuration>
335+
</execution>
322336
</executions>
323337
</plugin>
324338
</plugins>

‎core/src/main/java/com/google/adk/models/ApigeeLlm.java‎

Lines changed: 11 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,8 @@
1515
*/
1616
package com.google.adk.models;
1717

18-
import static com.google.common.base.StandardSystemProperty.JAVA_VERSION;
1918
import static com.google.common.base.Strings.isNullOrEmpty;
2019

21-
import com.google.adk.Version;
2220
import com.google.adk.models.chat.ChatCompletionsClient;
2321
import com.google.adk.models.chat.ChatCompletionsHttpClient;
2422
import com.google.common.annotations.VisibleForTesting;
@@ -46,17 +44,6 @@ public class ApigeeLlm extends BaseLlm {
4644
private static final String GOOGLE_GENAI_USE_VERTEXAI_ENV_VARIABLE_NAME =
4745
"GOOGLE_GENAI_USE_VERTEXAI";
4846
private static final String APIGEE_PROXY_URL_ENV_VARIABLE_NAME = "APIGEE_PROXY_URL";
49-
private static final ImmutableMap<String, String> TRACKING_HEADERS;
50-
51-
static {
52-
final String frameworkLabel = "google-adk/" + Version.JAVA_ADK_VERSION;
53-
final String languageLabel = "gl-java/" + JAVA_VERSION.value();
54-
final String versionHeaderValue = String.format("%s %s", frameworkLabel, languageLabel);
55-
TRACKING_HEADERS =
56-
ImmutableMap.of(
57-
"x-goog-api-client", versionHeaderValue,
58-
"user-agent", versionHeaderValue);
59-
}
6047

6148
/** Defines the type of API to be used by the Apigee proxy. */
6249
public enum ApiType {
@@ -108,17 +95,24 @@ private ApigeeLlm(
10895
+ " APIGEE_PROXY_URL.");
10996
}
11097

98+
// Chat completions clients send no language tag, so they get both tags; the genai SDK adds its
99+
// own gl-java tag.
100+
ImmutableMap<String, String> trackingHeaders =
101+
this.apiType == ApiType.CHAT_COMPLETIONS
102+
? TrackingHeaders.HEADERS
103+
: TrackingHeaders.GENAI_SDK_HEADERS;
104+
111105
// Build the Client
112106
HttpOptions.Builder httpOptionsBuilder =
113-
HttpOptions.builder().baseUrl(effectiveProxyUrl).headers(TRACKING_HEADERS);
107+
HttpOptions.builder().baseUrl(effectiveProxyUrl).headers(trackingHeaders);
114108
String apiVersion = identifyApiVersion(modelName);
115109
if (!apiVersion.isEmpty()) {
116110
httpOptionsBuilder.apiVersion(apiVersion);
117111
}
118112
if (customHeaders != null) {
119113
httpOptionsBuilder.headers(
120114
ImmutableMap.<String, String>builder()
121-
.putAll(TRACKING_HEADERS)
115+
.putAll(trackingHeaders)
122116
.putAll(customHeaders)
123117
.buildOrThrow());
124118
}
@@ -134,7 +128,8 @@ private ApigeeLlm(
134128
apiClientBuilder.vertexAI(true);
135129
}
136130
this.apiClient = apiClientBuilder.build();
137-
this.geminiDelegate = new Gemini(modelName, apiClient);
131+
this.geminiDelegate =
132+
new Gemini(modelName, apiClient, /* clientSendsTrackingHeaders= */ true);
138133
this.chatCompletionsClient = null;
139134
}
140135

‎core/src/main/java/com/google/adk/models/Gemini.java‎

Lines changed: 58 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,6 @@
1616

1717
package com.google.adk.models;
1818

19-
import static com.google.common.base.StandardSystemProperty.JAVA_VERSION;
20-
21-
import com.google.adk.Version;
2219
import com.google.adk.internal.http.HttpClientFactory;
2320
import com.google.common.annotations.VisibleForTesting;
2421
import com.google.common.collect.ImmutableList;
@@ -60,7 +57,6 @@
6057
public class Gemini extends BaseLlm {
6158

6259
private static final Logger logger = LoggerFactory.getLogger(Gemini.class);
63-
private static final ImmutableMap<String, String> TRACKING_HEADERS;
6460

6561
private static OkHttpClient prepareHttpClient(@Nullable ExecutorService executorService) {
6662
OkHttpClient client =
@@ -75,61 +71,53 @@ private static OkHttpClient prepareHttpClient(@Nullable ExecutorService executor
7571
.build();
7672
}
7773

78-
private static Client buildApiKeyClient(
79-
String apiKey, @Nullable ExecutorService executorService) {
74+
// Every client ADK builds starts here, so each sends ADK's tracking tag.
75+
private static Client.Builder adkClientBuilder(@Nullable ExecutorService executorService) {
8076
return Client.builder()
81-
.apiKey(apiKey)
82-
.httpOptions(HttpOptions.builder().headers(TRACKING_HEADERS).build())
77+
.httpOptions(HttpOptions.builder().headers(TrackingHeaders.GENAI_SDK_HEADERS).build())
8378
.clientOptions(
84-
ClientOptions.builder().customHttpClient(prepareHttpClient(executorService)).build())
85-
.build();
79+
ClientOptions.builder().customHttpClient(prepareHttpClient(executorService)).build());
80+
}
81+
82+
private static Client buildApiKeyClient(
83+
String apiKey, @Nullable ExecutorService executorService) {
84+
return adkClientBuilder(executorService).apiKey(apiKey).build();
8685
}
8786

8887
private static Client buildVertexClient(
8988
VertexCredentials vertexCredentials, @Nullable ExecutorService executorService) {
90-
Client.Builder apiClientBuilder =
91-
Client.builder()
92-
.httpOptions(HttpOptions.builder().headers(TRACKING_HEADERS).build())
93-
.clientOptions(
94-
ClientOptions.builder()
95-
.customHttpClient(prepareHttpClient(executorService))
96-
.build());
89+
Client.Builder apiClientBuilder = adkClientBuilder(executorService);
9790
vertexCredentials.project().ifPresent(apiClientBuilder::project);
9891
vertexCredentials.location().ifPresent(apiClientBuilder::location);
9992
vertexCredentials.credentials().ifPresent(apiClientBuilder::credentials);
10093
return apiClientBuilder.build();
10194
}
10295

10396
private static Client buildDefaultClient(@Nullable ExecutorService executorService) {
104-
return Client.builder()
105-
.httpOptions(HttpOptions.builder().headers(TRACKING_HEADERS).build())
106-
.clientOptions(
107-
ClientOptions.builder().customHttpClient(prepareHttpClient(executorService)).build())
108-
.build();
109-
}
110-
111-
static {
112-
String frameworkLabel = "google-adk/" + Version.JAVA_ADK_VERSION;
113-
String languageLabel = "gl-java/" + JAVA_VERSION.value();
114-
String versionHeaderValue = String.format("%s %s", frameworkLabel, languageLabel);
115-
116-
TRACKING_HEADERS =
117-
ImmutableMap.of(
118-
"x-goog-api-client", versionHeaderValue,
119-
"user-agent", versionHeaderValue);
97+
return adkClientBuilder(executorService).build();
12098
}
12199

122100
private final Client apiClient;
101+
// True when apiClient already sends ADK's tracking tag.
102+
private final boolean clientSendsTrackingHeaders;
123103

124104
/**
125-
* Constructs a new Gemini instance.
105+
* Constructs a new Gemini instance. Each generate request sent through {@code apiClient} carries
106+
* ADK's tracking tag, which the genai SDK appends to the client's tracking headers. If the
107+
* client's own headers already carry an ADK tag, requests send two because the SDK does not
108+
* expose a client's headers.
126109
*
127110
* @param modelName The name of the Gemini model to use (e.g., "gemini-2.0-flash").
128111
* @param apiClient The genai {@link com.google.genai.Client} instance for making API calls.
129112
*/
130113
public Gemini(String modelName, Client apiClient) {
114+
this(modelName, apiClient, /* clientSendsTrackingHeaders= */ false);
115+
}
116+
117+
Gemini(String modelName, Client apiClient, boolean clientSendsTrackingHeaders) {
131118
super(modelName);
132119
this.apiClient = Objects.requireNonNull(apiClient, "apiClient cannot be null");
120+
this.clientSendsTrackingHeaders = clientSendsTrackingHeaders;
133121
}
134122

135123
/**
@@ -139,9 +127,10 @@ public Gemini(String modelName, Client apiClient) {
139127
* @param apiKey The Google Gemini API key.
140128
*/
141129
public Gemini(String modelName, String apiKey) {
142-
super(modelName);
143-
Objects.requireNonNull(apiKey, "apiKey cannot be null");
144-
this.apiClient = buildApiKeyClient(apiKey, null);
130+
this(
131+
modelName,
132+
buildApiKeyClient(Objects.requireNonNull(apiKey, "apiKey cannot be null"), null),
133+
/* clientSendsTrackingHeaders= */ true);
145134
}
146135

147136
/**
@@ -151,9 +140,11 @@ public Gemini(String modelName, String apiKey) {
151140
* @param vertexCredentials The Vertex AI credentials to access the Gemini model.
152141
*/
153142
public Gemini(String modelName, VertexCredentials vertexCredentials) {
154-
super(modelName);
155-
Objects.requireNonNull(vertexCredentials, "vertexCredentials cannot be null");
156-
this.apiClient = buildVertexClient(vertexCredentials, null);
143+
this(
144+
modelName,
145+
buildVertexClient(
146+
Objects.requireNonNull(vertexCredentials, "vertexCredentials cannot be null"), null),
147+
/* clientSendsTrackingHeaders= */ true);
157148
}
158149

159150
/**
@@ -256,13 +247,16 @@ public Gemini build() {
256247

257248
if (apiClient != null) {
258249
return new Gemini(modelName, apiClient);
259-
} else if (apiKey != null) {
260-
return new Gemini(modelName, buildApiKeyClient(apiKey, httpExecutorService));
250+
}
251+
Client client;
252+
if (apiKey != null) {
253+
client = buildApiKeyClient(apiKey, httpExecutorService);
261254
} else if (vertexCredentials != null) {
262-
return new Gemini(modelName, buildVertexClient(vertexCredentials, httpExecutorService));
255+
client = buildVertexClient(vertexCredentials, httpExecutorService);
263256
} else {
264-
return new Gemini(modelName, buildDefaultClient(httpExecutorService));
257+
client = buildDefaultClient(httpExecutorService);
265258
}
259+
return new Gemini(modelName, client, /* clientSendsTrackingHeaders= */ true);
266260
}
267261
}
268262

@@ -292,6 +286,9 @@ private Flowable<LlmResponse> sendRequest(
292286
logger.trace("Request Contents: {}", llmRequest.contents());
293287
logger.trace("Request Config: {}", config);
294288

289+
if (!clientSendsTrackingHeaders) {
290+
config = withTrackingHeaders(config);
291+
}
295292
if (stream) {
296293
logger.debug("Sending streaming generateContent request to model {}", effectiveModelName);
297294
CompletableFuture<ResponseStream<GenerateContentResponse>> streamFuture =
@@ -316,6 +313,23 @@ private Flowable<LlmResponse> sendRequest(
316313
}
317314
}
318315

316+
/**
317+
* Returns {@code config} with ADK's tracking tag merged into its request headers, which the genai
318+
* SDK appends to the client's own.
319+
*/
320+
private static GenerateContentConfig withTrackingHeaders(@Nullable GenerateContentConfig config) {
321+
GenerateContentConfig base = config != null ? config : GenerateContentConfig.builder().build();
322+
HttpOptions httpOptions = base.httpOptions().orElseGet(() -> HttpOptions.builder().build());
323+
return base.toBuilder()
324+
.httpOptions(
325+
httpOptions.toBuilder()
326+
.headers(
327+
TrackingHeaders.mergeIntoGenAiSdkHeaders(
328+
httpOptions.headers().orElse(ImmutableMap.of())))
329+
.build())
330+
.build();
331+
}
332+
319333
private static LlmResponse withCacheMetadata(
320334
LlmResponse response, @Nullable CacheMetadata cacheMetadata) {
321335
return cacheMetadata == null
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
/*
2+
* Copyright 2026 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.google.adk.models;
18+
19+
import static com.google.common.base.StandardSystemProperty.JAVA_VERSION;
20+
import static com.google.common.collect.ImmutableSet.toImmutableSet;
21+
import static java.util.stream.Collectors.joining;
22+
23+
import com.google.adk.Version;
24+
import com.google.common.base.Ascii;
25+
import com.google.common.base.Splitter;
26+
import com.google.common.collect.ImmutableMap;
27+
import com.google.common.collect.ImmutableSet;
28+
import java.util.LinkedHashMap;
29+
import java.util.List;
30+
import java.util.Map;
31+
import java.util.stream.Stream;
32+
33+
/**
34+
* Headers that let Google APIs attribute model calls to ADK, in the format all ADK SDKs use. A
35+
* header value is a space-separated list of {@code name/version} tags.
36+
*/
37+
final class TrackingHeaders {
38+
39+
private static final Splitter TAG_SPLITTER = Splitter.on(' ').omitEmptyStrings();
40+
41+
/** ADK's own tag. */
42+
static final String ADK_TAG = "google-adk/" + Version.JAVA_ADK_VERSION;
43+
44+
/** ADK's tag followed by the Java version tag, for clients that send no language tag. */
45+
static final String VALUE = ADK_TAG + " gl-java/" + JAVA_VERSION.value();
46+
47+
static final ImmutableMap<String, String> HEADERS =
48+
ImmutableMap.of("x-goog-api-client", VALUE, "user-agent", VALUE);
49+
50+
/**
51+
* Headers for a genai SDK client, which puts its own {@code gl-java} tag first. Both carry only
52+
* {@link #ADK_TAG}.
53+
*/
54+
static final ImmutableMap<String, String> GENAI_SDK_HEADERS =
55+
ImmutableMap.of("x-goog-api-client", ADK_TAG, "user-agent", ADK_TAG);
56+
57+
/**
58+
* Like {@link #mergeInto}, but adds only {@link #ADK_TAG}, for headers that the genai SDK appends
59+
* to its own label.
60+
*/
61+
static ImmutableMap<String, String> mergeIntoGenAiSdkHeaders(Map<String, String> headers) {
62+
return mergeInto(headers, ADK_TAG);
63+
}
64+
65+
/**
66+
* Returns {@code headers} with each tracking header, matched case-insensitively, followed by
67+
* every tag of {@link #VALUE} whose name it lacks.
68+
*/
69+
static ImmutableMap<String, String> mergeInto(Map<String, String> headers) {
70+
return mergeInto(headers, VALUE);
71+
}
72+
73+
private static ImmutableMap<String, String> mergeInto(
74+
Map<String, String> headers, String adkTags) {
75+
Map<String, String> merged = new LinkedHashMap<>();
76+
Map<String, String> callerValues = new LinkedHashMap<>();
77+
headers.forEach(
78+
(name, value) -> {
79+
String trackingName = Ascii.toLowerCase(name);
80+
if (HEADERS.containsKey(trackingName)) {
81+
callerValues.merge(trackingName, value, (first, second) -> first + " " + second);
82+
} else {
83+
merged.put(name, value);
84+
}
85+
});
86+
for (String name : HEADERS.keySet()) {
87+
merged.put(name, extend(callerValues.getOrDefault(name, ""), adkTags));
88+
}
89+
return ImmutableMap.copyOf(merged);
90+
}
91+
92+
/** Returns {@code callerValue} followed by each tag of {@code adkTags} whose name it lacks. */
93+
private static String extend(String callerValue, String adkTags) {
94+
List<String> tags = TAG_SPLITTER.splitToList(callerValue);
95+
ImmutableSet<String> names =
96+
tags.stream().map(TrackingHeaders::tagName).collect(toImmutableSet());
97+
return Stream.concat(
98+
tags.stream(),
99+
TAG_SPLITTER.splitToStream(adkTags).filter(tag -> !names.contains(tagName(tag))))
100+
.collect(joining(" "));
101+
}
102+
103+
private static String tagName(String tag) {
104+
int slash = tag.indexOf('/');
105+
return slash < 0 ? tag : tag.substring(0, slash);
106+
}
107+
108+
private TrackingHeaders() {}
109+
}

0 commit comments

Comments
 (0)