This commit is contained in:
Timothy Jaeryang Baek
2026-07-26 18:46:39 -04:00
parent 11d72c1ce2
commit b81627b2c9
4 changed files with 80 additions and 2 deletions
+64 -2
View File
@@ -252,7 +252,7 @@ from open_webui.utils.oauth import (
from open_webui.utils.plugin import install_tool_and_function_dependencies
from open_webui.utils.redis import get_redis_client
from open_webui.utils.security_headers import SecurityHeadersMiddleware
from open_webui.utils.session_pool import get_session
from open_webui.utils.session_pool import cleanup_response, get_session, stream_wrapper
from open_webui.utils.tools import set_terminal_servers, set_tool_servers
if SAFE_MODE:
@@ -1779,6 +1779,7 @@ app.state.CHAT_COMPLETION_HANDLER = chat_completion
from open_webui.utils.anthropic import (
convert_anthropic_to_openai_payload,
convert_openai_to_anthropic_response,
is_anthropic_messages_passthrough,
openai_stream_to_anthropic_stream,
)
@@ -1793,6 +1794,65 @@ async def count_message_tokens(
return {'input_tokens': await openai.count_anthropic_tokens(request, form_data, user)}
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(
request, form_data, user
)
request_url = f'{url.rstrip("/")}/messages'
response = None
streaming = False
try:
session = await get_session()
response = await session.request(
method='POST',
url=request_url,
data=json.dumps(payload),
headers=headers,
cookies=cookies,
ssl=AIOHTTP_CLIENT_SESSION_SSL,
timeout=aiohttp.ClientTimeout(total=openai.AIOHTTP_CLIENT_TIMEOUT),
)
if 'text/event-stream' in response.headers.get('Content-Type', ''):
streaming = True
return StreamingResponse(
stream_wrapper(response),
status_code=response.status,
headers=openai._clean_proxy_headers(response.headers),
)
try:
response_data = await response.json()
except Exception:
response_data = await response.text()
if response.status >= 400:
await openai.publish_model_provider_request_failed(
request,
actor=user,
provider='openai-compatible',
base_url=url,
api_key=key,
status=response.status,
requested_model=requested_model,
upstream_error=response_data,
)
if isinstance(response_data, (dict, list)):
return JSONResponse(status_code=response.status, content=response_data)
return Response(status_code=response.status, content=response_data)
return response_data
except HTTPException:
raise
except Exception:
log.exception('Failed to passthrough Anthropic Messages request for model %s', requested_model)
raise HTTPException(status_code=502, detail=ERROR_MESSAGES.SERVER_CONNECTION_ERROR)
finally:
if not streaming:
await cleanup_response(response)
@app.post('/api/message')
@app.post('/api/v1/messages') # Anthropic Messages API compatible endpoint
async def generate_messages(
@@ -1833,7 +1893,9 @@ async def generate_messages(
models = request.app.state.OPENAI_MODELS
model = models.get(model_id)
if model:
_, _, api_config = await openai.get_openai_connection(model['urlIdx'])
url, _, api_config = await openai.get_openai_connection(model['urlIdx'])
if is_anthropic_messages_passthrough(url, api_config):
return await passthrough_anthropic_messages(request, form_data, user)
passthrough_params = api_config.get('passthrough_params') or []
# Convert Anthropic payload to OpenAI format
+1
View File
@@ -46,6 +46,7 @@ API_CONFIG_FIELDS = (
'auth_type',
'headers',
'azure',
'api_type',
'api_version',
'extra_params',
'passthrough_params',
+14
View File
@@ -126,6 +126,13 @@ def _finalize_openai_content(blocks: list) -> str | list:
return blocks
def is_anthropic_messages_passthrough(url: str, api_config: dict | None = None) -> bool:
api_config = api_config or {}
provider = str(api_config.get('provider', '')).lower()
return is_anthropic_url(url or '') or provider == 'litellm'
def convert_anthropic_to_openai_payload(
anthropic_payload: dict, passthrough_params: list[str] | str | None = None
) -> dict:
@@ -192,6 +199,8 @@ def convert_anthropic_to_openai_payload(
},
)
)
elif block_type in ('thinking', 'redacted_thinking'):
openai_content.append(_copy_cache_control(block, dict(block)))
elif block_type == 'image':
source = block.get('source', {})
if source.get('type') == 'base64':
@@ -492,6 +501,11 @@ def convert_openai_to_anthropic_response(
if not isinstance(block, dict):
continue
if block.get('type') == 'redacted_thinking':
content.append({k: v for k, v in block.items() if k in {'type', 'data'}})
has_thinking = True
continue
thinking = block.get('thinking') or block.get('content') or block.get('text')
if not thinking:
continue
@@ -597,6 +597,7 @@
<option value="">{$i18n.t('Default')}</option>
<option value="azure">{$i18n.t('Azure OpenAI')}</option>
<option value="llama.cpp">{$i18n.t('llama.cpp')}</option>
<option value="litellm">{$i18n.t('LiteLLM')}</option>
</select>
</div>
</div>