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. */
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<BackendProtocol, BackendCapabilities> = {
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.
@@ -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<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> {
// 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 });
+20
View File
@@ -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<void> | 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 {
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;
}
+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
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<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;
this._loading = true;
+22
View File
@@ -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. */
+1 -1
View File
@@ -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 {
+7 -1
View File
@@ -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 {
+1
View File
@@ -21,6 +21,7 @@ export {
backendChatUrl,
backendModelsUrl,
createLocalBackend,
getBackendCapabilities,
parseBackendsSettings
} from './backend';