{"record":{"id":"4a3a46aa0cbc9198","repo":"BigPizzaV3/CodexPlusPlus","slug":"error-4a3a46","errorCode":null,"errorMessage":"上游没有返回可用模型","messagePattern":"上游没有返回可用模型","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/codex-plus-core/src/model_catalog.rs","lineNumber":593,"sourceCode":"        base_url: if profile.upstream_base_url.trim().is_empty() {\n            profile.base_url.trim().to_string()\n        } else {\n            profile.upstream_base_url.trim().to_string()\n        },\n        api_key: profile.api_key.trim().to_string(),\n    };\n    if source.base_url.is_empty() {\n        anyhow::bail!(\"Base URL 不能为空\");\n    }\n    let endpoint = models_endpoint(&source.base_url);\n    let client = crate::http_client::proxied_client(&profile.user_agent)?;\n    let (models, status) = fetch_models_from_source(&client, &source).await;\n    if models.is_empty() {\n        let message = status\n            .get(\"message\")\n            .and_then(Value::as_str)\n            .unwrap_or(\"上游没有返回可用模型\");\n        anyhow::bail!(\"{message}\");\n    }\n    Ok((models, endpoint))\n}\n\nfn preferred_responses_api_status(sources: &[Value]) -> Value {\n    let statuses = sources\n        .iter()\n        .filter_map(|source| source.get(\"responses_api\"))\n        .collect::<Vec<_>>();\n    for wanted in [\"unsupported\", \"supported\", \"failed\"] {\n        if let Some(status) = statuses\n            .iter()\n            .find(|status| status.get(\"status\").and_then(Value::as_str) == Some(wanted))\n        {\n            return (*status).clone();\n        }\n    }\n    responses_api_status(\"unknown\", \"\", \"\")","sourceCodeStart":575,"sourceCodeEnd":611,"githubUrl":"https://github.com/BigPizzaV3/CodexPlusPlus/blob/1f431ae49b57b3055e0e6845ba6156c6b4232b4d/crates/codex-plus-core/src/model_catalog.rs#L575-L611","documentation":"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'.","triggerScenarios":"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).","commonSituations":"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.","solutions":["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","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')","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","Check proxy environment variables (HTTP_PROXY/HTTPS_PROXY) that proxied_client honors and that may block loopback/upstream reachability","Retry after upstream recovery — transient 5xx or empty lists surface identically"],"exampleFix":"// before\nlet profile = RelayProfile {\n    base_url: \"https://relay.example.com\".into(), // website root, not API root\n    ..Default::default()\n};\nfetch_relay_profile_model_ids(&profile).await?; // bail: 上游没有返回可用模型\n\n// after\nlet profile = RelayProfile {\n    base_url: \"https://relay.example.com/v1\".into(),\n    api_key: load_key(),\n    ..Default::default()\n};\nfetch_relay_profile_model_ids(&profile).await?;","handlingStrategy":"retry","validationCode":"// Probe the models endpoint before wiring the profile into the catalog\nasync fn models_endpoint_ok(client: &reqwest::Client, base: &str, key: &str) -> bool {\n    let url = format!(\"{}/models\", base.trim_end_matches('/'));\n    let mut req = client.get(&url).header(\"accept\", \"application/json\");\n    if !key.is_empty() { req = req.bearer_auth(key); }\n    matches!(req.send().await, Ok(resp) if resp.status().is_success())\n}","typeGuard":"fn profile_fetchable(profile: &RelayProfile) -> bool {\n    relay_profile_has_base_url(profile) // non-empty URL is the precondition; key may be optional\n}","tryCatchPattern":"match fetch_relay_profile_model_ids(&profile).await {\n    Err(e) => {\n        let msg = e.to_string();\n        if msg.contains(\"HTTP 401\") || msg.contains(\"HTTP 403\") {\n            surface_to_user(\"relay auth rejected; check api_key\");\n        } else if msg.contains(\"上游没有返回可用模型\") || msg.contains(\"HTTP 4\") {\n            surface_to_user(\"endpoint returned no models; check base_url includes /v1 and supports /models\");\n        } else {\n            retry_later(profile.id); // network-class failures: transient\n        }\n    }\n    Ok((models, endpoint)) => { /* cache models */ }\n}","preventionTips":["Store the API root (usually ending in /v1) as base_url and verify with curl before saving","Keep api_key current; blank keys omit Authorization and many relays return 401","Cache last-good model lists so transient upstream failures don't wipe the catalog","Check HTTP(S)_PROXY env vars the proxied client honors when requests fail"],"tags":["rust","relay-profile","model-catalog","upstream-api","http","empty-response"],"backgroundTag":"upstream-returned-no-models","analyzedSha":"1f431ae49b57b3055e0e6845ba6156c6b4232b4d","analyzedAt":"2026-08-16T20:54:18.598Z","schemaVersion":2},"datasetVersion":"2026-08-16T23:17:17.608Z"}