fix: authenticate native Anthropic requests with x-api-key

The Anthropic Messages passthrough and the token-count forwarding both build their upstream request through `get_anthropic_request_target`, which sends the connection key as `Authorization: Bearer <key>`. Anthropic's OpenAI-compatible `/chat/completions` endpoint accepts that, which is why the model works in the chat UI, but the native `/v1/messages` and `/v1/messages/count_tokens` endpoints do not: they require the key in `x-api-key` and reject a bearer token with 401 `Invalid bearer token` (and `jwt auth is not yet supported on count_tokens`). They also require an `anthropic-version` header, which was never sent.

For `api.anthropic.com` connections, send `anthropic-version` and move the key into `x-api-key`, dropping the bearer header. Connections using session, OAuth or Entra ID auth keep their token untouched, LiteLLM passthrough connections are unaffected, and admin-configured custom headers still win over both defaults.

Fixes #27695
This commit is contained in:
Classic298 2026-07-29 18:47:45 +02:00
parent ab1ad7bae4
commit f8fe0cde9a
3 changed files with 16 additions and 8 deletions

View file

@ -1848,7 +1848,7 @@ async def count_message_tokens(
async def passthrough_anthropic_messages(request: Request, form_data: dict, user) -> Response | dict:
requested_model, payload, url, key, headers, cookies = await openai.get_anthropic_token_count_target(
requested_model, payload, url, key, headers, cookies = await openai.get_anthropic_request_target(
request, form_data, user
)
request_url = f'{url.rstrip("/")}/messages'

View file

@ -39,7 +39,7 @@ from open_webui.models.groups import Groups
from open_webui.models.models import Models
from open_webui.models.users import UserModel
from open_webui.utils.access_control import check_model_access, has_connection_access, has_permission
from open_webui.utils.anthropic import get_anthropic_models, is_anthropic_url
from open_webui.utils.anthropic import ANTHROPIC_VERSION, get_anthropic_models, is_anthropic_url
from open_webui.utils.auth import get_admin_user, get_verified_user
from open_webui.utils.headers import get_custom_headers, include_user_info_headers
from open_webui.utils.json_codec import JSONCodec
@ -307,8 +307,8 @@ async def get_openai_connection(idx: int) -> tuple[str, str, dict]:
return url, key, api_config
async def get_anthropic_token_count_target(request: Request, form_data: dict, user: UserModel):
"""Resolve the upstream LiteLLM connection for an Anthropic token-count request."""
async def get_anthropic_request_target(request: Request, form_data: dict, user: UserModel):
"""Resolve the upstream connection, payload and auth headers for a native Anthropic request."""
requested_model = form_data.get('model')
if not requested_model:
raise HTTPException(status_code=400, detail='model is required')
@ -336,14 +336,20 @@ async def get_anthropic_token_count_target(request: Request, form_data: dict, us
payload['model'] = strip_provider_model_prefix(payload['model'], prefix_id)
headers, cookies = await get_headers_and_cookies(request, url, key, api_config, user=user)
# Anthropic's native endpoints reject bearer auth, the key belongs in x-api-key.
if is_anthropic_url(url):
headers.setdefault('anthropic-version', ANTHROPIC_VERSION)
if api_config.get('auth_type') in (None, 'bearer'):
headers.pop('Authorization', None)
headers.setdefault('x-api-key', key)
return requested_model, payload, url, key, headers, cookies
async def count_anthropic_tokens(request: Request, form_data: dict, user: UserModel) -> int:
"""Forward an Anthropic token-count request through an OpenAI-compatible connection."""
requested_model, payload, url, key, headers, cookies = await get_anthropic_token_count_target(
request, form_data, user
)
requested_model, payload, url, key, headers, cookies = await get_anthropic_request_target(request, form_data, user)
request_url = f'{url.rstrip("/")}/messages/count_tokens'
response = None

View file

@ -12,6 +12,8 @@ from open_webui.utils.json_codec import JSONCodec
log = logging.getLogger(__name__)
ANTHROPIC_VERSION = '2023-06-01'
ANTHROPIC_CONVERTED_REQUEST_PARAMS = {
'model',
'messages',
@ -48,7 +50,7 @@ async def get_anthropic_models(url: str, key: str, user: UserModel = None) -> di
async with aiohttp.ClientSession(timeout=timeout, trust_env=True) as session:
headers = {
'x-api-key': key,
'anthropic-version': '2023-06-01',
'anthropic-version': ANTHROPIC_VERSION,
}
if ENABLE_FORWARD_USER_INFO_HEADERS and user: