ui : list models from a backend

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 a0b5102d91
commit 12ca72a4d9
+61 -13
View File
@@ -1,15 +1,24 @@
/**
* BackendsService - Stateless backend connectivity checks
* BackendsService - Stateless backend connectivity checks and model listing
*
* Probes a backend's models endpoint to validate its URL and credentials.
* No reactive state; consumed by the backends settings UI.
* Probes a backend's models endpoint to validate its URL and credentials, and
* normalizes the response into the UI model shape. No reactive state;
* consumed by the backends settings UI and the per-backend model cache.
*/
import type { Backend } from '$lib/types';
import type { Backend, ModelOption } from '$lib/types';
import { isAbortError } from '$lib/utils/abort';
import { getAuthHeadersForBackend } from '$lib/utils/api-headers';
import { backendModelsUrl } from '$lib/utils/backend';
/** Models returned by a backend, plus the failure detail when the call fails. */
export interface BackendModelsResult {
error?: string;
models: ModelOption[];
ok: boolean;
status: number | null;
}
/** Outcome of a backend connectivity check. */
export interface BackendTestResult {
error?: string;
@@ -20,14 +29,14 @@ export interface BackendTestResult {
export class BackendsService {
/**
* Check that a backend answers on its models endpoint.
* List the models a backend exposes on its models endpoint.
*
* @param backend - Backend to probe. Does not need to be registered yet.
* @param signal - Optional abort signal for a cancelled test.
* @param backend - Backend to query. Does not need to be registered yet.
* @param signal - Optional abort signal for a cancelled request.
*/
static async test(backend: Backend, signal?: AbortSignal): Promise<BackendTestResult> {
static async listModels(backend: Backend, signal?: AbortSignal): Promise<BackendModelsResult> {
if (!backend.baseUrl.trim()) {
return { error: 'Backend URL is required', ok: false, status: null };
return { error: 'Backend URL is required', models: [], ok: false, status: null };
}
try {
@@ -37,25 +46,49 @@ export class BackendsService {
});
if (!response.ok) {
return { error: await describeFailure(response), ok: false, status: response.status };
return {
error: await describeFailure(response),
models: [],
ok: false,
status: response.status
};
}
const body = (await response.json()) as { data?: unknown };
const modelCount = Array.isArray(body?.data) ? body.data.length : 0;
const entries = Array.isArray(body?.data) ? body.data : [];
const models = entries.flatMap((entry) => normalizeBackendModel(entry));
return { modelCount, ok: true, status: response.status };
return { models, ok: true, status: response.status };
} catch (error) {
if (isAbortError(error)) {
return { ok: false, status: null };
return { models: [], ok: false, status: null };
}
return {
error: error instanceof Error ? error.message : String(error),
models: [],
ok: false,
status: null
};
}
}
/**
* Check that a backend answers on its models endpoint.
*
* @param backend - Backend to probe. Does not need to be registered yet.
* @param signal - Optional abort signal for a cancelled test.
*/
static async test(backend: Backend, signal?: AbortSignal): Promise<BackendTestResult> {
const result = await BackendsService.listModels(backend, signal);
return {
error: result.error,
modelCount: result.models.length,
ok: result.ok,
status: result.status
};
}
}
/** Build a human-readable message from a non-OK response. */
@@ -73,3 +106,18 @@ async function describeFailure(response: Response): Promise<string> {
return status;
}
/**
* Normalize one entry of an OpenAI-compatible `/v1/models` response. External
* backends only guarantee an id, so that doubles as the display name.
*/
function normalizeBackendModel(entry: unknown): ModelOption[] {
if (!entry || typeof entry !== 'object') return [];
const raw = entry as Record<string, unknown>;
const id = typeof raw.id === 'string' ? raw.id.trim() : '';
if (!id) return [];
return [{ capabilities: [], id, model: id, name: id }];
}