This commit is contained in:
Timothy Jaeryang Baek
2026-07-23 04:16:14 -04:00
parent cc9a44569e
commit bb12b1a18b
4 changed files with 434 additions and 211 deletions
+150 -4
View File
@@ -12,6 +12,23 @@ from open_webui.utils.headers import include_user_info_headers
log = logging.getLogger(__name__)
ANTHROPIC_CONVERTED_REQUEST_PARAMS = {
'model',
'messages',
'system',
'max_tokens',
'temperature',
'top_p',
'top_k',
'stop_sequences',
'stream',
'metadata',
'service_tier',
'tools',
'tool_choice',
'reasoning_effort',
}
def is_anthropic_url(url: str) -> bool:
"""Check if the URL is an Anthropic API endpoint."""
@@ -109,7 +126,9 @@ def _finalize_openai_content(blocks: list) -> str | list:
return blocks
def convert_anthropic_to_openai_payload(anthropic_payload: dict) -> dict:
def convert_anthropic_to_openai_payload(
anthropic_payload: dict, passthrough_params: list[str] | str | None = None
) -> dict:
"""
Convert an Anthropic Messages API request to OpenAI Chat Completions format.
@@ -350,6 +369,48 @@ def convert_anthropic_to_openai_payload(anthropic_payload: dict) -> dict:
if 'max_tokens' in anthropic_payload:
openai_payload['max_tokens'] = anthropic_payload['max_tokens']
captured_passthrough_params = {
param: value for param, value in anthropic_payload.items() if param not in ANTHROPIC_CONVERTED_REQUEST_PARAMS
}
if isinstance(passthrough_params, str):
passthrough_params = passthrough_params.split(',')
elif not isinstance(passthrough_params, (list, tuple, set)):
passthrough_params = []
passthrough_param_names = {str(item).strip() for item in passthrough_params if str(item).strip()}
if '*' in passthrough_param_names:
openai_payload.update(captured_passthrough_params)
else:
for param in passthrough_param_names:
if param in captured_passthrough_params:
openai_payload[param] = captured_passthrough_params[param]
output_config = anthropic_payload.get('output_config')
if isinstance(output_config, dict):
if 'effort' in output_config and 'reasoning_effort' not in anthropic_payload:
openai_payload['reasoning_effort'] = output_config['effort']
format_config = output_config.get('format')
if isinstance(format_config, dict):
format_type = format_config.get('type')
if format_type == 'json_schema':
json_schema = {
'name': format_config.get('name', 'response_schema'),
'schema': format_config.get('schema', {}),
}
if 'description' in format_config:
json_schema['description'] = format_config['description']
if 'strict' in format_config:
json_schema['strict'] = format_config['strict']
openai_payload['response_format'] = {
'type': 'json_schema',
'json_schema': json_schema,
}
elif format_type == 'json_object':
openai_payload['response_format'] = {'type': format_type}
if 'reasoning_effort' in anthropic_payload:
openai_payload['reasoning_effort'] = anthropic_payload['reasoning_effort']
# Common parameters
for param in ('temperature', 'top_p', 'top_k', 'stop_sequences', 'stream', 'metadata', 'service_tier'):
if param in anthropic_payload:
@@ -421,6 +482,32 @@ def convert_openai_to_anthropic_response(
# Build content blocks
content = []
message_thinking = message.get('thinking')
thinking_blocks = message.get('thinking_blocks') or []
if not thinking_blocks and isinstance(message_thinking, dict):
thinking_blocks = message_thinking.get('blocks') or []
has_thinking = False
for block in thinking_blocks:
if not isinstance(block, dict):
continue
thinking = block.get('thinking') or block.get('content') or block.get('text')
if not thinking:
continue
thinking_block = {'type': 'thinking', 'thinking': thinking}
if block.get('signature'):
thinking_block['signature'] = block['signature']
content.append(thinking_block)
has_thinking = True
reasoning_content = message.get('reasoning_content') or message.get('reasoning')
if not reasoning_content and isinstance(message_thinking, str):
reasoning_content = message_thinking
if reasoning_content and not has_thinking:
content.append({'type': 'thinking', 'thinking': reasoning_content})
message_content = message.get('content')
if message_content:
content.append({'type': 'text', 'text': message_content})
@@ -445,9 +532,11 @@ def convert_openai_to_anthropic_response(
# Usage
openai_usage = openai_response.get('usage', {})
usage = {
'input_tokens': input_tokens
if input_tokens is not None
else openai_usage.get('input_tokens', openai_usage.get('prompt_tokens', 0)),
'input_tokens': (
input_tokens
if input_tokens is not None
else openai_usage.get('input_tokens', openai_usage.get('prompt_tokens', 0))
),
'output_tokens': openai_usage.get('output_tokens', openai_usage.get('completion_tokens', 0)),
}
if 'cache_creation_input_tokens' in openai_usage:
@@ -490,6 +579,7 @@ async def openai_stream_to_anthropic_stream(openai_stream_generator, model: str
# Track content blocks with a running index.
# Each text block or tool_use block gets its own index.
current_block_index = 0
thinking_block_open = False
text_block_open = False
# Accumulated state for each tool call, keyed by tool call id.
@@ -563,11 +653,52 @@ async def openai_stream_to_anthropic_stream(openai_stream_generator, model: str
'output_tokens', data['usage'].get('completion_tokens', output_tokens)
)
reasoning_content = (
delta.get('reasoning_content')
or delta.get('reasoning')
or delta.get('thinking')
or message.get('reasoning_content')
or message.get('reasoning')
)
if not reasoning_content:
thinking_blocks = delta.get('thinking_blocks') or message.get('thinking_blocks') or []
for block in thinking_blocks:
if isinstance(block, dict):
reasoning_content = block.get('thinking') or block.get('content') or block.get('text')
if reasoning_content:
break
if reasoning_content and not text_block_open and not has_tool_calls:
if not thinking_block_open:
block_start = {
'type': 'content_block_start',
'index': current_block_index,
'content_block': {'type': 'thinking', 'thinking': ''},
}
yield f'event: content_block_start\ndata: {json.dumps(block_start)}\n\n'.encode()
thinking_block_open = True
block_delta = {
'type': 'content_block_delta',
'index': current_block_index,
'delta': {'type': 'thinking_delta', 'thinking': reasoning_content},
}
yield f'event: content_block_delta\ndata: {json.dumps(block_delta)}\n\n'.encode()
# --- Handle text content ---
# Anthropic expects text blocks before tool blocks, so skip
# text deltas once any tool call has started.
content = delta.get('content')
if content and not has_tool_calls:
if thinking_block_open:
block_stop = {
'type': 'content_block_stop',
'index': current_block_index,
}
yield f'event: content_block_stop\ndata: {json.dumps(block_stop)}\n\n'.encode()
thinking_block_open = False
current_block_index += 1
if not text_block_open:
block_start = {
'type': 'content_block_start',
@@ -593,6 +724,15 @@ async def openai_stream_to_anthropic_stream(openai_stream_generator, model: str
if tool_calls:
# Close text block if one is open (text comes before tools)
if thinking_block_open:
block_stop = {
'type': 'content_block_stop',
'index': current_block_index,
}
yield f'event: content_block_stop\ndata: {json.dumps(block_stop)}\n\n'.encode()
thinking_block_open = False
current_block_index += 1
if text_block_open:
block_stop = {
'type': 'content_block_stop',
@@ -704,6 +844,12 @@ async def openai_stream_to_anthropic_stream(openai_stream_generator, model: str
except Exception as e:
log.error(f'Error in Anthropic stream conversion: {e}')
# Close any open thinking block
if thinking_block_open:
block_stop = {'type': 'content_block_stop', 'index': current_block_index}
yield f'event: content_block_stop\ndata: {json.dumps(block_stop)}\n\n'.encode()
current_block_index += 1
# Flush any tools that buffered arguments but never emitted a block
for tool in tracked_tool_calls.values():
if not tool['started'] and tool['name']: