oxicode_ai/providers/event.rs
1//! Provider streaming events
2
3use std::sync::Arc;
4
5use crate::{AssistantMessage, StopReason, ToolCall};
6
7/// Streaming events emitted by providers
8///
9/// Note: We use crate::AssistantMessage directly to avoid type alias conflicts
10#[derive(Debug, Clone)]
11#[non_exhaustive]
12pub enum ProviderEvent {
13 /// Stream started with partial assistant message.
14 Start {
15 /// Partial assistant message state.
16 partial: Arc<AssistantMessage>,
17 },
18
19 /// Text content block started.
20 TextStart {
21 /// Index of the content block in the message.
22 content_index: usize,
23 /// Partial assistant message state.
24 partial: Arc<AssistantMessage>,
25 },
26
27 /// Incremental text delta received.
28 TextDelta {
29 /// Index of the content block in the message.
30 content_index: usize,
31 /// The text delta to append.
32 delta: String,
33 /// Partial assistant message state.
34 partial: Arc<AssistantMessage>,
35 },
36
37 /// Text content block finished.
38 TextEnd {
39 /// Index of the content block in the message.
40 content_index: usize,
41 /// The complete text content.
42 content: String,
43 /// Partial assistant message state.
44 partial: Arc<AssistantMessage>,
45 },
46
47 /// Thinking content block started.
48 ThinkingStart {
49 /// Index of the content block in the message.
50 content_index: usize,
51 /// Partial assistant message state.
52 partial: Arc<AssistantMessage>,
53 },
54
55 /// Incremental thinking delta received.
56 ThinkingDelta {
57 /// Index of the content block in the message.
58 content_index: usize,
59 /// The thinking text delta to append.
60 delta: String,
61 /// Partial assistant message state.
62 partial: Arc<AssistantMessage>,
63 },
64
65 /// Thinking content block finished.
66 ThinkingEnd {
67 /// Index of the content block in the message.
68 content_index: usize,
69 /// The complete thinking content.
70 content: String,
71 /// Partial assistant message state.
72 partial: Arc<AssistantMessage>,
73 },
74
75 /// Tool call block started.
76 ToolCallStart {
77 /// Index of the content block in the message.
78 content_index: usize,
79 /// The tool call ID from the provider, if available at start time.
80 /// Providers that only surface the ID later (in deltas/end) leave this `None`.
81 tool_call_id: Option<String>,
82 /// The tool name, if available at start time.
83 tool_name: Option<String>,
84 /// Partial assistant message state.
85 partial: Arc<AssistantMessage>,
86 },
87
88 /// Tool call delta received (partial JSON arguments).
89 ToolCallDelta {
90 /// Index of the content block in the message.
91 content_index: usize,
92 /// The delta string to append to tool arguments.
93 delta: String,
94 /// Partial assistant message state.
95 partial: Arc<AssistantMessage>,
96 },
97
98 /// Tool call block finished.
99 ToolCallEnd {
100 /// Index of the content block in the message.
101 content_index: usize,
102 /// The complete tool call with resolved arguments.
103 tool_call: ToolCall,
104 /// Partial assistant message state.
105 partial: Arc<AssistantMessage>,
106 },
107
108 /// Image content block started (incremental image streaming, e.g. Gemini).
109 ///
110 /// Mirrors omp's `AssistantMessageEvent::image_start`. Without this event
111 /// family, providers that stream images incrementally (Gemini) cannot
112 /// represent image output — see P0-D in the omp-realignment design.
113 ImageStart {
114 /// Index of the content block in the message.
115 content_index: usize,
116 /// MIME type of the incoming image (e.g. `image/png`).
117 mime_type: String,
118 /// Partial assistant message state.
119 partial: Arc<AssistantMessage>,
120 },
121
122 /// Incremental image delta received (base64-encoded chunk).
123 ImageDelta {
124 /// Index of the content block in the message.
125 content_index: usize,
126 /// Base64-encoded image bytes delta to append.
127 delta: String,
128 /// Partial assistant message state.
129 partial: Arc<AssistantMessage>,
130 },
131
132 /// Image content block finished.
133 ImageEnd {
134 /// Index of the content block in the message.
135 content_index: usize,
136 /// The complete base64-encoded image data.
137 data: String,
138 /// MIME type of the image.
139 mime_type: String,
140 /// Partial assistant message state.
141 partial: Arc<AssistantMessage>,
142 },
143
144 /// Stream completed successfully.
145 Done {
146 /// Why the model stopped generating.
147 reason: StopReason,
148 /// The final assistant message.
149 message: AssistantMessage,
150 },
151
152 /// Stream ended with an error.
153 Error {
154 /// The stop reason at time of error.
155 reason: StopReason,
156 /// Error details in assistant message form.
157 error: AssistantMessage,
158 },
159}
160
161impl ProviderEvent {
162 /// Extract the partial assistant message if present
163 pub fn partial(&self) -> Option<&AssistantMessage> {
164 match self {
165 ProviderEvent::Start { partial }
166 | ProviderEvent::TextStart { partial, .. }
167 | ProviderEvent::TextDelta { partial, .. }
168 | ProviderEvent::TextEnd { partial, .. }
169 | ProviderEvent::ThinkingStart { partial, .. }
170 | ProviderEvent::ThinkingDelta { partial, .. }
171 | ProviderEvent::ThinkingEnd { partial, .. }
172 | ProviderEvent::ToolCallStart { partial, .. }
173 | ProviderEvent::ToolCallDelta { partial, .. }
174 | ProviderEvent::ToolCallEnd { partial, .. }
175 | ProviderEvent::ImageStart { partial, .. }
176 | ProviderEvent::ImageDelta { partial, .. }
177 | ProviderEvent::ImageEnd { partial, .. } => Some(partial),
178 ProviderEvent::Done { message, .. } => Some(message),
179 ProviderEvent::Error { error, .. } => Some(error),
180 }
181 }
182
183 /// Check if this is a done event
184 pub fn is_done(&self) -> bool {
185 matches!(self, ProviderEvent::Done { .. })
186 }
187
188 /// Check if this is an error event
189 pub fn is_error(&self) -> bool {
190 matches!(self, ProviderEvent::Error { .. })
191 }
192}