diff --git a/astrbot/core/provider/sources/anthropic_source.py b/astrbot/core/provider/sources/anthropic_source.py index 20a3a813d5..19762282fb 100644 --- a/astrbot/core/provider/sources/anthropic_source.py +++ b/astrbot/core/provider/sources/anthropic_source.py @@ -112,11 +112,17 @@ def _init_api_key(self, provider_config: dict) -> None: self.chosen_api_key: str = "" self.api_keys: list = super().get_keys() self.chosen_api_key = self.api_keys[0] if len(self.api_keys) > 0 else "" + # SDK 2.x uses Bearer auth; compatible endpoints may require X-Api-Key. + default_headers = {"X-Api-Key": self.chosen_api_key} + for name, value in (self.custom_headers or {}).items(): + # Match older SDK casing to avoid duplicate API key headers. + name = "X-Api-Key" if name.lower() == "x-api-key" else name + default_headers[name] = value self.client = AsyncAnthropic( api_key=self.chosen_api_key, timeout=self.timeout, base_url=self.base_url, - default_headers=self.custom_headers, + default_headers=default_headers, http_client=self._create_http_client(provider_config), ) diff --git a/tests/test_anthropic_kimi_code_provider.py b/tests/test_anthropic_kimi_code_provider.py index d67fd56fd5..69b7431bb2 100644 --- a/tests/test_anthropic_kimi_code_provider.py +++ b/tests/test_anthropic_kimi_code_provider.py @@ -77,6 +77,7 @@ def test_anthropic_provider_passes_custom_headers_via_default_headers(monkeypatc assert provider.client.kwargs["default_headers"] == { "User-Agent": "custom-agent/1.0", "X-Test-Header": "123", + "X-Api-Key": "test-key", } assert provider.client.kwargs["http_client"] is None @@ -103,6 +104,7 @@ def test_kimi_code_provider_sets_defaults_and_preserves_custom_headers(monkeypat assert provider.client.kwargs["default_headers"] == { "User-Agent": kimi_code_source.KIMI_CODE_USER_AGENT, "X-Trace-Id": "trace-1", + "X-Api-Key": "test-key", } diff --git a/tests/test_anthropic_source.py b/tests/test_anthropic_source.py index 38d059aaad..60f4965382 100644 --- a/tests/test_anthropic_source.py +++ b/tests/test_anthropic_source.py @@ -20,6 +20,45 @@ def _provider() -> ProviderAnthropic: return ProviderAnthropic.__new__(ProviderAnthropic) +@pytest.mark.asyncio +@pytest.mark.parametrize( + ("custom_headers", "expected_key"), + [({}, "test-key"), ({"x-api-key": "override-key"}, "override-key")], +) +async def test_anthropic_request_sends_single_api_key_header( + monkeypatch, custom_headers, expected_key +): + monkeypatch.delenv("ANTHROPIC_AUTH_TOKEN", raising=False) + monkeypatch.delenv("ANTHROPIC_CUSTOM_HEADERS", raising=False) + + def handle(request): + assert request.headers.get_list("x-api-key") == [expected_key] + return sdk_httpx.Response(200, json={}) + + async with sdk_httpx.AsyncClient( + transport=sdk_httpx.MockTransport(handle) + ) as http_client: + monkeypatch.setattr( + ProviderAnthropic, + "_create_http_client", + lambda self, config: http_client, + ) + provider = ProviderAnthropic( + provider_config={ + "key": ["test-key"], + "api_base": "https://opencode.ai/zen/go", + "custom_headers": custom_headers, + }, + provider_settings={}, + ) + provider.client.max_retries = 0 + await provider.client.messages.with_raw_response.create( + model="claude-test", + max_tokens=16, + messages=[{"role": "user", "content": "hi"}], + ) + + def test_anthropic_extract_usage_counts_cache_creation_input(): provider = _provider()