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
- Verify the server actually exposes models (e.g. curl the endpoint and inspect the JSON)
- Fix the endpoint so it returns a recognized models-list format, or enter the model name manually via the free-text fallback
- 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
- Confirm the server has models loaded before running discovery
- Match the capsule's parser to the server's actual response schema
- Treat empty model lists as a normal case and always offer manual entry
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
- byte value must be non-negative and finite
- decode legacy log receipt
- empty byte specifier
- empty duration
- git emitted an invalid revision ID
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)