From 30f07497da7e50a2b59bbffe370b9ddef3968e09 Mon Sep 17 00:00:00 2001 From: skyfire Date: Fri, 16 Oct 2026 11:12:59 +0800 Subject: [PATCH 1/2] feat(model-settings): add image generation model category and configuration Add image_generation as a supported model category and capability across model settings, default model config, and category inference. This allows users to assign default image generation models and filter models by this capability. Co-Authored-By: Jarvis-AI-42 --- .../settings/pages/ai/ModelSettingsPage.tsx | 3 ++ .../config/components/DefaultModelConfig.tsx | 30 ++++++++++++++++++- .../config/services/modelCategory.test.ts | 13 ++++++++ .../config/services/modelCategory.ts | 26 ++++++++++++++++ .../src/infrastructure/config/types/index.ts | 5 ++++ 5 files changed, 76 insertions(+), 1 deletion(-) diff --git a/src/web-ui/src/app/scenes/settings/pages/ai/ModelSettingsPage.tsx b/src/web-ui/src/app/scenes/settings/pages/ai/ModelSettingsPage.tsx index c997f1412b..8f651c7004 100644 --- a/src/web-ui/src/app/scenes/settings/pages/ai/ModelSettingsPage.tsx +++ b/src/web-ui/src/app/scenes/settings/pages/ai/ModelSettingsPage.tsx @@ -568,6 +568,7 @@ const ModelSettingsPage: React.FC = () => { () => [ { label: t('category.general_chat'), value: 'general_chat' }, { label: t('category.multimodal'), value: 'multimodal' }, + { label: t('category.image_generation'), value: 'image_generation' }, { label: t('category.speech_recognition'), value: 'speech_recognition' }, ], [t] @@ -2438,6 +2439,7 @@ const ModelSettingsPage: React.FC = () => { { modelId: poolDefaults.primary, label: tDefault('core.primary.label') }, { modelId: poolDefaults.fast, label: tDefault('core.fast.label') }, { modelId: poolDefaults.image_understanding, label: tDefault('optional.capabilities.image_understanding.label') }, + { modelId: poolDefaults.image_generation, label: tDefault('optional.capabilities.image_generation.label') }, { modelId: poolDefaults.speech_recognition, label: tDefault('optional.capabilities.speech_recognition.label') }, ], [poolDefaults, tDefault]); @@ -4207,6 +4209,7 @@ const ModelSettingsPage: React.FC = () => { { value: '', label: t('pool.allCapabilities') }, { value: 'text_chat', label: t('capabilities.text_chat') }, { value: 'image_understanding', label: t('capabilities.image_understanding') }, + { value: 'image_generation', label: t('capabilities.image_generation') }, { value: 'speech_recognition', label: t('capabilities.speech_recognition') }, { value: 'function_calling', label: t('capabilities.function_calling') }, ]} /> diff --git a/src/web-ui/src/infrastructure/config/components/DefaultModelConfig.tsx b/src/web-ui/src/infrastructure/config/components/DefaultModelConfig.tsx index f5cd24e5a3..db902bd4ee 100644 --- a/src/web-ui/src/infrastructure/config/components/DefaultModelConfig.tsx +++ b/src/web-ui/src/infrastructure/config/components/DefaultModelConfig.tsx @@ -22,7 +22,7 @@ const log = createLogger('DefaultModelConfig'); const normalizeSelectValue = (value: string | number | (string | number)[]): string | number => Array.isArray(value) ? (value[0] ?? '') : value; -type DefaultModelSlot = 'primary' | 'fast' | 'image_understanding' | 'speech_recognition'; +type DefaultModelSlot = 'primary' | 'fast' | 'image_understanding' | 'image_generation' | 'speech_recognition'; interface DefaultModelConfigProps { compact?: boolean; @@ -82,6 +82,8 @@ export const DefaultModelConfig: React.FC = ({ return t('core.fast.label'); case 'image_understanding': return t('optional.capabilities.image_understanding.label'); + case 'image_generation': + return t('optional.capabilities.image_generation.label'); case 'speech_recognition': return t('optional.capabilities.speech_recognition.label'); default: { @@ -133,6 +135,9 @@ export const DefaultModelConfig: React.FC = ({ const imageUnderstandingModels = models.filter(model => ( isSelectableModelForCapability(model, 'image_understanding') )); + const imageGenerationModels = models.filter(model => ( + isSelectableModelForCapability(model, 'image_generation') + )); const speechRecognitionModels = models.filter(model => ( isSelectableModelForCapability(model, 'speech_recognition') )); @@ -204,6 +209,29 @@ export const DefaultModelConfig: React.FC = ({ /> + + handleDefaultModelChange('image_generation', normalizeSelectValue(value))} + placeholder={pendingPlaceholder ?? t('optional.notSet')} + options={[ + { label: t('optional.notSet'), value: '' }, + ...imageGenerationModels.map(buildModelOption), + ]} + className="default-model-config__model-select" + disabled={controlsDisabled || imageGenerationModels.length === 0} + size="sm" + /> + + { capabilities: [], })).toBe(false); }); + + it('maps image_generation category to the image_generation capability', () => { + const imageGenModel = { + id: 'dall-e-3', + enabled: true, + category: 'image_generation', + capabilities: [], + }; + + expect(getEffectiveModelCapabilities(imageGenModel)).toEqual(['image_generation']); + expect(isSelectableModelForCapability(imageGenModel, 'image_generation')).toBe(true); + expect(isSelectableTextChatModel(imageGenModel)).toBe(false); + }); }); diff --git a/src/web-ui/src/infrastructure/config/services/modelCategory.ts b/src/web-ui/src/infrastructure/config/services/modelCategory.ts index c642724911..535dcf8d50 100644 --- a/src/web-ui/src/infrastructure/config/services/modelCategory.ts +++ b/src/web-ui/src/infrastructure/config/services/modelCategory.ts @@ -25,6 +25,18 @@ const SPEECH_RECOGNITION_MODEL_HINTS = [ 'speech', ]; +const IMAGE_GENERATION_MODEL_HINTS = [ + 'dall-e', + 'dalle', + 'image', + 'flux', + 'sdxl', + 'stable-diffusion', + 'midjourney', + 'imagen', + 'draw', +]; + export function inferModelCategory( modelName: string, _provider?: string @@ -33,6 +45,9 @@ export function inferModelCategory( if (SPEECH_RECOGNITION_MODEL_HINTS.some(hint => normalized.includes(hint))) { return 'speech_recognition'; } + if (IMAGE_GENERATION_MODEL_HINTS.some(hint => normalized.includes(hint))) { + return 'image_generation'; + } if (MULTIMODAL_MODEL_HINTS.some(hint => normalized.includes(hint))) { return 'multimodal'; } @@ -50,6 +65,10 @@ export function resolveModelCategory( return 'multimodal'; } + if (category === 'image_generation') { + return 'image_generation'; + } + if (category === 'speech_recognition') { return 'speech_recognition'; } @@ -62,6 +81,10 @@ export function resolveModelCategory( return 'speech_recognition'; } + if (category === 'general_chat' && inferred === 'image_generation') { + return 'image_generation'; + } + return category ?? inferred; } @@ -69,6 +92,8 @@ export function getCapabilitiesByCategory(category?: ModelCategory | string | nu switch (category) { case 'speech_recognition': return ['speech_recognition']; + case 'image_generation': + return ['image_generation']; case 'multimodal': return ['text_chat', 'image_understanding', 'function_calling']; case 'general_chat': @@ -88,6 +113,7 @@ export function getEffectiveModelCapabilities(model: ModelCapabilitySource): Mod capability === 'text_chat' || capability === 'function_calling' || capability === 'image_understanding' + || capability === 'image_generation' || capability === 'speech_recognition' )) : []; diff --git a/src/web-ui/src/infrastructure/config/types/index.ts b/src/web-ui/src/infrastructure/config/types/index.ts index 98304561e1..91b145fe0f 100644 --- a/src/web-ui/src/infrastructure/config/types/index.ts +++ b/src/web-ui/src/infrastructure/config/types/index.ts @@ -199,11 +199,13 @@ export type ModelCapability = | 'text_chat' | 'function_calling' | 'image_understanding' + | 'image_generation' | 'speech_recognition'; export type ModelCategory = | 'general_chat' | 'multimodal' + | 'image_generation' | 'speech_recognition'; export type ReasoningCatalogBinding = @@ -260,12 +262,14 @@ export interface ModelMetadata { export const CATEGORY_LABELS: Record = { general_chat: t('settings/models:category.general_chat'), multimodal: t('settings/models:category.multimodal'), + image_generation: t('settings/models:category.image_generation'), speech_recognition: t('settings/models:category.speech_recognition') }; export const CATEGORY_ICONS: Record = { general_chat: t('settings/models:categoryIcons.general_chat'), multimodal: t('settings/models:categoryIcons.multimodal'), + image_generation: t('settings/models:categoryIcons.image_generation'), speech_recognition: t('settings/models:categoryIcons.speech_recognition') }; @@ -332,6 +336,7 @@ export interface DefaultModelsConfig { primary?: string | null; fast?: string | null; image_understanding?: string | null; + image_generation?: string | null; speech_recognition?: string | null; } From d8d81bfc13a8edacd55def8fdda718a099cc402c Mon Sep 17 00:00:00 2001 From: skyfire Date: Fri, 16 Oct 2026 12:18:42 +0800 Subject: [PATCH 2/2] CheckPoint #5616 --- .../scenes/settings/pages/ai/ModelSettingsPage.loading.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/web-ui/src/app/scenes/settings/pages/ai/ModelSettingsPage.loading.test.tsx b/src/web-ui/src/app/scenes/settings/pages/ai/ModelSettingsPage.loading.test.tsx index d209cd80c8..4bebd89236 100644 --- a/src/web-ui/src/app/scenes/settings/pages/ai/ModelSettingsPage.loading.test.tsx +++ b/src/web-ui/src/app/scenes/settings/pages/ai/ModelSettingsPage.loading.test.tsx @@ -104,7 +104,7 @@ describe('ModelSettingsPage loading and provider actions', () => { for (const key of ['acquisition.noneConfigured', 'acquisition.noneImported', 'pool.empty']) { expect(container.textContent).not.toContain(copy(key)); } - expect(selectors()).toHaveLength(4); + expect(selectors()).toHaveLength(5); expect(selectors().every(element => element.disabled)).toBe(true); expect(button('actions.addModel').disabled).toBe(true); expect(button('acquisition.import').disabled).toBe(false);