main
html 445 lines 17.7 KB
Raw
1 <html>
2 <head>
3 <script type="module">
4 import { store } from "/plugins/_model_config/webui/model-config-store.js";
5 </script>
6 </head>
7
8 <body>
9 <!--
10 Reusable model configuration field set.
11 Parent x-data scope must provide:
12 model — reactive object with provider, name, api_key, api_base, ctx_length, ctx_history, ctx_input, vision, max_embeds, timeout, max_tokens, rl_requests, rl_input, rl_output, kwargs, _kwargs_text
13 modelType — 'chat' | 'vision' | 'utility' | 'embedding'
14 providers — array of { value, label }
15 searchType — 'chat' | 'embedding'
16 apiKeyMode — 'store' (key lives in $store.modelConfig.apiKeyValues), 'inline' (key lives in model.api_key), or 'none'
17 Optional:
18 providerFallback — fallback provider string for search/API key status (e.g. preset.chat.provider for utility slot)
19 apiBaseFallback — fallback api_base string for search (e.g. preset.chat.api_base for utility slot)
20 visionModel — vision slot object, required by the chat model's sidecar switch
21 -->
22 <div x-data="{ get _prov() { return model.provider || (typeof providerFallback !== 'undefined' ? providerFallback : ''); }, get _apiBase() { return model.api_base || (typeof apiBaseFallback !== 'undefined' ? apiBaseFallback : ''); } }">
23 <!-- Provider -->
24 <div class="field">
25 <div class="field-label">
26 <div class="field-title">Provider</div>
27 <div class="field-description">LLM service provider for this model slot.</div>
28 </div>
29 <div class="field-control">
30 <select x-model="model.provider"
31 x-effect="$nextTick(() => { if (providers.length) $el.value = model.provider })"
32 @change="model.api_base = ''; model.kwargs = {}; model._kwargs_text = ''">
33 <option value="">&mdash; select &mdash;</option>
34 <template x-for="p in providers" :key="p.value">
35 <option :value="p.value" x-text="p.label"></option>
36 </template>
37 </select>
38 </div>
39 </div>
40
41 <!-- Model name + search -->
42 <div class="field">
43 <div class="field-label">
44 <div class="field-title">Model name</div>
45 <div class="field-description">Model identifier. Click the field or search icon to browse available models.</div>
46 </div>
47 <div class="field-control" style="position:relative;"
48 x-data="{ results: [], open: false, searching: false,
49 doSearch() { if (this.searching) return; this.searching = true; $store.modelConfig.searchModels(_prov, model.name, searchType, _apiBase).then(r => { this.results = r; this.open = true; }).finally(() => this.searching = false); },
50 openSearch(trigger) { trigger?.scrollIntoView({ block: 'center' }); this.doSearch(); },
51 grouped() { return $store.modelConfig.groupResults(this.results, model.name); }
52 }"
53 @click.outside="open = false">
54 <input type="text" x-model="model.name" style="padding-right:32px;"
55 @click="openSearch($el)"
56 @keydown.enter.prevent="doSearch()" />
57 <button class="model-search-btn"
58 type="button"
59 @click="openSearch($el)"
60 :disabled="searching"
61 title="Search available models"
62 aria-label="Search available models">
63 <x-icon :style="searching && 'opacity:0'" name="search"></x-icon>
64 <x-icon class="model-search-spinner" :style="!searching && 'opacity:0'" name="progress_activity"></x-icon>
65 </button>
66 <div class="model-search-results" x-show="open && results.length > 0" x-transition.opacity>
67 <template x-for="m in grouped().matched" :key="'m_'+m">
68 <div class="model-search-item matched" @click="model.name = m; open = false;" x-text="m"></div>
69 </template>
70 <div class="model-search-separator" x-show="grouped().matched.length > 0 && grouped().rest.length > 0"></div>
71 <template x-for="m in grouped().rest" :key="'r_'+m">
72 <div class="model-search-item" @click="model.name = m; open = false;" x-text="m"></div>
73 </template>
74 </div>
75 <div class="model-search-results" x-show="open && results.length === 0 && !searching">
76 <div class="model-search-item disabled">No models found</div>
77 </div>
78 </div>
79 </div>
80
81 <!-- API key (store mode: config.html) -->
82 <template x-if="apiKeyMode === 'store'">
83 <div class="field">
84 <div class="field-label">
85 <div class="field-title">API key</div>
86 <div class="field-description">Authentication key for this provider. Shared across all model slots using the same provider.</div>
87 </div>
88 <div class="field-control" style="position:relative;" x-data="{ showKey: false }">
89 <input type="text"
90 :class="{ 'masked-text-input': !showKey }"
91 :value="$store.modelConfig.apiKeyValues[_prov]"
92 :placeholder="$store.modelConfig.apiKeyStatus[_prov] ? '&#x2022;&#x2022;&#x2022;&#x2022;&#x2022;&#x2022;&#x2022;&#x2022;&#x2022;&#x2022;&#x2022;&#x2022;' : ''"
93 autocomplete="off"
94 @input="$store.modelConfig.setApiKeyValue(_prov, $el.value)"
95 style="padding-right:32px;" />
96 <x-icon class="eye-toggle"
97 @click="
98 showKey = !showKey;
99 const prov = _prov;
100 if (showKey && !$store.modelConfig.apiKeyValues[prov] && $store.modelConfig.apiKeyStatus[prov]) {
101 $store.modelConfig.revealApiKey(prov).then(v => { if (v) $store.modelConfig.apiKeyValues[prov] = v; });
102 }
103 "
104 :name="showKey ? 'visibility' : 'visibility_off'"></x-icon>
105 </div>
106 </div>
107 </template>
108
109 <!-- API key (inline mode: presets) -->
110 <template x-if="apiKeyMode === 'inline'">
111 <div class="field">
112 <div class="field-label">
113 <div class="field-title">API key</div>
114 <div class="field-description">Authentication key for this provider. Shared across all model slots using the same provider.</div>
115 </div>
116 <div class="field-control" style="position:relative;" x-data="{ showKey: false, _revealed: '' }">
117 <input type="text" :class="{ 'masked-text-input': !showKey }" x-model="model.api_key" autocomplete="off"
118 :placeholder="$store.modelConfig.apiKeyStatus[_prov] ? '&#x2022;&#x2022;&#x2022;&#x2022;&#x2022;&#x2022;&#x2022;&#x2022;&#x2022;&#x2022;&#x2022;&#x2022;' : ''"
119 style="padding-right:32px;" />
120 <x-icon class="eye-toggle"
121 @click="
122 showKey = !showKey;
123 if (showKey && !model.api_key && $store.modelConfig.apiKeyStatus[_prov]) {
124 $store.modelConfig.revealApiKey(_prov).then(v => { if (v) { model.api_key = v; _revealed = v; } });
125 }
126 if (!showKey && _revealed && model.api_key === _revealed) {
127 model.api_key = ''; _revealed = '';
128 }
129 "
130 :name="showKey ? 'visibility' : 'visibility_off'"></x-icon>
131 </div>
132 </div>
133 </template>
134
135 <!-- Vision support (chat only) -->
136 <template x-if="modelType === 'chat'">
137 <div class="field">
138 <div class="field-label">
139 <div class="field-title">Supports Vision</div>
140 <div class="field-description">Models capable of Vision can for example natively see the content of image attachments.</div>
141 </div>
142 <div class="field-control">
143 <label class="toggle">
144 <input type="checkbox" x-model="model.vision" />
145 <span class="toggler"></span>
146 </label>
147 </div>
148 </div>
149 </template>
150
151 <template x-if="modelType === 'chat' && model.vision">
152 <div class="field">
153 <div class="field-label">
154 <div class="field-title">Use separate Vision Model</div>
155 <div class="field-description">When disabled, vision_load uses this model's native vision.</div>
156 </div>
157 <div class="field-control">
158 <label class="toggle">
159 <input type="checkbox" x-model="visionModel.override_main" />
160 <span class="toggler"></span>
161 </label>
162 </div>
163 </div>
164 </template>
165
166 <!-- Context window size (main and utility only) -->
167 <template x-if="modelType === 'chat' || modelType === 'utility'">
168 <div class="field">
169 <div class="field-label">
170 <div class="field-title">Context window size</div>
171 <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>
172 </div>
173 <div class="field-control">
174 <input type="number" x-model.number="model.ctx_length" />
175 </div>
176 </div>
177 </template>
178
179 <!-- Advanced Settings (collapsed by default) -->
180 <div class="advanced-section" x-data="{ advOpen: false }">
181 <div class="advanced-toggle" @click="advOpen = !advOpen">
182 <x-icon class="advanced-toggle-icon"
183 :style="advOpen ? 'transform:rotate(90deg)' : ''" name="chevron_right"
184 ></x-icon>
185 <span>Advanced Settings</span>
186 </div>
187
188 <div class="advanced-body" x-show="advOpen" x-transition.opacity>
189 <!-- API base URL -->
190 <div class="field">
191 <div class="field-label">
192 <div class="field-title">API base URL</div>
193 <div class="field-description">Custom endpoint URL. Leave empty to use the provider's default.</div>
194 </div>
195 <div class="field-control">
196 <input type="text" x-model="model.api_base" />
197 </div>
198 </div>
199
200 <!-- Chat-specific: ctx_history, max_embeds -->
201 <template x-if="modelType === 'chat'">
202 <div>
203 <div class="field">
204 <div class="field-label">
205 <div class="field-title">Context window space for chat history</div>
206 <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>
207 </div>
208 <div class="field-control">
209 <input type="range" min="0.01" max="1" step="0.01" x-model.number="model.ctx_history" />
210 <span class="range-value" x-text="model.ctx_history"></span>
211 </div>
212 </div>
213 <template x-if="model.vision">
214 <div class="field">
215 <div class="field-label">
216 <div class="field-title">Max embeds</div>
217 <div class="field-description">Maximum number of embedded images used by the chat model. Set to 0 for unlimited.</div>
218 </div>
219 <div class="field-control">
220 <input type="number" min="0" x-model.number="model.max_embeds" x-init="if (!model.max_embeds) model.max_embeds = 10" />
221 </div>
222 </div>
223 </template>
224 </div>
225 </template>
226
227 <template x-if="modelType === 'vision'">
228 <div>
229 <div class="field">
230 <div class="field-label">
231 <div class="field-title">Max embeds</div>
232 <div class="field-description">Maximum number of images sent to the Vision Model in one call. Set to 0 for unlimited.</div>
233 </div>
234 <div class="field-control">
235 <input type="number" min="0" x-model.number="model.max_embeds" />
236 </div>
237 </div>
238
239 <div class="field">
240 <div class="field-label">
241 <div class="field-title">Timeout (seconds)</div>
242 <div class="field-description">How long to wait for the Vision Model.</div>
243 </div>
244 <div class="field-control">
245 <input type="number" min="1" x-model.number="model.timeout" />
246 </div>
247 </div>
248
249 <div class="field">
250 <div class="field-label">
251 <div class="field-title">Max tokens</div>
252 <div class="field-description">Maximum output tokens for each delegated vision call.</div>
253 </div>
254 <div class="field-control">
255 <input type="number" min="1" x-model.number="model.max_tokens" />
256 </div>
257 </div>
258
259 <p class="field-description">To customize the vision prompt, use Agent Editor &gt; Advanced &gt; Prompt files and edit <code>fw.vision_load.md</code>.</p>
260 </div>
261 </template>
262
263 <!-- Utility-specific: ctx_input slider -->
264 <template x-if="modelType === 'utility'">
265 <div class="field">
266 <div class="field-label">
267 <div class="field-title">Context window space for utility model input</div>
268 <div class="field-description">Portion of context window used for utility model input messages.</div>
269 </div>
270 <div class="field-control">
271 <input type="range" min="0.01" max="1" step="0.01" x-model.number="model.ctx_input" />
272 <span class="range-value" x-text="model.ctx_input"></span>
273 </div>
274 </div>
275 </template>
276
277 <!-- Rate limits -->
278 <div class="field">
279 <div class="field-label">
280 <div class="field-title">Requests per minute limit</div>
281 <div class="field-description">Limits the number of requests per minute. Waits if the limit is exceeded. Set to 0 to disable.</div>
282 </div>
283 <div class="field-control">
284 <input type="number" x-model.number="model.rl_requests" />
285 </div>
286 </div>
287
288 <div class="field">
289 <div class="field-label">
290 <div class="field-title">Input tokens per minute limit</div>
291 <div class="field-description">Limits the number of input tokens per minute. Waits if the limit is exceeded. Set to 0 to disable.</div>
292 </div>
293 <div class="field-control">
294 <input type="number" x-model.number="model.rl_input" />
295 </div>
296 </div>
297
298 <template x-if="modelType !== 'embedding'">
299 <div class="field">
300 <div class="field-label">
301 <div class="field-title">Output tokens per minute limit</div>
302 <div class="field-description">Limits the number of output tokens per minute. Waits if the limit is exceeded. Set to 0 to disable.</div>
303 </div>
304 <div class="field-control">
305 <input type="number" x-model.number="model.rl_output" />
306 </div>
307 </div>
308 </template>
309
310 <!-- Additional parameters -->
311 <div class="field field-full">
312 <div class="field-label">
313 <div class="field-title">Additional parameters</div>
314 <div class="field-description">
315 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.
316 </div>
317 </div>
318 <div class="field-control">
319 <textarea x-model="model._kwargs_text"
320 @change="model.kwargs = $store.modelConfig.textToKwargs(model._kwargs_text)"></textarea>
321 </div>
322 </div>
323
324 </div>
325 </div>
326 </div>
327
328 <style>
329 .eye-toggle {
330 position: absolute;
331 right: 8px;
332 top: 50%;
333 transform: translateY(-50%);
334 font-size: 18px;
335 cursor: pointer;
336 user-select: none;
337 opacity: 0.6;
338 z-index: 1;
339 }
340 .eye-toggle:hover {
341 opacity: 1;
342 }
343 .model-search-btn {
344 position: absolute;
345 right: 8px;
346 top: 50%;
347 transform: translateY(-50%);
348 width: 20px;
349 height: 20px;
350 padding: 0;
351 display: grid;
352 place-items: center;
353 border: 0;
354 background: transparent;
355 color: var(--color-text);
356 cursor: pointer;
357 user-select: none;
358 opacity: 0.6;
359 z-index: 1;
360 }
361 .model-search-btn:hover {
362 opacity: 1;
363 }
364 .model-search-btn:disabled {
365 cursor: default;
366 }
367 .model-search-btn > span {
368 grid-area: 1 / 1;
369 font-size: 18px;
370 transition: opacity 0.15s;
371 }
372 .model-search-spinner {
373 animation: spin 0.8s linear infinite;
374 }
375 @keyframes spin {
376 from { transform: rotate(0deg); }
377 to { transform: rotate(360deg); }
378 }
379 .model-search-results {
380 position: absolute;
381 top: calc(100% + 4px);
382 left: 0;
383 right: 0;
384 max-height: 200px;
385 overflow-y: auto;
386 background: var(--color-input);
387 border: 1px solid var(--color-border);
388 border-radius: 6px;
389 box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
390 z-index: 50;
391 padding: 4px;
392 }
393 .model-search-item {
394 padding: 5px 8px;
395 font-size: 0.8rem;
396 border-radius: 4px;
397 cursor: pointer;
398 word-break: break-all;
399 }
400 .model-search-item:hover {
401 background: var(--color-background-hover, rgba(255,255,255,0.06));
402 }
403 .model-search-item.disabled {
404 opacity: 0.4;
405 cursor: default;
406 font-style: italic;
407 }
408 .model-search-item.matched {
409 font-weight: 500;
410 }
411 .model-search-separator {
412 height: 1px;
413 margin: 4px 8px;
414 background: var(--color-border);
415 opacity: 0.5;
416 }
417 .model-search-item.disabled:hover {
418 background: transparent;
419 }
420
421 .advanced-section {
422 margin-top: 4px;
423 }
424 .advanced-toggle {
425 display: flex;
426 align-items: center;
427 gap: 4px;
428 font-size: 0.78rem;
429 opacity: 0.6;
430 cursor: pointer;
431 user-select: none;
432 padding: 4px 0;
433 }
434 .advanced-toggle:hover {
435 opacity: 1;
436 }
437 .advanced-toggle-icon {
438 font-size: 16px;
439 transition: transform 0.15s ease;
440 }
441 .advanced-body {
442 }
443 </style>
444 </body>
445 </html>