BigPizzaV3/CodexPlusPlus · error

chat response choices is empty

Error message

chat response choices is empty

What it means

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.

Source

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

    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,
    ));

    let mut response = json!({

View on GitHub (pinned to 1f431ae49b)

Solutions

  1. Inspect the full body — empty choices often comes with an error or usage object that names the real cause
  2. Retry the request: empty choices from load balancers is frequently transient (the proxy already has failover candidates for this)
  3. Add the upstream to the aggregate relay's member list so failover picks another provider when this one returns empty choices
  4. If it reproduces deterministically for one prompt, test the same payload directly against the upstream to confirm it is a provider-side filter

Example fix

// before
let choice = choices.first().ok_or_else(|| anyhow::anyhow!("chat response choices is empty"))?;

// after: retry with failover instead of hard-failing
let Some(choice) = choices.first() else {
    crate::relay_rotation::record_relay_request_failure(&settings);
    continue; // try next candidate relay
};
Defensive patterns

Strategy: retry

Validate before calling

if matches!(body.get("choices"), Some(Value::Array(a)) if a.is_empty()) {
    // treat as transient upstream failure: rotate to next relay instead of converting
}

Type guard

fn has_non_empty_choices(body: &Value) -> bool {
    matches!(body.get("choices"), Some(Value::Array(a)) if !a.is_empty())
}

Try / catch

match chat_completion_to_response_with_request(body, &request) {
    Ok(v) => v,
    Err(e) if e.to_string().contains("choices is empty") => retry_next_candidate().await?,
    Err(e) => return Err(e),
}

Prevention

When it happens

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

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

Related errors


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