1use std::collections::HashMap;
32
33use serde::{Deserialize, Serialize};
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(Debug, Serialize, Deserialize, Clone, PartialEq)]
342#[serde(tag = "type", rename_all = "snake_case")]
343pub enum Item {
344 Message(MessageItem),
345 FileSearchCall(FileSearchToolCall),
346 ComputerCall(ComputerToolCall),
347 ComputerCallOutput(ComputerCallOutputItemParam),
348 WebSearchCall(WebSearchToolCall),
349 FunctionCall(FunctionToolCall),
350 FunctionCallOutput(FunctionCallOutputItemParam),
351 ToolSearchCall(ToolSearchCallItemParam),
352 ToolSearchOutput(ToolSearchOutputItemParam),
353 Reasoning(InputReasoningItem),
354 Compaction(CompactionSummaryItemParam),
355 ImageGenerationCall(ImageGenToolCall),
356 CodeInterpreterCall(CodeInterpreterToolCall),
357 LocalShellCall(LocalShellToolCall),
358 LocalShellCallOutput(LocalShellToolCallOutput),
359 ShellCall(FunctionShellCallItemParam),
360 ShellCallOutput(FunctionShellCallOutputItemParam),
361 ApplyPatchCall(ApplyPatchToolCallItemParam),
362 ApplyPatchCallOutput(ApplyPatchToolCallOutputItemParam),
363 McpListTools(MCPListTools),
364 McpApprovalRequest(MCPApprovalRequest),
365 McpApprovalResponse(MCPApprovalResponse),
366 McpCall(MCPToolCall),
367 CustomToolCallOutput(CustomToolCallOutput),
368 CustomToolCall(CustomToolCall),
369}
370
371#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
373#[serde(untagged)]
374pub enum InputItem {
375 ItemReference(ItemReference),
376 Item(Item),
377 EasyMessage(EasyInputMessage),
378}
379
380#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
382#[serde(untagged)]
383pub enum InputParam {
384 Text(String),
385 Items(Vec<InputItem>),
386}
387
388impl Default for InputParam {
389 fn default() -> Self {
390 Self::Text(String::new())
391 }
392}
393
394#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)]
403pub struct CreateResponse {
404 #[serde(skip_serializing_if = "Option::is_none")]
405 pub background: Option<bool>,
406 #[serde(skip_serializing_if = "Option::is_none")]
407 pub conversation: Option<ConversationParam>,
408 #[serde(skip_serializing_if = "Option::is_none")]
409 pub include: Option<Vec<IncludeEnum>>,
410 pub input: InputParam,
411 #[serde(skip_serializing_if = "Option::is_none")]
412 pub instructions: Option<String>,
413 #[serde(skip_serializing_if = "Option::is_none")]
414 pub max_output_tokens: Option<u32>,
415 #[serde(skip_serializing_if = "Option::is_none")]
416 pub max_tool_calls: Option<u32>,
417 #[serde(skip_serializing_if = "Option::is_none")]
418 pub metadata: Option<HashMap<String, String>>,
419 #[serde(skip_serializing_if = "Option::is_none")]
420 pub model: Option<String>,
421 #[serde(skip_serializing_if = "Option::is_none")]
422 pub parallel_tool_calls: Option<bool>,
423 #[serde(skip_serializing_if = "Option::is_none")]
424 pub previous_response_id: Option<String>,
425 #[serde(skip_serializing_if = "Option::is_none")]
426 pub prompt: Option<Prompt>,
427 #[serde(skip_serializing_if = "Option::is_none")]
428 pub prompt_cache_key: Option<String>,
429 #[serde(skip_serializing_if = "Option::is_none")]
430 pub prompt_cache_retention: Option<PromptCacheRetention>,
431 #[serde(skip_serializing_if = "Option::is_none")]
432 pub reasoning: Option<Reasoning>,
433 #[serde(skip_serializing_if = "Option::is_none")]
434 pub safety_identifier: Option<String>,
435 #[serde(skip_serializing_if = "Option::is_none")]
436 pub service_tier: Option<ServiceTier>,
437 #[serde(skip_serializing_if = "Option::is_none")]
438 pub store: Option<bool>,
439 #[serde(skip_serializing_if = "Option::is_none")]
440 pub stream: Option<bool>,
441 #[serde(skip_serializing_if = "Option::is_none")]
442 pub stream_options: Option<ResponseStreamOptions>,
443 #[serde(skip_serializing_if = "Option::is_none")]
444 pub temperature: Option<f32>,
445 #[serde(skip_serializing_if = "Option::is_none")]
446 pub text: Option<ResponseTextParam>,
447 #[serde(
448 default,
449 deserialize_with = "deserialize_tool_choice",
450 skip_serializing_if = "Option::is_none"
451 )]
452 pub tool_choice: Option<ToolChoiceParam>,
453 #[serde(skip_serializing_if = "Option::is_none")]
454 pub tools: Option<Vec<Tool>>,
455 #[serde(skip_serializing_if = "Option::is_none")]
456 pub top_logprobs: Option<u8>,
457 #[serde(skip_serializing_if = "Option::is_none")]
458 pub top_p: Option<f32>,
459 #[serde(skip_serializing_if = "Option::is_none")]
460 pub truncation: Option<Truncation>,
461}
462
463#[cfg(test)]
464mod tests {
465 use super::*;
466
467 fn tool_choice_of(json: serde_json::Value) -> Option<ToolChoiceParam> {
470 let req: CreateResponse = serde_json::from_value(serde_json::json!({
471 "input": "hi",
472 "tool_choice": json,
473 }))
474 .expect("CreateResponse should deserialize");
475 req.tool_choice
476 }
477
478 #[test]
479 fn tool_choice_mode_object_coerces_to_mode() {
480 assert_eq!(
483 tool_choice_of(serde_json::json!({"type": "auto", "disable_parallel_tool_use": true})),
484 Some(ToolChoiceParam::Mode(ToolChoiceOptions::Auto)),
485 );
486 assert_eq!(
487 tool_choice_of(serde_json::json!({"type": "none"})),
488 Some(ToolChoiceParam::Mode(ToolChoiceOptions::None)),
489 );
490 assert_eq!(
491 tool_choice_of(serde_json::json!({"type": "required"})),
492 Some(ToolChoiceParam::Mode(ToolChoiceOptions::Required)),
493 );
494 }
495
496 #[test]
497 fn tool_choice_bare_string_still_works() {
498 assert_eq!(
499 tool_choice_of(serde_json::json!("auto")),
500 Some(ToolChoiceParam::Mode(ToolChoiceOptions::Auto)),
501 );
502 }
503
504 #[test]
505 fn tool_choice_specific_function_object_still_works() {
506 match tool_choice_of(serde_json::json!({"type": "function", "name": "get_weather"})) {
509 Some(ToolChoiceParam::Function(f)) => assert_eq!(f.name, "get_weather"),
510 other => panic!("expected Function tool choice, got {other:?}"),
511 }
512 }
513
514 #[test]
515 fn tool_choice_absent_is_none() {
516 let req: CreateResponse =
517 serde_json::from_value(serde_json::json!({"input": "hi"})).unwrap();
518 assert!(req.tool_choice.is_none());
519 }
520
521 #[test]
524 fn reasoning_input_without_id_deserializes() {
525 let json = serde_json::json!({
527 "type": "reasoning",
528 "summary": [{"type": "summary_text", "text": "thinking"}],
529 });
530 match serde_json::from_value::<InputItem>(json).expect("should deserialize") {
531 InputItem::Item(Item::Reasoning(r)) => {
532 assert!(r.id.is_none());
533 assert_eq!(r.summary.len(), 1);
534 }
535 other => panic!("expected Item::Reasoning, got {other:?}"),
536 }
537 }
538
539 #[test]
540 fn reasoning_input_encrypted_without_id_or_summary_deserializes() {
541 let json = serde_json::json!({
542 "type": "reasoning",
543 "encrypted_content": "AB==",
544 });
545 match serde_json::from_value::<InputItem>(json).expect("should deserialize") {
546 InputItem::Item(Item::Reasoning(r)) => {
547 assert!(r.id.is_none());
548 assert!(r.summary.is_empty());
549 assert_eq!(r.encrypted_content.as_deref(), Some("AB=="));
550 }
551 other => panic!("expected Item::Reasoning, got {other:?}"),
552 }
553 }
554
555 #[test]
556 fn reasoning_input_with_id_still_works() {
557 let json = serde_json::json!({
558 "type": "reasoning",
559 "id": "rs_1",
560 "summary": [{"type": "summary_text", "text": "x"}],
561 "status": "completed",
562 });
563 match serde_json::from_value::<InputItem>(json).expect("should deserialize") {
564 InputItem::Item(Item::Reasoning(r)) => assert_eq!(r.id.as_deref(), Some("rs_1")),
565 other => panic!("expected Item::Reasoning, got {other:?}"),
566 }
567 }
568
569 #[test]
570 fn full_request_with_idless_reasoning_item_deserializes() {
571 let req: Result<CreateResponse, _> = serde_json::from_value(serde_json::json!({
574 "model": "m",
575 "input": [
576 {"role": "user", "content": "hi"},
577 {"type": "reasoning", "summary": [{"type": "summary_text", "text": "x"}]},
578 ],
579 }));
580 assert!(
581 req.is_ok(),
582 "idless reasoning input should deserialize: {req:?}"
583 );
584 }
585
586 #[test]
587 fn relaxed_assistant_message_without_id_or_status() {
588 let json = serde_json::json!({
589 "type": "message",
590 "role": "assistant",
591 "content": [{"type": "output_text", "text": "hi"}]
592 });
593 let item: InputItem = serde_json::from_value(json).unwrap();
594 match item {
595 InputItem::Item(Item::Message(MessageItem::Output(out))) => {
596 assert_eq!(out.role, AssistantRole::Assistant);
597 assert!(out.id.is_none());
598 assert!(out.status.is_none());
599 }
600 other => panic!("expected Item::Message(Output), got {other:?}"),
601 }
602 }
603
604 #[test]
605 fn input_image_without_detail_defaults_to_auto() {
606 let json = serde_json::json!({
607 "type": "input_image",
608 "image_url": "https://example.com/cat.jpg"
609 });
610 let content: InputContent = serde_json::from_value(json).unwrap();
611 match content {
612 InputContent::InputImage(img) => assert_eq!(img.detail, ImageDetail::Auto),
613 other => panic!("expected InputImage, got {other:?}"),
614 }
615 }
616
617 #[test]
618 fn input_image_with_explicit_null_detail_defaults_to_auto() {
619 let json = serde_json::json!({
620 "type": "input_image",
621 "image_url": "https://example.com/cat.jpg",
622 "detail": null
623 });
624 let content: InputContent = serde_json::from_value(json).unwrap();
625 match content {
626 InputContent::InputImage(img) => assert_eq!(img.detail, ImageDetail::Auto),
627 other => panic!("expected InputImage, got {other:?}"),
628 }
629 }
630
631 #[test]
632 fn assistant_message_without_content_field_deserializes() {
633 let json = serde_json::json!({
637 "type": "message",
638 "role": "assistant"
639 });
640 let item: InputItem = serde_json::from_value(json).unwrap();
641 match item {
642 InputItem::Item(Item::Message(MessageItem::Output(out))) => {
643 assert_eq!(out.role, AssistantRole::Assistant);
644 assert!(out.content.is_empty());
645 assert!(out.id.is_none());
646 assert!(out.status.is_none());
647 }
648 other => panic!("expected Item::Message(Output), got {other:?}"),
649 }
650 }
651
652 #[test]
653 fn assistant_message_with_explicit_null_content_deserializes() {
654 let json = serde_json::json!({
658 "type": "message",
659 "role": "assistant",
660 "content": null
661 });
662 let item: InputItem = serde_json::from_value(json).unwrap();
663 match item {
664 InputItem::Item(Item::Message(MessageItem::Output(out))) => {
665 assert!(out.content.is_empty());
666 }
667 other => panic!("expected Item::Message(Output), got {other:?}"),
668 }
669 }
670
671 #[test]
672 fn mcp_call_item_deserializes() {
673 let json = serde_json::json!({
676 "type": "mcp_call",
677 "id": "mcp_1",
678 "server_label": "srv",
679 "name": "t",
680 "arguments": "{}"
681 });
682 let item: InputItem = serde_json::from_value(json).unwrap();
683 assert!(matches!(item, InputItem::Item(Item::McpCall(_))));
684 }
685
686 #[test]
687 fn strict_assistant_message_still_deserializes() {
688 let json = serde_json::json!({
689 "type": "message",
690 "role": "assistant",
691 "id": "msg_1",
692 "status": "completed",
693 "content": [{"type": "output_text", "text": "hi", "annotations": []}]
694 });
695 let item: InputItem = serde_json::from_value(json).unwrap();
696 match item {
697 InputItem::Item(Item::Message(MessageItem::Output(out))) => {
698 assert_eq!(out.id.as_deref(), Some("msg_1"));
699 assert_eq!(out.status, Some(OutputStatus::Completed));
700 }
701 other => panic!("expected Item::Message(Output), got {other:?}"),
702 }
703 }
704
705 #[test]
706 fn user_message_routes_to_input_variant() {
707 let json = serde_json::json!({
708 "type": "message",
709 "role": "user",
710 "content": [{"type": "input_text", "text": "hi"}]
711 });
712 let item: InputItem = serde_json::from_value(json).unwrap();
713 assert!(matches!(
714 item,
715 InputItem::Item(Item::Message(MessageItem::Input(_)))
716 ));
717 }
718
719 #[test]
720 fn function_call_item_still_deserializes() {
721 let json = serde_json::json!({
722 "type": "function_call",
723 "call_id": "c",
724 "name": "f",
725 "arguments": "{}"
726 });
727 let item: InputItem = serde_json::from_value(json).unwrap();
728 assert!(matches!(item, InputItem::Item(Item::FunctionCall(_))));
729 }
730
731 #[test]
732 fn easy_message_string_content_routes_to_easymessage() {
733 let json = serde_json::json!({"role": "assistant", "content": "x"});
734 let item: InputItem = serde_json::from_value(json).unwrap();
735 assert!(matches!(item, InputItem::EasyMessage(_)));
736 }
737
738 #[test]
739 fn output_text_without_annotations_defaults_empty() {
740 let json = serde_json::json!({"type": "output_text", "text": "hi"});
741 let part: InputOutputMessageContent = serde_json::from_value(json).unwrap();
742 match part {
743 InputOutputMessageContent::OutputText(t) => {
744 assert!(t.annotations.is_empty());
745 }
746 _ => panic!("expected OutputText"),
747 }
748 }
749
750 #[test]
751 fn output_text_with_explicit_null_annotations_deserializes_as_empty() {
752 let json = serde_json::json!({"type": "output_text", "text": "hi", "annotations": null});
756 let part: InputOutputMessageContent = serde_json::from_value(json).unwrap();
757 match part {
758 InputOutputMessageContent::OutputText(t) => {
759 assert!(t.annotations.is_empty());
760 }
761 _ => panic!("expected OutputText"),
762 }
763 }
764
765 #[test]
766 fn assistant_message_with_explicit_null_id_and_status_deserializes() {
767 let json = serde_json::json!({
772 "type": "message",
773 "role": "assistant",
774 "id": null,
775 "status": null,
776 "content": [{"type": "output_text", "text": "hi", "annotations": null}]
777 });
778 let item: InputItem = serde_json::from_value(json).unwrap();
779 match item {
780 InputItem::Item(Item::Message(MessageItem::Output(out))) => {
781 assert!(out.id.is_none());
782 assert!(out.status.is_none());
783 assert_eq!(out.content.len(), 1);
784 }
785 other => panic!("expected Item::Message(Output), got {other:?}"),
786 }
787 }
788
789 #[test]
790 fn create_response_roundtrip_with_relaxed_input() {
791 let body = serde_json::json!({
792 "model": "m",
793 "input": [
794 {"type": "message", "role": "user", "content": [
795 {"type": "input_text", "text": "hi"}
796 ]},
797 {"type": "function_call", "call_id": "c", "name": "f", "arguments": "{}"},
798 {"type": "message", "role": "assistant", "content": [
799 {"type": "output_text", "text": "\n\n"}
800 ]},
801 {"type": "function_call_output", "call_id": "c", "output": "x"}
802 ]
803 });
804
805 let req: CreateResponse = serde_json::from_value(body).unwrap();
806 let items = match &req.input {
807 InputParam::Items(items) => items,
808 _ => panic!("expected Items"),
809 };
810 assert_eq!(items.len(), 4);
811 assert!(matches!(
812 items[2],
813 InputItem::Item(Item::Message(MessageItem::Output(_)))
814 ));
815 }
816
817 #[test]
825 fn easy_message_multimodal_without_type_routes_to_easymessage() {
826 let json = serde_json::json!({
829 "role": "user",
830 "content": [
831 {"type": "input_image", "image_url": "data:image/png;base64,abc"}
832 ]
833 });
834 let item: InputItem = serde_json::from_value(json).unwrap();
835 match item {
836 InputItem::EasyMessage(easy) => {
837 assert_eq!(easy.role, Role::User);
838 assert_eq!(easy.r#type, MessageType::Message);
839 match easy.content {
840 EasyInputContent::ContentList(parts) => {
841 assert_eq!(parts.len(), 1);
842 match &parts[0] {
843 InputContent::InputImage(img) => {
844 assert_eq!(img.detail, ImageDetail::Auto);
845 assert_eq!(
846 img.image_url.as_deref(),
847 Some("data:image/png;base64,abc")
848 );
849 }
850 other => panic!("expected InputImage, got {other:?}"),
851 }
852 }
853 other => panic!("expected ContentList, got {other:?}"),
854 }
855 }
856 other => panic!("expected EasyMessage, got {other:?}"),
857 }
858 }
859
860 #[test]
861 fn easy_message_multimodal_with_explicit_null_detail() {
862 let json = serde_json::json!({
866 "role": "user",
867 "content": [
868 {"type": "input_image", "image_url": "data:image/png;base64,abc", "detail": null}
869 ]
870 });
871 let item: InputItem = serde_json::from_value(json).unwrap();
872 assert!(matches!(item, InputItem::EasyMessage(_)));
873 }
874
875 #[test]
876 fn easy_message_assistant_multimodal_without_type() {
877 let json = serde_json::json!({
881 "role": "assistant",
882 "content": [
883 {"type": "input_text", "text": "ok"}
884 ]
885 });
886 let item: InputItem = serde_json::from_value(json).unwrap();
887 match item {
888 InputItem::EasyMessage(easy) => {
889 assert_eq!(easy.role, Role::Assistant);
890 }
891 other => panic!("expected EasyMessage(assistant), got {other:?}"),
892 }
893 }
894
895 #[test]
896 fn easy_message_text_only_without_type_unchanged() {
897 let json = serde_json::json!({"role": "user", "content": "Hello"});
902 let item: InputItem = serde_json::from_value(json).unwrap();
903 match item {
904 InputItem::EasyMessage(easy) => {
905 assert_eq!(easy.role, Role::User);
906 assert!(matches!(easy.content, EasyInputContent::Text(ref s) if s == "Hello"));
907 }
908 other => panic!("expected EasyMessage(Text), got {other:?}"),
909 }
910 }
911
912 #[test]
913 fn easy_message_with_explicit_type_still_routes_to_item_message() {
914 let json = serde_json::json!({
918 "type": "message",
919 "role": "user",
920 "content": [
921 {"type": "input_image", "image_url": "data:image/png;base64,abc"}
922 ]
923 });
924 let item: InputItem = serde_json::from_value(json).unwrap();
925 match item {
926 InputItem::Item(Item::Message(MessageItem::Input(msg))) => {
927 assert_eq!(msg.role, InputRole::User);
928 assert_eq!(msg.content.len(), 1);
929 }
930 other => panic!("expected Item::Message(Input), got {other:?}"),
931 }
932 }
933
934 #[test]
935 fn create_response_roundtrip_aiperf_pre_pr931_payload() {
936 let body = serde_json::json!({
941 "model": "Qwen/Qwen2-VL-2B-Instruct",
942 "input": [
943 {
944 "role": "user",
945 "content": [
946 {"type": "input_text", "text": "Describe"},
947 {"type": "input_image", "image_url": "data:image/png;base64,abc"}
948 ]
949 },
950 {
951 "role": "assistant",
952 "content": [{"type": "input_text", "text": "ok"}]
953 },
954 {
955 "role": "user",
956 "content": [{"type": "input_text", "text": "Now describe a different one."}]
957 }
958 ]
959 });
960 let req: CreateResponse = serde_json::from_value(body).unwrap();
961 let items = match &req.input {
962 InputParam::Items(items) => items,
963 _ => panic!("expected Items"),
964 };
965 assert_eq!(items.len(), 3);
966 for (idx, item) in items.iter().enumerate() {
968 assert!(
969 matches!(item, InputItem::EasyMessage(_)),
970 "turn {idx} did not route to EasyMessage: {item:?}",
971 );
972 }
973 }
974}