refactor: Server store

This commit is contained in:
Aleksander Grygier 2025-11-26 17:16:41 +01:00
parent 456828b365
commit d6ee3d133a
19 changed files with 208 additions and 385 deletions

View File

@ -16,7 +16,7 @@
supportsVision, supportsVision,
fetchModelProps, fetchModelProps,
getModelProps getModelProps
} from '$lib/stores/props.svelte'; } from '$lib/stores/server.svelte';
import { getConversationModel } from '$lib/stores/chat.svelte'; import { getConversationModel } from '$lib/stores/chat.svelte';
import { activeMessages } from '$lib/stores/conversations.svelte'; import { activeMessages } from '$lib/stores/conversations.svelte';
import { import {

View File

@ -15,7 +15,7 @@
isRouterMode, isRouterMode,
fetchModelProps, fetchModelProps,
getModelProps getModelProps
} from '$lib/stores/props.svelte'; } from '$lib/stores/server.svelte';
import { config } from '$lib/stores/settings.svelte'; import { config } from '$lib/stores/settings.svelte';
import { modelOptions, selectedModelId, selectModelByName } from '$lib/stores/models.svelte'; import { modelOptions, selectedModelId, selectModelByName } from '$lib/stores/models.svelte';
import { getConversationModel } from '$lib/stores/chat.svelte'; import { getConversationModel } from '$lib/stores/chat.svelte';

View File

@ -18,7 +18,7 @@
import { INPUT_CLASSES } from '$lib/constants/input-classes'; import { INPUT_CLASSES } from '$lib/constants/input-classes';
import Label from '$lib/components/ui/label/label.svelte'; import Label from '$lib/components/ui/label/label.svelte';
import { config } from '$lib/stores/settings.svelte'; import { config } from '$lib/stores/settings.svelte';
import { isRouterMode } from '$lib/stores/props.svelte'; import { isRouterMode } from '$lib/stores/server.svelte';
import { selectModel } from '$lib/stores/models.svelte'; import { selectModel } from '$lib/stores/models.svelte';
import { copyToClipboard } from '$lib/utils/copy'; import { copyToClipboard } from '$lib/utils/copy';
import type { ApiChatCompletionToolCall } from '$lib/types/api'; import type { ApiChatCompletionToolCall } from '$lib/types/api';

View File

@ -32,13 +32,13 @@
import { import {
supportsVision, supportsVision,
supportsAudio, supportsAudio,
propsLoading, serverLoading,
propsError, serverError,
propsStore, serverStore,
isRouterMode, isRouterMode,
fetchModelProps, fetchModelProps,
getModelProps getModelProps
} from '$lib/stores/props.svelte'; } from '$lib/stores/server.svelte';
import { modelOptions, selectedModelId } from '$lib/stores/models.svelte'; import { modelOptions, selectedModelId } from '$lib/stores/models.svelte';
import { getConversationModel } from '$lib/stores/chat.svelte'; import { getConversationModel } from '$lib/stores/chat.svelte';
import { parseFilesToMessageExtras } from '$lib/utils/convert-files-to-extra'; import { parseFilesToMessageExtras } from '$lib/utils/convert-files-to-extra';
@ -87,8 +87,8 @@
); );
let activeErrorDialog = $derived(errorDialog()); let activeErrorDialog = $derived(errorDialog());
let isServerLoading = $derived(propsLoading()); let isServerLoading = $derived(serverLoading());
let hasPropsError = $derived(!!propsError()); let hasPropsError = $derived(!!serverError());
let isCurrentConversationLoading = $derived(isLoading()); let isCurrentConversationLoading = $derived(isLoading());
@ -407,10 +407,10 @@
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<AlertTriangle class="h-4 w-4 text-destructive" /> <AlertTriangle class="h-4 w-4 text-destructive" />
<span class="text-sm font-medium text-destructive">Server unavailable</span> <span class="text-sm font-medium text-destructive">Server unavailable</span>
<span class="text-sm text-muted-foreground">{propsError()}</span> <span class="text-sm text-muted-foreground">{serverError()}</span>
</div> </div>
<button <button
onclick={() => propsStore.fetch()} onclick={() => serverStore.fetch()}
disabled={isServerLoading} disabled={isServerLoading}
class="flex items-center gap-1.5 rounded-lg bg-destructive/20 px-3 py-1.5 text-xs font-medium text-destructive hover:bg-destructive/30 disabled:opacity-50" class="flex items-center gap-1.5 rounded-lg bg-destructive/20 px-3 py-1.5 text-xs font-medium text-destructive hover:bg-destructive/30 disabled:opacity-50"
> >
@ -454,7 +454,7 @@
<h1 class="mb-4 text-3xl font-semibold tracking-tight">llama.cpp</h1> <h1 class="mb-4 text-3xl font-semibold tracking-tight">llama.cpp</h1>
<p class="text-lg text-muted-foreground"> <p class="text-lg text-muted-foreground">
{propsStore.serverProps?.modalities?.audio {serverStore.props?.modalities?.audio
? 'Record audio, type a message ' ? 'Record audio, type a message '
: 'Type a message'} or upload files to get started : 'Type a message'} or upload files to get started
</p> </p>
@ -467,10 +467,10 @@
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<AlertTriangle class="h-4 w-4 text-destructive" /> <AlertTriangle class="h-4 w-4 text-destructive" />
<span class="text-sm font-medium text-destructive">Server unavailable</span> <span class="text-sm font-medium text-destructive">Server unavailable</span>
<span class="text-sm text-muted-foreground">{propsError()}</span> <span class="text-sm text-muted-foreground">{serverError()}</span>
</div> </div>
<button <button
onclick={() => propsStore.fetch()} onclick={() => serverStore.fetch()}
disabled={isServerLoading} disabled={isServerLoading}
class="flex items-center gap-1.5 rounded-lg bg-destructive/20 px-3 py-1.5 text-xs font-medium text-destructive hover:bg-destructive/30 disabled:opacity-50" class="flex items-center gap-1.5 rounded-lg bg-destructive/20 px-3 py-1.5 text-xs font-medium text-destructive hover:bg-destructive/30 disabled:opacity-50"
> >

View File

@ -6,7 +6,7 @@
import * as Select from '$lib/components/ui/select'; import * as Select from '$lib/components/ui/select';
import { Textarea } from '$lib/components/ui/textarea'; import { Textarea } from '$lib/components/ui/textarea';
import { SETTING_CONFIG_DEFAULT, SETTING_CONFIG_INFO } from '$lib/constants/settings-config'; import { SETTING_CONFIG_DEFAULT, SETTING_CONFIG_INFO } from '$lib/constants/settings-config';
import { supportsVision } from '$lib/stores/props.svelte'; import { supportsVision } from '$lib/stores/server.svelte';
import { getParameterInfo, resetParameterToServerDefault } from '$lib/stores/settings.svelte'; import { getParameterInfo, resetParameterToServerDefault } from '$lib/stores/settings.svelte';
import { ParameterSyncService } from '$lib/services/parameter-sync'; import { ParameterSyncService } from '$lib/services/parameter-sync';
import { ChatSettingsParameterSourceIndicator } from '$lib/components/app'; import { ChatSettingsParameterSourceIndicator } from '$lib/components/app';

View File

@ -2,7 +2,7 @@
import * as Dialog from '$lib/components/ui/dialog'; import * as Dialog from '$lib/components/ui/dialog';
import * as Table from '$lib/components/ui/table'; import * as Table from '$lib/components/ui/table';
import { BadgeModality, CopyToClipboardIcon } from '$lib/components/app'; import { BadgeModality, CopyToClipboardIcon } from '$lib/components/app';
import { propsStore } from '$lib/stores/props.svelte'; import { serverStore } from '$lib/stores/server.svelte';
import { ChatService } from '$lib/services/chat'; import { ChatService } from '$lib/services/chat';
import type { ApiModelListResponse } from '$lib/types/api'; import type { ApiModelListResponse } from '$lib/types/api';
import { formatFileSize, formatParameters, formatNumber } from '$lib/utils/formatters'; import { formatFileSize, formatParameters, formatNumber } from '$lib/utils/formatters';
@ -14,8 +14,8 @@
let { open = $bindable(), onOpenChange }: Props = $props(); let { open = $bindable(), onOpenChange }: Props = $props();
let serverProps = $derived(propsStore.serverProps); let serverProps = $derived(serverStore.props);
let modalities = $derived(propsStore.supportedModalities); let modalities = $derived(serverStore.supportedModalities);
let modelsData = $state<ApiModelListResponse | null>(null); let modelsData = $state<ApiModelListResponse | null>(null);
let isLoadingModels = $state(false); let isLoadingModels = $state(false);
@ -77,12 +77,12 @@
class="resizable-text-container min-w-0 flex-1 truncate" class="resizable-text-container min-w-0 flex-1 truncate"
style:--threshold="12rem" style:--threshold="12rem"
> >
{propsStore.modelName} {serverStore.modelName}
</span> </span>
<CopyToClipboardIcon <CopyToClipboardIcon
text={propsStore.modelName || ''} text={serverStore.modelName || ''}
canCopy={!!propsStore.modelName} canCopy={!!serverStore.modelName}
ariaLabel="Copy model name to clipboard" ariaLabel="Copy model name to clipboard"
/> />
</div> </div>

View File

@ -1,7 +1,7 @@
<script lang="ts"> <script lang="ts">
import { Package } from '@lucide/svelte'; import { Package } from '@lucide/svelte';
import { BadgeInfo, CopyToClipboardIcon } from '$lib/components/app'; import { BadgeInfo, CopyToClipboardIcon } from '$lib/components/app';
import { propsStore } from '$lib/stores/props.svelte'; import { serverStore } from '$lib/stores/server.svelte';
import * as Tooltip from '$lib/components/ui/tooltip'; import * as Tooltip from '$lib/components/ui/tooltip';
import { TOOLTIP_DELAY_DURATION } from '$lib/constants/tooltip-config'; import { TOOLTIP_DELAY_DURATION } from '$lib/constants/tooltip-config';
@ -21,8 +21,8 @@
showTooltip = false showTooltip = false
}: Props = $props(); }: Props = $props();
let model = $derived(modelProp || propsStore.modelName); let model = $derived(modelProp || serverStore.modelName);
let isModelMode = $derived(propsStore.isModelMode); let isModelMode = $derived(serverStore.isModelMode);
</script> </script>
{#snippet badgeContent()} {#snippet badgeContent()}

View File

@ -15,7 +15,7 @@
loadModel loadModel
} from '$lib/stores/models.svelte'; } from '$lib/stores/models.svelte';
import { ServerModelStatus } from '$lib/enums'; import { ServerModelStatus } from '$lib/enums';
import { isRouterMode, propsStore } from '$lib/stores/props.svelte'; import { isRouterMode, serverStore } from '$lib/stores/server.svelte';
import { DialogModelInformation } from '$lib/components/app'; import { DialogModelInformation } from '$lib/components/app';
import type { ModelOption } from '$lib/types/models'; import type { ModelOption } from '$lib/types/models';
@ -43,7 +43,7 @@
let updating = $derived(modelsUpdating()); let updating = $derived(modelsUpdating());
let activeId = $derived(selectedModelId()); let activeId = $derived(selectedModelId());
let isRouter = $derived(isRouterMode()); let isRouter = $derived(isRouterMode());
let serverModel = $derived(propsStore.modelName); let serverModel = $derived(serverStore.modelName);
// Reactive router models state - needed for proper reactivity of status checks // Reactive router models state - needed for proper reactivity of status checks
let currentRouterModels = $derived(routerModels()); let currentRouterModels = $derived(routerModels());

View File

@ -4,7 +4,7 @@
import { Button } from '$lib/components/ui/button'; import { Button } from '$lib/components/ui/button';
import { Input } from '$lib/components/ui/input'; import { Input } from '$lib/components/ui/input';
import Label from '$lib/components/ui/label/label.svelte'; import Label from '$lib/components/ui/label/label.svelte';
import { propsStore, propsLoading } from '$lib/stores/props.svelte'; import { serverStore, serverLoading } from '$lib/stores/server.svelte';
import { config, updateConfig } from '$lib/stores/settings.svelte'; import { config, updateConfig } from '$lib/stores/settings.svelte';
import { fade, fly, scale } from 'svelte/transition'; import { fade, fly, scale } from 'svelte/transition';
@ -24,7 +24,7 @@
showTroubleshooting = false showTroubleshooting = false
}: Props = $props(); }: Props = $props();
let isServerLoading = $derived(propsLoading()); let isServerLoading = $derived(serverLoading());
let isAccessDeniedError = $derived( let isAccessDeniedError = $derived(
error.toLowerCase().includes('access denied') || error.toLowerCase().includes('access denied') ||
error.toLowerCase().includes('invalid api key') || error.toLowerCase().includes('invalid api key') ||
@ -42,7 +42,7 @@
if (onRetry) { if (onRetry) {
onRetry(); onRetry();
} else { } else {
propsStore.fetch(); serverStore.fetch();
} }
} }

View File

@ -2,7 +2,7 @@
import { AlertTriangle, Server } from '@lucide/svelte'; import { AlertTriangle, Server } from '@lucide/svelte';
import { Badge } from '$lib/components/ui/badge'; import { Badge } from '$lib/components/ui/badge';
import { Button } from '$lib/components/ui/button'; import { Button } from '$lib/components/ui/button';
import { serverProps, propsLoading, propsError, modelName } from '$lib/stores/props.svelte'; import { serverProps, serverLoading, serverError, modelName } from '$lib/stores/server.svelte';
interface Props { interface Props {
class?: string; class?: string;
@ -11,8 +11,8 @@
let { class: className = '', showActions = false }: Props = $props(); let { class: className = '', showActions = false }: Props = $props();
let error = $derived(propsError()); let error = $derived(serverError());
let loading = $derived(propsLoading()); let loading = $derived(serverLoading());
let model = $derived(modelName()); let model = $derived(modelName());
let serverData = $derived(serverProps()); let serverData = $derived(serverProps());

View File

@ -1,7 +1,7 @@
import { config } from '$lib/stores/settings.svelte'; import { config } from '$lib/stores/settings.svelte';
import { getJsonHeaders } from '$lib/utils/api-headers'; import { getJsonHeaders } from '$lib/utils/api-headers';
import { selectedModelName } from '$lib/stores/models.svelte'; import { selectedModelName } from '$lib/stores/models.svelte';
import { isRouterMode, propsStore } from '$lib/stores/props.svelte'; import { isRouterMode, serverStore } from '$lib/stores/server.svelte';
import type { import type {
ApiChatCompletionRequest, ApiChatCompletionRequest,
ApiChatCompletionResponse, ApiChatCompletionResponse,
@ -792,7 +792,7 @@ export class ChatService {
* Handles various response formats including streaming chunks and final responses. * Handles various response formats including streaming chunks and final responses.
* *
* WORKAROUND: In single model mode, llama-server returns a default/incorrect model name * WORKAROUND: In single model mode, llama-server returns a default/incorrect model name
* in the response. We override it with the actual model name from propsStore. * in the response. We override it with the actual model name from serverStore.
* *
* @param data - Raw response data from the Chat Completions API * @param data - Raw response data from the Chat Completions API
* @returns Model name string if found, undefined otherwise * @returns Model name string if found, undefined otherwise
@ -803,7 +803,7 @@ export class ChatService {
// because llama-server returns `gpt-3.5-turbo` value in the `model` field // because llama-server returns `gpt-3.5-turbo` value in the `model` field
const isRouter = isRouterMode(); const isRouter = isRouterMode();
if (!isRouter) { if (!isRouter) {
const propsModelName = propsStore.modelName; const propsModelName = serverStore.modelName;
if (propsModelName) { if (propsModelName) {
return propsModelName; return propsModelName;
} }

View File

@ -2,7 +2,7 @@ import { DatabaseService } from '$lib/services/database';
import { chatService } from '$lib/services'; import { chatService } from '$lib/services';
import { conversationsStore } from '$lib/stores/conversations.svelte'; import { conversationsStore } from '$lib/stores/conversations.svelte';
import { config } from '$lib/stores/settings.svelte'; import { config } from '$lib/stores/settings.svelte';
import { contextSize } from '$lib/stores/props.svelte'; import { contextSize } from '$lib/stores/server.svelte';
import { normalizeModelName } from '$lib/utils/model-names'; import { normalizeModelName } from '$lib/utils/model-names';
import { filterByLeafNodeId, findDescendantMessages, findLeafNode } from '$lib/utils/branching'; import { filterByLeafNodeId, findDescendantMessages, findLeafNode } from '$lib/utils/branching';
import { SvelteMap, SvelteSet } from 'svelte/reactivity'; import { SvelteMap, SvelteSet } from 'svelte/reactivity';

View File

@ -1,7 +1,7 @@
import { SvelteSet } from 'svelte/reactivity'; import { SvelteSet } from 'svelte/reactivity';
import { ModelsService } from '$lib/services/models'; import { ModelsService } from '$lib/services/models';
import { ServerModelStatus } from '$lib/enums'; import { ServerModelStatus } from '$lib/enums';
import { propsStore } from '$lib/stores/props.svelte'; import { serverStore } from '$lib/stores/server.svelte';
import type { ModelOption, ModelModalities } from '$lib/types/models'; import type { ModelOption, ModelModalities } from '$lib/types/models';
import type { ApiModelDataEntry } from '$lib/types/api'; import type { ApiModelDataEntry } from '$lib/types/api';
@ -18,7 +18,7 @@ import type { ApiModelDataEntry } from '$lib/types/api';
* **Architecture & Relationships:** * **Architecture & Relationships:**
* - **ModelsService**: Stateless service for API communication * - **ModelsService**: Stateless service for API communication
* - **ModelsStore** (this class): Reactive store for model state * - **ModelsStore** (this class): Reactive store for model state
* - **PropsStore**: Provides server mode detection * - **ServerStore**: Provides server mode detection
* - **ConversationsStore**: Tracks which conversations use which models * - **ConversationsStore**: Tracks which conversations use which models
* *
* **Key Features:** * **Key Features:**
@ -32,121 +32,62 @@ class ModelsStore {
// State // State
// ───────────────────────────────────────────────────────────────────────────── // ─────────────────────────────────────────────────────────────────────────────
private _models = $state<ModelOption[]>([]); models = $state<ModelOption[]>([]);
private _routerModels = $state<ApiModelDataEntry[]>([]); routerModels = $state<ApiModelDataEntry[]>([]);
private _loading = $state(false); loading = $state(false);
private _updating = $state(false); updating = $state(false);
private _error = $state<string | null>(null); error = $state<string | null>(null);
private _selectedModelId = $state<string | null>(null); selectedModelId = $state<string | null>(null);
private _selectedModelName = $state<string | null>(null); selectedModelName = $state<string | null>(null);
/** Maps modelId -> Set of conversationIds that use this model */ private modelUsage = $state<Map<string, SvelteSet<string>>>(new Map());
private _modelUsage = $state<Map<string, SvelteSet<string>>>(new Map()); private modelLoadingStates = $state<Map<string, boolean>>(new Map());
/** Maps modelId -> loading state for load/unload operations */
private _modelLoadingStates = $state<Map<string, boolean>>(new Map());
// ───────────────────────────────────────────────────────────────────────────── // ─────────────────────────────────────────────────────────────────────────────
// Getters - Basic // Computed Getters
// ───────────────────────────────────────────────────────────────────────────── // ─────────────────────────────────────────────────────────────────────────────
get models(): ModelOption[] {
return this._models;
}
get routerModels(): ApiModelDataEntry[] {
return this._routerModels;
}
get loading(): boolean {
return this._loading;
}
get updating(): boolean {
return this._updating;
}
get error(): string | null {
return this._error;
}
get selectedModelId(): string | null {
return this._selectedModelId;
}
get selectedModelName(): string | null {
return this._selectedModelName;
}
get selectedModel(): ModelOption | null { get selectedModel(): ModelOption | null {
if (!this._selectedModelId) { if (!this.selectedModelId) return null;
return null; return this.models.find((model) => model.id === this.selectedModelId) ?? null;
} }
return this._models.find((model) => model.id === this._selectedModelId) ?? null;
}
// ─────────────────────────────────────────────────────────────────────────────
// Getters - Loaded Models (ROUTER mode)
// ─────────────────────────────────────────────────────────────────────────────
/**
* Get list of currently loaded model IDs
*/
get loadedModelIds(): string[] { get loadedModelIds(): string[] {
return this._routerModels return this.routerModels
.filter((m) => m.status.value === ServerModelStatus.LOADED) .filter((m) => m.status.value === ServerModelStatus.LOADED)
.map((m) => m.name); .map((m) => m.name);
} }
/**
* Get list of models currently being loaded/unloaded
*/
get loadingModelIds(): string[] { get loadingModelIds(): string[] {
return Array.from(this._modelLoadingStates.entries()) return Array.from(this.modelLoadingStates.entries())
.filter(([, loading]) => loading) .filter(([, loading]) => loading)
.map(([id]) => id); .map(([id]) => id);
} }
/** // ─────────────────────────────────────────────────────────────────────────────
* Check if a specific model is loaded // Methods - Model Status
*/ // ─────────────────────────────────────────────────────────────────────────────
isModelLoaded(modelId: string): boolean { isModelLoaded(modelId: string): boolean {
const model = this._routerModels.find((m) => m.name === modelId); const model = this.routerModels.find((m) => m.name === modelId);
return model?.status.value === ServerModelStatus.LOADED || false; return model?.status.value === ServerModelStatus.LOADED || false;
} }
/**
* Check if a specific model is currently loading/unloading
*/
isModelOperationInProgress(modelId: string): boolean { isModelOperationInProgress(modelId: string): boolean {
return this._modelLoadingStates.get(modelId) ?? false; return this.modelLoadingStates.get(modelId) ?? false;
} }
/**
* Get the status of a specific model
*/
getModelStatus(modelId: string): ServerModelStatus | null { getModelStatus(modelId: string): ServerModelStatus | null {
const model = this._routerModels.find((m) => m.name === modelId); const model = this.routerModels.find((m) => m.name === modelId);
return model?.status.value ?? null; return model?.status.value ?? null;
} }
// ─────────────────────────────────────────────────────────────────────────────
// Getters - Model Usage
// ─────────────────────────────────────────────────────────────────────────────
/**
* Get set of conversation IDs using a specific model
*/
getModelUsage(modelId: string): SvelteSet<string> { getModelUsage(modelId: string): SvelteSet<string> {
return this._modelUsage.get(modelId) ?? new SvelteSet<string>(); return this.modelUsage.get(modelId) ?? new SvelteSet<string>();
} }
/**
* Check if a model is used by any conversation
*/
isModelInUse(modelId: string): boolean { isModelInUse(modelId: string): boolean {
const usage = this._modelUsage.get(modelId); const usage = this.modelUsage.get(modelId);
return usage !== undefined && usage.size > 0; return usage !== undefined && usage.size > 0;
} }
@ -158,11 +99,11 @@ class ModelsStore {
* Fetch list of models from server * Fetch list of models from server
*/ */
async fetch(force = false): Promise<void> { async fetch(force = false): Promise<void> {
if (this._loading) return; if (this.loading) return;
if (this._models.length > 0 && !force) return; if (this.models.length > 0 && !force) return;
this._loading = true; this.loading = true;
this._error = null; this.error = null;
try { try {
const response = await ModelsService.list(); const response = await ModelsService.list();
@ -185,18 +126,13 @@ class ModelsStore {
} satisfies ModelOption; } satisfies ModelOption;
}); });
this._models = models; this.models = models;
// Don't auto-select any model - selection should come from:
// 1. User explicitly selecting a model in the UI
// 2. Conversation model (synced via ChatFormActions effect)
} catch (error) { } catch (error) {
this._models = []; this.models = [];
this._error = error instanceof Error ? error.message : 'Failed to load models'; this.error = error instanceof Error ? error.message : 'Failed to load models';
throw error; throw error;
} finally { } finally {
this._loading = false; this.loading = false;
} }
} }
@ -207,13 +143,11 @@ class ModelsStore {
async fetchRouterModels(): Promise<void> { async fetchRouterModels(): Promise<void> {
try { try {
const response = await ModelsService.listRouter(); const response = await ModelsService.listRouter();
this._routerModels = response.data; this.routerModels = response.data;
// Fetch modalities for loaded models
await this.fetchModalitiesForLoadedModels(); await this.fetchModalitiesForLoadedModels();
} catch (error) { } catch (error) {
console.warn('Failed to fetch router models:', error); console.warn('Failed to fetch router models:', error);
this._routerModels = []; this.routerModels = [];
} }
} }
@ -226,13 +160,13 @@ class ModelsStore {
if (loadedModelIds.length === 0) return; if (loadedModelIds.length === 0) return;
// Fetch props for each loaded model in parallel // Fetch props for each loaded model in parallel
const propsPromises = loadedModelIds.map((modelId) => propsStore.fetchModelProps(modelId)); const propsPromises = loadedModelIds.map((modelId) => serverStore.fetchModelProps(modelId));
try { try {
const results = await Promise.all(propsPromises); const results = await Promise.all(propsPromises);
// Update models with modalities // Update models with modalities
this._models = this._models.map((model) => { this.models = this.models.map((model) => {
const modelIndex = loadedModelIds.indexOf(model.model); const modelIndex = loadedModelIds.indexOf(model.model);
if (modelIndex === -1) return model; if (modelIndex === -1) return model;
@ -257,7 +191,7 @@ class ModelsStore {
*/ */
async updateModelModalities(modelId: string): Promise<void> { async updateModelModalities(modelId: string): Promise<void> {
try { try {
const props = await propsStore.fetchModelProps(modelId); const props = await serverStore.fetchModelProps(modelId);
if (!props?.modalities) return; if (!props?.modalities) return;
const modalities: ModelModalities = { const modalities: ModelModalities = {
@ -265,7 +199,7 @@ class ModelsStore {
audio: props.modalities.audio ?? false audio: props.modalities.audio ?? false
}; };
this._models = this._models.map((model) => this.models = this.models.map((model) =>
model.model === modelId ? { ...model, modalities } : model model.model === modelId ? { ...model, modalities } : model
); );
} catch (error) { } catch (error) {
@ -281,27 +215,20 @@ class ModelsStore {
* Select a model for new conversations * Select a model for new conversations
*/ */
async select(modelId: string): Promise<void> { async select(modelId: string): Promise<void> {
if (!modelId || this._updating) { if (!modelId || this.updating) return;
return; if (this.selectedModelId === modelId) return;
}
if (this._selectedModelId === modelId) { const option = this.models.find((model) => model.id === modelId);
return; if (!option) throw new Error('Selected model is not available');
}
const option = this._models.find((model) => model.id === modelId); this.updating = true;
if (!option) { this.error = null;
throw new Error('Selected model is not available');
}
this._updating = true;
this._error = null;
try { try {
this._selectedModelId = option.id; this.selectedModelId = option.id;
this._selectedModelName = option.model; this.selectedModelName = option.model;
} finally { } finally {
this._updating = false; this.updating = false;
} }
} }
@ -310,47 +237,28 @@ class ModelsStore {
* @param modelName - Model name to select (e.g., "unsloth/gemma-3-12b-it-GGUF:latest") * @param modelName - Model name to select (e.g., "unsloth/gemma-3-12b-it-GGUF:latest")
*/ */
selectModelByName(modelName: string): void { selectModelByName(modelName: string): void {
const option = this._models.find((model) => model.model === modelName); const option = this.models.find((model) => model.model === modelName);
if (option) { if (option) {
this._selectedModelId = option.id; this.selectedModelId = option.id;
this._selectedModelName = option.model; this.selectedModelName = option.model;
// Don't persist - this is just for syncing with conversation
} }
} }
/**
* Clear the current model selection
*/
clearSelection(): void { clearSelection(): void {
this._selectedModelId = null; this.selectedModelId = null;
this._selectedModelName = null; this.selectedModelName = null;
} }
/**
* Find a model by its model name
* @param modelName - Model name to search for (e.g., "unsloth/gemma-3-12b-it-GGUF:latest")
* @returns ModelOption if found, null otherwise
*/
findModelByName(modelName: string): ModelOption | null { findModelByName(modelName: string): ModelOption | null {
return this._models.find((model) => model.model === modelName) ?? null; return this.models.find((model) => model.model === modelName) ?? null;
} }
/**
* Find a model by its display ID
* @param modelId - Model ID to search for
* @returns ModelOption if found, null otherwise
*/
findModelById(modelId: string): ModelOption | null { findModelById(modelId: string): ModelOption | null {
return this._models.find((model) => model.id === modelId) ?? null; return this.models.find((model) => model.id === modelId) ?? null;
} }
/**
* Check if a model exists by name
* @param modelName - Model name to check
* @returns true if model exists
*/
hasModel(modelName: string): boolean { hasModel(modelName: string): boolean {
return this._models.some((model) => model.model === modelName); return this.models.some((model) => model.model === modelName);
} }
// ───────────────────────────────────────────────────────────────────────────── // ─────────────────────────────────────────────────────────────────────────────
@ -414,12 +322,10 @@ class ModelsStore {
return; return;
} }
if (this._modelLoadingStates.get(modelId)) { if (this.modelLoadingStates.get(modelId)) return;
return; // Already loading
}
this._modelLoadingStates.set(modelId, true); this.modelLoadingStates.set(modelId, true);
this._error = null; this.error = null;
try { try {
await ModelsService.load(modelId); await ModelsService.load(modelId);
@ -427,13 +333,12 @@ class ModelsStore {
// Poll until model is loaded // Poll until model is loaded
await this.pollForModelStatus(modelId, ServerModelStatus.LOADED); await this.pollForModelStatus(modelId, ServerModelStatus.LOADED);
// Update modalities for this specific model
await this.updateModelModalities(modelId); await this.updateModelModalities(modelId);
} catch (error) { } catch (error) {
this._error = error instanceof Error ? error.message : 'Failed to load model'; this.error = error instanceof Error ? error.message : 'Failed to load model';
throw error; throw error;
} finally { } finally {
this._modelLoadingStates.set(modelId, false); this.modelLoadingStates.set(modelId, false);
} }
} }
@ -446,23 +351,20 @@ class ModelsStore {
return; return;
} }
if (this._modelLoadingStates.get(modelId)) { if (this.modelLoadingStates.get(modelId)) return;
return; // Already unloading
}
this._modelLoadingStates.set(modelId, true); this.modelLoadingStates.set(modelId, true);
this._error = null; this.error = null;
try { try {
await ModelsService.unload(modelId); await ModelsService.unload(modelId);
// Poll until model is unloaded
await this.pollForModelStatus(modelId, ServerModelStatus.UNLOADED); await this.pollForModelStatus(modelId, ServerModelStatus.UNLOADED);
} catch (error) { } catch (error) {
this._error = error instanceof Error ? error.message : 'Failed to unload model'; this.error = error instanceof Error ? error.message : 'Failed to unload model';
throw error; throw error;
} finally { } finally {
this._modelLoadingStates.set(modelId, false); this.modelLoadingStates.set(modelId, false);
} }
} }
@ -486,9 +388,9 @@ class ModelsStore {
* Register that a conversation is using a model * Register that a conversation is using a model
*/ */
registerModelUsage(modelId: string, conversationId: string): void { registerModelUsage(modelId: string, conversationId: string): void {
const usage = this._modelUsage.get(modelId) ?? new SvelteSet<string>(); const usage = this.modelUsage.get(modelId) ?? new SvelteSet<string>();
usage.add(conversationId); usage.add(conversationId);
this._modelUsage.set(modelId, usage); this.modelUsage.set(modelId, usage);
} }
/** /**
@ -502,14 +404,11 @@ class ModelsStore {
conversationId: string, conversationId: string,
autoUnload = true autoUnload = true
): Promise<void> { ): Promise<void> {
const usage = this._modelUsage.get(modelId); const usage = this.modelUsage.get(modelId);
if (usage) { if (usage) {
usage.delete(conversationId); usage.delete(conversationId);
if (usage.size === 0) { if (usage.size === 0) {
this._modelUsage.delete(modelId); this.modelUsage.delete(modelId);
// Auto-unload if model is not used by any conversation
if (autoUnload && this.isModelLoaded(modelId)) { if (autoUnload && this.isModelLoaded(modelId)) {
await this.unloadModel(modelId); await this.unloadModel(modelId);
} }
@ -521,10 +420,8 @@ class ModelsStore {
* Clear all usage for a conversation (when conversation is deleted) * Clear all usage for a conversation (when conversation is deleted)
*/ */
async clearConversationUsage(conversationId: string): Promise<void> { async clearConversationUsage(conversationId: string): Promise<void> {
for (const [modelId, usage] of this._modelUsage.entries()) { for (const [modelId, usage] of this.modelUsage.entries()) {
if (usage.has(conversationId)) { if (usage.has(conversationId)) await this.unregisterModelUsage(modelId, conversationId);
await this.unregisterModelUsage(modelId, conversationId);
}
} }
} }
@ -544,15 +441,15 @@ class ModelsStore {
// ───────────────────────────────────────────────────────────────────────────── // ─────────────────────────────────────────────────────────────────────────────
clear(): void { clear(): void {
this._models = []; this.models = [];
this._routerModels = []; this.routerModels = [];
this._loading = false; this.loading = false;
this._updating = false; this.updating = false;
this._error = null; this.error = null;
this._selectedModelId = null; this.selectedModelId = null;
this._selectedModelName = null; this.selectedModelName = null;
this._modelUsage.clear(); this.modelUsage.clear();
this._modelLoadingStates.clear(); this.modelLoadingStates.clear();
} }
} }

View File

@ -2,143 +2,93 @@ import { PropsService } from '$lib/services/props';
import { ServerRole, ModelModality } from '$lib/enums'; import { ServerRole, ModelModality } from '$lib/enums';
/** /**
* PropsStore - Server properties management and mode detection * ServerStore - Server state, capabilities, and mode detection
* *
* This store manages the server properties fetched from the `/props` endpoint. * This store manages the server connection state and properties fetched from `/props`.
* It provides reactive state for server configuration, capabilities, and mode detection. * It provides reactive state for server configuration, capabilities, and role detection.
* *
* **Architecture & Relationships:** * **Architecture & Relationships:**
* - **PropsService**: Stateless service for fetching `/props` data * - **PropsService**: Stateless service for fetching `/props` data
* - **PropsStore** (this class): Reactive store for server properties * - **ServerStore** (this class): Reactive store for server state
* - **ModelsStore**: Uses server mode for model management strategy * - **ModelsStore**: Uses server role for model management strategy
* *
* **Key Features:** * **Key Features:**
* - **Server Properties**: Model info, context size, build information * - **Server State**: Connection status, loading, error handling
* - **Mode Detection**: MODEL (single model) vs ROUTER (multi-model) * - **Role Detection**: MODEL (single model) vs ROUTER (multi-model)
* - **Capability Detection**: Vision and audio modality support * - **Capability Detection**: Vision and audio modality support
* - **Error Handling**: Clear error states when server unavailable * - **Props Cache**: Per-model props caching for ROUTER mode
*/ */
class PropsStore { class ServerStore {
private _serverProps = $state<ApiLlamaCppServerProps | null>(null); props = $state<ApiLlamaCppServerProps | null>(null);
private _loading = $state(false); loading = $state(false);
private _error = $state<string | null>(null); error = $state<string | null>(null);
private _serverRole = $state<ServerRole | null>(null); role = $state<ServerRole | null>(null);
private fetchPromise: Promise<void> | null = null; private fetchPromise: Promise<void> | null = null;
// Model-specific props cache (ROUTER mode) // Model-specific props cache (ROUTER mode)
private _modelPropsCache = $state<Map<string, ApiLlamaCppServerProps>>(new Map()); private modelPropsCache = $state<Map<string, ApiLlamaCppServerProps>>(new Map());
private _modelPropsFetching = $state<Set<string>>(new Set()); private modelPropsFetching = $state<Set<string>>(new Set());
// ───────────────────────────────────────────────────────────────────────────── // ─────────────────────────────────────────────────────────────────────────────
// Getters - Server Properties // Computed Getters
// ───────────────────────────────────────────────────────────────────────────── // ─────────────────────────────────────────────────────────────────────────────
get serverProps(): ApiLlamaCppServerProps | null {
return this._serverProps;
}
get loading(): boolean {
return this._loading;
}
get error(): string | null {
return this._error;
}
/** /**
* Get model name from server props. * Get model name from server props.
* In MODEL mode: extracts from model_path or model_alias * In MODEL mode: extracts from model_path or model_alias
* In ROUTER mode: returns null (model is per-conversation) * In ROUTER mode: returns null (model is per-conversation)
*/ */
get modelName(): string | null { get modelName(): string | null {
if (this._serverRole === ServerRole.ROUTER) { if (this.role === ServerRole.ROUTER) return null;
return null; if (this.props?.model_alias) return this.props.model_alias;
} if (!this.props?.model_path) return null;
return this.props.model_path.split(/(\\|\/)/).pop() || null;
if (this._serverProps?.model_alias) {
return this._serverProps.model_alias;
}
if (!this._serverProps?.model_path) return null;
return this._serverProps.model_path.split(/(\\|\/)/).pop() || null;
} }
get supportedModalities(): ModelModality[] { get supportedModalities(): ModelModality[] {
const modalities: ModelModality[] = []; const modalities: ModelModality[] = [];
if (this._serverProps?.modalities?.audio) { if (this.props?.modalities?.audio) modalities.push(ModelModality.AUDIO);
modalities.push(ModelModality.AUDIO); if (this.props?.modalities?.vision) modalities.push(ModelModality.VISION);
}
if (this._serverProps?.modalities?.vision) {
modalities.push(ModelModality.VISION);
}
return modalities; return modalities;
} }
get supportsVision(): boolean { get supportsVision(): boolean {
return this._serverProps?.modalities?.vision ?? false; return this.props?.modalities?.vision ?? false;
} }
get supportsAudio(): boolean { get supportsAudio(): boolean {
return this._serverProps?.modalities?.audio ?? false; return this.props?.modalities?.audio ?? false;
} }
get defaultParams(): ApiLlamaCppServerProps['default_generation_settings']['params'] | null { get defaultParams(): ApiLlamaCppServerProps['default_generation_settings']['params'] | null {
return this._serverProps?.default_generation_settings?.params || null; return this.props?.default_generation_settings?.params || null;
} }
/**
* Get context size (n_ctx) from server props
*/
get contextSize(): number | null { get contextSize(): number | null {
return this._serverProps?.default_generation_settings?.n_ctx ?? null; return this.props?.default_generation_settings?.n_ctx ?? null;
} }
/**
* Check if slots endpoint is available (set by --slots flag on server)
*/
get slotsEndpointAvailable(): boolean { get slotsEndpointAvailable(): boolean {
return this._serverProps?.endpoint_slots ?? false; return this.props?.endpoint_slots ?? false;
} }
// ─────────────────────────────────────────────────────────────────────────────
// Getters - Server Mode
// ─────────────────────────────────────────────────────────────────────────────
/**
* Get current server mode
*/
get serverRole(): ServerRole | null {
return this._serverRole;
}
/**
* Detect if server is running in router mode (multi-model management)
*/
get isRouterMode(): boolean { get isRouterMode(): boolean {
return this._serverRole === ServerRole.ROUTER; return this.role === ServerRole.ROUTER;
} }
/**
* Detect if server is running in model mode (single model loaded)
*/
get isModelMode(): boolean { get isModelMode(): boolean {
return this._serverRole === ServerRole.MODEL; return this.role === ServerRole.MODEL;
} }
// ───────────────────────────────────────────────────────────────────────────── // ─────────────────────────────────────────────────────────────────────────────
// Server Mode Detection // Server Role Detection
// ───────────────────────────────────────────────────────────────────────────── // ─────────────────────────────────────────────────────────────────────────────
private detectServerRole(props: ApiLlamaCppServerProps): void { private detectRole(props: ApiLlamaCppServerProps): void {
console.log('Server props role:', props?.role); const newRole = props?.role === ServerRole.ROUTER ? ServerRole.ROUTER : ServerRole.MODEL;
const newMode = if (this.role !== newRole) {
// todo - `role` attribute should always be available on the `/props` endpoint this.role = newRole;
props?.role === ServerRole.ROUTER ? ServerRole.ROUTER : ServerRole.MODEL; console.info(`Server running in ${newRole === ServerRole.ROUTER ? 'ROUTER' : 'MODEL'} mode`);
// Only log when mode changes
if (this._serverRole !== newMode) {
this._serverRole = newMode;
console.info(`Server running in ${newMode === ServerRole.ROUTER ? 'ROUTER' : 'MODEL'} mode`);
} }
} }
@ -146,18 +96,13 @@ class PropsStore {
// Fetch Server Properties // Fetch Server Properties
// ───────────────────────────────────────────────────────────────────────────── // ─────────────────────────────────────────────────────────────────────────────
/**
* Fetches server properties from the server
*/
async fetch(): Promise<void> { async fetch(): Promise<void> {
if (this.fetchPromise) { if (this.fetchPromise) return this.fetchPromise;
return this.fetchPromise;
}
this._loading = true; this.loading = true;
this._error = null; this.error = null;
const previousBuildInfo = this._serverProps?.build_info; const previousBuildInfo = this.props?.build_info;
const fetchPromise = (async () => { const fetchPromise = (async () => {
try { try {
@ -165,18 +110,18 @@ class PropsStore {
// Clear model-specific props cache if server was restarted // Clear model-specific props cache if server was restarted
if (previousBuildInfo && previousBuildInfo !== props.build_info) { if (previousBuildInfo && previousBuildInfo !== props.build_info) {
this._modelPropsCache.clear(); this.modelPropsCache.clear();
console.info('Cleared model props cache due to server restart'); console.info('Cleared model props cache due to server restart');
} }
this._serverProps = props; this.props = props;
this._error = null; this.error = null;
this.detectServerRole(props); this.detectRole(props);
} catch (error) { } catch (error) {
this._error = this.getErrorMessage(error); this.error = this.getErrorMessage(error);
console.error('Error fetching server properties:', error); console.error('Error fetching server properties:', error);
} finally { } finally {
this._loading = false; this.loading = false;
this.fetchPromise = null; this.fetchPromise = null;
} }
})(); })();
@ -189,45 +134,31 @@ class PropsStore {
// Fetch Model-Specific Properties (ROUTER mode) // Fetch Model-Specific Properties (ROUTER mode)
// ───────────────────────────────────────────────────────────────────────────── // ─────────────────────────────────────────────────────────────────────────────
/**
* Get cached props for a specific model
*/
getModelProps(modelId: string): ApiLlamaCppServerProps | null { getModelProps(modelId: string): ApiLlamaCppServerProps | null {
return this._modelPropsCache.get(modelId) ?? null; return this.modelPropsCache.get(modelId) ?? null;
} }
/**
* Check if model props are being fetched
*/
isModelPropsFetching(modelId: string): boolean { isModelPropsFetching(modelId: string): boolean {
return this._modelPropsFetching.has(modelId); return this.modelPropsFetching.has(modelId);
} }
/**
* Fetches properties for a specific model (ROUTER mode)
* Results are cached for subsequent calls
*/
async fetchModelProps(modelId: string): Promise<ApiLlamaCppServerProps | null> { async fetchModelProps(modelId: string): Promise<ApiLlamaCppServerProps | null> {
// Return cached if available const cached = this.modelPropsCache.get(modelId);
const cached = this._modelPropsCache.get(modelId);
if (cached) return cached; if (cached) return cached;
// Don't fetch if already fetching if (this.modelPropsFetching.has(modelId)) return null;
if (this._modelPropsFetching.has(modelId)) {
return null;
}
this._modelPropsFetching.add(modelId); this.modelPropsFetching.add(modelId);
try { try {
const props = await PropsService.fetchForModel(modelId); const props = await PropsService.fetchForModel(modelId);
this._modelPropsCache.set(modelId, props); this.modelPropsCache.set(modelId, props);
return props; return props;
} catch (error) { } catch (error) {
console.warn(`Failed to fetch props for model ${modelId}:`, error); console.warn(`Failed to fetch props for model ${modelId}:`, error);
return null; return null;
} finally { } finally {
this._modelPropsFetching.delete(modelId); this.modelPropsFetching.delete(modelId);
} }
} }
@ -265,42 +196,37 @@ class PropsStore {
// Clear State // Clear State
// ───────────────────────────────────────────────────────────────────────────── // ─────────────────────────────────────────────────────────────────────────────
/**
* Clears all server state
*/
clear(): void { clear(): void {
this._serverProps = null; this.props = null;
this._error = null; this.error = null;
this._loading = false; this.loading = false;
this._serverRole = null; this.role = null;
this.fetchPromise = null; this.fetchPromise = null;
this._modelPropsCache.clear(); this.modelPropsCache.clear();
} }
} }
export const propsStore = new PropsStore(); export const serverStore = new ServerStore();
// ───────────────────────────────────────────────────────────────────────────── // ─────────────────────────────────────────────────────────────────────────────
// Reactive Getters (for use in components) // Reactive Getters (for use in components)
// ───────────────────────────────────────────────────────────────────────────── // ─────────────────────────────────────────────────────────────────────────────
export const serverProps = () => propsStore.serverProps; export const serverProps = () => serverStore.props;
export const propsLoading = () => propsStore.loading; export const serverLoading = () => serverStore.loading;
export const propsError = () => propsStore.error; export const serverError = () => serverStore.error;
export const modelName = () => propsStore.modelName; export const serverRole = () => serverStore.role;
export const supportedModalities = () => propsStore.supportedModalities; export const modelName = () => serverStore.modelName;
export const supportsVision = () => propsStore.supportsVision; export const supportedModalities = () => serverStore.supportedModalities;
export const supportsAudio = () => propsStore.supportsAudio; export const supportsVision = () => serverStore.supportsVision;
export const slotsEndpointAvailable = () => propsStore.slotsEndpointAvailable; export const supportsAudio = () => serverStore.supportsAudio;
export const defaultParams = () => propsStore.defaultParams; export const slotsEndpointAvailable = () => serverStore.slotsEndpointAvailable;
export const contextSize = () => propsStore.contextSize; export const defaultParams = () => serverStore.defaultParams;
export const contextSize = () => serverStore.contextSize;
// Server mode exports export const isRouterMode = () => serverStore.isRouterMode;
export const serverRole = () => propsStore.serverRole; export const isModelMode = () => serverStore.isModelMode;
export const isRouterMode = () => propsStore.isRouterMode;
export const isModelMode = () => propsStore.isModelMode;
// Actions // Actions
export const fetchProps = propsStore.fetch.bind(propsStore); export const fetchServerProps = serverStore.fetch.bind(serverStore);
export const fetchModelProps = propsStore.fetchModelProps.bind(propsStore); export const fetchModelProps = serverStore.fetchModelProps.bind(serverStore);
export const getModelProps = propsStore.getModelProps.bind(propsStore); export const getModelProps = serverStore.getModelProps.bind(serverStore);

View File

@ -35,7 +35,7 @@ import { browser } from '$app/environment';
import { SETTING_CONFIG_DEFAULT } from '$lib/constants/settings-config'; import { SETTING_CONFIG_DEFAULT } from '$lib/constants/settings-config';
import { normalizeFloatingPoint } from '$lib/utils/precision'; import { normalizeFloatingPoint } from '$lib/utils/precision';
import { ParameterSyncService } from '$lib/services/parameter-sync'; import { ParameterSyncService } from '$lib/services/parameter-sync';
import { propsStore } from '$lib/stores/props.svelte'; import { serverStore } from '$lib/stores/server.svelte';
import { setConfigValue, getConfigValue, configToParameterRecord } from '$lib/utils/config-helpers'; import { setConfigValue, getConfigValue, configToParameterRecord } from '$lib/utils/config-helpers';
import { import {
CONFIG_LOCALSTORAGE_KEY, CONFIG_LOCALSTORAGE_KEY,
@ -53,7 +53,7 @@ class SettingsStore {
* Centralizes the pattern of getting and extracting server defaults * Centralizes the pattern of getting and extracting server defaults
*/ */
private getServerDefaults(): Record<string, string | number | boolean> { private getServerDefaults(): Record<string, string | number | boolean> {
const serverParams = propsStore.defaultParams; const serverParams = serverStore.defaultParams;
return serverParams ? ParameterSyncService.extractServerDefaults(serverParams) : {}; return serverParams ? ParameterSyncService.extractServerDefaults(serverParams) : {};
} }
@ -259,7 +259,7 @@ class SettingsStore {
* This sets up the default values from /props endpoint * This sets up the default values from /props endpoint
*/ */
syncWithServerDefaults(): void { syncWithServerDefaults(): void {
const serverParams = propsStore.defaultParams; const serverParams = serverStore.defaultParams;
if (!serverParams) { if (!serverParams) {
console.warn('No server parameters available for initialization'); console.warn('No server parameters available for initialization');

View File

@ -3,7 +3,7 @@ import { isSvgMimeType, svgBase64UrlToPngDataURL } from './svg-to-png';
import { isWebpMimeType, webpBase64UrlToPngDataURL } from './webp-to-png'; import { isWebpMimeType, webpBase64UrlToPngDataURL } from './webp-to-png';
import { FileTypeCategory, AttachmentType } from '$lib/enums'; import { FileTypeCategory, AttachmentType } from '$lib/enums';
import { config, settingsStore } from '$lib/stores/settings.svelte'; import { config, settingsStore } from '$lib/stores/settings.svelte';
import { supportsVision } from '$lib/stores/props.svelte'; import { supportsVision } from '$lib/stores/server.svelte';
import { getFileTypeCategory } from '$lib/utils/file-type'; import { getFileTypeCategory } from '$lib/utils/file-type';
import { readFileAsText, isLikelyTextFile } from './text-files'; import { readFileAsText, isLikelyTextFile } from './text-files';
import { toast } from 'svelte-sonner'; import { toast } from 'svelte-sonner';

View File

@ -3,7 +3,7 @@ import { isTextFileByName } from './text-files';
import { isWebpMimeType, webpBase64UrlToPngDataURL } from './webp-to-png'; import { isWebpMimeType, webpBase64UrlToPngDataURL } from './webp-to-png';
import { FileTypeCategory } from '$lib/enums'; import { FileTypeCategory } from '$lib/enums';
import { getFileTypeCategory } from '$lib/utils/file-type'; import { getFileTypeCategory } from '$lib/utils/file-type';
import { supportsVision } from '$lib/stores/props.svelte'; import { supportsVision } from '$lib/stores/server.svelte';
import { settingsStore } from '$lib/stores/settings.svelte'; import { settingsStore } from '$lib/stores/settings.svelte';
import { toast } from 'svelte-sonner'; import { toast } from 'svelte-sonner';

View File

@ -9,7 +9,7 @@
setTitleUpdateConfirmationCallback setTitleUpdateConfirmationCallback
} from '$lib/stores/conversations.svelte'; } from '$lib/stores/conversations.svelte';
import * as Sidebar from '$lib/components/ui/sidebar/index.js'; import * as Sidebar from '$lib/components/ui/sidebar/index.js';
import { isRouterMode, propsStore } from '$lib/stores/props.svelte'; import { isRouterMode, serverStore } from '$lib/stores/server.svelte';
import { config, settingsStore } from '$lib/stores/settings.svelte'; import { config, settingsStore } from '$lib/stores/settings.svelte';
import { ModeWatcher } from 'mode-watcher'; import { ModeWatcher } from 'mode-watcher';
import { Toaster } from 'svelte-sonner'; import { Toaster } from 'svelte-sonner';
@ -95,16 +95,16 @@
// Initialize server properties on app load (run once) // Initialize server properties on app load (run once)
$effect(() => { $effect(() => {
// Only fetch if we don't already have props // Only fetch if we don't already have props
if (!propsStore.serverProps) { if (!serverStore.props) {
untrack(() => { untrack(() => {
propsStore.fetch(); serverStore.fetch();
}); });
} }
}); });
// Sync settings when server props are loaded // Sync settings when server props are loaded
$effect(() => { $effect(() => {
const serverProps = propsStore.serverProps; const serverProps = serverStore.props;
if (serverProps?.default_generation_settings?.params) { if (serverProps?.default_generation_settings?.params) {
settingsStore.syncWithServerDefaults(); settingsStore.syncWithServerDefaults();

View File

@ -1,12 +1,12 @@
import { propsStore } from '$lib/stores/props.svelte'; import { serverStore } from '$lib/stores/server.svelte';
/** /**
* Mock server properties for Storybook testing * Mock server properties for Storybook testing
* This utility allows setting mock server configurations without polluting production code * This utility allows setting mock server configurations without polluting production code
*/ */
export function mockServerProps(props: Partial<ApiLlamaCppServerProps>): void { export function mockServerProps(props: Partial<ApiLlamaCppServerProps>): void {
// Directly set the private _serverProps for testing purposes // Directly set the props for testing purposes
(propsStore as unknown as { _serverProps: ApiLlamaCppServerProps })._serverProps = { (serverStore as unknown as { props: ApiLlamaCppServerProps }).props = {
model_path: props.model_path || 'test-model', model_path: props.model_path || 'test-model',
modalities: { modalities: {
vision: props.modalities?.vision ?? false, vision: props.modalities?.vision ?? false,
@ -17,18 +17,18 @@ export function mockServerProps(props: Partial<ApiLlamaCppServerProps>): void {
} }
/** /**
* Reset props store to clean state for testing * Reset server store to clean state for testing
*/ */
export function resetPropsStore(): void { export function resetServerStore(): void {
(propsStore as unknown as { _serverProps: ApiLlamaCppServerProps })._serverProps = { (serverStore as unknown as { props: ApiLlamaCppServerProps }).props = {
model_path: '', model_path: '',
modalities: { modalities: {
vision: false, vision: false,
audio: false audio: false
} }
} as ApiLlamaCppServerProps; } as ApiLlamaCppServerProps;
(propsStore as unknown as { _error: string })._error = ''; (serverStore as unknown as { error: string }).error = '';
(propsStore as unknown as { _loading: boolean })._loading = false; (serverStore as unknown as { loading: boolean }).loading = false;
} }
/** /**