ui : brand the backend presets with provider logos

Assisted-by: pi:llama.cpp/DeepSeek-V4.1-Flash
This commit is contained in:
Aleksander Grygier
2026-09-16 23:20:55 +02:00
parent 709e871a6f
commit c441bb491b
19 changed files with 137 additions and 34 deletions
@@ -1,4 +1,5 @@
<script lang="ts">
import BackendPresetIcon from './BackendPresetIcon.svelte';
import { Pencil, Server, Trash2 } from '@lucide/svelte';
import { DialogConfirmation } from '$lib/components/app/dialogs';
import { Button } from '$lib/components/ui/button';
@@ -6,6 +7,7 @@
import { Switch } from '$lib/components/ui/switch';
import { ICON_CLASS_DEFAULT } from '$lib/constants';
import type { Backend, BackendProtocol } from '$lib/types';
import { findBackendPreset } from '$lib/utils';
const PROTOCOL_LABELS: Record<BackendProtocol, string> = {
anthropic: 'Anthropic',
@@ -26,12 +28,17 @@
let showDelete = $state(false);
let protocolLabel = $derived(PROTOCOL_LABELS[backend.protocol]);
let displayUrl = $derived(backend.baseUrl || 'This server');
let preset = $derived(findBackendPreset(backend.baseUrl));
</script>
<Card.Root class="!gap-3 bg-muted/30 p-4">
<div class="flex items-start justify-between gap-3">
<div class="flex min-w-0 items-center gap-2">
<Server class={ICON_CLASS_DEFAULT} />
{#if preset}
<BackendPresetIcon class={ICON_CLASS_DEFAULT} {preset} />
{:else}
<Server class={ICON_CLASS_DEFAULT} />
{/if}
<div class="min-w-0">
<p class="truncate text-sm font-medium">{backend.name}</p>
@@ -0,0 +1,34 @@
<script lang="ts">
import { Plug } from '@lucide/svelte';
import type { BackendPreset } from '$lib/types';
import { mode } from 'mode-watcher';
interface Props {
class?: string;
preset?: BackendPreset;
}
let { class: className = 'h-4 w-4', preset }: Props = $props();
let iconUrl = $derived.by(() => {
if (!preset) return null;
return (
(mode.current === 'dark'
? (preset.iconUrlDark ?? preset.iconUrl)
: (preset.iconUrlLight ?? preset.iconUrl)) ?? null
);
});
</script>
{#if iconUrl}
<img
alt=""
class={['shrink-0 rounded-sm object-contain', className]}
decoding="async"
loading="lazy"
src={iconUrl}
/>
{:else}
<Plug class={className} />
{/if}
@@ -1,5 +1,6 @@
<script lang="ts">
import BackendForm from './BackendForm.svelte';
import BackendPresetIcon from './BackendPresetIcon.svelte';
import { CheckCircle2, Loader2, XCircle } from '@lucide/svelte';
import { Button } from '$lib/components/ui/button';
import * as Dialog from '$lib/components/ui/dialog';
@@ -143,6 +144,8 @@
size="sm"
variant={selectedPresetId === preset.id ? 'secondary' : 'outline'}
>
<BackendPresetIcon class="h-4 w-4" {preset} />
{preset.name}
</Button>
{/each}
@@ -1,4 +1,5 @@
export { default as BackendCard } from './BackendCard.svelte';
export { default as BackendForm } from './BackendForm.svelte';
export { default as BackendPresetIcon } from './BackendPresetIcon.svelte';
export { default as DialogBackendForm } from './DialogBackendForm.svelte';
export { default as SettingsBackends } from './SettingsBackends.svelte';
@@ -2,6 +2,7 @@
import ModelsSelectorDownloadItem from './ModelsSelectorDownloadItem.svelte';
import { ChevronLeft, CircleAlert, Loader2 } from '@lucide/svelte';
import { ModelsSelectorOption } from '$lib/components/app';
import { BackendPresetIcon } from '$lib/components/app/backends';
import { DialogConfirmDownload } from '$lib/components/app/dialogs';
import type { GroupedModelOptions, ModelItem } from '$lib/components/app/navigation/utils';
import { ModelDownloadConfirmAction } from '$lib/enums';
@@ -117,6 +118,10 @@
</button>
{/if}
{#if provider.preset}
<BackendPresetIcon class="h-3.5 w-3.5" preset={provider.preset} />
{/if}
{provider.name}
{#if provider.loading}
@@ -1,4 +1,5 @@
import { ModelModality } from '$lib/enums';
import type { BackendPreset } from '$lib/types';
import type { ModelOption } from '$lib/types/models';
import { SvelteMap } from 'svelte/reactivity';
@@ -24,6 +25,8 @@ export interface ProviderGroup {
/** Rows left after the search filter, before the cap. */
matched: number;
name: string;
/** Brand mark of the endpoint the backend was created from, when known. */
preset?: BackendPreset;
}
export interface GroupedModelOptions {
@@ -136,6 +139,7 @@ export function groupProviderOptions(
error: string | null;
loading: boolean;
name: string;
preset?: BackendPreset;
}[],
limit = Infinity
): ProviderGroup[] {
+27 -31
View File
@@ -67,65 +67,61 @@ export const BACKEND_COMPAT: Record<BackendProtocol, BackendCompat> = {
* carries no URL so the user starts from an empty form.
*/
export const BACKEND_PRESETS: readonly BackendPreset[] = [
{
baseUrl: 'https://openrouter.ai/api',
id: 'openrouter',
name: 'OpenRouter',
protocol: 'openai'
},
{
baseUrl: 'https://api.together.xyz',
id: 'together',
name: 'Together',
protocol: 'openai'
},
{
baseUrl: 'https://router.huggingface.co',
iconUrl: '/backend-presets/huggingface.svg',
id: 'huggingface',
name: 'Hugging Face',
protocol: 'openai'
},
{
baseUrl: 'https://api.deepseek.com',
id: 'deepseek',
name: 'DeepSeek',
baseUrl: 'https://openrouter.ai/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'
},
{
baseUrl: 'https://api.moonshot.ai',
id: 'kimi',
name: 'Kimi',
baseUrl: 'https://generativelanguage.googleapis.com/v1beta/openai',
chatPath: '/chat/completions',
iconUrl: '/backend-presets/google.svg',
id: 'google',
modelsPath: '/models',
name: 'Google',
protocol: 'openai'
},
{
baseUrl: 'https://api.z.ai/api/paas/v4',
chatPath: '/chat/completions',
iconUrl: '/backend-presets/zai.png',
id: 'zai',
modelsPath: '/models',
name: 'Z.ai',
protocol: 'openai'
},
{
baseUrl: 'https://api.openai.com',
// newer OpenAI models reject max_tokens
compat: { maxTokensField: 'max_completion_tokens' },
id: 'openai',
name: 'OpenAI',
baseUrl: 'https://api.deepseek.com',
iconUrl: '/backend-presets/deepseek.svg',
id: 'deepseek',
name: 'DeepSeek',
protocol: 'openai'
},
{
baseUrl: 'https://api.anthropic.com',
chatPath: '/v1/messages',
id: 'anthropic',
name: 'Anthropic',
protocol: 'anthropic'
baseUrl: 'https://api.moonshot.ai',
iconUrl: '/backend-presets/kimi.png',
id: 'kimi',
name: 'Kimi',
protocol: 'openai'
},
{
baseUrl: 'https://generativelanguage.googleapis.com/v1beta/openai',
baseUrl: 'https://dashscope-intl.aliyuncs.com/compatible-mode/v1',
chatPath: '/chat/completions',
id: 'google',
iconUrl: '/backend-presets/qwen.svg',
id: 'qwen',
modelsPath: '/models',
name: 'Google',
name: 'Qwen',
protocol: 'openai'
},
{
@@ -15,6 +15,7 @@ import {
} from '$lib/constants';
import { backendsModelsStore, backendsStore, modelsStore, serverStore } from '$lib/stores';
import type { ModelOption } from '$lib/types/models';
import { findBackendPreset } from '$lib/utils/backend';
import { rawModelId } from '$lib/utils/model-option-id';
import { onMount } from 'svelte';
@@ -156,7 +157,8 @@ export function useModelsSelector(opts: UseModelsSelectorOptions): UseModelsSele
catalog: state.models.length,
error: state.error,
loading: state.loading,
name: backend.name
name: backend.name,
preset: findBackendPreset(backend.baseUrl)
};
})
);
+6
View File
@@ -76,6 +76,12 @@ export interface BackendPreset {
apiKeyHelp?: string;
baseUrl: string;
chatPath?: string;
/** Brand mark used in both themes, for logos that carry their own background. */
iconUrl?: string;
/** Brand mark for the dark theme. Preferred over `iconUrl` when paired with `iconUrlLight`. */
iconUrlDark?: string;
/** Brand mark for the light theme. Preferred over `iconUrl` when paired with `iconUrlDark`. */
iconUrlLight?: string;
/** Wire quirks this preset needs on top of the protocol defaults. */
compat?: Partial<BackendCompat>;
id: string;
+31 -1
View File
@@ -10,12 +10,19 @@ import {
BACKEND_CAPABILITIES,
BACKEND_COMPAT,
BACKEND_ID_PREFIX,
BACKEND_PRESETS,
BACKEND_PROTOCOLS,
DEFAULT_BACKEND_CHAT_PATH,
DEFAULT_BACKEND_MODELS_PATH,
LOCAL_BACKEND_ID
} 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 {
@@ -27,6 +34,29 @@ export function backendModelsUrl(backend: Backend): string {
return joinBackendUrl(backend.baseUrl, backend.modelsPath ?? DEFAULT_BACKEND_MODELS_PATH);
}
/**
* 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. */
export function getBackendCapabilities(backend: Backend): BackendCapabilities {
return BACKEND_CAPABILITIES[backend.protocol] ?? BACKEND_CAPABILITIES.openai;
+1
View File
@@ -28,6 +28,7 @@ export {
backendChatUrl,
backendModelsUrl,
createLocalBackend,
findBackendPreset,
getBackendCapabilities,
parseBackendsSettings
} from './backend';
@@ -0,0 +1 @@
<svg fill="#5786FE" role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>DeepSeek</title><path d="M23.748 4.651c-.254-.124-.364.113-.512.233-.051.04-.094.09-.137.137-.372.397-.806.657-1.373.626-.829-.046-1.537.214-2.163.848-.133-.782-.575-1.248-1.247-1.548-.352-.155-.708-.311-.955-.65-.172-.24-.219-.509-.305-.774-.055-.16-.11-.323-.293-.35-.2-.031-.278.136-.356.276-.313.572-.434 1.202-.422 1.84.027 1.436.633 2.58 1.838 3.393.137.094.172.187.129.323-.082.28-.18.553-.266.833-.055.179-.137.218-.328.14a5.5 5.5 0 0 1-1.737-1.179c-.857-.828-1.631-1.743-2.597-2.46a12 12 0 0 0-.689-.47c-.985-.957.13-1.743.387-1.836.27-.098.094-.433-.778-.428-.872.003-1.67.295-2.687.685a3 3 0 0 1-.465.136 9.6 9.6 0 0 0-2.883-.101c-1.885.21-3.39 1.1-4.497 2.622C.082 8.776-.231 10.854.152 13.02c.403 2.284 1.568 4.175 3.36 5.653 1.857 1.533 3.997 2.284 6.438 2.14 1.482-.085 3.132-.284 4.994-1.86.47.234.962.328 1.78.398.629.058 1.235-.031 1.705-.129.735-.155.684-.836.418-.961-2.155-1.004-1.682-.595-2.112-.926 1.095-1.295 2.768-3.598 3.284-6.733.05-.346.115-.834.108-1.114-.004-.171.035-.238.23-.257a4.2 4.2 0 0 0 1.545-.475c1.397-.763 1.96-2.016 2.093-3.517.02-.23-.004-.467-.247-.588M11.58 18.168c-2.088-1.642-3.101-2.183-3.52-2.16-.39.024-.32.472-.234.763.09.288.207.487.371.74.114.167.192.416-.113.603-.673.416-1.842-.14-1.897-.168-1.361-.801-2.5-1.86-3.301-3.306-.775-1.393-1.225-2.888-1.299-4.482-.02-.385.094-.522.477-.592a4.7 4.7 0 0 1 1.53-.038c2.131.311 3.946 1.264 5.467 2.774.868.86 1.525 1.887 2.202 2.89.72 1.066 1.494 2.082 2.48 2.915.348.291.626.513.892.677-.802.09-2.14.109-3.055-.615zm1.001-6.44a.306.306 0 0 1 .415-.287.3.3 0 0 1 .113.074.3.3 0 0 1 .086.214c0 .17-.136.307-.308.307a.303.303 0 0 1-.306-.307m3.11 1.596c-.2.081-.4.151-.591.16a1.25 1.25 0 0 1-.798-.254c-.274-.23-.47-.358-.551-.758a1.7 1.7 0 0 1 .015-.588c.07-.327-.007-.537-.238-.727-.188-.156-.426-.199-.689-.199a.6.6 0 0 1-.254-.078.253.253 0 0 1-.114-.358 1 1 0 0 1 .192-.21c.356-.202.767-.136 1.146.016.352.144.618.408 1.001.782.392.451.462.576.685.915.176.264.336.536.446.848.066.194-.02.353-.25.45"/></svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

@@ -0,0 +1 @@
<svg fill="#8E75B2" role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Google Gemini</title><path d="M11.04 19.32Q12 21.51 12 24q0-2.49.93-4.68.96-2.19 2.58-3.81t3.81-2.55Q21.51 12 24 12q-2.49 0-4.68-.93a12.3 12.3 0 0 1-3.81-2.58 12.3 12.3 0 0 1-2.58-3.81Q12 2.49 12 0q0 2.49-.96 4.68-.93 2.19-2.55 3.81a12.3 12.3 0 0 1-3.81 2.58Q2.49 12 0 12q2.49 0 4.68.96 2.19.93 3.81 2.55t2.55 3.81"/></svg>

After

Width:  |  Height:  |  Size: 416 B

@@ -0,0 +1 @@
<svg fill="#FFD21E" role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Hugging Face</title><path d="M12.025 1.13c-5.77 0-10.449 4.647-10.449 10.378 0 1.112.178 2.181.503 3.185.064-.222.203-.444.416-.577a.96.96 0 0 1 .524-.15c.293 0 .584.124.84.284.278.173.48.408.71.694.226.282.458.611.684.951v-.014c.017-.324.106-.622.264-.874s.403-.487.762-.543c.3-.047.596.06.787.203s.31.313.4.467c.15.257.212.468.233.542.01.026.653 1.552 1.657 2.54.616.605 1.01 1.223 1.082 1.912.055.537-.096 1.059-.38 1.572.637.121 1.294.187 1.967.187.657 0 1.298-.063 1.921-.178-.287-.517-.44-1.041-.384-1.581.07-.69.465-1.307 1.081-1.913 1.004-.987 1.647-2.513 1.657-2.539.021-.074.083-.285.233-.542.09-.154.208-.323.4-.467a1.08 1.08 0 0 1 .787-.203c.359.056.604.29.762.543s.247.55.265.874v.015c.225-.34.457-.67.683-.952.23-.286.432-.52.71-.694.257-.16.547-.284.84-.285a.97.97 0 0 1 .524.151c.228.143.373.388.43.625l.006.04a10.3 10.3 0 0 0 .534-3.273c0-5.731-4.678-10.378-10.449-10.378M8.327 6.583a1.5 1.5 0 0 1 .713.174 1.487 1.487 0 0 1 .617 2.013c-.183.343-.762-.214-1.102-.094-.38.134-.532.914-.917.71a1.487 1.487 0 0 1 .69-2.803m7.486 0a1.487 1.487 0 0 1 .689 2.803c-.385.204-.536-.576-.916-.71-.34-.12-.92.437-1.103.094a1.487 1.487 0 0 1 .617-2.013 1.5 1.5 0 0 1 .713-.174m-10.68 1.55a.96.96 0 1 1 0 1.921.96.96 0 0 1 0-1.92m13.838 0a.96.96 0 1 1 0 1.92.96.96 0 0 1 0-1.92M8.489 11.458c.588.01 1.965 1.157 3.572 1.164 1.607-.007 2.984-1.155 3.572-1.164.196-.003.305.12.305.454 0 .886-.424 2.328-1.563 3.202-.22-.756-1.396-1.366-1.63-1.32q-.011.001-.02.006l-.044.026-.01.008-.03.024q-.018.017-.035.036l-.032.04a1 1 0 0 0-.058.09l-.014.025q-.049.088-.11.19a1 1 0 0 1-.083.116 1.2 1.2 0 0 1-.173.18q-.035.029-.075.058a1.3 1.3 0 0 1-.251-.243 1 1 0 0 1-.076-.107c-.124-.193-.177-.363-.337-.444-.034-.016-.104-.008-.2.022q-.094.03-.216.087-.06.028-.125.063l-.13.074q-.067.04-.136.086a3 3 0 0 0-.135.096 3 3 0 0 0-.26.219 2 2 0 0 0-.12.121 2 2 0 0 0-.106.128l-.002.002a2 2 0 0 0-.09.132l-.001.001a1.2 1.2 0 0 0-.105.212q-.013.036-.024.073c-1.139-.875-1.563-2.317-1.563-3.203 0-.334.109-.457.305-.454m.836 10.354c.824-1.19.766-2.082-.365-3.194-1.13-1.112-1.789-2.738-1.789-2.738s-.246-.945-.806-.858-.97 1.499.202 2.362c1.173.864-.233 1.45-.685.64-.45-.812-1.683-2.896-2.322-3.295s-1.089-.175-.938.647 2.822 2.813 2.562 3.244-1.176-.506-1.176-.506-2.866-2.567-3.49-1.898.473 1.23 2.037 2.16c1.564.932 1.686 1.178 1.464 1.53s-3.675-2.511-4-1.297c-.323 1.214 3.524 1.567 3.287 2.405-.238.839-2.71-1.587-3.216-.642-.506.946 3.49 2.056 3.522 2.064 1.29.33 4.568 1.028 5.713-.624m5.349 0c-.824-1.19-.766-2.082.365-3.194 1.13-1.112 1.789-2.738 1.789-2.738s.246-.945.806-.858.97 1.499-.202 2.362c-1.173.864.233 1.45.685.64.451-.812 1.683-2.896 2.322-3.295s1.089-.175.938.647-2.822 2.813-2.562 3.244 1.176-.506 1.176-.506 2.866-2.567 3.49-1.898-.473 1.23-2.037 2.16c-1.564.932-1.686 1.178-1.464 1.53s3.675-2.511 4-1.297c.323 1.214-3.524 1.567-3.287 2.405.238.839 2.71-1.587 3.216-.642.506.946-3.49 2.056-3.522 2.064-1.29.33-4.568 1.028-5.713-.624"/></svg>

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 401.4 293.7">
<defs><style>.st-glyph { fill: #C8FF00; }</style></defs>
<path class="st-glyph" d="M303.9475,17.19926c42.79734,0,77.48933,34.69327,77.48933,77.48933s-34.69199,77.48933-77.48933,77.48933l76.86166,76.86244c9.76367,9.76313,2.84903,26.45667-10.95697,26.45667h-220.88335c-71.32686,0-129.14889-57.82202-129.14889-129.14889S77.64197,17.19926,148.96884,17.19926h154.97866ZM148.96884,68.85881c-42.79607,0-77.48933,34.69327-77.48933,77.48933s34.69327,77.48933,77.48933,77.48933,77.48933-34.69327,77.48933-77.48933-34.69327-77.48933-77.48933-77.48933Z"/>
</svg>

After

Width:  |  Height:  |  Size: 661 B

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 401.4 293.7">
<defs><style>.st-glyph { fill: #7624F4; }</style></defs>
<path class="st-glyph" d="M303.9475,17.19926c42.79734,0,77.48933,34.69327,77.48933,77.48933s-34.69199,77.48933-77.48933,77.48933l76.86166,76.86244c9.76367,9.76313,2.84903,26.45667-10.95697,26.45667h-220.88335c-71.32686,0-129.14889-57.82202-129.14889-129.14889S77.64197,17.19926,148.96884,17.19926h154.97866ZM148.96884,68.85881c-42.79607,0-77.48933,34.69327-77.48933,77.48933s34.69327,77.48933,77.48933,77.48933,77.48933-34.69327,77.48933-77.48933-34.69327-77.48933-77.48933-77.48933Z"/>
</svg>

After

Width:  |  Height:  |  Size: 661 B

+1
View File
@@ -0,0 +1 @@
<svg fill="#6950EF" role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>QWen</title><path d="M23.919 14.545 20.817 9.17l1.47-2.544a.56.56 0 0 0 0-.566l-1.633-2.83a.57.57 0 0 0-.49-.283h-6.207L12.487.402a.57.57 0 0 0-.49-.284H8.732a.56.56 0 0 0-.49.284L5.139 5.775h-2.94a.56.56 0 0 0-.49.284L.077 8.887a.56.56 0 0 0 0 .567L3.18 14.83l-1.47 2.545a.56.56 0 0 0 0 .566l1.634 2.83a.57.57 0 0 0 .49.283h6.205l1.47 2.545a.57.57 0 0 0 .49.284h3.266a.57.57 0 0 0 .49-.284l3.104-5.375h2.94a.57.57 0 0 0 .49-.283l1.634-2.828a.55.55 0 0 0-.004-.568M8.733.686l1.634 2.828-1.634 2.828H21.8L20.164 9.17H7.425L5.63 6.06Zm1.306 19.801-6.205-.002 1.634-2.83h3.265L2.201 6.344h3.267q3.182 5.517 6.367 11.032zm10.124-5.66L18.53 12l-6.532 11.315-1.634-2.83c2.129-3.673 4.25-7.351 6.373-11.028h3.592l3.102 5.374z"/></svg>

After

Width:  |  Height:  |  Size: 820 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB