onSelect(option.id)}
@@ -79,9 +85,12 @@
>
diff --git a/tools/ui/src/lib/components/app/models/utils.ts b/tools/ui/src/lib/components/app/models/utils.ts
index b78e7085b7..35bfb58a25 100644
--- a/tools/ui/src/lib/components/app/models/utils.ts
+++ b/tools/ui/src/lib/components/app/models/utils.ts
@@ -1,3 +1,4 @@
+import { ModelModality } from '$lib/enums';
import type { ModelOption } from '$lib/types/models';
import { SvelteMap } from 'svelte/reactivity';
@@ -17,6 +18,23 @@ export interface GroupedModelOptions {
available: OrgGroup[];
}
+function matchesModality(option: ModelOption, term: string): boolean {
+ const modalities = option.modalities;
+
+ if (!modalities) return false;
+
+ switch (term) {
+ case ModelModality.VISION.toLowerCase():
+ return modalities.vision;
+ case ModelModality.AUDIO.toLowerCase():
+ return modalities.audio;
+ case ModelModality.VIDEO.toLowerCase():
+ return modalities.video;
+ default:
+ return false;
+ }
+}
+
export function filterModelOptions(options: ModelOption[], searchTerm: string): ModelOption[] {
const term = searchTerm.trim().toLowerCase();
@@ -27,7 +45,8 @@ export function filterModelOptions(options: ModelOption[], searchTerm: string):
option.model.toLowerCase().includes(term) ||
option.name?.toLowerCase().includes(term) ||
option.aliases?.some((alias: string) => alias.toLowerCase().includes(term)) ||
- option.tags?.some((tag: string) => tag.toLowerCase().includes(term))
+ option.tags?.some((tag: string) => tag.toLowerCase().includes(term)) ||
+ matchesModality(option, term)
);
}
diff --git a/tools/ui/src/lib/constants/icons.constants.ts b/tools/ui/src/lib/constants/icons.constants.ts
index 5563740509..a0ce22f95e 100644
--- a/tools/ui/src/lib/constants/icons.constants.ts
+++ b/tools/ui/src/lib/constants/icons.constants.ts
@@ -8,10 +8,13 @@ import {
File as FileIcon,
FileText as FileTextIcon,
Image as ImageIcon,
+ Lightbulb as ReasoningIcon,
Mic as AudioIcon,
Video as VideoIcon
} from '@lucide/svelte';
-import { FileTypeCategory, ModelModality } from '$lib/enums';
+import { FileTypeCategory, ModelCapability, ModelModality } from '$lib/enums';
+import type { ModelCapabilities, ModelModalities } from '$lib/types/models';
+import type { Component } from 'svelte';
export const FILE_TYPE_ICONS = {
[FileTypeCategory.AUDIO]: AudioIcon,
@@ -35,6 +38,29 @@ export const MODALITY_LABELS = {
[ModelModality.VISION]: 'Vision'
} as const;
+/** Maps an input ModelModality to the boolean flag it drives on the ModelModalities type */
+export const MODALITY_FLAG_KEYS: Record<
+ Exclude
,
+ keyof ModelModalities
+> = {
+ [ModelModality.AUDIO]: 'audio',
+ [ModelModality.VIDEO]: 'video',
+ [ModelModality.VISION]: 'vision'
+};
+
+export const CAPABILITY_ICONS: Record = {
+ [ModelCapability.REASONING]: ReasoningIcon
+} as const;
+
+export const CAPABILITY_LABELS: Record = {
+ [ModelCapability.REASONING]: 'Reasoning'
+} as const;
+
+/** Maps a ModelCapability to the boolean flag it drives on the ModelCapabilities type */
+export const CAPABILITY_FLAG_KEYS: Record = {
+ [ModelCapability.REASONING]: 'reasoning'
+};
+
// Shared SVG icon strings for copy and preview buttons
export const COPY_ICON_SVG = ``;
diff --git a/tools/ui/src/lib/constants/settings-keys.constants.ts b/tools/ui/src/lib/constants/settings-keys.constants.ts
index c8761c7587..2c923cee38 100644
--- a/tools/ui/src/lib/constants/settings-keys.constants.ts
+++ b/tools/ui/src/lib/constants/settings-keys.constants.ts
@@ -54,6 +54,7 @@ export const SETTINGS_KEYS = {
SHOW_FULL_PATH_IN_MENTIONS: 'showFullPathInMentions',
// Display
SHOW_MESSAGE_STATS: 'showMessageStats',
+ SHOW_MODEL_ORG_NAME_IN_TRIGGER: 'showModelOrgNameInTrigger',
SHOW_MODEL_QUANTIZATION: 'showModelQuantization',
SHOW_MODEL_TAGS: 'showModelTags',
SHOW_RAW_MODEL_NAMES: 'showRawModelNames',
diff --git a/tools/ui/src/lib/constants/settings.constants.ts b/tools/ui/src/lib/constants/settings.constants.ts
index b4699af197..7140bbcc4a 100644
--- a/tools/ui/src/lib/constants/settings.constants.ts
+++ b/tools/ui/src/lib/constants/settings.constants.ts
@@ -111,9 +111,8 @@ export const SETTINGS_REGISTRY: SettingsSectionEntry[] = [
type: SettingsFieldType.CHECKBOX
},
{
- defaultValue: false,
+ defaultValue: true,
help: 'Automatically show microphone button instead of send button when textarea is empty for models with audio modality support.',
- isExperimental: true,
key: SETTINGS_KEYS.AUTO_MIC_ON_EMPTY,
label: 'Show microphone on empty input',
type: SettingsFieldType.CHECKBOX
@@ -283,6 +282,13 @@ export const SETTINGS_REGISTRY: SettingsSectionEntry[] = [
label: 'Show model tags',
type: SettingsFieldType.CHECKBOX
},
+ {
+ defaultValue: false,
+ help: 'Display the organization name in the model selector trigger button.',
+ key: SETTINGS_KEYS.SHOW_MODEL_ORG_NAME_IN_TRIGGER,
+ label: 'Show organization name in model selector trigger',
+ type: SettingsFieldType.CHECKBOX
+ },
{
defaultValue: false,
help: 'Display the current build version in the bottom-right corner of the interface.',
diff --git a/tools/ui/src/lib/enums/index.ts b/tools/ui/src/lib/enums/index.ts
index efe5789e20..e0b15bc729 100644
--- a/tools/ui/src/lib/enums/index.ts
+++ b/tools/ui/src/lib/enums/index.ts
@@ -67,7 +67,7 @@ export {
JsonSchemaType
} from './mcp.enums';
-export { ModelModality } from './model.enums';
+export { ModelCapability, ModelModality } from './model.enums';
export { ServerRole, ServerModelStatus, ServerModelsSseEventType } from './server.enums';
diff --git a/tools/ui/src/lib/enums/model.enums.ts b/tools/ui/src/lib/enums/model.enums.ts
index df85c9d896..db085a27a7 100644
--- a/tools/ui/src/lib/enums/model.enums.ts
+++ b/tools/ui/src/lib/enums/model.enums.ts
@@ -4,3 +4,7 @@ export enum ModelModality {
VIDEO = 'VIDEO',
VISION = 'VISION'
}
+
+export enum ModelCapability {
+ REASONING = 'REASONING'
+}
diff --git a/tools/ui/src/lib/hooks/use-reasoning-menu.svelte.ts b/tools/ui/src/lib/hooks/use-reasoning-menu.svelte.ts
index 2cb9c90609..3c52d7ab27 100644
--- a/tools/ui/src/lib/hooks/use-reasoning-menu.svelte.ts
+++ b/tools/ui/src/lib/hooks/use-reasoning-menu.svelte.ts
@@ -8,6 +8,7 @@ import { getConversationModel } from '$lib/utils';
export interface UseReasoningMenuReturn {
readonly modelSupportsThinking: boolean;
readonly thinkingEnabled: boolean;
+ readonly isReasoningActive: boolean;
readonly isOff: boolean;
readonly currentEffort: ReasoningEffort;
readonly levels: ReasoningEffortLevel[];
@@ -59,6 +60,12 @@ export function useReasoningMenu(): UseReasoningMenuReturn {
const thinkingEnabled = $derived(
currentEffort !== ReasoningEffort.OFF && currentEffort !== ReasoningEffort.DEFAULT
);
+ // Thinking is effectively on (lightbulb lit) either when an explicit effort
+ // is selected, or when the effort is left at "Default" and the model
+ // supports thinking.
+ const isReasoningActive = $derived(
+ thinkingEnabled || (currentEffort === ReasoningEffort.DEFAULT && modelSupportsThinking)
+ );
return {
get currentEffort() {
@@ -67,6 +74,9 @@ export function useReasoningMenu(): UseReasoningMenuReturn {
get isOff() {
return currentEffort === ReasoningEffort.OFF;
},
+ get isReasoningActive() {
+ return isReasoningActive;
+ },
isSelected(level: ReasoningEffortLevel): boolean {
return currentEffort === level.value;
},
diff --git a/tools/ui/src/lib/services/models.service.ts b/tools/ui/src/lib/services/models.service.ts
index bb1bbd356a..b0fcca0eec 100644
--- a/tools/ui/src/lib/services/models.service.ts
+++ b/tools/ui/src/lib/services/models.service.ts
@@ -187,8 +187,17 @@ export class ModelsService {
// 6. Model name = segments before params; tags = remaining segments after params
const pivotIdx = paramsIdx !== MODEL_ID.NOT_FOUND ? paramsIdx : segments.length;
+ const modelSegments = segments.slice(0, pivotIdx);
- result.modelName = segments.slice(0, pivotIdx).join(MODEL_ID.SEGMENT_SEPARATOR) || null;
+ // strip trailing container-format segments (e.g. GGUF) from the model name
+ while (
+ modelSegments.length > 0 &&
+ MODEL_ID.IGNORED_SEGMENTS.has(modelSegments[modelSegments.length - 1].toUpperCase())
+ ) {
+ modelSegments.pop();
+ }
+
+ result.modelName = modelSegments.join(MODEL_ID.SEGMENT_SEPARATOR) || null;
if (paramsIdx !== MODEL_ID.NOT_FOUND) {
result.tags = segments.slice(paramsIdx + 1).filter((_, relIdx) => {
diff --git a/tools/ui/src/lib/types/index.ts b/tools/ui/src/lib/types/index.ts
index 62947cd493..d91c2811a4 100644
--- a/tools/ui/src/lib/types/index.ts
+++ b/tools/ui/src/lib/types/index.ts
@@ -89,6 +89,7 @@ export type {
// Model types
export type {
+ ModelCapabilities,
ModelModalities,
ModelOption,
ModelLoadProgress,
diff --git a/tools/ui/src/lib/types/models.d.ts b/tools/ui/src/lib/types/models.d.ts
index b32c16f6f2..0b7523113d 100644
--- a/tools/ui/src/lib/types/models.d.ts
+++ b/tools/ui/src/lib/types/models.d.ts
@@ -6,6 +6,10 @@ export interface ModelModalities {
video: boolean;
}
+export interface ModelCapabilities {
+ reasoning: boolean;
+}
+
export interface ModelOption {
id: string;
name: string;
diff --git a/tools/ui/tests/unit/model-id-parser.test.ts b/tools/ui/tests/unit/model-id-parser.test.ts
index 3dac1dfaba..5903f196ab 100644
--- a/tools/ui/tests/unit/model-id-parser.test.ts
+++ b/tools/ui/tests/unit/model-id-parser.test.ts
@@ -97,6 +97,38 @@ describe('parseModelId', () => {
});
});
+ it('strips trailing container format segments from model names', () => {
+ expect(parseModelId('unsloth/DeepSeek-V4-Flash-0731-GGUF:Q2_K_XL')).toStrictEqual({
+ activatedParams: null,
+ modelName: 'DeepSeek-V4-Flash-0731',
+ orgName: 'unsloth',
+ params: null,
+ quantization: 'Q2_K_XL',
+ raw: 'unsloth/DeepSeek-V4-Flash-0731-GGUF:Q2_K_XL',
+ tags: []
+ });
+
+ expect(parseModelId('unsloth/Laguna-S-2.1-GGUF:Q4_K_XL')).toStrictEqual({
+ activatedParams: null,
+ modelName: 'Laguna-S-2.1',
+ orgName: 'unsloth',
+ params: null,
+ quantization: 'Q4_K_XL',
+ raw: 'unsloth/Laguna-S-2.1-GGUF:Q4_K_XL',
+ tags: []
+ });
+
+ expect(parseModelId('org/Model-Name-GGUF')).toStrictEqual({
+ activatedParams: null,
+ modelName: 'Model-Name',
+ orgName: 'org',
+ params: null,
+ quantization: null,
+ raw: 'org/Model-Name-GGUF',
+ tags: []
+ });
+ });
+
it('handles real-world examples correctly', () => {
expect(parseModelId('meta-llama/Llama-3.1-8B')).toStrictEqual({
activatedParams: null,