Skip to main content

rtb_ai/
error.rs

1//! Typed errors for the AI client. Every `String` payload runs
2//! through [`rtb_redact::string`] so leaked URLs / tokens / headers
3//! in upstream provider errors never reach our telemetry.
4
5use std::time::Duration;
6
7use miette::Diagnostic;
8use thiserror::Error;
9
10/// Failures surfaced by [`crate::AiClient`]. Every `String` payload
11/// has been through [`rtb_redact::string`] before storage.
12#[derive(Debug, Clone, Error, Diagnostic)]
13#[non_exhaustive]
14pub enum AiError {
15    /// Bad config — invalid base URL, empty API key, unsupported
16    /// provider+model combination.
17    #[error("invalid AI client config: {0}")]
18    #[diagnostic(code(rtb::ai::config))]
19    InvalidConfig(String),
20
21    /// Provider returned an error response (4xx / 5xx that isn't a
22    /// rate-limit or auth issue).
23    #[error("provider error: {0}")]
24    #[diagnostic(code(rtb::ai::provider))]
25    Provider(String),
26
27    /// HTTP transport failure — DNS, TCP, TLS, body read interrupted.
28    #[error("HTTP transport: {0}")]
29    #[diagnostic(code(rtb::ai::transport))]
30    Transport(String),
31
32    /// `chat_structured` got a response that didn't validate against
33    /// the requested type's `JsonSchema`.
34    #[error("response did not validate against schema: {0}")]
35    #[diagnostic(code(rtb::ai::schema))]
36    SchemaValidation(String),
37
38    /// `chat_structured` got JSON that validated against the schema
39    /// but failed `serde::Deserialize` for the target type.
40    #[error("response was not valid JSON for the requested type: {0}")]
41    #[diagnostic(code(rtb::ai::deserialize))]
42    Deserialize(String),
43
44    /// Provider rejected the request as unauthenticated or expired.
45    #[error("authentication failed: {0}")]
46    #[diagnostic(code(rtb::ai::auth))]
47    Auth(String),
48
49    /// Provider rate-limited us. `retry_after` is populated when the
50    /// `Retry-After` header is parseable.
51    #[error("rate limited by {host} (retry-after: {retry_after:?})")]
52    #[diagnostic(code(rtb::ai::rate_limited))]
53    RateLimited {
54        /// Host that returned the rate-limit response.
55        host: String,
56        /// Server-suggested wait, when present.
57        retry_after: Option<Duration>,
58    },
59}
60
61/// Sanitise a free-form provider error payload before it lands in an
62/// `AiError`. Centralised so every error site goes through the same
63/// redactor.
64pub(crate) fn redact(input: &str) -> String {
65    rtb_redact::string(input).into_owned()
66}