|
16 | 16 |
|
17 | 17 | package com.google.adk.models; |
18 | 18 |
|
| 19 | +import static com.google.common.collect.Iterables.getOnlyElement; |
19 | 20 | import static com.google.common.truth.Truth.assertThat; |
20 | 21 | import static java.nio.charset.StandardCharsets.UTF_8; |
21 | 22 | import static org.junit.Assert.assertEquals; |
|
26 | 27 | import static org.mockito.Mockito.verify; |
27 | 28 | import static org.mockito.Mockito.when; |
28 | 29 |
|
| 30 | +import com.anthropic.backends.Backend; |
29 | 31 | import com.anthropic.client.AnthropicClient; |
| 32 | +import com.anthropic.client.okhttp.AnthropicOkHttpClient; |
30 | 33 | import com.anthropic.core.JsonValue; |
31 | 34 | import com.anthropic.models.messages.ContentBlock; |
32 | 35 | import com.anthropic.models.messages.ContentBlockParam; |
|
46 | 49 | import com.fasterxml.jackson.core.type.TypeReference; |
47 | 50 | import com.google.common.collect.ImmutableList; |
48 | 51 | import com.google.common.collect.ImmutableMap; |
| 52 | +import com.google.common.net.InetAddresses; |
49 | 53 | import com.google.genai.types.Content; |
50 | 54 | import com.google.genai.types.FunctionCall; |
51 | 55 | import com.google.genai.types.FunctionDeclaration; |
52 | 56 | import com.google.genai.types.FunctionResponse; |
53 | 57 | import com.google.genai.types.Part; |
54 | 58 | import com.google.genai.types.Schema; |
| 59 | +import com.sun.net.httpserver.Headers; |
| 60 | +import com.sun.net.httpserver.HttpServer; |
| 61 | +import java.io.OutputStream; |
55 | 62 | import java.lang.reflect.Method; |
| 63 | +import java.net.InetAddress; |
| 64 | +import java.net.InetSocketAddress; |
56 | 65 | import java.util.Collections; |
57 | 66 | import java.util.List; |
58 | 67 | import java.util.Map; |
59 | 68 | import java.util.Optional; |
| 69 | +import java.util.concurrent.atomic.AtomicReference; |
60 | 70 | import org.junit.Before; |
61 | 71 | import org.junit.Test; |
62 | 72 | import org.junit.runner.RunWith; |
@@ -704,4 +714,82 @@ public void generateContent_userTurnOfOnlyEmptyText_isSentAsPlaceholder() { |
704 | 714 | assertThat(blocks).hasSize(1); |
705 | 715 | assertThat(blocks.get(0).asText().text()).isEqualTo("."); |
706 | 716 | } |
| 717 | + |
| 718 | + @Test |
| 719 | + public void generateContent_extendsTheSdkUserAgentWithAdkTrackingHeaders() throws Exception { |
| 720 | + Headers headers = headersSentThrough(AnthropicOkHttpClient.builder()); |
| 721 | + |
| 722 | + String userAgent = getOnlyElement(headers.get("user-agent")); |
| 723 | + assertThat(userAgent).startsWith("AnthropicClientImpl/Java "); |
| 724 | + assertThat(userAgent).endsWith(" " + TrackingHeaders.VALUE); |
| 725 | + assertThat(headers.get("x-goog-api-client")).containsExactly(TrackingHeaders.VALUE); |
| 726 | + } |
| 727 | + |
| 728 | + @Test |
| 729 | + public void generateContent_extendsTheCallerUserAgentWithAdkTrackingHeaders() throws Exception { |
| 730 | + Headers headers = |
| 731 | + headersSentThrough(AnthropicOkHttpClient.builder().putHeader("User-Agent", "my-app/1.0")); |
| 732 | + |
| 733 | + assertThat(headers.get("user-agent")).containsExactly("my-app/1.0 " + TrackingHeaders.VALUE); |
| 734 | + assertThat(headers.get("x-goog-api-client")).containsExactly(TrackingHeaders.VALUE); |
| 735 | + } |
| 736 | + |
| 737 | + @Test |
| 738 | + public void constructor_nullClient_doesNotThrow() { |
| 739 | + assertThat(new Claude("claude-test", (AnthropicClient) null).model()).isEqualTo("claude-test"); |
| 740 | + } |
| 741 | + |
| 742 | + private static final String MESSAGE_JSON = |
| 743 | + "{\"id\":\"msg_1\",\"type\":\"message\",\"role\":\"assistant\",\"model\":\"claude-test\"," |
| 744 | + + "\"content\":[{\"type\":\"text\",\"text\":\"pong\"}],\"stop_reason\":\"end_turn\"," |
| 745 | + + "\"stop_sequence\":null,\"usage\":{\"input_tokens\":3,\"output_tokens\":2}}"; |
| 746 | + |
| 747 | + /** |
| 748 | + * Sends one request through Claude using a client built from {@code clientBuilder} for a local |
| 749 | + * server, and returns the headers the server received. |
| 750 | + */ |
| 751 | + private static Headers headersSentThrough(AnthropicOkHttpClient.Builder clientBuilder) |
| 752 | + throws Exception { |
| 753 | + AtomicReference<Headers> requestHeaders = new AtomicReference<>(); |
| 754 | + HttpServer server = |
| 755 | + HttpServer.create(new InetSocketAddress(InetAddress.getLoopbackAddress(), 0), 0); |
| 756 | + server.createContext( |
| 757 | + "/", |
| 758 | + exchange -> { |
| 759 | + requestHeaders.set(exchange.getRequestHeaders()); |
| 760 | + byte[] body = MESSAGE_JSON.getBytes(UTF_8); |
| 761 | + exchange.getResponseHeaders().set("Content-Type", "application/json"); |
| 762 | + exchange.sendResponseHeaders(200, body.length); |
| 763 | + try (OutputStream out = exchange.getResponseBody()) { |
| 764 | + out.write(body); |
| 765 | + } |
| 766 | + }); |
| 767 | + server.start(); |
| 768 | + try { |
| 769 | + String baseUrl = |
| 770 | + "http://" |
| 771 | + + InetAddresses.toUriString(server.getAddress().getAddress()) |
| 772 | + + ":" |
| 773 | + + server.getAddress().getPort(); |
| 774 | + Backend backend = |
| 775 | + new Backend() { |
| 776 | + @Override |
| 777 | + public String baseUrl() { |
| 778 | + return baseUrl; |
| 779 | + } |
| 780 | + |
| 781 | + @Override |
| 782 | + public void close() {} |
| 783 | + }; |
| 784 | + Claude model = new Claude("claude-test", clientBuilder.backend(backend).build()); |
| 785 | + |
| 786 | + LlmResponse response = |
| 787 | + model.generateContent(request(userText("Ping")), /* stream= */ false).blockingFirst(); |
| 788 | + |
| 789 | + assertThat(response.content().get().parts().get().get(0).text()).hasValue("pong"); |
| 790 | + return requestHeaders.get(); |
| 791 | + } finally { |
| 792 | + server.stop(0); |
| 793 | + } |
| 794 | + } |
707 | 795 | } |
0 commit comments