Hmbown/CodeWhale · error · anyhow::Error
translate: unexpected API response shape
Error message
translate: unexpected API response shape
What it means
After the finish_reason check, the Chat Completions parser extracts choices[0].message.content as a string; if the field is missing or is not a string (e.g. null because the message contains only tool_calls or reasoning_content), it errors with "translate: unexpected API response shape". This is a strict parser invariant: the client expects a text-bearing completion message and refuses to guess from any other shape.
Solutions
- Log/inspect the raw response body to see the actual JSON shape the provider returned
- Ensure the request is a plain text completion (not a tool-use request) or handle tool-call messages on this path
- Check provider/gateway API version compatibility; some OpenAI-compatible servers put text in a different field
- If a proxy is in use, verify it forwards the standard choices[0].message.content field unchanged
Example fix
null
Defensive patterns
Strategy: try-catch
Type guard
fn message_has_text_content(resp: &serde_json::Value) -> bool {
resp["choices"][0]["message"]["content"].is_string()
&& !resp["choices"][0]["message"]["content"].as_str().unwrap_or("").trim().is_empty()
}
Try / catch
match result {
Err(e) if e.to_string().contains("unexpected API response shape") => {
// capture/log the raw response body for diagnosis; likely tool_call-only
// message or non-standard provider shape — handle tool_calls explicitly or
// flag the endpoint as incompatible
}
other => other?,
}
Prevention
- Inspect raw provider responses from any new OpenAI-compatible endpoint before wiring it in
- Avoid sending tool definitions on paths that expect plain text completions, or handle tool_call responses
- Pin and test against the provider's documented response schema version
- Prefer gateways that return the standard choices[0].message.content field
When it happens
Trigger: A Chat Completions reply where `choices[0].message.content` is null, absent, or non-string — typically a tool-call-only response, a reasoning-model message whose content is null with content elsewhere, or a non-standard provider response shape — that also passed the stop_reason check at client.rs:2889.
Common situations: Pointing the client at an OpenAI-compatible endpoint whose response schema deviates (proxies, gateways, local servers like llama.cpp/vLLM with differing response bodies); a model that emitted only tool calls; provider API version drift changing the response envelope.
Understand the failure class
Background: "invalid response format", "malformed payload", "missing data field": when an API returns 200 but the response shape is wrong — this error's family across 23 libraries.
Related errors
- Failed to call Chat Completions API: HTTP
- Gitee did not report a pull request URL; refusing to invent…
- Zen Chat Completions request should succeed
- Anthropic API error (HTTP )
- Anthropic stream error
AI-assisted analysis of Hmbown/CodeWhale@73e0f67d83 (2026-09-22).
Data as JSON: /api/errors/e912ee0d07a6c9ce.
Report an issue: GitHub.
Appendix: source
Thrown at crates/tui/src/client.rs:2896
"output_tokens",
"completion_tokens",
"total_tokens",
]
.iter()
.any(|field| usage.contains_key(*field))
});
let usage = parse_usage(value.get("usage"));
let stop_reason = value["choices"][0]["finish_reason"].as_str();
let usage = (usage_reported && usage != Usage::default()).then_some(usage);
let translated = if codewhale_models::is_incomplete_stop_reason(stop_reason) {
Err(anyhow::anyhow!(
"translate: provider response incomplete ({})",
codewhale_models::stop_reason_detail(stop_reason)
))
} else {
value["choices"][0]["message"]["content"]
.as_str()
.ok_or_else(|| anyhow::anyhow!("translate: unexpected API response shape"))
.and_then(|translated| {
let translated = translated.trim().to_string();
if translated.is_empty() {
bail!("translate: provider response did not contain text content");
}
Ok(translated)
})
};
Ok(TranslationProviderResponse {
translated,
route,
usage,
})
}
/// Test adapter for asserting the translated text; production retains receipts.
#[cfg(test)]View on GitHub (pinned to 73e0f67d83)