From 101139dbd4668cdc2d9244fa0f5e8465920b5e04 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 11 Mar 2026 02:56:44 +0000 Subject: [PATCH 1/4] fix(ui): resolve models by name+base+type when recalling metadata for reinstalled models When a model (IP Adapter, ControlNet, etc.) is deleted and reinstalled, it gets a new UUID key. Previously, metadata recall would fail because it only looked up models by their stored UUID key. Now the recall falls back to searching by name+base+type, allowing reinstalled models with the same name to be correctly resolved. https://claude.ai/code/session_01XYubzMK363BXGTvfJJqFnX --- .../web/src/features/metadata/parsing.tsx | 39 ++++++++++++++++--- 1 file changed, 33 insertions(+), 6 deletions(-) diff --git a/invokeai/frontend/web/src/features/metadata/parsing.tsx b/invokeai/frontend/web/src/features/metadata/parsing.tsx index c17f18ec93a..4cfba0dcc48 100644 --- a/invokeai/frontend/web/src/features/metadata/parsing.tsx +++ b/invokeai/frontend/web/src/features/metadata/parsing.tsx @@ -1063,7 +1063,8 @@ const CanvasLayers: SingleMetadataHandler = { for (const entity of parsed.controlLayers) { if (entity.controlAdapter.model) { - await throwIfModelDoesNotExist(entity.controlAdapter.model.key, store); + const resolvedConfig = await resolveModel(entity.controlAdapter.model, store); + entity.controlAdapter.model = zModelIdentifierField.parse(resolvedConfig); } for (const object of entity.objects) { if (object.type === 'image' && 'image_name' in object.image) { @@ -1099,7 +1100,8 @@ const CanvasLayers: SingleMetadataHandler = { await throwIfImageDoesNotExist(refImage.config.image.image_name, store); } if (refImage.config.model) { - await throwIfModelDoesNotExist(refImage.config.model.key, store); + const resolvedConfig = await resolveModel(refImage.config.model, store); + refImage.config.model = zModelIdentifierField.parse(resolvedConfig); } } } @@ -1165,7 +1167,9 @@ const RefImages: CollectionMetadataHandler = { } // FLUX.2 reference images don't have a model field (built-in support) if ('model' in refImage.config && refImage.config.model) { - await throwIfModelDoesNotExist(refImage.config.model.key, store); + const resolvedConfig = await resolveModel(refImage.config.model, store); + // Update the model reference in case the key changed (e.g. model was reinstalled) + refImage.config.model = zModelIdentifierField.parse(resolvedConfig); } } @@ -1562,10 +1566,33 @@ const throwIfImageDoesNotExist = async (name: string, store: AppStore): Promise< } }; -const throwIfModelDoesNotExist = async (key: string, store: AppStore): Promise => { +/** + * Resolve a model by key, falling back to name+base+type lookup if the key is not found. + * This handles the case where a model was deleted and reinstalled (getting a new UUID key). + * Returns the resolved model config, or throws if the model cannot be found by either method. + */ +const resolveModel = async ( + model: { key: string; name: string; base: string; type: string }, + store: AppStore +): Promise => { + // First try by key (fast path) try { - await store.dispatch(modelsApi.endpoints.getModelConfig.initiate(key, { subscribe: false })); + const req = store.dispatch(modelsApi.endpoints.getModelConfig.initiate(model.key, { subscribe: false })); + return await req.unwrap(); } catch { - throw new Error(`Model with key ${key} does not exist`); + // Key not found - try fallback + } + + // Fallback: look up by name + base + type (handles reinstalled models) + try { + const req = store.dispatch( + modelsApi.endpoints.getModelConfigByAttrs.initiate( + { name: model.name, base: model.base as any, type: model.type as any }, + { subscribe: false } + ) + ); + return await req.unwrap(); + } catch { + throw new Error(`Model "${model.name}" (key: ${model.key}) does not exist`); } }; From 33eed4c5171301c09b0c3f1621c69b3e2b59b2bf Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 11 Mar 2026 03:38:59 +0000 Subject: [PATCH 2/4] Add hash-based model recall fallback for reinstalled models MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When a model is deleted and reinstalled, it gets a new UUID key but retains the same BLAKE3 content hash. This adds hash as a middle fallback stage in model resolution (key → hash → name+base+type), making recall more robust. Changes: - Add /api/v2/models/get_by_hash backend endpoint (uses existing search_by_hash from model records store) - Add getModelConfigByHash RTK Query endpoint in frontend - Add hash fallback to both resolveModel and parseModelIdentifier https://claude.ai/code/session_01XYubzMK363BXGTvfJJqFnX --- invokeai/app/api/routers/model_manager.py | 17 +++++++++ .../web/src/features/metadata/parsing.tsx | 35 ++++++++++++++++--- .../web/src/services/api/endpoints/models.ts | 12 +++++++ 3 files changed, 59 insertions(+), 5 deletions(-) diff --git a/invokeai/app/api/routers/model_manager.py b/invokeai/app/api/routers/model_manager.py index 234c6c96629..81bc7c124a8 100644 --- a/invokeai/app/api/routers/model_manager.py +++ b/invokeai/app/api/routers/model_manager.py @@ -193,6 +193,23 @@ async def get_model_records_by_attrs( return configs[0] +@model_manager_router.get( + "/get_by_hash", + operation_id="get_model_records_by_hash", + response_model=AnyModelConfig, +) +async def get_model_records_by_hash( + hash: str = Query(description="The hash of the model"), +) -> AnyModelConfig: + """Gets a model by its hash. This is useful for recalling models that were deleted and reinstalled, + as the hash remains stable across reinstallations while the key (UUID) changes.""" + configs = ApiDependencies.invoker.services.model_manager.store.search_by_hash(hash) + if not configs: + raise HTTPException(status_code=404, detail="No model found with this hash") + + return configs[0] + + @model_manager_router.get( "/i/{key}", operation_id="get_model_record", diff --git a/invokeai/frontend/web/src/features/metadata/parsing.tsx b/invokeai/frontend/web/src/features/metadata/parsing.tsx index 4cfba0dcc48..3c1a8e1444b 100644 --- a/invokeai/frontend/web/src/features/metadata/parsing.tsx +++ b/invokeai/frontend/web/src/features/metadata/parsing.tsx @@ -1538,7 +1538,19 @@ const parseModelIdentifier = async (raw: unknown, store: AppStore, type: ModelTy const modelConfig = await req.unwrap(); return zModelIdentifierField.parse(modelConfig); } catch { - // We'll try to parse the old format identifier next + // We'll try hash-based lookup next + } + + // Try hash-based lookup (handles reinstalled models with new UUID keys) + try { + const { hash } = zModelIdentifierField.parse(raw); + if (hash) { + const req = store.dispatch(modelsApi.endpoints.getModelConfigByHash.initiate(hash, options)); + const modelConfig = await req.unwrap(); + return zModelIdentifierField.parse(modelConfig); + } + } catch { + // We'll try the old format identifier next } // Fall back to old format identifier: model_name, base_model @@ -1567,12 +1579,13 @@ const throwIfImageDoesNotExist = async (name: string, store: AppStore): Promise< }; /** - * Resolve a model by key, falling back to name+base+type lookup if the key is not found. + * Resolve a model by key, falling back to hash or name+base+type lookup if the key is not found. * This handles the case where a model was deleted and reinstalled (getting a new UUID key). - * Returns the resolved model config, or throws if the model cannot be found by either method. + * Fallback order: key → hash → name+base+type + * Returns the resolved model config, or throws if the model cannot be found by any method. */ const resolveModel = async ( - model: { key: string; name: string; base: string; type: string }, + model: { key: string; hash?: string; name: string; base: string; type: string }, store: AppStore ): Promise => { // First try by key (fast path) @@ -1583,7 +1596,19 @@ const resolveModel = async ( // Key not found - try fallback } - // Fallback: look up by name + base + type (handles reinstalled models) + // Second try by hash (most reliable for reinstalled models - hash is content-based) + if (model.hash) { + try { + const req = store.dispatch( + modelsApi.endpoints.getModelConfigByHash.initiate(model.hash, { subscribe: false }) + ); + return await req.unwrap(); + } catch { + // Hash not found - try next fallback + } + } + + // Last resort: look up by name + base + type try { const req = store.dispatch( modelsApi.endpoints.getModelConfigByAttrs.initiate( diff --git a/invokeai/frontend/web/src/services/api/endpoints/models.ts b/invokeai/frontend/web/src/services/api/endpoints/models.ts index f48b5867672..567d63a1000 100644 --- a/invokeai/frontend/web/src/services/api/endpoints/models.ts +++ b/invokeai/frontend/web/src/services/api/endpoints/models.ts @@ -239,6 +239,18 @@ export const modelsApi = api.injectEndpoints({ }, serializeQueryArgs: ({ queryArgs }) => `${queryArgs.name}.${queryArgs.base}.${queryArgs.type}`, }), + getModelConfigByHash: build.query({ + query: (hash) => buildModelsUrl(`get_by_hash?${queryString.stringify({ hash })}`), + providesTags: (result) => { + const tags: ApiTagDescription[] = []; + + if (result) { + tags.push({ type: 'ModelConfig', id: result.key }); + } + + return tags; + }, + }), scanFolder: build.query({ query: (arg) => { const folderQueryStr = arg ? queryString.stringify(arg, {}) : ''; From d2406f6be32b7f1dd827447b59a937fb53b16977 Mon Sep 17 00:00:00 2001 From: Alexander Eichhorn Date: Wed, 11 Mar 2026 18:08:58 +0100 Subject: [PATCH 3/4] Chore pnpm fix --- invokeai/frontend/web/src/features/metadata/parsing.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/invokeai/frontend/web/src/features/metadata/parsing.tsx b/invokeai/frontend/web/src/features/metadata/parsing.tsx index 3c1a8e1444b..7d1d511a3c2 100644 --- a/invokeai/frontend/web/src/features/metadata/parsing.tsx +++ b/invokeai/frontend/web/src/features/metadata/parsing.tsx @@ -1599,9 +1599,7 @@ const resolveModel = async ( // Second try by hash (most reliable for reinstalled models - hash is content-based) if (model.hash) { try { - const req = store.dispatch( - modelsApi.endpoints.getModelConfigByHash.initiate(model.hash, { subscribe: false }) - ); + const req = store.dispatch(modelsApi.endpoints.getModelConfigByHash.initiate(model.hash, { subscribe: false })); return await req.unwrap(); } catch { // Hash not found - try next fallback From 3cf7181dc38361e1d2a5f16c8775485ae107461c Mon Sep 17 00:00:00 2001 From: Alexander Eichhorn Date: Wed, 11 Mar 2026 18:16:14 +0100 Subject: [PATCH 4/4] Chore typegen --- .../frontend/web/src/services/api/schema.ts | 53 +++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/invokeai/frontend/web/src/services/api/schema.ts b/invokeai/frontend/web/src/services/api/schema.ts index b605413787b..905271d7f2d 100644 --- a/invokeai/frontend/web/src/services/api/schema.ts +++ b/invokeai/frontend/web/src/services/api/schema.ts @@ -231,6 +231,27 @@ export type paths = { patch?: never; trace?: never; }; + "/api/v2/models/get_by_hash": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Get Model Records By Hash + * @description Gets a model by its hash. This is useful for recalling models that were deleted and reinstalled, + * as the hash remains stable across reinstallations while the key (UUID) changes. + */ + get: operations["get_model_records_by_hash"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; "/api/v2/models/i/{key}": { parameters: { query?: never; @@ -28678,6 +28699,38 @@ export interface operations { }; }; }; + get_model_records_by_hash: { + parameters: { + query: { + /** @description The hash of the model */ + hash: string; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "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_FLUX_Config"] | components["schemas"]["Main_Diffusers_Flux2_Config"] | components["schemas"]["Main_Diffusers_CogView4_Config"] | components["schemas"]["Main_Diffusers_ZImage_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_Flux2_Config"] | components["schemas"]["Main_Checkpoint_FLUX_Config"] | components["schemas"]["Main_Checkpoint_ZImage_Config"] | components["schemas"]["Main_BnBNF4_FLUX_Config"] | components["schemas"]["Main_GGUF_Flux2_Config"] | components["schemas"]["Main_GGUF_FLUX_Config"] | components["schemas"]["Main_GGUF_ZImage_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_Checkpoint_Flux2_Config"] | components["schemas"]["VAE_Diffusers_SD1_Config"] | components["schemas"]["VAE_Diffusers_SDXL_Config"] | components["schemas"]["VAE_Diffusers_Flux2_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_Checkpoint_ZImage_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_Flux2_Config"] | components["schemas"]["LoRA_LyCORIS_FLUX_Config"] | components["schemas"]["LoRA_LyCORIS_ZImage_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_Flux2_Config"] | components["schemas"]["LoRA_Diffusers_FLUX_Config"] | components["schemas"]["LoRA_Diffusers_ZImage_Config"] | components["schemas"]["ControlLoRA_LyCORIS_FLUX_Config"] | components["schemas"]["T5Encoder_T5Encoder_Config"] | components["schemas"]["T5Encoder_BnBLLMint8_Config"] | components["schemas"]["Qwen3Encoder_Qwen3Encoder_Config"] | components["schemas"]["Qwen3Encoder_Checkpoint_Config"] | components["schemas"]["Qwen3Encoder_GGUF_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 Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; get_model_record: { parameters: { query?: never;