use std::collections::HashMap; use serde::{Deserialize, Serialize}; use serde_with::skip_serializing_none; use crate::providers::request::{ProviderRequest, ProviderRequestError}; impl TryFrom<&[u8]> for ResponsesAPIRequest { type Error = serde_json::Error; fn try_from(bytes: &[u8]) -> Result { serde_json::from_slice(bytes) } } /// Parameterized conversion for ResponsesAPIResponse impl TryFrom<&[u8]> for ResponsesAPIResponse { type Error = crate::apis::openai::OpenAIStreamError; fn try_from(bytes: &[u8]) -> Result { serde_json::from_slice(bytes).map_err(crate::apis::openai::OpenAIStreamError::from) } } // ============================================================================ // Request Structs - CreateResponse // ============================================================================ /// Request to create a model response #[skip_serializing_none] #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ResponsesAPIRequest { /// The model to use for generating the response pub model: String, /// Text, image, or file inputs to the model pub input: InputParam, /// Specify additional output data to include in the model response pub include: Option>, /// Whether to allow the model to run tool calls in parallel pub parallel_tool_calls: Option, /// Whether to store the generated model response for later retrieval via API pub store: Option, /// A system (or developer) message inserted into the model's context pub instructions: Option, /// If set to true, the model response data will be streamed to the client pub stream: Option, /// Stream options configuration pub stream_options: Option, /// Conversation state pub conversation: Option, /// Tools available to the model pub tools: Option>, /// Tool choice option pub tool_choice: Option, /// Maximum number of output tokens pub max_output_tokens: Option, /// Temperature for sampling (0-2) pub temperature: Option, /// Top-p nucleus sampling parameter pub top_p: Option, /// Metadata for the response pub metadata: Option>, /// Previous response ID for conversation continuation pub previous_response_id: Option, /// Response modalities pub modalities: Option>, /// Audio output configuration pub audio: Option, /// Text output format configuration pub text: Option, /// Reasoning effort level pub reasoning_effort: Option, /// Truncation strategy pub truncation: Option, /// User identifier pub user: Option, /// Maximum number of tool calls pub max_tool_calls: Option, /// Service tier pub service_tier: Option, /// Whether to run in background pub background: Option, /// Number of top logprobs to include pub top_logprobs: Option, } /// Input parameter - can be a simple string or array of input items #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(untagged)] pub enum InputParam { /// Simple text input Text(String), /// Array of input items Items(Vec), } /// Input item discriminated by type #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(tag = "type", rename_all = "snake_case")] pub enum InputItem { /// Input message Message(InputMessage), } /// Input message with role and content #[derive(Debug, Clone, Serialize, Deserialize)] pub struct InputMessage { /// Message role pub role: MessageRole, /// Message content pub content: Vec, } /// Message roles #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "lowercase")] pub enum MessageRole { User, Assistant, System, Developer, } /// Input content types #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(tag = "type", rename_all = "snake_case")] pub enum InputContent { /// Text input InputText { text: String, }, /// Image input via URL InputImage { image_url: String, detail: Option, }, /// File input via URL InputFile { file_url: String, }, /// Audio input InputAudio { data: Option, format: Option, }, } /// Modality options #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "lowercase")] pub enum Modality { Text, Audio, } /// Audio configuration #[skip_serializing_none] #[derive(Debug, Clone, Serialize, Deserialize)] pub struct AudioConfig { /// Voice to use for audio output pub voice: String, /// Audio output format pub format: Option, } /// Text configuration #[derive(Debug, Clone, Serialize, Deserialize)] pub struct TextConfig { /// Text format configuration pub format: TextFormat, } /// Text format #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(tag = "type", rename_all = "snake_case")] pub enum TextFormat { Text, JsonObject, JsonSchema { json_schema: serde_json::Value, }, } /// Reasoning effort levels #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "lowercase")] pub enum ReasoningEffort { Low, Medium, High, } /// Include enum for additional output data #[derive(Debug, Clone, Serialize, Deserialize)] pub enum IncludeEnum { #[serde(rename = "web_search_call.action.sources")] WebSearchCallActionSources, #[serde(rename = "code_interpreter_call.outputs")] CodeInterpreterCallOutputs, #[serde(rename = "computer_call_output.output.image_url")] ComputerCallOutputImageUrl, #[serde(rename = "file_search_call.results")] FileSearchCallResults, #[serde(rename = "message.input_image.image_url")] MessageInputImageImageUrl, #[serde(rename = "message.output_text.logprobs")] MessageOutputTextLogprobs, #[serde(rename = "reasoning.encrypted_content")] ReasoningEncryptedContent, } /// Response stream options #[skip_serializing_none] #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ResponseStreamOptions { /// Whether to include usage in stream pub include_usage: Option, } /// Conversation parameter #[skip_serializing_none] #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ConversationParam { /// Conversation ID pub id: Option, } /// Tool definitions #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(tag = "type", rename_all = "snake_case")] pub enum Tool { /// Function tool - flat structure in Responses API Function { name: String, description: Option, parameters: Option, strict: Option, }, /// File search tool FileSearch { vector_store_ids: Option>, max_num_results: Option, ranking_options: Option, filters: Option, }, /// Web search tool WebSearchPreview { domains: Option>, search_context_size: Option, user_location: Option, }, /// Code interpreter tool CodeInterpreter, /// Computer tool Computer { display_width_px: Option, display_height_px: Option, display_number: Option, }, } /// Ranking options for file search #[skip_serializing_none] #[derive(Debug, Clone, Serialize, Deserialize)] pub struct RankingOptions { /// Ranker type pub ranker: String, /// Score threshold pub score_threshold: Option, } /// User location for web search #[skip_serializing_none] #[derive(Debug, Clone, Serialize, Deserialize)] pub struct UserLocation { #[serde(rename = "type")] pub location_type: String, pub city: Option, pub country: Option, pub region: Option, pub timezone: Option, } /// Tool choice options #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(untagged)] pub enum ToolChoice { /// Auto, none, or required String(String), /// Named tool choice Named { #[serde(rename = "type")] tool_type: String, function: NamedFunction, }, } /// Named function for tool choice #[derive(Debug, Clone, Serialize, Deserialize)] pub struct NamedFunction { pub name: String, } // ============================================================================ // Response Structs - Response Object // ============================================================================ /// The response object returned from the API /// Request to create a model response #[skip_serializing_none] #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ResponsesAPIResponse { /// Unique identifier for this Response pub id: String, /// The object type - always "response" pub object: String, /// Unix timestamp (in seconds) of when this Response was created pub created_at: i64, /// The status of the response generation pub status: ResponseStatus, /// Error information if the response failed pub error: Option, /// Details about why the response is incomplete pub incomplete_details: Option, /// System/developer instructions pub instructions: Option, /// The model used pub model: String, /// An array of content items generated by the model pub output: Vec, /// Usage statistics pub usage: Option, /// Whether to allow parallel tool calls pub parallel_tool_calls: bool, /// Conversation state pub conversation: Option, /// Previous response ID pub previous_response_id: Option, /// Tools available pub tools: Vec, /// Tool choice setting pub tool_choice: String, /// Temperature setting pub temperature: f32, /// Top-p setting pub top_p: f32, /// Metadata pub metadata: HashMap, /// Truncation setting pub truncation: Option, /// Maximum output tokens pub max_output_tokens: Option, /// Reasoning configuration pub reasoning: Option, /// Whether response is stored pub store: Option, /// Text configuration pub text: Option, /// Audio configuration pub audio: Option, /// Modalities pub modalities: Option>, /// Service tier pub service_tier: Option, /// Background execution pub background: Option, /// Top logprobs count pub top_logprobs: Option, /// Maximum tool calls pub max_tool_calls: Option, } /// Response status #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "snake_case")] pub enum ResponseStatus { Completed, Failed, InProgress, Cancelled, Queued, Incomplete, } /// Response error information #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ResponseError { /// Error code pub code: ResponseErrorCode, /// Human-readable error message pub message: String, } /// Response error codes #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum ResponseErrorCode { ServerError, RateLimitExceeded, InvalidPrompt, VectorStoreTimeout, InvalidImage, InvalidImageFormat, InvalidBase64Image, InvalidImageUrl, ImageTooLarge, ImageTooSmall, ImageParseError, ImageContentPolicyViolation, InvalidImageMode, ImageFileTooLarge, UnsupportedImageMediaType, EmptyImageFile, FailedToDownloadImage, ImageFileNotFound, } /// Incomplete details #[derive(Debug, Clone, Serialize, Deserialize)] pub struct IncompleteDetails { /// The reason why the response is incomplete pub reason: IncompleteReason, } /// Incomplete reasons #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum IncompleteReason { MaxOutputTokens, ContentFilter, } /// Output items from the model #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(tag = "type", rename_all = "snake_case")] pub enum OutputItem { /// Output message Message { id: String, status: OutputItemStatus, role: String, content: Vec, }, /// Function tool call FunctionCall { id: String, status: OutputItemStatus, call_id: String, name: Option, arguments: Option, }, /// Function call output FunctionCallOutput { id: String, call_id: String, output: String, status: Option, }, /// File search tool call FileSearchCall { id: String, status: OutputItemStatus, queries: Option>, results: Option>, }, /// Web search tool call WebSearchCall { id: String, status: OutputItemStatus, }, /// Code interpreter tool call CodeInterpreterCall { id: String, status: OutputItemStatus, code: Option, outputs: Option>, }, /// Computer tool call ComputerCall { id: String, status: OutputItemStatus, action: Option, }, /// Computer call output ComputerCallOutput { id: String, call_id: String, output: Option, status: Option, }, /// Custom tool call CustomToolCall { id: String, status: OutputItemStatus, call_id: String, input: Option, }, /// Custom tool call output CustomToolCallOutput { id: String, call_id: String, output: String, status: Option, }, /// Reasoning item Reasoning { id: String, summary: Vec, }, } /// Output item status #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum OutputItemStatus { InProgress, Completed, Incomplete, } /// Output content types #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(tag = "type", rename_all = "snake_case")] pub enum OutputContent { /// Text output OutputText { text: String, annotations: Vec, logprobs: Option>, }, /// Audio output OutputAudio { data: Option, transcript: Option, }, /// Refusal output Refusal { refusal: String, }, } /// Annotations for output text #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(tag = "type", rename_all = "snake_case")] pub enum Annotation { /// File citation FileCitation { index: i32, file_id: String, filename: String, quote: Option, }, /// URL citation UrlCitation { start_index: i32, end_index: i32, url: String, title: String, }, } /// Log probability information #[derive(Debug, Clone, Serialize, Deserialize)] pub struct LogProb { /// The token pub token: String, /// Log probability value pub logprob: f32, /// Token bytes pub bytes: Vec, } /// File search result #[skip_serializing_none] #[derive(Debug, Clone, Serialize, Deserialize)] pub struct FileSearchResult { /// File ID pub file_id: String, /// File name pub filename: String, /// Score pub score: Option, /// Content excerpt pub content: Option, } /// Code interpreter output #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(tag = "type", rename_all = "snake_case")] pub enum CodeInterpreterOutput { /// Text output Text { text: String, }, /// Image output Image { image: String, }, } /// Response usage statistics #[skip_serializing_none] #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ResponseUsage { /// Input tokens used pub input_tokens: i32, /// Output tokens generated pub output_tokens: i32, /// Total tokens (input + output) pub total_tokens: i32, /// Input token details pub input_tokens_details: Option, /// Output token details pub output_tokens_details: Option, } impl crate::providers::response::TokenUsage for ResponseUsage { fn completion_tokens(&self) -> usize { self.output_tokens as usize } fn prompt_tokens(&self) -> usize { self.input_tokens as usize } fn total_tokens(&self) -> usize { self.total_tokens as usize } } /// Token details #[derive(Debug, Clone, Serialize, Deserialize)] pub struct TokenDetails { /// Cached tokens pub cached_tokens: i32, } /// Output token details #[derive(Debug, Clone, Serialize, Deserialize)] pub struct OutputTokenDetails { /// Reasoning tokens pub reasoning_tokens: i32, } /// Reasoning configuration and summary #[skip_serializing_none] #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Reasoning { /// Reasoning effort level pub effort: Option, /// Summary of reasoning pub summary: Option, } /// Conversation object #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Conversation { /// Conversation ID pub id: String, /// Conversation object type pub object: String, } // ============================================================================ // Streaming Response Events // ============================================================================ /// Stream events for responses #[skip_serializing_none] #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(tag = "type", rename_all = "snake_case")] pub enum ResponsesAPIStreamEvent { /// Response created #[serde(rename = "response.created")] ResponseCreated { response: ResponsesAPIResponse, sequence_number: i32, }, /// Response in progress #[serde(rename = "response.in_progress")] ResponseInProgress { response: ResponsesAPIResponse, sequence_number: i32, }, /// Response completed #[serde(rename = "response.completed")] ResponseCompleted { response: ResponsesAPIResponse, sequence_number: i32, }, /// Output item added #[serde(rename = "response.output_item.added")] ResponseOutputItemAdded { output_index: i32, item: OutputItem, sequence_number: i32, }, /// Output item done #[serde(rename = "response.output_item.done")] ResponseOutputItemDone { output_index: i32, item: OutputItem, sequence_number: i32, }, /// Content part added #[serde(rename = "response.content_part.added")] ResponseContentPartAdded { item_id: String, output_index: i32, content_index: i32, part: OutputContent, sequence_number: i32, }, /// Content part done #[serde(rename = "response.content_part.done")] ResponseContentPartDone { item_id: String, output_index: i32, content_index: i32, part: OutputContent, sequence_number: i32, }, /// Output text delta (incremental text streaming) #[serde(rename = "response.output_text.delta")] ResponseOutputTextDelta { item_id: String, output_index: i32, content_index: i32, delta: String, logprobs: Vec, obfuscation: Option, sequence_number: i32, }, /// Output text done (final complete text) #[serde(rename = "response.output_text.done")] ResponseOutputTextDone { item_id: String, output_index: i32, content_index: i32, text: String, logprobs: Vec, sequence_number: i32, }, /// Audio delta #[serde(rename = "response.audio.delta")] ResponseAudioDelta { item_id: Option, output_index: Option, content_index: Option, delta: String, sequence_number: i32, }, /// Audio done #[serde(rename = "response.audio.done")] ResponseAudioDone { item_id: Option, output_index: Option, content_index: Option, sequence_number: i32, }, /// Audio transcript delta #[serde(rename = "response.audio_transcript.delta")] ResponseAudioTranscriptDelta { item_id: Option, output_index: Option, content_index: Option, delta: String, sequence_number: i32, }, /// Audio transcript done #[serde(rename = "response.audio_transcript.done")] ResponseAudioTranscriptDone { item_id: Option, output_index: Option, content_index: Option, transcript: Option, sequence_number: i32, }, /// Function call arguments delta #[serde(rename = "response.function_call_arguments.delta")] ResponseFunctionCallArgumentsDelta { output_index: i32, item_id: String, delta: String, sequence_number: i32, call_id: Option, name: Option, }, /// Function call arguments done #[serde(rename = "response.function_call_arguments.done")] ResponseFunctionCallArgumentsDone { output_index: i32, item_id: String, arguments: String, sequence_number: i32, }, /// Code interpreter call code delta #[serde(rename = "response.code_interpreter_call.code.delta")] ResponseCodeInterpreterCallCodeDelta { output_index: i32, item_id: String, delta: String, sequence_number: i32, }, /// Code interpreter call code done #[serde(rename = "response.code_interpreter_call.code.done")] ResponseCodeInterpreterCallCodeDone { output_index: i32, item_id: String, code: String, sequence_number: i32, }, /// Code interpreter call in progress #[serde(rename = "response.code_interpreter_call.in_progress")] ResponseCodeInterpreterCallInProgress { output_index: i32, item_id: String, sequence_number: i32, }, /// Code interpreter call interpreting #[serde(rename = "response.code_interpreter_call.interpreting")] ResponseCodeInterpreterCallInterpreting { output_index: i32, item_id: String, sequence_number: i32, }, /// Code interpreter call completed #[serde(rename = "response.code_interpreter_call.completed")] ResponseCodeInterpreterCallCompleted { output_index: i32, item_id: String, sequence_number: i32, }, /// Custom tool call input delta #[serde(rename = "response.custom_tool_call.input.delta")] ResponseCustomToolCallInputDelta { output_index: i32, item_id: String, delta: String, sequence_number: i32, }, /// Custom tool call input done #[serde(rename = "response.custom_tool_call.input.done")] ResponseCustomToolCallInputDone { output_index: i32, item_id: String, input: String, sequence_number: i32, }, /// Error event Error { code: String, message: String, sequence_number: i32, }, /// Done event (end of stream) Done { sequence_number: i32, }, } // ============================================================================ // Additional Response Operations // ============================================================================ /// Retrieve response request (GET /responses/{response_id}) #[derive(Debug, Clone, Serialize, Deserialize)] pub struct GetResponseRequest { /// Response ID to retrieve pub response_id: String, } /// Delete response request (DELETE /responses/{response_id}) #[derive(Debug, Clone, Serialize, Deserialize)] pub struct DeleteResponseRequest { /// Response ID to delete pub response_id: String, } /// Delete response response #[derive(Debug, Clone, Serialize, Deserialize)] pub struct DeleteResponseResponse { /// Response ID that was deleted pub id: String, /// Object type pub object: String, /// Whether deletion was successful pub deleted: bool, } /// Cancel response request (POST /responses/{response_id}/cancel) #[derive(Debug, Clone, Serialize, Deserialize)] pub struct CancelResponseRequest { /// Response ID to cancel pub response_id: String, } /// List input items request (GET /responses/{response_id}/input_items) #[skip_serializing_none] #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ListInputItemsRequest { /// Response ID pub response_id: String, /// Limit for pagination pub limit: Option, /// Order for pagination pub order: Option, /// After cursor for pagination pub after: Option, /// Before cursor for pagination pub before: Option, } /// List input items response #[skip_serializing_none] #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ListInputItemsResponse { /// Object type - always "list" pub object: String, /// Array of input items pub data: Vec, /// First ID in the list pub first_id: Option, /// Last ID in the list pub last_id: Option, /// Whether there are more items pub has_more: bool, } // ============================================================================ // ProviderRequest Implementation // ============================================================================ impl ProviderRequest for ResponsesAPIRequest { fn model(&self) -> &str { &self.model } fn set_model(&mut self, model: String) { self.model = model; } fn is_streaming(&self) -> bool { self.stream.unwrap_or_default() } fn extract_messages_text(&self) -> String { match &self.input { InputParam::Text(text) => text.clone(), InputParam::Items(items) => { items.iter().fold(String::new(), |acc, item| { match item { InputItem::Message(msg) => { let content_text = msg.content.iter().fold(String::new(), |acc, content| { acc + " " + &match content { InputContent::InputText { text } => text.clone(), InputContent::InputImage { .. } => "[Image]".to_string(), InputContent::InputFile { .. } => "[File]".to_string(), InputContent::InputAudio { .. } => "[Audio]".to_string(), } }); acc + " " + &content_text } } }) } } } fn get_recent_user_message(&self) -> Option { match &self.input { InputParam::Text(text) => Some(text.clone()), InputParam::Items(items) => { items.iter().rev().find_map(|item| { match item { InputItem::Message(msg) if matches!(msg.role, MessageRole::User) => { // Extract text from the first text content msg.content.iter().find_map(|content| { match content { InputContent::InputText { text } => Some(text.clone()), _ => None, } }) } _ => None, } }) } } } fn get_tool_names(&self) -> Option> { self.tools.as_ref().map(|tools| { tools .iter() .filter_map(|tool| match tool { Tool::Function { name, .. } => Some(name.clone()), // Other tool types don't have user-defined names _ => None, }) .collect() }) } fn to_bytes(&self) -> Result, ProviderRequestError> { serde_json::to_vec(&self).map_err(|e| ProviderRequestError { message: format!("Failed to serialize Responses API request: {}", e), source: Some(Box::new(e)), }) } fn metadata(&self) -> &Option> { &self.metadata } fn remove_metadata_key(&mut self, key: &str) -> bool { if let Some(ref mut metadata) = self.metadata { metadata.remove(key).is_some() } else { false } } fn get_temperature(&self) -> Option { self.temperature } } // ============================================================================ // Into Implementation for SSE Formatting // ============================================================================ impl Into for ResponsesAPIStreamEvent { fn into(self) -> String { let transformed_json = serde_json::to_string(&self).unwrap_or_default(); let event_type = match &self { ResponsesAPIStreamEvent::ResponseCreated { .. } => "response.created", ResponsesAPIStreamEvent::ResponseInProgress { .. } => "response.in_progress", ResponsesAPIStreamEvent::ResponseCompleted { .. } => "response.completed", ResponsesAPIStreamEvent::ResponseOutputItemAdded { .. } => "response.output_item.added", ResponsesAPIStreamEvent::ResponseOutputItemDone { .. } => "response.output_item.done", ResponsesAPIStreamEvent::ResponseContentPartAdded { .. } => { "response.content_part.added" } ResponsesAPIStreamEvent::ResponseContentPartDone { .. } => "response.content_part.done", ResponsesAPIStreamEvent::ResponseOutputTextDelta { .. } => "response.output_text.delta", ResponsesAPIStreamEvent::ResponseOutputTextDone { .. } => "response.output_text.done", ResponsesAPIStreamEvent::ResponseAudioDelta { .. } => "response.audio.delta", ResponsesAPIStreamEvent::ResponseAudioDone { .. } => "response.audio.done", ResponsesAPIStreamEvent::ResponseAudioTranscriptDelta { .. } => { "response.audio_transcript.delta" } ResponsesAPIStreamEvent::ResponseAudioTranscriptDone { .. } => { "response.audio_transcript.done" } ResponsesAPIStreamEvent::ResponseFunctionCallArgumentsDelta { .. } => { "response.function_call_arguments.delta" } ResponsesAPIStreamEvent::ResponseFunctionCallArgumentsDone { .. } => { "response.function_call_arguments.done" } ResponsesAPIStreamEvent::ResponseCodeInterpreterCallCodeDelta { .. } => { "response.code_interpreter_call.code.delta" } ResponsesAPIStreamEvent::ResponseCodeInterpreterCallCodeDone { .. } => { "response.code_interpreter_call.code.done" } ResponsesAPIStreamEvent::ResponseCodeInterpreterCallInProgress { .. } => { "response.code_interpreter_call.in_progress" } ResponsesAPIStreamEvent::ResponseCodeInterpreterCallInterpreting { .. } => { "response.code_interpreter_call.interpreting" } ResponsesAPIStreamEvent::ResponseCodeInterpreterCallCompleted { .. } => { "response.code_interpreter_call.completed" } ResponsesAPIStreamEvent::ResponseCustomToolCallInputDelta { .. } => { "response.custom_tool_call.input.delta" } ResponsesAPIStreamEvent::ResponseCustomToolCallInputDone { .. } => { "response.custom_tool_call.input.done" } ResponsesAPIStreamEvent::Error { .. } => "error", ResponsesAPIStreamEvent::Done { .. } => "done", }; let event = format!("event: {}\n", event_type); let data = format!("data: {}\n\n", transformed_json); event + &data } } // ============================================================================ // ProviderStreamResponse Implementation // ============================================================================ impl crate::providers::streaming_response::ProviderStreamResponse for ResponsesAPIStreamEvent { fn content_delta(&self) -> Option<&str> { match self { ResponsesAPIStreamEvent::ResponseOutputTextDelta { delta, .. } => Some(delta), ResponsesAPIStreamEvent::ResponseAudioDelta { delta, .. } => Some(delta), ResponsesAPIStreamEvent::ResponseAudioTranscriptDelta { delta, .. } => Some(delta), ResponsesAPIStreamEvent::ResponseFunctionCallArgumentsDelta { delta, .. } => { Some(delta) } ResponsesAPIStreamEvent::ResponseCodeInterpreterCallCodeDelta { delta, .. } => { Some(delta) } ResponsesAPIStreamEvent::ResponseCustomToolCallInputDelta { delta, .. } => Some(delta), _ => None, } } fn is_final(&self) -> bool { matches!( self, ResponsesAPIStreamEvent::ResponseCompleted { .. } | ResponsesAPIStreamEvent::Done { .. } ) } fn role(&self) -> Option<&str> { match self { ResponsesAPIStreamEvent::ResponseOutputItemDone { item, .. } => match item { OutputItem::Message { role, .. } => Some(role.as_str()), _ => None, }, _ => None, } } fn event_type(&self) -> Option<&str> { Some(match self { ResponsesAPIStreamEvent::ResponseCreated { .. } => "response.created", ResponsesAPIStreamEvent::ResponseInProgress { .. } => "response.in_progress", ResponsesAPIStreamEvent::ResponseCompleted { .. } => "response.completed", ResponsesAPIStreamEvent::ResponseOutputItemAdded { .. } => "response.output_item.added", ResponsesAPIStreamEvent::ResponseOutputItemDone { .. } => "response.output_item.done", ResponsesAPIStreamEvent::ResponseContentPartAdded { .. } => { "response.content_part.added" } ResponsesAPIStreamEvent::ResponseContentPartDone { .. } => "response.content_part.done", ResponsesAPIStreamEvent::ResponseOutputTextDelta { .. } => "response.output_text.delta", ResponsesAPIStreamEvent::ResponseOutputTextDone { .. } => "response.output_text.done", ResponsesAPIStreamEvent::ResponseAudioDelta { .. } => "response.audio.delta", ResponsesAPIStreamEvent::ResponseAudioDone { .. } => "response.audio.done", ResponsesAPIStreamEvent::ResponseAudioTranscriptDelta { .. } => { "response.audio_transcript.delta" } ResponsesAPIStreamEvent::ResponseAudioTranscriptDone { .. } => { "response.audio_transcript.done" } ResponsesAPIStreamEvent::ResponseFunctionCallArgumentsDelta { .. } => { "response.function_call_arguments.delta" } ResponsesAPIStreamEvent::ResponseFunctionCallArgumentsDone { .. } => { "response.function_call_arguments.done" } ResponsesAPIStreamEvent::ResponseCodeInterpreterCallCodeDelta { .. } => { "response.code_interpreter_call.code.delta" } ResponsesAPIStreamEvent::ResponseCodeInterpreterCallCodeDone { .. } => { "response.code_interpreter_call.code.done" } ResponsesAPIStreamEvent::ResponseCodeInterpreterCallInProgress { .. } => { "response.code_interpreter_call.in_progress" } ResponsesAPIStreamEvent::ResponseCodeInterpreterCallInterpreting { .. } => { "response.code_interpreter_call.interpreting" } ResponsesAPIStreamEvent::ResponseCodeInterpreterCallCompleted { .. } => { "response.code_interpreter_call.completed" } ResponsesAPIStreamEvent::ResponseCustomToolCallInputDelta { .. } => { "response.custom_tool_call.input.delta" } ResponsesAPIStreamEvent::ResponseCustomToolCallInputDone { .. } => { "response.custom_tool_call.input.done" } ResponsesAPIStreamEvent::Error { .. } => "error", ResponsesAPIStreamEvent::Done { .. } => "done", }) } } #[cfg(test)] mod tests { use super::*; #[test] fn test_response_output_text_delta_deserialization() { let json = r#"{ "type":"response.output_text.delta", "sequence_number":811, "item_id":"msg_0d87415661475591006924ce5465748190bdc8874257743b5c", "output_index":1, "content_index":0, "delta":" first", "logprobs":[], "obfuscation":"sRhca4PA06" }"#; let event: ResponsesAPIStreamEvent = serde_json::from_str(json).expect("Failed to deserialize"); match event { ResponsesAPIStreamEvent::ResponseOutputTextDelta { item_id, output_index, content_index, delta, sequence_number, logprobs, obfuscation, } => { assert_eq!( item_id, "msg_0d87415661475591006924ce5465748190bdc8874257743b5c" ); assert_eq!(output_index, 1); assert_eq!(content_index, 0); assert_eq!(delta, " first"); assert_eq!(sequence_number, 811); assert_eq!(logprobs.len(), 0); assert_eq!(obfuscation, Some("sRhca4PA06".to_string())); } _ => panic!("Expected ResponseOutputTextDelta event"), } } #[test] fn test_response_output_text_done_deserialization() { let json = r#"{ "type":"response.output_text.done", "sequence_number":818, "item_id":"msg_0d87415661475591006924ce5465748190bdc8874257743b5c", "output_index":1, "content_index":0, "text":"The otters linked paws and laughed.", "logprobs":[] }"#; let event: ResponsesAPIStreamEvent = serde_json::from_str(json).expect("Failed to deserialize"); match event { ResponsesAPIStreamEvent::ResponseOutputTextDone { item_id, output_index, content_index, text, sequence_number, logprobs, } => { assert_eq!( item_id, "msg_0d87415661475591006924ce5465748190bdc8874257743b5c" ); assert_eq!(output_index, 1); assert_eq!(content_index, 0); assert_eq!(text, "The otters linked paws and laughed."); assert_eq!(sequence_number, 818); assert_eq!(logprobs.len(), 0); } _ => panic!("Expected ResponseOutputTextDone event"), } } #[test] fn test_response_completed_deserialization() { // Simplified response.completed event let json = r#"{ "type":"response.completed", "sequence_number":821, "response":{ "id":"resp_test123", "object":"response", "created_at":1764019793, "status":"completed", "background":false, "error":null, "incomplete_details":null, "instructions":null, "max_output_tokens":null, "max_tool_calls":null, "model":"o3-2025-04-16", "output":[], "output_text":null, "usage":{ "input_tokens":17, "output_tokens":946, "total_tokens":963 }, "parallel_tool_calls":true, "conversation":null, "previous_response_id":null, "tools":[], "tool_choice":"auto", "temperature":1.0, "top_p":1.0, "metadata":{}, "truncation":null, "user":null, "reasoning":null, "store":true, "text":null, "audio":null, "modalities":null, "service_tier":"default", "top_logprobs":0 } }"#; let event: ResponsesAPIStreamEvent = serde_json::from_str(json).expect("Failed to deserialize"); match event { ResponsesAPIStreamEvent::ResponseCompleted { response, sequence_number, } => { assert_eq!(response.id, "resp_test123"); assert_eq!(sequence_number, 821); assert_eq!(response.model, "o3-2025-04-16"); } _ => panic!("Expected ResponseCompleted event"), } } }