BigPizzaV3/CodexPlusPlus · error

chat response missing choices

Error message

chat response missing choices

What it means

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.

Source

Thrown at crates/codex-plus-core/src/protocol_proxy.rs:240

}

pub fn chat_completion_to_response_with_request(
    body: Value,
    original_request: &Value,
) -> anyhow::Result<Value> {
    let context = build_codex_tool_context(original_request.get("tools"));
    chat_completion_to_response_with_context(body, &context, Some(original_request))
}

fn chat_completion_to_response_with_context(
    body: Value,
    tool_context: &CodexToolContext,
    original_request: Option<&Value>,
) -> anyhow::Result<Value> {
    let choices = body
        .get("choices")
        .and_then(Value::as_array)
        .ok_or_else(|| anyhow::anyhow!("chat response missing choices"))?;
    let choice = choices
        .first()
        .ok_or_else(|| anyhow::anyhow!("chat response choices is empty"))?;
    let message = choice
        .get("message")
        .ok_or_else(|| anyhow::anyhow!("chat response choice missing message"))?;

    let response_id = response_id_from_chat_id(body.get("id").and_then(Value::as_str));
    let mut output = Vec::new();
    if let Some(reasoning) = chat_reasoning_to_response_output_item(message, &response_id) {
        output.push(reasoning);
    }
    if let Some(message) = chat_message_to_response_output_item(message, &response_id) {
        output.push(message);
    }
    output.extend(chat_tool_calls_to_response_output_items(
        message,
        tool_context,

View on GitHub (pinned to 1f431ae49b)

Solutions

  1. Log or dump the raw upstream body — the real problem (usually an embedded error message) is inside the JSON that lacks choices
  2. Check the upstream HTTP status and body.error field before attempting chat_completion_to_response conversion
  3. Verify the relay's base_url actually serves OpenAI-compatible /v1/chat/completions (curl it directly with the same key)
  4. If the upstream only speaks the Responses API, switch the relay profile protocol to Responses instead of ChatCompletions

Example fix

// before
let response = chat_completion_to_response_with_request(body, &request)?;

// after
if body.get("choices").and_then(Value::as_array).is_none() {
    anyhow::bail!("upstream returned non-completion body: {}", body);
}
let response = chat_completion_to_response_with_request(body, &request)?;
Defensive patterns

Strategy: type-guard

Validate before calling

fn upstream_ok_for_conversion(body: &Value) -> bool {
    body.get("choices").and_then(Value::as_array).is_some_and(|c| !c.is_empty())
        && body.get("error").is_none()
}

Type guard

fn is_chat_completion_body(body: &Value) -> bool {
    matches!(body.get("choices"), Some(Value::Array(choices)) if !choices.is_empty()
        && choices[0].get("message").is_some())
}

Try / catch

let response = chat_completion_to_response_with_request(body.clone(), &request)
    .with_context(|| format!("upstream returned non-standard body: {body}"))?;

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


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