unslothai/unsloth · error · ValueError

ControlNet '{spec_id}' has no repo

Error message

ControlNet '{spec_id}' has no repo

What it means

resolve_controlnet() matched a catalog entry whose source is not 'local' but whose repo_id is empty, so there is nothing to download and nothing on disk to load. It is a guard against a malformed curated/catalog entry rather than a user input error.

Source

Thrown at studio/backend/core/inference/diffusion_controlnet.py:181

    entry = _catalog_by_id().get(spec_id)
    if entry is None:
        # A curated entry named by its full repo id must still hit the family gate below, not slip through the bare-repo fallback.
        entry = next((e for e in _CURATED if e.repo_id and e.repo_id == spec_id), None)
    if entry is not None:
        # A direct API call could send an entry for another family; reject it before any download.
        fam = (family or "").strip().lower()
        if entry.families and fam and fam not in {f.lower() for f in entry.families}:
            raise ValueError(
                f"ControlNet '{spec_id}' is for {', '.join(entry.families)}, not the loaded "
                f"'{family}' model; pick a ControlNet built for this family."
            )
        if entry.source == "local":
            path = entry.local_path or ""
            if not path or not Path(path).is_dir():
                raise FileNotFoundError(f"ControlNet '{spec_id}' is no longer present on disk")
            return ResolvedControlNet(spec_id, path, is_local = True)
        if not entry.repo_id:
            raise ValueError(f"ControlNet '{spec_id}' has no repo")
        return ResolvedControlNet(spec_id, entry.repo_id, is_local = False)

    # A bare HF repo id (owner/name). STRICT shape so a filesystem-looking id can never reach from_pretrained.
    if re.fullmatch(r"[A-Za-z0-9][A-Za-z0-9_.-]*/[A-Za-z0-9][A-Za-z0-9_.-]*", spec_id):
        return ResolvedControlNet(spec_id, spec_id, is_local = False)

    raise FileNotFoundError(
        f"unknown ControlNet '{spec_id}': not a local model, catalog entry, or HF repo id"
    )


# Union ControlNet mode indices: a union model selects its head via an integer ``control_mode``.
_UNION_CONTROL_MODES: dict[str, int] = {
    "canny": 0,
    "tile": 1,
    "depth": 2,
    "blur": 3,
    "pose": 4,

View on GitHub (pinned to 203007d190)

Solutions

  1. Fix the catalog entry: give it a valid Hugging Face repo_id (owner/name) or convert it to source='local' with a real local_path
  2. If you manage _CURATED in code, add an assertion/test that every non-local entry has a non-empty repo_id
  3. As a caller, catch ValueError and surface it as a configuration error rather than retrying

Example fix

# before
ControlNetCatalogEntry(id="x", display_name="X", source="remote")

# after
ControlNetCatalogEntry(id="x", display_name="X", source="remote", repo_id="owner/x-controlnet")
Defensive patterns

Strategy: validation

Validate before calling

from studio.backend.core.inference.diffusion_controlnet import list_controlnets

bad = [e.id for e in list_controlnets() if e.source != "local" and not e.repo_id]
assert not bad, f"malformed catalog entries without repo_id: {bad}"

Try / catch

try:
    resolved = resolve_controlnet(spec_id, family=fam)
except ValueError as e:
    log.error("catalog misconfigured for %s: %s", spec_id, e)  # config defect, do not retry

Prevention

When it happens

Trigger: A catalog entry with source='remote' (or anything != 'local') and repo_id unset (None/'') reaching resolve_controlnet(); typically from editing the _CURATED list or injecting a custom entry with a missing repo_id.

Common situations: Hand-editing the curated ControlNet catalog and forgetting the repo_id field; a code change that constructs ControlNetCatalogEntry entries without repo_id; data migration that drops the field.

Related errors


AI-assisted analysis of unslothai/unsloth@203007d190 (2026-08-15). Data as JSON: /api/errors/b32470b2f06b01e5. Report an issue: GitHub.