BigPizzaV3/CodexPlusPlus · error
chat response choice missing message
Error message
chat response choice missing message
What it means
Thrown by chat_completion_to_response_with_context in crates/codex-plus-core/src/protocol_proxy.rs when choices[0] exists but has no message key. The converter reads choice.message to extract reasoning, content, and tool calls, so a choice without message (only delta, text, or vendor extensions) cannot be converted. Classic causes: a streaming chunk shape ({delta: ...}) fed to the non-streaming converter, or a legacy /v1/completions response whose choice carries text instead of message.
Source
Thrown at crates/codex-plus-core/src/protocol_proxy.rs:246
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!({
"id": response_id,
"object": "response",
"created_at": body.get("created").and_then(Value::as_u64).unwrap_or(0),View on GitHub (pinned to 1f431ae49b)
Solutions
- Confirm the request had stream:false / stream omitted, and that the upstream honors it — force-disable streaming on the relay if the provider always streams
- Point base_url at the chat completions endpoint, not the legacy completions endpoint (choices with text instead of message means wrong endpoint)
- Dump choices[0] and check whether it has delta (stream chunk) or text (legacy) to identify which mismatch you have
- If the upstream emits finish_reason content_filter with no message, adjust the prompt or switch provider
Example fix
// before
let message = choice.get("message").ok_or_else(|| anyhow::anyhow!("chat response choice missing message"))?;
// after: accept streaming chunk shape by falling back to delta
let message = choice
.get("message")
.or_else(|| choice.get("delta"))
.ok_or_else(|| anyhow::anyhow!("chat response choice missing message"))?; Defensive patterns
Strategy: type-guard
Validate before calling
fn choice_has_message(body: &Value) -> bool {
body.get("choices")
.and_then(Value::as_array)
.and_then(|c| c.first())
.map(|c| c.get("message").is_some())
.unwrap_or(false)
} Type guard
fn is_non_streaming_choice(body: &Value) -> bool {
matches!(
body.get("choices").and_then(Value::as_array).and_then(|c| c.first()),
Some(choice) if choice.get("message").is_some()
)
} Try / catch
if !is_non_streaming_choice(&body) {
anyhow::bail!("upstream sent a streaming/legacy choice shape: {}", body);
}
let response = chat_completion_to_response_with_request(body, &request)?; Prevention
- Ensure stream:false is sent and honored by the upstream for non-streaming calls
- Verify base_url targets /v1/chat/completions, not /v1/completions
- Inspect choices[0] keys (delta vs text vs message) when onboarding a new vendor
When it happens
Trigger: Upstream returns chat.completion.chunk objects (choices[0].delta, no message) for a non-stream request; base_url actually points at /v1/completions so choices contain text fields; vendor returns a choice with only finish_reason and no message when the model output was filtered.
Common situations: Misconfigured relay base_url mixing completions and chat/completions endpoints; upstream that forces stream:true regardless of the request; aggregating proxies forwarding SSE frames as JSON; upstream finishing with finish_reason=content_filter and omitting message.
Related errors
- chat response missing choices
- chat response choices is empty
- 当前中转未启用 Chat Completions 协议代理
- Chat Completions 上游 Base URL 不能为空
- Chat Completions 上游 Key 不能为空
AI-assisted analysis of BigPizzaV3/CodexPlusPlus@1f431ae49b (2026-08-16).
Data as JSON: /api/errors/954b3449fc8eb526.
Report an issue: GitHub.