{"record":{"id":"77b9e05ed98a2715","repo":"astrid-runtime/astrid","slug":"models-endpoint-returned-no-usable-options","errorCode":null,"errorMessage":"models endpoint returned no usable options","messagePattern":"models endpoint returned no usable options","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"warning","filePath":"crates/astrid-cli/src/commands/capsule/model_discovery.rs","lineNumber":254,"sourceCode":"        response.status()\n    );\n\n    // Cap the body: the endpoint is operator-supplied and otherwise\n    // unbounded. Reject up-front on an advertised over-limit length so a\n    // hostile `Content-Length` can't even start a large transfer (fast\n    // path), then stream-read with the same bound so an absent/lying length\n    // (e.g. a chunked response with no `Content-Length`) cannot OOM the\n    // installer either. An over-limit response errors → free-text fallback.\n    anyhow::ensure!(\n        response\n            .content_length()\n            .is_none_or(|len| len <= MAX_RESPONSE_BYTES as u64),\n        \"models response too large (advertised {} bytes; limit {MAX_RESPONSE_BYTES})\",\n        response.content_length().unwrap_or_default()\n    );\n    let body = read_capped_body(response).await?;\n    let options = parse_options_response(&body, opts.select_or_default());\n    anyhow::ensure!(\n        !options.is_empty(),\n        \"models endpoint returned no usable options\"\n    );\n    Ok(options)\n}\n\n/// Stream the response body into memory under a hard [`MAX_RESPONSE_BYTES`]\n/// cap, then decode it as UTF-8.\n///\n/// The body is accumulated chunk-by-chunk and the running total is checked\n/// **before** each chunk is appended, so the buffer never exceeds the cap\n/// (the transfer is aborted the moment the next chunk would cross it). This\n/// is the streaming guard the up-front `Content-Length` check cannot\n/// provide: a chunked / unknown-length (or lying-`Content-Length`) response\n/// is bounded by the bytes actually read, not by an advisory header.\n///\n/// Rejects an over-cap body and any non-UTF-8 payload; both errors map to\n/// the free-text fallback at the call site.","sourceCodeStart":236,"sourceCodeEnd":272,"githubUrl":"https://github.com/astrid-runtime/astrid/blob/affd8760f44190dbdfbec23403f4c4b642c33112/crates/astrid-cli/src/commands/capsule/model_discovery.rs#L236-L272","documentation":"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.","triggerScenarios":"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.","commonSituations":"Server running but with no models loaded/registered; OpenAI-compatible server returning an unusual JSON schema; query params filtering out all models.","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"],"exampleFix":"// before: server returns {\"data\": []}\n// after: load models on the server, or respond with\n{\"data\": [{\"id\": \"llama3\"}]}","handlingStrategy":"fallback","validationCode":"let body: serde_json::Value = serde_json::from_slice(&resp_bytes)?;\nlet n = body.get(\"data\").and_then(|d| d.as_array()).map(|a| a.len()).unwrap_or(0);\nif n == 0 {\n    eprintln!(\"server reports no models; use manual input\");\n    return Ok(());\n}","typeGuard":null,"tryCatchPattern":"let options = match fetch_options(opts, values).await {\n    Ok(o) if !o.is_empty() => o,\n    _ => vec![prompt_free_text()?\n};","preventionTips":["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"],"tags":["empty-response","parsing","api"],"backgroundTag":"empty-api-response","analyzedSha":"affd8760f44190dbdfbec23403f4c4b642c33112","analyzedAt":"2026-09-09T21:28:12.402Z","contentChangedAt":"2026-09-09T21:28:12.402Z","schemaVersion":2},"datasetVersion":"2026-09-17T15:17:12.973Z"}