From cc229b4b6d221baaeddfeb7bf3cf00859b59115e Mon Sep 17 00:00:00 2001 From: Aleksander Grygier Date: Mon, 28 Sep 2026 15:46:45 +0200 Subject: [PATCH] ui : add the manage providers view A provider is added from a preset card or by hand: the dialog probes the endpoint, reads a refusal as a llama.cpp server that wants a key, and shows the connection test as a status block. Presets carry their official artwork and a saved backend keeps its branding through the favicon fallback. The manager dialog gains its third view, and the settings save stops clobbering keys that other surfaces write. Assisted-by: pi:llama.cpp/DeepSeek-V4.1-Flash --- .../app/backends/BackendCard.svelte | 163 ++++++++ .../app/backends/BackendForm.svelte | 190 ++++++++++ .../app/backends/BackendIcon.svelte | 37 ++ .../app/backends/BackendPresetCard.svelte | 41 ++ .../app/backends/BackendPresetIcon.svelte | 34 ++ .../app/backends/DialogBackendForm.svelte | 356 ++++++++++++++++++ .../src/lib/components/app/backends/index.ts | 6 + .../app/dialogs/DialogManageModels.svelte | 33 +- tools/ui/src/lib/components/app/index.ts | 1 + .../ModelsManagerModelProviders.svelte | 108 ++++++ .../settings/SettingsChat/SettingsChat.svelte | 17 +- .../SettingsChat/SettingsChatFields.svelte | 2 +- .../src/lib/components/app/settings/index.ts | 5 + tools/ui/src/lib/components/ui/tabs/index.ts | 18 + .../components/ui/tabs/tabs-content.svelte | 17 + .../lib/components/ui/tabs/tabs-list.svelte | 40 ++ .../components/ui/tabs/tabs-trigger.svelte | 23 ++ .../ui/src/lib/components/ui/tabs/tabs.svelte | 19 + .../ui/src/lib/constants/backend.constants.ts | 35 +- .../ui/src/lib/constants/storage.constants.ts | 1 + tools/ui/src/lib/utils/backend.ts | 32 +- tools/ui/src/lib/utils/index.ts | 1 + tools/ui/static/backend-presets/deepseek.svg | 1 + tools/ui/static/backend-presets/google.svg | 10 + .../ui/static/backend-presets/huggingface.svg | 37 ++ tools/ui/static/backend-presets/kimi.png | Bin 0 -> 1835 bytes .../backend-presets/openrouter-dark.svg | 5 + .../backend-presets/openrouter-light.svg | 5 + tools/ui/static/backend-presets/qwen.png | Bin 0 -> 1658 bytes tools/ui/static/backend-presets/zai.png | Bin 0 -> 1064 bytes 30 files changed, 1228 insertions(+), 9 deletions(-) create mode 100644 tools/ui/src/lib/components/app/backends/BackendCard.svelte create mode 100644 tools/ui/src/lib/components/app/backends/BackendForm.svelte create mode 100644 tools/ui/src/lib/components/app/backends/BackendIcon.svelte create mode 100644 tools/ui/src/lib/components/app/backends/BackendPresetCard.svelte create mode 100644 tools/ui/src/lib/components/app/backends/BackendPresetIcon.svelte create mode 100644 tools/ui/src/lib/components/app/backends/DialogBackendForm.svelte create mode 100644 tools/ui/src/lib/components/app/backends/index.ts create mode 100644 tools/ui/src/lib/components/app/models/ModelsManager/ModelsManagerModelProviders.svelte create mode 100644 tools/ui/src/lib/components/ui/tabs/index.ts create mode 100644 tools/ui/src/lib/components/ui/tabs/tabs-content.svelte create mode 100644 tools/ui/src/lib/components/ui/tabs/tabs-list.svelte create mode 100644 tools/ui/src/lib/components/ui/tabs/tabs-trigger.svelte create mode 100644 tools/ui/src/lib/components/ui/tabs/tabs.svelte create mode 100644 tools/ui/static/backend-presets/deepseek.svg create mode 100644 tools/ui/static/backend-presets/google.svg create mode 100644 tools/ui/static/backend-presets/huggingface.svg create mode 100644 tools/ui/static/backend-presets/kimi.png create mode 100644 tools/ui/static/backend-presets/openrouter-dark.svg create mode 100644 tools/ui/static/backend-presets/openrouter-light.svg create mode 100644 tools/ui/static/backend-presets/qwen.png create mode 100644 tools/ui/static/backend-presets/zai.png diff --git a/tools/ui/src/lib/components/app/backends/BackendCard.svelte b/tools/ui/src/lib/components/app/backends/BackendCard.svelte new file mode 100644 index 0000000000..2067f3f46e --- /dev/null +++ b/tools/ui/src/lib/components/app/backends/BackendCard.svelte @@ -0,0 +1,163 @@ + + + +
+
+ + {#snippet fallback()} + {#if isLocal} + + {:else} + + {/if} + {/snippet} + + +
+

{backend.name}

+ +

{displayUrl}

+
+
+ + onToggle?.(value)} + /> +
+ + {#if isOffline && modelsState.error} + + +

{modelsState.error}

+
+ + +

{modelsState.error}

+
+
+ {/if} + +
+
+ {#if isChecking} + + + Checking + {:else if isOnline} + + + {modelCount === 1 ? '1 model' : `${modelCount} models`} + {:else if isOffline} + + + Unreachable + + + {/if} + + {#if hasStatus} + · + {/if} + + {metaText} +
+ + {#if !isLocal} +
+ + + +
+ {/if} +
+
+ + (showDelete = false)} + onConfirm={() => { + showDelete = false; + onDelete?.(); + }} + title="Delete backend?" + variant="destructive" +/> diff --git a/tools/ui/src/lib/components/app/backends/BackendForm.svelte b/tools/ui/src/lib/components/app/backends/BackendForm.svelte new file mode 100644 index 0000000000..28f2114e84 --- /dev/null +++ b/tools/ui/src/lib/components/app/backends/BackendForm.svelte @@ -0,0 +1,190 @@ + + +
+
+ + + onChange({ baseUrl: e.currentTarget.value })} + placeholder="https://api.example.com" + type="url" + value={backend.baseUrl} + /> + + {#if urlError} +

{urlError}

+ {:else if detectionLabel} +

{detectionLabel}

+ {/if} +
+ +
+
+ + + onChange({ name: e.currentTarget.value })} + placeholder="Name shown in the model selector" + type="text" + value={backend.name} + /> +
+ +
+ + + onChange({ apiKey: e.currentTarget.value || undefined })} + placeholder="Optional" + type="password" + value={backend.apiKey ?? ''} + /> + +

+ {#if apiKeyRequired} + Required by this endpoint. + {:else} + Sent as a Bearer token. + {/if} +

+
+
+ + + + {#if showAdvanced} + + {:else} + + {/if} + + Advanced + + + +
+
+ API format + + + onChange({ compat: undefined, protocol: value as BackendProtocol })} + type="single" + value={backend.protocol} + > + {protocolLabel} + + + {#each PROTOCOL_OPTIONS as option (option.value)} + {option.label} + {/each} + + + +

+ Detected from the endpoint. Override it when a server answers unusually. +

+
+ +
+
+ + + onChange({ chatPath: e.currentTarget.value || undefined })} + placeholder={DEFAULT_BACKEND_CHAT_PATH} + type="text" + value={backend.chatPath ?? ''} + /> +
+ +
+ + + onChange({ modelsPath: e.currentTarget.value || undefined })} + placeholder={DEFAULT_BACKEND_MODELS_PATH} + type="text" + value={backend.modelsPath ?? ''} + /> +
+
+
+
+
+
diff --git a/tools/ui/src/lib/components/app/backends/BackendIcon.svelte b/tools/ui/src/lib/components/app/backends/BackendIcon.svelte new file mode 100644 index 0000000000..d9ed53a518 --- /dev/null +++ b/tools/ui/src/lib/components/app/backends/BackendIcon.svelte @@ -0,0 +1,37 @@ + + +{#if preset} + +{:else if faviconUrl && !failedFavicons.has(faviconUrl)} + failedFavicons.add(faviconUrl)} + src={faviconUrl} + /> +{:else} + {@render fallback?.()} +{/if} diff --git a/tools/ui/src/lib/components/app/backends/BackendPresetCard.svelte b/tools/ui/src/lib/components/app/backends/BackendPresetCard.svelte new file mode 100644 index 0000000000..bcdc400cb4 --- /dev/null +++ b/tools/ui/src/lib/components/app/backends/BackendPresetCard.svelte @@ -0,0 +1,41 @@ + + + + {#if added} + + + Added + + {/if} + +
+ + +

{preset.name}

+
+ +

{preset.description}

+
diff --git a/tools/ui/src/lib/components/app/backends/BackendPresetIcon.svelte b/tools/ui/src/lib/components/app/backends/BackendPresetIcon.svelte new file mode 100644 index 0000000000..dccb9ff583 --- /dev/null +++ b/tools/ui/src/lib/components/app/backends/BackendPresetIcon.svelte @@ -0,0 +1,34 @@ + + +{#if iconUrl} + +{:else} + +{/if} diff --git a/tools/ui/src/lib/components/app/backends/DialogBackendForm.svelte b/tools/ui/src/lib/components/app/backends/DialogBackendForm.svelte new file mode 100644 index 0000000000..9cf2b627f3 --- /dev/null +++ b/tools/ui/src/lib/components/app/backends/DialogBackendForm.svelte @@ -0,0 +1,356 @@ + + + + + + + {isEdit ? 'Edit Provider' : 'Add New Provider'} + + + + Point at another llama-server, or connect an OpenAI-compatible endpoint. + + + + {#if !isEdit && presetsToShow.length > 0} +
+
+

Recommended providers

+ + +
+ +
+ {#each presetsToShow as preset (preset.id)} + applyPreset(preset)} + {preset} + selected={selectedPresetId === preset.id} + /> + {/each} +
+
+ {/if} + +
+
+ +
+ + +
+ {#if testing} + + +
+

Testing connection

+ +

+ Asking {hostOf(draft.baseUrl)} for the models it serves. +

+
+ {:else if testResult?.ok} + + +
+

Connected

+ +

+ {testResult.modelCount ?? 0} + model{(testResult.modelCount ?? 0) === 1 ? '' : 's'} available. +

+
+ {:else if testResult} + + +
+

Connection failed

+ +

+ {testResult.error ?? 'The endpoint did not answer.'}{testResult.status + ? ` (HTTP ${testResult.status})` + : ''} +

+
+ {:else} + + +
+

Connection not tested

+ +

+ Test it to list the models this provider serves. +

+
+ {/if} +
+ + + + + + + + +
+
+
diff --git a/tools/ui/src/lib/components/app/backends/index.ts b/tools/ui/src/lib/components/app/backends/index.ts new file mode 100644 index 0000000000..529bc7fc36 --- /dev/null +++ b/tools/ui/src/lib/components/app/backends/index.ts @@ -0,0 +1,6 @@ +export { default as BackendCard } from './BackendCard.svelte'; +export { default as BackendForm } from './BackendForm.svelte'; +export { default as BackendIcon } from './BackendIcon.svelte'; +export { default as BackendPresetCard } from './BackendPresetCard.svelte'; +export { default as BackendPresetIcon } from './BackendPresetIcon.svelte'; +export { default as DialogBackendForm } from './DialogBackendForm.svelte'; diff --git a/tools/ui/src/lib/components/app/dialogs/DialogManageModels.svelte b/tools/ui/src/lib/components/app/dialogs/DialogManageModels.svelte index 0b12310034..a466ad0533 100644 --- a/tools/ui/src/lib/components/app/dialogs/DialogManageModels.svelte +++ b/tools/ui/src/lib/components/app/dialogs/DialogManageModels.svelte @@ -1,7 +1,8 @@ + +
+ void backendsModelsStore.loadAll()} + /> + + {#if showsLocal && !serverStore.localServerMissing} + backendsStore.setLocalEnabled(enabled)} + /> + {/if} + + {#each backends as backend (backend.id)} + { + backendsStore.removeBackend(backend.id); + void backendsModelsStore.loadAll(); + }} + onEdit={() => handleEdit(backend)} + onToggle={(enabled) => { + backendsStore.updateBackend(backend.id, { enabled }); + void backendsModelsStore.loadAll(); + }} + /> + {/each} + +
+ + +

{addDescription}

+
+
diff --git a/tools/ui/src/lib/components/app/settings/SettingsChat/SettingsChat.svelte b/tools/ui/src/lib/components/app/settings/SettingsChat/SettingsChat.svelte index 8793900759..a128821142 100644 --- a/tools/ui/src/lib/components/app/settings/SettingsChat/SettingsChat.svelte +++ b/tools/ui/src/lib/components/app/settings/SettingsChat/SettingsChat.svelte @@ -47,12 +47,17 @@ let localConfig: SettingsConfigType = $state({ ...settingsStore.config }); + // Snapshot used to persist only the fields edited in this dialog. Config keys + // that other surfaces write directly (backends, MCP servers) must not be + // clobbered by the dialog's save. + let baseline: SettingsConfigType = $state({ ...settingsStore.config }); + let mobileHeader: { updateCarousel: () => void } | undefined; let fetchInitiated = false; $effect(() => { - if (serverStore.isRouterMode && currentSection.fields?.length && !fetchInitiated) { + if (serverStore.localIsRouter && currentSection.fields?.length && !fetchInitiated) { fetchInitiated = true; void modelsStore @@ -74,6 +79,7 @@ function handleReset() { localConfig = { ...settingsStore.config }; + baseline = { ...settingsStore.config }; setMode(localConfig.theme as ColorMode); mobileHeader?.updateCarousel(); } @@ -94,7 +100,13 @@ } } - const processedConfig = { ...localConfig }; + const processedConfig: SettingsConfigType = {}; + + for (const [key, value] of Object.entries(localConfig)) { + if (value !== baseline[key]) { + processedConfig[key] = value; + } + } for (const field of NUMERIC_FIELDS) { if (processedConfig[field] !== undefined && processedConfig[field] !== '') { @@ -126,6 +138,7 @@ export function reset() { localConfig = { ...settingsStore.config }; + baseline = { ...settingsStore.config }; } diff --git a/tools/ui/src/lib/components/app/settings/SettingsChat/SettingsChatFields.svelte b/tools/ui/src/lib/components/app/settings/SettingsChat/SettingsChatFields.svelte index e8795d7c52..f77b8089cc 100644 --- a/tools/ui/src/lib/components/app/settings/SettingsChat/SettingsChatFields.svelte +++ b/tools/ui/src/lib/components/app/settings/SettingsChat/SettingsChatFields.svelte @@ -25,7 +25,7 @@ let currentModelParams = $derived.by(() => { void modelsStore.props.cacheVersion; - if (serverStore.isRouterMode) { + if (serverStore.localIsRouter) { const currentModelName = modelsStore.selectedModelName; if (currentModelName) { diff --git a/tools/ui/src/lib/components/app/settings/index.ts b/tools/ui/src/lib/components/app/settings/index.ts index 7473e6af8e..899e34392d 100644 --- a/tools/ui/src/lib/components/app/settings/index.ts +++ b/tools/ui/src/lib/components/app/settings/index.ts @@ -74,3 +74,8 @@ export { default as SettingsChatFields } from './SettingsChat/SettingsChatFields * server favicons and permission management controls. */ export { default as SettingsChatToolsTab } from './SettingsChat/SettingsChatToolsTab.svelte'; + +/** + * Providers panel for chat settings. Switches between the llama.cpp and + * OpenAI-compatible protocols and lists the endpoints configured for each. + */ diff --git a/tools/ui/src/lib/components/ui/tabs/index.ts b/tools/ui/src/lib/components/ui/tabs/index.ts new file mode 100644 index 0000000000..95bbb36763 --- /dev/null +++ b/tools/ui/src/lib/components/ui/tabs/index.ts @@ -0,0 +1,18 @@ +import Root from './tabs.svelte'; +import Content from './tabs-content.svelte'; +import List, { type TabsListVariant, tabsListVariants } from './tabs-list.svelte'; +import Trigger from './tabs-trigger.svelte'; + +export { + Root, + Content, + List, + Trigger, + tabsListVariants, + type TabsListVariant, + // + Root as Tabs, + Content as TabsContent, + List as TabsList, + Trigger as TabsTrigger +}; diff --git a/tools/ui/src/lib/components/ui/tabs/tabs-content.svelte b/tools/ui/src/lib/components/ui/tabs/tabs-content.svelte new file mode 100644 index 0000000000..fae73d9199 --- /dev/null +++ b/tools/ui/src/lib/components/ui/tabs/tabs-content.svelte @@ -0,0 +1,17 @@ + + + diff --git a/tools/ui/src/lib/components/ui/tabs/tabs-list.svelte b/tools/ui/src/lib/components/ui/tabs/tabs-list.svelte new file mode 100644 index 0000000000..32c7f87a65 --- /dev/null +++ b/tools/ui/src/lib/components/ui/tabs/tabs-list.svelte @@ -0,0 +1,40 @@ + + + + + diff --git a/tools/ui/src/lib/components/ui/tabs/tabs-trigger.svelte b/tools/ui/src/lib/components/ui/tabs/tabs-trigger.svelte new file mode 100644 index 0000000000..be798b51ee --- /dev/null +++ b/tools/ui/src/lib/components/ui/tabs/tabs-trigger.svelte @@ -0,0 +1,23 @@ + + + diff --git a/tools/ui/src/lib/components/ui/tabs/tabs.svelte b/tools/ui/src/lib/components/ui/tabs/tabs.svelte new file mode 100644 index 0000000000..fcd40003f7 --- /dev/null +++ b/tools/ui/src/lib/components/ui/tabs/tabs.svelte @@ -0,0 +1,19 @@ + + + diff --git a/tools/ui/src/lib/constants/backend.constants.ts b/tools/ui/src/lib/constants/backend.constants.ts index 1f3ab2e394..100ee17dcd 100644 --- a/tools/ui/src/lib/constants/backend.constants.ts +++ b/tools/ui/src/lib/constants/backend.constants.ts @@ -1,4 +1,9 @@ -import type { BackendCapabilities, BackendCompat, BackendProtocol } from '$lib/types'; +import type { + BackendCapabilities, + BackendCompat, + BackendPreset, + BackendProtocol +} from '$lib/types'; /** Prefix for generated ids of user-added backends. */ export const BACKEND_ID_PREFIX = 'backend'; @@ -64,3 +69,31 @@ export const MODEL_CONTEXT_LENGTH_FIELDS = [ /** Favicon extract keyed by domain, for backends with no bundled mark. */ export const FAVICON_SERVICE_URL = 'https://www.google.com/s2/favicons?domain='; + +/** + * Ready-made endpoints offered when adding a backend. + * + * TODO: pair a llama.cpp server by scanning its QR code (WebRTC transport, so a + * hosted PWA can reach a server on the user's network), see + * ggml-org/llama.cpp#24577. + */ +export const BACKEND_PRESETS: readonly BackendPreset[] = [ + { + baseUrl: 'https://router.huggingface.co', + description: 'Open models from the Hub, served by inference providers.', + iconUrl: '/backend-presets/huggingface.svg', + id: 'huggingface', + name: 'Hugging Face', + protocol: 'openai' + }, + { + baseUrl: 'https://openrouter.ai/api', + description: 'Models from many providers behind a single API.', + // v2 brand: purple on light, lime on dark + iconUrlDark: '/backend-presets/openrouter-dark.svg', + iconUrlLight: '/backend-presets/openrouter-light.svg', + id: 'openrouter', + name: 'OpenRouter', + protocol: 'openai' + } +]; diff --git a/tools/ui/src/lib/constants/storage.constants.ts b/tools/ui/src/lib/constants/storage.constants.ts index f66a0a0112..8c6cb3462e 100644 --- a/tools/ui/src/lib/constants/storage.constants.ts +++ b/tools/ui/src/lib/constants/storage.constants.ts @@ -52,6 +52,7 @@ export const REASONING_EFFORT_DEFAULT_LOCALSTORAGE_KEY = `${STORAGE_APP_NAME}.re export const CONVERSATION_TABS_LOCALSTORAGE_KEY = `${STORAGE_APP_NAME}.conversationTabs`; export const USER_OVERRIDES_LOCALSTORAGE_KEY = `${STORAGE_APP_NAME}.userOverrides`; export const DISMISSED_RECOMMENDED_MCP_SERVERS_LOCALSTORAGE_KEY = `${STORAGE_APP_NAME}.dismissedRecommendedMcpServers`; +export const DISMISSED_RECOMMENDED_BACKENDS_LOCALSTORAGE_KEY = `${STORAGE_APP_NAME}.dismissedRecommendedBackends`; /** Key prefix for per-conversation resumable stream state, conversationId is appended */ export const STREAM_RESUME_LOCALSTORAGE_KEY_PREFIX = `${STORAGE_APP_NAME}.streamResume.`; diff --git a/tools/ui/src/lib/utils/backend.ts b/tools/ui/src/lib/utils/backend.ts index 55b671bbfc..29c68558f7 100644 --- a/tools/ui/src/lib/utils/backend.ts +++ b/tools/ui/src/lib/utils/backend.ts @@ -10,6 +10,7 @@ import { BACKEND_CAPABILITIES, BACKEND_COMPAT, BACKEND_ID_PREFIX, + BACKEND_PRESETS, BACKEND_PROTOCOLS, DEFAULT_BACKEND_CHAT_PATH, DEFAULT_BACKEND_MODELS_PATH, @@ -17,7 +18,13 @@ import { LOCAL_BACKEND_ID, MODEL_CONTEXT_LENGTH_FIELDS } from '$lib/constants'; -import type { Backend, BackendCapabilities, BackendCompat, BackendProtocol } from '$lib/types'; +import type { + Backend, + BackendCapabilities, + BackendCompat, + BackendPreset, + BackendProtocol +} from '$lib/types'; /** Absolute chat completions URL for a backend. */ export function backendChatUrl(backend: Backend): string { @@ -49,6 +56,29 @@ export function backendFaviconUrl(baseUrl: string): string | null { } } +/** + * Preset a backend was created from, matched on origin and path so a saved + * backend keeps its branding. Returns undefined for edited or custom URLs. + */ +export function findBackendPreset(baseUrl: string): BackendPreset | undefined { + const target = normalizeBaseUrl(baseUrl); + + if (!target) return undefined; + + return BACKEND_PRESETS.find((preset) => normalizeBaseUrl(preset.baseUrl) === target); +} + +/** Origin and path of a URL, without a trailing slash; null when unparsable. */ +function normalizeBaseUrl(url: string): string | null { + try { + const parsed = new URL(url); + + return `${parsed.origin}${parsed.pathname.replace(/\/+$/, '')}`.toLowerCase(); + } catch { + return null; + } +} + /** * Features a backend supports, derived from its protocol. A missing backend * (unknown model, early startup) gets the plain compatible defaults. diff --git a/tools/ui/src/lib/utils/index.ts b/tools/ui/src/lib/utils/index.ts index 3c8a3caf8c..ca22ac3340 100644 --- a/tools/ui/src/lib/utils/index.ts +++ b/tools/ui/src/lib/utils/index.ts @@ -29,6 +29,7 @@ export { backendFaviconUrl, backendModelsUrl, createLocalBackend, + findBackendPreset, getBackendCapabilities, parseBackendsSettings } from './backend'; diff --git a/tools/ui/static/backend-presets/deepseek.svg b/tools/ui/static/backend-presets/deepseek.svg new file mode 100644 index 0000000000..185c108b45 --- /dev/null +++ b/tools/ui/static/backend-presets/deepseek.svg @@ -0,0 +1 @@ +DeepSeek \ No newline at end of file diff --git a/tools/ui/static/backend-presets/google.svg b/tools/ui/static/backend-presets/google.svg new file mode 100644 index 0000000000..4545c8331f --- /dev/null +++ b/tools/ui/static/backend-presets/google.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/tools/ui/static/backend-presets/huggingface.svg b/tools/ui/static/backend-presets/huggingface.svg new file mode 100644 index 0000000000..6b3b4f687e --- /dev/null +++ b/tools/ui/static/backend-presets/huggingface.svg @@ -0,0 +1,37 @@ + + + + + + + + + + + diff --git a/tools/ui/static/backend-presets/kimi.png b/tools/ui/static/backend-presets/kimi.png new file mode 100644 index 0000000000000000000000000000000000000000..213b9b83d6cd30fd57077aa18244dfc4e6fd0d14 GIT binary patch literal 1835 zcmV+`2h{k9P)Px#L}ge>W=%~1DgXcg2mk?xX#fNO00031000^Q000001E2u_0{{R30RRC20H6W@ z1ONa40RR91FrWhf1ONa40RR91FaQ7m0NXcg3;+NIl}SWFRA>d&T1!lnR~Y^N8HTB) zfGCjK)Jl*bY}n9ZN>fW>jKIc~3%hV*!iJU6u)t*(MnfPmftA{Xg_|al7#B?eF+h_V z(y(b-!a_ogmVhV-$N)3*^_)B0ab{p1%nT22a^`>B*ZE%e-v8clR8=CAgM?={Pz2Nh zKLFnXRX{PoyxmMeG(HDR0wcihz!>lr2!L-iVLn@+0Lc{sHNbDcDzE_r05;6Zdja;v zKG`=p3m3Yrua18`ul7sq{fEOpsK2=v%U=@FK}wY<*i?LBa8S0@Bo}r306N71ZyG@g7r)g^DRaO0L6> z-EOz!=H{B({0RgCvazut!62=fINAChj|a-N1VeTFe!pz^(S~tx>*rYF_}689>Zxo@ zkD}nVeYhz=N+~BNM^2qOCAV(f65cZ{EG$TCYpc9|{W@VABwSQfBv-Cnk+WydMhURC zwkD4sKbHIVf0p3NtoZ)=MI4t0>$9K)X8)Ge@%s|=EkHOc*w(I?lN8XrottLUKDPlot>RV)3M}7M@O}yqC(-0nULQd0j%b4-@dKu>+4%I z|LoZ_BOHZHRe=f0W!D1qdAdzkS638y`i^xQ#*Oj$6s@0te0D8B@<)#zHRKtf)@?|h z!T;pR6RoVQ)GTS7^1JS{-YKYaL5j~_q28{{){p}Dy^J$dqEm^?LY-NurC^yrb&=dRmR81w~RIvfI-~U)TErj86QbTl1%c` z7l3ZUC4qGt61R!(xzuQDYtth~j_jrt(igyzjd#UrRhO5S^}&M&T2)nLtg#E0$c_Ni zIM+K2WD<`|9Em0-CbXfULG$wRvh7lt2|)MZ0&jA1QZHP%pjWS6)mN`xMJ@U$3v{m>zOlWbZl(Q2oV_{yr(sWhK97JrY2hf zp1uIo>htH%wY^ZriHo!I<;$0ig(ANrtV=OwXsJ7Q?wId_Jr{s8F|{ABV+wNU&>^j_uQv<1*ae=A zA1dPF#fx#g96H|LcHWJ&T;OPjz**^uCWDNWPY`br_S@57<$l2^pKgEsz zpt4NBi=26eY}|*(H6Pm%)8HoekNc31!zY6Q8}J4VxF#J^QD#3Ahbo>G4*@uGU literal 0 HcmV?d00001 diff --git a/tools/ui/static/backend-presets/openrouter-dark.svg b/tools/ui/static/backend-presets/openrouter-dark.svg new file mode 100644 index 0000000000..5df05ac315 --- /dev/null +++ b/tools/ui/static/backend-presets/openrouter-dark.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/tools/ui/static/backend-presets/openrouter-light.svg b/tools/ui/static/backend-presets/openrouter-light.svg new file mode 100644 index 0000000000..8fbbb87178 --- /dev/null +++ b/tools/ui/static/backend-presets/openrouter-light.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/tools/ui/static/backend-presets/qwen.png b/tools/ui/static/backend-presets/qwen.png new file mode 100644 index 0000000000000000000000000000000000000000..87a98a9526242daec54cd5d48a37eae55c5998eb GIT binary patch literal 1658 zcmV-=28H>FP)qlntr@V> z)dfm?aCph6ljCGORAVGWhPEj27u+a5m$*b?v|_-{k?zyK0*=t-axqdGwX#PvX-AfObw)nX(- zG!LXN-1CUM^(e{>rQV7>TIYD=;E^_$Zh@3h=uiGp?_6FI#W^sn2PRm%GU?6s z?Ue*G;B4>fV{>`UIVhwD7T9bG&OyNpm;*`acCKi}JO3RpsVkj2WGI`+`EjU*q&SZ# zHz=@yWK;ajI&Regd&l~G??^vY(bV!sdDD}rGHg0srtt=w2$hL?2iLqq8z2w5BoDd* z7fpSOM>Y82J&v@ge|y+0&M1w!`n1Xz-R>ow3~+_);dn})5FGd+PGWGE>5 znvMAiEz&drMnP|^G7PDqG=K+nX^dF)U`uOaVoFg{O|6;$qv!AGJkMOu)1JmqNQV$U zeFiB^Mxl7)bLKox={)b?h}D3zde*7Ew}O7`fa2NAVk^&=?YEZR5ExwAKo z2CO2IqdKtyr`7f0m##j=n$0W|_!KknvHHT&X^qZt5^7*vbOy&F5HG@NDwR_?Y2IaZ zT@jzbVFW&w0skjeMN$+sIhS-=6KDpfw-p&8pSqP}RGk zjb|wBD!tcun|QCcW_6k85^r?alGZqTpxIC1uH{Aa&es zsEZ|iyOp#2KdX29hzM;tFBczkGGNgCMmc(6sI;X4+r*Zu2ZofS3T|4{*6?O#(MZ__ zjI!Tordm_BFl%Ebwldr=KFl(pS#hnyswf8ARKwD_v~|x3wK;5=0#}VO`%@p0AT|8g zy5zc*JDwUQwpbphz-p7P(wKHTLCTD(&w!y)BB$bsZh2V zpJ#YMM2pJ$t3PgvuloH&l;j_6J7=k%|4|qrC{3P zbNd1p_RWSdA>+xFjz&evdH22f(B7}N#iB+IFHvTi3uE!>IxA^-+-=CT7dJ^)&Kwi literal 0 HcmV?d00001 diff --git a/tools/ui/static/backend-presets/zai.png b/tools/ui/static/backend-presets/zai.png new file mode 100644 index 0000000000000000000000000000000000000000..70c33481a4ce590c29759226284b89542de6fdd3 GIT binary patch literal 1064 zcmV+@1lRkCP)C0001HP)t-sEiEno z|NjvY5!Tk$%*@Qw)6)$N4JRij;Naj}TU!7C03#zKDk>@)8yg-T9tH*m?(XiSq@)%W z7GYsw`T6-&R8;8b=$e|Ee0+S1i;Hb-ZL6!Rw6wIpzrV)D#*&hfNJvOJIyy6c3kU!J z1AR$EK~#90?VIazqc9AGX)cDCI}}20ZFv8eTxc@c#28DFh1t$>es(nID~|0WVS67l z%Ub{zfCWJN*DP4GE69)lSze~8Sg}i_vdo7NDnQoA*!L!qAIoEH2FPVNRrZ@gxitGjn0oW`6 z3&1Y`h=ss-)7uj615xTuhX9DrL4a}Rm!2Rj&7r{|d>h~q0;uXXk*N4I2W9Hx`2hAs z7c0MjsH?iwu>l1ACYCfn)c0VoX9H-XZUCSHb3NQ@xd58pNXr0-@}sE$HvPX=?13nq z-RA-LOrmFVGop*mYVZJ*{p1$h&%r4h%D4}s`hcl29AU**34`byH4}jkS`8T801`mK# z8$ODD)`Hsg8l451OQD)jqQ080qjhOJ%FVNu?Da;A=UttP6#IesT0Bo z;9tiQ-vycwO90S>y!FlHeEja06r5!17I{EBmhnmLIOZ`LNJYO5&*6f;-{^-wO`h4gWej@gOgKLf3bSr|a12D_Suj3JDmrAU%*UPJn|16!0eY?x;n$jOh{fT zsaIEhbTA6ywcJr`{VARLs#mIXyj)*)Yt(XAcipd&s3X;HN(fmYuKV3o^W1TKmA=sU iN3VP?01LqX1n>uIghvXreKlGD0000