From a68b59e8531adf67e6725509af8511d8f55a0341 Mon Sep 17 00:00:00 2001 From: Aleksander Grygier Date: Tue, 15 Sep 2026 00:55:34 +0200 Subject: [PATCH] ui : gate llama.cpp-only features by backend capabilities Assisted-by: pi:llama.cpp/DeepSeek-V4.1-Flash --- .../ui/src/lib/constants/backend.constants.ts | 30 ++++++++++++++++++- tools/ui/src/lib/services/chat.service.ts | 4 +++ tools/ui/src/lib/stores/server.svelte.ts | 20 +++++++++++++ tools/ui/src/lib/stores/tools.svelte.ts | 9 ++++++ tools/ui/src/lib/types/backend.d.ts | 22 ++++++++++++++ tools/ui/src/lib/types/index.ts | 2 +- tools/ui/src/lib/utils/backend.ts | 8 ++++- tools/ui/src/lib/utils/index.ts | 1 + 8 files changed, 93 insertions(+), 3 deletions(-) diff --git a/tools/ui/src/lib/constants/backend.constants.ts b/tools/ui/src/lib/constants/backend.constants.ts index bdf79a5939..7008333c79 100644 --- a/tools/ui/src/lib/constants/backend.constants.ts +++ b/tools/ui/src/lib/constants/backend.constants.ts @@ -1,4 +1,4 @@ -import type { BackendPreset, BackendProtocol } from '$lib/types'; +import type { BackendCapabilities, BackendPreset, BackendProtocol } from '$lib/types'; /** Version sent with the Anthropic Messages API. */ export const ANTHROPIC_API_VERSION = '2023-06-01'; @@ -18,6 +18,34 @@ export const DEFAULT_BACKEND_MODELS_PATH = '/v1/models'; /** Id of the built-in backend that points at the server serving this UI. */ export const LOCAL_BACKEND_ID = 'local'; +/** Capabilities of a full llama.cpp server. */ +const LLAMA_CPP_CAPABILITIES: BackendCapabilities = { + corsProxy: true, + loadUnload: true, + props: true, + router: true, + slots: true, + statusFeed: true, + tools: true +}; +/** Capabilities of a plain OpenAI- or Anthropic-compatible endpoint. */ +const COMPATIBLE_CAPABILITIES: BackendCapabilities = { + corsProxy: false, + loadUnload: false, + props: false, + router: false, + slots: false, + statusFeed: false, + tools: false +}; + +/** Capabilities per backend protocol. */ +export const BACKEND_CAPABILITIES: Record = { + anthropic: COMPATIBLE_CAPABILITIES, + 'llama.cpp': LLAMA_CPP_CAPABILITIES, + openai: COMPATIBLE_CAPABILITIES +}; + /** * Ready-made endpoints offered when adding a backend. `custom` intentionally * carries no URL so the user starts from an empty form. diff --git a/tools/ui/src/lib/services/chat.service.ts b/tools/ui/src/lib/services/chat.service.ts index 75b781bcd2..7e652f99c5 100644 --- a/tools/ui/src/lib/services/chat.service.ts +++ b/tools/ui/src/lib/services/chat.service.ts @@ -33,6 +33,7 @@ import { StreamConnectionState } from '$lib/enums'; import { modelsStore } from '$lib/stores/models/index.svelte'; +import { serverStore } from '$lib/stores/server.svelte'; import { settingsStore } from '$lib/stores/settings/index.svelte'; import type { DatabaseMessageExtraMcpPrompt, DatabaseMessageExtraMcpResource } from '$lib/types'; import type { @@ -87,6 +88,9 @@ export class ChatService { * @returns {Promise} Promise that resolves to true if all slots are idle, false if any is processing */ static async areAllSlotsIdle(model?: string | null, signal?: AbortSignal): Promise { + // the /slots endpoint only exists on llama.cpp servers + if (!serverStore.capabilities.slots) return true; + try { const url = model ? `${API_SLOTS.LIST}?model=${encodeURIComponent(model)}` : API_SLOTS.LIST; const res = await fetch(apiUrl(url), { signal }); diff --git a/tools/ui/src/lib/stores/server.svelte.ts b/tools/ui/src/lib/stores/server.svelte.ts index e145e2891d..0fe4499781 100644 --- a/tools/ui/src/lib/stores/server.svelte.ts +++ b/tools/ui/src/lib/stores/server.svelte.ts @@ -6,9 +6,13 @@ * PropsService for the /props fetch. */ +import { BACKEND_CAPABILITIES } from '$lib/constants'; import { ServerRole } from '$lib/enums'; import { PropsService } from '$lib/services/props.service'; +import type { BackendCapabilities } from '$lib/types'; import { ApiError } from '$lib/utils'; +import { getBackend } from '$lib/utils/api-base'; +import { getBackendCapabilities } from '$lib/utils/backend'; const LOADING_RETRY_INTERVAL_MS = 1000; @@ -21,6 +25,13 @@ class ServerStore { private fetchPromise: Promise | null = null; private retryTimer: ReturnType | null = null; + /** Features of the active backend. Defaults to full llama.cpp support. */ + get capabilities(): BackendCapabilities { + const backend = getBackend(); + + return backend ? getBackendCapabilities(backend) : BACKEND_CAPABILITIES['llama.cpp']; + } + get contextSize(): number | null { const nCtx = this.props?.default_generation_settings?.n_ctx; @@ -63,6 +74,15 @@ class ServerStore { this.clearRetryTimer(); + // External backends expose no /props endpoint. Keep MODEL-mode defaults so + // role detection and generation defaults degrade instead of failing. + if (!this.capabilities.props) { + this.clear(); + this.role = ServerRole.MODEL; + + return; + } + if (!background) { this.loading = true; } diff --git a/tools/ui/src/lib/stores/tools.svelte.ts b/tools/ui/src/lib/stores/tools.svelte.ts index 1d4133408b..a7057914c6 100644 --- a/tools/ui/src/lib/stores/tools.svelte.ts +++ b/tools/ui/src/lib/stores/tools.svelte.ts @@ -30,6 +30,7 @@ import { ToolsService } from '$lib/services/tools.service'; // direct imports between stores, not via the barrel, to avoid circular deps import { mcpStore } from '$lib/stores/mcp/index.svelte'; import { modelsStore } from '$lib/stores/models/index.svelte'; +import { serverStore } from '$lib/stores/server.svelte'; import { settingsStore } from '$lib/stores/settings/index.svelte'; import type { OpenAIToolDefinition, ToolEntry, ToolGroup } from '$lib/types'; import { ApiError, buildSandboxToolDefinition } from '$lib/utils'; @@ -232,6 +233,14 @@ class ToolsStore { } async fetchServerTools(): Promise { + // the /tools endpoint only exists on llama.cpp servers + if (!serverStore.capabilities.tools) { + this._serverTools = []; + this.cwdAwareTools = new SvelteSet(); + + return; + } + if (this._loading) return; this._loading = true; diff --git a/tools/ui/src/lib/types/backend.d.ts b/tools/ui/src/lib/types/backend.d.ts index 3ecf0d2c69..0b7d709c54 100644 --- a/tools/ui/src/lib/types/backend.d.ts +++ b/tools/ui/src/lib/types/backend.d.ts @@ -10,6 +10,28 @@ /** Request/response shape a backend speaks. */ export type BackendProtocol = 'llama.cpp' | 'openai' | 'anthropic'; +/** + * Features a backend supports. A llama.cpp server exposes extra endpoints on + * top of the OpenAI-compatible API; plain OpenAI- and Anthropic-compatible + * endpoints only provide chat and model listing. + */ +export interface BackendCapabilities { + /** llama-server's /cors-proxy endpoint for cross-origin MCP requests. */ + corsProxy: boolean; + /** Router-mode model load/unload. */ + loadUnload: boolean; + /** The /props endpoint with server role and generation defaults. */ + props: boolean; + /** Multi-model router mode. */ + router: boolean; + /** The /slots introspection endpoint. */ + slots: boolean; + /** The /models/sse load and download progress feed. */ + statusFeed: boolean; + /** The /tools listing and execution endpoint. */ + tools: boolean; +} + /** One configured API endpoint. */ export interface Backend { /** Bearer token / API key used for this backend. */ diff --git a/tools/ui/src/lib/types/index.ts b/tools/ui/src/lib/types/index.ts index be5ac2bdfa..22ac655299 100644 --- a/tools/ui/src/lib/types/index.ts +++ b/tools/ui/src/lib/types/index.ts @@ -36,7 +36,7 @@ export type { } from './api'; // Backend types -export type { Backend, BackendPreset, BackendProtocol } from './backend'; +export type { Backend, BackendCapabilities, BackendPreset, BackendProtocol } from './backend'; // HuggingFace types export type { diff --git a/tools/ui/src/lib/utils/backend.ts b/tools/ui/src/lib/utils/backend.ts index f338f5136c..d00a37b86d 100644 --- a/tools/ui/src/lib/utils/backend.ts +++ b/tools/ui/src/lib/utils/backend.ts @@ -7,13 +7,14 @@ */ import { + BACKEND_CAPABILITIES, BACKEND_ID_PREFIX, BACKEND_PROTOCOLS, DEFAULT_BACKEND_CHAT_PATH, DEFAULT_BACKEND_MODELS_PATH, LOCAL_BACKEND_ID } from '$lib/constants'; -import type { Backend, BackendProtocol } from '$lib/types'; +import type { Backend, BackendCapabilities, BackendProtocol } from '$lib/types'; /** Absolute chat completions URL for a backend. */ export function backendChatUrl(backend: Backend): string { @@ -25,6 +26,11 @@ export function backendModelsUrl(backend: Backend): string { return joinBackendUrl(backend.baseUrl, backend.modelsPath ?? DEFAULT_BACKEND_MODELS_PATH); } +/** Features a backend supports, derived from its protocol. */ +export function getBackendCapabilities(backend: Backend): BackendCapabilities { + return BACKEND_CAPABILITIES[backend.protocol] ?? BACKEND_CAPABILITIES.openai; +} + /** The built-in backend pointing at the server that serves this UI. */ export function createLocalBackend(apiKey?: string, enabled = true): Backend { return { diff --git a/tools/ui/src/lib/utils/index.ts b/tools/ui/src/lib/utils/index.ts index a9b67f0100..2278531bb3 100644 --- a/tools/ui/src/lib/utils/index.ts +++ b/tools/ui/src/lib/utils/index.ts @@ -21,6 +21,7 @@ export { backendChatUrl, backendModelsUrl, createLocalBackend, + getBackendCapabilities, parseBackendsSettings } from './backend';