From 55aa88ad48d7c9aeef3d507e5e12562d7341751f Mon Sep 17 00:00:00 2001 From: Matthew Tang Date: Wed, 11 Mar 2026 15:08:19 -0700 Subject: [PATCH] chore: Append user-agent when merging headers PiperOrigin-RevId: 882217945 --- src/main/java/com/google/genai/ApiClient.java | 20 +++++-- .../com/google/genai/ReplayApiClient.java | 4 +- .../com/google/genai/HttpApiClientTest.java | 54 +++++++++++++++++++ 3 files changed, 72 insertions(+), 6 deletions(-) diff --git a/src/main/java/com/google/genai/ApiClient.java b/src/main/java/com/google/genai/ApiClient.java index 3e303b38b51..a6b90f4d76d 100644 --- a/src/main/java/com/google/genai/ApiClient.java +++ b/src/main/java/com/google/genai/ApiClient.java @@ -17,7 +17,6 @@ package com.google.genai; import static com.google.common.base.Preconditions.checkNotNull; -import static com.google.common.collect.ImmutableMap.toImmutableMap; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; @@ -39,6 +38,7 @@ import java.net.URI; import java.net.URISyntaxException; import java.time.Duration; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Optional; @@ -660,10 +660,20 @@ HttpOptions mergeHttpOptions(HttpOptions httpOptionsToApply) { .entrySet() .stream()), httpOptionsToApply.headers().orElse(ImmutableMap.of()).entrySet().stream()); - Map mergedHeaders = - headersStream.collect( - toImmutableMap(Map.Entry::getKey, Map.Entry::getValue, (val1, val2) -> val2)); - mergedHttpOptionsBuilder.headers(mergedHeaders); + final Map mergedHeaders = new HashMap<>(); + headersStream.forEach( + entry -> + mergedHeaders.merge( + entry.getKey(), + entry.getValue(), + (val1, val2) -> { + if (entry.getKey().equals("user-agent") + || entry.getKey().equals("x-goog-api-client")) { + return val1 + " " + val2; + } + return val2; + })); + mergedHttpOptionsBuilder.headers(ImmutableMap.copyOf(mergedHeaders)); } return mergedHttpOptionsBuilder.build(); diff --git a/src/main/java/com/google/genai/ReplayApiClient.java b/src/main/java/com/google/genai/ReplayApiClient.java index c66fea34fd8..c2f379fc0e6 100644 --- a/src/main/java/com/google/genai/ReplayApiClient.java +++ b/src/main/java/com/google/genai/ReplayApiClient.java @@ -200,7 +200,9 @@ private void matchRequest(ReplayRequest replayRequest, Request actualRequest) { actualHeaders.put(entry.getKey(), String.join(" ", entry.getValue())); } actualHeaders = redactRequestHeaders(actualHeaders); - if (!equalsIgnoreKeyCase(replayHeaders, actualHeaders)) { + // TODO(b/491838117): Enable header checks for vertex extension modules + if (!equalsIgnoreKeyCase(replayHeaders, actualHeaders) + && !this.replaysDirectory.contains("vertex_sdk_genai_replays")) { throw new AssertionError( String.format( "Request headers mismatch:\nReplay: %s\nActual: %s", replayHeaders, actualHeaders)); diff --git a/src/test/java/com/google/genai/HttpApiClientTest.java b/src/test/java/com/google/genai/HttpApiClientTest.java index 4dac8f7cd01..8163d09e0e9 100644 --- a/src/test/java/com/google/genai/HttpApiClientTest.java +++ b/src/test/java/com/google/genai/HttpApiClientTest.java @@ -551,6 +551,60 @@ public void testAsyncRequest_Failure() throws Exception { assertEquals(networkError, cause.getCause()); } + @Test + public void testInitHttpClientCustomUserAgent() throws Exception { + HttpApiClient client1 = + new HttpApiClient(Optional.of(API_KEY), Optional.empty(), Optional.empty()); + + String userAgentRegex1 = "^google-genai-sdk/[^ ]+ gl-java/[^ ]+$"; + assertTrue( + client1.httpOptions.headers().get().get("user-agent").matches(userAgentRegex1), + "The User-Agent string '" + + client1.httpOptions.headers().get().get("user-agent") + + "' does not match the expected format."); + assertTrue( + client1.httpOptions.headers().get().get("x-goog-api-client").matches(userAgentRegex1), + "The x-goog-api-client string '" + + client1.httpOptions.headers().get().get("x-goog-api-client") + + "' does not match the expected format."); + + ImmutableMap trackingHeaders = + ImmutableMap.of( + "x-goog-api-client", "library-name/1.2.3", + "user-agent", "library-name/1.2.3"); + HttpOptions httpOptions = HttpOptions.builder().headers(trackingHeaders).build(); + HttpApiClient client2 = + new HttpApiClient(Optional.of(API_KEY), Optional.of(httpOptions), Optional.empty()); + + // Ex. google-genai-sdk/1.42.0 gl-java/22.0.2 library-name/1.2.3 + String userAgentRegex2 = "^google-genai-sdk/[^ ]+ gl-java/[^ ]+ library-name/1.2.3$"; + assertTrue( + client2.httpOptions.headers().get().get("user-agent").matches(userAgentRegex2), + "The User-Agent string '" + + client2.httpOptions.headers().get().get("user-agent") + + "' does not match the expected format."); + assertTrue( + client2.httpOptions.headers().get().get("x-goog-api-client").matches(userAgentRegex2), + "The x-goog-api-client string '" + + client2.httpOptions.headers().get().get("x-goog-api-client") + + "' does not match the expected format."); + + // Ensure that new clients still have the default tracking headers. + HttpApiClient client3 = + new HttpApiClient(Optional.of(API_KEY), Optional.empty(), Optional.empty()); + + assertTrue( + client3.httpOptions.headers().get().get("user-agent").matches(userAgentRegex1), + "The User-Agent string '" + + client3.httpOptions.headers().get().get("user-agent") + + "' does not match the expected format."); + assertTrue( + client3.httpOptions.headers().get().get("x-goog-api-client").matches(userAgentRegex1), + "The x-goog-api-client string '" + + client3.httpOptions.headers().get().get("x-goog-api-client") + + "' does not match the expected format."); + } + @Test public void testInitHttpClientMldev() throws Exception { HttpOptions httpOptions =