fix: use the pooled client timeout for the Anthropic Messages passthrough (#27675)
* fix: use the pooled client timeout for the Anthropic Messages passthrough The native `/api/v1/messages` passthrough still referenced `openai.AIOHTTP_CLIENT_TIMEOUT`, which stopped existing when `routers/openai.py` moved onto `session_pool.get_client_timeout()`. Every passthrough request therefore raised `AttributeError: module 'open_webui.routers.openai' has no attribute 'AIOHTTP_CLIENT_TIMEOUT'` before it was sent, and the surrounding handler turned that into a 502 "Open WebUI: Server Connection Error", so Anthropic-format clients such as Cline could not reach any model at all. Use `get_client_timeout(stream=...)` like the OpenAI and Ollama proxies do, so the configured `AIOHTTP_CLIENT_TIMEOUT` applies and streaming requests additionally get the idle-read timeout. Fixes #27595 * 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:
1 parent
8a170897ba
commit
9cf1a07960
3 files changed
+18
-10
No files matched your search
@@ -267,7 +267,7 @@ from open_webui.utils.oauth import (
|
|||||||
from open_webui.utils.plugin import install_tool_and_function_dependencies
|
from open_webui.utils.plugin import install_tool_and_function_dependencies
|
||||||
from open_webui.utils.redis import get_redis_client
|
from open_webui.utils.redis import get_redis_client
|
||||||
from open_webui.utils.security_headers import SecurityHeadersMiddleware
|
from open_webui.utils.security_headers import SecurityHeadersMiddleware
|
||||||
from open_webui.utils.session_pool import cleanup_response, get_session, stream_wrapper
|
from open_webui.utils.session_pool import cleanup_response, get_client_timeout, get_session, stream_wrapper
|
||||||
from open_webui.utils.tool_approval import (
|
from open_webui.utils.tool_approval import (
|
||||||
ResolveToolCallForm,
|
ResolveToolCallForm,
|
||||||
build_tool_approval_resume_payload,
|
build_tool_approval_resume_payload,
|
||||||
@@ -1908,7 +1908,7 @@ async def count_message_tokens(
|
|||||||
|
|
||||||
|
|
||||||
async def passthrough_anthropic_messages(request: Request, form_data: dict, user) -> Response | dict:
|
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, form_data, user
|
||||||
)
|
)
|
||||||
request_url = f'{url.rstrip("/")}/messages'
|
request_url = f'{url.rstrip("/")}/messages'
|
||||||
@@ -1924,7 +1924,7 @@ async def passthrough_anthropic_messages(request: Request, form_data: dict, user
|
|||||||
headers=headers,
|
headers=headers,
|
||||||
cookies=cookies,
|
cookies=cookies,
|
||||||
ssl=AIOHTTP_CLIENT_SESSION_SSL,
|
ssl=AIOHTTP_CLIENT_SESSION_SSL,
|
||||||
timeout=aiohttp.ClientTimeout(total=openai.AIOHTTP_CLIENT_TIMEOUT),
|
timeout=get_client_timeout(stream=bool(payload.get('stream'))),
|
||||||
)
|
)
|
||||||
|
|
||||||
if 'text/event-stream' in response.headers.get('Content-Type', ''):
|
if 'text/event-stream' in response.headers.get('Content-Type', ''):
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ from open_webui.models.groups import Groups
|
|||||||
from open_webui.models.models import Models
|
from open_webui.models.models import Models
|
||||||
from open_webui.models.users import UserModel
|
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.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.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.headers import get_custom_headers, include_user_info_headers
|
||||||
from open_webui.utils.json_codec import JSONCodec
|
from open_webui.utils.json_codec import JSONCodec
|
||||||
@@ -451,8 +451,8 @@ async def send_model_management_request(
|
|||||||
await cleanup_response(response)
|
await cleanup_response(response)
|
||||||
|
|
||||||
|
|
||||||
async def get_anthropic_token_count_target(request: Request, form_data: dict, user: UserModel):
|
async def get_anthropic_request_target(request: Request, form_data: dict, user: UserModel):
|
||||||
"""Resolve the upstream LiteLLM connection for an Anthropic token-count request."""
|
"""Resolve the upstream connection, payload and auth headers for a native Anthropic request."""
|
||||||
requested_model = form_data.get('model')
|
requested_model = form_data.get('model')
|
||||||
if not requested_model:
|
if not requested_model:
|
||||||
raise HTTPException(status_code=400, detail='model is required')
|
raise HTTPException(status_code=400, detail='model is required')
|
||||||
@@ -480,14 +480,20 @@ async def get_anthropic_token_count_target(request: Request, form_data: dict, us
|
|||||||
payload['model'] = strip_provider_model_prefix(payload['model'], prefix_id)
|
payload['model'] = strip_provider_model_prefix(payload['model'], prefix_id)
|
||||||
|
|
||||||
headers, cookies = await get_headers_and_cookies(request, url, key, api_config, user=user)
|
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
|
return requested_model, payload, url, key, headers, cookies
|
||||||
|
|
||||||
|
|
||||||
async def count_anthropic_tokens(request: Request, form_data: dict, user: UserModel) -> int:
|
async def count_anthropic_tokens(request: Request, form_data: dict, user: UserModel) -> int:
|
||||||
"""Forward an Anthropic token-count request through an OpenAI-compatible connection."""
|
"""Forward an Anthropic token-count request through an OpenAI-compatible connection."""
|
||||||
requested_model, payload, url, key, headers, cookies = await get_anthropic_token_count_target(
|
requested_model, payload, url, key, headers, cookies = await get_anthropic_request_target(request, form_data, user)
|
||||||
request, form_data, user
|
|
||||||
)
|
|
||||||
request_url = f'{url.rstrip("/")}/messages/count_tokens'
|
request_url = f'{url.rstrip("/")}/messages/count_tokens'
|
||||||
response = None
|
response = None
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ from open_webui.utils.json_codec import JSONCodec
|
|||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
ANTHROPIC_VERSION = '2023-06-01'
|
||||||
|
|
||||||
ANTHROPIC_CONVERTED_REQUEST_PARAMS = {
|
ANTHROPIC_CONVERTED_REQUEST_PARAMS = {
|
||||||
'model',
|
'model',
|
||||||
'messages',
|
'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:
|
async with aiohttp.ClientSession(timeout=timeout, trust_env=True) as session:
|
||||||
headers = {
|
headers = {
|
||||||
'x-api-key': key,
|
'x-api-key': key,
|
||||||
'anthropic-version': '2023-06-01',
|
'anthropic-version': ANTHROPIC_VERSION,
|
||||||
}
|
}
|
||||||
|
|
||||||
if ENABLE_FORWARD_USER_INFO_HEADERS and user:
|
if ENABLE_FORWARD_USER_INFO_HEADERS and user:
|
||||||
|
|||||||
Reference in new issue
Block a user