1use std::collections::HashMap;
32
33use serde::{Deserialize, Serialize, de};
34
35pub use async_openai::types::responses::*;
39
40pub use async_openai::types::responses::InputContent as UpstreamInputContent;
46
47pub use crate::types::ImageDetail;
49pub use crate::types::ReasoningEffort;
50pub use crate::types::ResponseFormatJsonSchema;
51
52pub type Input = InputParam;
54pub type PromptConfig = Prompt;
55pub type TextConfig = ResponseTextParam;
56pub type TextResponseFormat = TextResponseFormatConfiguration;
57
58pub type ResponseStream = std::pin::Pin<
60 Box<dyn futures::Stream<Item = Result<ResponseStreamEvent, crate::error::OpenAIError>> + Send>,
61>;
62
63pub const SPEC_NULLABLE_REQUIRED_RESPONSE_FIELDS: &[&str] = &[
80 "billing",
81 "completed_at",
82 "conversation",
83 "error",
84 "incomplete_details",
85 "instructions",
86 "max_output_tokens",
87 "max_tool_calls",
88 "previous_response_id",
89 "prompt",
90 "prompt_cache_key",
91 "prompt_cache_retention",
92 "reasoning",
93 "safety_identifier",
94 "usage",
95];
96
97fn deserialize_null_as_empty_vec<'de, T, D>(deserializer: D) -> Result<Vec<T>, D::Error>
107where
108 T: Deserialize<'de>,
109 D: serde::Deserializer<'de>,
110{
111 Option::<Vec<T>>::deserialize(deserializer).map(Option::unwrap_or_default)
112}
113
114fn deserialize_null_as_default<'de, T, D>(deserializer: D) -> Result<T, D::Error>
120where
121 T: Deserialize<'de> + Default,
122 D: serde::Deserializer<'de>,
123{
124 Option::<T>::deserialize(deserializer).map(Option::unwrap_or_default)
125}
126
127fn deserialize_tool_choice<'de, D>(deserializer: D) -> Result<Option<ToolChoiceParam>, D::Error>
143where
144 D: serde::Deserializer<'de>,
145{
146 let Some(value) = Option::<serde_json::Value>::deserialize(deserializer)? else {
147 return Ok(None);
148 };
149 if let Some(serde_json::Value::String(t)) = value.get("type") {
150 let mode = match t.as_str() {
151 "auto" => Some(ToolChoiceOptions::Auto),
152 "none" => Some(ToolChoiceOptions::None),
153 "required" => Some(ToolChoiceOptions::Required),
154 _ => None,
155 };
156 if let Some(mode) = mode {
157 return Ok(Some(ToolChoiceParam::Mode(mode)));
158 }
159 }
160 ToolChoiceParam::deserialize(value)
161 .map(Some)
162 .map_err(serde::de::Error::custom)
163}
164
165#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
169pub struct InputOutputTextContent {
170 #[serde(default, deserialize_with = "deserialize_null_as_empty_vec")]
171 pub annotations: Vec<Annotation>,
172 #[serde(default, skip_serializing_if = "Option::is_none")]
173 pub logprobs: Option<Vec<LogProb>>,
174 pub text: String,
175}
176
177#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
179#[serde(tag = "type", rename_all = "snake_case")]
180pub enum InputOutputMessageContent {
181 OutputText(InputOutputTextContent),
182 Refusal(RefusalContent),
183}
184
185#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
192pub struct InputOutputMessage {
193 #[serde(default, deserialize_with = "deserialize_null_as_empty_vec")]
194 pub content: Vec<InputOutputMessageContent>,
195 #[serde(default, skip_serializing_if = "Option::is_none")]
196 pub id: Option<String>,
197 pub role: AssistantRole,
198 #[serde(default, skip_serializing_if = "Option::is_none")]
199 pub phase: Option<MessagePhase>,
200 #[serde(default, skip_serializing_if = "Option::is_none")]
201 pub status: Option<OutputStatus>,
202}
203
204#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
213pub struct InputImageContent {
214 #[serde(default, deserialize_with = "deserialize_null_as_default")]
215 pub detail: ImageDetail,
216 #[serde(default, skip_serializing_if = "Option::is_none")]
217 pub file_id: Option<String>,
218 #[serde(default, skip_serializing_if = "Option::is_none")]
219 pub image_url: Option<String>,
220}
221
222#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
226#[serde(tag = "type", rename_all = "snake_case")]
227pub enum InputContent {
228 InputText(InputTextContent),
229 InputImage(InputImageContent),
230 InputFile(InputFileContent),
231}
232
233#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)]
236pub struct InputMessage {
237 pub content: Vec<InputContent>,
238 pub role: InputRole,
239 #[serde(default, skip_serializing_if = "Option::is_none")]
240 pub status: Option<OutputStatus>,
241}
242
243#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
254#[serde(untagged)]
255pub enum EasyInputContent {
256 Text(String),
258 ContentList(Vec<InputContent>),
260}
261
262impl Default for EasyInputContent {
263 fn default() -> Self {
264 Self::Text(String::new())
265 }
266}
267
268#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)]
274pub struct EasyInputMessage {
275 #[serde(default)]
279 pub r#type: MessageType,
280 pub role: Role,
281 pub content: EasyInputContent,
282 #[serde(default, skip_serializing_if = "Option::is_none")]
283 pub phase: Option<MessagePhase>,
284}
285
286#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
299#[serde(untagged)]
300pub enum MessageItem {
301 Output(InputOutputMessage),
304 Input(InputMessage),
306}
307
308#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
324pub struct InputReasoningItem {
325 #[serde(default, skip_serializing_if = "Option::is_none")]
327 pub id: Option<String>,
328 #[serde(default)]
330 pub summary: Vec<SummaryPart>,
331 #[serde(default, skip_serializing_if = "Option::is_none")]
332 pub content: Option<Vec<ReasoningTextContent>>,
333 #[serde(default, skip_serializing_if = "Option::is_none")]
334 pub encrypted_content: Option<String>,
335 #[serde(default, skip_serializing_if = "Option::is_none")]
336 pub status: Option<OutputStatus>,
337}
338
339#[derive(Deserialize)]
341struct CodexAgentMessage {
342 #[serde(default)]
343 content: Option<CodexAgentMessageContent>,
344}
345
346#[derive(Deserialize)]
347#[serde(untagged)]
348enum CodexAgentMessageContent {
349 Text(String),
350 Parts(Vec<CodexAgentMessageInputContent>),
351}
352
353#[derive(Deserialize)]
354#[serde(tag = "type", rename_all = "snake_case")]
355enum CodexAgentMessageInputContent {
356 InputText(InputTextContent),
357 EncryptedContent { encrypted_content: String },
358}
359
360#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
363#[serde(tag = "type", rename_all = "snake_case")]
364pub enum Item {
365 Message(MessageItem),
366 FileSearchCall(FileSearchToolCall),
367 ComputerCall(ComputerToolCall),
368 ComputerCallOutput(ComputerCallOutputItemParam),
369 WebSearchCall(WebSearchToolCall),
370 FunctionCall(FunctionToolCall),
371 FunctionCallOutput(FunctionCallOutputItemParam),
372 ToolSearchCall(ToolSearchCallItemParam),
373 ToolSearchOutput(ToolSearchOutputItemParam),
374 Reasoning(InputReasoningItem),
375 Compaction(CompactionSummaryItemParam),
376 ImageGenerationCall(ImageGenToolCall),
377 CodeInterpreterCall(CodeInterpreterToolCall),
378 LocalShellCall(LocalShellToolCall),
379 LocalShellCallOutput(LocalShellToolCallOutput),
380 ShellCall(FunctionShellCallItemParam),
381 ShellCallOutput(FunctionShellCallOutputItemParam),
382 ApplyPatchCall(ApplyPatchToolCallItemParam),
383 ApplyPatchCallOutput(ApplyPatchToolCallOutputItemParam),
384 McpListTools(MCPListTools),
385 McpApprovalRequest(MCPApprovalRequest),
386 McpApprovalResponse(MCPApprovalResponse),
387 McpCall(MCPToolCall),
388 CustomToolCallOutput(CustomToolCallOutput),
389 CustomToolCall(CustomToolCall),
390}
391
392#[derive(Debug, Serialize, Clone, PartialEq)]
394#[serde(untagged)]
395pub enum InputItem {
396 ItemReference(ItemReference),
397 Item(Item),
398 EasyMessage(EasyInputMessage),
399}
400
401#[derive(Deserialize)]
402#[serde(untagged)]
403enum InputItemWire {
404 ItemReference(ItemReference),
405 Item(Item),
406 EasyMessage(EasyInputMessage),
407}
408
409impl<'de> Deserialize<'de> for InputItem {
410 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
411 where
412 D: serde::Deserializer<'de>,
413 {
414 let value = serde_json::Value::deserialize(deserializer)?;
415 if value.get("type").and_then(serde_json::Value::as_str) == Some("agent_message") {
416 let message = CodexAgentMessage::deserialize(value).map_err(de::Error::custom)?;
417 return Ok(normalize_codex_agent_message(message));
418 }
419
420 match InputItemWire::deserialize(value).map_err(de::Error::custom)? {
421 InputItemWire::ItemReference(item) => Ok(Self::ItemReference(item)),
422 InputItemWire::Item(item) => Ok(Self::Item(item)),
423 InputItemWire::EasyMessage(message) => Ok(Self::EasyMessage(message)),
424 }
425 }
426}
427
428fn normalize_codex_agent_message(message: CodexAgentMessage) -> InputItem {
429 let content = match message.content {
430 None => String::new(),
431 Some(CodexAgentMessageContent::Text(text)) => text,
432 Some(CodexAgentMessageContent::Parts(parts)) => parts
433 .into_iter()
434 .map(|part| match part {
435 CodexAgentMessageInputContent::InputText(part) => part.text,
436 CodexAgentMessageInputContent::EncryptedContent { encrypted_content } => {
437 encrypted_content
438 }
439 })
440 .collect::<Vec<_>>()
441 .join("\n"),
442 };
443 InputItem::EasyMessage(EasyInputMessage {
444 r#type: MessageType::Message,
445 role: Role::User,
446 content: EasyInputContent::Text(content),
447 phase: None,
448 })
449}
450
451#[derive(Debug, Serialize, Clone, PartialEq)]
453#[serde(untagged)]
454pub enum InputParam {
455 Text(String),
456 Items(Vec<InputItem>),
457}
458
459impl<'de> Deserialize<'de> for InputParam {
460 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
461 where
462 D: serde::Deserializer<'de>,
463 {
464 match serde_json::Value::deserialize(deserializer)? {
465 serde_json::Value::String(text) => Ok(Self::Text(text)),
466 serde_json::Value::Array(items) => {
467 serde_json::from_value(serde_json::Value::Array(items))
468 .map(Self::Items)
469 .map_err(de::Error::custom)
470 }
471 _ => Err(de::Error::custom(
472 "input must be a string or an array of input items",
473 )),
474 }
475 }
476}
477
478impl Default for InputParam {
479 fn default() -> Self {
480 Self::Text(String::new())
481 }
482}
483
484#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)]
493pub struct CreateResponse {
494 #[serde(skip_serializing_if = "Option::is_none")]
495 pub background: Option<bool>,
496 #[serde(skip_serializing_if = "Option::is_none")]
497 pub conversation: Option<ConversationParam>,
498 #[serde(skip_serializing_if = "Option::is_none")]
499 pub include: Option<Vec<IncludeEnum>>,
500 pub input: InputParam,
501 #[serde(skip_serializing_if = "Option::is_none")]
502 pub instructions: Option<String>,
503 #[serde(skip_serializing_if = "Option::is_none")]
504 pub max_output_tokens: Option<u32>,
505 #[serde(skip_serializing_if = "Option::is_none")]
506 pub max_tool_calls: Option<u32>,
507 #[serde(skip_serializing_if = "Option::is_none")]
508 pub metadata: Option<HashMap<String, String>>,
509 #[serde(skip_serializing_if = "Option::is_none")]
510 pub model: Option<String>,
511 #[serde(skip_serializing_if = "Option::is_none")]
512 pub parallel_tool_calls: Option<bool>,
513 #[serde(skip_serializing_if = "Option::is_none")]
514 pub previous_response_id: Option<String>,
515 #[serde(skip_serializing_if = "Option::is_none")]
516 pub prompt: Option<Prompt>,
517 #[serde(skip_serializing_if = "Option::is_none")]
518 pub prompt_cache_key: Option<String>,
519 #[serde(skip_serializing_if = "Option::is_none")]
520 pub prompt_cache_retention: Option<PromptCacheRetention>,
521 #[serde(skip_serializing_if = "Option::is_none")]
522 pub reasoning: Option<Reasoning>,
523 #[serde(skip_serializing_if = "Option::is_none")]
524 pub safety_identifier: Option<String>,
525 #[serde(skip_serializing_if = "Option::is_none")]
526 pub service_tier: Option<ServiceTier>,
527 #[serde(skip_serializing_if = "Option::is_none")]
528 pub store: Option<bool>,
529 #[serde(skip_serializing_if = "Option::is_none")]
530 pub stream: Option<bool>,
531 #[serde(skip_serializing_if = "Option::is_none")]
532 pub stream_options: Option<ResponseStreamOptions>,
533 #[serde(skip_serializing_if = "Option::is_none")]
534 pub temperature: Option<f32>,
535 #[serde(skip_serializing_if = "Option::is_none")]
536 pub text: Option<ResponseTextParam>,
537 #[serde(
538 default,
539 deserialize_with = "deserialize_tool_choice",
540 skip_serializing_if = "Option::is_none"
541 )]
542 pub tool_choice: Option<ToolChoiceParam>,
543 #[serde(skip_serializing_if = "Option::is_none")]
544 pub tools: Option<Vec<Tool>>,
545 #[serde(skip_serializing_if = "Option::is_none")]
546 pub top_logprobs: Option<u8>,
547 #[serde(skip_serializing_if = "Option::is_none")]
548 pub top_p: Option<f32>,
549 #[serde(skip_serializing_if = "Option::is_none")]
550 pub truncation: Option<Truncation>,
551}
552
553#[cfg(test)]
554mod tests {
555 use super::*;
556
557 fn tool_choice_of(json: serde_json::Value) -> Option<ToolChoiceParam> {
560 let req: CreateResponse = serde_json::from_value(serde_json::json!({
561 "input": "hi",
562 "tool_choice": json,
563 }))
564 .expect("CreateResponse should deserialize");
565 req.tool_choice
566 }
567
568 #[test]
569 fn tool_choice_mode_object_coerces_to_mode() {
570 assert_eq!(
573 tool_choice_of(serde_json::json!({"type": "auto", "disable_parallel_tool_use": true})),
574 Some(ToolChoiceParam::Mode(ToolChoiceOptions::Auto)),
575 );
576 assert_eq!(
577 tool_choice_of(serde_json::json!({"type": "none"})),
578 Some(ToolChoiceParam::Mode(ToolChoiceOptions::None)),
579 );
580 assert_eq!(
581 tool_choice_of(serde_json::json!({"type": "required"})),
582 Some(ToolChoiceParam::Mode(ToolChoiceOptions::Required)),
583 );
584 }
585
586 #[test]
587 fn tool_choice_bare_string_still_works() {
588 assert_eq!(
589 tool_choice_of(serde_json::json!("auto")),
590 Some(ToolChoiceParam::Mode(ToolChoiceOptions::Auto)),
591 );
592 }
593
594 #[test]
595 fn tool_choice_specific_function_object_still_works() {
596 match tool_choice_of(serde_json::json!({"type": "function", "name": "get_weather"})) {
599 Some(ToolChoiceParam::Function(f)) => assert_eq!(f.name, "get_weather"),
600 other => panic!("expected Function tool choice, got {other:?}"),
601 }
602 }
603
604 #[test]
605 fn tool_choice_absent_is_none() {
606 let req: CreateResponse =
607 serde_json::from_value(serde_json::json!({"input": "hi"})).unwrap();
608 assert!(req.tool_choice.is_none());
609 }
610
611 #[test]
614 fn reasoning_input_without_id_deserializes() {
615 let json = serde_json::json!({
617 "type": "reasoning",
618 "summary": [{"type": "summary_text", "text": "thinking"}],
619 });
620 match serde_json::from_value::<InputItem>(json).expect("should deserialize") {
621 InputItem::Item(Item::Reasoning(r)) => {
622 assert!(r.id.is_none());
623 assert_eq!(r.summary.len(), 1);
624 }
625 other => panic!("expected Item::Reasoning, got {other:?}"),
626 }
627 }
628
629 #[test]
630 fn reasoning_input_encrypted_without_id_or_summary_deserializes() {
631 let json = serde_json::json!({
632 "type": "reasoning",
633 "encrypted_content": "AB==",
634 });
635 match serde_json::from_value::<InputItem>(json).expect("should deserialize") {
636 InputItem::Item(Item::Reasoning(r)) => {
637 assert!(r.id.is_none());
638 assert!(r.summary.is_empty());
639 assert_eq!(r.encrypted_content.as_deref(), Some("AB=="));
640 }
641 other => panic!("expected Item::Reasoning, got {other:?}"),
642 }
643 }
644
645 #[test]
646 fn reasoning_input_with_id_still_works() {
647 let json = serde_json::json!({
648 "type": "reasoning",
649 "id": "rs_1",
650 "summary": [{"type": "summary_text", "text": "x"}],
651 "status": "completed",
652 });
653 match serde_json::from_value::<InputItem>(json).expect("should deserialize") {
654 InputItem::Item(Item::Reasoning(r)) => assert_eq!(r.id.as_deref(), Some("rs_1")),
655 other => panic!("expected Item::Reasoning, got {other:?}"),
656 }
657 }
658
659 #[test]
660 fn full_request_with_idless_reasoning_item_deserializes() {
661 let req: Result<CreateResponse, _> = serde_json::from_value(serde_json::json!({
664 "model": "m",
665 "input": [
666 {"role": "user", "content": "hi"},
667 {"type": "reasoning", "summary": [{"type": "summary_text", "text": "x"}]},
668 ],
669 }));
670 assert!(
671 req.is_ok(),
672 "idless reasoning input should deserialize: {req:?}"
673 );
674 }
675
676 #[test]
677 fn codex_agent_message_normalizes_to_user_message() {
678 let req: CreateResponse = serde_json::from_value(serde_json::json!({
679 "input": [{
680 "type": "agent_message",
681 "author": "/root",
682 "recipient": "/root/worker",
683 "content": [
684 {"type": "input_text", "text": "First."},
685 {"type": "input_text", "text": "Second."},
686 ],
687 }],
688 }))
689 .expect("Codex agent message should deserialize");
690
691 let InputParam::Items(items) = req.input else {
692 panic!("expected items");
693 };
694 assert!(matches!(
695 &items[0],
696 InputItem::EasyMessage(EasyInputMessage {
697 role: Role::User,
698 content: EasyInputContent::Text(text),
699 ..
700 }) if text == "First.\nSecond."
701 ));
702 }
703
704 #[test]
705 fn codex_agent_message_string_content_normalizes_to_user_message() {
706 let item: InputItem = serde_json::from_value(serde_json::json!({
707 "type": "agent_message",
708 "author": "/root",
709 "recipient": "/root/worker",
710 "content": "Return exactly OK.",
711 }))
712 .expect("Codex agent message with string content should deserialize");
713
714 assert!(matches!(
715 item,
716 InputItem::EasyMessage(EasyInputMessage {
717 content: EasyInputContent::Text(text),
718 ..
719 }) if text == "Return exactly OK."
720 ));
721 }
722
723 #[test]
724 fn codex_agent_message_normalizes_encrypted_content() {
725 let req: CreateResponse = serde_json::from_value(serde_json::json!({
726 "input": [{
727 "type": "agent_message",
728 "content": [
729 {"type": "input_text", "text": "Payload:"},
730 {"type": "encrypted_content", "encrypted_content": "Return exactly OK."},
731 ],
732 }],
733 }))
734 .expect("Codex agent message with encrypted content should deserialize");
735
736 let InputParam::Items(items) = req.input else {
737 panic!("expected items");
738 };
739 assert!(matches!(
740 &items[0],
741 InputItem::EasyMessage(EasyInputMessage {
742 content: EasyInputContent::Text(text),
743 ..
744 }) if text == "Payload:\nReturn exactly OK."
745 ));
746 }
747
748 #[test]
749 fn codex_agent_message_missing_content_normalizes_empty() {
750 let item: InputItem = serde_json::from_value(serde_json::json!({
751 "type": "agent_message",
752 "author": "/root",
753 "recipient": "/root/worker",
754 }))
755 .expect("Codex agent message without content should deserialize");
756 assert!(matches!(
757 item,
758 InputItem::EasyMessage(EasyInputMessage {
759 content: EasyInputContent::Text(text),
760 ..
761 }) if text.is_empty()
762 ));
763 }
764
765 #[test]
766 fn codex_agent_message_null_content_normalizes_empty() {
767 let item: InputItem = serde_json::from_value(serde_json::json!({
768 "type": "agent_message",
769 "author": "/root",
770 "recipient": "/root/worker",
771 "content": null,
772 }))
773 .expect("Codex agent message with null content should deserialize");
774 assert!(matches!(
775 item,
776 InputItem::EasyMessage(EasyInputMessage {
777 content: EasyInputContent::Text(text),
778 ..
779 }) if text.is_empty()
780 ));
781 }
782
783 #[test]
784 fn relaxed_assistant_message_without_id_or_status() {
785 let json = serde_json::json!({
786 "type": "message",
787 "role": "assistant",
788 "content": [{"type": "output_text", "text": "hi"}]
789 });
790 let item: InputItem = serde_json::from_value(json).unwrap();
791 match item {
792 InputItem::Item(Item::Message(MessageItem::Output(out))) => {
793 assert_eq!(out.role, AssistantRole::Assistant);
794 assert!(out.id.is_none());
795 assert!(out.status.is_none());
796 }
797 other => panic!("expected Item::Message(Output), got {other:?}"),
798 }
799 }
800
801 #[test]
802 fn input_image_without_detail_defaults_to_auto() {
803 let json = serde_json::json!({
804 "type": "input_image",
805 "image_url": "https://example.com/cat.jpg"
806 });
807 let content: InputContent = serde_json::from_value(json).unwrap();
808 match content {
809 InputContent::InputImage(img) => assert_eq!(img.detail, ImageDetail::Auto),
810 other => panic!("expected InputImage, got {other:?}"),
811 }
812 }
813
814 #[test]
815 fn input_image_with_explicit_null_detail_defaults_to_auto() {
816 let json = serde_json::json!({
817 "type": "input_image",
818 "image_url": "https://example.com/cat.jpg",
819 "detail": null
820 });
821 let content: InputContent = serde_json::from_value(json).unwrap();
822 match content {
823 InputContent::InputImage(img) => assert_eq!(img.detail, ImageDetail::Auto),
824 other => panic!("expected InputImage, got {other:?}"),
825 }
826 }
827
828 #[test]
829 fn assistant_message_without_content_field_deserializes() {
830 let json = serde_json::json!({
834 "type": "message",
835 "role": "assistant"
836 });
837 let item: InputItem = serde_json::from_value(json).unwrap();
838 match item {
839 InputItem::Item(Item::Message(MessageItem::Output(out))) => {
840 assert_eq!(out.role, AssistantRole::Assistant);
841 assert!(out.content.is_empty());
842 assert!(out.id.is_none());
843 assert!(out.status.is_none());
844 }
845 other => panic!("expected Item::Message(Output), got {other:?}"),
846 }
847 }
848
849 #[test]
850 fn assistant_message_with_explicit_null_content_deserializes() {
851 let json = serde_json::json!({
855 "type": "message",
856 "role": "assistant",
857 "content": null
858 });
859 let item: InputItem = serde_json::from_value(json).unwrap();
860 match item {
861 InputItem::Item(Item::Message(MessageItem::Output(out))) => {
862 assert!(out.content.is_empty());
863 }
864 other => panic!("expected Item::Message(Output), got {other:?}"),
865 }
866 }
867
868 #[test]
869 fn mcp_call_item_deserializes() {
870 let json = serde_json::json!({
873 "type": "mcp_call",
874 "id": "mcp_1",
875 "server_label": "srv",
876 "name": "t",
877 "arguments": "{}"
878 });
879 let item: InputItem = serde_json::from_value(json).unwrap();
880 assert!(matches!(item, InputItem::Item(Item::McpCall(_))));
881 }
882
883 #[test]
884 fn strict_assistant_message_still_deserializes() {
885 let json = serde_json::json!({
886 "type": "message",
887 "role": "assistant",
888 "id": "msg_1",
889 "status": "completed",
890 "content": [{"type": "output_text", "text": "hi", "annotations": []}]
891 });
892 let item: InputItem = serde_json::from_value(json).unwrap();
893 match item {
894 InputItem::Item(Item::Message(MessageItem::Output(out))) => {
895 assert_eq!(out.id.as_deref(), Some("msg_1"));
896 assert_eq!(out.status, Some(OutputStatus::Completed));
897 }
898 other => panic!("expected Item::Message(Output), got {other:?}"),
899 }
900 }
901
902 #[test]
903 fn user_message_routes_to_input_variant() {
904 let json = serde_json::json!({
905 "type": "message",
906 "role": "user",
907 "content": [{"type": "input_text", "text": "hi"}]
908 });
909 let item: InputItem = serde_json::from_value(json).unwrap();
910 assert!(matches!(
911 item,
912 InputItem::Item(Item::Message(MessageItem::Input(_)))
913 ));
914 }
915
916 #[test]
917 fn function_call_item_still_deserializes() {
918 let json = serde_json::json!({
919 "type": "function_call",
920 "call_id": "c",
921 "name": "f",
922 "arguments": "{}"
923 });
924 let item: InputItem = serde_json::from_value(json).unwrap();
925 assert!(matches!(item, InputItem::Item(Item::FunctionCall(_))));
926 }
927
928 #[test]
929 fn easy_message_string_content_routes_to_easymessage() {
930 let json = serde_json::json!({"role": "assistant", "content": "x"});
931 let item: InputItem = serde_json::from_value(json).unwrap();
932 assert!(matches!(item, InputItem::EasyMessage(_)));
933 }
934
935 #[test]
936 fn output_text_without_annotations_defaults_empty() {
937 let json = serde_json::json!({"type": "output_text", "text": "hi"});
938 let part: InputOutputMessageContent = serde_json::from_value(json).unwrap();
939 match part {
940 InputOutputMessageContent::OutputText(t) => {
941 assert!(t.annotations.is_empty());
942 }
943 _ => panic!("expected OutputText"),
944 }
945 }
946
947 #[test]
948 fn output_text_with_explicit_null_annotations_deserializes_as_empty() {
949 let json = serde_json::json!({"type": "output_text", "text": "hi", "annotations": null});
953 let part: InputOutputMessageContent = serde_json::from_value(json).unwrap();
954 match part {
955 InputOutputMessageContent::OutputText(t) => {
956 assert!(t.annotations.is_empty());
957 }
958 _ => panic!("expected OutputText"),
959 }
960 }
961
962 #[test]
963 fn assistant_message_with_explicit_null_id_and_status_deserializes() {
964 let json = serde_json::json!({
969 "type": "message",
970 "role": "assistant",
971 "id": null,
972 "status": null,
973 "content": [{"type": "output_text", "text": "hi", "annotations": null}]
974 });
975 let item: InputItem = serde_json::from_value(json).unwrap();
976 match item {
977 InputItem::Item(Item::Message(MessageItem::Output(out))) => {
978 assert!(out.id.is_none());
979 assert!(out.status.is_none());
980 assert_eq!(out.content.len(), 1);
981 }
982 other => panic!("expected Item::Message(Output), got {other:?}"),
983 }
984 }
985
986 #[test]
987 fn create_response_roundtrip_with_relaxed_input() {
988 let body = serde_json::json!({
989 "model": "m",
990 "input": [
991 {"type": "message", "role": "user", "content": [
992 {"type": "input_text", "text": "hi"}
993 ]},
994 {"type": "function_call", "call_id": "c", "name": "f", "arguments": "{}"},
995 {"type": "message", "role": "assistant", "content": [
996 {"type": "output_text", "text": "\n\n"}
997 ]},
998 {"type": "function_call_output", "call_id": "c", "output": "x"}
999 ]
1000 });
1001
1002 let req: CreateResponse = serde_json::from_value(body).unwrap();
1003 let items = match &req.input {
1004 InputParam::Items(items) => items,
1005 _ => panic!("expected Items"),
1006 };
1007 assert_eq!(items.len(), 4);
1008 assert!(matches!(
1009 items[2],
1010 InputItem::Item(Item::Message(MessageItem::Output(_)))
1011 ));
1012 }
1013
1014 #[test]
1022 fn easy_message_multimodal_without_type_routes_to_easymessage() {
1023 let json = serde_json::json!({
1026 "role": "user",
1027 "content": [
1028 {"type": "input_image", "image_url": "data:image/png;base64,abc"}
1029 ]
1030 });
1031 let item: InputItem = serde_json::from_value(json).unwrap();
1032 match item {
1033 InputItem::EasyMessage(easy) => {
1034 assert_eq!(easy.role, Role::User);
1035 assert_eq!(easy.r#type, MessageType::Message);
1036 match easy.content {
1037 EasyInputContent::ContentList(parts) => {
1038 assert_eq!(parts.len(), 1);
1039 match &parts[0] {
1040 InputContent::InputImage(img) => {
1041 assert_eq!(img.detail, ImageDetail::Auto);
1042 assert_eq!(
1043 img.image_url.as_deref(),
1044 Some("data:image/png;base64,abc")
1045 );
1046 }
1047 other => panic!("expected InputImage, got {other:?}"),
1048 }
1049 }
1050 other => panic!("expected ContentList, got {other:?}"),
1051 }
1052 }
1053 other => panic!("expected EasyMessage, got {other:?}"),
1054 }
1055 }
1056
1057 #[test]
1058 fn easy_message_multimodal_with_explicit_null_detail() {
1059 let json = serde_json::json!({
1063 "role": "user",
1064 "content": [
1065 {"type": "input_image", "image_url": "data:image/png;base64,abc", "detail": null}
1066 ]
1067 });
1068 let item: InputItem = serde_json::from_value(json).unwrap();
1069 assert!(matches!(item, InputItem::EasyMessage(_)));
1070 }
1071
1072 #[test]
1073 fn easy_message_assistant_multimodal_without_type() {
1074 let json = serde_json::json!({
1078 "role": "assistant",
1079 "content": [
1080 {"type": "input_text", "text": "ok"}
1081 ]
1082 });
1083 let item: InputItem = serde_json::from_value(json).unwrap();
1084 match item {
1085 InputItem::EasyMessage(easy) => {
1086 assert_eq!(easy.role, Role::Assistant);
1087 }
1088 other => panic!("expected EasyMessage(assistant), got {other:?}"),
1089 }
1090 }
1091
1092 #[test]
1093 fn easy_message_text_only_without_type_unchanged() {
1094 let json = serde_json::json!({"role": "user", "content": "Hello"});
1099 let item: InputItem = serde_json::from_value(json).unwrap();
1100 match item {
1101 InputItem::EasyMessage(easy) => {
1102 assert_eq!(easy.role, Role::User);
1103 assert!(matches!(easy.content, EasyInputContent::Text(ref s) if s == "Hello"));
1104 }
1105 other => panic!("expected EasyMessage(Text), got {other:?}"),
1106 }
1107 }
1108
1109 #[test]
1110 fn easy_message_with_explicit_type_still_routes_to_item_message() {
1111 let json = serde_json::json!({
1115 "type": "message",
1116 "role": "user",
1117 "content": [
1118 {"type": "input_image", "image_url": "data:image/png;base64,abc"}
1119 ]
1120 });
1121 let item: InputItem = serde_json::from_value(json).unwrap();
1122 match item {
1123 InputItem::Item(Item::Message(MessageItem::Input(msg))) => {
1124 assert_eq!(msg.role, InputRole::User);
1125 assert_eq!(msg.content.len(), 1);
1126 }
1127 other => panic!("expected Item::Message(Input), got {other:?}"),
1128 }
1129 }
1130
1131 #[test]
1132 fn create_response_roundtrip_aiperf_pre_pr931_payload() {
1133 let body = serde_json::json!({
1138 "model": "Qwen/Qwen2-VL-2B-Instruct",
1139 "input": [
1140 {
1141 "role": "user",
1142 "content": [
1143 {"type": "input_text", "text": "Describe"},
1144 {"type": "input_image", "image_url": "data:image/png;base64,abc"}
1145 ]
1146 },
1147 {
1148 "role": "assistant",
1149 "content": [{"type": "input_text", "text": "ok"}]
1150 },
1151 {
1152 "role": "user",
1153 "content": [{"type": "input_text", "text": "Now describe a different one."}]
1154 }
1155 ]
1156 });
1157 let req: CreateResponse = serde_json::from_value(body).unwrap();
1158 let items = match &req.input {
1159 InputParam::Items(items) => items,
1160 _ => panic!("expected Items"),
1161 };
1162 assert_eq!(items.len(), 3);
1163 for (idx, item) in items.iter().enumerate() {
1165 assert!(
1166 matches!(item, InputItem::EasyMessage(_)),
1167 "turn {idx} did not route to EasyMessage: {item:?}",
1168 );
1169 }
1170 }
1171}