// Only migrate if new key doesn't already exist
const newValue = localStorage.getItem(newKey);
if (newValue !== null) {
- console.log(`[Migration] localStorage: ${newKey} already exists, skipping`);
+ if (import.meta.env.DEV && import.meta.env.VITE_DEBUG)
+ console.log(`[Migration] localStorage: ${newKey} already exists, skipping`);
continue;
}
if (oldValue !== null) {
localStorage.setItem(newKey, oldValue);
// Keep old key for downgrade compatibility - DO NOT DELETE
- console.log(
- `[Migration] localStorage: copied ${deprecatedKey} → ${newKey} (preserved old)`
- );
+ if (import.meta.env.DEV && import.meta.env.VITE_DEBUG) {
+ console.log(
+ `[Migration] localStorage: copied ${deprecatedKey} → ${newKey} (preserved old)`
+ );
+ }
}
}
}
async run(): Promise<void> {
const oldDbNames = await Dexie.getDatabaseNames();
if (!oldDbNames.includes(DB_APP_NAME_DEPRECATED)) {
- console.log('[Migration] IndexedDB: no old database found, skipping');
+ if (import.meta.env.DEV && import.meta.env.VITE_DEBUG)
+ console.log('[Migration] IndexedDB: no old database found, skipping');
return;
}
newDb.version(1).stores(IDXDB_STORES);
const existingConvs = await newDb.table(IDXDB_TABLES.conversations).count();
if (existingConvs > 0) {
- console.log('[Migration] IndexedDB: new database already has data, skipping');
+ if (import.meta.env.DEV && import.meta.env.VITE_DEBUG)
+ console.log('[Migration] IndexedDB: new database already has data, skipping');
return;
}
- console.log('[Migration] IndexedDB: copying from', DB_APP_NAME_DEPRECATED);
+ if (import.meta.env.DEV && import.meta.env.VITE_DEBUG)
+ console.log('[Migration] IndexedDB: copying from', DB_APP_NAME_DEPRECATED);
const oldDb = new Dexie(DB_APP_NAME_DEPRECATED);
oldDb.version(1).stores(IDXDB_STORES);
if (conversations.length > 0) {
await newDb.table(IDXDB_TABLES.conversations).bulkAdd(conversations);
- console.log(`[Migration] IndexedDB: copied ${conversations.length} conversations`);
+ if (import.meta.env.DEV && import.meta.env.VITE_DEBUG)
+ console.log(`[Migration] IndexedDB: copied ${conversations.length} conversations`);
}
if (messages.length > 0) {
await newDb.table(IDXDB_TABLES.messages).bulkAdd(messages);
- console.log(`[Migration] IndexedDB: copied ${messages.length} messages`);
+ if (import.meta.env.DEV && import.meta.env.VITE_DEBUG)
+ console.log(`[Migration] IndexedDB: copied ${messages.length} messages`);
}
// Non-destructive: DO NOT delete old database - keep for downgrade compatibility
- console.log('[Migration] IndexedDB: preserved old database for downgrade compatibility');
+ if (import.meta.env.DEV && import.meta.env.VITE_DEBUG)
+ console.log('[Migration] IndexedDB: preserved old database for downgrade compatibility');
}
};
}
}
- console.log(`[Migration] Legacy messages: migrated ${migratedCount} messages`);
+ if (import.meta.env.DEV && import.meta.env.VITE_DEBUG)
+ console.log(`[Migration] Legacy messages: migrated ${migratedCount} messages`);
}
};
async run(): Promise<void> {
const legacyTheme = localStorage.getItem('theme');
if (legacyTheme === null) {
- console.log('[Migration] Theme: no legacy theme key found, skipping');
+ if (import.meta.env.DEV && import.meta.env.VITE_DEBUG)
+ console.log('[Migration] Theme: no legacy theme key found, skipping');
return;
}
const config = configRaw ? JSON.parse(configRaw) : {};
if (SETTINGS_KEYS.THEME in config) {
- console.log('[Migration] Theme: config already has theme, skipping');
+ if (import.meta.env.DEV && import.meta.env.VITE_DEBUG)
+ console.log('[Migration] Theme: config already has theme, skipping');
return;
}
localStorage.setItem(CONFIG_LOCALSTORAGE_KEY, JSON.stringify(config));
// Non-destructive: DO NOT delete legacy theme key - keep for downgrade compatibility
- console.log(`[Migration] Theme: copied standalone theme to config (preserved old key)`);
+ if (import.meta.env.DEV && import.meta.env.VITE_DEBUG)
+ console.log(`[Migration] Theme: copied standalone theme to config (preserved old key)`);
}
};
*/
resetState(): void {
localStorage.removeItem(MIGRATION_STATE_KEY);
- console.log('[Migration] State reset - all migrations will run again');
+ if (import.meta.env.DEV && import.meta.env.VITE_DEBUG)
+ console.log('[Migration] State reset - all migrations will run again');
},
/**
*/
async runAllMigrations(): Promise<void> {
const state = getMigrationState();
- console.log('[Migration] Starting migration run, state:', state);
+ if (import.meta.env.DEV && import.meta.env.VITE_DEBUG)
+ console.log('[Migration] Starting migration run, state:', state);
for (const migration of migrations) {
if (isMigrationCompleted(migration.id)) {
- console.log(`[Migration] ${migration.id}: already completed, skipping`);
+ if (import.meta.env.DEV && import.meta.env.VITE_DEBUG)
+ console.log(`[Migration] ${migration.id}: already completed, skipping`);
continue;
}
try {
- console.log(`[Migration] ${migration.id}: running...`);
+ if (import.meta.env.DEV && import.meta.env.VITE_DEBUG)
+ console.log(`[Migration] ${migration.id}: running...`);
await migration.run();
markMigrationCompleted(migration.id);
- console.log(`[Migration] ${migration.id}: completed successfully`);
+ if (import.meta.env.DEV && import.meta.env.VITE_DEBUG)
+ console.log(`[Migration] ${migration.id}: completed successfully`);
} catch (error) {
console.error(`[Migration] ${migration.id}: failed`, error);
markMigrationFailed(migration.id);
}
}
- console.log('[Migration] All migrations complete');
+ if (import.meta.env.DEV && import.meta.env.VITE_DEBUG)
+ console.log('[Migration] All migrations complete');
}
};
import { ServerModelStatus, ModelModality } from '$lib/enums';
import { ModelsService } from '$lib/services/models.service';
import { PropsService } from '$lib/services/props.service';
-import { serverStore } from '$lib/stores/server.svelte';
+import { serverStore, isRouterMode } from '$lib/stores/server.svelte';
import { TTLCache } from '$lib/utils';
import {
MODEL_PROPS_CACHE_TTL_MS,
import { conversationsStore } from '$lib/stores/conversations.svelte';
/**
- * modelsStore - Reactive store for model management in both MODEL and ROUTER modes
- *
- * This store manages:
- * - Available models list
- * - Selected model for new conversations
- * - Loaded models tracking (ROUTER mode)
- * - Model usage tracking per conversation
- * - Automatic unloading of unused models
+ * modelsStore - Reactive store for model management in both MODEL and ROUTER modes.
*
* **Architecture & Relationships:**
* - **ModelsService**: Stateless service for model API communication
*
* **API Inconsistency Workaround:**
* In MODEL mode, `/props` returns modalities for the single model.
- * In ROUTER mode, `/props` has no modalities - must use `/props?model=<id>` per model.
+ * In ROUTER mode, `/props` has no modalities — must use `/props?model=<id>` per model.
* This store normalizes this behavior so consumers don't need to know the server mode.
- *
- * **Key Features:**
- * - **MODEL mode**: Single model, always loaded
- * - **ROUTER mode**: Multi-model with load/unload capability
- * - **Auto-unload**: Automatically unloads models not used by any conversation
- * - **Lazy loading**: ensureModelLoaded() loads models on demand
*/
class ModelsStore {
/**
selectedModelId = $state<string | null>(null);
selectedModelName = $state<string | null>(null);
- // dedup concurrent fetch() callers, all awaiters share the same inflight promise
- // without this, ?model=<name> URL handler raced an in-progress fetch and saw an empty list
+ // Dedup concurrent fetch() callers — all awaiters share the same inflight promise.
+ // Without this, ?model=<name> URL handler races an in-progress fetch and sees an empty list.
private inflightFetch: Promise<void> | null = null;
private modelUsage = $state<Map<string, SvelteSet<string>>>(new Map());
favoriteModelIds = $state<Set<string>>(this.loadFavoritesFromStorage());
/**
- * Model-specific props cache with TTL
- * Key: modelId, Value: props data including modalities
- * TTL: 10 minutes - props don't change frequently
+ * Model-specific props cache with TTL.
+ * Key: modelId, Value: props data including modalities.
+ * TTL: 10 minutes — props don't change frequently.
*/
private modelPropsCache = new TTLCache<string, ApiLlamaCppServerProps>({
ttlMs: MODEL_PROPS_CACHE_TTL_MS,
private modelPropsFetching = $state<Set<string>>(new Set());
/**
- * Version counter for props cache - used to trigger reactivity when props are updated
+ * Version counter for props cache — used to trigger reactivity when props are updated.
*/
propsCacheVersion = $state(0);
get selectedModel(): ModelOption | null {
if (!this.selectedModelId) return null;
- return this.models.find((model) => model.id === this.selectedModelId) ?? null;
+ return this.models.find((m) => m.id === this.selectedModelId) ?? null;
}
get loadedModelIds(): string[] {
* In ROUTER mode, returns null (model is per-conversation).
*/
get singleModelName(): string | null {
- if (serverStore.isRouterMode) return null;
+ if (isRouterMode()) return null;
const props = serverStore.props;
if (props?.model_alias) return props.model_alias;
return props.model_path.split(/(\\|\/)/).pop() || null;
}
+ get selectedModelContextSize(): number | null {
+ if (!this.selectedModelName) return null;
+ return this.getModelContextSize(this.selectedModelName);
+ }
+
/**
*
*
*
*/
- /**
- * Get modalities for a specific model
- * Returns cached modalities from model props
- */
getModelModalities(modelId: string): ModelModalities | null {
const model = this.models.find((m) => m.model === modelId || m.id === modelId);
if (model?.modalities) {
const props = this.modelPropsCache.get(modelId);
if (props?.modalities) {
- return {
- vision: props.modalities.vision ?? false,
- audio: props.modalities.audio ?? false,
- video: props.modalities.video ?? false
- };
+ return this.buildModalities(props.modalities);
}
return null;
}
- /**
- * Check if a model supports vision modality
- */
modelSupportsVision(modelId: string): boolean {
return this.getModelModalities(modelId)?.vision ?? false;
}
- /**
- * Check if a model supports audio modality
- */
modelSupportsAudio(modelId: string): boolean {
return this.getModelModalities(modelId)?.audio ?? false;
}
- /**
- * Check if a model supports video modality
- */
modelSupportsVideo(modelId: string): boolean {
return this.getModelModalities(modelId)?.video ?? false;
}
- /**
- * Get model modalities as an array of ModelModality enum values
- */
getModelModalitiesArray(modelId: string): ModelModality[] {
const modalities = this.getModelModalities(modelId);
if (!modalities) return [];
const result: ModelModality[] = [];
-
if (modalities.vision) result.push(ModelModality.VISION);
if (modalities.audio) result.push(ModelModality.AUDIO);
if (modalities.video) result.push(ModelModality.VIDEO);
return result;
}
- /**
- * Get props for a specific model (from cache)
- */
getModelProps(modelId: string): ApiLlamaCppServerProps | null {
return this.modelPropsCache.get(modelId);
}
- /**
- * Get context size (n_ctx) for a specific model from cached props
- */
getModelContextSize(modelId: string): number | null {
const props = this.getModelProps(modelId);
const nCtx = props?.default_generation_settings?.n_ctx;
return typeof nCtx === 'number' ? nCtx : null;
}
- /**
- * Get context size for the currently selected model or null if no model is selected
- */
- get selectedModelContextSize(): number | null {
- if (!this.selectedModelName) return null;
- return this.getModelContextSize(this.selectedModelName);
- }
-
- /**
- * Check if props are being fetched for a model
- */
isModelPropsFetching(modelId: string): boolean {
return this.modelPropsFetching.has(modelId);
}
isModelLoaded(modelId: string): boolean {
const model = this.routerModels.find((m) => m.id === modelId);
+
return (
model?.status.value === ServerModelStatus.LOADED ||
- model?.status.value === ServerModelStatus.SLEEPING ||
- false
+ model?.status.value === ServerModelStatus.SLEEPING
);
}
getModelStatus(modelId: string): ServerModelStatus | null {
const model = this.routerModels.find((m) => m.id === modelId);
+
return model?.status.value ?? null;
}
isModelInUse(modelId: string): boolean {
const usage = this.modelUsage.get(modelId);
+
return usage !== undefined && usage.size > 0;
}
*/
/**
- * Fetch list of models from server and detect server role
- * Also fetches modalities for MODEL mode (single model)
+ * Fetch list of models from server and detect server role.
+ * Also fetches modalities for MODEL mode (single model).
*/
async fetch(force = false): Promise<void> {
if (this.inflightFetch) return this.inflightFetch;
await serverStore.fetch();
}
- const response = await ModelsService.list();
-
- const models: ModelOption[] = response.data.map((item: ApiModelDataEntry, index: number) => {
- const details = response.models?.[index];
- const rawCapabilities = Array.isArray(details?.capabilities) ? details?.capabilities : [];
- const displayNameSource =
- details?.name && details.name.trim().length > 0 ? details.name : item.id;
- const displayName = this.toDisplayName(displayNameSource);
- const modelId = details?.model || item.id;
-
- return {
- id: item.id,
- name: displayName,
- model: modelId,
- description: details?.description,
- capabilities: rawCapabilities.filter((value: unknown): value is string => Boolean(value)),
- details: details?.details,
- meta: item.meta ?? null,
- parsedId: ModelsService.parseModelId(modelId),
- aliases: item.aliases ?? [],
- tags: item.tags ?? []
- } satisfies ModelOption;
- });
+ const router = isRouterMode();
- this.models = models;
-
- // WORKAROUND: In MODEL mode, /props returns modalities for the single model,
- // but /v1/models doesn't include modalities. We bridge this gap here.
- const serverProps = serverStore.props;
- if (serverStore.isModelMode && this.models.length > 0 && serverProps?.modalities) {
- const modalities: ModelModalities = {
- vision: serverProps.modalities.vision ?? false,
- audio: serverProps.modalities.audio ?? false,
- video: serverProps.modalities.video ?? false
- };
- this.modelPropsCache.set(this.models[0].model, serverProps);
- this.models = this.models.map((model, index) =>
- index === 0 ? { ...model, modalities } : model
- );
+ if (router) {
+ const response = await ModelsService.listRouter();
+
+ this.routerModels = response.data;
+ this.models = this.buildModelOptions(response);
+
+ await this.fetchModalitiesForLoadedModels();
+
+ const visible = this.getVisibleModels();
+
+ if (visible.length === 1 && this.isModelLoaded(visible[0].model)) {
+ this.selectModelById(visible[0].id);
+ }
+ } else {
+ this.models = await this.fetchModelModeInternal();
}
} catch (error) {
this.models = [];
this.error = error instanceof Error ? error.message : 'Failed to load models';
+
throw error;
} finally {
this.loading = false;
}
}
+ /** Fetch models in MODEL mode (single model, standard OpenAI-compatible). */
+ private async fetchModelModeInternal(): Promise<ModelOption[]> {
+ const response = await ModelsService.list();
+
+ return this.buildModelOptions(response);
+ }
+
+ /**
+ * Build ModelOption[] from an API response.
+ * Both MODEL and ROUTER modes share the same mapping logic;
+ * they differ only in which endpoint is called.
+ */
+ private buildModelOptions(
+ response: ApiModelListResponse | ApiRouterModelsListResponse
+ ): ModelOption[] {
+ return response.data.map((item: ApiModelDataEntry, index: number) => {
+ const details = response.models?.[index];
+ const rawCapabilities = Array.isArray(details?.capabilities) ? details?.capabilities : [];
+ const displayNameSource =
+ details?.name && details.name.trim().length > 0 ? details.name : item.id;
+ const modelId = details?.model || item.id;
+
+ return {
+ id: item.id,
+ name: this.toDisplayName(displayNameSource),
+ model: modelId,
+ description: details?.description,
+ capabilities: rawCapabilities.filter((value: unknown): value is string => Boolean(value)),
+ details: details?.details,
+ meta: item.meta ?? null,
+ parsedId: ModelsService.parseModelId(modelId),
+ aliases: item.aliases ?? [],
+ tags: item.tags ?? []
+ };
+ });
+ }
+
/**
- * Fetch router models with full metadata (ROUTER mode only)
- * This fetches the /models endpoint which returns status info for each model
+ * Fetch router models with full metadata (ROUTER mode only).
+ * No-op in router mode — fetch() already calls listRouter() internally.
+ * Kept for API compatibility (e.g. handleOpenChange dropdown open handler).
*/
async fetchRouterModels(): Promise<void> {
+ if (!isRouterMode()) return;
+
try {
const response = await ModelsService.listRouter();
this.routerModels = response.data;
await this.fetchModalitiesForLoadedModels();
- const o = this.models.filter((option) => this.getModelProps(option.model)?.ui !== false);
-
- if (o.length === 1 && this.isModelLoaded(o[0].model)) {
- this.selectModelById(o[0].id);
+ const visible = this.getVisibleModels();
+ if (visible.length === 1 && this.isModelLoaded(visible[0].model)) {
+ this.selectModelById(visible[0].id);
}
} catch (error) {
console.warn('Failed to fetch router models:', error);
}
/**
- * Fetch props for a specific model from /props endpoint
- * Uses caching to avoid redundant requests
+ * Fetch props for a specific model from /props endpoint.
+ * Uses caching to avoid redundant requests.
*
- * In ROUTER mode, this will only fetch props if the model is loaded,
+ * In ROUTER mode, this only fetches props if the model is loaded,
* since unloaded models return 400 from /props endpoint.
*
* @param modelId - Model identifier to fetch props for
}
}
- /**
- * Fetch modalities for all loaded models from /props endpoint
- * This updates the modalities field in models array
- */
+ /** Fetch modalities for all loaded models from /props endpoint. */
async fetchModalitiesForLoadedModels(): Promise<void> {
const loadedModelIds = this.loadedModelIds;
if (loadedModelIds.length === 0) return;
try {
const results = await Promise.all(propsPromises);
- // Update models with modalities
this.models = this.models.map((model) => {
const modelIndex = loadedModelIds.indexOf(model.model);
if (modelIndex === -1) return model;
const props = results[modelIndex];
if (!props?.modalities) return model;
- const modalities: ModelModalities = {
- vision: props.modalities.vision ?? false,
- audio: props.modalities.audio ?? false,
- video: props.modalities.video ?? false
- };
-
- return { ...model, modalities };
+ return { ...model, modalities: this.buildModalities(props.modalities) };
});
this.propsCacheVersion++;
}
}
+ /**
+ * Update modalities for a specific model.
+ * Called when a model is loaded or when we need fresh modality data.
+ */
+ async updateModelModalities(modelId: string): Promise<void> {
+ const props = await this.fetchModelProps(modelId);
+ if (!props?.modalities) return;
+
+ this.models = this.models.map((model) =>
+ model.model === modelId
+ ? { ...model, modalities: this.buildModalities(props.modalities!) }
+ : model
+ );
+
+ this.propsCacheVersion++;
+ }
+
+ /**
+ * Filter to models visible in the UI (ui !== false).
+ */
+ private getVisibleModels(): ModelOption[] {
+ return this.models.filter((option) => this.getModelProps(option.model)?.ui !== false);
+ }
+
/**
* Gets the model name from the last assistant message in the active conversation.
- * Iterates backward through messages to find the most recent message with a model.
* Used by both the chat page and settings page to maintain model consistency.
- * @returns The model name or null if not found
*/
getModelFromLastAssistantResponse(): string | null {
const messages = conversationsStore.activeMessages;
if (!messages || messages.length === 0) return null;
- // Iterate backward to find the last message with a model
for (let i = messages.length - 1; i >= 0; i--) {
if (messages[i].model) {
return messages[i].model;
/**
* Auto-selects the model from the last assistant response if available and loaded.
* Returns true if a model was selected, false otherwise.
- * This is used by the chat page to maintain model consistency across page navigation.
*/
async selectModelFromLastAssistantResponse(): Promise<boolean> {
const lastModel = this.getModelFromLastAssistantResponse();
- if (!lastModel) return false;
-
- // Skip if already selected
- if (this.selectedModelName === lastModel) return false;
+ if (!lastModel || this.selectedModelName === lastModel) return false;
const matchingModel = this.models.find((option) => option.model === lastModel);
- if (!matchingModel) return false;
-
- if (!this.isModelLoaded(lastModel)) {
- console.log('[modelsStore] last assistant model not loaded:', lastModel);
- return false;
- }
+ if (!matchingModel || !this.isModelLoaded(lastModel)) return false;
try {
await this.selectModelById(matchingModel.id);
}
/**
- * Auto-selects the first available model if none is selected, and fetches its props.
+ * Auto-selects the first available model if none is selected.
* Prioritizes:
* 1. Model from active conversation's last assistant response (if loaded)
* 2. Model from active conversation's last assistant response (if not loaded)
* 3. First loaded model (not from active conversation)
* 4. First available model
- * This is used to ensure default values are populated in settings pages.
*/
async ensureFirstModelSelected(): Promise<void> {
if (this.selectedModelName) return;
- // Filter models that are visible in the UI
- const availableModels = this.models.filter(
- (option) => this.getModelProps(option.model)?.ui !== false
- );
-
+ const availableModels = this.getVisibleModels();
if (availableModels.length === 0) return;
// Try to select model from last assistant response first
}
}
- // Try to find a loaded model first
+ // Try a loaded model first
const loadedModel = availableModels.find((m) => this.isModelLoaded(m.model));
if (loadedModel) {
await this.selectModelById(loadedModel.id);
}
// Fall back to the first available model
- const firstModel = availableModels[0];
- await this.selectModelById(firstModel.id);
- // Don't fetch props for unloaded models (will fail in ROUTER mode)
- }
-
- /**
- * Update modalities for a specific model
- * Called when a model is loaded or when we need fresh modality data
- */
- async updateModelModalities(modelId: string): Promise<void> {
- try {
- const props = await this.fetchModelProps(modelId);
- if (!props?.modalities) return;
-
- const modalities: ModelModalities = {
- vision: props.modalities.vision ?? false,
- audio: props.modalities.audio ?? false,
- video: props.modalities.video ?? false
- };
-
- this.models = this.models.map((model) =>
- model.model === modelId ? { ...model, modalities } : model
- );
-
- this.propsCacheVersion++;
- } catch (error) {
- console.warn(`Failed to update modalities for model ${modelId}:`, error);
- }
+ await this.selectModelById(availableModels[0].id);
}
/**
*
*/
- /**
- * Select a model for new conversations
- */
async selectModelById(modelId: string): Promise<void> {
if (!modelId || this.updating) return;
if (this.selectedModelId === modelId) return;
}
/**
- * Select a model by its model name (used for syncing with conversation model)
- * @param modelName - Model name to select (e.g., "ggml-org/GLM-4.7-Flash-GGUF")
+ * Select a model by its model name (used for syncing with conversation model).
*/
selectModelByName(modelName: string): void {
const option = this.models.find((model) => model.model === modelName);
/**
*
*
- * Loading/Unloading Models
+ * Loading / Unloading Models
*
*
*/
/**
* WORKAROUND: Polling for model status after load/unload operations.
*
- * Currently, the `/models/load` and `/models/unload` endpoints return success
- * before the operation actually completes on the server. This means an immediate
- * request to `/models` returns stale status (e.g., "loading" after load request,
- * "loaded" after unload request).
+ * Currently, `/models/load` and `/models/unload` return success before
+ * the operation actually completes on the server.
*
- * TODO: Remove this polling once llama-server properly waits for the operation
- * to complete before returning success from `/load` and `/unload` endpoints.
- * At that point, a single `fetchRouterModels()` call after the operation will
- * be sufficient to get the correct status.
+ * TODO: Remove polling once llama-server properly waits for the operation
+ * to complete before returning success.
*/
- /** Polling interval in ms for checking model status */
private static readonly STATUS_POLL_INTERVAL = 500;
/**
* Poll for expected model status after load/unload operation.
- * Keeps polling indefinitely until the model reaches the expected status or fails.
- *
- * @param modelId - Model identifier to check
- * @param expectedStatus - Expected status to wait for
- * @throws Error if model reaches FAILED status
+ * Keeps polling until the model reaches the expected status or fails.
*/
private async pollForModelStatus(
modelId: string,
await this.fetchRouterModels();
const currentStatus = this.getModelStatus(modelId);
- if (currentStatus === expectedStatus) {
- return;
- }
+ if (currentStatus === expectedStatus) return;
if (currentStatus === ServerModelStatus.FAILED) {
throw new Error(
}
}
- /**
- * Load a model (ROUTER mode)
- * @param modelId - Model identifier to load
- */
async loadModel(modelId: string): Promise<void> {
- if (this.isModelLoaded(modelId)) {
- return;
- }
-
+ if (this.isModelLoaded(modelId)) return;
if (this.modelLoadingStates.get(modelId)) return;
this.modelLoadingStates.set(modelId, true);
try {
await ModelsService.load(modelId);
await this.pollForModelStatus(modelId, ServerModelStatus.LOADED);
-
await this.updateModelModalities(modelId);
toast.success(`Model loaded: ${this.toDisplayName(modelId)}`);
} catch (error) {
}
}
- /**
- * Unload a model (ROUTER mode)
- * @param modelId - Model identifier to unload
- */
async unloadModel(modelId: string): Promise<void> {
- if (!this.isModelLoaded(modelId)) {
- return;
- }
-
+ if (!this.isModelLoaded(modelId)) return;
if (this.modelLoadingStates.get(modelId)) return;
this.modelLoadingStates.set(modelId, true);
try {
await ModelsService.unload(modelId);
-
await this.pollForModelStatus(modelId, ServerModelStatus.UNLOADED);
toast.info(`Model unloaded: ${this.toDisplayName(modelId)}`);
} catch (error) {
}
}
- /**
- * Ensure a model is loaded before use
- * @param modelId - Model identifier to ensure is loaded
- */
async ensureModelLoaded(modelId: string): Promise<void> {
- if (this.isModelLoaded(modelId)) {
- return;
- }
-
+ if (this.isModelLoaded(modelId)) return;
await this.loadModel(modelId);
}
private loadFavoritesFromStorage(): Set<string> {
try {
const raw = localStorage.getItem(FAVORITE_MODELS_LOCALSTORAGE_KEY);
-
return raw ? new Set(JSON.parse(raw) as string[]) : new Set();
} catch {
toast.error('Failed to load favorite models from local storage');
-
return new Set();
}
}
private toDisplayName(id: string): string {
const segments = id.split(/\\|\//);
const candidate = segments.pop();
-
return candidate && candidate.trim().length > 0 ? candidate : id;
}
+ private buildModalities(
+ modalities: NonNullable<ApiLlamaCppServerProps['modalities']>
+ ): ModelModalities {
+ return {
+ vision: modalities.vision ?? false,
+ audio: modalities.audio ?? false,
+ video: modalities.video ?? false
+ };
+ }
+
clear(): void {
this.models = [];
this.routerModels = [];