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 {
358 #[serde(rename = "encrypted_content")]
359 _encrypted_content: String,
360 },
361}
362
363#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
366#[serde(tag = "type", rename_all = "snake_case")]
367pub enum Item {
368 Message(MessageItem),
369 FileSearchCall(FileSearchToolCall),
370 ComputerCall(ComputerToolCall),
371 ComputerCallOutput(ComputerCallOutputItemParam),
372 WebSearchCall(WebSearchToolCall),
373 FunctionCall(FunctionToolCall),
374 FunctionCallOutput(FunctionCallOutputItemParam),
375 ToolSearchCall(ToolSearchCallItemParam),
376 ToolSearchOutput(ToolSearchOutputItemParam),
377 Reasoning(InputReasoningItem),
378 Compaction(CompactionSummaryItemParam),
379 ImageGenerationCall(ImageGenToolCall),
380 CodeInterpreterCall(CodeInterpreterToolCall),
381 LocalShellCall(LocalShellToolCall),
382 LocalShellCallOutput(LocalShellToolCallOutput),
383 ShellCall(FunctionShellCallItemParam),
384 ShellCallOutput(FunctionShellCallOutputItemParam),
385 ApplyPatchCall(ApplyPatchToolCallItemParam),
386 ApplyPatchCallOutput(ApplyPatchToolCallOutputItemParam),
387 McpListTools(MCPListTools),
388 McpApprovalRequest(MCPApprovalRequest),
389 McpApprovalResponse(MCPApprovalResponse),
390 McpCall(MCPToolCall),
391 CustomToolCallOutput(CustomToolCallOutput),
392 CustomToolCall(CustomToolCall),
393}
394
395#[derive(Debug, Serialize, Clone, PartialEq)]
397#[serde(untagged)]
398pub enum InputItem {
399 ItemReference(ItemReference),
400 Item(Item),
401 EasyMessage(EasyInputMessage),
402}
403
404#[derive(Deserialize)]
405#[serde(untagged)]
406enum InputItemWire {
407 ItemReference(ItemReference),
408 Item(Item),
409 EasyMessage(EasyInputMessage),
410}
411
412impl<'de> Deserialize<'de> for InputItem {
413 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
414 where
415 D: serde::Deserializer<'de>,
416 {
417 let value = serde_json::Value::deserialize(deserializer)?;
418 if value.get("type").and_then(serde_json::Value::as_str) == Some("agent_message") {
419 let message = CodexAgentMessage::deserialize(value).map_err(de::Error::custom)?;
420 return normalize_codex_agent_message(message).map_err(de::Error::custom);
421 }
422
423 match InputItemWire::deserialize(value).map_err(de::Error::custom)? {
424 InputItemWire::ItemReference(item) => Ok(Self::ItemReference(item)),
425 InputItemWire::Item(item) => Ok(Self::Item(item)),
426 InputItemWire::EasyMessage(message) => Ok(Self::EasyMessage(message)),
427 }
428 }
429}
430
431fn normalize_codex_agent_message(message: CodexAgentMessage) -> Result<InputItem, &'static str> {
432 let content = match message.content {
433 None => String::new(),
434 Some(CodexAgentMessageContent::Text(text)) => text,
435 Some(CodexAgentMessageContent::Parts(parts)) => parts
436 .into_iter()
437 .map(|part| match part {
438 CodexAgentMessageInputContent::InputText(part) => Ok(part.text),
439 CodexAgentMessageInputContent::EncryptedContent { .. } => {
440 Err("Codex agent_message with encrypted content is unsupported")
441 }
442 })
443 .collect::<Result<Vec<_>, _>>()?
444 .join("\n"),
445 };
446 Ok(InputItem::EasyMessage(EasyInputMessage {
447 r#type: MessageType::Message,
448 role: Role::User,
449 content: EasyInputContent::Text(content),
450 phase: None,
451 }))
452}
453
454#[derive(Debug, Serialize, Clone, PartialEq)]
456#[serde(untagged)]
457pub enum InputParam {
458 Text(String),
459 Items(Vec<InputItem>),
460}
461
462impl<'de> Deserialize<'de> for InputParam {
463 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
464 where
465 D: serde::Deserializer<'de>,
466 {
467 match serde_json::Value::deserialize(deserializer)? {
468 serde_json::Value::String(text) => Ok(Self::Text(text)),
469 serde_json::Value::Array(items) => {
470 serde_json::from_value(serde_json::Value::Array(items))
471 .map(Self::Items)
472 .map_err(de::Error::custom)
473 }
474 _ => Err(de::Error::custom(
475 "input must be a string or an array of input items",
476 )),
477 }
478 }
479}
480
481impl Default for InputParam {
482 fn default() -> Self {
483 Self::Text(String::new())
484 }
485}
486
487#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)]
496pub struct CreateResponse {
497 #[serde(skip_serializing_if = "Option::is_none")]
498 pub background: Option<bool>,
499 #[serde(skip_serializing_if = "Option::is_none")]
500 pub conversation: Option<ConversationParam>,
501 #[serde(skip_serializing_if = "Option::is_none")]
502 pub include: Option<Vec<IncludeEnum>>,
503 pub input: InputParam,
504 #[serde(skip_serializing_if = "Option::is_none")]
505 pub instructions: Option<String>,
506 #[serde(skip_serializing_if = "Option::is_none")]
507 pub max_output_tokens: Option<u32>,
508 #[serde(skip_serializing_if = "Option::is_none")]
509 pub max_tool_calls: Option<u32>,
510 #[serde(skip_serializing_if = "Option::is_none")]
511 pub metadata: Option<HashMap<String, String>>,
512 #[serde(skip_serializing_if = "Option::is_none")]
513 pub model: Option<String>,
514 #[serde(skip_serializing_if = "Option::is_none")]
515 pub parallel_tool_calls: Option<bool>,
516 #[serde(skip_serializing_if = "Option::is_none")]
517 pub previous_response_id: Option<String>,
518 #[serde(skip_serializing_if = "Option::is_none")]
519 pub prompt: Option<Prompt>,
520 #[serde(skip_serializing_if = "Option::is_none")]
521 pub prompt_cache_key: Option<String>,
522 #[serde(skip_serializing_if = "Option::is_none")]
523 pub prompt_cache_retention: Option<PromptCacheRetention>,
524 #[serde(skip_serializing_if = "Option::is_none")]
525 pub reasoning: Option<Reasoning>,
526 #[serde(skip_serializing_if = "Option::is_none")]
527 pub safety_identifier: Option<String>,
528 #[serde(skip_serializing_if = "Option::is_none")]
529 pub service_tier: Option<ServiceTier>,
530 #[serde(skip_serializing_if = "Option::is_none")]
531 pub store: Option<bool>,
532 #[serde(skip_serializing_if = "Option::is_none")]
533 pub stream: Option<bool>,
534 #[serde(skip_serializing_if = "Option::is_none")]
535 pub stream_options: Option<ResponseStreamOptions>,
536 #[serde(skip_serializing_if = "Option::is_none")]
537 pub temperature: Option<f32>,
538 #[serde(skip_serializing_if = "Option::is_none")]
539 pub text: Option<ResponseTextParam>,
540 #[serde(
541 default,
542 deserialize_with = "deserialize_tool_choice",
543 skip_serializing_if = "Option::is_none"
544 )]
545 pub tool_choice: Option<ToolChoiceParam>,
546 #[serde(skip_serializing_if = "Option::is_none")]
547 pub tools: Option<Vec<Tool>>,
548 #[serde(skip_serializing_if = "Option::is_none")]
549 pub top_logprobs: Option<u8>,
550 #[serde(skip_serializing_if = "Option::is_none")]
551 pub top_p: Option<f32>,
552 #[serde(skip_serializing_if = "Option::is_none")]
553 pub truncation: Option<Truncation>,
554}
555
556#[cfg(test)]
557mod tests {
558 use super::*;
559
560 fn tool_choice_of(json: serde_json::Value) -> Option<ToolChoiceParam> {
563 let req: CreateResponse = serde_json::from_value(serde_json::json!({
564 "input": "hi",
565 "tool_choice": json,
566 }))
567 .expect("CreateResponse should deserialize");
568 req.tool_choice
569 }
570
571 #[test]
572 fn tool_choice_mode_object_coerces_to_mode() {
573 assert_eq!(
576 tool_choice_of(serde_json::json!({"type": "auto", "disable_parallel_tool_use": true})),
577 Some(ToolChoiceParam::Mode(ToolChoiceOptions::Auto)),
578 );
579 assert_eq!(
580 tool_choice_of(serde_json::json!({"type": "none"})),
581 Some(ToolChoiceParam::Mode(ToolChoiceOptions::None)),
582 );
583 assert_eq!(
584 tool_choice_of(serde_json::json!({"type": "required"})),
585 Some(ToolChoiceParam::Mode(ToolChoiceOptions::Required)),
586 );
587 }
588
589 #[test]
590 fn tool_choice_bare_string_still_works() {
591 assert_eq!(
592 tool_choice_of(serde_json::json!("auto")),
593 Some(ToolChoiceParam::Mode(ToolChoiceOptions::Auto)),
594 );
595 }
596
597 #[test]
598 fn tool_choice_specific_function_object_still_works() {
599 match tool_choice_of(serde_json::json!({"type": "function", "name": "get_weather"})) {
602 Some(ToolChoiceParam::Function(f)) => assert_eq!(f.name, "get_weather"),
603 other => panic!("expected Function tool choice, got {other:?}"),
604 }
605 }
606
607 #[test]
608 fn tool_choice_absent_is_none() {
609 let req: CreateResponse =
610 serde_json::from_value(serde_json::json!({"input": "hi"})).unwrap();
611 assert!(req.tool_choice.is_none());
612 }
613
614 #[test]
617 fn reasoning_input_without_id_deserializes() {
618 let json = serde_json::json!({
620 "type": "reasoning",
621 "summary": [{"type": "summary_text", "text": "thinking"}],
622 });
623 match serde_json::from_value::<InputItem>(json).expect("should deserialize") {
624 InputItem::Item(Item::Reasoning(r)) => {
625 assert!(r.id.is_none());
626 assert_eq!(r.summary.len(), 1);
627 }
628 other => panic!("expected Item::Reasoning, got {other:?}"),
629 }
630 }
631
632 #[test]
633 fn reasoning_input_encrypted_without_id_or_summary_deserializes() {
634 let json = serde_json::json!({
635 "type": "reasoning",
636 "encrypted_content": "AB==",
637 });
638 match serde_json::from_value::<InputItem>(json).expect("should deserialize") {
639 InputItem::Item(Item::Reasoning(r)) => {
640 assert!(r.id.is_none());
641 assert!(r.summary.is_empty());
642 assert_eq!(r.encrypted_content.as_deref(), Some("AB=="));
643 }
644 other => panic!("expected Item::Reasoning, got {other:?}"),
645 }
646 }
647
648 #[test]
649 fn reasoning_input_with_id_still_works() {
650 let json = serde_json::json!({
651 "type": "reasoning",
652 "id": "rs_1",
653 "summary": [{"type": "summary_text", "text": "x"}],
654 "status": "completed",
655 });
656 match serde_json::from_value::<InputItem>(json).expect("should deserialize") {
657 InputItem::Item(Item::Reasoning(r)) => assert_eq!(r.id.as_deref(), Some("rs_1")),
658 other => panic!("expected Item::Reasoning, got {other:?}"),
659 }
660 }
661
662 #[test]
663 fn full_request_with_idless_reasoning_item_deserializes() {
664 let req: Result<CreateResponse, _> = serde_json::from_value(serde_json::json!({
667 "model": "m",
668 "input": [
669 {"role": "user", "content": "hi"},
670 {"type": "reasoning", "summary": [{"type": "summary_text", "text": "x"}]},
671 ],
672 }));
673 assert!(
674 req.is_ok(),
675 "idless reasoning input should deserialize: {req:?}"
676 );
677 }
678
679 #[test]
680 fn codex_agent_message_normalizes_to_user_message() {
681 let req: CreateResponse = serde_json::from_value(serde_json::json!({
682 "input": [{
683 "type": "agent_message",
684 "author": "/root",
685 "recipient": "/root/worker",
686 "content": [
687 {"type": "input_text", "text": "First."},
688 {"type": "input_text", "text": "Second."},
689 ],
690 }],
691 }))
692 .expect("Codex agent message should deserialize");
693
694 let InputParam::Items(items) = req.input else {
695 panic!("expected items");
696 };
697 assert!(matches!(
698 &items[0],
699 InputItem::EasyMessage(EasyInputMessage {
700 role: Role::User,
701 content: EasyInputContent::Text(text),
702 ..
703 }) if text == "First.\nSecond."
704 ));
705 }
706
707 #[test]
708 fn codex_agent_message_string_content_normalizes_to_user_message() {
709 let item: InputItem = serde_json::from_value(serde_json::json!({
710 "type": "agent_message",
711 "author": "/root",
712 "recipient": "/root/worker",
713 "content": "Return exactly OK.",
714 }))
715 .expect("Codex agent message with string content should deserialize");
716
717 assert!(matches!(
718 item,
719 InputItem::EasyMessage(EasyInputMessage {
720 content: EasyInputContent::Text(text),
721 ..
722 }) if text == "Return exactly OK."
723 ));
724 }
725
726 #[test]
727 fn codex_agent_message_rejects_encrypted_content() {
728 let error = serde_json::from_value::<CreateResponse>(serde_json::json!({
729 "input": [{
730 "type": "agent_message",
731 "content": [{"type": "encrypted_content", "encrypted_content": "AB=="}],
732 }],
733 }))
734 .expect_err("encrypted agent messages must not be accepted");
735 assert!(
736 error
737 .to_string()
738 .contains("encrypted content is unsupported")
739 );
740 }
741
742 #[test]
743 fn codex_agent_message_missing_content_normalizes_empty() {
744 let item: InputItem = serde_json::from_value(serde_json::json!({
745 "type": "agent_message",
746 "author": "/root",
747 "recipient": "/root/worker",
748 }))
749 .expect("Codex agent message without content should deserialize");
750 assert!(matches!(
751 item,
752 InputItem::EasyMessage(EasyInputMessage {
753 content: EasyInputContent::Text(text),
754 ..
755 }) if text.is_empty()
756 ));
757 }
758
759 #[test]
760 fn codex_agent_message_null_content_normalizes_empty() {
761 let item: InputItem = serde_json::from_value(serde_json::json!({
762 "type": "agent_message",
763 "author": "/root",
764 "recipient": "/root/worker",
765 "content": null,
766 }))
767 .expect("Codex agent message with null content should deserialize");
768 assert!(matches!(
769 item,
770 InputItem::EasyMessage(EasyInputMessage {
771 content: EasyInputContent::Text(text),
772 ..
773 }) if text.is_empty()
774 ));
775 }
776
777 #[test]
778 fn relaxed_assistant_message_without_id_or_status() {
779 let json = serde_json::json!({
780 "type": "message",
781 "role": "assistant",
782 "content": [{"type": "output_text", "text": "hi"}]
783 });
784 let item: InputItem = serde_json::from_value(json).unwrap();
785 match item {
786 InputItem::Item(Item::Message(MessageItem::Output(out))) => {
787 assert_eq!(out.role, AssistantRole::Assistant);
788 assert!(out.id.is_none());
789 assert!(out.status.is_none());
790 }
791 other => panic!("expected Item::Message(Output), got {other:?}"),
792 }
793 }
794
795 #[test]
796 fn input_image_without_detail_defaults_to_auto() {
797 let json = serde_json::json!({
798 "type": "input_image",
799 "image_url": "https://example.com/cat.jpg"
800 });
801 let content: InputContent = serde_json::from_value(json).unwrap();
802 match content {
803 InputContent::InputImage(img) => assert_eq!(img.detail, ImageDetail::Auto),
804 other => panic!("expected InputImage, got {other:?}"),
805 }
806 }
807
808 #[test]
809 fn input_image_with_explicit_null_detail_defaults_to_auto() {
810 let json = serde_json::json!({
811 "type": "input_image",
812 "image_url": "https://example.com/cat.jpg",
813 "detail": null
814 });
815 let content: InputContent = serde_json::from_value(json).unwrap();
816 match content {
817 InputContent::InputImage(img) => assert_eq!(img.detail, ImageDetail::Auto),
818 other => panic!("expected InputImage, got {other:?}"),
819 }
820 }
821
822 #[test]
823 fn assistant_message_without_content_field_deserializes() {
824 let json = serde_json::json!({
828 "type": "message",
829 "role": "assistant"
830 });
831 let item: InputItem = serde_json::from_value(json).unwrap();
832 match item {
833 InputItem::Item(Item::Message(MessageItem::Output(out))) => {
834 assert_eq!(out.role, AssistantRole::Assistant);
835 assert!(out.content.is_empty());
836 assert!(out.id.is_none());
837 assert!(out.status.is_none());
838 }
839 other => panic!("expected Item::Message(Output), got {other:?}"),
840 }
841 }
842
843 #[test]
844 fn assistant_message_with_explicit_null_content_deserializes() {
845 let json = serde_json::json!({
849 "type": "message",
850 "role": "assistant",
851 "content": null
852 });
853 let item: InputItem = serde_json::from_value(json).unwrap();
854 match item {
855 InputItem::Item(Item::Message(MessageItem::Output(out))) => {
856 assert!(out.content.is_empty());
857 }
858 other => panic!("expected Item::Message(Output), got {other:?}"),
859 }
860 }
861
862 #[test]
863 fn mcp_call_item_deserializes() {
864 let json = serde_json::json!({
867 "type": "mcp_call",
868 "id": "mcp_1",
869 "server_label": "srv",
870 "name": "t",
871 "arguments": "{}"
872 });
873 let item: InputItem = serde_json::from_value(json).unwrap();
874 assert!(matches!(item, InputItem::Item(Item::McpCall(_))));
875 }
876
877 #[test]
878 fn strict_assistant_message_still_deserializes() {
879 let json = serde_json::json!({
880 "type": "message",
881 "role": "assistant",
882 "id": "msg_1",
883 "status": "completed",
884 "content": [{"type": "output_text", "text": "hi", "annotations": []}]
885 });
886 let item: InputItem = serde_json::from_value(json).unwrap();
887 match item {
888 InputItem::Item(Item::Message(MessageItem::Output(out))) => {
889 assert_eq!(out.id.as_deref(), Some("msg_1"));
890 assert_eq!(out.status, Some(OutputStatus::Completed));
891 }
892 other => panic!("expected Item::Message(Output), got {other:?}"),
893 }
894 }
895
896 #[test]
897 fn user_message_routes_to_input_variant() {
898 let json = serde_json::json!({
899 "type": "message",
900 "role": "user",
901 "content": [{"type": "input_text", "text": "hi"}]
902 });
903 let item: InputItem = serde_json::from_value(json).unwrap();
904 assert!(matches!(
905 item,
906 InputItem::Item(Item::Message(MessageItem::Input(_)))
907 ));
908 }
909
910 #[test]
911 fn function_call_item_still_deserializes() {
912 let json = serde_json::json!({
913 "type": "function_call",
914 "call_id": "c",
915 "name": "f",
916 "arguments": "{}"
917 });
918 let item: InputItem = serde_json::from_value(json).unwrap();
919 assert!(matches!(item, InputItem::Item(Item::FunctionCall(_))));
920 }
921
922 #[test]
923 fn easy_message_string_content_routes_to_easymessage() {
924 let json = serde_json::json!({"role": "assistant", "content": "x"});
925 let item: InputItem = serde_json::from_value(json).unwrap();
926 assert!(matches!(item, InputItem::EasyMessage(_)));
927 }
928
929 #[test]
930 fn output_text_without_annotations_defaults_empty() {
931 let json = serde_json::json!({"type": "output_text", "text": "hi"});
932 let part: InputOutputMessageContent = serde_json::from_value(json).unwrap();
933 match part {
934 InputOutputMessageContent::OutputText(t) => {
935 assert!(t.annotations.is_empty());
936 }
937 _ => panic!("expected OutputText"),
938 }
939 }
940
941 #[test]
942 fn output_text_with_explicit_null_annotations_deserializes_as_empty() {
943 let json = serde_json::json!({"type": "output_text", "text": "hi", "annotations": null});
947 let part: InputOutputMessageContent = serde_json::from_value(json).unwrap();
948 match part {
949 InputOutputMessageContent::OutputText(t) => {
950 assert!(t.annotations.is_empty());
951 }
952 _ => panic!("expected OutputText"),
953 }
954 }
955
956 #[test]
957 fn assistant_message_with_explicit_null_id_and_status_deserializes() {
958 let json = serde_json::json!({
963 "type": "message",
964 "role": "assistant",
965 "id": null,
966 "status": null,
967 "content": [{"type": "output_text", "text": "hi", "annotations": null}]
968 });
969 let item: InputItem = serde_json::from_value(json).unwrap();
970 match item {
971 InputItem::Item(Item::Message(MessageItem::Output(out))) => {
972 assert!(out.id.is_none());
973 assert!(out.status.is_none());
974 assert_eq!(out.content.len(), 1);
975 }
976 other => panic!("expected Item::Message(Output), got {other:?}"),
977 }
978 }
979
980 #[test]
981 fn create_response_roundtrip_with_relaxed_input() {
982 let body = serde_json::json!({
983 "model": "m",
984 "input": [
985 {"type": "message", "role": "user", "content": [
986 {"type": "input_text", "text": "hi"}
987 ]},
988 {"type": "function_call", "call_id": "c", "name": "f", "arguments": "{}"},
989 {"type": "message", "role": "assistant", "content": [
990 {"type": "output_text", "text": "\n\n"}
991 ]},
992 {"type": "function_call_output", "call_id": "c", "output": "x"}
993 ]
994 });
995
996 let req: CreateResponse = serde_json::from_value(body).unwrap();
997 let items = match &req.input {
998 InputParam::Items(items) => items,
999 _ => panic!("expected Items"),
1000 };
1001 assert_eq!(items.len(), 4);
1002 assert!(matches!(
1003 items[2],
1004 InputItem::Item(Item::Message(MessageItem::Output(_)))
1005 ));
1006 }
1007
1008 #[test]
1016 fn easy_message_multimodal_without_type_routes_to_easymessage() {
1017 let json = serde_json::json!({
1020 "role": "user",
1021 "content": [
1022 {"type": "input_image", "image_url": "data:image/png;base64,abc"}
1023 ]
1024 });
1025 let item: InputItem = serde_json::from_value(json).unwrap();
1026 match item {
1027 InputItem::EasyMessage(easy) => {
1028 assert_eq!(easy.role, Role::User);
1029 assert_eq!(easy.r#type, MessageType::Message);
1030 match easy.content {
1031 EasyInputContent::ContentList(parts) => {
1032 assert_eq!(parts.len(), 1);
1033 match &parts[0] {
1034 InputContent::InputImage(img) => {
1035 assert_eq!(img.detail, ImageDetail::Auto);
1036 assert_eq!(
1037 img.image_url.as_deref(),
1038 Some("data:image/png;base64,abc")
1039 );
1040 }
1041 other => panic!("expected InputImage, got {other:?}"),
1042 }
1043 }
1044 other => panic!("expected ContentList, got {other:?}"),
1045 }
1046 }
1047 other => panic!("expected EasyMessage, got {other:?}"),
1048 }
1049 }
1050
1051 #[test]
1052 fn easy_message_multimodal_with_explicit_null_detail() {
1053 let json = serde_json::json!({
1057 "role": "user",
1058 "content": [
1059 {"type": "input_image", "image_url": "data:image/png;base64,abc", "detail": null}
1060 ]
1061 });
1062 let item: InputItem = serde_json::from_value(json).unwrap();
1063 assert!(matches!(item, InputItem::EasyMessage(_)));
1064 }
1065
1066 #[test]
1067 fn easy_message_assistant_multimodal_without_type() {
1068 let json = serde_json::json!({
1072 "role": "assistant",
1073 "content": [
1074 {"type": "input_text", "text": "ok"}
1075 ]
1076 });
1077 let item: InputItem = serde_json::from_value(json).unwrap();
1078 match item {
1079 InputItem::EasyMessage(easy) => {
1080 assert_eq!(easy.role, Role::Assistant);
1081 }
1082 other => panic!("expected EasyMessage(assistant), got {other:?}"),
1083 }
1084 }
1085
1086 #[test]
1087 fn easy_message_text_only_without_type_unchanged() {
1088 let json = serde_json::json!({"role": "user", "content": "Hello"});
1093 let item: InputItem = serde_json::from_value(json).unwrap();
1094 match item {
1095 InputItem::EasyMessage(easy) => {
1096 assert_eq!(easy.role, Role::User);
1097 assert!(matches!(easy.content, EasyInputContent::Text(ref s) if s == "Hello"));
1098 }
1099 other => panic!("expected EasyMessage(Text), got {other:?}"),
1100 }
1101 }
1102
1103 #[test]
1104 fn easy_message_with_explicit_type_still_routes_to_item_message() {
1105 let json = serde_json::json!({
1109 "type": "message",
1110 "role": "user",
1111 "content": [
1112 {"type": "input_image", "image_url": "data:image/png;base64,abc"}
1113 ]
1114 });
1115 let item: InputItem = serde_json::from_value(json).unwrap();
1116 match item {
1117 InputItem::Item(Item::Message(MessageItem::Input(msg))) => {
1118 assert_eq!(msg.role, InputRole::User);
1119 assert_eq!(msg.content.len(), 1);
1120 }
1121 other => panic!("expected Item::Message(Input), got {other:?}"),
1122 }
1123 }
1124
1125 #[test]
1126 fn create_response_roundtrip_aiperf_pre_pr931_payload() {
1127 let body = serde_json::json!({
1132 "model": "Qwen/Qwen2-VL-2B-Instruct",
1133 "input": [
1134 {
1135 "role": "user",
1136 "content": [
1137 {"type": "input_text", "text": "Describe"},
1138 {"type": "input_image", "image_url": "data:image/png;base64,abc"}
1139 ]
1140 },
1141 {
1142 "role": "assistant",
1143 "content": [{"type": "input_text", "text": "ok"}]
1144 },
1145 {
1146 "role": "user",
1147 "content": [{"type": "input_text", "text": "Now describe a different one."}]
1148 }
1149 ]
1150 });
1151 let req: CreateResponse = serde_json::from_value(body).unwrap();
1152 let items = match &req.input {
1153 InputParam::Items(items) => items,
1154 _ => panic!("expected Items"),
1155 };
1156 assert_eq!(items.len(), 3);
1157 for (idx, item) in items.iter().enumerate() {
1159 assert!(
1160 matches!(item, InputItem::EasyMessage(_)),
1161 "turn {idx} did not route to EasyMessage: {item:?}",
1162 );
1163 }
1164 }
1165}