-
+
{#if isLoading}
@@ -125,7 +198,7 @@
{:else if shouldShowRecordButton}
-
+
{:else}
void;
}
@@ -11,6 +13,8 @@
let {
accept = $bindable(),
class: className = '',
+ hasAudioModality = false,
+ hasVisionModality = false,
multiple = true,
onFileSelect
}: Props = $props();
@@ -18,7 +22,13 @@
let fileInputElement: HTMLInputElement | undefined;
// Use modality-aware accept string by default, but allow override
- let finalAccept = $derived(accept ?? generateModalityAwareAcceptString());
+ let finalAccept = $derived(
+ accept ??
+ generateModalityAwareAcceptString({
+ hasVision: hasVisionModality,
+ hasAudio: hasAudioModality
+ })
+ );
export function click() {
fileInputElement?.click();
diff --git a/tools/server/webui/src/lib/components/app/chat/ChatMessages/ChatMessage.svelte b/tools/server/webui/src/lib/components/app/chat/ChatMessages/ChatMessage.svelte
index ae0dc2ed9f..5656e08334 100644
--- a/tools/server/webui/src/lib/components/app/chat/ChatMessages/ChatMessage.svelte
+++ b/tools/server/webui/src/lib/components/app/chat/ChatMessages/ChatMessage.svelte
@@ -20,7 +20,7 @@
) => void;
onEditUserMessagePreserveResponses?: (message: DatabaseMessage, newContent: string) => void;
onNavigateToSibling?: (siblingId: string) => void;
- onRegenerateWithBranching?: (message: DatabaseMessage) => void;
+ onRegenerateWithBranching?: (message: DatabaseMessage, modelOverride?: string) => void;
siblingInfo?: ChatMessageSiblingInfo | null;
}
@@ -133,8 +133,8 @@
}
}
- function handleRegenerate() {
- onRegenerateWithBranching?.(message);
+ function handleRegenerate(modelOverride?: string) {
+ onRegenerateWithBranching?.(message, modelOverride);
}
function handleContinue() {
diff --git a/tools/server/webui/src/lib/components/app/chat/ChatMessages/ChatMessageAssistant.svelte b/tools/server/webui/src/lib/components/app/chat/ChatMessages/ChatMessageAssistant.svelte
index 7034c17a3e..68ebde42b8 100644
--- a/tools/server/webui/src/lib/components/app/chat/ChatMessages/ChatMessageAssistant.svelte
+++ b/tools/server/webui/src/lib/components/app/chat/ChatMessages/ChatMessageAssistant.svelte
@@ -44,7 +44,7 @@
onEditKeydown?: (event: KeyboardEvent) => void;
onEditedContentChange?: (content: string) => void;
onNavigateToSibling?: (siblingId: string) => void;
- onRegenerate: () => void;
+ onRegenerate: (modelOverride?: string) => void;
onSaveEdit?: () => void;
onShowDeleteDialogChange: (show: boolean) => void;
onShouldBranchAfterEditChange?: (value: boolean) => void;
@@ -101,11 +101,12 @@
return null;
});
- async function handleModelChange(modelId: string) {
+ async function handleModelChange(modelId: string, modelName: string) {
try {
await selectModel(modelId);
- onRegenerate();
+ // Pass the selected model name for regeneration
+ onRegenerate(modelName);
} catch (error) {
console.error('Failed to change model:', error);
}
diff --git a/tools/server/webui/src/lib/components/app/chat/ChatMessages/ChatMessages.svelte b/tools/server/webui/src/lib/components/app/chat/ChatMessages/ChatMessages.svelte
index 020873dc6e..b13d5100ba 100644
--- a/tools/server/webui/src/lib/components/app/chat/ChatMessages/ChatMessages.svelte
+++ b/tools/server/webui/src/lib/components/app/chat/ChatMessages/ChatMessages.svelte
@@ -87,10 +87,10 @@
refreshAllMessages();
}
- async function handleRegenerateWithBranching(message: DatabaseMessage) {
+ async function handleRegenerateWithBranching(message: DatabaseMessage, modelOverride?: string) {
onUserAction?.();
- await regenerateMessageWithBranching(message.id);
+ await regenerateMessageWithBranching(message.id, modelOverride);
refreshAllMessages();
}
diff --git a/tools/server/webui/src/lib/components/app/chat/ChatScreen/ChatScreen.svelte b/tools/server/webui/src/lib/components/app/chat/ChatScreen/ChatScreen.svelte
index d8952ba07e..7c8d3f042f 100644
--- a/tools/server/webui/src/lib/components/app/chat/ChatScreen/ChatScreen.svelte
+++ b/tools/server/webui/src/lib/components/app/chat/ChatScreen/ChatScreen.svelte
@@ -36,8 +36,13 @@
supportsAudio,
propsLoading,
serverWarning,
- propsStore
+ propsStore,
+ isRouterMode,
+ fetchModelProps,
+ getModelProps
} from '$lib/stores/props.svelte';
+ import { modelOptions, selectedModelId } from '$lib/stores/models.svelte';
+ import { getConversationModel } from '$lib/stores/chat.svelte';
import { parseFilesToMessageExtras } from '$lib/utils/convert-files-to-extra';
import { isFileTypeSupported } from '$lib/utils/file-type';
import { filterFilesByModalities } from '$lib/utils/modality-file-validation';
@@ -89,6 +94,72 @@
let isCurrentConversationLoading = $derived(isLoading());
+ // Model-specific capability detection (same logic as ChatFormActions)
+ let isRouter = $derived(isRouterMode());
+ let conversationModel = $derived(getConversationModel(activeMessages() as DatabaseMessage[]));
+
+ // Get active model ID for fetching props
+ let activeModelId = $derived.by(() => {
+ if (!isRouter) return null;
+
+ const options = modelOptions();
+
+ // First try user-selected model
+ const selectedId = selectedModelId();
+ if (selectedId) {
+ const model = options.find((m) => m.id === selectedId);
+ if (model) return model.model;
+ }
+
+ // Fallback to conversation model
+ if (conversationModel) {
+ const model = options.find((m) => m.model === conversationModel);
+ if (model) return model.model;
+ }
+
+ return null;
+ });
+
+ // State for model props reactivity
+ let modelPropsVersion = $state(0);
+
+ // Fetch model props when active model changes
+ $effect(() => {
+ if (isRouter && activeModelId) {
+ const cached = getModelProps(activeModelId);
+ if (!cached) {
+ fetchModelProps(activeModelId).then(() => {
+ modelPropsVersion++;
+ });
+ }
+ }
+ });
+
+ // Derive modalities from model props (ROUTER) or server props (MODEL)
+ let hasAudioModality = $derived.by(() => {
+ if (!isRouter) return supportsAudio();
+
+ if (activeModelId) {
+ void modelPropsVersion;
+ const props = getModelProps(activeModelId);
+ if (props) return props.modalities?.audio ?? false;
+ }
+
+ return false;
+ });
+
+ let hasVisionModality = $derived.by(() => {
+ if (!isRouter) return supportsVision();
+
+ if (activeModelId) {
+ void modelPropsVersion;
+ const props = getModelProps(activeModelId);
+ if (props) return props.modalities?.vision ?? false;
+ }
+
+ return false;
+ });
+
async function handleDeleteConfirm() {
const conversation = activeConversation();
if (conversation) {
@@ -220,16 +291,20 @@
}
}
- const { supportedFiles, unsupportedFiles, modalityReasons } =
- filterFilesByModalities(generallySupported);
+ // Use model-specific capabilities for file validation
+ const capabilities = { hasVision: hasVisionModality, hasAudio: hasAudioModality };
+ const { supportedFiles, unsupportedFiles, modalityReasons } = filterFilesByModalities(
+ generallySupported,
+ capabilities
+ );
const allUnsupportedFiles = [...generallyUnsupported, ...unsupportedFiles];
if (allUnsupportedFiles.length > 0) {
const supportedTypes: string[] = ['text files', 'PDFs'];
- if (supportsVision()) supportedTypes.push('images');
- if (supportsAudio()) supportedTypes.push('audio files');
+ if (hasVisionModality) supportedTypes.push('images');
+ if (hasAudioModality) supportedTypes.push('audio files');
fileErrorData = {
generallyUnsupported,
diff --git a/tools/server/webui/src/lib/components/app/misc/SelectorModel.svelte b/tools/server/webui/src/lib/components/app/misc/SelectorModel.svelte
index fd1e6b5694..fc65f1fee5 100644
--- a/tools/server/webui/src/lib/components/app/misc/SelectorModel.svelte
+++ b/tools/server/webui/src/lib/components/app/misc/SelectorModel.svelte
@@ -21,6 +21,8 @@
onModelChange?: (modelId: string, modelName: string) => void;
disabled?: boolean;
forceForegroundText?: boolean;
+ /** When true, user's global selection takes priority over currentModel (for form selector) */
+ useGlobalSelection?: boolean;
}
let {
@@ -28,7 +30,8 @@
currentModel = null,
onModelChange,
disabled = false,
- forceForegroundText = false
+ forceForegroundText = false,
+ useGlobalSelection = false
}: Props = $props();
let options = $derived(modelOptions());
@@ -260,6 +263,14 @@
return undefined;
}
+ // When useGlobalSelection is true (form selector), prioritize user selection
+ // Otherwise (message display), prioritize currentModel
+ if (useGlobalSelection && activeId) {
+ const selected = options.find((option) => option.id === activeId);
+ if (selected) return selected;
+ }
+
+ // Show currentModel (from message payload or conversation)
if (currentModel) {
if (!isCurrentModelInCache()) {
return {
@@ -273,7 +284,7 @@
return options.find((option) => option.model === currentModel);
}
- // Check if user has selected a model (for new chats before first message)
+ // Fallback to user selection (for new chats before first message)
if (activeId) {
return options.find((option) => option.id === activeId);
}
diff --git a/tools/server/webui/src/lib/services/chat.ts b/tools/server/webui/src/lib/services/chat.ts
index 1474f9b692..c781e3badc 100644
--- a/tools/server/webui/src/lib/services/chat.ts
+++ b/tools/server/webui/src/lib/services/chat.ts
@@ -150,7 +150,7 @@ export class ChatService {
};
const isRouter = isRouterMode();
- const activeModel = isRouter ? selectedModelName() : null;
+ const activeModel = isRouter ? options.model || selectedModelName() : null;
if (isRouter && activeModel) {
requestBody.model = activeModel;
diff --git a/tools/server/webui/src/lib/services/props.ts b/tools/server/webui/src/lib/services/props.ts
index f133619fca..bc0dd7a965 100644
--- a/tools/server/webui/src/lib/services/props.ts
+++ b/tools/server/webui/src/lib/services/props.ts
@@ -40,4 +40,34 @@ export class PropsService {
const data = await response.json();
return data as ApiLlamaCppServerProps;
}
+
+ /**
+ * Fetches server properties for a specific model (ROUTER mode)
+ *
+ * @param modelId - The model ID to fetch properties for
+ * @returns {Promise} Server properties for the model
+ * @throws {Error} If the request fails or returns invalid data
+ */
+ static async fetchForModel(modelId: string): Promise {
+ const currentConfig = config();
+ const apiKey = currentConfig.apiKey?.toString().trim();
+
+ const url = new URL('./props', window.location.href);
+ url.searchParams.set('model', modelId);
+
+ const response = await fetch(url.toString(), {
+ headers: {
+ ...(apiKey ? { Authorization: `Bearer ${apiKey}` } : {})
+ }
+ });
+
+ if (!response.ok) {
+ throw new Error(
+ `Failed to fetch model properties: ${response.status} ${response.statusText}`
+ );
+ }
+
+ const data = await response.json();
+ return data as ApiLlamaCppServerProps;
+ }
}
diff --git a/tools/server/webui/src/lib/stores/chat.svelte.ts b/tools/server/webui/src/lib/stores/chat.svelte.ts
index b12c5d8d20..d6d4cc4ba9 100644
--- a/tools/server/webui/src/lib/stores/chat.svelte.ts
+++ b/tools/server/webui/src/lib/stores/chat.svelte.ts
@@ -489,7 +489,8 @@ class ChatStore {
allMessages: DatabaseMessage[],
assistantMessage: DatabaseMessage,
onComplete?: (content: string) => Promise,
- onError?: (error: Error) => void
+ onError?: (error: Error) => void,
+ modelOverride?: string | null
): Promise {
let streamedContent = '';
let streamedReasoningContent = '';
@@ -520,6 +521,7 @@ class ChatStore {
allMessages,
{
...this.getApiOptions(),
+ ...(modelOverride ? { model: modelOverride } : {}),
onChunk: (chunk: string) => {
streamedContent += chunk;
this.setChatStreaming(assistantMessage.convId, streamedContent, assistantMessage.id);
@@ -996,7 +998,7 @@ class ChatStore {
}
}
- async regenerateMessageWithBranching(messageId: string): Promise {
+ async regenerateMessageWithBranching(messageId: string, modelOverride?: string): Promise {
const activeConv = conversationsStore.activeConversation;
if (!activeConv || this.isLoading) return;
try {
@@ -1035,7 +1037,16 @@ class ChatStore {
parentMessage.id,
false
) as DatabaseMessage[];
- await this.streamChatCompletion(conversationPath, newAssistantMessage);
+ // Use modelOverride if provided, otherwise use the original message's model
+ // If neither is available, don't pass model (will use global selection)
+ const modelToUse = modelOverride || msg.model || undefined;
+ await this.streamChatCompletion(
+ conversationPath,
+ newAssistantMessage,
+ undefined,
+ undefined,
+ modelToUse
+ );
} catch (error) {
if (!this.isAbortError(error))
console.error('Failed to regenerate message with branching:', error);
diff --git a/tools/server/webui/src/lib/stores/props.svelte.ts b/tools/server/webui/src/lib/stores/props.svelte.ts
index a996bdfed9..e68b422d4b 100644
--- a/tools/server/webui/src/lib/stores/props.svelte.ts
+++ b/tools/server/webui/src/lib/stores/props.svelte.ts
@@ -39,6 +39,10 @@ class PropsStore {
private _serverMode = $state(null);
private fetchPromise: Promise | null = null;
+ // Model-specific props cache (ROUTER mode)
+ private _modelPropsCache = $state