BigPizzaV3/CodexPlusPlus · error

上游没有返回可用模型

Error message

上游没有返回可用模型

What it means

fetch_relay_profile_model_ids (crates/codex-plus-core/src/model_catalog.rs:588-593) fetches the /models endpoint via fetch_models_from_source; when zero model ids are parsed out of the response it bails, using the status JSON's `message` when present (e.g. 'HTTP 401' or a reqwest error) and defaulting to '上游没有返回可用模型' when the fetch technically succeeded but parse_model_payload found nothing. So this error is the aggregate of 'fetch failed' and 'fetch succeeded but list is empty'.

Source

Thrown at crates/codex-plus-core/src/model_catalog.rs:593

        base_url: if profile.upstream_base_url.trim().is_empty() {
            profile.base_url.trim().to_string()
        } else {
            profile.upstream_base_url.trim().to_string()
        },
        api_key: profile.api_key.trim().to_string(),
    };
    if source.base_url.is_empty() {
        anyhow::bail!("Base URL 不能为空");
    }
    let endpoint = models_endpoint(&source.base_url);
    let client = crate::http_client::proxied_client(&profile.user_agent)?;
    let (models, status) = fetch_models_from_source(&client, &source).await;
    if models.is_empty() {
        let message = status
            .get("message")
            .and_then(Value::as_str)
            .unwrap_or("上游没有返回可用模型");
        anyhow::bail!("{message}");
    }
    Ok((models, endpoint))
}

fn preferred_responses_api_status(sources: &[Value]) -> Value {
    let statuses = sources
        .iter()
        .filter_map(|source| source.get("responses_api"))
        .collect::<Vec<_>>();
    for wanted in ["unsupported", "supported", "failed"] {
        if let Some(status) = statuses
            .iter()
            .find(|status| status.get("status").and_then(Value::as_str) == Some(wanted))
        {
            return (*status).clone();
        }
    }
    responses_api_status("unknown", "", "")

View on GitHub (pinned to 1f431ae49b)

Solutions

  1. curl the endpoint yourself: `curl -H 'Authorization: Bearer <key>' <base_url>/models` and check status plus JSON shape — fix the URL (add /v1) or key accordingly
  2. Verify api_key is set on the profile: a blank key sends no Authorization header and many relays answer 401 (surfaced as message 'HTTP 401')
  3. Confirm the upstream implements the OpenAI-compatible models list (data[].id entries) — parse_model_payload only recognizes that shape; if not, populate the model list manually in the profile
  4. Check proxy environment variables (HTTP_PROXY/HTTPS_PROXY) that proxied_client honors and that may block loopback/upstream reachability
  5. Retry after upstream recovery — transient 5xx or empty lists surface identically

Example fix

// before
let profile = RelayProfile {
    base_url: "https://relay.example.com".into(), // website root, not API root
    ..Default::default()
};
fetch_relay_profile_model_ids(&profile).await?; // bail: 上游没有返回可用模型

// after
let profile = RelayProfile {
    base_url: "https://relay.example.com/v1".into(),
    api_key: load_key(),
    ..Default::default()
};
fetch_relay_profile_model_ids(&profile).await?;
Defensive patterns

Strategy: retry

Validate before calling

// Probe the models endpoint before wiring the profile into the catalog
async fn models_endpoint_ok(client: &reqwest::Client, base: &str, key: &str) -> bool {
    let url = format!("{}/models", base.trim_end_matches('/'));
    let mut req = client.get(&url).header("accept", "application/json");
    if !key.is_empty() { req = req.bearer_auth(key); }
    matches!(req.send().await, Ok(resp) if resp.status().is_success())
}

Type guard

fn profile_fetchable(profile: &RelayProfile) -> bool {
    relay_profile_has_base_url(profile) // non-empty URL is the precondition; key may be optional
}

Try / catch

match fetch_relay_profile_model_ids(&profile).await {
    Err(e) => {
        let msg = e.to_string();
        if msg.contains("HTTP 401") || msg.contains("HTTP 403") {
            surface_to_user("relay auth rejected; check api_key");
        } else if msg.contains("上游没有返回可用模型") || msg.contains("HTTP 4") {
            surface_to_user("endpoint returned no models; check base_url includes /v1 and supports /models");
        } else {
            retry_later(profile.id); // network-class failures: transient
        }
    }
    Ok((models, endpoint)) => { /* cache models */ }
}

Prevention

When it happens

Trigger: Refreshing a relay profile's model list when: the endpoint returns non-2xx (message becomes 'HTTP 401/404/...'), the connection fails (message becomes the reqwest error), or the response is 2xx but contains no recognizable model ids (unexpected JSON schema, empty data[] array, HTML login page parsed as JSON failure).

Common situations: Wrong or incomplete base URL (missing /v1, pointing at a website root); expired or missing api_key causing 401; relay providers that don't implement OpenAI-compatible GET /models; upstream temporarily empty or filtered; a proxy env var (proxied_client honors it) black-holing the request.

Related errors


AI-assisted analysis of BigPizzaV3/CodexPlusPlus@1f431ae49b (2026-08-16). Data as JSON: /api/errors/4a3a46aa0cbc9198. Report an issue: GitHub.