fix(chat): apr chat could not open a local model — every path with a slash became hf://<path> - #2413
fix(chat): apr chat could not open a local model — every path with a slash became hf://<path>#2413noahgift wants to merge 2 commits into
Conversation
…slash became hf://<path>
`apr chat /home/noah/models/qwen2.5-coder-0.5b-instruct.apr --offline` on the
crates.io 0.63.0 binary never looked at the file. It rewrote the absolute path
into a HuggingFace repo id and went to the network, which `--offline` did not
stop either:
$ apr chat /home/noah/models/qwen2.5-coder-0.5b-instruct.apr --offline
Downloading hf:///home...
Downloading model.safetensors...
error: Validation failed: Download failed: https://huggingface.co//home/resolve/main/model.safetensors: status code 404
rc=5
The same three fixtures (0.5B .apr, 1.5B q4k .apr, 0.8B .gguf) all produced
`Downloading hf:///home...`, and `apr run` opened all of them. Local models were
simply unreachable from `apr chat`.
Root cause: chat carried its own copy of the model-source resolution instead of
using run's. crates/apr-cli/src/commands/chat.rs:116 read
let hf_uri = if !resolved_source.contains("://") && resolved_source.contains('/') {
format!("hf://{resolved_source}")
with none of the local-path guards `run::resolve_model_source` has
(`!Path::new(..).exists()`, `!starts_with('/')`), so any argument containing a
slash became a repo id. chat.rs:126 then called
`resolve_model(&model_source, false, false)` with `offline` hard-coded to
`false`, which is why `--offline` could not stop the request.
chat now calls `run::resolve_model_source` and threads the real `--offline`
through to `resolve_model`, so a local path is a local path in both commands and
`--offline` refuses uncached repos locally.
The second half is in run.rs. `resolve_model_source` decided correctly that an
existing relative path like `models/tiny.gguf` was local, then handed it to
`pull::resolve_hf_model`, whose `normalize_hf_uri` re-prepends `hf://` to any
slash-bearing scheme-less argument — undoing the decision. That call is now made
only for arguments that are already `hf://` references. `apr run` had the same
defect for relative paths and is fixed by the same line.
After, with a release binary built from this branch:
$ apr chat /home/noah/models/Qwen3.5-0.8B-Q4_K_M.gguf --offline </dev/null
=== Model Chat (GGUF Format) ===
Model: /home/noah/models/Qwen3.5-0.8B-Q4_K_M.gguf
Loaded GGUF format in 0.27s (532.5 MB)
Loaded tokenizer with 248320 tokens
rc=0
$ apr chat /home/noah/models/qwen2.5-coder-1.5b-instruct-q4k.apr --offline </dev/null
Loaded APR format ... rc=0
$ apr chat /home/noah/models/qwen2.5-coder-0.5b-instruct.apr --offline </dev/null
Loaded APR format in 0.47s (991.9 MB)
error: Invalid APR format: No Qwen tokenizer found. ...
rc=4
The third still exits non-zero, but for an honest reason: the file is opened and
read, and that fixture has no sidecar tokenizer.json. Two adjacent behaviours are
also checked, because a resolver fix can easily overshoot: a nonexistent absolute
path still reports `File not found: /home/noah/models/does-not-exist.apr` (rc=3)
rather than a 404, and `--offline` on an uncached repo now prints
`OFFLINE MODE: Model hf://... not cached` (rc=5) with no request made.
Falsifiers in crates/apr-cli/src/commands/chat_local_path.rs assert behaviour,
not shape: an absolute path resolves to itself, a slash-bearing relative path
resolves to itself, and `--offline` on an uncached repo fails with OFFLINE MODE
without contacting huggingface.co.
Mutation check — restoring the pre-fix chat body (HEAD chat.rs:113-126) with the
tests kept turns all three RED:
panicked at chat_local_path.rs:28: an existing absolute path must resolve
without touching the network: ValidationFailed("Download failed:
https://huggingface.co//tmp/resolve/main/model.safetensors: status code 404")
panicked at chat_local_path.rs:59: an existing relative path must resolve
without touching the network: ValidationFailed("Download failed:
https://huggingface.co/apr-chat-rel-595528-ThreadId(20)/models/resolve/main/tiny.gguf: status code 401")
panicked at chat_local_path.rs:78: offline chat must refuse locally, got:
Validation failed: Download failed:
https://huggingface.co/apr-offline-falsifier-org/apr-offline-falsifier-repo/resolve/main/model.safetensors: status code 401
test result: FAILED. 16 passed; 3 failed
Reverting only the run.rs half (keeping the chat delegation) leaves the relative
path RED on its own, so both edits are load-bearing.
cargo test -p apr-cli --lib: 6625 passed, 0 failed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Pull request was converted to draft
Conflict in crates/apr-cli/src/commands/chat.rs. Both sides rewrote the same model-resolution block, and this one needed reading rather than picking a side. main (#2416) added a FileNotFound fast-fail for absolute / ./ / ../ paths that do not exist, then kept the old resolution: alias lookup followed by `format!("hf://{resolved_source}")` for anything containing a slash, and threaded `offline` into ModelSource::parse. this branch (#2387) replaced the whole block with `resolve_chat_model`. Took this branch's resolver, because it strictly supersedes main's block rather than merely conflicting with it: `resolve_chat_model` carries main's FileNotFound fast-fail verbatim, threads `offline` through both `resolve_model_source` and `resolve_model`, and drops the hf:// rewrite that is the defect this branch exists for. Keeping main's version would have restored `apr chat /path/to/model.apr` resolving to `hf:///path`. Confirmed no local from main's block is referenced after the conflict region - `fully_resolved_source`, `hf_uri`, `model_source`, `source_str` and `looks_like_path` are all consumed inside it. 250 apr-cli chat unit tests pass; build and fmt clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Parking to let the merge queue drain — branch untouched, this will be reopened. Six PRs are in the merge queue and their The queue is the only path by which anything actually merges, so it gets the runners until it is empty. Reopening immediately afterwards. |
Pull request was closed
|
Superseded by #2449 — this branch is merged verbatim into that batch. The binding constraint was one ~50-minute Closing rather than leaving open so this PR cannot move #2449's base and force it to re-run. The branch is untouched and this is reopenable if the batch does not land. |
Pull request was closed
apr chat /home/noah/models/qwen2.5-coder-0.5b-instruct.apr --offlineon thecrates.io 0.63.0 binary never looked at the file. It rewrote the absolute path
into a HuggingFace repo id and went to the network, which
--offlinedid notstop either:
The same three fixtures (0.5B .apr, 1.5B q4k .apr, 0.8B .gguf) all produced
Downloading hf:///home..., andapr runopened all of them. Local models weresimply unreachable from
apr chat.Root cause: chat carried its own copy of the model-source resolution instead of
using run's. crates/apr-cli/src/commands/chat.rs:116 read
with none of the local-path guards
run::resolve_model_sourcehas(
!Path::new(..).exists(),!starts_with('/')), so any argument containing aslash became a repo id. chat.rs:126 then called
resolve_model(&model_source, false, false)withofflinehard-coded tofalse, which is why--offlinecould not stop the request.chat now calls
run::resolve_model_sourceand threads the real--offlinethrough to
resolve_model, so a local path is a local path in both commands and--offlinerefuses uncached repos locally.The second half is in run.rs.
resolve_model_sourcedecided correctly that anexisting relative path like
models/tiny.ggufwas local, then handed it topull::resolve_hf_model, whosenormalize_hf_urire-prependshf://to anyslash-bearing scheme-less argument — undoing the decision. That call is now made
only for arguments that are already
hf://references.apr runhad the samedefect for relative paths and is fixed by the same line.
After, with a release binary built from this branch:
The third still exits non-zero, but for an honest reason: the file is opened and
read, and that fixture has no sidecar tokenizer.json. Two adjacent behaviours are
also checked, because a resolver fix can easily overshoot: a nonexistent absolute
path still reports
File not found: /home/noah/models/does-not-exist.apr(rc=3)rather than a 404, and
--offlineon an uncached repo now printsOFFLINE MODE: Model hf://... not cached(rc=5) with no request made.Falsifiers in crates/apr-cli/src/commands/chat_local_path.rs assert behaviour,
not shape: an absolute path resolves to itself, a slash-bearing relative path
resolves to itself, and
--offlineon an uncached repo fails with OFFLINE MODEwithout contacting huggingface.co.
Mutation check — restoring the pre-fix chat body (HEAD chat.rs:113-126) with the
tests kept turns all three RED:
Reverting only the run.rs half (keeping the chat delegation) leaves the relative
path RED on its own, so both edits are load-bearing.
cargo test -p apr-cli --lib: 6625 passed, 0 failed.
Co-Authored-By: Claude Opus 5 noreply@anthropic.com
🤖 Generated with Claude Code
Fixes #2387
Audit epic: #2373