Skip to main content

litellm_rs/core/models/openai/
responses_api.rs

1//! OpenAI Responses API types
2//!
3//! This module defines request and response types for the OpenAI Responses API
4//! (POST /v1/responses), which is the new standard replacing the Assistants API.
5//!
6//! Reference: <https://platform.openai.com/docs/api-reference/responses>
7
8use serde::{Deserialize, Serialize};
9use serde_json::Value;
10
11pub use crate::core::types::codex::wire::{
12    CodexCustomTool, CodexCustomToolCall, CodexCustomToolCallOutput, CodexFunctionCall,
13    CodexFunctionCallOutput, CodexInternalChatMessageMetadataPassthrough, CodexUnsupportedWire,
14};
15
16// ── Request types ────────────────────────────────────────────────────────────
17
18/// POST /v1/responses request body
19#[derive(Debug, Clone, Serialize, Deserialize)]
20pub struct ResponsesApiRequest {
21    /// Model to use (e.g. "gpt-4o", "gpt-4.1")
22    pub model: String,
23
24    /// Input items: a plain string or an array of input objects
25    pub input: ResponseInput,
26
27    /// System-level instructions (equivalent to a system message)
28    /// Dynamically loaded tools proposed by Codex.
29    #[serde(skip_serializing_if = "Option::is_none")]
30    pub instructions: Option<String>,
31
32    /// ID of a previous response to continue from (stateful chaining)
33    #[serde(skip_serializing_if = "Option::is_none")]
34    pub previous_response_id: Option<String>,
35
36    /// Whether to store this response for future chaining (default: true)
37    #[serde(skip_serializing_if = "Option::is_none")]
38    pub store: Option<bool>,
39
40    /// Built-in or function tools available to the model
41    #[serde(skip_serializing_if = "Option::is_none")]
42    pub tools: Option<Vec<ResponseTool>>,
43
44    #[serde(skip_serializing_if = "Option::is_none")]
45    pub additional_tools: Option<Vec<ResponseTool>>,
46
47    /// Stream the response as server-sent events
48    #[serde(skip_serializing_if = "Option::is_none")]
49    pub stream: Option<bool>,
50
51    /// Run as a background (async) task
52    #[serde(skip_serializing_if = "Option::is_none")]
53    pub background: Option<bool>,
54
55    /// Maximum number of output tokens to generate
56    #[serde(skip_serializing_if = "Option::is_none")]
57    pub max_output_tokens: Option<u32>,
58
59    /// Sampling temperature (0.0–2.0)
60    #[serde(skip_serializing_if = "Option::is_none")]
61    pub temperature: Option<f32>,
62
63    /// Nucleus sampling probability (0.0–1.0)
64    #[serde(skip_serializing_if = "Option::is_none")]
65    pub top_p: Option<f32>,
66
67    /// Stable user identifier for abuse detection
68    #[serde(skip_serializing_if = "Option::is_none")]
69    pub user: Option<String>,
70
71    /// Reasoning configuration (for o-series / extended-thinking models)
72    #[serde(skip_serializing_if = "Option::is_none")]
73    pub reasoning: Option<ReasoningParams>,
74
75    /// Metadata key-value pairs (max 16 pairs, 64-char keys, 512-char values)
76    #[serde(skip_serializing_if = "Option::is_none")]
77    pub metadata: Option<std::collections::HashMap<String, String>>,
78
79    /// How to truncate context when the model's context window is exceeded
80    #[serde(skip_serializing_if = "Option::is_none")]
81    pub truncation: Option<String>,
82}
83
84/// Input to the Responses API — a raw string or a list of structured items
85#[derive(Debug, Clone, Serialize, Deserialize)]
86#[serde(untagged)]
87pub enum ResponseInput {
88    /// Simple text prompt
89    Text(String),
90    /// Array of structured input items
91    Items(Vec<ResponseInputItem>),
92}
93
94/// A single item in the `input` array
95#[derive(Debug, Clone)]
96pub enum ResponseInputItem {
97    /// A conversational message (user / assistant / system)
98    Message(ResponseInputMessage),
99    /// Function call retained in multi-turn Codex context.
100    FunctionCall(CodexFunctionCall),
101    /// Result returned by Codex for a function call.
102    FunctionCallOutput(CodexFunctionCallOutput),
103    /// Freeform/custom tool call retained in multi-turn context.
104    CustomToolCall(CodexCustomToolCall),
105    /// Result returned by Codex for a freeform/custom tool call.
106    CustomToolCallOutput(CodexCustomToolCallOutput),
107    Unsupported(CodexUnsupportedWire),
108    Unknown(CodexUnsupportedWire),
109}
110
111/// A conversational message inside `input`
112#[derive(Debug, Clone, Serialize, Deserialize)]
113pub struct ResponseInputMessage {
114    /// Optional item ID supplied by Responses-compatible clients.
115    #[serde(default, skip_serializing_if = "Option::is_none")]
116    pub id: Option<String>,
117    #[serde(default, skip_serializing_if = "Option::is_none")]
118    pub phase: Option<String>,
119    #[serde(default, skip_serializing_if = "Option::is_none")]
120    pub internal_chat_message_metadata_passthrough:
121        Option<CodexInternalChatMessageMetadataPassthrough>,
122    /// Role: "user" | "assistant" | "system"
123    pub role: String,
124    /// Content: plain string or array of content parts
125    pub content: ResponseInputContent,
126}
127
128/// Message content — plain text or multi-part
129#[derive(Debug, Clone, Serialize, Deserialize)]
130#[serde(untagged)]
131pub enum ResponseInputContent {
132    /// Single text string
133    Text(String),
134    /// Array of typed content parts
135    Parts(Vec<ResponseInputContentPart>),
136}
137
138/// A typed content part inside a Responses API message
139#[derive(Debug, Clone, Serialize, Deserialize)]
140#[serde(tag = "type", rename_all = "snake_case")]
141pub enum ResponseInputContentPart {
142    /// Plain text input
143    InputText { text: String },
144    /// Base-64-encoded or URL-referenced image
145    InputImage {
146        #[serde(skip_serializing_if = "Option::is_none")]
147        image_url: Option<String>,
148        #[serde(skip_serializing_if = "Option::is_none")]
149        detail: Option<String>,
150    },
151    /// URL-referenced audio input from the Codex wire protocol.
152    InputAudio { audio_url: String },
153    /// Plain text output (used in assistant turns returned by the API)
154    OutputText { text: String },
155}
156
157// ── Tool types ────────────────────────────────────────────────────────────────
158
159/// A tool definition for the Responses API
160#[derive(Debug, Clone)]
161pub enum ResponseTool {
162    /// Web-search built-in tool
163    WebSearch(WebSearchTool),
164    /// Alias used in some preview builds
165    WebSearchPreview(WebSearchTool),
166    /// File-search built-in tool
167    FileSearch(FileSearchTool),
168    /// Code-interpreter built-in tool
169    CodeInterpreter(CodeInterpreterTool),
170    /// Computer-use built-in tool (preview)
171    ComputerUsePreview(ComputerUseTool),
172    /// MCP server integration
173    Mcp(McpTool),
174    /// Regular function-calling tool
175    Function(ResponseFunctionTool),
176    CodexFunction(ResponseFunctionDefinition),
177    /// Codex freeform/custom tool.
178    Custom(CodexCustomTool),
179    Unsupported(CodexUnsupportedWire),
180    Unknown(CodexUnsupportedWire),
181}
182
183impl ResponseTool {
184    pub fn feature_name(&self) -> &str {
185        match self {
186            Self::WebSearch(_) | Self::WebSearchPreview(_) => "web_search",
187            Self::FileSearch(_) => "file_search",
188            Self::CodeInterpreter(_) => "code_interpreter",
189            Self::ComputerUsePreview(_) => "computer_use",
190            Self::Mcp(_) => "mcp",
191            Self::Function(_) | Self::CodexFunction(_) => "function",
192            Self::Custom(_) => "custom",
193            Self::Unsupported(value) | Self::Unknown(value) => &value.wire_type,
194        }
195    }
196}
197
198/// Web-search tool configuration
199#[derive(Debug, Clone, Default, Serialize, Deserialize)]
200pub struct WebSearchTool {
201    /// Optional user location for localised results
202    #[serde(skip_serializing_if = "Option::is_none")]
203    pub user_location: Option<Value>,
204    /// Search context size: "low" | "medium" | "high"
205    #[serde(skip_serializing_if = "Option::is_none")]
206    pub search_context_size: Option<String>,
207}
208
209/// File-search tool configuration
210#[derive(Debug, Clone, Serialize, Deserialize)]
211pub struct FileSearchTool {
212    /// Vector store IDs to search
213    #[serde(skip_serializing_if = "Option::is_none")]
214    pub vector_store_ids: Option<Vec<String>>,
215    /// Maximum number of chunks to retrieve
216    #[serde(skip_serializing_if = "Option::is_none")]
217    pub max_num_results: Option<u32>,
218    /// Ranking options
219    #[serde(skip_serializing_if = "Option::is_none")]
220    pub ranking_options: Option<Value>,
221    /// Filters
222    #[serde(skip_serializing_if = "Option::is_none")]
223    pub filters: Option<Value>,
224}
225
226/// Code-interpreter tool configuration
227#[derive(Debug, Clone, Default, Serialize, Deserialize)]
228pub struct CodeInterpreterTool {
229    /// Container configuration
230    #[serde(skip_serializing_if = "Option::is_none")]
231    pub container: Option<Value>,
232}
233
234/// Computer-use tool configuration
235#[derive(Debug, Clone, Serialize, Deserialize)]
236pub struct ComputerUseTool {
237    /// Display width in pixels
238    pub display_width: u32,
239    /// Display height in pixels
240    pub display_height: u32,
241    /// Environment type: "browser" | "desktop"
242    pub environment: String,
243}
244
245/// MCP server tool configuration
246#[derive(Debug, Clone, Serialize, Deserialize)]
247pub struct McpTool {
248    /// Human-readable server label
249    pub server_label: String,
250    /// Base URL of the MCP server
251    pub server_url: String,
252    /// Allowed tool names (empty = all)
253    #[serde(skip_serializing_if = "Option::is_none")]
254    pub allowed_tools: Option<Vec<String>>,
255    /// Additional headers for MCP server requests
256    #[serde(skip_serializing_if = "Option::is_none")]
257    pub headers: Option<std::collections::HashMap<String, String>>,
258    /// Whether to require human approval before tool execution
259    #[serde(skip_serializing_if = "Option::is_none")]
260    pub require_approval: Option<Value>,
261}
262
263/// Regular function-calling tool (same schema as chat completions)
264#[derive(Debug, Clone, Serialize, Deserialize)]
265pub struct ResponseFunctionTool {
266    /// Function definition
267    pub function: ResponseFunctionDefinition,
268}
269
270/// Function definition for the Responses API
271#[derive(Debug, Clone, Serialize, Deserialize)]
272pub struct ResponseFunctionDefinition {
273    /// Function name
274    pub name: String,
275    /// Human-readable description
276    #[serde(skip_serializing_if = "Option::is_none")]
277    pub description: Option<String>,
278    /// JSON Schema for the function parameters
279    #[serde(skip_serializing_if = "Option::is_none")]
280    pub parameters: Option<Value>,
281    /// Whether to enforce strict schema adherence
282    #[serde(skip_serializing_if = "Option::is_none")]
283    pub strict: Option<bool>,
284    #[serde(skip_serializing_if = "Option::is_none")]
285    pub defer_loading: Option<bool>,
286}
287
288/// Reasoning / extended-thinking configuration
289#[derive(Debug, Clone, Serialize, Deserialize)]
290pub struct ReasoningParams {
291    /// Effort level: "low" | "medium" | "high"
292    #[serde(skip_serializing_if = "Option::is_none")]
293    pub effort: Option<String>,
294    /// Whether to surface the reasoning summary in the response
295    #[serde(skip_serializing_if = "Option::is_none")]
296    pub summary: Option<String>,
297}
298
299// ── Response types ────────────────────────────────────────────────────────────
300
301/// POST /v1/responses response object
302#[derive(Debug, Clone, Serialize, Deserialize)]
303pub struct ResponsesApiResponse {
304    /// Unique identifier (e.g. "resp_…")
305    pub id: String,
306    /// Always "response"
307    pub object: String,
308    /// Unix timestamp of creation
309    pub created_at: i64,
310    /// "completed" | "in_progress" | "failed" | "incomplete"
311    pub status: String,
312    /// Model that was used
313    pub model: String,
314    /// Output items produced by the model
315    pub output: Vec<ResponseOutputItem>,
316    /// Token usage statistics
317    #[serde(skip_serializing_if = "Option::is_none")]
318    pub usage: Option<ResponseUsage>,
319    /// Error details if `status` is "failed"
320    #[serde(skip_serializing_if = "Option::is_none")]
321    pub error: Option<ResponseApiError>,
322    /// Parallel response ID (for background runs)
323    #[serde(skip_serializing_if = "Option::is_none")]
324    pub previous_response_id: Option<String>,
325    /// Metadata echoed from the request
326    #[serde(skip_serializing_if = "Option::is_none")]
327    pub metadata: Option<std::collections::HashMap<String, String>>,
328}
329
330/// An item in the `output` array
331#[derive(Debug, Clone, Serialize, Deserialize)]
332#[serde(tag = "type", rename_all = "snake_case")]
333pub enum ResponseOutputItem {
334    /// Assistant message containing text or refusal
335    Message(ResponseOutputMessage),
336    /// A function call invocation
337    FunctionCall(ResponseFunctionCall),
338    /// A Codex freeform/custom tool invocation.
339    CustomToolCall(CodexCustomToolCall),
340    /// Result of a built-in tool call
341    WebSearchCall(ResponseToolCall),
342    /// Result of a file-search call
343    FileSearchCall(ResponseToolCall),
344    /// Result of a code-interpreter call
345    CodeInterpreterCall(ResponseToolCall),
346    /// Computer-use action call
347    ComputerCall(ResponseToolCall),
348    /// MCP tool call
349    McpCall(ResponseToolCall),
350    /// Extended reasoning / thinking block
351    Reasoning(ResponseReasoningItem),
352}
353
354/// An assistant message in the output
355#[derive(Debug, Clone, Serialize, Deserialize)]
356pub struct ResponseOutputMessage {
357    /// Unique item ID
358    pub id: String,
359    /// Always "assistant"
360    pub role: String,
361    /// "completed" | "in_progress" | "incomplete"
362    pub status: String,
363    /// Content parts (text, refusal, …)
364    pub content: Vec<ResponseOutputContent>,
365}
366
367/// A content part inside an output message
368#[derive(Debug, Clone, Serialize, Deserialize)]
369#[serde(tag = "type", rename_all = "snake_case")]
370pub enum ResponseOutputContent {
371    /// Model-generated text
372    OutputText {
373        /// The text string
374        text: String,
375        /// Annotation objects (citations, links, …)
376        #[serde(skip_serializing_if = "Option::is_none")]
377        annotations: Option<Vec<Value>>,
378        /// Log-probability information
379        #[serde(skip_serializing_if = "Option::is_none")]
380        logprobs: Option<Vec<Value>>,
381    },
382    /// Model refused to answer
383    Refusal {
384        /// The refusal message
385        refusal: String,
386    },
387}
388
389/// A function call in the output
390#[derive(Debug, Clone, Serialize, Deserialize)]
391pub struct ResponseFunctionCall {
392    /// Unique call ID
393    pub id: String,
394    /// Function name
395    pub name: String,
396    /// JSON-encoded arguments
397    pub arguments: String,
398    /// "in_progress" | "completed" | "incomplete"
399    pub status: String,
400    /// Call ID used to correlate with tool outputs
401    #[serde(skip_serializing_if = "Option::is_none")]
402    pub call_id: Option<String>,
403}
404
405/// Generic built-in tool call result
406#[derive(Debug, Clone, Serialize, Deserialize)]
407pub struct ResponseToolCall {
408    /// Unique call ID
409    pub id: String,
410    /// "in_progress" | "completed" | "incomplete" | "searching" | "failed"
411    pub status: String,
412    /// Tool-specific result data
413    #[serde(flatten)]
414    pub data: Value,
415}
416
417/// Extended reasoning / thinking block
418#[derive(Debug, Clone, Serialize, Deserialize)]
419pub struct ResponseReasoningItem {
420    /// Unique item ID
421    pub id: String,
422    /// "completed"
423    pub status: String,
424    /// Summary of the reasoning (if enabled)
425    #[serde(skip_serializing_if = "Option::is_none")]
426    pub summary: Option<Vec<Value>>,
427}
428
429/// Token usage for a Responses API response
430#[derive(Debug, Clone, Default, Serialize, Deserialize)]
431pub struct ResponseUsage {
432    /// Tokens in the input
433    pub input_tokens: u32,
434    /// Tokens in the output
435    pub output_tokens: u32,
436    /// Total tokens consumed
437    pub total_tokens: u32,
438    /// Detailed breakdown (optional)
439    #[serde(skip_serializing_if = "Option::is_none")]
440    pub input_tokens_details: Option<ResponseInputTokensDetails>,
441    /// Output tokens breakdown (optional)
442    #[serde(skip_serializing_if = "Option::is_none")]
443    pub output_tokens_details: Option<ResponseOutputTokensDetails>,
444}
445
446/// Input-token breakdown
447#[derive(Debug, Clone, Default, Serialize, Deserialize)]
448pub struct ResponseInputTokensDetails {
449    /// Tokens retrieved from the context cache
450    pub cached_tokens: u32,
451}
452
453/// Output-token breakdown
454#[derive(Debug, Clone, Default, Serialize, Deserialize)]
455pub struct ResponseOutputTokensDetails {
456    /// Tokens used for extended reasoning
457    pub reasoning_tokens: u32,
458}
459
460/// API error embedded in a failed response
461#[derive(Debug, Clone, Serialize, Deserialize)]
462pub struct ResponseApiError {
463    /// OpenAI error code
464    pub code: String,
465    /// Human-readable message
466    pub message: String,
467}
468
469// ── Streaming event types ─────────────────────────────────────────────────────
470
471/// A server-sent event emitted during Responses API streaming
472#[derive(Debug, Clone, Serialize, Deserialize)]
473#[serde(tag = "type")]
474pub enum ResponseStreamEvent {
475    /// Initial event — response shell created
476    #[serde(rename = "response.created")]
477    ResponseCreated { response: Box<ResponsesApiResponse> },
478    /// Model started producing output
479    #[serde(rename = "response.in_progress")]
480    ResponseInProgress { response: Box<ResponsesApiResponse> },
481    /// A new output item was added to `output`
482    #[serde(rename = "response.output_item.added")]
483    ResponseOutputItemAdded {
484        output_index: u32,
485        item: ResponseOutputItem,
486    },
487    /// A new content part started streaming
488    #[serde(rename = "response.content_part.added")]
489    ResponseContentPartAdded {
490        output_index: u32,
491        content_index: u32,
492        part: ResponseOutputContent,
493    },
494    /// Incremental text delta
495    #[serde(rename = "response.output_text.delta")]
496    ResponseOutputTextDelta {
497        output_index: u32,
498        content_index: u32,
499        delta: String,
500    },
501    /// Text part finished streaming
502    #[serde(rename = "response.output_text.done")]
503    ResponseOutputTextDone {
504        output_index: u32,
505        content_index: u32,
506        text: String,
507    },
508    /// A content part finished
509    #[serde(rename = "response.content_part.done")]
510    ResponseContentPartDone {
511        output_index: u32,
512        content_index: u32,
513        part: ResponseOutputContent,
514    },
515    /// An output item finished
516    #[serde(rename = "response.output_item.done")]
517    ResponseOutputItemDone {
518        output_index: u32,
519        item: ResponseOutputItem,
520    },
521    /// Entire response completed
522    #[serde(rename = "response.completed")]
523    ResponseCompleted { response: Box<ResponsesApiResponse> },
524    /// Response failed
525    #[serde(rename = "response.failed")]
526    ResponseFailed { response: Box<ResponsesApiResponse> },
527    /// Incremental function-call arguments
528    #[serde(rename = "response.function_call_arguments.delta")]
529    ResponseFunctionCallArgumentsDelta {
530        output_index: u32,
531        item_id: String,
532        delta: String,
533    },
534    /// Function-call arguments finished
535    #[serde(rename = "response.function_call_arguments.done")]
536    ResponseFunctionCallArgumentsDone {
537        output_index: u32,
538        item_id: String,
539        name: String,
540        arguments: String,
541    },
542    /// Incremental custom-tool input.
543    #[serde(rename = "response.custom_tool_call_input.delta")]
544    ResponseCustomToolCallInputDelta {
545        output_index: u32,
546        item_id: String,
547        delta: String,
548    },
549    /// Custom-tool input finished.
550    #[serde(rename = "response.custom_tool_call_input.done")]
551    ResponseCustomToolCallInputDone {
552        output_index: u32,
553        item_id: String,
554        input: String,
555    },
556    /// Incremental reasoning-summary delta (o-series / Anthropic
557    /// extended thinking / DeepSeek R1 / Gemini thinking).
558    ///
559    /// Maps each upstream `delta.thinking` chunk to the OpenAI Responses
560    /// API `response.reasoning_summary_text.delta` event so consumers
561    /// receive the model's reasoning trace in real time.
562    #[serde(rename = "response.reasoning_summary_text.delta")]
563    ResponseReasoningSummaryTextDelta {
564        output_index: u32,
565        summary_index: u32,
566        delta: String,
567    },
568    /// Reasoning-summary part finished — emitted once with the full
569    /// accumulated text after the upstream stream ends.
570    #[serde(rename = "response.reasoning_summary_text.done")]
571    ResponseReasoningSummaryTextDone {
572        output_index: u32,
573        summary_index: u32,
574        text: String,
575    },
576    /// Catch-all for forward-compatible events
577    #[serde(other)]
578    Unknown,
579}
580
581#[cfg(test)]
582mod tests {
583    use super::*;
584
585    #[test]
586    fn test_request_text_input_deserialise() {
587        let json = r#"{"model":"gpt-4o","input":"Hello"}"#;
588        let req: ResponsesApiRequest = serde_json::from_str(json).unwrap();
589        assert_eq!(req.model, "gpt-4o");
590        assert!(matches!(req.input, ResponseInput::Text(_)));
591    }
592
593    #[test]
594    fn test_request_array_input_deserialise() {
595        let json = r#"{
596            "model": "gpt-4o",
597            "input": [
598                {"type": "message", "role": "user", "content": "Hello"}
599            ]
600        }"#;
601        let req: ResponsesApiRequest = serde_json::from_str(json).unwrap();
602        assert!(matches!(req.input, ResponseInput::Items(_)));
603    }
604
605    #[test]
606    fn test_request_with_instructions() {
607        let json = r#"{"model":"gpt-4o","input":"Hi","instructions":"Be concise"}"#;
608        let req: ResponsesApiRequest = serde_json::from_str(json).unwrap();
609        assert_eq!(req.instructions.as_deref(), Some("Be concise"));
610    }
611
612    #[test]
613    fn test_request_with_previous_response_id() {
614        let json = r#"{"model":"gpt-4o","input":"Follow up","previous_response_id":"resp_abc"}"#;
615        let req: ResponsesApiRequest = serde_json::from_str(json).unwrap();
616        assert_eq!(req.previous_response_id.as_deref(), Some("resp_abc"));
617    }
618
619    #[test]
620    fn test_request_with_store_flag() {
621        let json = r#"{"model":"gpt-4o","input":"Hi","store":true}"#;
622        let req: ResponsesApiRequest = serde_json::from_str(json).unwrap();
623        assert_eq!(req.store, Some(true));
624    }
625
626    #[test]
627    fn test_web_search_tool_deserialise() {
628        let json = r#"{"type":"web_search"}"#;
629        let tool: ResponseTool = serde_json::from_str(json).unwrap();
630        assert!(matches!(tool, ResponseTool::WebSearch(_)));
631    }
632
633    #[test]
634    fn test_file_search_tool_deserialise() {
635        let json = r#"{"type":"file_search","vector_store_ids":["vs_abc"]}"#;
636        let tool: ResponseTool = serde_json::from_str(json).unwrap();
637        assert!(matches!(tool, ResponseTool::FileSearch(_)));
638    }
639
640    #[test]
641    fn test_mcp_tool_deserialise() {
642        let json = r#"{
643            "type": "mcp",
644            "server_label": "my-server",
645            "server_url": "https://example.com/mcp"
646        }"#;
647        let tool: ResponseTool = serde_json::from_str(json).unwrap();
648        assert!(matches!(tool, ResponseTool::Mcp(_)));
649    }
650
651    #[test]
652    fn test_function_tool_deserialise() {
653        let json = r#"{
654            "type": "function",
655            "function": {"name": "get_weather", "description": "Get weather"}
656        }"#;
657        let tool: ResponseTool = serde_json::from_str(json).unwrap();
658        assert!(matches!(tool, ResponseTool::Function(_)));
659    }
660
661    #[test]
662    fn test_response_serialise() {
663        let resp = ResponsesApiResponse {
664            id: "resp_123".to_string(),
665            object: "response".to_string(),
666            created_at: 1_700_000_000,
667            status: "completed".to_string(),
668            model: "gpt-4o".to_string(),
669            output: vec![],
670            usage: None,
671            error: None,
672            previous_response_id: None,
673            metadata: None,
674        };
675        let json = serde_json::to_string(&resp).unwrap();
676        assert!(json.contains("\"object\":\"response\""));
677        assert!(json.contains("resp_123"));
678    }
679
680    #[test]
681    fn test_response_usage_defaults() {
682        let usage = ResponseUsage::default();
683        assert_eq!(usage.input_tokens, 0);
684        assert_eq!(usage.output_tokens, 0);
685        assert_eq!(usage.total_tokens, 0);
686    }
687
688    #[test]
689    fn test_stream_event_text_delta_serialise() {
690        let event = ResponseStreamEvent::ResponseOutputTextDelta {
691            output_index: 0,
692            content_index: 0,
693            delta: "Hello".to_string(),
694        };
695        let json = serde_json::to_string(&event).unwrap();
696        assert!(json.contains("response.output_text.delta"));
697        assert!(json.contains("Hello"));
698    }
699
700    #[test]
701    fn test_reasoning_params_deserialise() {
702        let json = r#"{"effort":"high","summary":"auto"}"#;
703        let params: ReasoningParams = serde_json::from_str(json).unwrap();
704        assert_eq!(params.effort.as_deref(), Some("high"));
705    }
706}