mirror of
https://github.com/ggml-org/llama.cpp.git
synced 2026-09-17 20:31:47 +02:00
ui : add backend store and API base resolution
Assisted-by: pi:llama.cpp/DeepSeek-V4.1-Flash
This commit is contained in:
@@ -11,6 +11,7 @@ export const SETTINGS_KEYS = {
|
||||
API_KEY: 'apiKey',
|
||||
AUTO_MIC_ON_EMPTY: 'autoMicOnEmpty',
|
||||
BACKEND_SAMPLING: 'backend_sampling',
|
||||
BACKENDS: 'backends',
|
||||
CONVERSATION_TABS: 'conversationTabs',
|
||||
COPY_TEXT_ATTACHMENTS_AS_PLAIN_TEXT: 'copyTextAttachmentsAsPlainText',
|
||||
CUSTOM_CSS: 'customCss',
|
||||
|
||||
@@ -188,6 +188,14 @@ export const SETTINGS_REGISTRY: SettingsSectionEntry[] = [
|
||||
key: SETTINGS_KEYS.MAX_IMAGE_RESOLUTION,
|
||||
label: 'Maximum image resolution (megapixels)',
|
||||
type: SettingsFieldType.INPUT
|
||||
},
|
||||
{
|
||||
defaultValue: '[]',
|
||||
help: 'Configure external API backends as a JSON list. The local backend is always available.',
|
||||
key: SETTINGS_KEYS.BACKENDS,
|
||||
label: 'Backends',
|
||||
standaloneField: false,
|
||||
type: SettingsFieldType.INPUT
|
||||
}
|
||||
],
|
||||
slug: SETTINGS_SECTION_SLUGS.GENERAL,
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
/**
|
||||
* backendsStore - API endpoints the UI can talk to.
|
||||
*
|
||||
* The built-in local backend is the llama-server serving this UI. External
|
||||
* backends are user-configured endpoints read from settings. The store
|
||||
* registers the resolved list with the api-base registry, which services use
|
||||
* to build request URLs.
|
||||
*/
|
||||
|
||||
import { browser } from '$app/environment';
|
||||
import { LOCAL_BACKEND_ID, SETTINGS_KEYS } from '$lib/constants';
|
||||
import { settingsStore } from '$lib/stores/settings/index.svelte';
|
||||
import type { Backend } from '$lib/types';
|
||||
import { setBackendsResolver } from '$lib/utils/api-base';
|
||||
import { createLocalBackend, parseBackendsSettings } from '$lib/utils/backend';
|
||||
|
||||
class BackendsStore {
|
||||
activeId = $state<string>(LOCAL_BACKEND_ID);
|
||||
|
||||
get active(): Backend {
|
||||
return this.list.find((backend) => backend.id === this.activeId) ?? this.local;
|
||||
}
|
||||
|
||||
get enabled(): Backend[] {
|
||||
return this.list.filter((backend) => backend.enabled);
|
||||
}
|
||||
|
||||
get external(): Backend[] {
|
||||
return parseBackendsSettings(settingsStore.config[SETTINGS_KEYS.BACKENDS]);
|
||||
}
|
||||
|
||||
get list(): Backend[] {
|
||||
return [this.local, ...this.external];
|
||||
}
|
||||
|
||||
get local(): Backend {
|
||||
return createLocalBackend();
|
||||
}
|
||||
|
||||
initialize(): void {
|
||||
if (!browser) return;
|
||||
|
||||
setBackendsResolver(() => ({ activeId: this.activeId, backends: this.list }));
|
||||
}
|
||||
|
||||
setActive(backendId: string): void {
|
||||
this.activeId = this.list.some((backend) => backend.id === backendId)
|
||||
? backendId
|
||||
: LOCAL_BACKEND_ID;
|
||||
}
|
||||
}
|
||||
|
||||
export const backendsStore = new BackendsStore();
|
||||
@@ -37,6 +37,9 @@ export { conversationsStore } from './conversations/index.svelte';
|
||||
// MCP
|
||||
export { mcpStore } from './mcp/index.svelte';
|
||||
|
||||
// BACKENDS
|
||||
export { backendsStore } from './backends.svelte';
|
||||
|
||||
// MODELS
|
||||
export { modelsStore } from './models/index.svelte';
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
// direct imports, not via the barrel, to avoid circular deps
|
||||
import { backendsStore } from './backends.svelte';
|
||||
import { conversationsStore } from './conversations/index.svelte';
|
||||
import { permissionsStore } from './permissions.svelte';
|
||||
import { settingsStore } from './settings/index.svelte';
|
||||
@@ -17,6 +18,7 @@ export function initStores(): Promise<void> {
|
||||
await MigrationService.runAllMigrations();
|
||||
|
||||
settingsStore.initialize();
|
||||
backendsStore.initialize();
|
||||
permissionsStore.initialize();
|
||||
toolsStore.initialize();
|
||||
void versionStore.initialize();
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
/**
|
||||
* API base resolution for backends.
|
||||
*
|
||||
* The UI can talk to more than one backend endpoint. Services build request
|
||||
* URLs through {@link apiUrl} so a request always targets the right backend.
|
||||
* The backends store registers a resolver here; this module never imports the
|
||||
* store, which keeps URL resolution free of store dependencies.
|
||||
*/
|
||||
|
||||
import { base } from '$app/paths';
|
||||
import { API_ABSOLUTE_URL_PROTOCOLS } from '$lib/constants';
|
||||
import type { Backend } from '$lib/types';
|
||||
|
||||
/** Backend list and active selection as exposed to URL resolution. */
|
||||
export interface BackendsSnapshot {
|
||||
activeId: string;
|
||||
backends: Backend[];
|
||||
}
|
||||
|
||||
type BackendsResolver = () => BackendsSnapshot;
|
||||
|
||||
let resolveBackends: BackendsResolver | null = null;
|
||||
|
||||
/** Registered once by the backends store. */
|
||||
export function setBackendsResolver(resolver: BackendsResolver | null): void {
|
||||
resolveBackends = resolver;
|
||||
}
|
||||
|
||||
/**
|
||||
* Look up a backend by id, defaulting to the active one.
|
||||
*/
|
||||
export function getBackend(backendId?: string): Backend | undefined {
|
||||
const snapshot = resolveBackends?.();
|
||||
|
||||
if (!snapshot) return undefined;
|
||||
|
||||
const id = backendId ?? snapshot.activeId;
|
||||
|
||||
return snapshot.backends.find((backend) => backend.id === id);
|
||||
}
|
||||
|
||||
/** API root for a backend, or an empty string for the local backend. */
|
||||
export function getBackendBaseUrl(backendId?: string): string {
|
||||
return getBackend(backendId)?.baseUrl.trim() ?? '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Absolute URL for an API path on a backend.
|
||||
*
|
||||
* Absolute URLs pass through untouched. Paths on the local backend keep the
|
||||
* existing base-path-relative form, so serving under a subpath still works.
|
||||
* Paths on external backends resolve against the backend's API root.
|
||||
*/
|
||||
export function apiUrl(path: string, backendId?: string): string {
|
||||
if (API_ABSOLUTE_URL_PROTOCOLS.some((protocol) => path.startsWith(protocol))) {
|
||||
return path;
|
||||
}
|
||||
|
||||
const baseUrl = getBackendBaseUrl(backendId);
|
||||
|
||||
if (!baseUrl) {
|
||||
return `${base}${path}`;
|
||||
}
|
||||
|
||||
const root = baseUrl.endsWith('/') ? baseUrl : `${baseUrl}/`;
|
||||
|
||||
return new URL(path.replace(/^\.?\//, ''), root).toString();
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
/**
|
||||
* Backend list parsing and defaults.
|
||||
*
|
||||
* External backends are persisted in settings as a JSON list. Malformed
|
||||
* entries are dropped instead of throwing so a corrupted settings value can
|
||||
* never break URL resolution.
|
||||
*/
|
||||
|
||||
import { BACKEND_ID_PREFIX, BACKEND_PROTOCOLS, LOCAL_BACKEND_ID } from '$lib/constants';
|
||||
import type { Backend, BackendProtocol } from '$lib/types';
|
||||
|
||||
/** The built-in backend pointing at the server that serves this UI. */
|
||||
export function createLocalBackend(): Backend {
|
||||
return {
|
||||
baseUrl: '',
|
||||
enabled: true,
|
||||
id: LOCAL_BACKEND_ID,
|
||||
name: 'Local',
|
||||
protocol: 'llama.cpp'
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the persisted backends JSON into backend entries.
|
||||
*/
|
||||
export function parseBackendsSettings(rawBackends: unknown): Backend[] {
|
||||
if (!rawBackends) return [];
|
||||
|
||||
let parsed: unknown;
|
||||
|
||||
if (typeof rawBackends === 'string') {
|
||||
const trimmed = rawBackends.trim();
|
||||
|
||||
if (!trimmed) return [];
|
||||
|
||||
try {
|
||||
parsed = JSON.parse(trimmed);
|
||||
} catch (error) {
|
||||
console.warn('[backends] Failed to parse backends JSON, ignoring value:', error);
|
||||
|
||||
return [];
|
||||
}
|
||||
} else {
|
||||
parsed = rawBackends;
|
||||
}
|
||||
|
||||
if (!Array.isArray(parsed)) return [];
|
||||
|
||||
return parsed.flatMap((entry, index) => {
|
||||
const backend = parseBackendEntry(entry, index);
|
||||
|
||||
return backend ? [backend] : [];
|
||||
});
|
||||
}
|
||||
|
||||
function parseBackendEntry(entry: unknown, index: number): Backend | null {
|
||||
if (!entry || typeof entry !== 'object') return null;
|
||||
|
||||
const raw = entry as Record<string, unknown>;
|
||||
const baseUrl = typeof raw.baseUrl === 'string' ? raw.baseUrl.trim() : '';
|
||||
|
||||
// the local backend is built in and never persisted
|
||||
if (!baseUrl || raw.id === LOCAL_BACKEND_ID) return null;
|
||||
|
||||
const protocol = BACKEND_PROTOCOLS.includes(raw.protocol as BackendProtocol)
|
||||
? (raw.protocol as BackendProtocol)
|
||||
: 'openai';
|
||||
const id =
|
||||
typeof raw.id === 'string' && raw.id.trim()
|
||||
? raw.id.trim()
|
||||
: `${BACKEND_ID_PREFIX}-${index + 1}`;
|
||||
const name = typeof raw.name === 'string' && raw.name.trim() ? raw.name.trim() : baseUrl;
|
||||
const apiKey =
|
||||
typeof raw.apiKey === 'string' && raw.apiKey.trim() ? raw.apiKey.trim() : undefined;
|
||||
|
||||
return {
|
||||
apiKey,
|
||||
baseUrl,
|
||||
enabled: raw.enabled !== false,
|
||||
headers: parseBackendHeaders(raw.headers),
|
||||
id,
|
||||
name,
|
||||
protocol
|
||||
};
|
||||
}
|
||||
|
||||
function parseBackendHeaders(raw: unknown): Record<string, string> | undefined {
|
||||
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return undefined;
|
||||
|
||||
const entries = Object.entries(raw as Record<string, unknown>)
|
||||
.filter(([, value]) => typeof value === 'string' && value.trim() !== '')
|
||||
.map(([key, value]) => [key.trim(), (value as string).trim()] as const);
|
||||
|
||||
return entries.length > 0 ? Object.fromEntries(entries) : undefined;
|
||||
}
|
||||
@@ -8,9 +8,11 @@
|
||||
*/
|
||||
|
||||
// API utilities
|
||||
export { apiUrl, getBackend, getBackendBaseUrl, type BackendsSnapshot } from './api-base';
|
||||
export { getAuthHeaders, getJsonHeaders, sanitizeHeaders } from './api-headers';
|
||||
export { ApiError, apiDelete, apiFetch, apiFetchWithParams, apiPost } from './api-fetch';
|
||||
export { validateApiKey } from './api-key-validation';
|
||||
export { createLocalBackend, parseBackendsSettings } from './backend';
|
||||
|
||||
// Attachment utilities
|
||||
export { getAttachmentDisplayItems, isMcpPrompt, isMcpResource } from './attachment-display';
|
||||
|
||||
Reference in New Issue
Block a user