astrid-runtime/astrid · warning

models endpoint returned no usable options

Error message

models endpoint returned no usable options

What it means

After fetching and parsing the models response, fetch_options requires at least one usable option and throws if the parsed list is empty. This triggers the caller's free-text fallback instead of showing an empty picker.

Solutions

  1. Verify the server actually exposes models (e.g. curl the endpoint and inspect the JSON)
  2. Fix the endpoint so it returns a recognized models-list format, or enter the model name manually via the free-text fallback
  3. Update capsule parsing config to match the server's response schema

Example fix

// before: server returns {"data": []}
// after: load models on the server, or respond with
{"data": [{"id": "llama3"}]}
Defensive patterns

Strategy: fallback

Validate before calling

let body: serde_json::Value = serde_json::from_slice(&resp_bytes)?;
let n = body.get("data").and_then(|d| d.as_array()).map(|a| a.len()).unwrap_or(0);
if n == 0 {
    eprintln!("server reports no models; use manual input");
    return Ok(());
}

Try / catch

let options = match fetch_options(opts, values).await {
    Ok(o) if !o.is_empty() => o,
    _ => vec![prompt_free_text()?
};

Prevention

When it happens

Trigger: The endpoint returns 200 but parse_options_response yields zero options — empty `data` array, JSON shape the parser doesn't recognize, or a filtered selection that drops everything.

Common situations: Server running but with no models loaded/registered; OpenAI-compatible server returning an unusual JSON schema; query params filtering out all models.

Understand the failure class

Background: "empty response", "returned no data", "empty embeddings": what HTTP 200-with-empty-body errors mean across libraries — this error's family across 36 libraries.

Related errors


AI-assisted analysis of astrid-runtime/astrid@affd8760f4 (2026-09-09). Data as JSON: /api/errors/77b9e05ed98a2715. Report an issue: GitHub.

Appendix: source

Thrown at crates/astrid-cli/src/commands/capsule/model_discovery.rs:254

        response.status()
    );

    // Cap the body: the endpoint is operator-supplied and otherwise
    // unbounded. Reject up-front on an advertised over-limit length so a
    // hostile `Content-Length` can't even start a large transfer (fast
    // path), then stream-read with the same bound so an absent/lying length
    // (e.g. a chunked response with no `Content-Length`) cannot OOM the
    // installer either. An over-limit response errors → free-text fallback.
    anyhow::ensure!(
        response
            .content_length()
            .is_none_or(|len| len <= MAX_RESPONSE_BYTES as u64),
        "models response too large (advertised {} bytes; limit {MAX_RESPONSE_BYTES})",
        response.content_length().unwrap_or_default()
    );
    let body = read_capped_body(response).await?;
    let options = parse_options_response(&body, opts.select_or_default());
    anyhow::ensure!(
        !options.is_empty(),
        "models endpoint returned no usable options"
    );
    Ok(options)
}

/// Stream the response body into memory under a hard [`MAX_RESPONSE_BYTES`]
/// cap, then decode it as UTF-8.
///
/// The body is accumulated chunk-by-chunk and the running total is checked
/// **before** each chunk is appended, so the buffer never exceeds the cap
/// (the transfer is aborted the moment the next chunk would cross it). This
/// is the streaming guard the up-front `Content-Length` check cannot
/// provide: a chunked / unknown-length (or lying-`Content-Length`) response
/// is bounded by the bytes actually read, not by an advisory header.
///
/// Rejects an over-cap body and any non-UTF-8 payload; both errors map to
/// the free-text fallback at the call site.

View on GitHub (pinned to affd8760f4)