Summary
apr chat silently substitutes the built-in toy demo model for any sharded
SafeTensors model — with no error, no warning, and the banner still shows the
real model's path — instead of loading the actual model or refusing clearly.
This breaks the exact workflow apr pull itself recommends for any 3B+
SafeTensors model.
Repro
apr pull hf://Qwen/Qwen2.5-7B-Instruct
# ...
# ✓ Downloaded successfully
# Path: /home/alfredo/.apr/cache/hf/Qwen/Qwen2.5-7B-Instruct/model.safetensors.index.json
# Shards: 4
#
# Usage:
# apr run /home/.../model.safetensors.index.json
# apr serve /home/.../model.safetensors.index.json
apr chat /home/alfredo/.apr/cache/hf/Qwen/Qwen2.5-7B-Instruct/model.safetensors.index.json --gpu
Output:
=== Chat Demo (Tiny Model) ===
Note: Using tiny demo model. Pass .apr, .gguf, or .safetensors file for full model.
Model: /home/alfredo/.apr/cache/hf/Qwen/Qwen2.5-7B-Instruct/model.safetensors.index.json
Chat Template: Raw
...
Loading model...
Loaded Demo format in 0.00s (0.0 MB)
Warning: Could not detect chat template for 'demo', using raw format (no ChatML/Instruct wrapping)
You: hey
[0 tokens in 0.0s = 0.0 tok/s]
Assistant:
You:
No model was loaded (0.0 MB, 0 tokens generated). The banner still prints the
real model's path right above "Chat Demo (Tiny Model)", which makes it look
like the 7B model loaded and produced an empty/garbage response — it never
touched the real model at all.
Root cause
crates/apr-cli/src/commands/chat.rs::detect_format (line ~214):
fn detect_format(path: &Path) -> ModelFormat {
match path.extension().and_then(|e| e.to_str()) {
Some("apr") => ModelFormat::Apr,
Some("gguf") => ModelFormat::Gguf,
Some("safetensors") => ModelFormat::SafeTensors,
_ => ModelFormat::Demo,
}
}
Path::extension() on model.safetensors.index.json returns Some("json")
(the last dot-segment), which matches none of the three arms, so it falls
through to ModelFormat::Demo.
The secondary magic-byte detector, detect_format_from_bytes (same file,
#[cfg(feature = "inference")]), can't rescue this either: the file at that
path is a JSON index manifest, not tensor binary data, so its header-size
heuristic also can't recognize it as SafeTensors.
Compare to apr run's inference path
(crates/aprender-serve/src/infer/inference_result.rs::run_inference), which
DOES special-case this:
let path_str = config.model_path.to_string_lossy();
if path_str.ends_with(".safetensors.index.json") {
...
return run_sharded_safetensors_inference(config, &prepared);
}
apr chat has no equivalent branch anywhere in commands/chat.rs — no
ShardedSafeTensorsModel, no .index.json suffix check, nothing. Sharded
SafeTensors support exists in the codebase (crates/aprender-core/src/format/sharded_index.rs,
crates/aprender-serve/src/safetensors_infer.rs) but apr chat never wires
into it.
Impact
- Any model 3B+ pulled as SafeTensors (i.e. anything
apr pull shards, which
is apr pull's own default for models of that size) cannot be used with
apr chat at all — it silently runs the toy demo instead.
- No error is raised, so this fails quietly. A user unfamiliar with the
internals reasonably concludes their large model produced an empty/garbage
response, or that --gpu broke something, rather than realizing their real
model was never loaded.
apr pull's own printed instructions recommend apr run/apr serve for
this exact path but do NOT mention apr chat — so the fix might also be:
make apr chat on an unsupported format at minimum print the same
"Note: Using tiny demo model" Demo fallback loudly enough (e.g. as an
actual error:/refusal) that it can't be mistaken for real model output,
until the sharded-safetensors path is wired in.
Ask
- Wire
apr chat into the existing sharded-SafeTensors inference path
(ShardedSafeTensorsModel / run_sharded_safetensors_inference), the same
way apr run already is, so .safetensors.index.json loads the real model.
- Until that lands, make the
Demo fallback fail loudly instead of silently
when the input path exists and does not match any real extension — a
demo-model substitution for an existing, non-trivial file the user
explicitly pointed at should never look like a normal successful load.
Environment
apr 0.65.2 (b1a6324b8)
- Model:
Qwen/Qwen2.5-7B-Instruct, sharded SafeTensors (4 shards + index.json),
pulled via apr pull hf://Qwen/Qwen2.5-7B-Instruct
Summary
apr chatsilently substitutes the built-in toy demo model for any shardedSafeTensors model — with no error, no warning, and the banner still shows the
real model's path — instead of loading the actual model or refusing clearly.
This breaks the exact workflow
apr pullitself recommends for any 3B+SafeTensors model.
Repro
Output:
No model was loaded (0.0 MB, 0 tokens generated). The banner still prints the
real model's path right above "Chat Demo (Tiny Model)", which makes it look
like the 7B model loaded and produced an empty/garbage response — it never
touched the real model at all.
Root cause
crates/apr-cli/src/commands/chat.rs::detect_format(line ~214):Path::extension()onmodel.safetensors.index.jsonreturnsSome("json")(the last dot-segment), which matches none of the three arms, so it falls
through to
ModelFormat::Demo.The secondary magic-byte detector,
detect_format_from_bytes(same file,#[cfg(feature = "inference")]), can't rescue this either: the file at thatpath is a JSON index manifest, not tensor binary data, so its header-size
heuristic also can't recognize it as SafeTensors.
Compare to
apr run's inference path(
crates/aprender-serve/src/infer/inference_result.rs::run_inference), whichDOES special-case this:
apr chathas no equivalent branch anywhere incommands/chat.rs— noShardedSafeTensorsModel, no.index.jsonsuffix check, nothing. ShardedSafeTensors support exists in the codebase (
crates/aprender-core/src/format/sharded_index.rs,crates/aprender-serve/src/safetensors_infer.rs) butapr chatnever wiresinto it.
Impact
apr pullshards, whichis
apr pull's own default for models of that size) cannot be used withapr chatat all — it silently runs the toy demo instead.internals reasonably concludes their large model produced an empty/garbage
response, or that
--gpubroke something, rather than realizing their realmodel was never loaded.
apr pull's own printed instructions recommendapr run/apr serveforthis exact path but do NOT mention
apr chat— so the fix might also be:make
apr chaton an unsupported format at minimum print the same"Note: Using tiny demo model"
Demofallback loudly enough (e.g. as anactual
error:/refusal) that it can't be mistaken for real model output,until the sharded-safetensors path is wired in.
Ask
apr chatinto the existing sharded-SafeTensors inference path(
ShardedSafeTensorsModel/run_sharded_safetensors_inference), the sameway
apr runalready is, so.safetensors.index.jsonloads the real model.Demofallback fail loudly instead of silentlywhen the input path exists and does not match any real extension — a
demo-model substitution for an existing, non-trivial file the user
explicitly pointed at should never look like a normal successful load.
Environment
apr 0.65.2 (b1a6324b8)Qwen/Qwen2.5-7B-Instruct, sharded SafeTensors (4 shards + index.json),pulled via
apr pull hf://Qwen/Qwen2.5-7B-Instruct