AlexsJones/llmfit · error

fit_to_json returns an object

Error message

fit_to_json returns an object

What it means

Panic from .expect("fit_to_json returns an object") in the CLI's fit_to_json wrapper (display.rs:782-800). It takes the serde_json::Value returned by the shared serializer crate::serve_shared::fit_to_json and calls as_object_mut() so it can overlay legacy keys (fit_level, run_mode, runtime, capabilities) that scripts depend on (see the #759 comment in-source). The expect encodes a cross-module API contract: the shared serializer must return a JSON object; if it ever returns an array, string, null, or a wrapped/versioned envelope, this layer panics.

Solutions

  1. Check git history / recent changes to serve_shared::fit_to_json — this expect only fires when that function stops returning an object.
  2. Restore the contract: keep serve_shared::fit_to_json returning a JSON object for every ModelFit, or update the CLI overlay in display.rs to handle the new shape (e.g. unwrap the versioned envelope before overlaying).
  3. Add a unit test asserting serve_shared::fit_to_json(&mock_fit).is_object() so CI catches shape drift.
  4. If you are consuming the CLI, switch to parsing `llmfit fit --json` output only, which exercises the shared serializer's stable contract.

Example fix

// before
let obj = value
    .as_object_mut()
    .expect("fit_to_json returns an object");

// after
let Some(obj) = value.as_object_mut() else {
    eprintln!("internal error: fit_to_json produced non-object JSON for {}", fit.model.name);
    std::process::exit(1);
};
Defensive patterns

Strategy: type-guard

Validate before calling

// Before calling any display_json_fits* helper, verify the shared serializer's contract.
let ok = fits.iter().all(|f| crate::serve_shared::fit_to_json(f).is_object());
if !ok {
    eprintln!("shared fit_to_json violated its object contract; skipping JSON output");
    return;
}
display::display_json_fits(specs, fits);

Type guard

fn shared_fit_json_is_object(fit: &llmfit_core::fit::ModelFit) -> bool {
    crate::serve_shared::fit_to_json(fit).is_object()
}

Try / catch

let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
    let _ = display::fit_to_json(fit); // overlay path that asserts object shape
}));
if result.is_err() {
    eprintln!("error: fit_to_json shape contract broken; this is a bug — please report it");
    std::process::exit(1);
}

Prevention

When it happens

Trigger: Any `llmfit fit --json`-family command after serve_shared::fit_to_json (llmfit-tui/src/serve_shared.rs) is changed to return a non-object Value — e.g. someone wraps the payload for API versioning as {"v2": {...}} or returns serde_json::Value::Null for an unknown model. The related bare .unwrap() in display_json_fits_with_llamacpp (display.rs:528) panics first for the llamacpp variant.

Common situations: Refactors that unify the API/MCP serializer and change its return shape without updating the CLI overlay; adding a null/error sentinel to fit_to_json for models with missing data; the two frontends drifting during parallel feature work.

Related errors


AI-assisted analysis of AlexsJones/llmfit@8f16394d74 (2026-08-17). Data as JSON: /api/errors/e7227f22d39ad9d8. Report an issue: GitHub.

Appendix: source

Thrown at llmfit-tui/src/display.rs:841

/// `fit.best_quant` for CLI display, cleared to an em dash when it's a
/// GGUF-style label that doesn't apply to a native NVFP4/MXFP4-named repo
/// (issue #969, problem 3). Mirrors `serve_shared::sanitized_best_quant`,
/// the JSON-facing version of the same check.
fn display_best_quant(fit: &ModelFit) -> &str {
    crate::serve_shared::sanitized_best_quant(fit).unwrap_or("\u{2014}")
}

/// CLI `fit --json` envelope: the shared serializer plus this frontend's legacy
/// overlays. The overlaid keys carry human-readable strings the API/MCP side
/// expresses as machine codes (with the human string under a `*_label` key);
/// the CLI's overloaded values are load-bearing for existing scripts, so they
/// stay put here until a future PR deprecates them (see #759).
fn fit_to_json(fit: &ModelFit) -> serde_json::Value {
    let mut value = crate::serve_shared::fit_to_json(fit);
    let obj = value
        .as_object_mut()
        .expect("fit_to_json returns an object");
    obj.insert("fit_level".to_string(), serde_json::json!(fit.fit_text()));
    obj.insert(
        "run_mode".to_string(),
        serde_json::json!(fit.run_mode_text()),
    );
    obj.insert("runtime".to_string(), serde_json::json!(fit.runtime_text()));
    obj.insert(
        "capabilities".to_string(),
        serde_json::json!(
            fit.model
                .capabilities
                .iter()
                .map(|c| c.label())
                .collect::<Vec<_>>()
        ),
    );
    value
}

View on GitHub (pinned to 8f16394d74)