use reqwest::StatusCode;
use serde::Deserialize;
use tracing::debug;
use super::client;
use crate::types::RunnerError;
pub(super) const PROVIDER_NAME: &str = "cohere";
#[derive(Debug, Deserialize)]
pub struct CohereErrorResponse {
#[serde(default)]
pub message: Option<String>,
}
const EMPTY_COMPLETION_MARKERS: [&str; 2] =
["no tool calls or response", "no valid response generated"];
pub fn parse_error_response(status: StatusCode, body: &str) -> RunnerError {
let parsed_message = serde_json::from_str::<CohereErrorResponse>(body)
.ok()
.and_then(|err| err.message)
.filter(|m| !m.is_empty());
let error_message = parsed_message.unwrap_or_else(|| {
debug!(
status = status.as_u16(),
body_preview = %body.chars().take(200).collect::<String>(),
"Cohere API returned non-JSON error response"
);
format!("HTTP {status}")
});
if matches!(status.as_u16(), 400 | 422) {
let lower = error_message.to_lowercase();
if EMPTY_COMPLETION_MARKERS.iter().any(|m| lower.contains(m)) {
return RunnerError::external_service(PROVIDER_NAME, error_message);
}
}
client::map_http_error(PROVIDER_NAME, status, &error_message)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::types::ErrorKind;
#[test]
fn empty_completion_422_is_the_providers_fault() {
let body = r#"{"message":"No tool calls or response was generated. Try updating messages or tool definitions"}"#;
let err = parse_error_response(StatusCode::UNPROCESSABLE_ENTITY, body);
assert_eq!(err.kind, ErrorKind::ExternalService);
assert!(
err.kind.is_provider_fault(),
"empty-completion 422 must cascade; got kind {:?}",
err.kind
);
}
#[test]
fn the_other_empty_completion_phrasing_also_cascades() {
let body = r#"{"message":"No valid response generated. Try updating messages"}"#;
let err = parse_error_response(StatusCode::BAD_REQUEST, body);
assert!(
err.kind.is_provider_fault(),
"this phrasing must cascade too; got kind {:?}",
err.kind
);
}
#[test]
fn genuine_validation_422_is_an_invalid_request() {
let body = r#"{"message":"invalid request: messages must not be empty"}"#;
let err = parse_error_response(StatusCode::UNPROCESSABLE_ENTITY, body);
assert_eq!(err.kind, ErrorKind::InvalidRequest);
assert!(
!err.kind.is_provider_fault(),
"a genuine validation 422 must not cascade; got kind {:?}",
err.kind
);
}
#[test]
fn maps_401_to_auth_failure() {
let body = r#"{"message":"invalid api token"}"#;
let err = parse_error_response(StatusCode::UNAUTHORIZED, body);
assert_eq!(err.kind, ErrorKind::AuthFailure);
assert!(err.message.contains("invalid api token"));
}
#[test]
fn maps_429_to_rate_limit() {
let body = r#"{"message":"too many requests"}"#;
let err = parse_error_response(StatusCode::TOO_MANY_REQUESTS, body);
assert_eq!(err.kind, ErrorKind::RateLimit);
}
#[test]
fn falls_back_to_the_status_on_an_unknown_body_shape() {
let body = "<html>nginx 502</html>";
let err = parse_error_response(StatusCode::BAD_GATEWAY, body);
assert_eq!(err.kind, ErrorKind::ExternalService);
assert!(err.message.contains("502"));
}
}