Skip to content
Open
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
10 changes: 7 additions & 3 deletions invokeai/backend/model_manager/metadata/metadata_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -126,14 +126,18 @@ def download_urls(

paths = filter_files([x.path for x in self.files], variant, subfolder, subfolders) # all files in the model

repo_root = Path(self.name) if paths and paths[0].parts[0] == self.name else Path()

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: In production every path starts with the repo name, because from_id() builds Path(name, rfilename). So the Path() fallback only serves the older unprefixed tests.

If the two ever disagree, the filter silently switches itself off again, which is exactly #9901. I'd use Path(self.name) unconditionally and move those tests to prefixed paths.


# Determine prefix for model_index.json check - only applies for single subfolder
prefix = ""
if subfolder and not subfolders:
prefix = f"{subfolder}/"

# the next step reads model_index.json to determine which subdirectories belong
# to the model (only for single subfolder case)
if Path(f"{prefix}model_index.json") in paths:
model_root = repo_root / prefix
model_index_path = model_root / "model_index.json"

@joshistoast joshistoast Oct 19, 2026 •

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] Authenticate the newly enabled model-index request

Recognizing repository-prefixed paths now makes normal repo-id installs execute the index GET below, which uses a plain requests.Session without Hugging Face credentials. HuggingFaceMetadataFetch.from_id() can successfully retrieve a private repository through the authenticated HfApi, and the installer supplies the saved token to the download queue, but neither credential path reaches this request. Consequently, an otherwise authorized private or gated Diffusers install now fails with HTTP 401/403 before downloads start; the base revision skipped this request for these paths. Fetch the index with the same authentication context as the repository/download operations.

if model_index_path in paths:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Fetch the model index from the selected revision

When a repository exposes an actual variant revision (for example fp16) whose index differs from its default branch, this newly reachable condition filters the variant's files with the wrong index: HuggingFaceMetadataFetch.from_id() builds each file URL with revision=variant (metadata/fetch/huggingface.py:123-130), while the request below reconstructs model_index.json without a revision. Required variant-only components can then be silently omitted; request the URL already stored for model_index_path, which also records whether metadata fetching fell back to the default revision.

AGENTS.md reference: invokeai/backend/AGENTS.md:L9-L9

Useful? React with 👍 / 👎.

url = hf_hub_url(self.id, filename="model_index.json", subfolder=str(subfolder) if subfolder else None)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On Windows, str(subfolder) uses backslashes for a nested subfolder. For example, hf_hub_url(..., subfolder=str(Path("FL2VA/sub"))) gives .../resolve/main/FL2VA%5Csub/model_index.json. That 404s, and the 404 now raises out of the install.

The line predates this PR, but this PR is what makes it reachable. subfolder.as_posix() fixes it.

try:
resp = session.get(url, timeout=HF_METADATA_REQUEST_TIMEOUT_SECONDS)
Expand All @@ -145,8 +149,8 @@ def download_urls(
except RequestException as e:
raise ModelMetadataUnavailableError(f"Could not fetch model index for '{self.id}'") from e
submodels = resp.json()
paths = [Path(subfolder or "", x) for x in paths if Path(x).parent.as_posix() in submodels]
paths.insert(0, Path(f"{prefix}model_index.json"))
paths = [x for x in paths if x.relative_to(model_root).parent.as_posix() in submodels]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve the modular pipeline index

When installing MiniMaxAI/MiniMax-H3 by repo ID, filter_files() selects both root indexes (tests/backend/model_manager/util/test_hf_model_select.py:419-420), but modular_model_index.json has relative parent . and is removed by this predicate before only the classic index is reinserted. Main_Diffusers_MiniMaxH3_Config.from_model_on_disk() requires the modular index to identify H3 (invokeai/backend/model_manager/configs/main.py:1757-1763), so a normal full-repo install is left unrecognizable; retain this required root pipeline index while filtering unrelated root artifacts.

AGENTS.md reference: invokeai/backend/AGENTS.md:L9-L9

Useful? React with 👍 / 👎.

paths.insert(0, model_index_path)

return [x for x in self.files if x.path in paths]

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
from pathlib import Path
from unittest.mock import Mock

from invokeai.backend.model_manager.metadata.metadata_base import HuggingFaceMetadata, RemoteModelFile


def test_repo_id_diffusers_download_uses_model_index_components() -> None:
session = Mock()
response = Mock(status_code=200)
response.json.return_value = {
"_class_name": "StableDiffusionPipeline",
"unet": ["diffusers", "UNet2DConditionModel"],
"vae": ["diffusers", "AutoencoderKL"],
}
session.get.return_value = response
repo_files = [
"model_index.json",
"model.safetensors",
"unet/config.json",
"unet/diffusion_pytorch_model.safetensors",
"vae/config.json",
"vae/diffusion_pytorch_model.safetensors",
"README.md",
]
metadata = HuggingFaceMetadata(
id="org/diffusers-repo",
name="diffusers-repo",
files=[
RemoteModelFile(
url=f"https://huggingface.co/org/diffusers-repo/resolve/main/{path}",
path=Path("diffusers-repo", path),
)
for path in repo_files
],
)

selected = {file.path.as_posix() for file in metadata.download_urls(session=session)}

assert selected == {
"diffusers-repo/model_index.json",
"diffusers-repo/unet/config.json",
"diffusers-repo/unet/diffusion_pytorch_model.safetensors",
"diffusers-repo/vae/config.json",
"diffusers-repo/vae/diffusion_pytorch_model.safetensors",
}
assert session.get.call_args.args[0].endswith("model_index.json")


def test_repo_id_diffusers_subfolder_uses_its_model_index_components() -> None:
session = Mock()
response = Mock(status_code=200)
response.json.return_value = {
"_class_name": "StableDiffusionPipeline",
"unet": ["diffusers", "UNet2DConditionModel"],
"vae": ["diffusers", "AutoencoderKL"],
}
session.get.return_value = response
repo_files = [
"pipeline/model_index.json",
"pipeline/model.safetensors",
"pipeline/unet/config.json",
"pipeline/unet/diffusion_pytorch_model.safetensors",
"pipeline/vae/config.json",
"pipeline/vae/diffusion_pytorch_model.safetensors",
]
metadata = HuggingFaceMetadata(
id="org/diffusers-repo",
name="diffusers-repo",
files=[
RemoteModelFile(
url=f"https://huggingface.co/org/diffusers-repo/resolve/main/{path}",
path=Path("diffusers-repo", path),
)
for path in repo_files
],
)

selected = {file.path.as_posix() for file in metadata.download_urls(subfolder=Path("pipeline"), session=session)}

assert selected == {
"diffusers-repo/pipeline/model_index.json",
"diffusers-repo/pipeline/unet/config.json",
"diffusers-repo/pipeline/unet/diffusion_pytorch_model.safetensors",
"diffusers-repo/pipeline/vae/config.json",
"diffusers-repo/pipeline/vae/diffusion_pytorch_model.safetensors",
}
assert session.get.call_args.args[0].endswith("pipeline/model_index.json")