{"record":{"id":"323b3709251ae959","repo":"sigoden/aichat","slug":"no-valid-models","errorCode":null,"errorMessage":"No valid models","messagePattern":"No valid models","errorType":"exception","errorClass":"anyhow::Error","httpStatus":null,"severity":"error","filePath":"src/utils/request.rs","lineNumber":189,"sourceCode":"        Ok(ref client) => client,\n        Err(ref err) => bail!(\"{err}\"),\n    };\n    let mut builder = client.get(format!(\"{}/models\", api_base.trim_end_matches('/')));\n    if let Some(api_key) = api_key {\n        builder = builder.bearer_auth(api_key);\n    }\n    let res_body: Value = builder.send().await?.json().await?;\n    let mut result: Vec<String> = res_body\n        .get(\"data\")\n        .and_then(|v| v.as_array())\n        .map(|v| {\n            v.iter()\n                .filter_map(|v| v.get(\"id\").and_then(|v| v.as_str().map(|v| v.to_string())))\n                .collect()\n        })\n        .unwrap_or_default();\n    if result.is_empty() {\n        bail!(\"No valid models\")\n    }\n    result.sort_unstable();\n    Ok(result)\n}\n\n#[derive(Debug, Clone, Default)]\npub struct CrawlOptions {\n    extract: Option<String>,\n    exclude: Vec<String>,\n    no_log: bool,\n}\n\nimpl CrawlOptions {\n    pub fn preset(start_url: &str) -> CrawlOptions {\n        for (re, options) in PRESET.iter() {\n            if let Ok(true) = re.is_match(start_url) {\n                return options.clone();\n            }","sourceCodeStart":171,"sourceCodeEnd":207,"githubUrl":"https://github.com/sigoden/aichat/blob/82976d349ad97ac9aae0655ad631dace5e2a6385/src/utils/request.rs#L171-L207","documentation":"fetch_models calls the provider's {api_base}/models endpoint and extracts the 'id' fields from the 'data' array of the JSON response. If the extracted list is empty — because the response has no 'data' array, wrong shape, or zero entries — it throws 'No valid models'.","triggerScenarios":"Calling fetch_models when the /models response contains no data array of objects with string 'id' fields, or the array is empty.","commonSituations":"Wrong api_base pointing to a non-OpenAI-compatible server; missing/invalid API key so the server returns an error object instead of a model list; provider version change that renamed the response field; self-hosted gateway that returns an empty model list.","solutions":["Verify api_base points to an OpenAI-compatible endpoint exposing GET /models","Check that the API key is valid and has access to at least one model; test with curl -H \"Authorization: Bearer $KEY\" {api_base}/models","Inspect the raw JSON response shape and confirm it contains data[].id strings","Point at a different deployment/provider that actually serves models"],"exampleFix":"// before\nlet models = fetch_models(\"https://wrong-host.example/v1\", Some(\"sk-bad\"))?;\n// after\nlet models = fetch_models(\"https://api.openai.com/v1\", Some(\"sk-correct\"))?;","handlingStrategy":"validation","validationCode":"// preflight: call /models yourself and check shape\nlet v: serde_json::Value = client.get(format!(\"{base}/models\")).send().await?.json().await?;\nlet ok = v.get(\"data\").and_then(|d| d.as_array()).map(|a| !a.is_empty()).unwrap_or(false);\nif !ok { /* fix api_base/api_key before calling fetch_models */ }","typeGuard":"fn has_models(v: &serde_json::Value) -> bool {\n    v.get(\"data\").and_then(|d| d.as_array())\n     .map(|a| a.iter().any(|m| m.get(\"id\").and_then(|i| i.as_str()).is_some()))\n     .unwrap_or(false)\n}","tryCatchPattern":"match fetch_models(api_base, api_key).await {\n    Ok(models) if !models.is_empty() => models,\n    Ok(_) => anyhow::bail!(\"provider returned empty model list; check api_base/key\"),\n    Err(e) => return Err(e),\n}","preventionTips":["Verify api_base with curl -H \"Authorization: Bearer $KEY\" {base}/models before wiring it up","Confirm the server is OpenAI-compatible and returns data[].id","Use a scoped key with model-list permissions"],"tags":["api","models","empty-response"],"backgroundTag":"empty-result-set","analyzedSha":"82976d349ad97ac9aae0655ad631dace5e2a6385","analyzedAt":"2026-09-09T18:33:06.139Z","contentChangedAt":"2026-09-09T18:33:06.139Z","schemaVersion":2},"datasetVersion":"2026-09-16T09:17:16.951Z"}