{"record":{"id":"ab4d95aba09870fb","repo":"BigPizzaV3/CodexPlusPlus","slug":"chat-response-choices-is-empty","errorCode":null,"errorMessage":"chat response choices is empty","messagePattern":"chat response choices is empty","errorType":"http","errorClass":null,"httpStatus":502,"severity":"error","filePath":"crates/codex-plus-core/src/protocol_proxy.rs","lineNumber":243,"sourceCode":"    body: Value,\n    original_request: &Value,\n) -> anyhow::Result<Value> {\n    let context = build_codex_tool_context(original_request.get(\"tools\"));\n    chat_completion_to_response_with_context(body, &context, Some(original_request))\n}\n\nfn chat_completion_to_response_with_context(\n    body: Value,\n    tool_context: &CodexToolContext,\n    original_request: Option<&Value>,\n) -> anyhow::Result<Value> {\n    let choices = body\n        .get(\"choices\")\n        .and_then(Value::as_array)\n        .ok_or_else(|| anyhow::anyhow!(\"chat response missing choices\"))?;\n    let choice = choices\n        .first()\n        .ok_or_else(|| anyhow::anyhow!(\"chat response choices is empty\"))?;\n    let message = choice\n        .get(\"message\")\n        .ok_or_else(|| anyhow::anyhow!(\"chat response choice missing message\"))?;\n\n    let response_id = response_id_from_chat_id(body.get(\"id\").and_then(Value::as_str));\n    let mut output = Vec::new();\n    if let Some(reasoning) = chat_reasoning_to_response_output_item(message, &response_id) {\n        output.push(reasoning);\n    }\n    if let Some(message) = chat_message_to_response_output_item(message, &response_id) {\n        output.push(message);\n    }\n    output.extend(chat_tool_calls_to_response_output_items(\n        message,\n        tool_context,\n    ));\n\n    let mut response = json!({","sourceCodeStart":225,"sourceCodeEnd":261,"githubUrl":"https://github.com/BigPizzaV3/CodexPlusPlus/blob/1f431ae49b57b3055e0e6845ba6156c6b4232b4d/crates/codex-plus-core/src/protocol_proxy.rs#L225-L261","documentation":"Thrown by chat_completion_to_response_with_context in crates/codex-plus-core/src/protocol_proxy.rs when the upstream Chat Completions body has a choices field and it is an array, but the array is empty (choices.first() returns None). The converter takes choices[0] to build the Responses output items, so an empty array means there is no completion to translate. Empty choices usually accompany load-shedding responses, content-filter short-circuits, or gateways that return 200 with an empty payload.","triggerScenarios":"Upstream returns {\"choices\": [], ...} under heavy load or rate limiting; a content moderation layer strips the only choice and leaves an empty array; a streaming-aggregating proxy emits a final aggregate object with empty choices.","commonSituations":"Overcrowded third-party relay pools returning empty completions; upstream returning empty choices when the prompt trips a filter; partial upstream outages where the gateway still answers 200.","solutions":["Inspect the full body — empty choices often comes with an error or usage object that names the real cause","Retry the request: empty choices from load balancers is frequently transient (the proxy already has failover candidates for this)","Add the upstream to the aggregate relay's member list so failover picks another provider when this one returns empty choices","If it reproduces deterministically for one prompt, test the same payload directly against the upstream to confirm it is a provider-side filter"],"exampleFix":"// before\nlet choice = choices.first().ok_or_else(|| anyhow::anyhow!(\"chat response choices is empty\"))?;\n\n// after: retry with failover instead of hard-failing\nlet Some(choice) = choices.first() else {\n    crate::relay_rotation::record_relay_request_failure(&settings);\n    continue; // try next candidate relay\n};","handlingStrategy":"retry","validationCode":"if matches!(body.get(\"choices\"), Some(Value::Array(a)) if a.is_empty()) {\n    // treat as transient upstream failure: rotate to next relay instead of converting\n}","typeGuard":"fn has_non_empty_choices(body: &Value) -> bool {\n    matches!(body.get(\"choices\"), Some(Value::Array(a)) if !a.is_empty())\n}","tryCatchPattern":"match chat_completion_to_response_with_request(body, &request) {\n    Ok(v) => v,\n    Err(e) if e.to_string().contains(\"choices is empty\") => retry_next_candidate().await?,\n    Err(e) => return Err(e),\n}","preventionTips":["Add multiple member relays to the aggregate so empty-choice responses fail over","Monitor diagnostic log protocol_proxy.upstream_response for members that frequently return empty choices and retire them"],"tags":["chat-completions","empty-response","upstream-schema","protocol-conversion"],"backgroundTag":"upstream-response-schema-invalid","analyzedSha":"1f431ae49b57b3055e0e6845ba6156c6b4232b4d","analyzedAt":"2026-08-16T20:54:18.598Z","schemaVersion":2},"datasetVersion":"2026-08-16T23:17:17.608Z"}