{"record":{"id":"25119eec88c6ec2f","repo":"BigPizzaV3/CodexPlusPlus","slug":"chat-response-missing-choices","errorCode":null,"errorMessage":"chat response missing choices","messagePattern":"chat response missing choices","errorType":"http","errorClass":null,"httpStatus":502,"severity":"error","filePath":"crates/codex-plus-core/src/protocol_proxy.rs","lineNumber":240,"sourceCode":"}\n\npub fn chat_completion_to_response_with_request(\n    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,","sourceCodeStart":222,"sourceCodeEnd":258,"githubUrl":"https://github.com/BigPizzaV3/CodexPlusPlus/blob/1f431ae49b57b3055e0e6845ba6156c6b4232b4d/crates/codex-plus-core/src/protocol_proxy.rs#L222-L258","documentation":"Thrown by chat_completion_to_response_with_context in crates/codex-plus-core/src/protocol_proxy.rs while converting an upstream Chat Completions JSON body into the Responses API shape used by codex. The converter requires body.choices to exist and be an array; if choices is absent or not an array, conversion aborts. In practice the upstream returned a 2xx body that is not a Chat Completions response — most often an error envelope like {\"error\": ...}, a gateway HTML page, or a vendor-specific schema.","triggerScenarios":"Relay whose /chat/completions endpoint returns 200 with an error object instead of a completion; upstream gateway (Cloudflare, nginx) returning a JSON status body without choices; pointing the ChatCompletions relay at a Responses-only or legacy completions endpoint whose response schema has no choices array.","commonSituations":"Third-party API relays that wrap errors in 200 responses; base_url pointing at the wrong path so the upstream answers with a generic JSON document; upstream model overloaded and returning a non-standard body; version drift where the vendor renamed response fields.","solutions":["Log or dump the raw upstream body — the real problem (usually an embedded error message) is inside the JSON that lacks choices","Check the upstream HTTP status and body.error field before attempting chat_completion_to_response conversion","Verify the relay's base_url actually serves OpenAI-compatible /v1/chat/completions (curl it directly with the same key)","If the upstream only speaks the Responses API, switch the relay profile protocol to Responses instead of ChatCompletions"],"exampleFix":"// before\nlet response = chat_completion_to_response_with_request(body, &request)?;\n\n// after\nif body.get(\"choices\").and_then(Value::as_array).is_none() {\n    anyhow::bail!(\"upstream returned non-completion body: {}\", body);\n}\nlet response = chat_completion_to_response_with_request(body, &request)?;","handlingStrategy":"type-guard","validationCode":"fn upstream_ok_for_conversion(body: &Value) -> bool {\n    body.get(\"choices\").and_then(Value::as_array).is_some_and(|c| !c.is_empty())\n        && body.get(\"error\").is_none()\n}","typeGuard":"fn is_chat_completion_body(body: &Value) -> bool {\n    matches!(body.get(\"choices\"), Some(Value::Array(choices)) if !choices.is_empty()\n        && choices[0].get(\"message\").is_some())\n}","tryCatchPattern":"let response = chat_completion_to_response_with_request(body.clone(), &request)\n    .with_context(|| format!(\"upstream returned non-standard body: {body}\"))?;","preventionTips":["Check body.error and the choices array before converting","curl the relay's /v1/chat/completions once with a trivial prompt to confirm the schema before adding it as a ChatCompletions provider","Keep a failover member configured so one non-compliant upstream does not kill the request"],"tags":["chat-completions","responses-api","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"}