unicity-aos/aos-ce · warning
/v1/models discovery failed, advertising hardcoded catalog
Error message
/v1/models discovery failed, advertising hardcoded catalog: {e} What it means
capsule-openai's llm_describe first tries to discover the live model list via the provider's /v1/models endpoint. On any discovery failure (missing/blank API key, non-2xx status, non-JSON body, empty data, or network error) it logs this warning and falls back to advertising the full hardcoded model catalog instead of failing the describe call.
Solutions
- Set the API key in the capsule configuration/environment so discovery can authenticate.
- Verify network egress to the provider and that the base URL implements GET /v1/models.
- Check the {e} detail: non-2xx → inspect provider status/quota; non-JSON → wrong base URL or proxy page.
- Accept the fallback if acceptable — the capsule still advertises the hardcoded catalog and functions.
Example fix
// before
// config: openai.base_url = "https://internal-proxy/" (no /v1/models)
Err(e) => {
log::warn(format!("/v1/models discovery failed, advertising hardcoded catalog: {e}"));
models::build_provider_entries(&default_model, REQUEST_TOPIC, STREAM_TOPIC)
}
// after
// config: openai.base_url = "https://api.openai.com/v1", openai.api_key set
Err(e) => {
log::warn(format!("/v1/models discovery failed, advertising hardcoded catalog: {e}"));
models::build_provider_entries(&default_model, REQUEST_TOPIC, STREAM_TOPIC)
} Defensive patterns
Strategy: fallback
Validate before calling
// before calling describe, verify config
if (!config.apiKey || config.apiKey.trim() === '') throw new Error('missing api key');
const res = await fetch(config.baseUrl + '/v1/models', { headers: { Authorization: 'Bearer ' + config.apiKey } });
if (!res.ok) throw new Error('models endpoint non-2xx: ' + res.status); Type guard
fn discovery_usable(cfg: &Config) -> bool {
!cfg.api_key.trim().is_empty() && cfg.base_url.ends_with("/v1")
} Try / catch
let entries = match models::discover(&cfg).await {
Ok(live) => build_live_entries(&live, ...),
Err(e) => { log::warn("discovery failed: {e}"); build_provider_entries(...) }
}; Prevention
- Set and verify the API key in capsule config
- Point base_url at an endpoint implementing /v1/models
- Monitor discovery failures and alert on sustained fallback
When it happens
Trigger: The /v1/models discovery request fails inside llm_describe: API key missing or blank in config, the OpenAI-compatible endpoint returns non-2xx or non-JSON, the data array is empty, or a network error occurs.
Common situations: OPENAI_API_KEY not set or empty in the capsule environment; pointing the capsule at a proxy that doesn't implement /v1/models; network egress blocked from the capsule; provider outage.
Related errors
AI-assisted analysis of unicity-aos/aos-ce@f6f22024fb (2026-09-13).
Data as JSON: /api/errors/e625f8636631fe4b.
Report an issue: GitHub.
Appendix: source
Thrown at capsules/capsule-openai/src/lib.rs:100
/// fanned out to the caller under the new ABI.
#[astrid::interceptor("llm_describe")]
pub fn llm_describe(&self, _payload: serde_json::Value) -> Result<serde_json::Value, SysError> {
// The env `model` is the *default selection* hint, not the only usable
// model. We advertise the LIVE `/v1/models` catalogue (enriched from the
// capability table) when reachable, and fall back to the full hardcoded
// catalog otherwise so an offline/keyless install never regresses.
let default_model = env::var("model").unwrap_or_else(|_| DEFAULT_MODEL.into());
let entries = match Self::discover_models() {
Ok(live_ids) => {
// Live list is authority; enrich each id from the catalog and
// hoist (or prepend) the configured default so it is first.
models::build_live_entries(&live_ids, &default_model, REQUEST_TOPIC, STREAM_TOPIC)
}
Err(e) => {
// Any discovery failure (missing/blank key, non-2xx, non-JSON,
// empty data, network error) falls back to the full catalog.
log::warn(format!(
"/v1/models discovery failed, advertising hardcoded catalog: {e}"
));
models::build_provider_entries(&default_model, REQUEST_TOPIC, STREAM_TOPIC)
}
};
let response = serde_json::json!({ "providers": entries });
ipc::publish_json("llm.v1.response.describe", &response)?;
Ok(response)
}
}
impl OpenAIProvider {
/// Build the `Authorization` header value from a raw configured key.
///
/// Returns `Some("Bearer <trimmed>")` only when the key has non-whitespace
/// content; a missing, empty, or whitespace/newline-only key (common from a
/// copy-paste) is treated as **keyless** (`None`) so discovery never emitsView on GitHub (pinned to f6f22024fb)