refactor: Model modality handling
This commit is contained in:
parent
d6ee3d133a
commit
13e7988459
|
|
@ -9,14 +9,15 @@
|
||||||
} from '$lib/components/app';
|
} from '$lib/components/app';
|
||||||
import { INPUT_CLASSES } from '$lib/constants/input-classes';
|
import { INPUT_CLASSES } from '$lib/constants/input-classes';
|
||||||
import { config } from '$lib/stores/settings.svelte';
|
import { config } from '$lib/stores/settings.svelte';
|
||||||
import { modelOptions, selectedModelId } from '$lib/stores/models.svelte';
|
|
||||||
import {
|
import {
|
||||||
|
modelOptions,
|
||||||
|
selectedModelId,
|
||||||
isRouterMode,
|
isRouterMode,
|
||||||
supportsAudio,
|
|
||||||
supportsVision,
|
|
||||||
fetchModelProps,
|
fetchModelProps,
|
||||||
getModelProps
|
getModelProps,
|
||||||
} from '$lib/stores/server.svelte';
|
modelSupportsVision,
|
||||||
|
modelSupportsAudio
|
||||||
|
} from '$lib/stores/models.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 {
|
||||||
|
|
@ -117,28 +118,20 @@
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Derive modalities from model props (ROUTER) or server props (MODEL)
|
// Derive modalities from active model (works for both MODEL and ROUTER mode)
|
||||||
let hasAudioModality = $derived.by(() => {
|
let hasAudioModality = $derived.by(() => {
|
||||||
if (!isRouter) return supportsAudio();
|
|
||||||
|
|
||||||
if (activeModelId) {
|
if (activeModelId) {
|
||||||
void modelPropsVersion;
|
void modelPropsVersion; // Trigger reactivity on props fetch
|
||||||
const props = getModelProps(activeModelId);
|
return modelSupportsAudio(activeModelId);
|
||||||
if (props) return props.modalities?.audio ?? false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
});
|
});
|
||||||
|
|
||||||
let hasVisionModality = $derived.by(() => {
|
let hasVisionModality = $derived.by(() => {
|
||||||
if (!isRouter) return supportsVision();
|
|
||||||
|
|
||||||
if (activeModelId) {
|
if (activeModelId) {
|
||||||
void modelPropsVersion;
|
void modelPropsVersion; // Trigger reactivity on props fetch
|
||||||
const props = getModelProps(activeModelId);
|
return modelSupportsVision(activeModelId);
|
||||||
if (props) return props.modalities?.vision ?? false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,15 +9,17 @@
|
||||||
} from '$lib/components/app';
|
} from '$lib/components/app';
|
||||||
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 { config } from '$lib/stores/settings.svelte';
|
||||||
import {
|
import {
|
||||||
supportsAudio,
|
modelOptions,
|
||||||
supportsVision,
|
selectedModelId,
|
||||||
|
selectModelByName,
|
||||||
isRouterMode,
|
isRouterMode,
|
||||||
fetchModelProps,
|
fetchModelProps,
|
||||||
getModelProps
|
getModelProps,
|
||||||
} from '$lib/stores/server.svelte';
|
modelSupportsVision,
|
||||||
import { config } from '$lib/stores/settings.svelte';
|
modelSupportsAudio
|
||||||
import { modelOptions, selectedModelId, selectModelByName } from '$lib/stores/models.svelte';
|
} from '$lib/stores/models.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 type { ChatUploadedFile } from '$lib/types/chat';
|
import type { ChatUploadedFile } from '$lib/types/chat';
|
||||||
|
|
@ -101,29 +103,20 @@
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Derive modalities from active model (works for both MODEL and ROUTER mode)
|
||||||
let hasAudioModality = $derived.by(() => {
|
let hasAudioModality = $derived.by(() => {
|
||||||
if (!isRouter) return supportsAudio();
|
|
||||||
|
|
||||||
if (activeModelId) {
|
if (activeModelId) {
|
||||||
void modelPropsVersion;
|
void modelPropsVersion; // Trigger reactivity on props fetch
|
||||||
|
return modelSupportsAudio(activeModelId);
|
||||||
const props = getModelProps(activeModelId);
|
|
||||||
if (props) return props.modalities?.audio ?? false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
});
|
});
|
||||||
|
|
||||||
let hasVisionModality = $derived.by(() => {
|
let hasVisionModality = $derived.by(() => {
|
||||||
if (!isRouter) return supportsVision();
|
|
||||||
|
|
||||||
if (activeModelId) {
|
if (activeModelId) {
|
||||||
void modelPropsVersion;
|
void modelPropsVersion; // Trigger reactivity on props fetch
|
||||||
|
return modelSupportsVision(activeModelId);
|
||||||
const props = getModelProps(activeModelId);
|
|
||||||
if (props) return props.modalities?.vision ?? false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -29,17 +29,16 @@
|
||||||
deleteConversation
|
deleteConversation
|
||||||
} from '$lib/stores/conversations.svelte';
|
} from '$lib/stores/conversations.svelte';
|
||||||
import { config } from '$lib/stores/settings.svelte';
|
import { config } from '$lib/stores/settings.svelte';
|
||||||
|
import { serverLoading, serverError, serverStore } from '$lib/stores/server.svelte';
|
||||||
import {
|
import {
|
||||||
supportsVision,
|
modelOptions,
|
||||||
supportsAudio,
|
selectedModelId,
|
||||||
serverLoading,
|
|
||||||
serverError,
|
|
||||||
serverStore,
|
|
||||||
isRouterMode,
|
isRouterMode,
|
||||||
fetchModelProps,
|
fetchModelProps,
|
||||||
getModelProps
|
getModelProps,
|
||||||
} from '$lib/stores/server.svelte';
|
modelSupportsVision,
|
||||||
import { modelOptions, selectedModelId } from '$lib/stores/models.svelte';
|
modelSupportsAudio
|
||||||
|
} 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';
|
||||||
import { isFileTypeSupported } from '$lib/utils/file-type';
|
import { isFileTypeSupported } from '$lib/utils/file-type';
|
||||||
|
|
@ -133,28 +132,20 @@
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Derive modalities from model props (ROUTER) or server props (MODEL)
|
// Derive modalities from active model (works for both MODEL and ROUTER mode)
|
||||||
let hasAudioModality = $derived.by(() => {
|
let hasAudioModality = $derived.by(() => {
|
||||||
if (!isRouter) return supportsAudio();
|
|
||||||
|
|
||||||
if (activeModelId) {
|
if (activeModelId) {
|
||||||
void modelPropsVersion;
|
void modelPropsVersion; // Trigger reactivity on props fetch
|
||||||
const props = getModelProps(activeModelId);
|
return modelSupportsAudio(activeModelId);
|
||||||
if (props) return props.modalities?.audio ?? false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
});
|
});
|
||||||
|
|
||||||
let hasVisionModality = $derived.by(() => {
|
let hasVisionModality = $derived.by(() => {
|
||||||
if (!isRouter) return supportsVision();
|
|
||||||
|
|
||||||
if (activeModelId) {
|
if (activeModelId) {
|
||||||
void modelPropsVersion;
|
void modelPropsVersion; // Trigger reactivity on props fetch
|
||||||
const props = getModelProps(activeModelId);
|
return modelSupportsVision(activeModelId);
|
||||||
if (props) return props.modalities?.vision ?? false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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 { PropsService } from '$lib/services/props';
|
||||||
import { serverStore } from '$lib/stores/server.svelte';
|
import { ServerModelStatus, ServerRole } from '$lib/enums';
|
||||||
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';
|
||||||
|
|
||||||
|
|
@ -16,11 +16,16 @@ import type { ApiModelDataEntry } from '$lib/types/api';
|
||||||
* - Automatic unloading of unused models
|
* - Automatic unloading of unused models
|
||||||
*
|
*
|
||||||
* **Architecture & Relationships:**
|
* **Architecture & Relationships:**
|
||||||
* - **ModelsService**: Stateless service for API communication
|
* - **ModelsService**: Stateless service for model API communication
|
||||||
|
* - **PropsService**: Stateless service for props/modalities fetching
|
||||||
* - **ModelsStore** (this class): Reactive store for model state
|
* - **ModelsStore** (this class): Reactive store for model state
|
||||||
* - **ServerStore**: Provides server mode detection
|
|
||||||
* - **ConversationsStore**: Tracks which conversations use which models
|
* - **ConversationsStore**: Tracks which conversations use which models
|
||||||
*
|
*
|
||||||
|
* **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.
|
||||||
|
* This store normalizes this behavior so consumers don't need to know the server mode.
|
||||||
|
*
|
||||||
* **Key Features:**
|
* **Key Features:**
|
||||||
* - **MODEL mode**: Single model, always loaded
|
* - **MODEL mode**: Single model, always loaded
|
||||||
* - **ROUTER mode**: Multi-model with load/unload capability
|
* - **ROUTER mode**: Multi-model with load/unload capability
|
||||||
|
|
@ -43,6 +48,20 @@ class ModelsStore {
|
||||||
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());
|
private modelLoadingStates = $state<Map<string, boolean>>(new Map());
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Server role detection - determines API behavior
|
||||||
|
* In ROUTER mode, modalities come from /props?model=<id>
|
||||||
|
* In MODEL mode, modalities come from /props (single model)
|
||||||
|
*/
|
||||||
|
serverRole = $state<ServerRole | null>(null);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Model-specific props cache
|
||||||
|
* Key: modelId, Value: props data including modalities
|
||||||
|
*/
|
||||||
|
private modelPropsCache = $state<Map<string, ApiLlamaCppServerProps>>(new Map());
|
||||||
|
private modelPropsFetching = $state<Set<string>>(new Set());
|
||||||
|
|
||||||
// ─────────────────────────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
// Computed Getters
|
// Computed Getters
|
||||||
// ─────────────────────────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
@ -64,6 +83,69 @@ class ModelsStore {
|
||||||
.map(([id]) => id);
|
.map(([id]) => id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get isRouterMode(): boolean {
|
||||||
|
return this.serverRole === ServerRole.ROUTER;
|
||||||
|
}
|
||||||
|
|
||||||
|
get isModelMode(): boolean {
|
||||||
|
return this.serverRole === ServerRole.MODEL;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
// Methods - Model Modalities
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get modalities for a specific model
|
||||||
|
* Returns cached modalities from model props
|
||||||
|
*/
|
||||||
|
getModelModalities(modelId: string): ModelModalities | null {
|
||||||
|
// First check if modalities are stored in the model option
|
||||||
|
const model = this.models.find((m) => m.model === modelId || m.id === modelId);
|
||||||
|
if (model?.modalities) {
|
||||||
|
return model.modalities;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fall back to props cache
|
||||||
|
const props = this.modelPropsCache.get(modelId);
|
||||||
|
if (props?.modalities) {
|
||||||
|
return {
|
||||||
|
vision: props.modalities.vision ?? false,
|
||||||
|
audio: props.modalities.audio ?? false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get props for a specific model (from cache)
|
||||||
|
*/
|
||||||
|
getModelProps(modelId: string): ApiLlamaCppServerProps | null {
|
||||||
|
return this.modelPropsCache.get(modelId) ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if props are being fetched for a model
|
||||||
|
*/
|
||||||
|
isModelPropsFetching(modelId: string): boolean {
|
||||||
|
return this.modelPropsFetching.has(modelId);
|
||||||
|
}
|
||||||
|
|
||||||
// ─────────────────────────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
// Methods - Model Status
|
// Methods - Model Status
|
||||||
// ─────────────────────────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
@ -96,7 +178,8 @@ class ModelsStore {
|
||||||
// ─────────────────────────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetch list of models from server
|
* Fetch list of models from server and detect server role
|
||||||
|
* Also fetches modalities for MODEL mode (single model)
|
||||||
*/
|
*/
|
||||||
async fetch(force = false): Promise<void> {
|
async fetch(force = false): Promise<void> {
|
||||||
if (this.loading) return;
|
if (this.loading) return;
|
||||||
|
|
@ -106,6 +189,11 @@ class ModelsStore {
|
||||||
this.error = null;
|
this.error = null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// Fetch server props to detect role and get modalities for MODEL mode
|
||||||
|
const serverProps = await PropsService.fetch();
|
||||||
|
this.serverRole =
|
||||||
|
serverProps.role === ServerRole.ROUTER ? ServerRole.ROUTER : ServerRole.MODEL;
|
||||||
|
|
||||||
const response = await ModelsService.list();
|
const response = await ModelsService.list();
|
||||||
|
|
||||||
const models: ModelOption[] = response.data.map((item, index) => {
|
const models: ModelOption[] = response.data.map((item, index) => {
|
||||||
|
|
@ -127,6 +215,22 @@ class ModelsStore {
|
||||||
});
|
});
|
||||||
|
|
||||||
this.models = models;
|
this.models = models;
|
||||||
|
|
||||||
|
// In MODEL mode, populate modalities from /props (single model)
|
||||||
|
// WORKAROUND: In MODEL mode, /props returns modalities for the single model,
|
||||||
|
// but /v1/models doesn't include modalities. We bridge this gap here.
|
||||||
|
if (this.isModelMode && this.models.length > 0 && serverProps.modalities) {
|
||||||
|
const modalities: ModelModalities = {
|
||||||
|
vision: serverProps.modalities.vision ?? false,
|
||||||
|
audio: serverProps.modalities.audio ?? false
|
||||||
|
};
|
||||||
|
// Cache props for the single model
|
||||||
|
this.modelPropsCache.set(this.models[0].model, serverProps);
|
||||||
|
// Update model with modalities
|
||||||
|
this.models = this.models.map((model, index) =>
|
||||||
|
index === 0 ? { ...model, modalities } : model
|
||||||
|
);
|
||||||
|
}
|
||||||
} 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';
|
||||||
|
|
@ -151,16 +255,45 @@ class ModelsStore {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch props for a specific model from /props endpoint
|
||||||
|
* Uses caching to avoid redundant requests
|
||||||
|
*
|
||||||
|
* @param modelId - Model identifier to fetch props for
|
||||||
|
* @returns Props data or null if fetch failed
|
||||||
|
*/
|
||||||
|
async fetchModelProps(modelId: string): Promise<ApiLlamaCppServerProps | null> {
|
||||||
|
// Return cached props if available
|
||||||
|
const cached = this.modelPropsCache.get(modelId);
|
||||||
|
if (cached) return cached;
|
||||||
|
|
||||||
|
// Avoid duplicate fetches
|
||||||
|
if (this.modelPropsFetching.has(modelId)) return null;
|
||||||
|
|
||||||
|
this.modelPropsFetching.add(modelId);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const props = await PropsService.fetchForModel(modelId);
|
||||||
|
this.modelPropsCache.set(modelId, props);
|
||||||
|
return props;
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(`Failed to fetch props for model ${modelId}:`, error);
|
||||||
|
return null;
|
||||||
|
} finally {
|
||||||
|
this.modelPropsFetching.delete(modelId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetch modalities for all loaded models from /props endpoint
|
* Fetch modalities for all loaded models from /props endpoint
|
||||||
* This updates the modalities field in _models array
|
* This updates the modalities field in models array
|
||||||
*/
|
*/
|
||||||
async fetchModalitiesForLoadedModels(): Promise<void> {
|
async fetchModalitiesForLoadedModels(): Promise<void> {
|
||||||
const loadedModelIds = this.loadedModelIds;
|
const loadedModelIds = this.loadedModelIds;
|
||||||
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) => serverStore.fetchModelProps(modelId));
|
const propsPromises = loadedModelIds.map((modelId) => this.fetchModelProps(modelId));
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const results = await Promise.all(propsPromises);
|
const results = await Promise.all(propsPromises);
|
||||||
|
|
@ -191,7 +324,7 @@ class ModelsStore {
|
||||||
*/
|
*/
|
||||||
async updateModelModalities(modelId: string): Promise<void> {
|
async updateModelModalities(modelId: string): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const props = await serverStore.fetchModelProps(modelId);
|
const props = await this.fetchModelProps(modelId);
|
||||||
if (!props?.modalities) return;
|
if (!props?.modalities) return;
|
||||||
|
|
||||||
const modalities: ModelModalities = {
|
const modalities: ModelModalities = {
|
||||||
|
|
@ -448,8 +581,11 @@ class ModelsStore {
|
||||||
this.error = null;
|
this.error = null;
|
||||||
this.selectedModelId = null;
|
this.selectedModelId = null;
|
||||||
this.selectedModelName = null;
|
this.selectedModelName = null;
|
||||||
|
this.serverRole = null;
|
||||||
this.modelUsage.clear();
|
this.modelUsage.clear();
|
||||||
this.modelLoadingStates.clear();
|
this.modelLoadingStates.clear();
|
||||||
|
this.modelPropsCache.clear();
|
||||||
|
this.modelPropsFetching.clear();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -469,6 +605,8 @@ export const selectedModelName = () => modelsStore.selectedModelName;
|
||||||
export const selectedModelOption = () => modelsStore.selectedModel;
|
export const selectedModelOption = () => modelsStore.selectedModel;
|
||||||
export const loadedModelIds = () => modelsStore.loadedModelIds;
|
export const loadedModelIds = () => modelsStore.loadedModelIds;
|
||||||
export const loadingModelIds = () => modelsStore.loadingModelIds;
|
export const loadingModelIds = () => modelsStore.loadingModelIds;
|
||||||
|
export const isRouterMode = () => modelsStore.isRouterMode;
|
||||||
|
export const isModelMode = () => modelsStore.isModelMode;
|
||||||
|
|
||||||
// ─────────────────────────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
// Actions
|
// Actions
|
||||||
|
|
@ -491,3 +629,10 @@ export const clearModelSelection = modelsStore.clearSelection.bind(modelsStore);
|
||||||
export const findModelByName = modelsStore.findModelByName.bind(modelsStore);
|
export const findModelByName = modelsStore.findModelByName.bind(modelsStore);
|
||||||
export const findModelById = modelsStore.findModelById.bind(modelsStore);
|
export const findModelById = modelsStore.findModelById.bind(modelsStore);
|
||||||
export const hasModel = modelsStore.hasModel.bind(modelsStore);
|
export const hasModel = modelsStore.hasModel.bind(modelsStore);
|
||||||
|
|
||||||
|
// Model modalities
|
||||||
|
export const getModelModalities = modelsStore.getModelModalities.bind(modelsStore);
|
||||||
|
export const modelSupportsVision = modelsStore.modelSupportsVision.bind(modelsStore);
|
||||||
|
export const modelSupportsAudio = modelsStore.modelSupportsAudio.bind(modelsStore);
|
||||||
|
export const fetchModelProps = modelsStore.fetchModelProps.bind(modelsStore);
|
||||||
|
export const getModelProps = modelsStore.getModelProps.bind(modelsStore);
|
||||||
|
|
|
||||||
|
|
@ -2,21 +2,26 @@ import { PropsService } from '$lib/services/props';
|
||||||
import { ServerRole, ModelModality } from '$lib/enums';
|
import { ServerRole, ModelModality } from '$lib/enums';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ServerStore - Server state, capabilities, and mode detection
|
* ServerStore - Server connection state, configuration, and role detection
|
||||||
*
|
*
|
||||||
* This store manages the server connection state and properties fetched from `/props`.
|
* This store manages the server connection state and properties fetched from `/props`.
|
||||||
* It provides reactive state for server configuration, capabilities, and role detection.
|
* It provides reactive state for server configuration and role detection.
|
||||||
*
|
*
|
||||||
* **Architecture & Relationships:**
|
* **Architecture & Relationships:**
|
||||||
* - **PropsService**: Stateless service for fetching `/props` data
|
* - **PropsService**: Stateless service for fetching `/props` data
|
||||||
* - **ServerStore** (this class): Reactive store for server state
|
* - **ServerStore** (this class): Reactive store for server state
|
||||||
* - **ModelsStore**: Uses server role for model management strategy
|
* - **ModelsStore**: Independent store for model management (uses PropsService directly)
|
||||||
*
|
*
|
||||||
* **Key Features:**
|
* **Key Features:**
|
||||||
* - **Server State**: Connection status, loading, error handling
|
* - **Server State**: Connection status, loading, error handling
|
||||||
* - **Role Detection**: MODEL (single model) vs ROUTER (multi-model)
|
* - **Role Detection**: MODEL (single model) vs ROUTER (multi-model)
|
||||||
* - **Capability Detection**: Vision and audio modality support
|
* - **Default Params**: Server-wide generation defaults
|
||||||
* - **Props Cache**: Per-model props caching for ROUTER mode
|
*
|
||||||
|
* **Note on Modalities:**
|
||||||
|
* Model-specific modalities (vision, audio) are now managed by ModelsStore.
|
||||||
|
* Use `modelsStore.getModelModalities(modelId)` for per-model modality info.
|
||||||
|
* The `supportsVision`/`supportsAudio` getters here are deprecated and only
|
||||||
|
* apply to MODEL mode (single model).
|
||||||
*/
|
*/
|
||||||
class ServerStore {
|
class ServerStore {
|
||||||
props = $state<ApiLlamaCppServerProps | null>(null);
|
props = $state<ApiLlamaCppServerProps | null>(null);
|
||||||
|
|
@ -25,10 +30,6 @@ class ServerStore {
|
||||||
role = $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)
|
|
||||||
private modelPropsCache = $state<Map<string, ApiLlamaCppServerProps>>(new Map());
|
|
||||||
private modelPropsFetching = $state<Set<string>>(new Set());
|
|
||||||
|
|
||||||
// ─────────────────────────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
// Computed Getters
|
// Computed Getters
|
||||||
// ─────────────────────────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
@ -45,6 +46,10 @@ class ServerStore {
|
||||||
return this.props.model_path.split(/(\\|\/)/).pop() || null;
|
return this.props.model_path.split(/(\\|\/)/).pop() || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated Use modelsStore.getModelModalities(modelId) for per-model modalities.
|
||||||
|
* This only works in MODEL mode (single model).
|
||||||
|
*/
|
||||||
get supportedModalities(): ModelModality[] {
|
get supportedModalities(): ModelModality[] {
|
||||||
const modalities: ModelModality[] = [];
|
const modalities: ModelModality[] = [];
|
||||||
if (this.props?.modalities?.audio) modalities.push(ModelModality.AUDIO);
|
if (this.props?.modalities?.audio) modalities.push(ModelModality.AUDIO);
|
||||||
|
|
@ -52,10 +57,18 @@ class ServerStore {
|
||||||
return modalities;
|
return modalities;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated Use modelsStore.modelSupportsVision(modelId) for per-model check.
|
||||||
|
* This only works in MODEL mode (single model).
|
||||||
|
*/
|
||||||
get supportsVision(): boolean {
|
get supportsVision(): boolean {
|
||||||
return this.props?.modalities?.vision ?? false;
|
return this.props?.modalities?.vision ?? false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated Use modelsStore.modelSupportsAudio(modelId) for per-model check.
|
||||||
|
* This only works in MODEL mode (single model).
|
||||||
|
*/
|
||||||
get supportsAudio(): boolean {
|
get supportsAudio(): boolean {
|
||||||
return this.props?.modalities?.audio ?? false;
|
return this.props?.modalities?.audio ?? false;
|
||||||
}
|
}
|
||||||
|
|
@ -102,18 +115,9 @@ class ServerStore {
|
||||||
this.loading = true;
|
this.loading = true;
|
||||||
this.error = null;
|
this.error = null;
|
||||||
|
|
||||||
const previousBuildInfo = this.props?.build_info;
|
|
||||||
|
|
||||||
const fetchPromise = (async () => {
|
const fetchPromise = (async () => {
|
||||||
try {
|
try {
|
||||||
const props = await PropsService.fetch();
|
const props = await PropsService.fetch();
|
||||||
|
|
||||||
// Clear model-specific props cache if server was restarted
|
|
||||||
if (previousBuildInfo && previousBuildInfo !== props.build_info) {
|
|
||||||
this.modelPropsCache.clear();
|
|
||||||
console.info('Cleared model props cache due to server restart');
|
|
||||||
}
|
|
||||||
|
|
||||||
this.props = props;
|
this.props = props;
|
||||||
this.error = null;
|
this.error = null;
|
||||||
this.detectRole(props);
|
this.detectRole(props);
|
||||||
|
|
@ -130,38 +134,6 @@ class ServerStore {
|
||||||
await fetchPromise;
|
await fetchPromise;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─────────────────────────────────────────────────────────────────────────────
|
|
||||||
// Fetch Model-Specific Properties (ROUTER mode)
|
|
||||||
// ─────────────────────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
getModelProps(modelId: string): ApiLlamaCppServerProps | null {
|
|
||||||
return this.modelPropsCache.get(modelId) ?? null;
|
|
||||||
}
|
|
||||||
|
|
||||||
isModelPropsFetching(modelId: string): boolean {
|
|
||||||
return this.modelPropsFetching.has(modelId);
|
|
||||||
}
|
|
||||||
|
|
||||||
async fetchModelProps(modelId: string): Promise<ApiLlamaCppServerProps | null> {
|
|
||||||
const cached = this.modelPropsCache.get(modelId);
|
|
||||||
if (cached) return cached;
|
|
||||||
|
|
||||||
if (this.modelPropsFetching.has(modelId)) return null;
|
|
||||||
|
|
||||||
this.modelPropsFetching.add(modelId);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const props = await PropsService.fetchForModel(modelId);
|
|
||||||
this.modelPropsCache.set(modelId, props);
|
|
||||||
return props;
|
|
||||||
} catch (error) {
|
|
||||||
console.warn(`Failed to fetch props for model ${modelId}:`, error);
|
|
||||||
return null;
|
|
||||||
} finally {
|
|
||||||
this.modelPropsFetching.delete(modelId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ─────────────────────────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
// Error Handling
|
// Error Handling
|
||||||
// ─────────────────────────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
@ -202,7 +174,6 @@ class ServerStore {
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
this.role = null;
|
this.role = null;
|
||||||
this.fetchPromise = null;
|
this.fetchPromise = null;
|
||||||
this.modelPropsCache.clear();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -228,5 +199,3 @@ export const isModelMode = () => serverStore.isModelMode;
|
||||||
|
|
||||||
// Actions
|
// Actions
|
||||||
export const fetchServerProps = serverStore.fetch.bind(serverStore);
|
export const fetchServerProps = serverStore.fetch.bind(serverStore);
|
||||||
export const fetchModelProps = serverStore.fetchModelProps.bind(serverStore);
|
|
||||||
export const getModelProps = serverStore.getModelProps.bind(serverStore);
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue