server: introduce API for serving / loading / unloading multiple models (#17470)
* server: add model management and proxy * fix compile error * does this fix windows? * fix windows build * use subprocess.h, better logging * add test * fix windows * feat: Model/Router server architecture WIP * more stable * fix unsafe pointer * also allow terminate loading model * add is_active() * refactor: Architecture improvements * tmp apply upstream fix * address most problems * address thread safety issue * address review comment * add docs (first version) * address review comment * feat: Improved UX for model information, modality interactions etc * chore: update webui build output * refactor: Use only the message data `model` property for displaying model used info * chore: update webui build output * add --models-dir param * feat: New Model Selection UX WIP * chore: update webui build output * feat: Add auto-mic setting * feat: Attachments UX improvements * implement LRU * remove default model path * better --models-dir * add env for args * address review comments * fix compile * refactor: Chat Form Submit component * ad endpoint docs * Merge remote-tracking branch 'webui/allozaur/server_model_management_v1_2' into xsn/server_model_maagement_v1_2 Co-authored-by: Aleksander <aleksander.grygier@gmail.com> * feat: Add copy to clipboard to model name in model info dialog * feat: Model unavailable UI state for model selector * feat: Chat Form Actions UI logic improvements * feat: Auto-select model from last assistant response * chore: update webui build output * expose args and exit_code in API * add note * support extra_args on loading model * allow reusing args if auto_load * typo docs * oai-compat /models endpoint * cleaner * address review comments * feat: Use `model` property for displaying the `repo/model-name` naming format * refactor: Attachments data * chore: update webui build output * refactor: Enum imports * feat: Improve Model Selector responsiveness * chore: update webui build output * refactor: Cleanup * refactor: Cleanup * refactor: Formatters * chore: update webui build output * refactor: Copy To Clipboard Icon component * chore: update webui build output * refactor: Cleanup * chore: update webui build output * refactor: UI badges * chore: update webui build output * refactor: Cleanup * refactor: Cleanup * chore: update webui build output * add --models-allow-extra-args for security * nits * add stdin_file * fix merge * fix: Retrieve lost setting after resolving merge conflict * refactor: DatabaseStore -> DatabaseService * refactor: Database, Conversations & Chat services + stores architecture improvements (WIP) * refactor: Remove redundant settings * refactor: Multi-model business logic WIP * chore: update webui build output * feat: Switching models logic for ChatForm or when regenerating messges + modality detection logic * chore: update webui build output * fix: Add `untrack` inside chat processing info data logic to prevent infinite effect * fix: Regenerate * feat: Remove redundant settigns + rearrange * fix: Audio attachments * refactor: Icons * chore: update webui build output * feat: Model management and selection features WIP * chore: update webui build output * refactor: Improve server properties management * refactor: Icons * chore: update webui build output * feat: Improve model loading/unloading status updates * chore: update webui build output * refactor: Improve API header management via utility functions * remove support for extra args * set hf_repo/docker_repo as model alias when posible * refactor: Remove ConversationsService * refactor: Chat requests abort handling * refactor: Server store * tmp webui build * refactor: Model modality handling * chore: update webui build output * refactor: Processing state reactivity * fix: UI * refactor: Services/Stores syntax + logic improvements Refactors components to access stores directly instead of using exported getter functions. This change centralizes store access and logic, simplifying component code and improving maintainability by reducing the number of exported functions and promoting direct store interaction. Removes exported getter functions from `chat.svelte.ts`, `conversations.svelte.ts`, `models.svelte.ts` and `settings.svelte.ts`. * refactor: Architecture cleanup * feat: Improve statistic badges * feat: Condition available models based on modality + better model loading strategy & UX * docs: Architecture documentation * feat: Update logic for PDF as Image * add TODO for http client * refactor: Enhance model info and attachment handling * chore: update webui build output * refactor: Components naming * chore: update webui build output * refactor: Cleanup * refactor: DRY `getAttachmentDisplayItems` function + fix UI * chore: update webui build output * fix: Modality detection improvement for text-based PDF attachments * refactor: Cleanup * docs: Add info comment * refactor: Cleanup * re * refactor: Cleanup * refactor: Cleanup * feat: Attachment logic & UI improvements * refactor: Constants * feat: Improve UI sidebar background color * chore: update webui build output * refactor: Utils imports + move types to `app.d.ts` * test: Fix Storybook mocks * chore: update webui build output * test: Update Chat Form UI tests * refactor: Tooltip Provider from core layout * refactor: Tests to separate location * decouple server_models from server_routes * test: Move demo test to tests/server * refactor: Remove redundant method * chore: update webui build output * also route anthropic endpoints * fix duplicated arg * fix invalid ptr to shutdown_handler * server : minor * rm unused fn * add ?autoload=true|false query param * refactor: Remove redundant code * docs: Update README documentations + architecture & data flow diagrams * fix: Disable autoload on calling server props for the model * chore: update webui build output * fix ubuntu build * fix: Model status reactivity * fix: Modality detection for MODEL mode * chore: update webui build output --------- Co-authored-by: Aleksander Grygier <aleksander.grygier@gmail.com> Co-authored-by: Georgi Gerganov <ggerganov@gmail.com>
This commit is contained in:
@@ -0,0 +1,118 @@
|
||||
import { modelsStore } from '$lib/stores/models.svelte';
|
||||
import { isRouterMode } from '$lib/stores/server.svelte';
|
||||
import { toast } from 'svelte-sonner';
|
||||
|
||||
interface UseModelChangeValidationOptions {
|
||||
/**
|
||||
* Function to get required modalities for validation.
|
||||
* For ChatForm: () => usedModalities() - all messages
|
||||
* For ChatMessageAssistant: () => getModalitiesUpToMessage(messageId) - messages before
|
||||
*/
|
||||
getRequiredModalities: () => ModelModalities;
|
||||
|
||||
/**
|
||||
* Optional callback to execute after successful validation.
|
||||
* For ChatForm: undefined - just select model
|
||||
* For ChatMessageAssistant: (modelName) => onRegenerate(modelName)
|
||||
*/
|
||||
onSuccess?: (modelName: string) => void;
|
||||
|
||||
/**
|
||||
* Optional callback for rollback on validation failure.
|
||||
* For ChatForm: (previousId) => selectModelById(previousId)
|
||||
* For ChatMessageAssistant: undefined - no rollback needed
|
||||
*/
|
||||
onValidationFailure?: (previousModelId: string | null) => Promise<void>;
|
||||
}
|
||||
|
||||
export function useModelChangeValidation(options: UseModelChangeValidationOptions) {
|
||||
const { getRequiredModalities, onSuccess, onValidationFailure } = options;
|
||||
|
||||
let previousSelectedModelId: string | null = null;
|
||||
const isRouter = $derived(isRouterMode());
|
||||
|
||||
async function handleModelChange(modelId: string, modelName: string): Promise<boolean> {
|
||||
try {
|
||||
// Store previous selection for potential rollback
|
||||
if (onValidationFailure) {
|
||||
previousSelectedModelId = modelsStore.selectedModelId;
|
||||
}
|
||||
|
||||
// Load model if not already loaded (router mode only)
|
||||
let hasLoadedModel = false;
|
||||
const isModelLoadedBefore = modelsStore.isModelLoaded(modelName);
|
||||
|
||||
if (isRouter && !isModelLoadedBefore) {
|
||||
try {
|
||||
await modelsStore.loadModel(modelName);
|
||||
hasLoadedModel = true;
|
||||
} catch {
|
||||
toast.error(`Failed to load model "${modelName}"`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch model props to validate modalities
|
||||
const props = await modelsStore.fetchModelProps(modelName);
|
||||
|
||||
if (props?.modalities) {
|
||||
const requiredModalities = getRequiredModalities();
|
||||
|
||||
// Check if model supports required modalities
|
||||
const missingModalities: string[] = [];
|
||||
if (requiredModalities.vision && !props.modalities.vision) {
|
||||
missingModalities.push('vision');
|
||||
}
|
||||
if (requiredModalities.audio && !props.modalities.audio) {
|
||||
missingModalities.push('audio');
|
||||
}
|
||||
|
||||
if (missingModalities.length > 0) {
|
||||
toast.error(
|
||||
`Model "${modelName}" doesn't support required modalities: ${missingModalities.join(', ')}. Please select a different model.`
|
||||
);
|
||||
|
||||
// Unload the model if we just loaded it
|
||||
if (isRouter && hasLoadedModel) {
|
||||
try {
|
||||
await modelsStore.unloadModel(modelName);
|
||||
} catch (error) {
|
||||
console.error('Failed to unload incompatible model:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Execute rollback callback if provided
|
||||
if (onValidationFailure && previousSelectedModelId) {
|
||||
await onValidationFailure(previousSelectedModelId);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Select the model (validation passed)
|
||||
await modelsStore.selectModelById(modelId);
|
||||
|
||||
// Execute success callback if provided
|
||||
if (onSuccess) {
|
||||
onSuccess(modelName);
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Failed to change model:', error);
|
||||
toast.error('Failed to validate model capabilities');
|
||||
|
||||
// Execute rollback callback on error if provided
|
||||
if (onValidationFailure && previousSelectedModelId) {
|
||||
await onValidationFailure(previousSelectedModelId);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
handleModelChange
|
||||
};
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { slotsService } from '$lib/services';
|
||||
import { activeProcessingState } from '$lib/stores/chat.svelte';
|
||||
import { config } from '$lib/stores/settings.svelte';
|
||||
|
||||
export interface UseProcessingStateReturn {
|
||||
@@ -6,7 +6,7 @@ export interface UseProcessingStateReturn {
|
||||
getProcessingDetails(): string[];
|
||||
getProcessingMessage(): string;
|
||||
shouldShowDetails(): boolean;
|
||||
startMonitoring(): Promise<void>;
|
||||
startMonitoring(): void;
|
||||
stopMonitoring(): void;
|
||||
}
|
||||
|
||||
@@ -14,92 +14,71 @@ export interface UseProcessingStateReturn {
|
||||
* useProcessingState - Reactive processing state hook
|
||||
*
|
||||
* This hook provides reactive access to the processing state of the server.
|
||||
* It subscribes to timing data updates from the slots service and provides
|
||||
* It directly reads from chatStore's reactive state and provides
|
||||
* formatted processing details for UI display.
|
||||
*
|
||||
* **Features:**
|
||||
* - Real-time processing state monitoring
|
||||
* - Real-time processing state via direct reactive state binding
|
||||
* - Context and output token tracking
|
||||
* - Tokens per second calculation
|
||||
* - Graceful degradation when slots endpoint unavailable
|
||||
* - Automatic cleanup on component unmount
|
||||
* - Automatic updates when streaming data arrives
|
||||
* - Supports multiple concurrent conversations
|
||||
*
|
||||
* @returns Hook interface with processing state and control methods
|
||||
*/
|
||||
export function useProcessingState(): UseProcessingStateReturn {
|
||||
let isMonitoring = $state(false);
|
||||
let processingState = $state<ApiProcessingState | null>(null);
|
||||
let lastKnownState = $state<ApiProcessingState | null>(null);
|
||||
let unsubscribe: (() => void) | null = null;
|
||||
|
||||
async function startMonitoring(): Promise<void> {
|
||||
if (isMonitoring) return;
|
||||
|
||||
isMonitoring = true;
|
||||
|
||||
unsubscribe = slotsService.subscribe((state) => {
|
||||
processingState = state;
|
||||
if (state) {
|
||||
lastKnownState = state;
|
||||
} else {
|
||||
lastKnownState = null;
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
const currentState = await slotsService.getCurrentState();
|
||||
|
||||
if (currentState) {
|
||||
processingState = currentState;
|
||||
lastKnownState = currentState;
|
||||
}
|
||||
|
||||
if (slotsService.isStreaming()) {
|
||||
slotsService.startStreaming();
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Failed to start slots monitoring:', error);
|
||||
// Continue without slots monitoring - graceful degradation
|
||||
// Derive processing state reactively from chatStore's direct state
|
||||
const processingState = $derived.by(() => {
|
||||
if (!isMonitoring) {
|
||||
return lastKnownState;
|
||||
}
|
||||
// Read directly from the reactive state export
|
||||
return activeProcessingState();
|
||||
});
|
||||
|
||||
// Track last known state for keepStatsVisible functionality
|
||||
$effect(() => {
|
||||
if (processingState && isMonitoring) {
|
||||
lastKnownState = processingState;
|
||||
}
|
||||
});
|
||||
|
||||
function startMonitoring(): void {
|
||||
if (isMonitoring) return;
|
||||
isMonitoring = true;
|
||||
}
|
||||
|
||||
function stopMonitoring(): void {
|
||||
if (!isMonitoring) return;
|
||||
|
||||
isMonitoring = false;
|
||||
|
||||
// Only clear processing state if keepStatsVisible is disabled
|
||||
// This preserves the last known state for display when stats should remain visible
|
||||
// Only clear last known state if keepStatsVisible is disabled
|
||||
const currentConfig = config();
|
||||
if (!currentConfig.keepStatsVisible) {
|
||||
processingState = null;
|
||||
} else if (lastKnownState) {
|
||||
// Keep the last known state visible when keepStatsVisible is enabled
|
||||
processingState = lastKnownState;
|
||||
}
|
||||
|
||||
if (unsubscribe) {
|
||||
unsubscribe();
|
||||
unsubscribe = null;
|
||||
lastKnownState = null;
|
||||
}
|
||||
}
|
||||
|
||||
function getProcessingMessage(): string {
|
||||
if (!processingState) {
|
||||
const state = processingState;
|
||||
if (!state) {
|
||||
return 'Processing...';
|
||||
}
|
||||
|
||||
switch (processingState.status) {
|
||||
switch (state.status) {
|
||||
case 'initializing':
|
||||
return 'Initializing...';
|
||||
case 'preparing':
|
||||
if (processingState.progressPercent !== undefined) {
|
||||
return `Processing (${processingState.progressPercent}%)`;
|
||||
if (state.progressPercent !== undefined) {
|
||||
return `Processing (${state.progressPercent}%)`;
|
||||
}
|
||||
return 'Preparing response...';
|
||||
case 'generating':
|
||||
if (processingState.tokensDecoded > 0) {
|
||||
return `Generating... (${processingState.tokensDecoded} tokens)`;
|
||||
if (state.tokensDecoded > 0) {
|
||||
return `Generating... (${state.tokensDecoded} tokens)`;
|
||||
}
|
||||
return 'Generating...';
|
||||
default:
|
||||
@@ -115,7 +94,6 @@ export function useProcessingState(): UseProcessingStateReturn {
|
||||
}
|
||||
|
||||
const details: string[] = [];
|
||||
const currentConfig = config(); // Get fresh config each time
|
||||
|
||||
// Always show context info when we have valid data
|
||||
if (stateToUse.contextUsed >= 0 && stateToUse.contextTotal > 0) {
|
||||
@@ -141,11 +119,7 @@ export function useProcessingState(): UseProcessingStateReturn {
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
currentConfig.showTokensPerSecond &&
|
||||
stateToUse.tokensPerSecond &&
|
||||
stateToUse.tokensPerSecond > 0
|
||||
) {
|
||||
if (stateToUse.tokensPerSecond && stateToUse.tokensPerSecond > 0) {
|
||||
details.push(`${stateToUse.tokensPerSecond.toFixed(1)} tokens/sec`);
|
||||
}
|
||||
|
||||
@@ -157,7 +131,8 @@ export function useProcessingState(): UseProcessingStateReturn {
|
||||
}
|
||||
|
||||
function shouldShowDetails(): boolean {
|
||||
return processingState !== null && processingState.status !== 'idle';
|
||||
const state = processingState;
|
||||
return state !== null && state.status !== 'idle';
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
Reference in New Issue
Block a user