{"record":{"id":"e912ee0d07a6c9ce","repo":"Hmbown/CodeWhale","slug":"translate-unexpected-api-response-shape","errorCode":null,"errorMessage":"translate: unexpected API response shape","messagePattern":"translate: unexpected API response shape","errorType":"exception","errorClass":"anyhow::Error","httpStatus":null,"severity":"error","filePath":"crates/tui/src/client.rs","lineNumber":2896,"sourceCode":"                    \"output_tokens\",\n                    \"completion_tokens\",\n                    \"total_tokens\",\n                ]\n                .iter()\n                .any(|field| usage.contains_key(*field))\n            });\n        let usage = parse_usage(value.get(\"usage\"));\n        let stop_reason = value[\"choices\"][0][\"finish_reason\"].as_str();\n        let usage = (usage_reported && usage != Usage::default()).then_some(usage);\n        let translated = if codewhale_models::is_incomplete_stop_reason(stop_reason) {\n            Err(anyhow::anyhow!(\n                \"translate: provider response incomplete ({})\",\n                codewhale_models::stop_reason_detail(stop_reason)\n            ))\n        } else {\n            value[\"choices\"][0][\"message\"][\"content\"]\n                .as_str()\n                .ok_or_else(|| anyhow::anyhow!(\"translate: unexpected API response shape\"))\n                .and_then(|translated| {\n                    let translated = translated.trim().to_string();\n                    if translated.is_empty() {\n                        bail!(\"translate: provider response did not contain text content\");\n                    }\n                    Ok(translated)\n                })\n        };\n\n        Ok(TranslationProviderResponse {\n            translated,\n            route,\n            usage,\n        })\n    }\n\n    /// Test adapter for asserting the translated text; production retains receipts.\n    #[cfg(test)]","sourceCodeStart":2878,"sourceCodeEnd":2914,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/73e0f67d83c59909b571efdfc88c4bc28c309cb1/crates/tui/src/client.rs#L2878-L2914","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"null","handlingStrategy":"try-catch","validationCode":null,"typeGuard":"fn message_has_text_content(resp: &serde_json::Value) -> bool {\n    resp[\"choices\"][0][\"message\"][\"content\"].is_string()\n        && !resp[\"choices\"][0][\"message\"][\"content\"].as_str().unwrap_or(\"\").trim().is_empty()\n}\n","tryCatchPattern":"match result {\n    Err(e) if e.to_string().contains(\"unexpected API response shape\") => {\n        // capture/log the raw response body for diagnosis; likely tool_call-only\n        // message or non-standard provider shape — handle tool_calls explicitly or\n        // flag the endpoint as incompatible\n    }\n    other => other?,\n}\n","preventionTips":["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"],"tags":["api","openai","response-parsing","chat-completions"],"backgroundTag":"unexpected-api-response-shape","analyzedSha":"73e0f67d83c59909b571efdfc88c4bc28c309cb1","analyzedAt":"2026-09-22T01:30:00.501Z","contentChangedAt":"2026-09-22T01:30:00.501Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}