Skip to main content

edge_completions/
error.rs

1use std::time::Duration;
2
3use reqwest::StatusCode;
4
5/// Errors returned while configuring or calling the provider endpoint.
6#[derive(Debug, thiserror::Error)]
7#[non_exhaustive]
8pub enum Error {
9    /// A local configuration value violated its invariant.
10    #[error(transparent)]
11    InvalidConfiguration(#[from] InvalidConfiguration),
12
13    /// The HTTP exchange failed before a complete response body was received.
14    #[error("provider request failed before receiving a complete response: {source}")]
15    Transport {
16        /// The underlying HTTP client failure.
17        #[source]
18        source: reqwest::Error,
19    },
20
21    /// The configured request deadline elapsed.
22    #[error("provider request timed out after {duration:?}")]
23    Timeout {
24        /// Configured whole-request timeout.
25        duration: Duration,
26        /// The underlying HTTP client timeout.
27        #[source]
28        source: reqwest::Error,
29    },
30
31    /// The provider returned a non-success HTTP status.
32    #[error("provider returned HTTP {status}: {failure}")]
33    Provider {
34        /// The HTTP status returned by the provider.
35        status: StatusCode,
36        /// A sanitized, typed representation of the provider failure.
37        failure: ProviderFailure,
38    },
39
40    /// A success response did not match the supported typed contract.
41    #[error("provider returned an invalid chat-completion response: {source}")]
42    InvalidResponse {
43        /// The JSON decoding failure. The response body itself is not retained.
44        #[source]
45        source: serde_json::Error,
46    },
47
48    /// The response body exceeded the configured safety limit.
49    #[error("provider response exceeded the configured {limit_bytes}-byte limit")]
50    ResponseTooLarge {
51        /// Maximum number of response bytes accepted by this client.
52        limit_bytes: usize,
53    },
54
55    /// A completion did not contain any choices.
56    #[error("provider returned a chat completion without any choices")]
57    MissingChoice,
58
59    /// A tool-call completion did not contain a tool call.
60    #[error("provider returned a tool-call completion without a tool call")]
61    MissingToolCall,
62
63    /// A final completion did not contain text content.
64    #[error("provider returned a final completion without text content")]
65    MissingContent,
66
67    /// A typed tool contract was invalid or could not decode provider output.
68    #[error(transparent)]
69    Tool(#[from] ToolError),
70}
71
72/// A numeric error code returned by Cloudflare's API envelope.
73#[derive(Debug, Clone, Copy, PartialEq, Eq)]
74pub struct ProviderErrorCode(u64);
75
76impl ProviderErrorCode {
77    pub(crate) fn new(value: u64) -> Self {
78        Self(value)
79    }
80
81    /// Returns the provider's numeric code.
82    #[must_use]
83    pub fn value(self) -> u64 {
84        self.0
85    }
86}
87
88impl std::fmt::Display for ProviderErrorCode {
89    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
90        self.0.fmt(formatter)
91    }
92}
93
94/// A sanitized failure decoded from a non-success provider response.
95#[derive(Debug, thiserror::Error)]
96#[non_exhaustive]
97pub enum ProviderFailure {
98    /// Cloudflare returned both a stable numeric code and a message.
99    #[error("{message} (provider code {code})")]
100    Coded {
101        /// Cloudflare's numeric error code.
102        code: ProviderErrorCode,
103        /// Provider message truncated to a safe maximum length.
104        message: String,
105    },
106    /// An OpenAI-compatible error envelope contained a message.
107    #[error("{message}")]
108    Message {
109        /// Provider message truncated to a safe maximum length.
110        message: String,
111    },
112    /// The response did not match a supported error envelope.
113    #[error("provider returned an unrecognized error response")]
114    Unrecognized,
115}
116
117/// Failures raised while validating local SDK configuration.
118#[derive(Debug, Clone, PartialEq, thiserror::Error)]
119#[non_exhaustive]
120pub enum InvalidConfiguration {
121    /// A required environment variable was absent or not valid Unicode.
122    #[error("required environment variable {name} is missing or is not valid Unicode")]
123    MissingEnvironmentVariable {
124        /// Name of the required variable. Its value is never included.
125        name: &'static str,
126    },
127    /// The Cloudflare account identifier was empty.
128    #[error("Cloudflare account ID must not be empty")]
129    EmptyAccountId,
130    /// The Cloudflare API token was empty.
131    #[error("Cloudflare API token must not be empty")]
132    EmptyApiToken,
133    /// The provider model identifier was empty.
134    #[error("model ID must not be empty")]
135    EmptyModelId,
136    /// A chat request did not contain any messages.
137    #[error("chat request must contain at least one message")]
138    EmptyMessages,
139    /// A tool-enabled request did not contain any tool definitions.
140    #[error("a tool-enabled request must contain at least one tool definition")]
141    EmptyTools,
142    /// Sampling temperature was outside the supported range.
143    #[error("temperature must be finite and between 0 and 2 inclusive, got {value}")]
144    InvalidTemperature {
145        /// Rejected temperature.
146        value: f32,
147    },
148    /// The maximum completion-token count was zero.
149    #[error("maximum token count must be greater than zero")]
150    ZeroMaxTokens,
151    /// The request timeout was zero.
152    #[error("request timeout must be greater than zero")]
153    ZeroTimeout,
154    /// The response-body limit was zero.
155    #[error("response-body byte limit must be greater than zero")]
156    ZeroResponseSizeLimit,
157    /// The optional AI Gateway identifier could not be used as a header value.
158    #[error("Cloudflare AI Gateway ID is not a valid HTTP header value")]
159    InvalidGatewayId,
160    /// The API base URL was not a valid hierarchical URL.
161    #[error("provider API base URL is invalid: {reason}")]
162    InvalidBaseUrl {
163        /// Safe explanation of the violated URL invariant.
164        reason: String,
165    },
166    /// A non-TLS URL did not point to a loopback test server.
167    #[error("provider API base URL must use HTTPS unless it points to a loopback host")]
168    InsecureBaseUrl,
169}
170
171/// Failures while defining, decoding, or encoding a typed tool contract.
172#[derive(Debug, thiserror::Error)]
173#[non_exhaustive]
174pub enum ToolError {
175    /// A tool definition has an empty required field.
176    #[error("tool definition field {field} must not be empty")]
177    InvalidDefinition {
178        /// Name of the invalid definition field.
179        field: &'static str,
180    },
181
182    /// A model proposed a different tool from the typed contract requested by the caller.
183    #[error("expected tool {expected}, but the model requested {actual}")]
184    UnexpectedName {
185        /// Name declared by the typed tool contract.
186        expected: &'static str,
187        /// Name proposed by the model.
188        actual: String,
189    },
190
191    /// Model-produced JSON arguments failed typed deserialization or validation.
192    #[error("tool {tool} returned invalid arguments: {source}")]
193    InvalidArguments {
194        /// Tool name associated with the model proposal.
195        tool: String,
196        /// JSON or custom deserialization failure.
197        #[source]
198        source: serde_json::Error,
199    },
200
201    /// A typed application result could not be encoded for the follow-up request.
202    #[error("failed to encode the typed result for tool {tool}: {source}")]
203    ResultEncoding {
204        /// Tool name associated with the typed result.
205        tool: String,
206        /// JSON serialization failure.
207        #[source]
208        source: serde_json::Error,
209    },
210}