Skip to content

Persist the catalog↔download association for discovered models (retire the client base-name heuristic) #123

Description

@jdmanring

Summary

Discovered catalog models carry no link to the repo they were actually downloaded from, so the app cannot reliably tell a model is installed. #121 papered over this on the client with a base-name heuristic; this issue is the root-cause fix that lets that heuristic be retired.

The data gap

In services/hwfit/data/hf_models.json (~900 entries, managed by services/hwfit/models.py), auto-discovered models are flagged "_discovered": true with gguf_sources: null, quant_repo: null, and a null repo_id — they carry only a base name.

When the user downloads such a model, the app resolves a community quant repo (via the /api/cookbook/resolve-gguf path: bartowski/Meta-Llama-3.1-8B-Instruct-GGUF, legraphista/DeepSeek-V2-Lite-Chat-IMat-GGUF, nvidia/Qwen3-30B-A3B-NVFP4, ...) and downloads that repo. The model cache then records the quant repo's repo_id. Nothing records which catalog model that download satisfies. The catalog entry and the downloaded repo now share only the base model name.

The real problem: lost provenance, not a matching bug

At download time the system knows the answer for free: the user chose catalog model X, the resolver picked repo Y, the downloader fetched Y for X. Then it discards that fact and forces every render site to reconstruct it by guessing. So this is not "the matcher is wrong" — it is "we threw away a fact we had, then re-derived it with heuristics."

That guess is what #121 shipped (strip org prefix + quant/format tag, compare base names). It works today but is debt:

  • It relies on a hardcoded quant/format tag vocabulary (gguf|awq|gptq|nvfp4|fp8|...). New formats appear constantly; each is a future "doesn't grey" bug, the same drift class as the original problem.
  • Base names are fuzzy, so it needs guarding (untagged-entry restriction, length floor) and still mis-fires at the edges (it grayed an -AWQ-8bit sibling of a downloaded -AWQ-4bit until that was patched).

The fix is to capture ground truth at the point it is known and never throw it away.

Senior design

1. Record provenance at the download chokepoint

When a download completes successfully (the cache already filters status != stalled), persist the association downloaded_repo_id -> { satisfies: <catalog model id>, resolved_from: discovery, quant: ... }. Idempotent: a re-download updates, never duplicates.

2. Store it as user/runtime state, NOT in the catalog

Explicitly reject writing the resolved source back into hf_models.json:

  • Wrong layer. hf_models.json is shippable reference data (what models exist); download provenance is per-machine user state. Don't conflate them.
  • Doesn't survive updates. A git pull / reinstall overwrites the catalog and the associations vanish. That is exactly upstream #4049 ("cache becomes unaware of installed models after update") — writing into the catalog builds the next regression.
  • Multi-user-unsafe. Odysseus has _users; one user's download must not mutate shared reference data.

Store provenance alongside the existing model-cache state (a small persistent map / sidecar in services/hwfit/), so the catalog stays read-only and shippable and the associations survive updates because they are user data.

3. Match by exact join

"Is X downloaded?" becomes: does any downloaded entry's satisfies equal X's id. Zero tag-stripping, nothing to rot. The client static/js/model/downloaded.js predicate then prefers this exact association.

4. Give catalog entries a stable identity

Discovered models currently have only a name (null repo_id). Provenance must reference a stable key, not a display string, or the fuzziness just moves.

5. Plan the backfill; then retire the heuristic

Existing downloads predate provenance. This is the one place the base-name heuristic is correct: run it once as a migration to seed associations for legacy downloads, persist the results, keep it only as a fallback for un-provenanced entries, and make a mis-association correctable. New downloads never touch the heuristic, so it dies instead of growing.

6. Do not over-engineer

No new DB table or "model registry service." The smallest correct thing — provenance written at download-complete, joined at render — is the right scope. Match existing storage patterns.

One line: don't fix the guess, eliminate the need to guess: record what each download satisfies at the moment you know it, store it as update-surviving user state (not in the catalog), join exactly, and let the heuristic live only as a one-shot backfill that then retires.

Relation to upstream

  • #4049 — cache becomes unaware of installed models after update. Same problem space; storing provenance as update-surviving user state also hardens against this (and is the reason NOT to write into the catalog).
  • #2342 — auto-discovered models without gguf_sources. This is the natural completion: resolve once, then persist the provenance so the gap doesn't recur per render.

Classification

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingupstream-candidateBug or feature suitable for contributing to upstream

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions