Skip to main content

tea_model/
failure.rs

1use std::time::Duration;
2
3use tea_protocol::{ProtocolMetadata, RetryClass};
4
5use crate::ModelStreamValueError;
6
7/// Stable provider-neutral model failure classification.
8#[derive(Debug, Clone, Copy, PartialEq, Eq)]
9pub enum ModelFailureCode {
10    /// Request failed provider-neutral validation.
11    InvalidRequest,
12    /// Prompt and requested output exceed model context.
13    ContextOverflow,
14    /// Provider credentials are missing or invalid.
15    Authentication,
16    /// Credentials are valid but operation is not permitted.
17    PermissionDenied,
18    /// Provider rate limit rejected the operation.
19    RateLimited,
20    /// Provider or selected model is temporarily unavailable.
21    Unavailable,
22    /// Network or transport failed.
23    Transport,
24    /// Provider response could not be normalized safely.
25    MalformedResponse,
26    /// Operation was cooperatively cancelled.
27    Cancelled,
28    /// Unexpected adapter/runtime failure.
29    Internal,
30}
31
32impl ModelFailureCode {
33    /// All stable failure codes.
34    pub const ALL: [Self; 10] = [
35        Self::InvalidRequest,
36        Self::ContextOverflow,
37        Self::Authentication,
38        Self::PermissionDenied,
39        Self::RateLimited,
40        Self::Unavailable,
41        Self::Transport,
42        Self::MalformedResponse,
43        Self::Cancelled,
44        Self::Internal,
45    ];
46}
47
48/// Provider-neutral terminal model failure.
49#[derive(Debug, Clone, PartialEq)]
50pub struct ModelFailure {
51    code: ModelFailureCode,
52    message: String,
53    retry: RetryClass,
54    retry_after: Option<Duration>,
55    metadata: ProtocolMetadata,
56    safe_diagnostic: bool,
57}
58
59impl ModelFailure {
60    /// Creates a fixed internal adapter failure.
61    #[must_use]
62    pub fn internal_adapter_failure() -> Self {
63        Self {
64            code: ModelFailureCode::Internal,
65            message: "model adapter failed internally".to_owned(),
66            retry: RetryClass::Never,
67            retry_after: None,
68            metadata: ProtocolMetadata::default(),
69            safe_diagnostic: false,
70        }
71    }
72
73    /// Creates a bounded technical failure without an internal source chain.
74    ///
75    /// # Errors
76    ///
77    /// Returns an error when `message` is empty, exceeds 4 KiB, or contains a
78    /// null character.
79    pub fn new(
80        code: ModelFailureCode,
81        message: impl Into<String>,
82        retry: RetryClass,
83    ) -> Result<Self, ModelStreamValueError> {
84        let message = message.into();
85        if message.is_empty() || message.len() > 4096 || message.contains('\0') {
86            return Err(ModelStreamValueError::InvalidFailureMessage);
87        }
88        Ok(Self {
89            code,
90            message,
91            retry,
92            retry_after: None,
93            metadata: ProtocolMetadata::default(),
94            safe_diagnostic: false,
95        })
96    }
97
98    /// Creates a provider failure whose message was normalized for display.
99    ///
100    /// The caller must have removed provider payload fields, bounded the text,
101    /// and stripped terminal control characters before using this constructor.
102    ///
103    /// # Errors
104    ///
105    /// Returns an error when the normalized message violates the model failure
106    /// bounds.
107    pub fn safe(
108        code: ModelFailureCode,
109        message: impl Into<String>,
110        retry: RetryClass,
111    ) -> Result<Self, ModelStreamValueError> {
112        let mut failure = Self::new(code, message, retry)?;
113        failure.safe_diagnostic = true;
114        Ok(failure)
115    }
116
117    /// Adds bounded namespaced safe metadata.
118    #[must_use]
119    pub fn with_metadata(mut self, metadata: ProtocolMetadata) -> Self {
120        self.metadata = metadata;
121        self
122    }
123
124    /// Adds a provider-requested delay before this failure is retried.
125    #[must_use]
126    pub fn with_retry_after(mut self, retry_after: Duration) -> Self {
127        self.retry_after = Some(retry_after);
128        self
129    }
130
131    /// Returns the stable failure code.
132    #[must_use]
133    pub const fn code(&self) -> ModelFailureCode {
134        self.code
135    }
136
137    /// Returns the English technical message.
138    #[must_use]
139    pub fn message(&self) -> &str {
140        &self.message
141    }
142
143    /// Returns the retry classification.
144    #[must_use]
145    pub const fn retry(&self) -> RetryClass {
146        self.retry
147    }
148
149    /// Returns the provider-requested retry delay when one was supplied.
150    #[must_use]
151    pub const fn retry_after(&self) -> Option<Duration> {
152        self.retry_after
153    }
154
155    /// Returns safe namespaced metadata.
156    #[must_use]
157    pub const fn metadata(&self) -> &ProtocolMetadata {
158        &self.metadata
159    }
160
161    /// Returns whether the message is safe to expose as provider diagnostics.
162    #[must_use]
163    pub const fn is_safe_diagnostic(&self) -> bool {
164        self.safe_diagnostic
165    }
166}