Skip to content

fix(models): download the AI models family by family - #473

Merged
bmachek merged 3 commits into
mainfrom
claude/brave-curie-ck0twg
Nov 4, 2026
Merged

bmachek merged 3 commits into
mainfrom
claude/brave-curie-ck0twg

Conversation

@bmachek

@bmachek bmachek commented Nov 3, 2026 •

Copy link
Copy Markdown
Member

Summary

Download AI models (POST /v1/models/assets/downloads) put the files of every missing model family into one list and handed it to download_url_assets. That function sent a HEAD request per file and stopped the whole job at the first failure. So one unreachable release meant nothing downloaded, not even the search model for a new user. A release can be unreachable when its tag is not published yet, when a model-assets-*.yml run failed, or when a host is blocked.

This is the case on main right now. face-state-assets-v1 does not exist: the only run of model-assets-face-state.yml, after the #471 merge (run 41688897316), was cancelled after 15 minutes (its limit is 45). Until that release is published, every user who is still missing any model gets nothing from "Download AI models" on main. With this PR the other families download, and "Eye and expression analysis" is named as missing. Re-running that workflow (it has workflow_dispatch) publishes the release.

Now each family downloads on its own:

  • A family whose files cannot be resolved (HEAD) or fetched (GET or write) is skipped, and the others still download.
  • The job still ends in error when any family failed:
    • error_messages holds one "<family name>: <reason>" per failed family.
    • error names each failed family and says what to do. The status stays downloading until every family has been tried.
  • Progress stays one bar. Sizes are resolved for every family before the first byte, so total covers only what will be fetched. A family that fails part-way takes the bytes it will not fetch out of total, so the bar still ends full.
  • The single-family routes do not change. CLIP, BioCLIP and AuraFace share the split-out HEAD pass (resolve_sizes) and transfer (fetch_assets). They stay all-or-nothing, with the same messages. A test pins that.

The progress total was 0

The HEAD pass used resp.content_length(). reqwest computes it from the received body, and a HEAD response has none, so it is always 0 (seanmonstar/reqwest#1814). Every download on this machinery therefore had total = 0 until set_done() filled it in at the end. Sizes now come from the Content-Length header. Switching back to content_length() makes 3 of the new tests fail (left: 0, right: 57344).

Changes

  • routes/clip.rs
    • Splits download_url_assets into resolve_sizes, fetch_assets and release_urls.
    • ModelDownloadStatus gets error_messages, omitted from the JSON when empty so the other download routes answer exactly as before, plus set_errors.
  • routes/assets.rs
    • A Family enum is the one place for each family's id, human name, size estimate and readiness. GET /v1/models/assets uses it and returns the same JSON, now pinned by a contract test.
    • download_families is the per-family loop, and failure_summary builds the user-facing text.
    • With main merged in, feat(cull): eye focus, eye state, smile and gaze per face #471's face_state is Family::FaceState: "Eye and expression analysis", face-state-assets-v1.
  • src/fake_release.rs (#[cfg(test)]): a local axum stand-in for GitHub release downloads with three behaviours: files that answer, a 404, and HEAD-OK/GET-502. It sits outside routes/ because check-docs.py reads every .route( there as an endpoint.
  • Plugin (APISearchIndex.lua startAssetDownload): all three callers end in this loop: the Plug-in Manager button, the setup wizard and the run preflight. The loop already showed the backend's error. When error_messages is non-empty, the headline now reads Not all AI models could be downloaded. The Plug-in Manager's 5-second /v1/models/assets poll picks up what landed.
  • Docs
    • Dev-Backend-API.md: the assets download section.
    • Troubleshooting.md: §3 resolution steps, and the intro now lists all five models.
    • server-rs/README.md, and the Dev-Server-README.md generated from it: the BioCLIP 404 note.
    • Getting-Started.md: main's text, about 4.2 GB.
    • Credits.md is still accurate (Docs-Reviewed trailer).

Testing

  • CI is green on 3292f52, the merged head: cargo-test (cargo test --workspace with LRG_REQUIRE_GOLDENS=face), format-lint, format-lint-rust, docs-check and busted.
  • New unit tests, against a real HTTP server on a local port:
    • a family whose release 404s doesn't stop the others; the status names only that family
    • a family whose transfer breaks part-way is skipped, and total shrinks
    • every family landing ends completed
    • the summary wording
    • the single-model path stops at its first missing file with the unchanged message
    • sizes come from the announced length
  • Contract tests for the GET /v1/models/assets shape (five families, now including face_state), and for no error_messages without failures.
  • Locally: cargo fmt --check and cargo clippy --workspace --all-targets -- -D warnings are clean. stylua --check, luacheck (0/0) and busted (614) pass. scripts/check-docs.py exits 0.
  • Live, the debug binary against the real releases. Search and quality-scoring models were dummy files, so they counted as ready.
    • /v1/models/assets listed five families with missing_approx_bytes: 977000000.
    • The download chose exactly bioclip, face, face_state and skipped each one with its own reason.
    • The final error, as the plugin shows it:
      These AI models could not be downloaded:
      - Species identification: bioclip2_image_fp16.onnx not found at …/bioclip-assets-v1/bioclip2_image_fp16.onnx (HTTP 401 Unauthorized) — the release may not exist yet
      - Face detection: yunet_face_detection.onnx not found at …/face-assets-v1/yunet_face_detection.onnx (HTTP 401 Unauthorized) — the release may not exist yet
      - Eye and expression analysis: face_mesh_v2.onnx not found at …/face-state-assets-v1/face_mesh_v2.onnx (HTTP 404 Not Found) — the release may not exist yet
      
      The other AI models are downloaded and ready. Press "Download AI models" in the Plug-in Manager again later; it fetches only what is still missing. If it keeps failing, check that the computer the backend runs on can reach github.com, and that the backend can write to its model folder.
      
    • The 401s come from the test sandbox. Its proxy mediates github.com, and the release redirect lands on a pre-signed S3 URL that refuses HEAD (curl: HEAD → 401, ranged GET → 206). So no family could land there. The unit tests cover the successful path against a real server.

Notes

  • Follow-up, not in this PR: routes/llm.rs (the GGUF download) has the same content_length()-on-HEAD bug in its own loop, so its progress total is 0 as well.
  • Possible hardening, not done here: if real users ever see HTTP 401/403 … the release may not exist yet, resolve_sizes could fall back to a one-byte ranged GET, which pre-signed GET URLs accept. The download counts on the asset releases suggest HEAD works outside the sandbox, so I left it out.

🤖 Generated with Claude Code

https://claude.ai/code/session_01HaMMvtwbAwMWB9jcpdCc3T

"Download AI models" (POST /v1/models/assets/downloads) put the assets of
every missing model family into one download, and the first file whose
HEAD request failed ended it. So one unreachable release (a family whose
tag is not published yet, a failed workflow run, a blocked host) meant
nothing was downloaded at all, not even the search model for a new user.

The combined download now goes family by family. A family whose files
cannot be resolved or fetched is skipped, and the others still download.
The job still ends in `error` when any family failed: `error_messages`
holds one "<family name>: <reason>" per failed family, and `error` names
each of them and says what to do. The plugin shows that under "Not all AI
models could be downloaded". A `Family` enum is now the one place for
each family's id, name, size estimate and readiness, which
`GET /v1/models/assets` reads too (same JSON).

The single-family routes (CLIP, BioCLIP, AuraFace) share the split-out
HEAD pass and transfer, and keep their all-or-nothing behaviour and
messages.

Sizes now come from the Content-Length header. reqwest's content_length()
is the length of the body it received, which is 0 for a HEAD response
(seanmonstar/reqwest#1814), so every download here ran with a progress
total of 0 until it had finished.

Tested against a local fake release server: a missing family, a family
whose transfer breaks part-way, every family landing, and the single-model
path, plus contract tests for the /v1/models/assets shape.

Docs-Reviewed: Credits.md
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HaMMvtwbAwMWB9jcpdCc3T
From review:
- The summary's closing advice only named the network, but a family can
  also fail on the disk ("failed to create model directory", "failed
  writing"). It now names both: reaching github.com, and the backend
  being able to write to its model folder.
- Troubleshooting step 1 still said an installation with the search model
  downloads only the species one, which stopped being true when the
  quality and face models became part of the download.
- Step 2 claimed every model has an indicator in the Plug-in Manager. The
  quality model has none, so it now says that every model the dialog does
  not name is ready, and that "All AI models are ready" turns green once
  the last one is in.

Also puts the test module's imports in rustfmt's order: they were moved
with the fake release server and not re-sorted.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HaMMvtwbAwMWB9jcpdCc3T
…wnload

#471 added the `face_state` family ("Eye and expression analysis",
`face-state-assets-v1`) as one more `need_*` flag in the combined
download, which this branch replaced with per-family `Family` entries.
It becomes `Family::FaceState` here, with #471's size estimate, release tag
and assets, so a family that is not published yet (the case that started
this branch) is skipped and named while the others still download.

- routes/assets.rs: `Family::FaceState` with id `face_state`, name "Eye
  and expression analysis", `FACE_STATE_APPROX_BYTES`,
  `state.face_state.is_cached()` and `face_state_assets`.
- tests/contract.rs: the /v1/models/assets id list includes `face_state`.
- Getting-Started.md: main's text (about 4.2 GB, with the eye and
  expression model).
- Troubleshooting.md: the §3 intro names the fifth model.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HaMMvtwbAwMWB9jcpdCc3T
@bmachek
bmachek merged commit e603151 into main Nov 4, 2026
5 checks passed
@bmachek
bmachek deleted the claude/brave-curie-ck0twg branch November 4, 2026 14:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants