Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions invokeai/app/api/routers/model_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
64 changes: 57 additions & 7 deletions invokeai/frontend/web/src/features/metadata/parsing.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1063,7 +1063,8 @@ const CanvasLayers: SingleMetadataHandler<CanvasMetadata> = {

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) {
Expand Down Expand Up @@ -1099,7 +1100,8 @@ const CanvasLayers: SingleMetadataHandler<CanvasMetadata> = {
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);
}
}
}
Expand Down Expand Up @@ -1165,7 +1167,9 @@ const RefImages: CollectionMetadataHandler<RefImageState[]> = {
}
// 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);
}
}

Expand Down Expand Up @@ -1534,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
Expand Down Expand Up @@ -1562,10 +1578,44 @@ const throwIfImageDoesNotExist = async (name: string, store: AppStore): Promise<
}
};

const throwIfModelDoesNotExist = async (key: string, store: AppStore): Promise<void> => {
/**
* 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).
* 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; hash?: string; name: string; base: string; type: string },
store: AppStore
): Promise<AnyModelConfig> => {
// First try by key (fast path)
try {
const req = store.dispatch(modelsApi.endpoints.getModelConfig.initiate(model.key, { subscribe: false }));
return await req.unwrap();
} catch {
// Key not found - try fallback
}

// 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 {
await store.dispatch(modelsApi.endpoints.getModelConfig.initiate(key, { subscribe: false }));
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 with key ${key} does not exist`);
throw new Error(`Model "${model.name}" (key: ${model.key}) does not exist`);
}
};
12 changes: 12 additions & 0 deletions invokeai/frontend/web/src/services/api/endpoints/models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,18 @@ export const modelsApi = api.injectEndpoints({
},
serializeQueryArgs: ({ queryArgs }) => `${queryArgs.name}.${queryArgs.base}.${queryArgs.type}`,
}),
getModelConfigByHash: build.query<AnyModelConfig, string>({
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<ScanFolderResponse, ScanFolderArg>({
query: (arg) => {
const folderQueryStr = arg ? queryString.stringify(arg, {}) : '';
Expand Down
53 changes: 53 additions & 0 deletions invokeai/frontend/web/src/services/api/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -369,6 +369,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;
Expand Down Expand Up @@ -29117,6 +29138,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;
Expand Down
Loading