From be2d7dea79493c47bff085c700581e6a1e270891 Mon Sep 17 00:00:00 2001 From: qiyu-lu <1186387362@qq.com> Date: Tue, 8 Sep 2026 14:16:26 +0800 Subject: [PATCH 1/4] feat(anthropic): support bearer token authentication --- .../model/anthropic/AnthropicChatModel.java | 58 ++++++ .../AnthropicChatModelAuthenticationTest.java | 166 ++++++++++++++++++ .../anthropic/AnthropicAutoConfiguration.java | 8 +- .../boot/anthropic/AnthropicProperties.java | 24 +++ .../AnthropicAutoConfigurationTest.java | 96 ++++++++++ docs/v2/en/integration/model/anthropic.md | 33 ++++ docs/v2/zh/integration/model/anthropic.md | 32 ++++ 7 files changed, 415 insertions(+), 2 deletions(-) create mode 100644 agentscope-extensions/agentscope-extensions-model/agentscope-extensions-model-anthropic/src/test/java/io/agentscope/extensions/model/anthropic/AnthropicChatModelAuthenticationTest.java diff --git a/agentscope-extensions/agentscope-extensions-model/agentscope-extensions-model-anthropic/src/main/java/io/agentscope/extensions/model/anthropic/AnthropicChatModel.java b/agentscope-extensions/agentscope-extensions-model/agentscope-extensions-model-anthropic/src/main/java/io/agentscope/extensions/model/anthropic/AnthropicChatModel.java index c073acdbc3..baf1714c37 100644 --- a/agentscope-extensions/agentscope-extensions-model/agentscope-extensions-model-anthropic/src/main/java/io/agentscope/extensions/model/anthropic/AnthropicChatModel.java +++ b/agentscope-extensions/agentscope-extensions-model/agentscope-extensions-model-anthropic/src/main/java/io/agentscope/extensions/model/anthropic/AnthropicChatModel.java @@ -96,6 +96,44 @@ public AnthropicChatModel( AnthropicBaseFormatter formatter, ProxyConfig proxyConfig, String cacheTtl) { + this( + baseUrl, + apiKey, + null, + modelName, + streamEnabled, + defaultOptions, + formatter, + proxyConfig, + cacheTtl); + } + + /** + * Creates an Anthropic chat model with optional bearer token authentication. + * + *
When both {@code apiKey} and {@code authToken} are configured, the SDK sends both + * {@code X-Api-Key} and {@code Authorization} headers. + * + * @param baseUrl the base URL for the Anthropic API (null for default) + * @param apiKey the API key for authentication (null to omit) + * @param authToken the bearer token without the {@code Bearer } prefix (null to omit) + * @param modelName the model name to use + * @param streamEnabled whether streaming should be enabled + * @param defaultOptions default generation options + * @param formatter the message formatter to use (null for the default formatter) + * @param proxyConfig the proxy configuration (null for no proxy) + * @param cacheTtl the TTL for prompt-caching markers (null for default 5m) + */ + public AnthropicChatModel( + String baseUrl, + String apiKey, + String authToken, + String modelName, + boolean streamEnabled, + GenerateOptions defaultOptions, + AnthropicBaseFormatter formatter, + ProxyConfig proxyConfig, + String cacheTtl) { this.baseUrl = baseUrl; this.apiKey = apiKey; this.modelName = modelName; @@ -112,6 +150,10 @@ public AnthropicChatModel( clientBuilder.apiKey(apiKey); } + if (authToken != null) { + clientBuilder.authToken(authToken); + } + if (baseUrl != null) { clientBuilder.baseUrl(baseUrl); } @@ -282,6 +324,7 @@ public static Builder builder() { public static class Builder { private String baseUrl; private String apiKey; + private String authToken; private String modelName = "claude-sonnet-4-5-20250929"; private boolean streamEnabled = true; private GenerateOptions defaultOptions; @@ -312,6 +355,20 @@ public Builder apiKey(String apiKey) { return this; } + /** + * Sets the bearer token for authentication with an Anthropic-compatible gateway. + * + *
The SDK adds the {@code Bearer } prefix to the {@code Authorization} header. If an
+ * API key is also configured, the SDK sends both authentication headers.
+ *
+ * @param authToken the token without the {@code Bearer } prefix (null to omit)
+ * @return this builder
+ */
+ public Builder authToken(String authToken) {
+ this.authToken = authToken;
+ return this;
+ }
+
/**
* Sets the model name.
*
@@ -397,6 +454,7 @@ public AnthropicChatModel build() {
new AnthropicChatModel(
baseUrl,
apiKey,
+ authToken,
modelName,
streamEnabled,
defaultOptions,
diff --git a/agentscope-extensions/agentscope-extensions-model/agentscope-extensions-model-anthropic/src/test/java/io/agentscope/extensions/model/anthropic/AnthropicChatModelAuthenticationTest.java b/agentscope-extensions/agentscope-extensions-model/agentscope-extensions-model-anthropic/src/test/java/io/agentscope/extensions/model/anthropic/AnthropicChatModelAuthenticationTest.java
new file mode 100644
index 0000000000..4b285f6d62
--- /dev/null
+++ b/agentscope-extensions/agentscope-extensions-model/agentscope-extensions-model-anthropic/src/test/java/io/agentscope/extensions/model/anthropic/AnthropicChatModelAuthenticationTest.java
@@ -0,0 +1,166 @@
+/*
+ * Copyright 2024-2026 the original author or authors.
+ *
+ * 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 io.agentscope.extensions.model.anthropic;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+import io.agentscope.core.message.Msg;
+import io.agentscope.core.message.MsgRole;
+import io.agentscope.core.message.TextBlock;
+import io.agentscope.core.model.ChatResponse;
+import java.io.IOException;
+import java.time.Duration;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+import okhttp3.mockwebserver.MockResponse;
+import okhttp3.mockwebserver.MockWebServer;
+import okhttp3.mockwebserver.RecordedRequest;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Tag;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.CsvSource;
+
+/** Verifies authentication headers through the real SDK against a local HTTP server. */
+@Tag("integration")
+class AnthropicChatModelAuthenticationTest {
+
+ private static final String MESSAGE_RESPONSE =
+ """
+ {
+ "id": "msg_gateway", "type": "message", "role": "assistant",
+ "model": "claude-sonnet-4.5",
+ "content": [{"type": "text", "text": "Hello"}],
+ "stop_reason": "end_turn", "stop_sequence": null,
+ "usage": {"input_tokens": 1, "output_tokens": 1}
+ }
+ """;
+
+ private static final String STREAM_RESPONSE =
+ """
+ event: message_start
+ data: {"type":"message_start","message":{"id":"msg_gateway","type":"message","role":"assistant","model":"claude-sonnet-4.5","content":[],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":1,"output_tokens":0}}}
+
+ event: content_block_start
+ data: {"type":"content_block_start","index":0,"content_block":{"type":"text","text":""}}
+
+ event: content_block_delta
+ data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"Hello"}}
+
+ event: content_block_stop
+ data: {"type":"content_block_stop","index":0}
+
+ event: message_delta
+ data: {"type":"message_delta","delta":{"stop_reason":"end_turn","stop_sequence":null},"usage":{"output_tokens":1}}
+
+ event: message_stop
+ data: {"type":"message_stop"}
+
+ """;
+
+ private MockWebServer server;
+
+ @BeforeEach
+ void setUp() throws IOException {
+ server = new MockWebServer();
+ server.start();
+ }
+
+ @AfterEach
+ void tearDown() throws IOException {
+ server.close();
+ }
+
+ @ParameterizedTest
+ @CsvSource({
+ "false, , test-gateway-token",
+ "true, , test-gateway-token",
+ "false, test-api-key, ",
+ "true, test-api-key, ",
+ "false, test-api-key, test-gateway-token",
+ "true, test-api-key, test-gateway-token"
+ })
+ void shouldSendConfiguredAuthenticationHeaders(
+ boolean streaming, String apiKey, String authToken) throws Exception {
+ AnthropicChatModel model =
+ AnthropicChatModel.builder()
+ .baseUrl(server.url("/anthropic/").toString())
+ .apiKey(apiKey)
+ .authToken(authToken)
+ .modelName("claude-sonnet-4.5")
+ .stream(streaming)
+ .build();
+
+ assertExchange(model, streaming, apiKey, authToken);
+ }
+
+ @Test
+ void shouldPreserveApiKeyAuthenticationWithExistingConstructor() throws Exception {
+ AnthropicChatModel model =
+ new AnthropicChatModel(
+ server.url("/anthropic/").toString(),
+ "test-api-key",
+ "claude-sonnet-4.5",
+ false,
+ null,
+ null,
+ null,
+ null);
+
+ assertExchange(model, false, "test-api-key", null);
+ }
+
+ private void assertExchange(
+ AnthropicChatModel model, boolean streaming, String apiKey, String authToken)
+ throws Exception {
+ server.enqueue(
+ new MockResponse()
+ .setHeader(
+ "Content-Type",
+ streaming ? "text/event-stream" : "application/json")
+ .setBody(streaming ? STREAM_RESPONSE : MESSAGE_RESPONSE));
+
+ List When both {@code apiKey} and {@code authToken} are configured, the SDK sends both
- * {@code X-Api-Key} and {@code Authorization} headers.
+ * {@code apiKey} and {@code authToken} are mutually exclusive.
*
* @param baseUrl the base URL for the Anthropic API (null for default)
* @param apiKey the API key for authentication (null to omit)
@@ -123,6 +122,7 @@ public AnthropicChatModel(
* @param formatter the message formatter to use (null for the default formatter)
* @param proxyConfig the proxy configuration (null for no proxy)
* @param cacheTtl the TTL for prompt-caching markers (null for default 5m)
+ * @throws IllegalArgumentException if both API key and bearer token are configured
*/
public AnthropicChatModel(
String baseUrl,
@@ -134,6 +134,10 @@ public AnthropicChatModel(
AnthropicBaseFormatter formatter,
ProxyConfig proxyConfig,
String cacheTtl) {
+ if (apiKey != null && authToken != null) {
+ throw new IllegalArgumentException(
+ "apiKey and authToken are mutually exclusive; configure only one credential");
+ }
this.baseUrl = baseUrl;
this.apiKey = apiKey;
this.modelName = modelName;
@@ -358,8 +362,8 @@ public Builder apiKey(String apiKey) {
/**
* Sets the bearer token for authentication with an Anthropic-compatible gateway.
*
- * The SDK adds the {@code Bearer } prefix to the {@code Authorization} header. If an
- * API key is also configured, the SDK sends both authentication headers.
+ * The SDK adds the {@code Bearer } prefix to the {@code Authorization} header.
+ * Configuring both an API key and a bearer token causes model construction to fail.
*
* @param authToken the token without the {@code Bearer } prefix (null to omit)
* @return this builder
diff --git a/agentscope-extensions/agentscope-extensions-model/agentscope-extensions-model-anthropic/src/test/java/io/agentscope/extensions/model/anthropic/AnthropicChatModelAuthenticationTest.java b/agentscope-extensions/agentscope-extensions-model/agentscope-extensions-model-anthropic/src/test/java/io/agentscope/extensions/model/anthropic/AnthropicChatModelAuthenticationTest.java
index 4b285f6d62..9cdd747a39 100644
--- a/agentscope-extensions/agentscope-extensions-model/agentscope-extensions-model-anthropic/src/test/java/io/agentscope/extensions/model/anthropic/AnthropicChatModelAuthenticationTest.java
+++ b/agentscope-extensions/agentscope-extensions-model/agentscope-extensions-model-anthropic/src/test/java/io/agentscope/extensions/model/anthropic/AnthropicChatModelAuthenticationTest.java
@@ -16,7 +16,9 @@
package io.agentscope.extensions.model.anthropic;
import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
import io.agentscope.core.message.Msg;
import io.agentscope.core.message.MsgRole;
@@ -91,9 +93,7 @@ void tearDown() throws IOException {
"false, , test-gateway-token",
"true, , test-gateway-token",
"false, test-api-key, ",
- "true, test-api-key, ",
- "false, test-api-key, test-gateway-token",
- "true, test-api-key, test-gateway-token"
+ "true, test-api-key, "
})
void shouldSendConfiguredAuthenticationHeaders(
boolean streaming, String apiKey, String authToken) throws Exception {
@@ -125,6 +125,49 @@ void shouldPreserveApiKeyAuthenticationWithExistingConstructor() throws Exceptio
assertExchange(model, false, "test-api-key", null);
}
+ @Test
+ void shouldRejectBothCredentialsWithoutExposingTheirValues() {
+ IllegalArgumentException builderError =
+ assertThrows(
+ IllegalArgumentException.class,
+ () ->
+ AnthropicChatModel.builder()
+ .apiKey("secret-api-key")
+ .authToken("secret-bearer-token")
+ .build());
+ IllegalArgumentException constructorError =
+ assertThrows(
+ IllegalArgumentException.class,
+ () ->
+ new AnthropicChatModel(
+ null,
+ "secret-api-key",
+ "secret-bearer-token",
+ "claude-sonnet-4.5",
+ false,
+ null,
+ null,
+ null,
+ null));
+
+ for (IllegalArgumentException error : List.of(builderError, constructorError)) {
+ assertEquals(
+ "apiKey and authToken are mutually exclusive; configure only one credential",
+ error.getMessage());
+ assertFalse(error.toString().contains("secret-api-key"));
+ assertFalse(error.toString().contains("secret-bearer-token"));
+ }
+ assertEquals(0, server.getRequestCount());
+ }
+
+ @Test
+ void shouldNotExposeBearerTokenInModelOrBuilderToString() {
+ AnthropicChatModel.Builder builder =
+ AnthropicChatModel.builder().authToken("secret-bearer-token");
+ assertFalse(builder.toString().contains("secret-bearer-token"));
+ assertFalse(builder.build().toString().contains("secret-bearer-token"));
+ }
+
private void assertExchange(
AnthropicChatModel model, boolean streaming, String apiKey, String authToken)
throws Exception {
diff --git a/agentscope-extensions/agentscope-spring-boot-starters/agentscope-anthropic-spring-boot-starter/src/main/java/io/agentscope/spring/boot/anthropic/AnthropicProperties.java b/agentscope-extensions/agentscope-spring-boot-starters/agentscope-anthropic-spring-boot-starter/src/main/java/io/agentscope/spring/boot/anthropic/AnthropicProperties.java
index b17d017833..597c4a1c06 100644
--- a/agentscope-extensions/agentscope-spring-boot-starters/agentscope-anthropic-spring-boot-starter/src/main/java/io/agentscope/spring/boot/anthropic/AnthropicProperties.java
+++ b/agentscope-extensions/agentscope-spring-boot-starters/agentscope-anthropic-spring-boot-starter/src/main/java/io/agentscope/spring/boot/anthropic/AnthropicProperties.java
@@ -49,7 +49,7 @@ public class AnthropicProperties {
/**
* Bearer token for an Anthropic-compatible gateway, without the {@code Bearer } prefix.
- * When an API key is also configured, the SDK sends both authentication headers.
+ * Mutually exclusive with the API key; configuring both causes model construction to fail.
*/
private String authToken;
diff --git a/agentscope-extensions/agentscope-spring-boot-starters/agentscope-anthropic-spring-boot-starter/src/test/java/io/agentscope/spring/boot/anthropic/AnthropicAutoConfigurationTest.java b/agentscope-extensions/agentscope-spring-boot-starters/agentscope-anthropic-spring-boot-starter/src/test/java/io/agentscope/spring/boot/anthropic/AnthropicAutoConfigurationTest.java
index ba51e3c94c..8856bd1336 100644
--- a/agentscope-extensions/agentscope-spring-boot-starters/agentscope-anthropic-spring-boot-starter/src/test/java/io/agentscope/spring/boot/anthropic/AnthropicAutoConfigurationTest.java
+++ b/agentscope-extensions/agentscope-spring-boot-starters/agentscope-anthropic-spring-boot-starter/src/test/java/io/agentscope/spring/boot/anthropic/AnthropicAutoConfigurationTest.java
@@ -109,6 +109,49 @@ void shouldApplyAuthTokenCustomizerAfterProperties() throws Exception {
"Bearer customized-token");
}
+ @Test
+ void shouldRejectBothCredentialsWithoutExposingTheirValues() {
+ contextRunner
+ .withPropertyValues(
+ "agentscope.model.provider=anthropic",
+ "agentscope.anthropic.api-key=secret-api-key",
+ "agentscope.anthropic.auth-token=secret-bearer-token")
+ .run(
+ context -> {
+ assertThat(context).hasFailed();
+ assertThat(context.getStartupFailure())
+ .hasRootCauseInstanceOf(IllegalArgumentException.class)
+ .hasStackTraceContaining(
+ "apiKey and authToken are mutually exclusive");
+ assertThat(context.getStartupFailure())
+ .hasStackTraceContaining("configure only one credential");
+ assertThat(context.getStartupFailure().toString())
+ .doesNotContain("secret-api-key", "secret-bearer-token");
+ });
+ }
+
+ @Test
+ void shouldAllowCustomizerToResolveConflictingCredentials() throws Exception {
+ assertGatewayAuthentication(
+ contextRunner
+ .withPropertyValues(
+ "agentscope.anthropic.api-key=test-api-key",
+ "agentscope.anthropic.auth-token=test-gateway-token")
+ .withBean(
+ AnthropicChatModelBuilderCustomizer.class,
+ () -> builder -> builder.apiKey(null)),
+ null,
+ "Bearer test-gateway-token");
+ }
+
+ @Test
+ void shouldNotExposeCredentialsInPropertiesToString() {
+ AnthropicProperties properties = new AnthropicProperties();
+ properties.setApiKey("secret-api-key");
+ properties.setAuthToken("secret-bearer-token");
+ assertThat(properties.toString()).doesNotContain("secret-api-key", "secret-bearer-token");
+ }
+
private void assertGatewayAuthentication(
ApplicationContextRunner runner, String expectedApiKey, String expectedAuthorization)
throws Exception {
diff --git a/docs/v2/en/integration/model/anthropic.md b/docs/v2/en/integration/model/anthropic.md
index 882993292d..96b96f692f 100644
--- a/docs/v2/en/integration/model/anthropic.md
+++ b/docs/v2/en/integration/model/anthropic.md
@@ -53,7 +53,8 @@ AnthropicChatModel model = AnthropicChatModel.builder()
```
Pass the token without the `Bearer ` prefix; the SDK adds it. `apiKey` sets `X-Api-Key`,
-while `authToken` sets `Authorization`. If both are configured, both headers are sent.
+while `authToken` sets `Authorization`. Configure only one: setting both causes model
+construction to fail with an `IllegalArgumentException`.
Configure authentication through the builder rather than `GenerateOptions.additionalHeaders`,
because the SDK owns these authentication headers.
@@ -82,6 +83,8 @@ agentscope:
```
`agentscope.anthropic.auth-token` is optional. An unset or blank value leaves bearer
-authentication disabled. Existing `agentscope.anthropic.api-key` configuration remains supported.
+authentication disabled. Existing `agentscope.anthropic.api-key` configuration remains supported,
+but configuring both nonblank credentials causes startup to fail. Builder customizers run before
+validation and can clear a credential with `apiKey(null)` or `authToken(null)`.
Full builder options, formatters, credentials, and registry context details are covered in [Model](/v2/en/docs/building-blocks/model).
diff --git a/docs/v2/zh/integration/model/anthropic.md b/docs/v2/zh/integration/model/anthropic.md
index e14d5d1762..7553692e86 100644
--- a/docs/v2/zh/integration/model/anthropic.md
+++ b/docs/v2/zh/integration/model/anthropic.md
@@ -53,7 +53,8 @@ AnthropicChatModel model = AnthropicChatModel.builder()
```
传入的 Token 不需要包含 `Bearer ` 前缀,SDK 会自动添加。`apiKey` 设置 `X-Api-Key`,
-`authToken` 设置 `Authorization`;同时配置时,两种请求头都会发送。
+`authToken` 设置 `Authorization`;两者只能配置一个,同时配置会在创建模型时抛出
+`IllegalArgumentException`。
这些鉴权请求头由 SDK 管理,请通过 builder 配置,不要通过 `GenerateOptions.additionalHeaders` 添加。
## Spring Boot
@@ -81,6 +82,7 @@ agentscope:
```
`agentscope.anthropic.auth-token` 为可选配置,未设置或为空白时不启用 Bearer 鉴权。
-原有的 `agentscope.anthropic.api-key` 配置仍然可用。
+原有的 `agentscope.anthropic.api-key` 配置仍然可用,但同时配置两个非空白凭据会导致启动失败。
+Builder customizer 在校验前执行,可以通过 `apiKey(null)` 或 `authToken(null)` 清除其中一种凭据。
完整 builder 选项、formatter、credential 和 registry context 细节见 [模型](/v2/zh/docs/building-blocks/model)。
From 6bf1a3a40f3d2ae9a97f640bd42215cf1c49310c Mon Sep 17 00:00:00 2001
From: jujn <2087687391@qq.com>
Date: Sat, 12 Sep 2026 18:37:56 +0800
Subject: [PATCH 3/4] fix: improve
---
.../model/anthropic/AnthropicChatModel.java | 15 ----
.../anthropic/AnthropicModelProvider.java | 30 +++++--
.../anthropic/AnthropicModelProviderTest.java | 90 +++++++++++++++++++
3 files changed, 115 insertions(+), 20 deletions(-)
diff --git a/agentscope-extensions/agentscope-extensions-model/agentscope-extensions-model-anthropic/src/main/java/io/agentscope/extensions/model/anthropic/AnthropicChatModel.java b/agentscope-extensions/agentscope-extensions-model/agentscope-extensions-model-anthropic/src/main/java/io/agentscope/extensions/model/anthropic/AnthropicChatModel.java
index 741226d161..25c8703a01 100644
--- a/agentscope-extensions/agentscope-extensions-model/agentscope-extensions-model-anthropic/src/main/java/io/agentscope/extensions/model/anthropic/AnthropicChatModel.java
+++ b/agentscope-extensions/agentscope-extensions-model/agentscope-extensions-model-anthropic/src/main/java/io/agentscope/extensions/model/anthropic/AnthropicChatModel.java
@@ -72,21 +72,6 @@ public class AnthropicChatModel extends ChatModelBase {
private final GenerateOptions defaultOptions;
private final AnthropicBaseFormatter formatter;
- /**
- * Creates a new Anthropic chat model instance.
- *
- * @param baseUrl the base URL for Anthropic API (null for default)
- * @param apiKey the API key for authentication (null to load from
- * ANTHROPIC_API_KEY env var)
- * @param modelName the model name to use (e.g.,
- * "claude-sonnet-4-5-20250929")
- * @param streamEnabled whether streaming should be enabled
- * @param defaultOptions default generation options
- * @param formatter the message formatter to use (null for default
- * Anthropic formatter)
- * @param proxyConfig the proxy configuration (null for no proxy)
- * @param cacheTtl the TTL for prompt-caching markers (null for default 5m)
- */
public AnthropicChatModel(
String baseUrl,
String apiKey,
diff --git a/agentscope-extensions/agentscope-extensions-model/agentscope-extensions-model-anthropic/src/main/java/io/agentscope/extensions/model/anthropic/AnthropicModelProvider.java b/agentscope-extensions/agentscope-extensions-model/agentscope-extensions-model-anthropic/src/main/java/io/agentscope/extensions/model/anthropic/AnthropicModelProvider.java
index 6fa06a9530..ae7ca64400 100644
--- a/agentscope-extensions/agentscope-extensions-model/agentscope-extensions-model-anthropic/src/main/java/io/agentscope/extensions/model/anthropic/AnthropicModelProvider.java
+++ b/agentscope-extensions/agentscope-extensions-model/agentscope-extensions-model-anthropic/src/main/java/io/agentscope/extensions/model/anthropic/AnthropicModelProvider.java
@@ -15,8 +15,8 @@
*/
package io.agentscope.extensions.model.anthropic;
-import static io.agentscope.core.model.ModelProviderSupport.firstNonBlank;
import static io.agentscope.core.model.ModelProviderSupport.intOption;
+import static io.agentscope.core.model.ModelProviderSupport.stringOption;
import static io.agentscope.core.model.ModelProviderSupport.trimToNull;
import io.agentscope.core.model.GenerateOptions;
@@ -27,12 +27,20 @@
import io.agentscope.extensions.model.anthropic.formatter.AnthropicBaseFormatter;
import java.util.regex.Pattern;
-/** Anthropic provider registered through {@link java.util.ServiceLoader}. */
+/**
+ * Anthropic provider registered through {@link java.util.ServiceLoader}.
+ *
+ * Credentials come from the context's standard {@code apiKey} field or the {@code
+ * "authToken"} context option (a bearer token for Anthropic-compatible gateways; the two are
+ * mutually exclusive). When neither is set, the {@code ANTHROPIC_API_KEY} environment variable
+ * is used, then {@code ANTHROPIC_AUTH_TOKEN}.
+ */
public final class AnthropicModelProvider implements ModelProvider {
private static final String PREFIX = "anthropic:";
private static final Pattern MODEL_ID = Pattern.compile("anthropic:.+");
private static final String OPTION_CONTEXT_WINDOW_SIZE = "contextWindowSize";
+ private static final String OPTION_AUTH_TOKEN = "authToken";
@Override
public String providerId() {
@@ -55,10 +63,22 @@ public Model create(String modelId, ModelCreationContext context) {
throw new IllegalArgumentException("Unsupported Anthropic model id: " + modelId);
}
String modelName = modelId.substring(PREFIX.length());
- String apiKey = firstNonBlank(context.getApiKey(), System.getenv("ANTHROPIC_API_KEY"));
+ String apiKey = trimToNull(context.getApiKey());
+ String authToken = stringOption(context, OPTION_AUTH_TOKEN);
+ if (apiKey == null && authToken == null) {
+ // No explicit credential: fall back to the environment, keeping the historical
+ // precedence of ANTHROPIC_API_KEY over ANTHROPIC_AUTH_TOKEN.
+ apiKey = trimToNull(System.getenv("ANTHROPIC_API_KEY"));
+ if (apiKey == null) {
+ authToken = trimToNull(System.getenv("ANTHROPIC_AUTH_TOKEN"));
+ }
+ }
AnthropicChatModel.Builder builder =
- AnthropicChatModel.builder().apiKey(apiKey).modelName(modelName).stream(
- context.getStream() != null ? context.getStream() : true);
+ AnthropicChatModel.builder()
+ .apiKey(apiKey)
+ .authToken(authToken)
+ .modelName(modelName)
+ .stream(context.getStream() != null ? context.getStream() : true);
String baseUrl = trimToNull(context.getBaseUrl());
if (baseUrl != null) {
builder.baseUrl(baseUrl);
diff --git a/agentscope-extensions/agentscope-extensions-model/agentscope-extensions-model-anthropic/src/test/java/io/agentscope/extensions/model/anthropic/AnthropicModelProviderTest.java b/agentscope-extensions/agentscope-extensions-model/agentscope-extensions-model-anthropic/src/test/java/io/agentscope/extensions/model/anthropic/AnthropicModelProviderTest.java
index 16546d32bb..f53ce917cb 100644
--- a/agentscope-extensions/agentscope-extensions-model/agentscope-extensions-model-anthropic/src/test/java/io/agentscope/extensions/model/anthropic/AnthropicModelProviderTest.java
+++ b/agentscope-extensions/agentscope-extensions-model/agentscope-extensions-model-anthropic/src/test/java/io/agentscope/extensions/model/anthropic/AnthropicModelProviderTest.java
@@ -17,14 +17,23 @@
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
+import io.agentscope.core.message.Msg;
+import io.agentscope.core.message.MsgRole;
+import io.agentscope.core.model.ChatResponse;
import io.agentscope.core.model.GenerateOptions;
import io.agentscope.core.model.Model;
import io.agentscope.core.model.ModelCreationContext;
import io.agentscope.core.model.ModelRegistry;
import io.agentscope.core.model.transport.ProxyConfig;
+import java.time.Duration;
+import okhttp3.mockwebserver.MockResponse;
+import okhttp3.mockwebserver.MockWebServer;
+import okhttp3.mockwebserver.RecordedRequest;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
@@ -72,6 +81,87 @@ void createUsesModelCreationContext() {
assertEquals(200000, model.getContextWindowSize());
}
+ @Test
+ void createWithAuthTokenOptionSendsBearerHeader() throws Exception {
+ try (MockWebServer server = new MockWebServer()) {
+ server.start();
+ server.enqueue(
+ new MockResponse()
+ .setHeader("Content-Type", "application/json")
+ .setBody(
+ """
+ {
+ "id": "msg_gateway", "type": "message", "role": "assistant",
+ "model": "claude-sonnet-4.5",
+ "content": [{"type": "text", "text": "Hello"}],
+ "stop_reason": "end_turn", "stop_sequence": null,
+ "usage": {"input_tokens": 1, "output_tokens": 1}
+ }
+ """));
+
+ AnthropicModelProvider provider = new AnthropicModelProvider();
+ ModelCreationContext context =
+ ModelCreationContext.builder()
+ .baseUrl(server.url("/anthropic/").toString())
+ .stream(false)
+ .option("authToken", "gateway-token")
+ .build();
+
+ Model model = provider.create("anthropic:claude-sonnet-4.5", context);
+ java.util.List