ui : gate llama.cpp-only features by backend capabilities

Assisted-by: pi:llama.cpp/DeepSeek-V4.1-Flash
This commit is contained in:
Aleksander Grygier
2026-09-16 19:22:47 +02:00
parent 5d08b6d65a
commit a68b59e853
8 changed files with 93 additions and 3 deletions
@@ -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. */ /** Version sent with the Anthropic Messages API. */
export const ANTHROPIC_API_VERSION = '2023-06-01'; 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. */ /** Id of the built-in backend that points at the server serving this UI. */
export const LOCAL_BACKEND_ID = 'local'; 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<BackendProtocol, BackendCapabilities> = {
anthropic: COMPATIBLE_CAPABILITIES,
'llama.cpp': LLAMA_CPP_CAPABILITIES,
openai: COMPATIBLE_CAPABILITIES
};
/** /**
* Ready-made endpoints offered when adding a backend. `custom` intentionally * Ready-made endpoints offered when adding a backend. `custom` intentionally
* carries no URL so the user starts from an empty form. * carries no URL so the user starts from an empty form.
@@ -33,6 +33,7 @@ import {
StreamConnectionState StreamConnectionState
} from '$lib/enums'; } from '$lib/enums';
import { modelsStore } from '$lib/stores/models/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 { settingsStore } from '$lib/stores/settings/index.svelte';
import type { DatabaseMessageExtraMcpPrompt, DatabaseMessageExtraMcpResource } from '$lib/types'; import type { DatabaseMessageExtraMcpPrompt, DatabaseMessageExtraMcpResource } from '$lib/types';
import type { import type {
@@ -87,6 +88,9 @@ export class ChatService {
* @returns {Promise<boolean>} Promise that resolves to true if all slots are idle, false if any is processing * @returns {Promise<boolean>} Promise that resolves to true if all slots are idle, false if any is processing
*/ */
static async areAllSlotsIdle(model?: string | null, signal?: AbortSignal): Promise<boolean> { static async areAllSlotsIdle(model?: string | null, signal?: AbortSignal): Promise<boolean> {
// the /slots endpoint only exists on llama.cpp servers
if (!serverStore.capabilities.slots) return true;
try { try {
const url = model ? `${API_SLOTS.LIST}?model=${encodeURIComponent(model)}` : API_SLOTS.LIST; const url = model ? `${API_SLOTS.LIST}?model=${encodeURIComponent(model)}` : API_SLOTS.LIST;
const res = await fetch(apiUrl(url), { signal }); const res = await fetch(apiUrl(url), { signal });
+20
View File
@@ -6,9 +6,13 @@
* PropsService for the /props fetch. * PropsService for the /props fetch.
*/ */
import { BACKEND_CAPABILITIES } from '$lib/constants';
import { ServerRole } from '$lib/enums'; import { ServerRole } from '$lib/enums';
import { PropsService } from '$lib/services/props.service'; import { PropsService } from '$lib/services/props.service';
import type { BackendCapabilities } from '$lib/types';
import { ApiError } from '$lib/utils'; import { ApiError } from '$lib/utils';
import { getBackend } from '$lib/utils/api-base';
import { getBackendCapabilities } from '$lib/utils/backend';
const LOADING_RETRY_INTERVAL_MS = 1000; const LOADING_RETRY_INTERVAL_MS = 1000;
@@ -21,6 +25,13 @@ class ServerStore {
private fetchPromise: Promise<void> | null = null; private fetchPromise: Promise<void> | null = null;
private retryTimer: ReturnType<typeof setTimeout> | null = null; private retryTimer: ReturnType<typeof setTimeout> | 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 { get contextSize(): number | null {
const nCtx = this.props?.default_generation_settings?.n_ctx; const nCtx = this.props?.default_generation_settings?.n_ctx;
@@ -63,6 +74,15 @@ class ServerStore {
this.clearRetryTimer(); 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) { if (!background) {
this.loading = true; this.loading = true;
} }
+9
View File
@@ -30,6 +30,7 @@ import { ToolsService } from '$lib/services/tools.service';
// direct imports between stores, not via the barrel, to avoid circular deps // direct imports between stores, not via the barrel, to avoid circular deps
import { mcpStore } from '$lib/stores/mcp/index.svelte'; import { mcpStore } from '$lib/stores/mcp/index.svelte';
import { modelsStore } from '$lib/stores/models/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 { settingsStore } from '$lib/stores/settings/index.svelte';
import type { OpenAIToolDefinition, ToolEntry, ToolGroup } from '$lib/types'; import type { OpenAIToolDefinition, ToolEntry, ToolGroup } from '$lib/types';
import { ApiError, buildSandboxToolDefinition } from '$lib/utils'; import { ApiError, buildSandboxToolDefinition } from '$lib/utils';
@@ -232,6 +233,14 @@ class ToolsStore {
} }
async fetchServerTools(): Promise<void> { async fetchServerTools(): Promise<void> {
// the /tools endpoint only exists on llama.cpp servers
if (!serverStore.capabilities.tools) {
this._serverTools = [];
this.cwdAwareTools = new SvelteSet();
return;
}
if (this._loading) return; if (this._loading) return;
this._loading = true; this._loading = true;
+22
View File
@@ -10,6 +10,28 @@
/** Request/response shape a backend speaks. */ /** Request/response shape a backend speaks. */
export type BackendProtocol = 'llama.cpp' | 'openai' | 'anthropic'; 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. */ /** One configured API endpoint. */
export interface Backend { export interface Backend {
/** Bearer token / API key used for this backend. */ /** Bearer token / API key used for this backend. */
+1 -1
View File
@@ -36,7 +36,7 @@ export type {
} from './api'; } from './api';
// Backend types // Backend types
export type { Backend, BackendPreset, BackendProtocol } from './backend'; export type { Backend, BackendCapabilities, BackendPreset, BackendProtocol } from './backend';
// HuggingFace types // HuggingFace types
export type { export type {
+7 -1
View File
@@ -7,13 +7,14 @@
*/ */
import { import {
BACKEND_CAPABILITIES,
BACKEND_ID_PREFIX, BACKEND_ID_PREFIX,
BACKEND_PROTOCOLS, BACKEND_PROTOCOLS,
DEFAULT_BACKEND_CHAT_PATH, DEFAULT_BACKEND_CHAT_PATH,
DEFAULT_BACKEND_MODELS_PATH, DEFAULT_BACKEND_MODELS_PATH,
LOCAL_BACKEND_ID LOCAL_BACKEND_ID
} from '$lib/constants'; } 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. */ /** Absolute chat completions URL for a backend. */
export function backendChatUrl(backend: Backend): string { 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); 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. */ /** The built-in backend pointing at the server that serves this UI. */
export function createLocalBackend(apiKey?: string, enabled = true): Backend { export function createLocalBackend(apiKey?: string, enabled = true): Backend {
return { return {
+1
View File
@@ -21,6 +21,7 @@ export {
backendChatUrl, backendChatUrl,
backendModelsUrl, backendModelsUrl,
createLocalBackend, createLocalBackend,
getBackendCapabilities,
parseBackendsSettings parseBackendsSettings
} from './backend'; } from './backend';