From f6c752a785a2a042b972f4457838fb0456fac1bc Mon Sep 17 00:00:00 2001
From: psychedelicious <4822129+psychedelicious@users.noreply.github.com>
Date: Thu, 16 Oct 2025 10:03:21 +1100
Subject: [PATCH 1/4] fix(mm): ignore files in hidden directories when
identifying models
---
invokeai/backend/model_manager/configs/factory.py | 6 +++++-
1 file changed, 5 insertions(+), 1 deletion(-)
diff --git a/invokeai/backend/model_manager/configs/factory.py b/invokeai/backend/model_manager/configs/factory.py
index 6b8d122d615..e596c960953 100644
--- a/invokeai/backend/model_manager/configs/factory.py
+++ b/invokeai/backend/model_manager/configs/factory.py
@@ -333,7 +333,11 @@ def _validate_path_looks_like_model(path: Path) -> None:
# For directories, do a quick file count check with early exit
total_files = 0
# Ignore hidden files and directories
- paths_to_check = (p for p in path.rglob("*") if not p.name.startswith("."))
+ paths_to_check = (
+ p
+ for p in path.rglob("*")
+ if not p.name.startswith(".") and not any(part.startswith(".") for part in p.parts)
+ )
for item in paths_to_check:
if item.is_file():
total_files += 1
From 61fd80d5791fd61199b69ff1233b326d8f5c82b5 Mon Sep 17 00:00:00 2001
From: psychedelicious <4822129+psychedelicious@users.noreply.github.com>
Date: Thu, 16 Oct 2025 10:06:36 +1100
Subject: [PATCH 2/4] feat(mm): reidentify models
Add route and model record service method to reidentify a model. This
re-probes the model files and replaces the model's config with the new
one if it does not error.
---
invokeai/app/api/routers/model_manager.py | 37 ++++++++++++++++++-
.../model_records/model_records_base.py | 12 ++++++
.../model_records/model_records_sql.py | 17 +++++++++
3 files changed, 65 insertions(+), 1 deletion(-)
diff --git a/invokeai/app/api/routers/model_manager.py b/invokeai/app/api/routers/model_manager.py
index 0c325a4ce05..567c1deaa34 100644
--- a/invokeai/app/api/routers/model_manager.py
+++ b/invokeai/app/api/routers/model_manager.py
@@ -28,7 +28,7 @@
UnknownModelException,
)
from invokeai.app.util.suppress_output import SuppressOutput
-from invokeai.backend.model_manager.configs.factory import AnyModelConfig
+from invokeai.backend.model_manager.configs.factory import AnyModelConfig, ModelConfigFactory
from invokeai.backend.model_manager.configs.main import (
Main_Checkpoint_SD1_Config,
Main_Checkpoint_SD2_Config,
@@ -38,6 +38,7 @@
from invokeai.backend.model_manager.load.model_cache.cache_stats import CacheStats
from invokeai.backend.model_manager.metadata.fetch.huggingface import HuggingFaceMetadataFetch
from invokeai.backend.model_manager.metadata.metadata_base import ModelMetadataWithFiles, UnknownMetadataException
+from invokeai.backend.model_manager.model_on_disk import ModelOnDisk
from invokeai.backend.model_manager.search import ModelSearch
from invokeai.backend.model_manager.starter_models import (
STARTER_BUNDLES,
@@ -191,6 +192,40 @@ async def get_model_record(
raise HTTPException(status_code=404, detail=str(e))
+@model_manager_router.post(
+ "/i/{key}/reidentify",
+ operation_id="reidentify_model",
+ responses={
+ 200: {
+ "description": "The model configuration was retrieved successfully",
+ "content": {"application/json": {"example": example_model_config}},
+ },
+ 400: {"description": "Bad request"},
+ 404: {"description": "The model could not be found"},
+ },
+)
+async def reidentify_model(
+ key: Annotated[str, Path(description="Key of the model to reidentify.")],
+) -> AnyModelConfig:
+ """Attempt to reidentify a model by re-probing its weights file."""
+ try:
+ config = ApiDependencies.invoker.services.model_manager.store.get_model(key)
+ models_path = ApiDependencies.invoker.services.configuration.models_path
+ if pathlib.Path(config.path).is_relative_to(models_path):
+ model_path = pathlib.Path(config.path)
+ else:
+ model_path = models_path / config.path
+ mod = ModelOnDisk(model_path)
+ result = ModelConfigFactory.from_model_on_disk(mod)
+ if result.config is None:
+ raise InvalidModelException("Unable to identify model format")
+ result.config.key = config.key # retain the same key
+ new_config = ApiDependencies.invoker.services.model_manager.store.replace_model(config.key, result.config)
+ return new_config
+ except UnknownModelException as e:
+ raise HTTPException(status_code=404, detail=str(e))
+
+
class FoundModel(BaseModel):
path: str = Field(description="Path to the model")
is_installed: bool = Field(description="Whether or not the model is already installed")
diff --git a/invokeai/app/services/model_records/model_records_base.py b/invokeai/app/services/model_records/model_records_base.py
index 4ac227ba9f4..9dd69a00f54 100644
--- a/invokeai/app/services/model_records/model_records_base.py
+++ b/invokeai/app/services/model_records/model_records_base.py
@@ -138,6 +138,18 @@ def update_model(self, key: str, changes: ModelRecordChanges, allow_class_change
"""
pass
+ @abstractmethod
+ def replace_model(self, key: str, new_config: AnyModelConfig) -> AnyModelConfig:
+ """
+ Replace the model record entirely, returning the new record.
+
+ This is used when we re-identify a model and have a new config object.
+
+ :param key: Unique key for the model to be updated.
+ :param new_config: The new model config to write.
+ """
+ pass
+
@abstractmethod
def get_model(self, key: str) -> AnyModelConfig:
"""
diff --git a/invokeai/app/services/model_records/model_records_sql.py b/invokeai/app/services/model_records/model_records_sql.py
index 943ceefdbc8..edcbba2acdc 100644
--- a/invokeai/app/services/model_records/model_records_sql.py
+++ b/invokeai/app/services/model_records/model_records_sql.py
@@ -179,6 +179,23 @@ def update_model(self, key: str, changes: ModelRecordChanges, allow_class_change
return self.get_model(key)
+ def replace_model(self, key: str, new_config: AnyModelConfig) -> AnyModelConfig:
+ if key != new_config.key:
+ raise ValueError("key does not match new_config.key")
+ with self._db.transaction() as cursor:
+ cursor.execute(
+ """--sql
+ UPDATE models
+ SET
+ config=?
+ WHERE id=?;
+ """,
+ (new_config.model_dump_json(), key),
+ )
+ if cursor.rowcount == 0:
+ raise UnknownModelException("model not found")
+ return self.get_model(key)
+
def get_model(self, key: str) -> AnyModelConfig:
"""
Retrieve the ModelConfigBase instance for the indicated model.
From 91230bc21bca0276eda3e058650b4d7182de6586 Mon Sep 17 00:00:00 2001
From: psychedelicious <4822129+psychedelicious@users.noreply.github.com>
Date: Thu, 16 Oct 2025 10:06:45 +1100
Subject: [PATCH 3/4] chore(ui): typegen
---
.../frontend/web/src/services/api/schema.ts | 84 +++++++++++++++++++
1 file changed, 84 insertions(+)
diff --git a/invokeai/frontend/web/src/services/api/schema.ts b/invokeai/frontend/web/src/services/api/schema.ts
index b52f6eb74c6..40214ffa554 100644
--- a/invokeai/frontend/web/src/services/api/schema.ts
+++ b/invokeai/frontend/web/src/services/api/schema.ts
@@ -91,6 +91,26 @@ export type paths = {
patch: operations["update_model_record"];
trace?: never;
};
+ "/api/v2/models/i/{key}/reidentify": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ get?: never;
+ put?: never;
+ /**
+ * Reidentify Model
+ * @description Attempt to reidentify a model by re-probing its weights file.
+ */
+ post: operations["reidentify_model"];
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
"/api/v2/models/scan_folder": {
parameters: {
query?: never;
@@ -24655,6 +24675,70 @@ export interface operations {
};
};
};
+ reidentify_model: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ /** @description Key of the model to reidentify. */
+ key: string;
+ };
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description The model configuration was retrieved successfully */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ /** @example {
+ * "path": "string",
+ * "name": "string",
+ * "base": "sd-1",
+ * "type": "main",
+ * "format": "checkpoint",
+ * "config_path": "string",
+ * "key": "string",
+ * "hash": "string",
+ * "file_size": 1,
+ * "description": "string",
+ * "source": "string",
+ * "converted_at": 0,
+ * "variant": "normal",
+ * "prediction_type": "epsilon",
+ * "repo_variant": "fp16",
+ * "upcast_attention": false
+ * } */
+ "application/json": components["schemas"]["Main_Diffusers_SD1_Config"] | components["schemas"]["Main_Diffusers_SD2_Config"] | components["schemas"]["Main_Diffusers_SDXL_Config"] | components["schemas"]["Main_Diffusers_SDXLRefiner_Config"] | components["schemas"]["Main_Diffusers_SD3_Config"] | components["schemas"]["Main_Diffusers_CogView4_Config"] | components["schemas"]["Main_Checkpoint_SD1_Config"] | components["schemas"]["Main_Checkpoint_SD2_Config"] | components["schemas"]["Main_Checkpoint_SDXL_Config"] | components["schemas"]["Main_Checkpoint_SDXLRefiner_Config"] | components["schemas"]["Main_Checkpoint_FLUX_Config"] | components["schemas"]["Main_BnBNF4_FLUX_Config"] | components["schemas"]["Main_GGUF_FLUX_Config"] | components["schemas"]["VAE_Checkpoint_SD1_Config"] | components["schemas"]["VAE_Checkpoint_SD2_Config"] | components["schemas"]["VAE_Checkpoint_SDXL_Config"] | components["schemas"]["VAE_Checkpoint_FLUX_Config"] | components["schemas"]["VAE_Diffusers_SD1_Config"] | components["schemas"]["VAE_Diffusers_SDXL_Config"] | components["schemas"]["ControlNet_Checkpoint_SD1_Config"] | components["schemas"]["ControlNet_Checkpoint_SD2_Config"] | components["schemas"]["ControlNet_Checkpoint_SDXL_Config"] | components["schemas"]["ControlNet_Checkpoint_FLUX_Config"] | components["schemas"]["ControlNet_Diffusers_SD1_Config"] | components["schemas"]["ControlNet_Diffusers_SD2_Config"] | components["schemas"]["ControlNet_Diffusers_SDXL_Config"] | components["schemas"]["ControlNet_Diffusers_FLUX_Config"] | components["schemas"]["LoRA_LyCORIS_SD1_Config"] | components["schemas"]["LoRA_LyCORIS_SD2_Config"] | components["schemas"]["LoRA_LyCORIS_SDXL_Config"] | components["schemas"]["LoRA_LyCORIS_FLUX_Config"] | components["schemas"]["LoRA_OMI_SDXL_Config"] | components["schemas"]["LoRA_OMI_FLUX_Config"] | components["schemas"]["LoRA_Diffusers_SD1_Config"] | components["schemas"]["LoRA_Diffusers_SD2_Config"] | components["schemas"]["LoRA_Diffusers_SDXL_Config"] | components["schemas"]["LoRA_Diffusers_FLUX_Config"] | components["schemas"]["ControlLoRA_LyCORIS_FLUX_Config"] | components["schemas"]["T5Encoder_T5Encoder_Config"] | components["schemas"]["T5Encoder_BnBLLMint8_Config"] | components["schemas"]["TI_File_SD1_Config"] | components["schemas"]["TI_File_SD2_Config"] | components["schemas"]["TI_File_SDXL_Config"] | components["schemas"]["TI_Folder_SD1_Config"] | components["schemas"]["TI_Folder_SD2_Config"] | components["schemas"]["TI_Folder_SDXL_Config"] | components["schemas"]["IPAdapter_InvokeAI_SD1_Config"] | components["schemas"]["IPAdapter_InvokeAI_SD2_Config"] | components["schemas"]["IPAdapter_InvokeAI_SDXL_Config"] | components["schemas"]["IPAdapter_Checkpoint_SD1_Config"] | components["schemas"]["IPAdapter_Checkpoint_SD2_Config"] | components["schemas"]["IPAdapter_Checkpoint_SDXL_Config"] | components["schemas"]["IPAdapter_Checkpoint_FLUX_Config"] | components["schemas"]["T2IAdapter_Diffusers_SD1_Config"] | components["schemas"]["T2IAdapter_Diffusers_SDXL_Config"] | components["schemas"]["Spandrel_Checkpoint_Config"] | components["schemas"]["CLIPEmbed_Diffusers_G_Config"] | components["schemas"]["CLIPEmbed_Diffusers_L_Config"] | components["schemas"]["CLIPVision_Diffusers_Config"] | components["schemas"]["SigLIP_Diffusers_Config"] | components["schemas"]["FLUXRedux_Checkpoint_Config"] | components["schemas"]["LlavaOnevision_Diffusers_Config"] | components["schemas"]["Unknown_Config"];
+ };
+ };
+ /** @description Bad request */
+ 400: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
+ /** @description The model could not be found */
+ 404: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
+ /** @description Validation Error */
+ 422: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["HTTPValidationError"];
+ };
+ };
+ };
+ };
scan_for_models: {
parameters: {
query?: {
From 2711437a46d4b01613b88747c8fd4a8f8ffa2a02 Mon Sep 17 00:00:00 2001
From: psychedelicious <4822129+psychedelicious@users.noreply.github.com>
Date: Thu, 16 Oct 2025 10:06:59 +1100
Subject: [PATCH 4/4] feat(ui): add button to reidentify model to mm
---
invokeai/frontend/web/public/locales/en.json | 5 ++
.../ModelPanel/ModelReidentifyButton.tsx | 58 +++++++++++++++++++
.../subpanels/ModelPanel/ModelView.tsx | 3 +
.../web/src/services/api/endpoints/models.ts | 35 +++++++++++
4 files changed, 101 insertions(+)
create mode 100644 invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/ModelReidentifyButton.tsx
diff --git a/invokeai/frontend/web/public/locales/en.json b/invokeai/frontend/web/public/locales/en.json
index 28e0adf81db..8a6bd7b337e 100644
--- a/invokeai/frontend/web/public/locales/en.json
+++ b/invokeai/frontend/web/public/locales/en.json
@@ -844,6 +844,11 @@
"clipLEmbed": "CLIP-L Embed",
"clipGEmbed": "CLIP-G Embed",
"config": "Config",
+ "reidentify": "Reidentify",
+ "reidentifyTooltip": "If a model didn't install correctly (e.g. it has the wrong type or doesn't work), you can try reidentifying it. This will reset any custom settings you may have applied.",
+ "reidentifySuccess": "Model reidentified successfully",
+ "reidentifyUnknown": "Unable to identify model",
+ "reidentifyError": "Error reidentifying model",
"convert": "Convert",
"convertingModelBegin": "Converting Model. Please wait.",
"convertToDiffusers": "Convert To Diffusers",
diff --git a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/ModelReidentifyButton.tsx b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/ModelReidentifyButton.tsx
new file mode 100644
index 00000000000..31334c0510d
--- /dev/null
+++ b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/ModelReidentifyButton.tsx
@@ -0,0 +1,58 @@
+import { Button } from '@invoke-ai/ui-library';
+import { toast } from 'features/toast/toast';
+import { memo, useCallback } from 'react';
+import { useTranslation } from 'react-i18next';
+import { PiSparkleFill } from 'react-icons/pi';
+import { useReidentifyModelMutation } from 'services/api/endpoints/models';
+import type { AnyModelConfig } from 'services/api/types';
+
+interface Props {
+ modelConfig: AnyModelConfig;
+}
+
+export const ModelReidentifyButton = memo(({ modelConfig }: Props) => {
+ const { t } = useTranslation();
+ const [reidentifyModel, { isLoading }] = useReidentifyModelMutation();
+
+ const onClick = useCallback(() => {
+ reidentifyModel({ key: modelConfig.key })
+ .unwrap()
+ .then(({ type }) => {
+ if (type === 'unknown') {
+ toast({
+ id: 'MODEL_REIDENTIFY_UNKNOWN',
+ title: t('modelManager.reidentifyUnknown'),
+ status: 'warning',
+ });
+ }
+ toast({
+ id: 'MODEL_REIDENTIFY_SUCCESS',
+ title: t('modelManager.reidentifySuccess'),
+ status: 'success',
+ });
+ })
+ .catch((_) => {
+ toast({
+ id: 'MODEL_REIDENTIFY_ERROR',
+ title: t('modelManager.reidentifyError'),
+ status: 'error',
+ });
+ });
+ }, [modelConfig.key, reidentifyModel, t]);
+
+ return (
+ }
+ >
+ {t('modelManager.reidentify')}
+
+ );
+});
+
+ModelReidentifyButton.displayName = 'ModelReidentifyButton';
diff --git a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/ModelView.tsx b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/ModelView.tsx
index 538ebf597e0..9a8288f5d6c 100644
--- a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/ModelView.tsx
+++ b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/ModelView.tsx
@@ -13,6 +13,7 @@ import type { AnyModelConfig } from 'services/api/types';
import { MainModelDefaultSettings } from './MainModelDefaultSettings/MainModelDefaultSettings';
import { ModelAttrView } from './ModelAttrView';
import { ModelFooter } from './ModelFooter';
+import { ModelReidentifyButton } from './ModelReidentifyButton';
import { RelatedModels } from './RelatedModels';
type Props = {
@@ -21,6 +22,7 @@ type Props = {
export const ModelView = memo(({ modelConfig }: Props) => {
const { t } = useTranslation();
+
const withSettings = useMemo(() => {
if (modelConfig.type === 'main' && modelConfig.base !== 'sdxl-refiner') {
return true;
@@ -46,6 +48,7 @@ export const ModelView = memo(({ modelConfig }: Props) => {
)}
+
diff --git a/invokeai/frontend/web/src/services/api/endpoints/models.ts b/invokeai/frontend/web/src/services/api/endpoints/models.ts
index bb526fab364..51b4a84bdc1 100644
--- a/invokeai/frontend/web/src/services/api/endpoints/models.ts
+++ b/invokeai/frontend/web/src/services/api/endpoints/models.ts
@@ -299,6 +299,40 @@ export const modelsApi = api.injectEndpoints({
emptyModelCache: build.mutation({
query: () => ({ url: buildModelsUrl('empty_model_cache'), method: 'POST' }),
}),
+ reidentifyModel: build.mutation<
+ paths['/api/v2/models/i/{key}/reidentify']['post']['responses']['200']['content']['application/json'],
+ { key: string }
+ >({
+ query: ({ key }) => {
+ return {
+ url: buildModelsUrl(`i/${key}/reidentify`),
+ method: 'POST',
+ };
+ },
+ onQueryStarted: async (_, { dispatch, queryFulfilled }) => {
+ try {
+ const { data } = await queryFulfilled;
+
+ // Update the individual model query caches
+ dispatch(modelsApi.util.upsertQueryData('getModelConfig', data.key, data));
+
+ const { base, name, type } = data;
+ dispatch(modelsApi.util.upsertQueryData('getModelConfigByAttrs', { base, name, type }, data));
+
+ // Update the list query cache
+ dispatch(
+ modelsApi.util.updateQueryData('getModelConfigs', undefined, (draft) => {
+ modelConfigsAdapter.updateOne(draft, {
+ id: data.key,
+ changes: data,
+ });
+ })
+ );
+ } catch {
+ // no-op
+ }
+ },
+ }),
}),
});
@@ -321,6 +355,7 @@ export const {
useSetHFTokenMutation,
useResetHFTokenMutation,
useEmptyModelCacheMutation,
+ useReidentifyModelMutation,
} = modelsApi;
export const selectModelConfigsQuery = modelsApi.endpoints.getModelConfigs.select();