Spaces:
Paused
Paused
| <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 })"> | |
| <option value="">— select —</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> | |
| <!-- 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 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] ? '••••••••••••' : ''" | |
| 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] ? '••••••••••••' : ''" | |
| 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> | |
| <!-- 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, vision, 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> | |
| <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 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> | |