1use std::collections::HashMap;
30
31use serde::{Deserialize, Serialize};
32
33pub use async_openai::types::responses::*;
37
38pub use async_openai::types::responses::InputContent as UpstreamInputContent;
44
45pub use crate::types::ImageDetail;
47pub use crate::types::ReasoningEffort;
48pub use crate::types::ResponseFormatJsonSchema;
49
50pub type Input = InputParam;
52pub type PromptConfig = Prompt;
53pub type TextConfig = ResponseTextParam;
54pub type TextResponseFormat = TextResponseFormatConfiguration;
55
56pub type ResponseStream = std::pin::Pin<
58 Box<dyn futures::Stream<Item = Result<ResponseStreamEvent, crate::error::OpenAIError>> + Send>,
59>;
60
61pub const SPEC_NULLABLE_REQUIRED_RESPONSE_FIELDS: &[&str] = &[
78 "billing",
79 "completed_at",
80 "conversation",
81 "error",
82 "incomplete_details",
83 "instructions",
84 "max_output_tokens",
85 "max_tool_calls",
86 "previous_response_id",
87 "prompt",
88 "prompt_cache_key",
89 "prompt_cache_retention",
90 "reasoning",
91 "safety_identifier",
92 "usage",
93];
94
95fn deserialize_null_as_empty_vec<'de, T, D>(deserializer: D) -> Result<Vec<T>, D::Error>
105where
106 T: Deserialize<'de>,
107 D: serde::Deserializer<'de>,
108{
109 Option::<Vec<T>>::deserialize(deserializer).map(Option::unwrap_or_default)
110}
111
112fn deserialize_null_as_default<'de, T, D>(deserializer: D) -> Result<T, D::Error>
118where
119 T: Deserialize<'de> + Default,
120 D: serde::Deserializer<'de>,
121{
122 Option::<T>::deserialize(deserializer).map(Option::unwrap_or_default)
123}
124
125#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
129pub struct InputOutputTextContent {
130 #[serde(default, deserialize_with = "deserialize_null_as_empty_vec")]
131 pub annotations: Vec<Annotation>,
132 #[serde(default, skip_serializing_if = "Option::is_none")]
133 pub logprobs: Option<Vec<LogProb>>,
134 pub text: String,
135}
136
137#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
139#[serde(tag = "type", rename_all = "snake_case")]
140pub enum InputOutputMessageContent {
141 OutputText(InputOutputTextContent),
142 Refusal(RefusalContent),
143}
144
145#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
152pub struct InputOutputMessage {
153 #[serde(default, deserialize_with = "deserialize_null_as_empty_vec")]
154 pub content: Vec<InputOutputMessageContent>,
155 #[serde(default, skip_serializing_if = "Option::is_none")]
156 pub id: Option<String>,
157 pub role: AssistantRole,
158 #[serde(default, skip_serializing_if = "Option::is_none")]
159 pub phase: Option<MessagePhase>,
160 #[serde(default, skip_serializing_if = "Option::is_none")]
161 pub status: Option<OutputStatus>,
162}
163
164#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
173pub struct InputImageContent {
174 #[serde(default, deserialize_with = "deserialize_null_as_default")]
175 pub detail: ImageDetail,
176 #[serde(default, skip_serializing_if = "Option::is_none")]
177 pub file_id: Option<String>,
178 #[serde(default, skip_serializing_if = "Option::is_none")]
179 pub image_url: Option<String>,
180}
181
182#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
186#[serde(tag = "type", rename_all = "snake_case")]
187pub enum InputContent {
188 InputText(InputTextContent),
189 InputImage(InputImageContent),
190 InputFile(InputFileContent),
191}
192
193#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)]
196pub struct InputMessage {
197 pub content: Vec<InputContent>,
198 pub role: InputRole,
199 #[serde(default, skip_serializing_if = "Option::is_none")]
200 pub status: Option<OutputStatus>,
201}
202
203#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
216#[serde(untagged)]
217pub enum MessageItem {
218 Output(InputOutputMessage),
221 Input(InputMessage),
223}
224
225#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
228#[serde(tag = "type", rename_all = "snake_case")]
229pub enum Item {
230 Message(MessageItem),
231 FileSearchCall(FileSearchToolCall),
232 ComputerCall(ComputerToolCall),
233 ComputerCallOutput(ComputerCallOutputItemParam),
234 WebSearchCall(WebSearchToolCall),
235 FunctionCall(FunctionToolCall),
236 FunctionCallOutput(FunctionCallOutputItemParam),
237 ToolSearchCall(ToolSearchCallItemParam),
238 ToolSearchOutput(ToolSearchOutputItemParam),
239 Reasoning(ReasoningItem),
240 Compaction(CompactionSummaryItemParam),
241 ImageGenerationCall(ImageGenToolCall),
242 CodeInterpreterCall(CodeInterpreterToolCall),
243 LocalShellCall(LocalShellToolCall),
244 LocalShellCallOutput(LocalShellToolCallOutput),
245 ShellCall(FunctionShellCallItemParam),
246 ShellCallOutput(FunctionShellCallOutputItemParam),
247 ApplyPatchCall(ApplyPatchToolCallItemParam),
248 ApplyPatchCallOutput(ApplyPatchToolCallOutputItemParam),
249 McpListTools(MCPListTools),
250 McpApprovalRequest(MCPApprovalRequest),
251 McpApprovalResponse(MCPApprovalResponse),
252 McpCall(MCPToolCall),
253 CustomToolCallOutput(CustomToolCallOutput),
254 CustomToolCall(CustomToolCall),
255}
256
257#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
259#[serde(untagged)]
260pub enum InputItem {
261 ItemReference(ItemReference),
262 Item(Item),
263 EasyMessage(EasyInputMessage),
264}
265
266#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
268#[serde(untagged)]
269pub enum InputParam {
270 Text(String),
271 Items(Vec<InputItem>),
272}
273
274impl Default for InputParam {
275 fn default() -> Self {
276 Self::Text(String::new())
277 }
278}
279
280#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)]
289pub struct CreateResponse {
290 #[serde(skip_serializing_if = "Option::is_none")]
291 pub background: Option<bool>,
292 #[serde(skip_serializing_if = "Option::is_none")]
293 pub conversation: Option<ConversationParam>,
294 #[serde(skip_serializing_if = "Option::is_none")]
295 pub include: Option<Vec<IncludeEnum>>,
296 pub input: InputParam,
297 #[serde(skip_serializing_if = "Option::is_none")]
298 pub instructions: Option<String>,
299 #[serde(skip_serializing_if = "Option::is_none")]
300 pub max_output_tokens: Option<u32>,
301 #[serde(skip_serializing_if = "Option::is_none")]
302 pub max_tool_calls: Option<u32>,
303 #[serde(skip_serializing_if = "Option::is_none")]
304 pub metadata: Option<HashMap<String, String>>,
305 #[serde(skip_serializing_if = "Option::is_none")]
306 pub model: Option<String>,
307 #[serde(skip_serializing_if = "Option::is_none")]
308 pub parallel_tool_calls: Option<bool>,
309 #[serde(skip_serializing_if = "Option::is_none")]
310 pub previous_response_id: Option<String>,
311 #[serde(skip_serializing_if = "Option::is_none")]
312 pub prompt: Option<Prompt>,
313 #[serde(skip_serializing_if = "Option::is_none")]
314 pub prompt_cache_key: Option<String>,
315 #[serde(skip_serializing_if = "Option::is_none")]
316 pub prompt_cache_retention: Option<PromptCacheRetention>,
317 #[serde(skip_serializing_if = "Option::is_none")]
318 pub reasoning: Option<Reasoning>,
319 #[serde(skip_serializing_if = "Option::is_none")]
320 pub safety_identifier: Option<String>,
321 #[serde(skip_serializing_if = "Option::is_none")]
322 pub service_tier: Option<ServiceTier>,
323 #[serde(skip_serializing_if = "Option::is_none")]
324 pub store: Option<bool>,
325 #[serde(skip_serializing_if = "Option::is_none")]
326 pub stream: Option<bool>,
327 #[serde(skip_serializing_if = "Option::is_none")]
328 pub stream_options: Option<ResponseStreamOptions>,
329 #[serde(skip_serializing_if = "Option::is_none")]
330 pub temperature: Option<f32>,
331 #[serde(skip_serializing_if = "Option::is_none")]
332 pub text: Option<ResponseTextParam>,
333 #[serde(skip_serializing_if = "Option::is_none")]
334 pub tool_choice: Option<ToolChoiceParam>,
335 #[serde(skip_serializing_if = "Option::is_none")]
336 pub tools: Option<Vec<Tool>>,
337 #[serde(skip_serializing_if = "Option::is_none")]
338 pub top_logprobs: Option<u8>,
339 #[serde(skip_serializing_if = "Option::is_none")]
340 pub top_p: Option<f32>,
341 #[serde(skip_serializing_if = "Option::is_none")]
342 pub truncation: Option<Truncation>,
343}
344
345#[cfg(test)]
346mod tests {
347 use super::*;
348
349 #[test]
350 fn relaxed_assistant_message_without_id_or_status() {
351 let json = serde_json::json!({
352 "type": "message",
353 "role": "assistant",
354 "content": [{"type": "output_text", "text": "hi"}]
355 });
356 let item: InputItem = serde_json::from_value(json).unwrap();
357 match item {
358 InputItem::Item(Item::Message(MessageItem::Output(out))) => {
359 assert_eq!(out.role, AssistantRole::Assistant);
360 assert!(out.id.is_none());
361 assert!(out.status.is_none());
362 }
363 other => panic!("expected Item::Message(Output), got {other:?}"),
364 }
365 }
366
367 #[test]
368 fn input_image_without_detail_defaults_to_auto() {
369 let json = serde_json::json!({
370 "type": "input_image",
371 "image_url": "https://example.com/cat.jpg"
372 });
373 let content: InputContent = serde_json::from_value(json).unwrap();
374 match content {
375 InputContent::InputImage(img) => assert_eq!(img.detail, ImageDetail::Auto),
376 other => panic!("expected InputImage, got {other:?}"),
377 }
378 }
379
380 #[test]
381 fn input_image_with_explicit_null_detail_defaults_to_auto() {
382 let json = serde_json::json!({
383 "type": "input_image",
384 "image_url": "https://example.com/cat.jpg",
385 "detail": null
386 });
387 let content: InputContent = serde_json::from_value(json).unwrap();
388 match content {
389 InputContent::InputImage(img) => assert_eq!(img.detail, ImageDetail::Auto),
390 other => panic!("expected InputImage, got {other:?}"),
391 }
392 }
393
394 #[test]
395 fn assistant_message_without_content_field_deserializes() {
396 let json = serde_json::json!({
400 "type": "message",
401 "role": "assistant"
402 });
403 let item: InputItem = serde_json::from_value(json).unwrap();
404 match item {
405 InputItem::Item(Item::Message(MessageItem::Output(out))) => {
406 assert_eq!(out.role, AssistantRole::Assistant);
407 assert!(out.content.is_empty());
408 assert!(out.id.is_none());
409 assert!(out.status.is_none());
410 }
411 other => panic!("expected Item::Message(Output), got {other:?}"),
412 }
413 }
414
415 #[test]
416 fn assistant_message_with_explicit_null_content_deserializes() {
417 let json = serde_json::json!({
421 "type": "message",
422 "role": "assistant",
423 "content": null
424 });
425 let item: InputItem = serde_json::from_value(json).unwrap();
426 match item {
427 InputItem::Item(Item::Message(MessageItem::Output(out))) => {
428 assert!(out.content.is_empty());
429 }
430 other => panic!("expected Item::Message(Output), got {other:?}"),
431 }
432 }
433
434 #[test]
435 fn mcp_call_item_deserializes() {
436 let json = serde_json::json!({
439 "type": "mcp_call",
440 "id": "mcp_1",
441 "server_label": "srv",
442 "name": "t",
443 "arguments": "{}"
444 });
445 let item: InputItem = serde_json::from_value(json).unwrap();
446 assert!(matches!(item, InputItem::Item(Item::McpCall(_))));
447 }
448
449 #[test]
450 fn strict_assistant_message_still_deserializes() {
451 let json = serde_json::json!({
452 "type": "message",
453 "role": "assistant",
454 "id": "msg_1",
455 "status": "completed",
456 "content": [{"type": "output_text", "text": "hi", "annotations": []}]
457 });
458 let item: InputItem = serde_json::from_value(json).unwrap();
459 match item {
460 InputItem::Item(Item::Message(MessageItem::Output(out))) => {
461 assert_eq!(out.id.as_deref(), Some("msg_1"));
462 assert_eq!(out.status, Some(OutputStatus::Completed));
463 }
464 other => panic!("expected Item::Message(Output), got {other:?}"),
465 }
466 }
467
468 #[test]
469 fn user_message_routes_to_input_variant() {
470 let json = serde_json::json!({
471 "type": "message",
472 "role": "user",
473 "content": [{"type": "input_text", "text": "hi"}]
474 });
475 let item: InputItem = serde_json::from_value(json).unwrap();
476 assert!(matches!(
477 item,
478 InputItem::Item(Item::Message(MessageItem::Input(_)))
479 ));
480 }
481
482 #[test]
483 fn function_call_item_still_deserializes() {
484 let json = serde_json::json!({
485 "type": "function_call",
486 "call_id": "c",
487 "name": "f",
488 "arguments": "{}"
489 });
490 let item: InputItem = serde_json::from_value(json).unwrap();
491 assert!(matches!(item, InputItem::Item(Item::FunctionCall(_))));
492 }
493
494 #[test]
495 fn easy_message_string_content_routes_to_easymessage() {
496 let json = serde_json::json!({"role": "assistant", "content": "x"});
497 let item: InputItem = serde_json::from_value(json).unwrap();
498 assert!(matches!(item, InputItem::EasyMessage(_)));
499 }
500
501 #[test]
502 fn output_text_without_annotations_defaults_empty() {
503 let json = serde_json::json!({"type": "output_text", "text": "hi"});
504 let part: InputOutputMessageContent = serde_json::from_value(json).unwrap();
505 match part {
506 InputOutputMessageContent::OutputText(t) => {
507 assert!(t.annotations.is_empty());
508 }
509 _ => panic!("expected OutputText"),
510 }
511 }
512
513 #[test]
514 fn output_text_with_explicit_null_annotations_deserializes_as_empty() {
515 let json = serde_json::json!({"type": "output_text", "text": "hi", "annotations": null});
519 let part: InputOutputMessageContent = serde_json::from_value(json).unwrap();
520 match part {
521 InputOutputMessageContent::OutputText(t) => {
522 assert!(t.annotations.is_empty());
523 }
524 _ => panic!("expected OutputText"),
525 }
526 }
527
528 #[test]
529 fn assistant_message_with_explicit_null_id_and_status_deserializes() {
530 let json = serde_json::json!({
535 "type": "message",
536 "role": "assistant",
537 "id": null,
538 "status": null,
539 "content": [{"type": "output_text", "text": "hi", "annotations": null}]
540 });
541 let item: InputItem = serde_json::from_value(json).unwrap();
542 match item {
543 InputItem::Item(Item::Message(MessageItem::Output(out))) => {
544 assert!(out.id.is_none());
545 assert!(out.status.is_none());
546 assert_eq!(out.content.len(), 1);
547 }
548 other => panic!("expected Item::Message(Output), got {other:?}"),
549 }
550 }
551
552 #[test]
553 fn create_response_roundtrip_with_relaxed_input() {
554 let body = serde_json::json!({
555 "model": "m",
556 "input": [
557 {"type": "message", "role": "user", "content": [
558 {"type": "input_text", "text": "hi"}
559 ]},
560 {"type": "function_call", "call_id": "c", "name": "f", "arguments": "{}"},
561 {"type": "message", "role": "assistant", "content": [
562 {"type": "output_text", "text": "\n\n"}
563 ]},
564 {"type": "function_call_output", "call_id": "c", "output": "x"}
565 ]
566 });
567
568 let req: CreateResponse = serde_json::from_value(body).unwrap();
569 let items = match &req.input {
570 InputParam::Items(items) => items,
571 _ => panic!("expected Items"),
572 };
573 assert_eq!(items.len(), 4);
574 assert!(matches!(
575 items[2],
576 InputItem::Item(Item::Message(MessageItem::Output(_)))
577 ));
578 }
579}