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

  1. Log/inspect the raw response body to see the actual JSON shape the provider returned
  2. Ensure the request is a plain text completion (not a tool-use request) or handle tool-call messages on this path
  3. Check provider/gateway API version compatibility; some OpenAI-compatible servers put text in a different field
  4. 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

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


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)