From bb12b1a18b77d80829cedb2d5bf965808222415b Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Thu, 23 Jul 2026 04:16:14 -0400 Subject: [PATCH] refac --- backend/open_webui/main.py | 17 +- backend/open_webui/models/config.py | 1 + backend/open_webui/utils/anthropic.py | 154 +++++- src/lib/components/AddConnectionModal.svelte | 473 +++++++++++-------- 4 files changed, 434 insertions(+), 211 deletions(-) diff --git a/backend/open_webui/main.py b/backend/open_webui/main.py index 2f8542ec55..1b9ed9cc53 100644 --- a/backend/open_webui/main.py +++ b/backend/open_webui/main.py @@ -1795,8 +1795,23 @@ async def generate_messages( # Counting must not turn a compatible generation request into an outage. log.warning('Unable to count Anthropic input tokens for model %s', requested_model, exc_info=True) + model_id = requested_model + model_info = await Models.get_model_by_id(model_id) + if model_info and model_info.base_model_id: + model_id = model_info.base_model_id + + passthrough_params = [] + models = request.app.state.OPENAI_MODELS + if not models or model_id not in models: + await openai.get_all_models(request, user=user) + models = request.app.state.OPENAI_MODELS + model = models.get(model_id) + if model: + _, _, api_config = await openai.get_openai_connection(model['urlIdx']) + passthrough_params = api_config.get('passthrough_params') or [] + # Convert Anthropic payload to OpenAI format - openai_payload = convert_anthropic_to_openai_payload(form_data) + openai_payload = convert_anthropic_to_openai_payload(form_data, passthrough_params) # Route through the existing chat_completion handler response = await chat_completion(request, openai_payload, user) diff --git a/backend/open_webui/models/config.py b/backend/open_webui/models/config.py index 93a55f690d..5d0b2266b7 100644 --- a/backend/open_webui/models/config.py +++ b/backend/open_webui/models/config.py @@ -48,6 +48,7 @@ API_CONFIG_FIELDS = ( 'azure', 'api_version', 'extra_params', + 'passthrough_params', ) diff --git a/backend/open_webui/utils/anthropic.py b/backend/open_webui/utils/anthropic.py index 336767bd91..52dd2ea36a 100644 --- a/backend/open_webui/utils/anthropic.py +++ b/backend/open_webui/utils/anthropic.py @@ -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']: diff --git a/src/lib/components/AddConnectionModal.svelte b/src/lib/components/AddConnectionModal.svelte index 8df0436283..6715c80b1a 100644 --- a/src/lib/components/AddConnectionModal.svelte +++ b/src/lib/components/AddConnectionModal.svelte @@ -49,6 +49,7 @@ let apiType = ''; // '' = chat completions (default), 'responses' = Responses API let headers = ''; + let passthroughParams = ''; let tags = []; @@ -57,12 +58,19 @@ let loading = false; let showDeleteConfirmDialog = false; + let showAdvanced = false; const inputClass = 'bg-transparent outline-hidden placeholder:text-gray-300 dark:placeholder:text-gray-700'; const selectClass = 'dark:bg-gray-900 bg-transparent pr-5 outline-hidden placeholder:text-gray-300 dark:placeholder:text-gray-700'; + const parsePassthroughParams = (value: string) => + value + .split(',') + .map((param) => param.trim()) + .filter(Boolean); + const verifyOllamaHandler = async () => { // remove trailing slash from url url = url.replace(/\/$/, ''); @@ -109,6 +117,7 @@ ...(provider ? { provider } : {}), ...(azure ? { azure: true } : {}), api_version: apiVersion, + passthrough_params: parsePassthroughParams(passthroughParams), ...(_headers ? { headers: _headers } : {}) } }, @@ -149,6 +158,7 @@ if (azure) { if (!apiVersion) { loading = false; + showAdvanced = true; toast.error($i18n.t('API Version is required')); return; @@ -163,6 +173,7 @@ if (modelIds.length === 0) { loading = false; + showAdvanced = true; toast.error($i18n.t('Deployment names are required for Azure OpenAI')); return; } @@ -195,6 +206,7 @@ connection_type: connectionType, auth_type, headers: headers ? JSON.parse(headers) : undefined, + passthrough_params: parsePassthroughParams(passthroughParams), ...(provider ? { provider } : {}), ...(!ollama && azure ? { azure: true } : {}), ...(azure ? { api_version: apiVersion } : {}), @@ -211,6 +223,8 @@ key = ''; auth_type = 'bearer'; prefixId = ''; + passthroughParams = ''; + showAdvanced = false; tags = []; modelIds = []; }; @@ -228,6 +242,9 @@ enable = connection.config?.enable ?? true; tags = connection.config?.tags ?? []; prefixId = connection.config?.prefix_id ?? ''; + passthroughParams = Array.isArray(connection.config?.passthrough_params) + ? connection.config.passthrough_params.join(', ') + : (connection.config?.passthrough_params ?? ''); modelIds = connection.config?.model_ids ?? []; if (ollama) { @@ -429,107 +446,6 @@ - {#if !direct} -
-
- - -
- -