use ferrin_spec::CustomKind;
use ferrin_spec::FinishReason;
use ferrin_spec::JsonValue;
use ferrin_spec::PartId;
use ferrin_spec::ProviderMetadata;
use ferrin_spec::ToolCallId;
use ferrin_spec::ToolName;
use ferrin_spec::Usage;
use ferrin_spec::Warning;
use ferrin_spec::language_model::Source;
use serde::Deserialize;
use serde::Serialize;
use crate::error::Error;
use crate::error::ErrorKind;
use crate::generate_text::GeneratedFile;
use crate::generate_text::ParsedToolCall;
use crate::generate_text::StepPerformance;
use crate::generate_text::StepRequest;
use crate::generate_text::StepResponse;
use crate::generate_text::ToolApprovalRequestContent;
use crate::generate_text::ToolApprovalResponseContent;
use crate::generate_text::ToolExecutionError;
use crate::generate_text::ToolOutputDenied;
use crate::generate_text::ToolResult;
use crate::telemetry::ModelIdentity;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "kebab-case")]
#[non_exhaustive]
#[allow(
clippy::large_enum_variant,
reason = "events flow through channels one at a time; flat variants keep pattern matching simple"
)]
pub enum StreamEvent {
Start {
call_id: String,
},
StartStep {
step_number: u32,
model: ModelIdentity,
request: StepRequest,
warnings: Vec<Warning>,
},
TextStart {
id: PartId,
#[serde(default, skip_serializing_if = "Option::is_none")]
provider_metadata: Option<ProviderMetadata>,
},
TextDelta {
id: PartId,
text: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
provider_metadata: Option<ProviderMetadata>,
},
TextEnd {
id: PartId,
#[serde(default, skip_serializing_if = "Option::is_none")]
provider_metadata: Option<ProviderMetadata>,
},
ReasoningStart {
id: PartId,
#[serde(default, skip_serializing_if = "Option::is_none")]
provider_metadata: Option<ProviderMetadata>,
},
ReasoningDelta {
id: PartId,
text: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
provider_metadata: Option<ProviderMetadata>,
},
ReasoningEnd {
id: PartId,
#[serde(default, skip_serializing_if = "Option::is_none")]
provider_metadata: Option<ProviderMetadata>,
},
ReasoningFile(GeneratedFile),
File(GeneratedFile),
Source(Source),
Custom {
kind: CustomKind,
#[serde(default, skip_serializing_if = "Option::is_none")]
provider_metadata: Option<ProviderMetadata>,
},
ToolInputStart {
id: ToolCallId,
tool_name: ToolName,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
provider_executed: bool,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
dynamic: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
title: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
provider_metadata: Option<ProviderMetadata>,
},
ToolInputDelta {
id: ToolCallId,
delta: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
provider_metadata: Option<ProviderMetadata>,
},
ToolInputEnd {
id: ToolCallId,
#[serde(default, skip_serializing_if = "Option::is_none")]
provider_metadata: Option<ProviderMetadata>,
},
ToolCall(ParsedToolCall),
ToolResult(ToolResult),
ToolError(ToolExecutionError),
ToolApprovalRequest(ToolApprovalRequestContent),
ToolApprovalResponse(ToolApprovalResponseContent),
ToolOutputDenied(ToolOutputDenied),
FinishStep {
step_number: u32,
finish_reason: FinishReason,
usage: Usage,
response: StepResponse,
#[serde(default, skip_serializing_if = "Option::is_none")]
provider_metadata: Option<ProviderMetadata>,
performance: StepPerformance,
},
Finish {
finish_reason: FinishReason,
total_usage: Usage,
},
Error {
error: StreamErrorInfo,
},
Abort,
RetryAttempt {
step_number: u32,
attempt: u32,
request: StepRequest,
warnings: Vec<Warning>,
},
Raw {
raw_value: JsonValue,
},
}
impl StreamEvent {
#[must_use]
pub fn kind_name(&self) -> &'static str {
match self {
Self::Start { .. } => "start",
Self::StartStep { .. } => "start-step",
Self::TextStart { .. } => "text-start",
Self::TextDelta { .. } => "text-delta",
Self::TextEnd { .. } => "text-end",
Self::ReasoningStart { .. } => "reasoning-start",
Self::ReasoningDelta { .. } => "reasoning-delta",
Self::ReasoningEnd { .. } => "reasoning-end",
Self::ReasoningFile(_) => "reasoning-file",
Self::File(_) => "file",
Self::Source(_) => "source",
Self::Custom { .. } => "custom",
Self::ToolInputStart { .. } => "tool-input-start",
Self::ToolInputDelta { .. } => "tool-input-delta",
Self::ToolInputEnd { .. } => "tool-input-end",
Self::ToolCall(_) => "tool-call",
Self::ToolResult(_) => "tool-result",
Self::ToolError(_) => "tool-error",
Self::ToolApprovalRequest(_) => "tool-approval-request",
Self::ToolApprovalResponse(_) => "tool-approval-response",
Self::ToolOutputDenied(_) => "tool-output-denied",
Self::FinishStep { .. } => "finish-step",
Self::Finish { .. } => "finish",
Self::Error { .. } => "error",
Self::Abort => "abort",
Self::RetryAttempt { .. } => "retry-attempt",
Self::Raw { .. } => "raw",
}
}
#[must_use]
pub fn as_text_delta(&self) -> Option<&str> {
match self {
Self::TextDelta { text, .. } => Some(text),
_ => None,
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct StreamErrorInfo {
pub kind: ErrorKind,
pub message: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub status_code: Option<u16>,
pub is_retryable: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub provider_data: Option<JsonValue>,
}
impl StreamErrorInfo {
#[must_use]
pub fn from_error(error: &Error) -> Self {
let provider_data = error
.as_provider()
.and_then(ferrin_spec::error::ProviderError::as_api_call)
.and_then(|api| api.data.clone());
Self {
kind: error.kind(),
message: error.to_string(),
status_code: error.status_code().map(|status| status.as_u16()),
is_retryable: error.is_retryable(),
provider_data,
}
}
}
impl From<&Error> for StreamErrorInfo {
fn from(error: &Error) -> Self {
Self::from_error(error)
}
}