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)]
214#[serde(untagged)]
215pub enum EasyInputContent {
216 Text(String),
218 ContentList(Vec<InputContent>),
220}
221
222impl Default for EasyInputContent {
223 fn default() -> Self {
224 Self::Text(String::new())
225 }
226}
227
228#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)]
234pub struct EasyInputMessage {
235 #[serde(default)]
239 pub r#type: MessageType,
240 pub role: Role,
241 pub content: EasyInputContent,
242 #[serde(default, skip_serializing_if = "Option::is_none")]
243 pub phase: Option<MessagePhase>,
244}
245
246#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
259#[serde(untagged)]
260pub enum MessageItem {
261 Output(InputOutputMessage),
264 Input(InputMessage),
266}
267
268#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
271#[serde(tag = "type", rename_all = "snake_case")]
272pub enum Item {
273 Message(MessageItem),
274 FileSearchCall(FileSearchToolCall),
275 ComputerCall(ComputerToolCall),
276 ComputerCallOutput(ComputerCallOutputItemParam),
277 WebSearchCall(WebSearchToolCall),
278 FunctionCall(FunctionToolCall),
279 FunctionCallOutput(FunctionCallOutputItemParam),
280 ToolSearchCall(ToolSearchCallItemParam),
281 ToolSearchOutput(ToolSearchOutputItemParam),
282 Reasoning(ReasoningItem),
283 Compaction(CompactionSummaryItemParam),
284 ImageGenerationCall(ImageGenToolCall),
285 CodeInterpreterCall(CodeInterpreterToolCall),
286 LocalShellCall(LocalShellToolCall),
287 LocalShellCallOutput(LocalShellToolCallOutput),
288 ShellCall(FunctionShellCallItemParam),
289 ShellCallOutput(FunctionShellCallOutputItemParam),
290 ApplyPatchCall(ApplyPatchToolCallItemParam),
291 ApplyPatchCallOutput(ApplyPatchToolCallOutputItemParam),
292 McpListTools(MCPListTools),
293 McpApprovalRequest(MCPApprovalRequest),
294 McpApprovalResponse(MCPApprovalResponse),
295 McpCall(MCPToolCall),
296 CustomToolCallOutput(CustomToolCallOutput),
297 CustomToolCall(CustomToolCall),
298}
299
300#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
302#[serde(untagged)]
303pub enum InputItem {
304 ItemReference(ItemReference),
305 Item(Item),
306 EasyMessage(EasyInputMessage),
307}
308
309#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
311#[serde(untagged)]
312pub enum InputParam {
313 Text(String),
314 Items(Vec<InputItem>),
315}
316
317impl Default for InputParam {
318 fn default() -> Self {
319 Self::Text(String::new())
320 }
321}
322
323#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)]
332pub struct CreateResponse {
333 #[serde(skip_serializing_if = "Option::is_none")]
334 pub background: Option<bool>,
335 #[serde(skip_serializing_if = "Option::is_none")]
336 pub conversation: Option<ConversationParam>,
337 #[serde(skip_serializing_if = "Option::is_none")]
338 pub include: Option<Vec<IncludeEnum>>,
339 pub input: InputParam,
340 #[serde(skip_serializing_if = "Option::is_none")]
341 pub instructions: Option<String>,
342 #[serde(skip_serializing_if = "Option::is_none")]
343 pub max_output_tokens: Option<u32>,
344 #[serde(skip_serializing_if = "Option::is_none")]
345 pub max_tool_calls: Option<u32>,
346 #[serde(skip_serializing_if = "Option::is_none")]
347 pub metadata: Option<HashMap<String, String>>,
348 #[serde(skip_serializing_if = "Option::is_none")]
349 pub model: Option<String>,
350 #[serde(skip_serializing_if = "Option::is_none")]
351 pub parallel_tool_calls: Option<bool>,
352 #[serde(skip_serializing_if = "Option::is_none")]
353 pub previous_response_id: Option<String>,
354 #[serde(skip_serializing_if = "Option::is_none")]
355 pub prompt: Option<Prompt>,
356 #[serde(skip_serializing_if = "Option::is_none")]
357 pub prompt_cache_key: Option<String>,
358 #[serde(skip_serializing_if = "Option::is_none")]
359 pub prompt_cache_retention: Option<PromptCacheRetention>,
360 #[serde(skip_serializing_if = "Option::is_none")]
361 pub reasoning: Option<Reasoning>,
362 #[serde(skip_serializing_if = "Option::is_none")]
363 pub safety_identifier: Option<String>,
364 #[serde(skip_serializing_if = "Option::is_none")]
365 pub service_tier: Option<ServiceTier>,
366 #[serde(skip_serializing_if = "Option::is_none")]
367 pub store: Option<bool>,
368 #[serde(skip_serializing_if = "Option::is_none")]
369 pub stream: Option<bool>,
370 #[serde(skip_serializing_if = "Option::is_none")]
371 pub stream_options: Option<ResponseStreamOptions>,
372 #[serde(skip_serializing_if = "Option::is_none")]
373 pub temperature: Option<f32>,
374 #[serde(skip_serializing_if = "Option::is_none")]
375 pub text: Option<ResponseTextParam>,
376 #[serde(skip_serializing_if = "Option::is_none")]
377 pub tool_choice: Option<ToolChoiceParam>,
378 #[serde(skip_serializing_if = "Option::is_none")]
379 pub tools: Option<Vec<Tool>>,
380 #[serde(skip_serializing_if = "Option::is_none")]
381 pub top_logprobs: Option<u8>,
382 #[serde(skip_serializing_if = "Option::is_none")]
383 pub top_p: Option<f32>,
384 #[serde(skip_serializing_if = "Option::is_none")]
385 pub truncation: Option<Truncation>,
386}
387
388#[cfg(test)]
389mod tests {
390 use super::*;
391
392 #[test]
393 fn relaxed_assistant_message_without_id_or_status() {
394 let json = serde_json::json!({
395 "type": "message",
396 "role": "assistant",
397 "content": [{"type": "output_text", "text": "hi"}]
398 });
399 let item: InputItem = serde_json::from_value(json).unwrap();
400 match item {
401 InputItem::Item(Item::Message(MessageItem::Output(out))) => {
402 assert_eq!(out.role, AssistantRole::Assistant);
403 assert!(out.id.is_none());
404 assert!(out.status.is_none());
405 }
406 other => panic!("expected Item::Message(Output), got {other:?}"),
407 }
408 }
409
410 #[test]
411 fn input_image_without_detail_defaults_to_auto() {
412 let json = serde_json::json!({
413 "type": "input_image",
414 "image_url": "https://example.com/cat.jpg"
415 });
416 let content: InputContent = serde_json::from_value(json).unwrap();
417 match content {
418 InputContent::InputImage(img) => assert_eq!(img.detail, ImageDetail::Auto),
419 other => panic!("expected InputImage, got {other:?}"),
420 }
421 }
422
423 #[test]
424 fn input_image_with_explicit_null_detail_defaults_to_auto() {
425 let json = serde_json::json!({
426 "type": "input_image",
427 "image_url": "https://example.com/cat.jpg",
428 "detail": null
429 });
430 let content: InputContent = serde_json::from_value(json).unwrap();
431 match content {
432 InputContent::InputImage(img) => assert_eq!(img.detail, ImageDetail::Auto),
433 other => panic!("expected InputImage, got {other:?}"),
434 }
435 }
436
437 #[test]
438 fn assistant_message_without_content_field_deserializes() {
439 let json = serde_json::json!({
443 "type": "message",
444 "role": "assistant"
445 });
446 let item: InputItem = serde_json::from_value(json).unwrap();
447 match item {
448 InputItem::Item(Item::Message(MessageItem::Output(out))) => {
449 assert_eq!(out.role, AssistantRole::Assistant);
450 assert!(out.content.is_empty());
451 assert!(out.id.is_none());
452 assert!(out.status.is_none());
453 }
454 other => panic!("expected Item::Message(Output), got {other:?}"),
455 }
456 }
457
458 #[test]
459 fn assistant_message_with_explicit_null_content_deserializes() {
460 let json = serde_json::json!({
464 "type": "message",
465 "role": "assistant",
466 "content": null
467 });
468 let item: InputItem = serde_json::from_value(json).unwrap();
469 match item {
470 InputItem::Item(Item::Message(MessageItem::Output(out))) => {
471 assert!(out.content.is_empty());
472 }
473 other => panic!("expected Item::Message(Output), got {other:?}"),
474 }
475 }
476
477 #[test]
478 fn mcp_call_item_deserializes() {
479 let json = serde_json::json!({
482 "type": "mcp_call",
483 "id": "mcp_1",
484 "server_label": "srv",
485 "name": "t",
486 "arguments": "{}"
487 });
488 let item: InputItem = serde_json::from_value(json).unwrap();
489 assert!(matches!(item, InputItem::Item(Item::McpCall(_))));
490 }
491
492 #[test]
493 fn strict_assistant_message_still_deserializes() {
494 let json = serde_json::json!({
495 "type": "message",
496 "role": "assistant",
497 "id": "msg_1",
498 "status": "completed",
499 "content": [{"type": "output_text", "text": "hi", "annotations": []}]
500 });
501 let item: InputItem = serde_json::from_value(json).unwrap();
502 match item {
503 InputItem::Item(Item::Message(MessageItem::Output(out))) => {
504 assert_eq!(out.id.as_deref(), Some("msg_1"));
505 assert_eq!(out.status, Some(OutputStatus::Completed));
506 }
507 other => panic!("expected Item::Message(Output), got {other:?}"),
508 }
509 }
510
511 #[test]
512 fn user_message_routes_to_input_variant() {
513 let json = serde_json::json!({
514 "type": "message",
515 "role": "user",
516 "content": [{"type": "input_text", "text": "hi"}]
517 });
518 let item: InputItem = serde_json::from_value(json).unwrap();
519 assert!(matches!(
520 item,
521 InputItem::Item(Item::Message(MessageItem::Input(_)))
522 ));
523 }
524
525 #[test]
526 fn function_call_item_still_deserializes() {
527 let json = serde_json::json!({
528 "type": "function_call",
529 "call_id": "c",
530 "name": "f",
531 "arguments": "{}"
532 });
533 let item: InputItem = serde_json::from_value(json).unwrap();
534 assert!(matches!(item, InputItem::Item(Item::FunctionCall(_))));
535 }
536
537 #[test]
538 fn easy_message_string_content_routes_to_easymessage() {
539 let json = serde_json::json!({"role": "assistant", "content": "x"});
540 let item: InputItem = serde_json::from_value(json).unwrap();
541 assert!(matches!(item, InputItem::EasyMessage(_)));
542 }
543
544 #[test]
545 fn output_text_without_annotations_defaults_empty() {
546 let json = serde_json::json!({"type": "output_text", "text": "hi"});
547 let part: InputOutputMessageContent = serde_json::from_value(json).unwrap();
548 match part {
549 InputOutputMessageContent::OutputText(t) => {
550 assert!(t.annotations.is_empty());
551 }
552 _ => panic!("expected OutputText"),
553 }
554 }
555
556 #[test]
557 fn output_text_with_explicit_null_annotations_deserializes_as_empty() {
558 let json = serde_json::json!({"type": "output_text", "text": "hi", "annotations": null});
562 let part: InputOutputMessageContent = serde_json::from_value(json).unwrap();
563 match part {
564 InputOutputMessageContent::OutputText(t) => {
565 assert!(t.annotations.is_empty());
566 }
567 _ => panic!("expected OutputText"),
568 }
569 }
570
571 #[test]
572 fn assistant_message_with_explicit_null_id_and_status_deserializes() {
573 let json = serde_json::json!({
578 "type": "message",
579 "role": "assistant",
580 "id": null,
581 "status": null,
582 "content": [{"type": "output_text", "text": "hi", "annotations": null}]
583 });
584 let item: InputItem = serde_json::from_value(json).unwrap();
585 match item {
586 InputItem::Item(Item::Message(MessageItem::Output(out))) => {
587 assert!(out.id.is_none());
588 assert!(out.status.is_none());
589 assert_eq!(out.content.len(), 1);
590 }
591 other => panic!("expected Item::Message(Output), got {other:?}"),
592 }
593 }
594
595 #[test]
596 fn create_response_roundtrip_with_relaxed_input() {
597 let body = serde_json::json!({
598 "model": "m",
599 "input": [
600 {"type": "message", "role": "user", "content": [
601 {"type": "input_text", "text": "hi"}
602 ]},
603 {"type": "function_call", "call_id": "c", "name": "f", "arguments": "{}"},
604 {"type": "message", "role": "assistant", "content": [
605 {"type": "output_text", "text": "\n\n"}
606 ]},
607 {"type": "function_call_output", "call_id": "c", "output": "x"}
608 ]
609 });
610
611 let req: CreateResponse = serde_json::from_value(body).unwrap();
612 let items = match &req.input {
613 InputParam::Items(items) => items,
614 _ => panic!("expected Items"),
615 };
616 assert_eq!(items.len(), 4);
617 assert!(matches!(
618 items[2],
619 InputItem::Item(Item::Message(MessageItem::Output(_)))
620 ));
621 }
622
623 #[test]
631 fn easy_message_multimodal_without_type_routes_to_easymessage() {
632 let json = serde_json::json!({
635 "role": "user",
636 "content": [
637 {"type": "input_image", "image_url": "data:image/png;base64,abc"}
638 ]
639 });
640 let item: InputItem = serde_json::from_value(json).unwrap();
641 match item {
642 InputItem::EasyMessage(easy) => {
643 assert_eq!(easy.role, Role::User);
644 assert_eq!(easy.r#type, MessageType::Message);
645 match easy.content {
646 EasyInputContent::ContentList(parts) => {
647 assert_eq!(parts.len(), 1);
648 match &parts[0] {
649 InputContent::InputImage(img) => {
650 assert_eq!(img.detail, ImageDetail::Auto);
651 assert_eq!(
652 img.image_url.as_deref(),
653 Some("data:image/png;base64,abc")
654 );
655 }
656 other => panic!("expected InputImage, got {other:?}"),
657 }
658 }
659 other => panic!("expected ContentList, got {other:?}"),
660 }
661 }
662 other => panic!("expected EasyMessage, got {other:?}"),
663 }
664 }
665
666 #[test]
667 fn easy_message_multimodal_with_explicit_null_detail() {
668 let json = serde_json::json!({
672 "role": "user",
673 "content": [
674 {"type": "input_image", "image_url": "data:image/png;base64,abc", "detail": null}
675 ]
676 });
677 let item: InputItem = serde_json::from_value(json).unwrap();
678 assert!(matches!(item, InputItem::EasyMessage(_)));
679 }
680
681 #[test]
682 fn easy_message_assistant_multimodal_without_type() {
683 let json = serde_json::json!({
687 "role": "assistant",
688 "content": [
689 {"type": "input_text", "text": "ok"}
690 ]
691 });
692 let item: InputItem = serde_json::from_value(json).unwrap();
693 match item {
694 InputItem::EasyMessage(easy) => {
695 assert_eq!(easy.role, Role::Assistant);
696 }
697 other => panic!("expected EasyMessage(assistant), got {other:?}"),
698 }
699 }
700
701 #[test]
702 fn easy_message_text_only_without_type_unchanged() {
703 let json = serde_json::json!({"role": "user", "content": "Hello"});
708 let item: InputItem = serde_json::from_value(json).unwrap();
709 match item {
710 InputItem::EasyMessage(easy) => {
711 assert_eq!(easy.role, Role::User);
712 assert!(matches!(easy.content, EasyInputContent::Text(ref s) if s == "Hello"));
713 }
714 other => panic!("expected EasyMessage(Text), got {other:?}"),
715 }
716 }
717
718 #[test]
719 fn easy_message_with_explicit_type_still_routes_to_item_message() {
720 let json = serde_json::json!({
724 "type": "message",
725 "role": "user",
726 "content": [
727 {"type": "input_image", "image_url": "data:image/png;base64,abc"}
728 ]
729 });
730 let item: InputItem = serde_json::from_value(json).unwrap();
731 match item {
732 InputItem::Item(Item::Message(MessageItem::Input(msg))) => {
733 assert_eq!(msg.role, InputRole::User);
734 assert_eq!(msg.content.len(), 1);
735 }
736 other => panic!("expected Item::Message(Input), got {other:?}"),
737 }
738 }
739
740 #[test]
741 fn create_response_roundtrip_aiperf_pre_pr931_payload() {
742 let body = serde_json::json!({
747 "model": "Qwen/Qwen2-VL-2B-Instruct",
748 "input": [
749 {
750 "role": "user",
751 "content": [
752 {"type": "input_text", "text": "Describe"},
753 {"type": "input_image", "image_url": "data:image/png;base64,abc"}
754 ]
755 },
756 {
757 "role": "assistant",
758 "content": [{"type": "input_text", "text": "ok"}]
759 },
760 {
761 "role": "user",
762 "content": [{"type": "input_text", "text": "Now describe a different one."}]
763 }
764 ]
765 });
766 let req: CreateResponse = serde_json::from_value(body).unwrap();
767 let items = match &req.input {
768 InputParam::Items(items) => items,
769 _ => panic!("expected Items"),
770 };
771 assert_eq!(items.len(), 3);
772 for (idx, item) in items.iter().enumerate() {
774 assert!(
775 matches!(item, InputItem::EasyMessage(_)),
776 "turn {idx} did not route to EasyMessage: {item:?}",
777 );
778 }
779 }
780}