1use ferrin_spec::CustomKind;
4use ferrin_spec::FinishReason;
5use ferrin_spec::JsonValue;
6use ferrin_spec::PartId;
7use ferrin_spec::ProviderMetadata;
8use ferrin_spec::ToolCallId;
9use ferrin_spec::ToolName;
10use ferrin_spec::Usage;
11use ferrin_spec::Warning;
12use ferrin_spec::language_model::Source;
13use serde::Deserialize;
14use serde::Serialize;
15
16use crate::error::Error;
17use crate::error::ErrorKind;
18use crate::generate_text::GeneratedFile;
19use crate::generate_text::ParsedToolCall;
20use crate::generate_text::StepPerformance;
21use crate::generate_text::StepRequest;
22use crate::generate_text::StepResponse;
23use crate::generate_text::ToolApprovalRequestContent;
24use crate::generate_text::ToolApprovalResponseContent;
25use crate::generate_text::ToolExecutionError;
26use crate::generate_text::ToolOutputDenied;
27use crate::generate_text::ToolResult;
28use crate::telemetry::ModelIdentity;
29
30#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
33#[serde(tag = "type", rename_all = "kebab-case")]
34#[non_exhaustive]
35#[allow(
36 clippy::large_enum_variant,
37 reason = "events flow through channels one at a time; flat variants keep pattern matching simple"
38)]
39pub enum StreamEvent {
40 Start {
42 call_id: String,
44 },
45 StartStep {
47 step_number: u32,
49 model: ModelIdentity,
51 request: StepRequest,
53 warnings: Vec<Warning>,
55 },
56 TextStart {
58 id: PartId,
60 #[serde(default, skip_serializing_if = "Option::is_none")]
62 provider_metadata: Option<ProviderMetadata>,
63 },
64 TextDelta {
66 id: PartId,
68 text: String,
70 #[serde(default, skip_serializing_if = "Option::is_none")]
72 provider_metadata: Option<ProviderMetadata>,
73 },
74 TextEnd {
76 id: PartId,
78 #[serde(default, skip_serializing_if = "Option::is_none")]
80 provider_metadata: Option<ProviderMetadata>,
81 },
82 ReasoningStart {
84 id: PartId,
86 #[serde(default, skip_serializing_if = "Option::is_none")]
88 provider_metadata: Option<ProviderMetadata>,
89 },
90 ReasoningDelta {
92 id: PartId,
94 text: String,
96 #[serde(default, skip_serializing_if = "Option::is_none")]
98 provider_metadata: Option<ProviderMetadata>,
99 },
100 ReasoningEnd {
102 id: PartId,
104 #[serde(default, skip_serializing_if = "Option::is_none")]
106 provider_metadata: Option<ProviderMetadata>,
107 },
108 ReasoningFile(GeneratedFile),
110 File(GeneratedFile),
112 Source(Source),
114 Custom {
116 kind: CustomKind,
118 #[serde(default, skip_serializing_if = "Option::is_none")]
120 provider_metadata: Option<ProviderMetadata>,
121 },
122 ToolInputStart {
124 id: ToolCallId,
126 tool_name: ToolName,
128 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
130 provider_executed: bool,
131 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
133 dynamic: bool,
134 #[serde(default, skip_serializing_if = "Option::is_none")]
136 title: Option<String>,
137 #[serde(default, skip_serializing_if = "Option::is_none")]
139 provider_metadata: Option<ProviderMetadata>,
140 },
141 ToolInputDelta {
143 id: ToolCallId,
145 delta: String,
147 #[serde(default, skip_serializing_if = "Option::is_none")]
149 provider_metadata: Option<ProviderMetadata>,
150 },
151 ToolInputEnd {
153 id: ToolCallId,
155 #[serde(default, skip_serializing_if = "Option::is_none")]
157 provider_metadata: Option<ProviderMetadata>,
158 },
159 ToolCall(ParsedToolCall),
161 ToolResult(ToolResult),
163 ToolError(ToolExecutionError),
165 ToolApprovalRequest(ToolApprovalRequestContent),
167 ToolApprovalResponse(ToolApprovalResponseContent),
169 ToolOutputDenied(ToolOutputDenied),
171 FinishStep {
173 step_number: u32,
175 finish_reason: FinishReason,
177 usage: Usage,
179 response: StepResponse,
181 #[serde(default, skip_serializing_if = "Option::is_none")]
183 provider_metadata: Option<ProviderMetadata>,
184 performance: StepPerformance,
186 },
187 Finish {
189 finish_reason: FinishReason,
191 total_usage: Usage,
193 },
194 Error {
196 error: StreamErrorInfo,
198 },
199 Abort,
201 RetryAttempt {
205 step_number: u32,
207 attempt: u32,
209 request: StepRequest,
211 warnings: Vec<Warning>,
213 },
214 Raw {
216 raw_value: JsonValue,
218 },
219}
220
221impl StreamEvent {
222 #[must_use]
224 pub fn kind_name(&self) -> &'static str {
225 match self {
226 Self::Start { .. } => "start",
227 Self::StartStep { .. } => "start-step",
228 Self::TextStart { .. } => "text-start",
229 Self::TextDelta { .. } => "text-delta",
230 Self::TextEnd { .. } => "text-end",
231 Self::ReasoningStart { .. } => "reasoning-start",
232 Self::ReasoningDelta { .. } => "reasoning-delta",
233 Self::ReasoningEnd { .. } => "reasoning-end",
234 Self::ReasoningFile(_) => "reasoning-file",
235 Self::File(_) => "file",
236 Self::Source(_) => "source",
237 Self::Custom { .. } => "custom",
238 Self::ToolInputStart { .. } => "tool-input-start",
239 Self::ToolInputDelta { .. } => "tool-input-delta",
240 Self::ToolInputEnd { .. } => "tool-input-end",
241 Self::ToolCall(_) => "tool-call",
242 Self::ToolResult(_) => "tool-result",
243 Self::ToolError(_) => "tool-error",
244 Self::ToolApprovalRequest(_) => "tool-approval-request",
245 Self::ToolApprovalResponse(_) => "tool-approval-response",
246 Self::ToolOutputDenied(_) => "tool-output-denied",
247 Self::FinishStep { .. } => "finish-step",
248 Self::Finish { .. } => "finish",
249 Self::Error { .. } => "error",
250 Self::Abort => "abort",
251 Self::RetryAttempt { .. } => "retry-attempt",
252 Self::Raw { .. } => "raw",
253 }
254 }
255
256 #[must_use]
258 pub fn as_text_delta(&self) -> Option<&str> {
259 match self {
260 Self::TextDelta { text, .. } => Some(text),
261 _ => None,
262 }
263 }
264}
265
266#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
268pub struct StreamErrorInfo {
269 pub kind: ErrorKind,
271 pub message: String,
273 #[serde(default, skip_serializing_if = "Option::is_none")]
275 pub status_code: Option<u16>,
276 pub is_retryable: bool,
278 #[serde(default, skip_serializing_if = "Option::is_none")]
280 pub provider_data: Option<JsonValue>,
281}
282
283impl StreamErrorInfo {
284 #[must_use]
286 pub fn from_error(error: &Error) -> Self {
287 let provider_data = error
288 .as_provider()
289 .and_then(ferrin_spec::error::ProviderError::as_api_call)
290 .and_then(|api| api.data.clone());
291 Self {
292 kind: error.kind(),
293 message: error.to_string(),
294 status_code: error.status_code().map(|status| status.as_u16()),
295 is_retryable: error.is_retryable(),
296 provider_data,
297 }
298 }
299}
300
301impl From<&Error> for StreamErrorInfo {
302 fn from(error: &Error) -> Self {
303 Self::from_error(error)
304 }
305}