agent-zero/plugins/_model_config/webui/model-field.html
Alessandro ba0d90c380 Improve model config provider controls
Reset the custom API base URL whenever the provider dropdown changes so stale endpoints do not carry across provider tests. Move the chat Supports Vision toggle out of Advanced Settings while keeping dependent vision settings, such as Max embeds, inside Advanced.
2026-05-12 03:52:18 +02:00

380 lines
15 KiB
HTML

<html>
<head>
<script type="module">
import { store } from "/plugins/_model_config/webui/model-config-store.js";
</script>
</head>
<body>
<!--
Reusable model configuration field set.
Parent x-data scope must provide:
model — reactive object with provider, name, api_key, api_base, ctx_length, ctx_history, ctx_input, vision, max_embeds, rl_requests, rl_input, rl_output, kwargs, _kwargs_text
modelType — 'chat' | 'utility' | 'embedding'
providers — array of { value, label }
searchType — 'chat' | 'embedding'
apiKeyMode — 'store' (config.html: key lives in $store.modelConfig.apiKeyValues) | 'inline' (preset: key lives in model.api_key)
Optional:
providerFallback — fallback provider string for search/API key status (e.g. preset.chat.provider for utility slot)
apiBaseFallback — fallback api_base string for search (e.g. preset.chat.api_base for utility slot)
-->
<div x-data="{ get _prov() { return model.provider || (typeof providerFallback !== 'undefined' ? providerFallback : ''); }, get _apiBase() { return model.api_base || (typeof apiBaseFallback !== 'undefined' ? apiBaseFallback : ''); } }">
<!-- Provider -->
<div class="field">
<div class="field-label">
<div class="field-title">Provider</div>
<div class="field-description">LLM service provider for this model slot.</div>
</div>
<div class="field-control">
<select x-model="model.provider"
x-effect="$nextTick(() => { if (providers.length) $el.value = model.provider })"
@change="model.api_base = ''">
<option value="">&mdash; select &mdash;</option>
<template x-for="p in providers" :key="p.value">
<option :value="p.value" x-text="p.label"></option>
</template>
</select>
</div>
</div>
<!-- Model name + search -->
<div class="field">
<div class="field-label">
<div class="field-title">Model name</div>
<div class="field-description">Model identifier. Click the search icon to browse available models.</div>
</div>
<div class="field-control" style="position:relative;"
x-data="{ results: [], open: false, searching: false,
doSearch() { this.searching = true; $store.modelConfig.searchModels(_prov, model.name, searchType, _apiBase).then(r => { this.results = r; this.open = true; }).finally(() => this.searching = false); },
grouped() { return $store.modelConfig.groupResults(this.results, model.name); }
}"
@click.outside="open = false">
<input type="text" x-model="model.name" style="padding-right:32px;"
@keydown.enter.prevent="doSearch()" />
<span class="model-search-btn"
@click="if (!searching) doSearch()"
title="Search available models">
<span class="material-symbols-outlined" :style="searching && 'opacity:0'">search</span>
<span class="material-symbols-outlined model-search-spinner" :style="!searching && 'opacity:0'">progress_activity</span>
</span>
<div class="model-search-results" x-show="open && results.length > 0" x-transition.opacity>
<template x-for="m in grouped().matched" :key="'m_'+m">
<div class="model-search-item matched" @click="model.name = m; open = false;" x-text="m"></div>
</template>
<div class="model-search-separator" x-show="grouped().matched.length > 0 && grouped().rest.length > 0"></div>
<template x-for="m in grouped().rest" :key="'r_'+m">
<div class="model-search-item" @click="model.name = m; open = false;" x-text="m"></div>
</template>
</div>
<div class="model-search-results" x-show="open && results.length === 0 && !searching">
<div class="model-search-item disabled">No models found</div>
</div>
</div>
</div>
<!-- API key (store mode: config.html) -->
<template x-if="apiKeyMode === 'store'">
<div class="field">
<div class="field-label">
<div class="field-title">API key</div>
<div class="field-description">Authentication key for this provider. Shared across all model slots using the same provider.</div>
</div>
<div class="field-control" style="position:relative;" x-data="{ showKey: false }">
<input :type="showKey ? 'text' : 'password'"
:value="$store.modelConfig.apiKeyValues[_prov]"
:placeholder="$store.modelConfig.apiKeyStatus[_prov] ? '&#x2022;&#x2022;&#x2022;&#x2022;&#x2022;&#x2022;&#x2022;&#x2022;&#x2022;&#x2022;&#x2022;&#x2022;' : ''"
autocomplete="off"
@input="$store.modelConfig.setApiKeyValue(_prov, $el.value)"
style="padding-right:32px;" />
<span class="material-symbols-outlined eye-toggle"
@click="
showKey = !showKey;
const prov = _prov;
if (showKey && !$store.modelConfig.apiKeyValues[prov] && $store.modelConfig.apiKeyStatus[prov]) {
$store.modelConfig.revealApiKey(prov).then(v => { if (v) $store.modelConfig.apiKeyValues[prov] = v; });
}
"
x-text="showKey ? 'visibility' : 'visibility_off'"></span>
</div>
</div>
</template>
<!-- API key (inline mode: presets) -->
<template x-if="apiKeyMode === 'inline'">
<div class="field">
<div class="field-label">
<div class="field-title">API key</div>
<div class="field-description">Authentication key for this provider. Shared across all model slots using the same provider.</div>
</div>
<div class="field-control" style="position:relative;" x-data="{ showKey: false, _revealed: '' }">
<input :type="showKey ? 'text' : 'password'" x-model="model.api_key" autocomplete="off"
:placeholder="$store.modelConfig.apiKeyStatus[_prov] ? '&#x2022;&#x2022;&#x2022;&#x2022;&#x2022;&#x2022;&#x2022;&#x2022;&#x2022;&#x2022;&#x2022;&#x2022;' : ''"
style="padding-right:32px;" />
<span class="material-symbols-outlined eye-toggle"
@click="
showKey = !showKey;
if (showKey && !model.api_key && $store.modelConfig.apiKeyStatus[_prov]) {
$store.modelConfig.revealApiKey(_prov).then(v => { if (v) { model.api_key = v; _revealed = v; } });
}
if (!showKey && _revealed && model.api_key === _revealed) {
model.api_key = ''; _revealed = '';
}
"
x-text="showKey ? 'visibility' : 'visibility_off'"></span>
</div>
</div>
</template>
<!-- Vision support (chat only) -->
<template x-if="modelType === 'chat'">
<div class="field">
<div class="field-label">
<div class="field-title">Supports Vision</div>
<div class="field-description">Models capable of Vision can for example natively see the content of image attachments.</div>
</div>
<div class="field-control">
<label class="toggle">
<input type="checkbox" x-model="model.vision" />
<span class="toggler"></span>
</label>
</div>
</div>
</template>
<!-- Advanced Settings (collapsed by default) -->
<div class="advanced-section" x-data="{ advOpen: false }">
<div class="advanced-toggle" @click="advOpen = !advOpen">
<span class="material-symbols-outlined advanced-toggle-icon"
:style="advOpen ? 'transform:rotate(90deg)' : ''"
>chevron_right</span>
<span>Advanced Settings</span>
</div>
<div class="advanced-body" x-show="advOpen" x-transition.opacity>
<!-- API base URL -->
<div class="field">
<div class="field-label">
<div class="field-title">API base URL</div>
<div class="field-description">Custom endpoint URL. Leave empty to use the provider's default.</div>
</div>
<div class="field-control">
<input type="text" x-model="model.api_base" />
</div>
</div>
<!-- Context length (not for embedding) -->
<template x-if="modelType !== 'embedding'">
<div class="field">
<div class="field-label">
<div class="field-title">Context length</div>
<div class="field-description">Maximum number of tokens in the context window. System prompt, chat history, RAG and response all count towards this limit.</div>
</div>
<div class="field-control">
<input type="number" x-model.number="model.ctx_length" />
</div>
</div>
</template>
<!-- Chat-specific: ctx_history, max_embeds -->
<template x-if="modelType === 'chat'">
<div>
<div class="field">
<div class="field-label">
<div class="field-title">Context window space for chat history</div>
<div class="field-description">Portion of context window dedicated to chat history visible to the agent. Smaller size will result in shorter and more summarized history.</div>
</div>
<div class="field-control">
<input type="range" min="0.01" max="1" step="0.01" x-model.number="model.ctx_history" />
<span class="range-value" x-text="model.ctx_history"></span>
</div>
</div>
<template x-if="model.vision">
<div class="field">
<div class="field-label">
<div class="field-title">Max embeds</div>
<div class="field-description">Maximum number of embedded images used by the chat model. Set to 0 for unlimited.</div>
</div>
<div class="field-control">
<input type="number" min="0" x-model.number="model.max_embeds" x-init="if (!model.max_embeds) model.max_embeds = 10" />
</div>
</div>
</template>
</div>
</template>
<!-- Utility-specific: ctx_input slider -->
<template x-if="modelType === 'utility'">
<div class="field">
<div class="field-label">
<div class="field-title">Context window space for utility model input</div>
<div class="field-description">Portion of context window used for utility model input messages.</div>
</div>
<div class="field-control">
<input type="range" min="0.01" max="1" step="0.01" x-model.number="model.ctx_input" />
<span class="range-value" x-text="model.ctx_input"></span>
</div>
</div>
</template>
<!-- Rate limits -->
<div class="field">
<div class="field-label">
<div class="field-title">Requests per minute limit</div>
<div class="field-description">Limits the number of requests per minute. Waits if the limit is exceeded. Set to 0 to disable.</div>
</div>
<div class="field-control">
<input type="number" x-model.number="model.rl_requests" />
</div>
</div>
<div class="field">
<div class="field-label">
<div class="field-title">Input tokens per minute limit</div>
<div class="field-description">Limits the number of input tokens per minute. Waits if the limit is exceeded. Set to 0 to disable.</div>
</div>
<div class="field-control">
<input type="number" x-model.number="model.rl_input" />
</div>
</div>
<template x-if="modelType !== 'embedding'">
<div class="field">
<div class="field-label">
<div class="field-title">Output tokens per minute limit</div>
<div class="field-description">Limits the number of output tokens per minute. Waits if the limit is exceeded. Set to 0 to disable.</div>
</div>
<div class="field-control">
<input type="number" x-model.number="model.rl_output" />
</div>
</div>
</template>
<!-- Additional parameters -->
<div class="field field-full">
<div class="field-label">
<div class="field-title">Additional parameters</div>
<div class="field-description">
Any other parameters supported by <a href='https://docs.litellm.ai/docs/set_keys' target='_blank'>LiteLLM</a>. Format is KEY=VALUE on individual lines. Value can be JSON objects; unquoted is treated as object/number, quoted as string.
</div>
</div>
<div class="field-control">
<textarea x-model="model._kwargs_text"
@change="model.kwargs = $store.modelConfig.textToKwargs(model._kwargs_text)"></textarea>
</div>
</div>
</div>
</div>
</div>
<style>
.eye-toggle {
position: absolute;
right: 8px;
top: 50%;
transform: translateY(-50%);
font-size: 18px;
cursor: pointer;
user-select: none;
opacity: 0.6;
z-index: 1;
}
.eye-toggle:hover {
opacity: 1;
}
.model-search-btn {
position: absolute;
right: 8px;
top: 50%;
transform: translateY(-50%);
width: 20px;
height: 20px;
display: grid;
place-items: center;
cursor: pointer;
user-select: none;
opacity: 0.6;
z-index: 1;
}
.model-search-btn:hover {
opacity: 1;
}
.model-search-btn > span {
grid-area: 1 / 1;
font-size: 18px;
transition: opacity 0.15s;
}
.model-search-spinner {
animation: spin 0.8s linear infinite;
}
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
.model-search-results {
position: absolute;
top: calc(100% + 4px);
left: 0;
right: 0;
max-height: 200px;
overflow-y: auto;
background: var(--color-input);
border: 1px solid var(--color-border);
border-radius: 6px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
z-index: 50;
padding: 4px;
}
.model-search-item {
padding: 5px 8px;
font-size: 0.8rem;
border-radius: 4px;
cursor: pointer;
word-break: break-all;
}
.model-search-item:hover {
background: var(--color-background-hover, rgba(255,255,255,0.06));
}
.model-search-item.disabled {
opacity: 0.4;
cursor: default;
font-style: italic;
}
.model-search-item.matched {
font-weight: 500;
}
.model-search-separator {
height: 1px;
margin: 4px 8px;
background: var(--color-border);
opacity: 0.5;
}
.model-search-item.disabled:hover {
background: transparent;
}
.advanced-section {
margin-top: 4px;
}
.advanced-toggle {
display: flex;
align-items: center;
gap: 4px;
font-size: 0.78rem;
opacity: 0.6;
cursor: pointer;
user-select: none;
padding: 4px 0;
}
.advanced-toggle:hover {
opacity: 1;
}
.advanced-toggle-icon {
font-size: 16px;
transition: transform 0.15s ease;
}
.advanced-body {
}
</style>
</body>
</html>