use crate::adapter::AdapterKind;
use crate::chat::ChatRole;
use crate::{ModelIden, resolver, webc};
use derive_more::{Display, From};
use reqwest::StatusCode;
use reqwest::header::HeaderMap;
use value_ext::JsonValueExtError;
pub type BoxError = Box<dyn std::error::Error + Send + Sync>;
pub type Result<T> = core::result::Result<T, Error>;
#[derive(Debug, From, Display)]
#[allow(missing_docs)]
pub enum Error {
#[display("Chat Request has no messages. (for model {model_iden}")]
ChatReqHasNoMessages { model_iden: ModelIden },
#[display("Last chat request message is not of Role 'user' (Actual role '{actual_role}') for model '{model_iden}'")]
LastChatMessageIsNotUser {
model_iden: ModelIden,
actual_role: ChatRole,
},
#[display("Role '{role}' not supported for model '{model_iden}'")]
MessageRoleNotSupported { model_iden: ModelIden, role: ChatRole },
#[display("Content type not supported for model '{model_iden}'.\nCause: {cause}")]
MessageContentTypeNotSupported { model_iden: ModelIden, cause: &'static str },
#[display("JSON mode requested but no instruction/prompt provided.")]
JsonModeWithoutInstruction,
#[display("Failed to parse verbosity. Actual: '{actual}'")]
VerbosityParsing { actual: String },
#[display("Failed to parse reasoning. Actual: '{actual}'")]
ReasoningParsingError { actual: String },
#[display("Failed to parse service tier. Actual: '{actual}'")]
ServiceTierParsing { actual: String },
#[display("Failed to parse prompt cache retention. Actual: '{actual}'")]
PromptCacheRetentionParsing { actual: String },
#[display("No chat response from model '{model_iden}'")]
NoChatResponse { model_iden: ModelIden },
#[display("Invalid JSON response element: {info}")]
InvalidJsonResponseElement { info: &'static str },
#[display("Model '{model_iden}' requires an API key.")]
RequiresApiKey { model_iden: ModelIden },
#[display("No authentication resolver found for model '{model_iden}'.")]
NoAuthResolver { model_iden: ModelIden },
#[display("No authentication data available for model '{model_iden}'.")]
NoAuthData { model_iden: ModelIden },
#[display("Model mapping failed for '{model_iden}'.\nCause: {cause}")]
ModelMapperFailed {
model_iden: ModelIden,
cause: resolver::Error,
},
#[display("Web call failed for adapter '{adapter_kind}'.\nCause: {webc_error}")]
WebAdapterCall {
adapter_kind: AdapterKind,
webc_error: webc::Error,
},
#[display("Web call failed for model '{model_iden}'.\nCause: {webc_error}")]
WebModelCall {
model_iden: ModelIden,
webc_error: webc::Error,
},
#[display(
"Error while generating a ChatResponse from a ChatRequest. (for Model: '{model_iden}')
Request Payload:\n{request_payload:#}
Response Body:\n{response_body:#}
Cause:\n{cause}
"
)]
ChatResponseGeneration {
model_iden: ModelIden,
request_payload: Box<serde_json::Value>,
response_body: Box<serde_json::Value>,
cause: String,
},
#[display("Error event in stream for model '{model_iden}'. Body: {body}")]
ChatResponse {
model_iden: ModelIden,
body: serde_json::Value,
},
#[display("Failed to parse stream data for model '{model_iden}'.\nCause: {serde_error}")]
StreamParse {
model_iden: ModelIden,
serde_error: serde_json::Error,
},
#[display("Web stream error for model '{model_iden}'.\nCause: {cause}")]
WebStream {
model_iden: ModelIden,
cause: String,
error: BoxError,
},
#[display("HTTP error.\nStatus: {status} {canonical_reason}\nBody: {body}")]
HttpError {
status: StatusCode,
canonical_reason: String,
body: String,
headers: Box<HeaderMap>,
},
#[display("Resolver error for model '{model_iden}'.\nCause: {resolver_error}")]
Resolver {
model_iden: ModelIden,
resolver_error: resolver::Error,
},
#[display("Adapter '{adapter_kind}' does not support feature '{feature}'")]
AdapterNotSupported { adapter_kind: AdapterKind, feature: String },
#[display("Cache breakpoint requested for model '{model_iden}', but {scope} has no eligible OpenAI content block.")]
CacheBreakpointNoEligibleContent { model_iden: ModelIden, scope: &'static str },
#[display(
"Client is bound to adapter '{bound}' but model '{model}' resolved to adapter '{requested}'. \
A Client configured with `with_adapter_kind` targets a single provider — its \
AuthResolver and ServiceTargetResolver are gated on that adapter, so routing \
through a different one would silently drop auth and the configured endpoint. \
Drop the `::` namespace prefix or `ModelSpec::Iden`, or build a Client without \
`with_adapter_kind` for per-call routing."
)]
AdapterKindMismatch {
bound: AdapterKind,
requested: AdapterKind,
model: String,
},
#[display("Internal error: {_0}")]
Internal(String),
#[display("Failed to build client.\nCause: {cause}")]
ClientBuildFail { cause: String },
#[display("JSON value extension error: {_0}")]
#[from]
JsonValueExt(JsonValueExtError),
#[display("Serde JSON error: {_0}")]
#[from]
SerdeJson(serde_json::Error),
}
impl Error {
pub fn status(&self) -> Option<StatusCode> {
match self {
Error::HttpError { status, .. } => Some(*status),
Error::WebModelCall { webc_error, .. } | Error::WebAdapterCall { webc_error, .. } => webc_error.status(),
_ => None,
}
}
}
impl std::error::Error for Error {}
#[cfg(test)]
mod tests {
type Result<T> = core::result::Result<T, Box<dyn std::error::Error>>;
use super::*;
use reqwest::header::HeaderMap;
fn webc_status_error(status: u16) -> webc::Error {
webc::Error::ResponseFailedStatus {
status: StatusCode::from_u16(status).expect("valid status code"),
body: "body".to_string(),
headers: Box::new(HeaderMap::new()),
}
}
#[test]
fn test_error_status_from_web_model_call() -> Result<()> {
let error = Error::WebModelCall {
model_iden: ModelIden::new(AdapterKind::OpenAI, "gpt-4o"),
webc_error: webc_status_error(429),
};
assert_eq!(error.status(), Some(StatusCode::TOO_MANY_REQUESTS));
Ok(())
}
#[test]
fn test_error_status_from_web_adapter_call() -> Result<()> {
let error = Error::WebAdapterCall {
adapter_kind: AdapterKind::OpenAI,
webc_error: webc_status_error(503),
};
assert_eq!(error.status(), Some(StatusCode::SERVICE_UNAVAILABLE));
Ok(())
}
#[test]
fn test_error_status_from_http_error() -> Result<()> {
let error = Error::HttpError {
status: StatusCode::BAD_GATEWAY,
canonical_reason: "Bad Gateway".to_string(),
body: "body".to_string(),
headers: Box::new(HeaderMap::new()),
};
assert_eq!(error.status(), Some(StatusCode::BAD_GATEWAY));
Ok(())
}
#[test]
fn test_error_status_none_without_a_response() -> Result<()> {
let error = Error::NoAuthData {
model_iden: ModelIden::new(AdapterKind::OpenAI, "gpt-4o"),
};
assert_eq!(error.status(), None);
Ok(())
}
#[test]
fn test_webc_error_status_none_for_non_status_failures() -> Result<()> {
let error = webc::Error::ResponseFailedNotJson {
content_type: "text/html".to_string(),
body: "<html></html>".to_string(),
};
assert_eq!(error.status(), None);
Ok(())
}
}