mirror of
https://github.com/ggml-org/llama.cpp.git
synced 2026-09-17 20:31:47 +02:00
ui : aggregate models across backends
Assisted-by: pi:llama.cpp/DeepSeek-V4.1-Flash
This commit is contained in:
@@ -24,6 +24,7 @@
|
||||
import { useReasoningMenu } from '$lib/hooks/use-reasoning-menu.svelte';
|
||||
import { modelsStore, settingsStore } from '$lib/stores';
|
||||
import { modelLoadFraction } from '$lib/utils';
|
||||
import { rawModelId } from '$lib/utils/model-option-id';
|
||||
|
||||
interface Props {
|
||||
class?: string;
|
||||
@@ -141,12 +142,13 @@
|
||||
return;
|
||||
}
|
||||
|
||||
const model = modelsStore.routerModels.find((m) => m.id === modelId);
|
||||
const rawId = rawModelId(modelId);
|
||||
const model = modelsStore.routerModels.find((m) => m.id === rawId);
|
||||
const status = model?.status?.value as ServerModelStatus | undefined;
|
||||
|
||||
if (status === ServerModelStatus.LOADING) return;
|
||||
|
||||
await modelsStore.status.unload(modelId);
|
||||
await modelsStore.status.unload(rawId);
|
||||
}
|
||||
|
||||
export function open() {
|
||||
|
||||
@@ -3,6 +3,7 @@ import { CHAT_INPUT_FOCUS_SELECTOR } from '$lib/constants';
|
||||
import { backendsModelsStore, backendsStore, modelsStore, serverStore } from '$lib/stores';
|
||||
import type { Backend } from '$lib/types';
|
||||
import type { ModelOption } from '$lib/types/models';
|
||||
import { rawModelId } from '$lib/utils/model-option-id';
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
export interface UseModelsSelectorOptions {
|
||||
@@ -145,7 +146,7 @@ export function useModelsSelector(opts: UseModelsSelectorOptions): UseModelsSele
|
||||
let shouldCloseMenu = true;
|
||||
|
||||
if (onModelChange) {
|
||||
const result = await onModelChange(option.id, option.model);
|
||||
const result = await onModelChange(rawModelId(option.id), option.model);
|
||||
|
||||
if (result === false) {
|
||||
shouldCloseMenu = false;
|
||||
|
||||
@@ -56,7 +56,7 @@ class BackendsModelsStore {
|
||||
return this.states[backendId] ?? EMPTY_STATE;
|
||||
}
|
||||
|
||||
/** Prefetch every enabled external backend, skipping the active one. */
|
||||
/** Prefetch every enabled backend's model list. */
|
||||
async loadAll(): Promise<void> {
|
||||
const enabled = backendsStore.enabled;
|
||||
const ids = new Set(enabled.map((backend) => backend.id));
|
||||
@@ -67,11 +67,7 @@ class BackendsModelsStore {
|
||||
}
|
||||
}
|
||||
|
||||
await Promise.all(
|
||||
enabled
|
||||
.filter((backend) => backend.id !== backendsStore.active.id)
|
||||
.map((backend) => this.ensureLoaded(backend.id))
|
||||
);
|
||||
await Promise.all(enabled.map((backend) => this.ensureLoaded(backend.id)));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -18,14 +18,15 @@ import { type ModelPropsHost, ModelPropsManager } from '$lib/stores/models/props
|
||||
import { type ModelStatusHost, ModelStatusManager } from '$lib/stores/models/status.svelte';
|
||||
import { serverStore } from '$lib/stores/server.svelte';
|
||||
import { getConversationModel } from '$lib/utils/conversation-utils';
|
||||
import { backendIdFromModelId, qualifyModelId, rawModelId } from '$lib/utils/model-option-id';
|
||||
import { SvelteSet } from 'svelte/reactivity';
|
||||
import { toast } from 'svelte-sonner';
|
||||
|
||||
class ModelsStore implements ModelPropsHost, ModelStatusHost {
|
||||
activeModels = $state<ModelOption[]>([]);
|
||||
error = $state<string | null>(null);
|
||||
favoriteModelIds = $state<Set<string>>(this.loadFavoritesFromStorage());
|
||||
loading = $state(false);
|
||||
models = $state<ModelOption[]>([]);
|
||||
routerModels = $state<ApiModelDataEntry[]>([]);
|
||||
selectedModelId = $state<string | null>(null);
|
||||
selectedModelName = $state<string | null>(null);
|
||||
@@ -86,6 +87,39 @@ class ModelsStore implements ModelPropsHost, ModelStatusHost {
|
||||
.map((m) => m.id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Every selectable model across enabled backends. The active backend's
|
||||
* models come from {@link activeModels}; the rest come from the background
|
||||
* prefetch cache. Ids are backend-qualified so the same model name on two
|
||||
* backends stays distinct.
|
||||
*/
|
||||
get models(): ModelOption[] {
|
||||
const activeBackendId = backendsStore.active.id;
|
||||
const merged: ModelOption[] = [];
|
||||
|
||||
for (const option of this.activeModels) {
|
||||
merged.push({
|
||||
...option,
|
||||
backendId: activeBackendId,
|
||||
id: qualifyModelId(activeBackendId, option.id)
|
||||
});
|
||||
}
|
||||
|
||||
for (const backend of backendsStore.enabled) {
|
||||
if (backend.id === activeBackendId) continue;
|
||||
|
||||
for (const option of backendsModelsStore.get(backend.id).models) {
|
||||
merged.push({
|
||||
...option,
|
||||
backendId: backend.id,
|
||||
id: qualifyModelId(backend.id, option.id)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return merged;
|
||||
}
|
||||
|
||||
get props() {
|
||||
return this._props;
|
||||
}
|
||||
@@ -195,7 +229,7 @@ class ModelsStore implements ModelPropsHost, ModelStatusHost {
|
||||
async fetch(force = false): Promise<void> {
|
||||
if (this.inflightFetch) return this.inflightFetch;
|
||||
|
||||
if (this.models.length > 0 && !force) return;
|
||||
if (this.activeModels.length > 0 && !force) return;
|
||||
|
||||
this.inflightFetch = this.runFetch();
|
||||
try {
|
||||
@@ -219,7 +253,7 @@ class ModelsStore implements ModelPropsHost, ModelStatusHost {
|
||||
this.routerModels = response.data;
|
||||
// keep the selector options in sync: a downloaded / deleted model shows
|
||||
// up here too, not only in the router model rows
|
||||
this.models = this.buildModelOptions(response);
|
||||
this.activeModels = this.buildModelOptions(response);
|
||||
await this.props.fetchModalitiesForLoadedModels();
|
||||
|
||||
const visible = this.getVisibleModels();
|
||||
@@ -290,9 +324,19 @@ class ModelsStore implements ModelPropsHost, ModelStatusHost {
|
||||
async selectModelById(modelId: string): Promise<void> {
|
||||
if (!modelId || this.updating) return;
|
||||
|
||||
const backendId = backendIdFromModelId(modelId) ?? backendsStore.active.id;
|
||||
const rawId = rawModelId(modelId);
|
||||
|
||||
// a model from another backend makes that backend active first
|
||||
if (backendId !== backendsStore.active.id) {
|
||||
backendsStore.setActive(backendId);
|
||||
await backendsModelsStore.ensureLoaded(backendId);
|
||||
await this.switchBackend();
|
||||
}
|
||||
|
||||
if (this.selectedModelId === modelId) return;
|
||||
|
||||
const option = this.models.find((model) => model.id === modelId);
|
||||
const option = this.activeModels.find((model) => model.id === rawId);
|
||||
|
||||
if (!option) throw new Error('Selected model is not available');
|
||||
|
||||
@@ -300,7 +344,7 @@ class ModelsStore implements ModelPropsHost, ModelStatusHost {
|
||||
this.error = null;
|
||||
|
||||
try {
|
||||
this.selectedModelId = option.id;
|
||||
this.selectedModelId = modelId;
|
||||
this.selectedModelName = option.model;
|
||||
} finally {
|
||||
this.updating = false;
|
||||
@@ -314,8 +358,7 @@ class ModelsStore implements ModelPropsHost, ModelStatusHost {
|
||||
const option = this.models.find((model) => model.model === modelName);
|
||||
|
||||
if (option) {
|
||||
this.selectedModelId = option.id;
|
||||
this.selectedModelName = option.model;
|
||||
void this.selectModelById(option.id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -361,12 +404,12 @@ class ModelsStore implements ModelPropsHost, ModelStatusHost {
|
||||
const cached = backend.baseUrl.trim() ? backendsModelsStore.get(backend.id) : null;
|
||||
|
||||
if (cached?.loaded) {
|
||||
this.models = cached.models;
|
||||
this.activeModels = cached.models;
|
||||
this.loading = false;
|
||||
|
||||
await serverStore.fetch();
|
||||
|
||||
if (this.models.length > 0) {
|
||||
if (this.activeModels.length > 0) {
|
||||
await this.ensureFirstModelSelected();
|
||||
}
|
||||
|
||||
@@ -457,7 +500,9 @@ class ModelsStore implements ModelPropsHost, ModelStatusHost {
|
||||
* Filter to models visible in the UI (ui !== false).
|
||||
*/
|
||||
private getVisibleModels(): ModelOption[] {
|
||||
return this.models.filter((option) => this.props.getModelProps(option.model)?.ui !== false);
|
||||
return this.activeModels.filter(
|
||||
(option) => this.props.getModelProps(option.model)?.ui !== false
|
||||
);
|
||||
}
|
||||
|
||||
private loadFavoritesFromStorage(): Set<string> {
|
||||
@@ -487,7 +532,7 @@ class ModelsStore implements ModelPropsHost, ModelStatusHost {
|
||||
const response = await ModelsService.list();
|
||||
|
||||
this.routerModels = response.data;
|
||||
this.models = this.buildModelOptions(response);
|
||||
this.activeModels = this.buildModelOptions(response);
|
||||
|
||||
await this.props.fetchModalitiesForLoadedModels();
|
||||
|
||||
@@ -497,7 +542,7 @@ class ModelsStore implements ModelPropsHost, ModelStatusHost {
|
||||
this.selectModelById(visible[0].id);
|
||||
}
|
||||
} else {
|
||||
this.models = await this.fetchModelModeInternal();
|
||||
this.activeModels = await this.fetchModelModeInternal();
|
||||
|
||||
// external backends expose a selectable list; pick a default so the
|
||||
// first send and title generation have a model to target
|
||||
@@ -506,7 +551,7 @@ class ModelsStore implements ModelPropsHost, ModelStatusHost {
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
this.models = [];
|
||||
this.activeModels = [];
|
||||
this.error = error instanceof Error ? error.message : 'Failed to load models';
|
||||
|
||||
throw error;
|
||||
|
||||
@@ -28,7 +28,7 @@ import { SvelteSet } from 'svelte/reactivity';
|
||||
*/
|
||||
export interface ModelPropsHost {
|
||||
/** Model rows the manager mirrors fetched modalities onto. */
|
||||
models: ModelOption[];
|
||||
activeModels: ModelOption[];
|
||||
readonly selectedModelName: string | null;
|
||||
readonly loadedModelIds: string[];
|
||||
isModelLoaded(modelId: string): boolean;
|
||||
@@ -123,7 +123,7 @@ export class ModelPropsManager {
|
||||
try {
|
||||
const results = await Promise.all(propsPromises);
|
||||
|
||||
this.host.models = this.host.models.map((model) => {
|
||||
this.host.activeModels = this.host.activeModels.map((model) => {
|
||||
const modelIndex = loadedModelIds.indexOf(model.model);
|
||||
|
||||
if (modelIndex === -1) return model;
|
||||
@@ -195,7 +195,7 @@ export class ModelPropsManager {
|
||||
return this.buildModalities(serverStore.props.modalities);
|
||||
}
|
||||
|
||||
const model = this.host.models.find((m) => m.model === modelId || m.id === modelId);
|
||||
const model = this.host.activeModels.find((m) => m.model === modelId || m.id === modelId);
|
||||
|
||||
if (model?.modalities) {
|
||||
return model.modalities;
|
||||
@@ -255,7 +255,7 @@ export class ModelPropsManager {
|
||||
|
||||
if (!props?.modalities) return;
|
||||
|
||||
this.host.models = this.host.models.map((model) =>
|
||||
this.host.activeModels = this.host.activeModels.map((model) =>
|
||||
model.model === modelId
|
||||
? { ...model, modalities: this.buildModalities(props.modalities!) }
|
||||
: model
|
||||
|
||||
Vendored
+2
@@ -16,6 +16,8 @@ export interface ModelOption {
|
||||
id: string;
|
||||
name: string;
|
||||
model: string;
|
||||
/** Backend that serves this model; set on the aggregated option list. */
|
||||
backendId?: string;
|
||||
description?: string;
|
||||
capabilities: string[];
|
||||
modalities?: ModelModalities;
|
||||
|
||||
@@ -127,6 +127,9 @@ export {
|
||||
// Model name utilities
|
||||
export { isValidModelName, normalizeModelName, orgOf } from './model-names';
|
||||
|
||||
// Backend-qualified model option ids
|
||||
export { backendIdFromModelId, qualifyModelId, rawModelId } from './model-option-id';
|
||||
|
||||
// Sidecar token utilities
|
||||
export { isAuxSidecar, isDraftSidecar, sidecarFromFileToken } from './sidecars';
|
||||
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
/**
|
||||
* Backend-qualified model option ids.
|
||||
*
|
||||
* The selector lists models from every enabled backend, so a bare model id can
|
||||
* collide. Option ids are qualified as `<backendId>::<rawModelId>`; selection
|
||||
* strips the prefix to find the row in the active backend's list.
|
||||
*/
|
||||
|
||||
const MODEL_OPTION_ID_SEPARATOR = '::';
|
||||
|
||||
/** Prefix a raw model id with the backend that serves it. */
|
||||
export function qualifyModelId(backendId: string, modelId: string): string {
|
||||
return `${backendId}${MODEL_OPTION_ID_SEPARATOR}${modelId}`;
|
||||
}
|
||||
|
||||
/** Backend id of a qualified model option id, or null when unqualified. */
|
||||
export function backendIdFromModelId(qualifiedId: string): string | null {
|
||||
const index = qualifiedId.indexOf(MODEL_OPTION_ID_SEPARATOR);
|
||||
|
||||
return index === -1 ? null : qualifiedId.slice(0, index);
|
||||
}
|
||||
|
||||
/** Strip the backend prefix from a qualified model option id. */
|
||||
export function rawModelId(qualifiedId: string): string {
|
||||
const index = qualifiedId.indexOf(MODEL_OPTION_ID_SEPARATOR);
|
||||
|
||||
return index === -1 ? qualifiedId : qualifiedId.slice(index + MODEL_OPTION_ID_SEPARATOR.length);
|
||||
}
|
||||
Reference in New Issue
Block a user