Skip to content

apr chat silently loads the toy Demo model for sharded SafeTensors (.safetensors.index.json) instead of the real model #3022

Description

@alfredodeza

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

  1. 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.
  2. 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

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    P0Critical prioritybugSomething isn't workinginst:APP-066 instance claim (I14): inst:App-066PP-066 (0.66) DAG row

    Type

    No type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions