Skip to main content

oxi_ai/providers/
event.rs

1//! Provider streaming events
2
3use std::sync::Arc;
4
5use crate::{AssistantMessage, StopReason, ToolCall};
6
7/// Reason for a model fallback in MultiProvider.
8#[derive(Debug, Clone, PartialEq, Eq)]
9pub enum FallbackReason {
10    /// Rate limit exceeded (HTTP 429).
11    RateLimit,
12    /// Context window exceeded.
13    ContextOverflow,
14    /// Auth / quota error (HTTP 401/403).
15    AuthError,
16    /// Network error or connection failure.
17    NetworkError,
18    /// Server-side error (HTTP 5xx).
19    ServerError,
20    /// Model returned an error response.
21    ModelError,
22    /// Circuit breaker is open.
23    CircuitBreaker,
24    /// Unknown or custom reason.
25    Unknown,
26}
27
28impl FallbackReason {
29    /// Returns a string representation for serialization/logging.
30    pub fn as_str(&self) -> &'static str {
31        match self {
32            FallbackReason::RateLimit => "rate_limit",
33            FallbackReason::ContextOverflow => "context_overflow",
34            FallbackReason::AuthError => "auth_error",
35            FallbackReason::NetworkError => "network_error",
36            FallbackReason::ServerError => "server_error",
37            FallbackReason::ModelError => "model_error",
38            FallbackReason::CircuitBreaker => "circuit_breaker",
39            FallbackReason::Unknown => "unknown",
40        }
41    }
42}
43
44impl std::fmt::Display for FallbackReason {
45    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
46        write!(f, "{}", self.as_str())
47    }
48}
49
50/// Streaming events emitted by providers
51///
52/// Note: We use crate::AssistantMessage directly to avoid type alias conflicts
53#[derive(Debug, Clone)]
54#[non_exhaustive]
55pub enum ProviderEvent {
56    /// Stream started with partial assistant message.
57    Start {
58        /// Partial assistant message state.
59        partial: Arc<AssistantMessage>,
60    },
61
62    /// Text content block started.
63    TextStart {
64        /// Index of the content block in the message.
65        content_index: usize,
66        /// Partial assistant message state.
67        partial: Arc<AssistantMessage>,
68    },
69
70    /// Incremental text delta received.
71    TextDelta {
72        /// Index of the content block in the message.
73        content_index: usize,
74        /// The text delta to append.
75        delta: String,
76        /// Partial assistant message state.
77        partial: Arc<AssistantMessage>,
78    },
79
80    /// Text content block finished.
81    TextEnd {
82        /// Index of the content block in the message.
83        content_index: usize,
84        /// The complete text content.
85        content: String,
86        /// Partial assistant message state.
87        partial: Arc<AssistantMessage>,
88    },
89
90    /// Thinking content block started.
91    ThinkingStart {
92        /// Index of the content block in the message.
93        content_index: usize,
94        /// Partial assistant message state.
95        partial: Arc<AssistantMessage>,
96    },
97
98    /// Incremental thinking delta received.
99    ThinkingDelta {
100        /// Index of the content block in the message.
101        content_index: usize,
102        /// The thinking text delta to append.
103        delta: String,
104        /// Partial assistant message state.
105        partial: Arc<AssistantMessage>,
106    },
107
108    /// Thinking content block finished.
109    ThinkingEnd {
110        /// Index of the content block in the message.
111        content_index: usize,
112        /// The complete thinking content.
113        content: String,
114        /// Partial assistant message state.
115        partial: Arc<AssistantMessage>,
116    },
117
118    /// Tool call block started.
119    ToolCallStart {
120        /// Index of the content block in the message.
121        content_index: usize,
122        /// The tool call ID from the provider, if available at start time.
123        /// Providers that only surface the ID later (in deltas/end) leave this `None`.
124        tool_call_id: Option<String>,
125        /// The tool name, if available at start time.
126        tool_name: Option<String>,
127        /// Partial assistant message state.
128        partial: Arc<AssistantMessage>,
129    },
130
131    /// Tool call delta received (partial JSON arguments).
132    ToolCallDelta {
133        /// Index of the content block in the message.
134        content_index: usize,
135        /// The delta string to append to tool arguments.
136        delta: String,
137        /// Partial assistant message state.
138        partial: Arc<AssistantMessage>,
139    },
140
141    /// Tool call block finished.
142    ToolCallEnd {
143        /// Index of the content block in the message.
144        content_index: usize,
145        /// The complete tool call with resolved arguments.
146        tool_call: ToolCall,
147        /// Partial assistant message state.
148        partial: Arc<AssistantMessage>,
149    },
150
151    /// Stream completed successfully.
152    Done {
153        /// Why the model stopped generating.
154        reason: StopReason,
155        /// The final assistant message.
156        message: AssistantMessage,
157    },
158
159    /// Stream ended with an error.
160    Error {
161        /// The stop reason at time of error.
162        reason: StopReason,
163        /// Error details in assistant message form.
164        error: AssistantMessage,
165    },
166
167    // ── Routing / Fallback events ─────────────────────────────────────────
168    /// Model fallback occurred — primary model replaced by fallback.
169    ///
170    /// Emitted by `MultiProvider` when it switches from one model to another
171    /// in the candidate list due to errors, circuit breaker opens, etc.
172    FallbackStart {
173        /// Model that was being attempted.
174        from_model: String,
175        /// Model that will be used instead.
176        to_model: String,
177        /// Reason for the fallback.
178        reason: FallbackReason,
179    },
180
181    /// Fallback chain exhausted — all models failed.
182    ///
183    /// Emitted by `MultiProvider` when all candidates in the fallback chain
184    /// have been exhausted without success.
185    FallbackExhausted {
186        /// All models that were tried, in order.
187        models_tried: Vec<String>,
188        /// Final error from the last model.
189        final_error: String,
190    },
191}
192
193impl ProviderEvent {
194    /// Extract the partial assistant message if present
195    pub fn partial(&self) -> Option<&AssistantMessage> {
196        match self {
197            ProviderEvent::Start { partial }
198            | ProviderEvent::TextStart { partial, .. }
199            | ProviderEvent::TextDelta { partial, .. }
200            | ProviderEvent::TextEnd { partial, .. }
201            | ProviderEvent::ThinkingStart { partial, .. }
202            | ProviderEvent::ThinkingDelta { partial, .. }
203            | ProviderEvent::ThinkingEnd { partial, .. }
204            | ProviderEvent::ToolCallStart { partial, .. }
205            | ProviderEvent::ToolCallDelta { partial, .. }
206            | ProviderEvent::ToolCallEnd { partial, .. } => Some(partial),
207            ProviderEvent::Done { message, .. } => Some(message),
208            ProviderEvent::Error { error, .. } => Some(error),
209            _ => None,
210        }
211    }
212
213    /// Check if this is a done event
214    pub fn is_done(&self) -> bool {
215        matches!(self, ProviderEvent::Done { .. })
216    }
217
218    /// Check if this is an error event
219    pub fn is_error(&self) -> bool {
220        matches!(self, ProviderEvent::Error { .. })
221    }
222
223    /// Check if this is a fallback event
224    pub fn is_fallback(&self) -> bool {
225        matches!(self, ProviderEvent::FallbackStart { .. })
226    }
227
228    /// Check if this is a fallback exhausted event
229    pub fn is_fallback_exhausted(&self) -> bool {
230        matches!(self, ProviderEvent::FallbackExhausted { .. })
231    }
232}