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 #[serde(default, skip_serializing_if = "Option::is_none")]
51 runtime_context: Option<JsonValue>,
52 #[serde(default, skip_serializing_if = "Option::is_none")]
54 tools_context: Option<JsonValue>,
55 model: ModelIdentity,
57 request: StepRequest,
59 warnings: Vec<Warning>,
61 },
62 TextStart {
64 id: PartId,
66 #[serde(default, skip_serializing_if = "Option::is_none")]
68 provider_metadata: Option<ProviderMetadata>,
69 },
70 TextDelta {
72 id: PartId,
74 text: String,
76 #[serde(default, skip_serializing_if = "Option::is_none")]
78 provider_metadata: Option<ProviderMetadata>,
79 },
80 TextEnd {
82 id: PartId,
84 #[serde(default, skip_serializing_if = "Option::is_none")]
86 provider_metadata: Option<ProviderMetadata>,
87 },
88 ReasoningStart {
90 id: PartId,
92 #[serde(default, skip_serializing_if = "Option::is_none")]
94 provider_metadata: Option<ProviderMetadata>,
95 },
96 ReasoningDelta {
98 id: PartId,
100 text: String,
102 #[serde(default, skip_serializing_if = "Option::is_none")]
104 provider_metadata: Option<ProviderMetadata>,
105 },
106 ReasoningEnd {
108 id: PartId,
110 #[serde(default, skip_serializing_if = "Option::is_none")]
112 provider_metadata: Option<ProviderMetadata>,
113 },
114 ReasoningFile(GeneratedFile),
116 File(GeneratedFile),
118 Source(Source),
120 Custom {
122 kind: CustomKind,
124 #[serde(default, skip_serializing_if = "Option::is_none")]
126 provider_metadata: Option<ProviderMetadata>,
127 },
128 ToolInputStart {
130 id: ToolCallId,
132 tool_name: ToolName,
134 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
136 provider_executed: bool,
137 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
139 dynamic: bool,
140 #[serde(default, skip_serializing_if = "Option::is_none")]
142 title: Option<String>,
143 #[serde(default, skip_serializing_if = "Option::is_none")]
145 provider_metadata: Option<ProviderMetadata>,
146 },
147 ToolInputDelta {
149 id: ToolCallId,
151 delta: String,
153 #[serde(default, skip_serializing_if = "Option::is_none")]
155 provider_metadata: Option<ProviderMetadata>,
156 },
157 ToolInputEnd {
159 id: ToolCallId,
161 #[serde(default, skip_serializing_if = "Option::is_none")]
163 provider_metadata: Option<ProviderMetadata>,
164 },
165 ToolCall(ParsedToolCall),
167 ToolResult(ToolResult),
169 ToolError(ToolExecutionError),
171 ToolApprovalRequest(ToolApprovalRequestContent),
173 ToolApprovalResponse(ToolApprovalResponseContent),
175 ToolOutputDenied(ToolOutputDenied),
177 FinishStep {
179 step_number: u32,
181 finish_reason: FinishReason,
183 usage: Usage,
185 response: StepResponse,
187 #[serde(default, skip_serializing_if = "Option::is_none")]
189 provider_metadata: Option<ProviderMetadata>,
190 performance: StepPerformance,
192 },
193 Finish {
195 finish_reason: FinishReason,
197 total_usage: Usage,
199 },
200 Error {
202 error: StreamErrorInfo,
204 },
205 Abort,
207 RetryAttempt {
211 step_number: u32,
213 attempt: u32,
215 request: StepRequest,
217 warnings: Vec<Warning>,
219 },
220 Raw {
222 raw_value: JsonValue,
224 },
225}
226
227impl StreamEvent {
228 #[must_use]
230 pub fn kind_name(&self) -> &'static str {
231 match self {
232 Self::Start { .. } => "start",
233 Self::StartStep { .. } => "start-step",
234 Self::TextStart { .. } => "text-start",
235 Self::TextDelta { .. } => "text-delta",
236 Self::TextEnd { .. } => "text-end",
237 Self::ReasoningStart { .. } => "reasoning-start",
238 Self::ReasoningDelta { .. } => "reasoning-delta",
239 Self::ReasoningEnd { .. } => "reasoning-end",
240 Self::ReasoningFile(_) => "reasoning-file",
241 Self::File(_) => "file",
242 Self::Source(_) => "source",
243 Self::Custom { .. } => "custom",
244 Self::ToolInputStart { .. } => "tool-input-start",
245 Self::ToolInputDelta { .. } => "tool-input-delta",
246 Self::ToolInputEnd { .. } => "tool-input-end",
247 Self::ToolCall(_) => "tool-call",
248 Self::ToolResult(_) => "tool-result",
249 Self::ToolError(_) => "tool-error",
250 Self::ToolApprovalRequest(_) => "tool-approval-request",
251 Self::ToolApprovalResponse(_) => "tool-approval-response",
252 Self::ToolOutputDenied(_) => "tool-output-denied",
253 Self::FinishStep { .. } => "finish-step",
254 Self::Finish { .. } => "finish",
255 Self::Error { .. } => "error",
256 Self::Abort => "abort",
257 Self::RetryAttempt { .. } => "retry-attempt",
258 Self::Raw { .. } => "raw",
259 }
260 }
261
262 #[must_use]
264 pub fn as_text_delta(&self) -> Option<&str> {
265 match self {
266 Self::TextDelta { text, .. } => Some(text),
267 _ => None,
268 }
269 }
270}
271
272#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
274pub struct StreamErrorInfo {
275 pub kind: ErrorKind,
277 pub message: String,
279 #[serde(default, skip_serializing_if = "Option::is_none")]
281 pub status_code: Option<u16>,
282 pub is_retryable: bool,
284 #[serde(default, skip_serializing_if = "Option::is_none")]
286 pub provider_data: Option<JsonValue>,
287}
288
289impl StreamErrorInfo {
290 #[must_use]
292 pub fn from_error(error: &Error) -> Self {
293 let provider_data = error
294 .as_provider()
295 .and_then(ferrin_spec::error::ProviderError::as_api_call)
296 .and_then(|api| api.data.clone());
297 Self {
298 kind: error.kind(),
299 message: error.to_string(),
300 status_code: error.status_code().map(|status| status.as_u16()),
301 is_retryable: error.is_retryable(),
302 provider_data,
303 }
304 }
305}
306
307impl From<&Error> for StreamErrorInfo {
308 fn from(error: &Error) -> Self {
309 Self::from_error(error)
310 }
311}