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}