mirror of
https://github.com/open-webui/open-webui.git
synced 2026-07-23 09:10:55 -05:00
refac
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -48,6 +48,7 @@ API_CONFIG_FIELDS = (
|
||||
'azure',
|
||||
'api_version',
|
||||
'extra_params',
|
||||
'passthrough_params',
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -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']:
|
||||
|
||||
@@ -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 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if !direct}
|
||||
<div class="flex gap-2 mt-2">
|
||||
<div class="flex flex-col w-full">
|
||||
<label
|
||||
for="headers-input"
|
||||
class={`mb-0.5 text-xs text-gray-500
|
||||
`}>{$i18n.t('Headers')}</label
|
||||
>
|
||||
|
||||
<div class="flex-1">
|
||||
<Tooltip
|
||||
content={$i18n.t(
|
||||
'Enter additional headers in JSON format (e.g. {"X-Custom-Header": "value"}'
|
||||
)}
|
||||
>
|
||||
<Textarea
|
||||
className="w-full text-sm outline-hidden"
|
||||
bind:value={headers}
|
||||
placeholder={$i18n.t('Enter additional headers in JSON format')}
|
||||
required={false}
|
||||
minSize={30}
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="flex gap-2 mt-2">
|
||||
<div class="flex flex-col w-full">
|
||||
<label
|
||||
for="prefix-id-input"
|
||||
class={`mb-0.5 text-xs text-gray-500
|
||||
`}>{$i18n.t('Prefix ID')}</label
|
||||
>
|
||||
|
||||
<div class="flex-1">
|
||||
<Tooltip
|
||||
content={$i18n.t(
|
||||
'Prefix ID is used to avoid conflicts with other connections by adding a prefix to the model IDs - leave empty to disable'
|
||||
)}
|
||||
>
|
||||
<input
|
||||
class={`w-full text-sm ${inputClass}`}
|
||||
type="text"
|
||||
id="prefix-id-input"
|
||||
bind:value={prefixId}
|
||||
placeholder={$i18n.t('Prefix ID')}
|
||||
autocomplete="off"
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if !ollama && !direct}
|
||||
<div class="flex flex-row justify-between items-center w-full mt-2">
|
||||
<label
|
||||
for="provider-select"
|
||||
class={`mb-0.5 text-xs text-gray-500
|
||||
`}>{$i18n.t('Provider')}</label
|
||||
>
|
||||
|
||||
<div>
|
||||
<select
|
||||
id="provider-select"
|
||||
bind:value={provider}
|
||||
class="text-xs text-gray-700 dark:text-gray-300 bg-transparent outline-hidden"
|
||||
>
|
||||
<option value="">{$i18n.t('Default')}</option>
|
||||
<option value="azure">{$i18n.t('Azure OpenAI')}</option>
|
||||
<option value="llama.cpp">{$i18n.t('llama.cpp')}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if azure}
|
||||
<div class="flex gap-2 mt-2">
|
||||
<div class="flex flex-col w-full">
|
||||
<label
|
||||
for="api-version-input"
|
||||
class={`mb-0.5 text-xs text-gray-500
|
||||
`}>{$i18n.t('API Version')}</label
|
||||
>
|
||||
|
||||
<div class="flex-1">
|
||||
<input
|
||||
id="api-version-input"
|
||||
class={`w-full text-sm ${inputClass}`}
|
||||
type="text"
|
||||
bind:value={apiVersion}
|
||||
placeholder={$i18n.t('API Version')}
|
||||
autocomplete="off"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if !ollama && !direct}
|
||||
<div class="flex flex-row justify-between items-center w-full mt-1">
|
||||
<label
|
||||
@@ -548,19 +464,7 @@
|
||||
class=" text-xs text-gray-700 dark:text-gray-300"
|
||||
>
|
||||
{#if apiType === 'responses'}
|
||||
<Tooltip
|
||||
className="flex items-center gap-1"
|
||||
content={$i18n.t(
|
||||
'This feature is currently experimental and may not work as expected.'
|
||||
)}
|
||||
>
|
||||
<span
|
||||
class="inline-flex items-center text-[0.625rem] font-normal uppercase leading-none text-gray-400 dark:text-gray-600"
|
||||
>{$i18n.t('Experimental')}</span
|
||||
>
|
||||
|
||||
{$i18n.t('Responses')}
|
||||
</Tooltip>
|
||||
{$i18n.t('Responses')}
|
||||
{:else}
|
||||
{$i18n.t('Chat Completions')}
|
||||
{/if}
|
||||
@@ -569,111 +473,268 @@
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="flex flex-col w-full mt-2">
|
||||
<div class="mb-1 flex justify-between">
|
||||
<div
|
||||
class={`mb-0.5 text-xs text-gray-500
|
||||
`}
|
||||
<div class="flex items-center justify-between">
|
||||
<button
|
||||
type="button"
|
||||
class="flex items-center gap-1 text-xs text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-200 transition mt-2"
|
||||
on:click={() => (showAdvanced = !showAdvanced)}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
class="w-3 h-3 transition-transform {showAdvanced ? 'rotate-90' : ''}"
|
||||
>
|
||||
{$i18n.t('Model IDs')}
|
||||
</div>
|
||||
</div>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M7.21 14.77a.75.75 0 01.02-1.06L11.168 10 7.23 6.29a.75.75 0 111.04-1.08l4.5 4.25a.75.75 0 010 1.08l-4.5 4.25a.75.75 0 01-1.06-.02z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
{$i18n.t('Advanced')}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{#if modelIds.length > 0}
|
||||
<ul class="flex flex-col">
|
||||
{#each modelIds as modelId, modelIdx}
|
||||
<li class=" flex gap-2 w-full justify-between items-center">
|
||||
<div class=" text-sm flex-1 py-1 rounded-lg">
|
||||
{modelId}
|
||||
</div>
|
||||
<div class="shrink-0">
|
||||
<button
|
||||
aria-label={$i18n.t(`Remove {{MODELID}} from list.`, {
|
||||
MODELID: modelId
|
||||
})}
|
||||
type="button"
|
||||
on:click={() => {
|
||||
modelIds = modelIds.filter((_, idx) => idx !== modelIdx);
|
||||
}}
|
||||
>
|
||||
<Minus strokeWidth="2" className="size-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
{:else}
|
||||
<div
|
||||
class={`text-gray-500 text-xs text-center py-2 px-10
|
||||
`}
|
||||
>
|
||||
{#if ollama}
|
||||
{$i18n.t('Leave empty to include all models from "{{url}}/api/tags" endpoint', {
|
||||
url: url
|
||||
})}
|
||||
{:else if azure}
|
||||
{$i18n.t('Deployment names are required for Azure OpenAI')}
|
||||
<!-- {$i18n.t('Leave empty to include all models from "{{url}}" endpoint', {
|
||||
url: `${url}/openai/deployments`
|
||||
})} -->
|
||||
{:else}
|
||||
{$i18n.t('Leave empty to include all models from "{{url}}/models" endpoint', {
|
||||
url: url
|
||||
})}
|
||||
{/if}
|
||||
{#if showAdvanced}
|
||||
{#if !direct}
|
||||
<div class="flex gap-2 mt-2">
|
||||
<div class="flex flex-col w-full">
|
||||
<label
|
||||
for="headers-input"
|
||||
class={`mb-0.5 text-xs text-gray-500
|
||||
`}>{$i18n.t('Headers')}</label
|
||||
>
|
||||
|
||||
<div class="flex-1">
|
||||
<Tooltip
|
||||
content={$i18n.t(
|
||||
'Enter additional headers in JSON format (e.g. {"X-Custom-Header": "value"}'
|
||||
)}
|
||||
>
|
||||
<Textarea
|
||||
className="w-full text-sm outline-hidden"
|
||||
bind:value={headers}
|
||||
placeholder={$i18n.t('Enter additional headers in JSON format')}
|
||||
required={false}
|
||||
minSize={30}
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="flex items-center">
|
||||
<label class="sr-only" for="add-model-id-input">{$i18n.t('Add a model ID')}</label>
|
||||
<input
|
||||
class={`w-full text-sm ${inputClass} ${modelId ? '' : 'text-gray-500'}`}
|
||||
bind:value={modelId}
|
||||
id="add-model-id-input"
|
||||
placeholder={$i18n.t('Add a model ID')}
|
||||
/>
|
||||
{#if !ollama && !direct}
|
||||
<div class="flex gap-2 mt-2">
|
||||
<div class="flex flex-col w-full">
|
||||
<label
|
||||
for="allowed-passthrough-params-input"
|
||||
class={`mb-0.5 text-xs text-gray-500
|
||||
`}>{$i18n.t('Passthrough params')}</label
|
||||
>
|
||||
|
||||
<div>
|
||||
<button
|
||||
type="button"
|
||||
aria-label={$i18n.t('Add')}
|
||||
on:click={() => {
|
||||
addModelHandler();
|
||||
}}
|
||||
>
|
||||
<Plus className="size-3.5" strokeWidth="2" />
|
||||
</button>
|
||||
<div class="flex-1">
|
||||
<Tooltip
|
||||
content={$i18n.t(
|
||||
'Comma-separated top-level request parameters this upstream may receive without translation. Use * to allow all captured passthrough params.'
|
||||
)}
|
||||
>
|
||||
<input
|
||||
class={`w-full text-sm ${inputClass}`}
|
||||
type="text"
|
||||
id="allowed-passthrough-params-input"
|
||||
bind:value={passthroughParams}
|
||||
placeholder={$i18n.t('thinking, output_config')}
|
||||
autocomplete="off"
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="flex gap-2 mt-2">
|
||||
<div class="flex flex-col w-full">
|
||||
<label
|
||||
for="prefix-id-input"
|
||||
class={`mb-0.5 text-xs text-gray-500
|
||||
`}>{$i18n.t('Prefix ID')}</label
|
||||
>
|
||||
|
||||
<div class="flex-1">
|
||||
<Tooltip
|
||||
content={$i18n.t(
|
||||
'Prefix ID is used to avoid conflicts with other connections by adding a prefix to the model IDs - leave empty to disable'
|
||||
)}
|
||||
>
|
||||
<input
|
||||
class={`w-full text-sm ${inputClass}`}
|
||||
type="text"
|
||||
id="prefix-id-input"
|
||||
bind:value={prefixId}
|
||||
placeholder={$i18n.t('Prefix ID')}
|
||||
autocomplete="off"
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-2 mt-2">
|
||||
<div class="flex flex-col w-full">
|
||||
<div
|
||||
class={`mb-0.5 text-xs text-gray-500
|
||||
{#if !ollama && !direct}
|
||||
<div class="flex flex-row justify-between items-center w-full mt-2">
|
||||
<label
|
||||
for="provider-select"
|
||||
class={`mb-0.5 text-xs text-gray-500
|
||||
`}>{$i18n.t('Provider')}</label
|
||||
>
|
||||
|
||||
<div>
|
||||
<select
|
||||
id="provider-select"
|
||||
bind:value={provider}
|
||||
class="text-xs text-gray-700 dark:text-gray-300 bg-transparent outline-hidden"
|
||||
>
|
||||
<option value="">{$i18n.t('Default')}</option>
|
||||
<option value="azure">{$i18n.t('Azure OpenAI')}</option>
|
||||
<option value="llama.cpp">{$i18n.t('llama.cpp')}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if azure}
|
||||
<div class="flex gap-2 mt-2">
|
||||
<div class="flex flex-col w-full">
|
||||
<label
|
||||
for="api-version-input"
|
||||
class={`mb-0.5 text-xs text-gray-500
|
||||
`}>{$i18n.t('API Version')}</label
|
||||
>
|
||||
|
||||
<div class="flex-1">
|
||||
<input
|
||||
id="api-version-input"
|
||||
class={`w-full text-sm ${inputClass}`}
|
||||
type="text"
|
||||
bind:value={apiVersion}
|
||||
placeholder={$i18n.t('API Version')}
|
||||
autocomplete="off"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="flex flex-col w-full mt-2">
|
||||
<div class="mb-1 flex justify-between">
|
||||
<div
|
||||
class={`mb-0.5 text-xs text-gray-500
|
||||
`}
|
||||
>
|
||||
{$i18n.t('Tags')}
|
||||
>
|
||||
{$i18n.t('Model IDs')}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if modelIds.length > 0}
|
||||
<ul class="flex flex-col">
|
||||
{#each modelIds as modelId, modelIdx}
|
||||
<li class=" flex gap-2 w-full justify-between items-center">
|
||||
<div class=" text-sm flex-1 py-1 rounded-lg">
|
||||
{modelId}
|
||||
</div>
|
||||
<div class="shrink-0">
|
||||
<button
|
||||
aria-label={$i18n.t(`Remove {{MODELID}} from list.`, {
|
||||
MODELID: modelId
|
||||
})}
|
||||
type="button"
|
||||
on:click={() => {
|
||||
modelIds = modelIds.filter((_, idx) => idx !== modelIdx);
|
||||
}}
|
||||
>
|
||||
<Minus strokeWidth="2" className="size-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
{:else}
|
||||
<div
|
||||
class={`text-gray-500 text-xs text-center py-2 px-10
|
||||
`}
|
||||
>
|
||||
{#if ollama}
|
||||
{$i18n.t(
|
||||
'Leave empty to include all models from "{{url}}/api/tags" endpoint',
|
||||
{
|
||||
url: url
|
||||
}
|
||||
)}
|
||||
{:else if azure}
|
||||
{$i18n.t('Deployment names are required for Azure OpenAI')}
|
||||
<!-- {$i18n.t('Leave empty to include all models from "{{url}}" endpoint', {
|
||||
url: `${url}/openai/deployments`
|
||||
})} -->
|
||||
{:else}
|
||||
{$i18n.t('Leave empty to include all models from "{{url}}/models" endpoint', {
|
||||
url: url
|
||||
})}
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="flex-1 mt-0.5">
|
||||
<Tags
|
||||
bind:tags
|
||||
on:add={(e) => {
|
||||
tags = [
|
||||
...tags,
|
||||
{
|
||||
name: e.detail
|
||||
}
|
||||
];
|
||||
}}
|
||||
on:delete={(e) => {
|
||||
tags = tags.filter((tag) => tag.name !== e.detail);
|
||||
}}
|
||||
<div class="flex items-center">
|
||||
<label class="sr-only" for="add-model-id-input">{$i18n.t('Add a model ID')}</label>
|
||||
<input
|
||||
class={`w-full text-sm ${inputClass} ${modelId ? '' : 'text-gray-500'}`}
|
||||
bind:value={modelId}
|
||||
id="add-model-id-input"
|
||||
placeholder={$i18n.t('Add a model ID')}
|
||||
/>
|
||||
|
||||
<div>
|
||||
<button
|
||||
type="button"
|
||||
aria-label={$i18n.t('Add')}
|
||||
on:click={() => {
|
||||
addModelHandler();
|
||||
}}
|
||||
>
|
||||
<Plus className="size-3.5" strokeWidth="2" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-2 mt-2">
|
||||
<div class="flex flex-col w-full">
|
||||
<div
|
||||
class={`mb-0.5 text-xs text-gray-500
|
||||
`}
|
||||
>
|
||||
{$i18n.t('Tags')}
|
||||
</div>
|
||||
|
||||
<div class="flex-1 mt-0.5">
|
||||
<Tags
|
||||
bind:tags
|
||||
on:add={(e) => {
|
||||
tags = [
|
||||
...tags,
|
||||
{
|
||||
name: e.detail
|
||||
}
|
||||
];
|
||||
}}
|
||||
on:delete={(e) => {
|
||||
tags = tags.filter((tag) => tag.name !== e.detail);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="flex justify-between items-center pt-3 text-sm font-medium">
|
||||
|
||||
Reference in New Issue
Block a user