use std::time::Duration;
use reqwest::StatusCode;
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum Error {
#[error(transparent)]
InvalidConfiguration(#[from] InvalidConfiguration),
#[error("provider request failed before receiving a complete response: {source}")]
Transport {
#[source]
source: reqwest::Error,
},
#[error("provider request timed out after {duration:?}")]
Timeout {
duration: Duration,
#[source]
source: reqwest::Error,
},
#[error("provider returned HTTP {status}: {failure}")]
Provider {
status: StatusCode,
failure: ProviderFailure,
},
#[error("provider returned an invalid chat-completion response: {source}")]
InvalidResponse {
#[source]
source: serde_json::Error,
},
#[error("provider response exceeded the configured {limit_bytes}-byte limit")]
ResponseTooLarge {
limit_bytes: usize,
},
#[error("provider returned a chat completion without any choices")]
MissingChoice,
#[error("provider returned a tool-call completion without a tool call")]
MissingToolCall,
#[error("provider returned a final completion without text content")]
MissingContent,
#[error(transparent)]
Tool(#[from] ToolError),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ProviderErrorCode(u64);
impl ProviderErrorCode {
pub(crate) fn new(value: u64) -> Self {
Self(value)
}
#[must_use]
pub fn value(self) -> u64 {
self.0
}
}
impl std::fmt::Display for ProviderErrorCode {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.0.fmt(formatter)
}
}
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum ProviderFailure {
#[error("{message} (provider code {code})")]
Coded {
code: ProviderErrorCode,
message: String,
},
#[error("{message}")]
Message {
message: String,
},
#[error("provider returned an unrecognized error response")]
Unrecognized,
}
#[derive(Debug, Clone, PartialEq, thiserror::Error)]
#[non_exhaustive]
pub enum InvalidConfiguration {
#[error("required environment variable {name} is missing or is not valid Unicode")]
MissingEnvironmentVariable {
name: &'static str,
},
#[error("Cloudflare account ID must not be empty")]
EmptyAccountId,
#[error("Cloudflare API token must not be empty")]
EmptyApiToken,
#[error("model ID must not be empty")]
EmptyModelId,
#[error("chat request must contain at least one message")]
EmptyMessages,
#[error("a tool-enabled request must contain at least one tool definition")]
EmptyTools,
#[error("temperature must be finite and between 0 and 2 inclusive, got {value}")]
InvalidTemperature {
value: f32,
},
#[error("maximum token count must be greater than zero")]
ZeroMaxTokens,
#[error("request timeout must be greater than zero")]
ZeroTimeout,
#[error("response-body byte limit must be greater than zero")]
ZeroResponseSizeLimit,
#[error("Cloudflare AI Gateway ID is not a valid HTTP header value")]
InvalidGatewayId,
#[error("provider API base URL is invalid: {reason}")]
InvalidBaseUrl {
reason: String,
},
#[error("provider API base URL must use HTTPS unless it points to a loopback host")]
InsecureBaseUrl,
}
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum ToolError {
#[error("tool definition field {field} must not be empty")]
InvalidDefinition {
field: &'static str,
},
#[error("expected tool {expected}, but the model requested {actual}")]
UnexpectedName {
expected: &'static str,
actual: String,
},
#[error("tool {tool} returned invalid arguments: {source}")]
InvalidArguments {
tool: String,
#[source]
source: serde_json::Error,
},
#[error("failed to encode the typed result for tool {tool}: {source}")]
ResultEncoding {
tool: String,
#[source]
source: serde_json::Error,
},
}