use reqwest::StatusCode;
use serde::Deserialize;
use std::error::Error as StdError;
use thiserror::Error;
use tracing::warn;
use markhor_core::{chat::error::ChatError, embedding::EmbeddingError};
#[derive(Deserialize, Debug, Clone)]
pub struct GeminiErrorResponse {
pub error: GeminiErrorDetail,
}
#[derive(Deserialize, Debug, Clone)]
pub struct GeminiErrorDetail {
pub code: u16, pub message: String,
pub status: String,
}
#[derive(Error, Debug)]
pub enum GeminiError {
#[error("Network error: {0}")]
Network(#[from] reqwest::Error),
#[error("Failed to serialize request body: {0}")]
RequestSerialization(#[source] serde_json::Error),
#[error("Failed to parse successful response body ({context}): {source}")]
ResponseParsing {
context: String,
#[source]
source: serde_json::Error,
},
#[error("Gemini API error: status={status}, message='{body_text}'")]
ApiError {
status: StatusCode,
detail: Option<GeminiErrorDetail>,
body_text: String,
},
#[error("Invalid configuration: {0}")]
InvalidConfiguration(String),
#[error("Invalid input: {0}")]
InvalidInput(String),
#[error("Unexpected response format or data: {0}")]
UnexpectedResponse(String),
#[error("Streaming error: {0}")]
Streaming(String),
#[error("Input batch size too large (limit: {limit:?}, actual: {actual})")]
BatchTooLarge {
limit: Option<usize>,
actual: usize, },
}
pub(crate) async fn map_response_error(response: reqwest::Response) -> GeminiError {
let status = response.status();
debug_assert!(!status.is_success(), "map_response_error called with success status");
let body_text_result = response.text().await;
match body_text_result {
Ok(body_text) => {
match serde_json::from_str::<GeminiErrorResponse>(&body_text) {
Ok(parsed_error) => {
GeminiError::ApiError {
status,
detail: Some(parsed_error.error),
body_text,
}
}
Err(parse_err) => {
warn!(
status = %status,
error = %parse_err,
body = %body_text,
"Failed to parse Gemini error response JSON, returning raw body."
);
GeminiError::ApiError {
status,
detail: None, body_text,
}
}
}
}
Err(e) => {
warn!(
status = %status,
error = %e,
"Failed to read Gemini error response body text."
);
GeminiError::Network(e)
}
}
}
impl From<GeminiError> for ChatError {
fn from(err: GeminiError) -> Self {
match err {
GeminiError::Network(source) => {
if source.is_timeout() {
ChatError::Network(Box::new(source))
} else {
ChatError::Network(Box::new(source))
}
}
GeminiError::RequestSerialization(source) => {
ChatError::InvalidRequest(format!("Failed to serialize request: {}", source))
}
GeminiError::ResponseParsing { context, source } => {
ChatError::Parsing(Box::new(source)) }
GeminiError::ApiError { status, detail, body_text } => {
let message = detail
.map(|d| format!("{} (Status: {}, Code: {})", d.message, d.status, d.code))
.unwrap_or_else(|| body_text.clone());
match status {
StatusCode::BAD_REQUEST => ChatError::InvalidRequest(message), StatusCode::UNAUTHORIZED => ChatError::Authentication(message), StatusCode::FORBIDDEN => ChatError::Authentication(message), StatusCode::NOT_FOUND => ChatError::ModelNotFound(message), StatusCode::TOO_MANY_REQUESTS => ChatError::RateLimited, StatusCode::INTERNAL_SERVER_ERROR | StatusCode::BAD_GATEWAY | StatusCode::SERVICE_UNAVAILABLE | StatusCode::GATEWAY_TIMEOUT => ChatError::Api {
status: Some(status.as_u16()),
message,
source: None, },
_ => {
ChatError::Api {
status: Some(status.as_u16()),
message,
source: None,
}
}
}
}
GeminiError::InvalidConfiguration(msg) => ChatError::Configuration(msg),
GeminiError::InvalidInput(msg) => ChatError::InvalidRequest(msg), GeminiError::UnexpectedResponse(msg) => {
ChatError::Provider(msg.into()) }
GeminiError::Streaming(msg) => {
ChatError::Streaming(msg.into())
}
GeminiError::BatchTooLarge { .. } => {
ChatError::Provider(format!("Unexpected batch size error during chat operation: {}", err).into())
}
}
}
}
impl From<GeminiError> for EmbeddingError {
fn from(err: GeminiError) -> Self {
match err {
GeminiError::Network(source) => EmbeddingError::Network(Box::new(source)),
GeminiError::RequestSerialization(source) => {
EmbeddingError::Provider(Box::new(GeminiError::RequestSerialization(source)))
}
GeminiError::ResponseParsing { context, source } => {
EmbeddingError::Parsing(Box::new(source))
}
GeminiError::ApiError { status, detail, body_text } => {
let message = detail
.map(|d| format!("{} (Status: {}, Code: {})", d.message, d.status, d.code))
.unwrap_or_else(|| body_text.clone());
match status {
StatusCode::BAD_REQUEST => {
EmbeddingError::InvalidRequest(message)
}
StatusCode::UNAUTHORIZED => EmbeddingError::Authentication(message),
StatusCode::FORBIDDEN => EmbeddingError::Authentication(message),
StatusCode::NOT_FOUND => EmbeddingError::ModelNotFound(message),
StatusCode::TOO_MANY_REQUESTS => EmbeddingError::RateLimited,
StatusCode::INTERNAL_SERVER_ERROR
| StatusCode::BAD_GATEWAY
| StatusCode::SERVICE_UNAVAILABLE
| StatusCode::GATEWAY_TIMEOUT
=> EmbeddingError::Api {
status: Some(status.as_u16()),
message,
source: None,
},
_ => EmbeddingError::Api {
status: Some(status.as_u16()),
message,
source: None,
}
}
}
GeminiError::InvalidConfiguration(msg) => EmbeddingError::Configuration(msg),
GeminiError::InvalidInput(msg) => {
EmbeddingError::InvalidRequest(msg)
}
GeminiError::UnexpectedResponse(msg) => {
EmbeddingError::Provider(msg.into())
}
GeminiError::Streaming(_) => {
EmbeddingError::Provider(format!("Unexpected streaming error during embedding operation: {}", err).into())
}
GeminiError::BatchTooLarge { limit, actual } => {
EmbeddingError::BatchTooLarge { limit , actual }
}
}
}
}