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 text =
+ model.stream(
+ List.of(
+ Msg.builder()
+ .role(MsgRole.USER)
+ .textContent("Hello")
+ .build()),
+ null,
+ null)
+ .flatMapIterable(ChatResponse::getContent)
+ .ofType(TextBlock.class)
+ .map(TextBlock::getText)
+ .collectList()
+ .block(Duration.ofSeconds(10));
+
+ assertEquals(List.of("Hello"), text);
+ RecordedRequest request = server.takeRequest(1, TimeUnit.SECONDS);
+ assertNotNull(request);
+ assertEquals("POST", request.getMethod());
+ assertEquals("/anthropic/v1/messages", request.getPath());
+ assertEquals(
+ apiKey == null ? List.of() : List.of(apiKey),
+ request.getHeaders().values("X-Api-Key"));
+ assertEquals(
+ authToken == null ? List.of() : List.of("Bearer " + authToken),
+ request.getHeaders().values("Authorization"));
+ assertEquals(1, server.getRequestCount());
+ }
+}
diff --git a/agentscope-extensions/agentscope-spring-boot-starters/agentscope-anthropic-spring-boot-starter/src/main/java/io/agentscope/spring/boot/anthropic/AnthropicAutoConfiguration.java b/agentscope-extensions/agentscope-spring-boot-starters/agentscope-anthropic-spring-boot-starter/src/main/java/io/agentscope/spring/boot/anthropic/AnthropicAutoConfiguration.java
index d4e5541741..90f81e7af0 100644
--- a/agentscope-extensions/agentscope-spring-boot-starters/agentscope-anthropic-spring-boot-starter/src/main/java/io/agentscope/spring/boot/anthropic/AnthropicAutoConfiguration.java
+++ b/agentscope-extensions/agentscope-spring-boot-starters/agentscope-anthropic-spring-boot-starter/src/main/java/io/agentscope/spring/boot/anthropic/AnthropicAutoConfiguration.java
@@ -56,9 +56,13 @@ public AnthropicChatModel anthropicChatModel(
}
String apiKey = trimToNull(properties.getApiKey());
+ String authToken = trimToNull(properties.getAuthToken());
AnthropicChatModel.Builder builder =
- AnthropicChatModel.builder().apiKey(apiKey).modelName(modelName).stream(
- properties.isStream());
+ AnthropicChatModel.builder()
+ .apiKey(apiKey)
+ .authToken(authToken)
+ .modelName(modelName)
+ .stream(properties.isStream());
String baseUrl = trimToNull(properties.getBaseUrl());
if (baseUrl != null) {
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 f6cbfbafbc..b17d017833 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
@@ -47,6 +47,12 @@ public class AnthropicProperties {
*/
private String apiKey;
+ /**
+ * 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.
+ */
+ private String authToken;
+
/**
* Anthropic API base URL (optional).
*/
@@ -78,6 +84,24 @@ public void setApiKey(String apiKey) {
this.apiKey = apiKey;
}
+ /**
+ * Returns the bearer token for gateway authentication.
+ *
+ * @return the token without the {@code Bearer } prefix, or null if unset
+ */
+ public String getAuthToken() {
+ return authToken;
+ }
+
+ /**
+ * Sets the bearer token for gateway authentication.
+ *
+ * @param authToken the token without the {@code Bearer } prefix
+ */
+ public void setAuthToken(String authToken) {
+ this.authToken = authToken;
+ }
+
public String getBaseUrl() {
return baseUrl;
}
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 8ff93ace98..ba51e3c94c 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
@@ -19,13 +19,20 @@
import io.agentscope.core.ReActAgent;
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 io.agentscope.core.model.GenerateOptions;
import io.agentscope.core.model.Model;
import io.agentscope.core.model.ToolSchema;
import io.agentscope.extensions.model.anthropic.AnthropicChatModel;
import io.agentscope.spring.boot.AgentscopeAutoConfiguration;
+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.Test;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
@@ -71,6 +78,95 @@ void shouldBindSupportedAnthropicProperties() {
});
}
+ @Test
+ void shouldAuthenticateGatewayRequestsWithAuthToken() throws Exception {
+ assertGatewayAuthentication(
+ contextRunner.withPropertyValues(
+ "agentscope.anthropic.auth-token=test-gateway-token"),
+ null,
+ "Bearer test-gateway-token");
+ }
+
+ @Test
+ void shouldIgnoreBlankAuthTokenAndKeepApiKeyAuthentication() throws Exception {
+ assertGatewayAuthentication(
+ contextRunner.withPropertyValues(
+ "agentscope.anthropic.api-key=test-api-key",
+ "agentscope.anthropic.auth-token= "),
+ "test-api-key",
+ null);
+ }
+
+ @Test
+ void shouldApplyAuthTokenCustomizerAfterProperties() throws Exception {
+ assertGatewayAuthentication(
+ contextRunner
+ .withPropertyValues("agentscope.anthropic.auth-token=property-token")
+ .withBean(
+ AnthropicChatModelBuilderCustomizer.class,
+ () -> builder -> builder.authToken("customized-token")),
+ null,
+ "Bearer customized-token");
+ }
+
+ private void assertGatewayAuthentication(
+ ApplicationContextRunner runner, String expectedApiKey, String expectedAuthorization)
+ 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}
+ }
+ """));
+
+ runner.withPropertyValues(
+ "agentscope.model.provider=anthropic",
+ "agentscope.anthropic.base-url=" + server.url("/anthropic/"),
+ "agentscope.anthropic.stream=false")
+ .run(
+ context -> {
+ assertThat(context).hasSingleBean(AnthropicChatModel.class);
+ AnthropicChatModel model =
+ context.getBean(AnthropicChatModel.class);
+ List responses =
+ model.stream(
+ List.of(
+ Msg.builder()
+ .role(MsgRole.USER)
+ .textContent("Hello")
+ .build()),
+ null,
+ null)
+ .collectList()
+ .block(Duration.ofSeconds(10));
+
+ assertThat(responses).hasSize(1);
+ assertThat(responses.get(0).getContent())
+ .singleElement()
+ .isInstanceOfSatisfying(
+ TextBlock.class,
+ text ->
+ assertThat(text.getText())
+ .isEqualTo("Hello"));
+ RecordedRequest request = server.takeRequest(1, TimeUnit.SECONDS);
+ assertThat(request).isNotNull();
+ assertThat(request.getHeader("Authorization"))
+ .isEqualTo(expectedAuthorization);
+ assertThat(request.getHeader("X-Api-Key"))
+ .isEqualTo(expectedApiKey);
+ });
+ }
+ }
+
@Test
void shouldNotCreateAnthropicModelWhenProviderIsDifferent() {
contextRunner
diff --git a/docs/v2/en/integration/model/anthropic.md b/docs/v2/en/integration/model/anthropic.md
index 1a008d2bf9..596f5ac58a 100644
--- a/docs/v2/en/integration/model/anthropic.md
+++ b/docs/v2/en/integration/model/anthropic.md
@@ -37,6 +37,24 @@ AnthropicChatModel model = AnthropicChatModel.builder()
.build();
```
+### Bearer token authentication
+
+For an Anthropic-compatible gateway that requires `Authorization: Bearer `, set
+`authToken` on the model builder:
+
+```java
+AnthropicChatModel model = AnthropicChatModel.builder()
+ .baseUrl("https://gateway.example.com/anthropic")
+ .authToken(System.getenv("ANTHROPIC_AUTH_TOKEN"))
+ .modelName("claude-sonnet-4.5")
+ .build();
+```
+
+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.
+Configure authentication through the builder rather than `GenerateOptions.additionalHeaders`,
+because the SDK owns these authentication headers.
+
## Spring Boot
Spring Boot applications can use the Anthropic starter:
@@ -49,4 +67,19 @@ Spring Boot applications can use the Anthropic starter:
```
+To use a gateway with bearer token authentication:
+
+```yaml
+agentscope:
+ model:
+ provider: anthropic
+ anthropic:
+ base-url: https://gateway.example.com/anthropic
+ auth-token: ${ANTHROPIC_AUTH_TOKEN}
+ model-name: claude-sonnet-4.5
+```
+
+`agentscope.anthropic.auth-token` is optional. An unset or blank value leaves bearer
+authentication disabled. Existing `agentscope.anthropic.api-key` configuration remains supported.
+
Full builder options, formatters, credentials, and registry context details are covered in [Model](../../docs/building-blocks/model.md).
diff --git a/docs/v2/zh/integration/model/anthropic.md b/docs/v2/zh/integration/model/anthropic.md
index 5c880e1d71..394dbd86bc 100644
--- a/docs/v2/zh/integration/model/anthropic.md
+++ b/docs/v2/zh/integration/model/anthropic.md
@@ -37,6 +37,23 @@ AnthropicChatModel model = AnthropicChatModel.builder()
.build();
```
+### Bearer Token 鉴权
+
+对于需要 `Authorization: Bearer ` 的 Anthropic 兼容网关,通过模型 builder 设置
+`authToken`:
+
+```java
+AnthropicChatModel model = AnthropicChatModel.builder()
+ .baseUrl("https://gateway.example.com/anthropic")
+ .authToken(System.getenv("ANTHROPIC_AUTH_TOKEN"))
+ .modelName("claude-sonnet-4.5")
+ .build();
+```
+
+传入的 Token 不需要包含 `Bearer ` 前缀,SDK 会自动添加。`apiKey` 设置 `X-Api-Key`,
+`authToken` 设置 `Authorization`;同时配置时,两种请求头都会发送。
+这些鉴权请求头由 SDK 管理,请通过 builder 配置,不要通过 `GenerateOptions.additionalHeaders` 添加。
+
## Spring Boot
Spring Boot 应用可以使用 Anthropic starter:
@@ -49,4 +66,19 @@ Spring Boot 应用可以使用 Anthropic starter:
```
+通过 Bearer Token 接入网关的配置示例:
+
+```yaml
+agentscope:
+ model:
+ provider: anthropic
+ anthropic:
+ base-url: https://gateway.example.com/anthropic
+ auth-token: ${ANTHROPIC_AUTH_TOKEN}
+ model-name: claude-sonnet-4.5
+```
+
+`agentscope.anthropic.auth-token` 为可选配置,未设置或为空白时不启用 Bearer 鉴权。
+原有的 `agentscope.anthropic.api-key` 配置仍然可用。
+
完整 builder 选项、formatter、credential 和 registry context 细节见 [模型](../../docs/building-blocks/model.md)。