1use std::collections::HashMap;
32
33use serde::{Deserialize, Serialize, de};
34
35pub use async_openai::types::responses::*;
39
40pub use crate::types::ImageDetail;
42pub use crate::types::ReasoningEffort;
43pub use crate::types::ResponseFormatJsonSchema;
44
45pub type Input = InputParam;
47pub type PromptConfig = Prompt;
48pub type TextConfig = ResponseTextParam;
49pub type TextResponseFormat = TextResponseFormatConfiguration;
50
51pub type ResponseStream = std::pin::Pin<
53 Box<dyn futures::Stream<Item = Result<ResponseStreamEvent, crate::error::OpenAIError>> + Send>,
54>;
55
56pub const SPEC_NULLABLE_REQUIRED_RESPONSE_FIELDS: &[&str] = &[
73 "billing",
74 "completed_at",
75 "conversation",
76 "error",
77 "incomplete_details",
78 "instructions",
79 "max_output_tokens",
80 "max_tool_calls",
81 "previous_response_id",
82 "prompt",
83 "prompt_cache_key",
84 "prompt_cache_retention",
85 "reasoning",
86 "safety_identifier",
87 "usage",
88];
89
90fn deserialize_null_as_empty_vec<'de, T, D>(deserializer: D) -> Result<Vec<T>, D::Error>
100where
101 T: Deserialize<'de>,
102 D: serde::Deserializer<'de>,
103{
104 Option::<Vec<T>>::deserialize(deserializer).map(Option::unwrap_or_default)
105}
106
107fn deserialize_null_as_default<'de, T, D>(deserializer: D) -> Result<T, D::Error>
113where
114 T: Deserialize<'de> + Default,
115 D: serde::Deserializer<'de>,
116{
117 Option::<T>::deserialize(deserializer).map(Option::unwrap_or_default)
118}
119
120fn deserialize_tool_choice<'de, D>(deserializer: D) -> Result<Option<ToolChoiceParam>, D::Error>
136where
137 D: serde::Deserializer<'de>,
138{
139 let Some(value) = Option::<serde_json::Value>::deserialize(deserializer)? else {
140 return Ok(None);
141 };
142 if let Some(serde_json::Value::String(t)) = value.get("type") {
143 let mode = match t.as_str() {
144 "auto" => Some(ToolChoiceOptions::Auto),
145 "none" => Some(ToolChoiceOptions::None),
146 "required" => Some(ToolChoiceOptions::Required),
147 _ => None,
148 };
149 if let Some(mode) = mode {
150 return Ok(Some(ToolChoiceParam::Option(mode)));
151 }
152 }
153 ToolChoiceParam::deserialize(value)
154 .map(Some)
155 .map_err(serde::de::Error::custom)
156}
157
158#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
162pub struct InputOutputTextContent {
163 #[serde(default, deserialize_with = "deserialize_null_as_empty_vec")]
164 pub annotations: Vec<Annotation>,
165 #[serde(default, skip_serializing_if = "Option::is_none")]
166 pub logprobs: Option<Vec<LogProb>>,
167 pub text: String,
168}
169
170#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
172#[serde(tag = "type", rename_all = "snake_case")]
173pub enum InputOutputMessageContent {
174 OutputText(InputOutputTextContent),
175 Refusal(RefusalContent),
176}
177
178#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
185pub struct InputOutputMessage {
186 #[serde(default, deserialize_with = "deserialize_null_as_empty_vec")]
187 pub content: Vec<InputOutputMessageContent>,
188 #[serde(default, skip_serializing_if = "Option::is_none")]
189 pub id: Option<String>,
190 pub role: AssistantRole,
191 #[serde(default, skip_serializing_if = "Option::is_none")]
192 pub phase: Option<MessagePhase>,
193 #[serde(default, skip_serializing_if = "Option::is_none")]
194 pub status: Option<OutputStatus>,
195}
196
197pub use async_openai::types::responses::InputContent as UpstreamInputContent;
202
203#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
212pub struct InputImageContent {
213 #[serde(default, deserialize_with = "deserialize_null_as_default")]
214 pub detail: ImageDetail,
215 #[serde(default, skip_serializing_if = "Option::is_none")]
216 pub file_id: Option<String>,
217 #[serde(default, skip_serializing_if = "Option::is_none")]
218 pub image_url: Option<String>,
219}
220
221#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
225#[serde(tag = "type", rename_all = "snake_case")]
226pub enum InputContent {
227 InputText(InputTextContent),
228 InputImage(InputImageContent),
229 InputFile(InputFileContent),
230}
231
232#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)]
235pub struct InputMessage {
236 pub content: Vec<InputContent>,
237 pub role: InputRole,
238 #[serde(default, skip_serializing_if = "Option::is_none")]
239 pub status: Option<OutputStatus>,
240}
241
242#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
253#[serde(untagged)]
254pub enum EasyInputContent {
255 Text(String),
257 ContentList(Vec<InputContent>),
259}
260
261impl Default for EasyInputContent {
262 fn default() -> Self {
263 Self::Text(String::new())
264 }
265}
266
267#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
274#[serde(untagged)]
275pub enum FunctionCallOutput {
276 Text(String),
278 Content(Vec<InputContent>),
280}
281
282impl From<&str> for FunctionCallOutput {
285 fn from(text: &str) -> Self {
286 FunctionCallOutput::Text(text.to_string())
287 }
288}
289
290impl From<String> for FunctionCallOutput {
291 fn from(text: String) -> Self {
292 FunctionCallOutput::Text(text)
293 }
294}
295
296impl From<Vec<InputContent>> for FunctionCallOutput {
297 fn from(content: Vec<InputContent>) -> Self {
298 FunctionCallOutput::Content(content)
299 }
300}
301
302#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
305pub struct FunctionCallOutputItemParam {
306 pub call_id: String,
307 pub output: FunctionCallOutput,
308 #[serde(default, skip_serializing_if = "Option::is_none")]
309 pub id: Option<String>,
310 #[serde(default, skip_serializing_if = "Option::is_none")]
311 pub status: Option<OutputStatus>,
312}
313
314#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)]
320pub struct EasyInputMessage {
321 #[serde(default)]
325 pub r#type: MessageType,
326 pub role: Role,
327 pub content: EasyInputContent,
328 #[serde(default, skip_serializing_if = "Option::is_none")]
329 pub phase: Option<MessagePhase>,
330}
331
332#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
345#[serde(untagged)]
346pub enum MessageItem {
347 Output(InputOutputMessage),
350 Input(InputMessage),
352}
353
354#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
370pub struct InputReasoningItem {
371 #[serde(default, skip_serializing_if = "Option::is_none")]
373 pub id: Option<String>,
374 #[serde(default)]
376 pub summary: Vec<SummaryPart>,
377 #[serde(default, skip_serializing_if = "Option::is_none")]
378 pub content: Option<Vec<ReasoningTextContent>>,
379 #[serde(default, skip_serializing_if = "Option::is_none")]
380 pub encrypted_content: Option<String>,
381 #[serde(default, skip_serializing_if = "Option::is_none")]
382 pub status: Option<OutputStatus>,
383}
384
385#[derive(Deserialize)]
387struct CodexAgentMessage {
388 #[serde(default)]
389 content: Option<CodexAgentMessageContent>,
390}
391
392#[derive(Deserialize)]
393#[serde(untagged)]
394enum CodexAgentMessageContent {
395 Text(String),
396 Parts(Vec<CodexAgentMessageInputContent>),
397}
398
399#[derive(Deserialize)]
400#[serde(tag = "type", rename_all = "snake_case")]
401enum CodexAgentMessageInputContent {
402 InputText(InputTextContent),
403 EncryptedContent { encrypted_content: String },
404}
405
406#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
409#[serde(tag = "type", rename_all = "snake_case")]
410pub enum Item {
411 Message(MessageItem),
412 FileSearchCall(FileSearchToolCall),
413 ComputerCall(ComputerToolCall),
414 ComputerCallOutput(ComputerCallOutputItemParam),
415 WebSearchCall(WebSearchToolCall),
416 FunctionCall(FunctionToolCall),
417 FunctionCallOutput(FunctionCallOutputItemParam),
418 ToolSearchCall(ToolSearchCallItemParam),
419 ToolSearchOutput(ToolSearchOutputItemParam),
420 Reasoning(InputReasoningItem),
421 Compaction(CompactionSummaryItemParam),
422 ImageGenerationCall(ImageGenToolCall),
423 CodeInterpreterCall(CodeInterpreterToolCall),
424 LocalShellCall(LocalShellToolCall),
425 LocalShellCallOutput(LocalShellToolCallOutput),
426 ShellCall(FunctionShellCallItemParam),
427 ShellCallOutput(FunctionShellCallOutputItemParam),
428 ApplyPatchCall(ApplyPatchToolCallItemParam),
429 ApplyPatchCallOutput(ApplyPatchToolCallOutputItemParam),
430 McpListTools(MCPListTools),
431 McpApprovalRequest(MCPApprovalRequest),
432 McpApprovalResponse(MCPApprovalResponse),
433 McpCall(MCPToolCall),
434 CustomToolCallOutput(CustomToolCallOutput),
435 CustomToolCall(CustomToolCall),
436}
437
438#[derive(Debug, Serialize, Clone, PartialEq)]
440#[serde(untagged)]
441pub enum InputItem {
442 ItemReference(ItemReference),
443 Item(Item),
444 EasyMessage(EasyInputMessage),
445}
446
447#[derive(Deserialize)]
448#[serde(untagged)]
449enum InputItemWire {
450 ItemReference(ItemReference),
451 Item(Item),
452 EasyMessage(EasyInputMessage),
453}
454
455impl<'de> Deserialize<'de> for InputItem {
456 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
457 where
458 D: serde::Deserializer<'de>,
459 {
460 let value = serde_json::Value::deserialize(deserializer)?;
461 if value.get("type").and_then(serde_json::Value::as_str) == Some("agent_message") {
462 let message = CodexAgentMessage::deserialize(value).map_err(de::Error::custom)?;
463 return Ok(normalize_codex_agent_message(message));
464 }
465
466 match InputItemWire::deserialize(value).map_err(de::Error::custom)? {
467 InputItemWire::ItemReference(item) => Ok(Self::ItemReference(item)),
468 InputItemWire::Item(item) => Ok(Self::Item(item)),
469 InputItemWire::EasyMessage(message) => Ok(Self::EasyMessage(message)),
470 }
471 }
472}
473
474fn normalize_codex_agent_message(message: CodexAgentMessage) -> InputItem {
475 let content = match message.content {
476 None => String::new(),
477 Some(CodexAgentMessageContent::Text(text)) => text,
478 Some(CodexAgentMessageContent::Parts(parts)) => parts
479 .into_iter()
480 .map(|part| match part {
481 CodexAgentMessageInputContent::InputText(part) => part.text,
482 CodexAgentMessageInputContent::EncryptedContent { encrypted_content } => {
483 encrypted_content
484 }
485 })
486 .collect::<Vec<_>>()
487 .join("\n"),
488 };
489 InputItem::EasyMessage(EasyInputMessage {
490 r#type: MessageType::Message,
491 role: Role::User,
492 content: EasyInputContent::Text(content),
493 phase: None,
494 })
495}
496
497#[derive(Debug, Serialize, Clone, PartialEq)]
499#[serde(untagged)]
500pub enum InputParam {
501 Text(String),
502 Items(Vec<InputItem>),
503}
504
505impl<'de> Deserialize<'de> for InputParam {
506 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
507 where
508 D: serde::Deserializer<'de>,
509 {
510 match serde_json::Value::deserialize(deserializer)? {
511 serde_json::Value::String(text) => Ok(Self::Text(text)),
512 serde_json::Value::Array(items) => {
513 serde_json::from_value(serde_json::Value::Array(items))
514 .map(Self::Items)
515 .map_err(de::Error::custom)
516 }
517 _ => Err(de::Error::custom(
518 "input must be a string or an array of input items",
519 )),
520 }
521 }
522}
523
524impl Default for InputParam {
525 fn default() -> Self {
526 Self::Text(String::new())
527 }
528}
529
530#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)]
539pub struct CreateResponse {
540 #[serde(skip_serializing_if = "Option::is_none")]
541 pub background: Option<bool>,
542 #[serde(skip_serializing_if = "Option::is_none")]
543 pub conversation: Option<ConversationParam>,
544 #[serde(skip_serializing_if = "Option::is_none")]
545 pub include: Option<Vec<IncludeEnum>>,
546 pub input: InputParam,
547 #[serde(skip_serializing_if = "Option::is_none")]
548 pub instructions: Option<String>,
549 #[serde(alias = "max_tokens", skip_serializing_if = "Option::is_none")]
553 pub max_output_tokens: Option<u32>,
554 #[serde(skip_serializing_if = "Option::is_none")]
555 pub max_tool_calls: Option<u32>,
556 #[serde(skip_serializing_if = "Option::is_none")]
557 pub metadata: Option<HashMap<String, String>>,
558 #[serde(skip_serializing_if = "Option::is_none")]
559 pub model: Option<String>,
560 #[serde(skip_serializing_if = "Option::is_none")]
561 pub parallel_tool_calls: Option<bool>,
562 #[serde(skip_serializing_if = "Option::is_none")]
563 pub previous_response_id: Option<String>,
564 #[serde(skip_serializing_if = "Option::is_none")]
565 pub prompt: Option<Prompt>,
566 #[serde(skip_serializing_if = "Option::is_none")]
567 pub prompt_cache_key: Option<String>,
568 #[serde(skip_serializing_if = "Option::is_none")]
569 pub prompt_cache_retention: Option<PromptCacheRetention>,
570 #[serde(skip_serializing_if = "Option::is_none")]
571 pub reasoning: Option<Reasoning>,
572 #[serde(skip_serializing_if = "Option::is_none")]
573 pub safety_identifier: Option<String>,
574 #[serde(skip_serializing_if = "Option::is_none")]
575 pub service_tier: Option<ServiceTierResponses>,
576 #[serde(skip_serializing_if = "Option::is_none")]
577 pub store: Option<bool>,
578 #[serde(skip_serializing_if = "Option::is_none")]
579 pub stream: Option<bool>,
580 #[serde(skip_serializing_if = "Option::is_none")]
581 pub stream_options: Option<ResponseStreamOptions>,
582 #[serde(skip_serializing_if = "Option::is_none")]
583 pub temperature: Option<f32>,
584 #[serde(skip_serializing_if = "Option::is_none")]
585 pub text: Option<ResponseTextParam>,
586 #[serde(
587 default,
588 deserialize_with = "deserialize_tool_choice",
589 skip_serializing_if = "Option::is_none"
590 )]
591 pub tool_choice: Option<ToolChoiceParam>,
592 #[serde(skip_serializing_if = "Option::is_none")]
593 pub tools: Option<Vec<Tool>>,
594 #[serde(skip_serializing_if = "Option::is_none")]
595 pub top_logprobs: Option<u8>,
596 #[serde(skip_serializing_if = "Option::is_none")]
597 pub top_p: Option<f32>,
598 #[serde(skip_serializing_if = "Option::is_none")]
599 pub truncation: Option<Truncation>,
600}
601
602pub const RESPONSE_INPUT_TOKENS_OBJECT: &str = "response.input_tokens";
608
609#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)]
628pub struct CountInputTokensRequest {
629 #[serde(default, skip_serializing_if = "Option::is_none")]
630 pub model: Option<String>,
631 #[serde(default, deserialize_with = "deserialize_null_default_input")]
635 pub input: InputParam,
636 #[serde(default, skip_serializing_if = "Option::is_none")]
637 pub instructions: Option<String>,
638 #[serde(
639 default,
640 skip_serializing_if = "Option::is_none",
641 deserialize_with = "deserialize_lenient_tools"
642 )]
643 pub tools: Option<Vec<Tool>>,
644}
645
646fn deserialize_null_default_input<'de, D>(deserializer: D) -> Result<InputParam, D::Error>
647where
648 D: serde::Deserializer<'de>,
649{
650 Ok(Option::<InputParam>::deserialize(deserializer)?.unwrap_or_default())
651}
652
653fn deserialize_lenient_tools<'de, D>(deserializer: D) -> Result<Option<Vec<Tool>>, D::Error>
671where
672 D: serde::Deserializer<'de>,
673{
674 let Some(raw) = Option::<Vec<serde_json::Value>>::deserialize(deserializer)? else {
675 return Ok(None);
676 };
677 Ok(Some(
678 raw.into_iter()
679 .filter_map(|tool| serde_json::from_value::<Tool>(tool).ok())
680 .collect(),
681 ))
682}
683
684#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
690pub struct CountInputTokensResponse {
691 pub object: String,
693 pub input_tokens: u32,
694}
695
696impl CountInputTokensResponse {
697 pub fn new(input_tokens: u32) -> Self {
698 Self {
699 object: RESPONSE_INPUT_TOKENS_OBJECT.to_string(),
700 input_tokens,
701 }
702 }
703}
704
705impl CountInputTokensRequest {
706 pub fn estimate_tokens(&self) -> u32 {
716 let mut total_len: usize = 0;
717
718 if let Some(instructions) = &self.instructions.as_ref().filter(|text| !text.is_empty()) {
728 total_len += role_len(Role::System) + instructions.len();
729 }
730
731 match &self.input {
732 InputParam::Text(text) if text.is_empty() => {}
733 InputParam::Text(text) => total_len += role_len(Role::User) + text.len(),
734 InputParam::Items(items) => total_len += estimate_input_items_len(items),
735 }
736
737 if let Some(tools) = &self.tools {
738 for tool in tools {
739 total_len += estimate_tool_len(tool);
740 }
741 }
742
743 let tokens = total_len / 3;
744 if tokens == 0 && total_len > 0 {
745 1
746 } else {
747 tokens as u32
748 }
749 }
750}
751
752fn role_len(role: Role) -> usize {
755 match role {
756 Role::User => 4,
757 Role::Assistant => 9,
758 Role::System => 6,
759 Role::Developer => 9,
760 }
761}
762
763fn input_role_len(role: InputRole) -> usize {
764 match role {
765 InputRole::User => 4,
766 InputRole::System => 6,
767 InputRole::Developer => 9,
768 }
769}
770
771const TOOL_ROLE_LEN: usize = 4;
777
778enum GroupEffect {
780 Assistant,
783 Flush,
785 Skip,
787}
788
789fn estimate_input_items_len(items: &[InputItem]) -> usize {
802 let mut total = 0;
803 let mut assistant_open = false;
804
805 for item in items {
806 let (effect, len) = measure_input_item(item);
807 total += len;
808 match effect {
809 GroupEffect::Assistant => {
810 if !assistant_open {
811 assistant_open = true;
812 total += role_len(Role::Assistant);
813 }
814 }
815 GroupEffect::Flush => assistant_open = false,
816 GroupEffect::Skip => {}
817 }
818 }
819
820 total
821}
822
823fn measure_input_item(item: &InputItem) -> (GroupEffect, usize) {
828 match item {
829 InputItem::ItemReference(_) => (GroupEffect::Skip, 0),
833 InputItem::EasyMessage(message) => {
834 let content = estimate_easy_content_len(&message.content);
835 match message.role {
836 Role::Assistant => (GroupEffect::Assistant, content),
839 role => (GroupEffect::Flush, role_len(role) + content),
840 }
841 }
842 InputItem::Item(item) => measure_item(item),
843 }
844}
845
846fn estimate_easy_content_len(content: &EasyInputContent) -> usize {
847 match content {
848 EasyInputContent::Text(text) => text.len(),
849 EasyInputContent::ContentList(parts) => parts.iter().map(estimate_input_content_len).sum(),
850 }
851}
852
853fn estimate_input_content_len(part: &InputContent) -> usize {
857 match part {
858 InputContent::InputText(text) => text.text.len(),
859 InputContent::InputImage(_) | InputContent::InputFile(_) => 0,
860 }
861}
862
863fn measure_item(item: &Item) -> (GroupEffect, usize) {
864 match item {
865 Item::Message(MessageItem::Input(message)) => (
866 GroupEffect::Flush,
867 input_role_len(message.role)
868 + message
869 .content
870 .iter()
871 .map(estimate_input_content_len)
872 .sum::<usize>(),
873 ),
874 Item::Message(MessageItem::Output(message)) => (
877 GroupEffect::Assistant,
878 message
879 .content
880 .iter()
881 .map(|part| match part {
882 InputOutputMessageContent::OutputText(text) => text.text.len(),
883 InputOutputMessageContent::Refusal(refusal) => refusal.refusal.len(),
884 })
885 .sum::<usize>(),
886 ),
887 Item::FunctionCall(call) => (
891 GroupEffect::Assistant,
892 call.name.len() + call.arguments.len(),
893 ),
894 Item::FunctionCallOutput(output) => (
897 GroupEffect::Flush,
898 TOOL_ROLE_LEN
899 + match &output.output {
900 FunctionCallOutput::Text(text) => text.len(),
901 FunctionCallOutput::Content(parts) => {
902 parts.iter().map(estimate_input_content_len).sum()
903 }
904 },
905 ),
906 Item::Reasoning(reasoning) => (
914 GroupEffect::Assistant,
915 reasoning
916 .summary
917 .iter()
918 .map(|part| match part {
919 SummaryPart::SummaryText(text) => text.text.len(),
920 })
921 .sum(),
922 ),
923 Item::FileSearchCall(_)
940 | Item::ComputerCall(_)
941 | Item::ComputerCallOutput(_)
942 | Item::WebSearchCall(_)
943 | Item::ToolSearchCall(_)
944 | Item::ToolSearchOutput(_)
945 | Item::Compaction(_)
946 | Item::ImageGenerationCall(_)
947 | Item::CodeInterpreterCall(_)
948 | Item::LocalShellCall(_)
949 | Item::LocalShellCallOutput(_)
950 | Item::ShellCall(_)
951 | Item::ShellCallOutput(_)
952 | Item::ApplyPatchCall(_)
953 | Item::ApplyPatchCallOutput(_)
954 | Item::McpListTools(_)
955 | Item::McpApprovalRequest(_)
956 | Item::McpApprovalResponse(_)
957 | Item::McpCall(_)
958 | Item::CustomToolCallOutput(_)
959 | Item::CustomToolCall(_) => (GroupEffect::Flush, 0),
960 }
961}
962
963fn estimate_tool_len(tool: &Tool) -> usize {
974 match tool {
975 Tool::Function(function) => function_tool_len(
976 &function.name,
977 function.description.as_ref(),
978 function.parameters.as_ref(),
979 ),
980 Tool::Namespace(namespace) => namespace
981 .tools
982 .iter()
983 .map(|tool| match tool {
984 NamespaceToolParamTool::Function(function) => function_tool_len(
988 &function.name,
989 function.description.as_ref(),
990 function.parameters.as_ref(),
991 ),
992 NamespaceToolParamTool::Custom(_) => 0,
993 })
994 .sum(),
995 _ => 0,
996 }
997}
998
999fn function_tool_len(
1000 name: &str,
1001 description: Option<&String>,
1002 parameters: Option<&serde_json::Value>,
1003) -> usize {
1004 name.len()
1005 + description.map_or(0, |description| description.len())
1006 + parameters.map_or(0, |schema| schema.to_string().len())
1007}
1008
1009#[cfg(test)]
1010mod tests {
1011 use super::*;
1012
1013 fn tool_choice_of(json: serde_json::Value) -> Option<ToolChoiceParam> {
1016 let req: CreateResponse = serde_json::from_value(serde_json::json!({
1017 "input": "hi",
1018 "tool_choice": json,
1019 }))
1020 .expect("CreateResponse should deserialize");
1021 req.tool_choice
1022 }
1023
1024 #[test]
1025 fn tool_choice_mode_object_coerces_to_mode() {
1026 assert_eq!(
1029 tool_choice_of(serde_json::json!({"type": "auto", "disable_parallel_tool_use": true})),
1030 Some(ToolChoiceParam::Option(ToolChoiceOptions::Auto)),
1031 );
1032 assert_eq!(
1033 tool_choice_of(serde_json::json!({"type": "none"})),
1034 Some(ToolChoiceParam::Option(ToolChoiceOptions::None)),
1035 );
1036 assert_eq!(
1037 tool_choice_of(serde_json::json!({"type": "required"})),
1038 Some(ToolChoiceParam::Option(ToolChoiceOptions::Required)),
1039 );
1040 }
1041
1042 #[test]
1043 fn tool_choice_bare_string_still_works() {
1044 assert_eq!(
1045 tool_choice_of(serde_json::json!("auto")),
1046 Some(ToolChoiceParam::Option(ToolChoiceOptions::Auto)),
1047 );
1048 }
1049
1050 #[test]
1051 fn tool_choice_specific_function_object_still_works() {
1052 match tool_choice_of(serde_json::json!({"type": "function", "name": "get_weather"})) {
1055 Some(ToolChoiceParam::Function(f)) => assert_eq!(f.name, "get_weather"),
1056 other => panic!("expected Function tool choice, got {other:?}"),
1057 }
1058 }
1059
1060 #[test]
1061 fn tool_choice_absent_is_none() {
1062 let req: CreateResponse =
1063 serde_json::from_value(serde_json::json!({"input": "hi"})).unwrap();
1064 assert!(req.tool_choice.is_none());
1065 }
1066
1067 #[test]
1070 fn reasoning_input_without_id_deserializes() {
1071 let json = serde_json::json!({
1073 "type": "reasoning",
1074 "summary": [{"type": "summary_text", "text": "thinking"}],
1075 });
1076 match serde_json::from_value::<InputItem>(json).expect("should deserialize") {
1077 InputItem::Item(Item::Reasoning(r)) => {
1078 assert!(r.id.is_none());
1079 assert_eq!(r.summary.len(), 1);
1080 }
1081 other => panic!("expected Item::Reasoning, got {other:?}"),
1082 }
1083 }
1084
1085 #[test]
1086 fn reasoning_input_encrypted_without_id_or_summary_deserializes() {
1087 let json = serde_json::json!({
1088 "type": "reasoning",
1089 "encrypted_content": "AB==",
1090 });
1091 match serde_json::from_value::<InputItem>(json).expect("should deserialize") {
1092 InputItem::Item(Item::Reasoning(r)) => {
1093 assert!(r.id.is_none());
1094 assert!(r.summary.is_empty());
1095 assert_eq!(r.encrypted_content.as_deref(), Some("AB=="));
1096 }
1097 other => panic!("expected Item::Reasoning, got {other:?}"),
1098 }
1099 }
1100
1101 #[test]
1102 fn reasoning_input_with_id_still_works() {
1103 let json = serde_json::json!({
1104 "type": "reasoning",
1105 "id": "rs_1",
1106 "summary": [{"type": "summary_text", "text": "x"}],
1107 "status": "completed",
1108 });
1109 match serde_json::from_value::<InputItem>(json).expect("should deserialize") {
1110 InputItem::Item(Item::Reasoning(r)) => assert_eq!(r.id.as_deref(), Some("rs_1")),
1111 other => panic!("expected Item::Reasoning, got {other:?}"),
1112 }
1113 }
1114
1115 #[test]
1116 fn full_request_with_idless_reasoning_item_deserializes() {
1117 let req: Result<CreateResponse, _> = serde_json::from_value(serde_json::json!({
1120 "model": "m",
1121 "input": [
1122 {"role": "user", "content": "hi"},
1123 {"type": "reasoning", "summary": [{"type": "summary_text", "text": "x"}]},
1124 ],
1125 }));
1126 assert!(
1127 req.is_ok(),
1128 "idless reasoning input should deserialize: {req:?}"
1129 );
1130 }
1131
1132 #[test]
1133 fn codex_agent_message_normalizes_to_user_message() {
1134 let req: CreateResponse = serde_json::from_value(serde_json::json!({
1135 "input": [{
1136 "type": "agent_message",
1137 "author": "/root",
1138 "recipient": "/root/worker",
1139 "content": [
1140 {"type": "input_text", "text": "First."},
1141 {"type": "input_text", "text": "Second."},
1142 ],
1143 }],
1144 }))
1145 .expect("Codex agent message should deserialize");
1146
1147 let InputParam::Items(items) = req.input else {
1148 panic!("expected items");
1149 };
1150 assert!(matches!(
1151 &items[0],
1152 InputItem::EasyMessage(EasyInputMessage {
1153 role: Role::User,
1154 content: EasyInputContent::Text(text),
1155 ..
1156 }) if text == "First.\nSecond."
1157 ));
1158 }
1159
1160 #[test]
1161 fn codex_agent_message_string_content_normalizes_to_user_message() {
1162 let item: InputItem = serde_json::from_value(serde_json::json!({
1163 "type": "agent_message",
1164 "author": "/root",
1165 "recipient": "/root/worker",
1166 "content": "Return exactly OK.",
1167 }))
1168 .expect("Codex agent message with string content should deserialize");
1169
1170 assert!(matches!(
1171 item,
1172 InputItem::EasyMessage(EasyInputMessage {
1173 content: EasyInputContent::Text(text),
1174 ..
1175 }) if text == "Return exactly OK."
1176 ));
1177 }
1178
1179 #[test]
1180 fn codex_agent_message_normalizes_encrypted_content() {
1181 let req: CreateResponse = serde_json::from_value(serde_json::json!({
1182 "input": [{
1183 "type": "agent_message",
1184 "content": [
1185 {"type": "input_text", "text": "Payload:"},
1186 {"type": "encrypted_content", "encrypted_content": "Return exactly OK."},
1187 ],
1188 }],
1189 }))
1190 .expect("Codex agent message with encrypted content should deserialize");
1191
1192 let InputParam::Items(items) = req.input else {
1193 panic!("expected items");
1194 };
1195 assert!(matches!(
1196 &items[0],
1197 InputItem::EasyMessage(EasyInputMessage {
1198 content: EasyInputContent::Text(text),
1199 ..
1200 }) if text == "Payload:\nReturn exactly OK."
1201 ));
1202 }
1203
1204 #[test]
1205 fn codex_agent_message_missing_content_normalizes_empty() {
1206 let item: InputItem = serde_json::from_value(serde_json::json!({
1207 "type": "agent_message",
1208 "author": "/root",
1209 "recipient": "/root/worker",
1210 }))
1211 .expect("Codex agent message without content should deserialize");
1212 assert!(matches!(
1213 item,
1214 InputItem::EasyMessage(EasyInputMessage {
1215 content: EasyInputContent::Text(text),
1216 ..
1217 }) if text.is_empty()
1218 ));
1219 }
1220
1221 #[test]
1222 fn codex_agent_message_null_content_normalizes_empty() {
1223 let item: InputItem = serde_json::from_value(serde_json::json!({
1224 "type": "agent_message",
1225 "author": "/root",
1226 "recipient": "/root/worker",
1227 "content": null,
1228 }))
1229 .expect("Codex agent message with null content should deserialize");
1230 assert!(matches!(
1231 item,
1232 InputItem::EasyMessage(EasyInputMessage {
1233 content: EasyInputContent::Text(text),
1234 ..
1235 }) if text.is_empty()
1236 ));
1237 }
1238
1239 #[test]
1240 fn relaxed_assistant_message_without_id_or_status() {
1241 let json = serde_json::json!({
1242 "type": "message",
1243 "role": "assistant",
1244 "content": [{"type": "output_text", "text": "hi"}]
1245 });
1246 let item: InputItem = serde_json::from_value(json).unwrap();
1247 match item {
1248 InputItem::Item(Item::Message(MessageItem::Output(out))) => {
1249 assert_eq!(out.role, AssistantRole::Assistant);
1250 assert!(out.id.is_none());
1251 assert!(out.status.is_none());
1252 }
1253 other => panic!("expected Item::Message(Output), got {other:?}"),
1254 }
1255 }
1256
1257 #[test]
1258 fn function_call_output_image_part_without_detail_parses() {
1259 let json = serde_json::json!({
1260 "input": [
1261 {"type": "function_call", "call_id": "c1", "name": "screenshot", "arguments": "{}"},
1262 {"type": "function_call_output", "call_id": "c1", "output": [
1263 {"type": "input_text", "text": "captured"},
1264 {"type": "input_image", "image_url": "data:image/png;base64,iVBORw0KGgo="}
1265 ]}
1266 ]
1267 });
1268 let req: CreateResponse = serde_json::from_value(json).unwrap();
1269 let InputParam::Items(items) = req.input else {
1270 panic!("expected Items")
1271 };
1272 match &items[1] {
1273 InputItem::Item(Item::FunctionCallOutput(fco)) => {
1274 assert_eq!(fco.call_id, "c1");
1275 let FunctionCallOutput::Content(parts) = &fco.output else {
1276 panic!("expected Content, got {:?}", fco.output)
1277 };
1278 assert_eq!(parts.len(), 2);
1279 match &parts[1] {
1280 InputContent::InputImage(img) => {
1281 assert_eq!(img.detail, ImageDetail::Auto);
1282 assert_eq!(
1283 img.image_url.as_deref(),
1284 Some("data:image/png;base64,iVBORw0KGgo=")
1285 );
1286 }
1287 other => panic!("expected InputImage, got {other:?}"),
1288 }
1289 }
1290 other => panic!("expected FunctionCallOutput, got {other:?}"),
1291 }
1292 }
1293
1294 #[test]
1295 fn function_call_output_image_part_with_null_detail_matches_message_content() {
1296 let part = serde_json::json!({
1299 "type": "input_image", "image_url": "data:image/png;base64,iVBORw0KGgo=", "detail": null
1300 });
1301 let message: Item = serde_json::from_value(serde_json::json!({
1302 "type": "message", "role": "user", "content": [part]
1303 }))
1304 .unwrap();
1305 assert!(matches!(message, Item::Message(_)));
1306 let output: Item = serde_json::from_value(serde_json::json!({
1307 "type": "function_call_output", "call_id": "c1", "output": [part]
1308 }))
1309 .unwrap();
1310 let Item::FunctionCallOutput(fco) = output else {
1311 panic!("expected FunctionCallOutput, got {output:?}")
1312 };
1313 match &fco.output {
1314 FunctionCallOutput::Content(parts) => match &parts[0] {
1315 InputContent::InputImage(img) => assert_eq!(img.detail, ImageDetail::Auto),
1316 other => panic!("expected InputImage, got {other:?}"),
1317 },
1318 other => panic!("expected Content, got {other:?}"),
1319 }
1320 }
1321
1322 #[test]
1323 fn function_call_output_from_conversions_match_upstream() {
1324 assert_eq!(
1325 FunctionCallOutput::from("ok"),
1326 FunctionCallOutput::Text("ok".to_string())
1327 );
1328 assert_eq!(
1329 FunctionCallOutput::from(String::from("ok")),
1330 FunctionCallOutput::Text("ok".to_string())
1331 );
1332 let parts = vec![InputContent::InputText(InputTextContent {
1333 text: "captured".to_string(),
1334 prompt_cache_breakpoint: None,
1335 })];
1336 let item = FunctionCallOutputItemParam {
1337 call_id: "c1".to_string(),
1338 output: parts.clone().into(),
1339 id: None,
1340 status: None,
1341 };
1342 assert_eq!(item.output, FunctionCallOutput::Content(parts));
1343 }
1344
1345 #[test]
1346 fn function_call_output_string_still_parses() {
1347 let item: Item = serde_json::from_value(serde_json::json!({
1348 "type": "function_call_output", "call_id": "c1", "output": "{\"ok\":true}"
1349 }))
1350 .unwrap();
1351 match item {
1352 Item::FunctionCallOutput(fco) => {
1353 assert!(
1354 matches!(fco.output, FunctionCallOutput::Text(ref t) if t == "{\"ok\":true}")
1355 );
1356 assert!(fco.id.is_none() && fco.status.is_none());
1357 }
1358 other => panic!("expected FunctionCallOutput, got {other:?}"),
1359 }
1360 }
1361
1362 #[test]
1363 fn input_image_without_detail_defaults_to_auto() {
1364 let json = serde_json::json!({
1365 "type": "input_image",
1366 "image_url": "https://example.com/cat.jpg"
1367 });
1368 let content: InputContent = serde_json::from_value(json).unwrap();
1369 match content {
1370 InputContent::InputImage(img) => assert_eq!(img.detail, ImageDetail::Auto),
1371 other => panic!("expected InputImage, got {other:?}"),
1372 }
1373 }
1374
1375 #[test]
1376 fn input_image_with_explicit_null_detail_defaults_to_auto() {
1377 let json = serde_json::json!({
1378 "type": "input_image",
1379 "image_url": "https://example.com/cat.jpg",
1380 "detail": null
1381 });
1382 let content: InputContent = serde_json::from_value(json).unwrap();
1383 match content {
1384 InputContent::InputImage(img) => assert_eq!(img.detail, ImageDetail::Auto),
1385 other => panic!("expected InputImage, got {other:?}"),
1386 }
1387 }
1388
1389 #[test]
1390 fn assistant_message_without_content_field_deserializes() {
1391 let json = serde_json::json!({
1395 "type": "message",
1396 "role": "assistant"
1397 });
1398 let item: InputItem = serde_json::from_value(json).unwrap();
1399 match item {
1400 InputItem::Item(Item::Message(MessageItem::Output(out))) => {
1401 assert_eq!(out.role, AssistantRole::Assistant);
1402 assert!(out.content.is_empty());
1403 assert!(out.id.is_none());
1404 assert!(out.status.is_none());
1405 }
1406 other => panic!("expected Item::Message(Output), got {other:?}"),
1407 }
1408 }
1409
1410 #[test]
1411 fn assistant_message_with_explicit_null_content_deserializes() {
1412 let json = serde_json::json!({
1416 "type": "message",
1417 "role": "assistant",
1418 "content": null
1419 });
1420 let item: InputItem = serde_json::from_value(json).unwrap();
1421 match item {
1422 InputItem::Item(Item::Message(MessageItem::Output(out))) => {
1423 assert!(out.content.is_empty());
1424 }
1425 other => panic!("expected Item::Message(Output), got {other:?}"),
1426 }
1427 }
1428
1429 #[test]
1430 fn mcp_call_item_deserializes() {
1431 let json = serde_json::json!({
1434 "type": "mcp_call",
1435 "id": "mcp_1",
1436 "server_label": "srv",
1437 "name": "t",
1438 "arguments": "{}"
1439 });
1440 let item: InputItem = serde_json::from_value(json).unwrap();
1441 assert!(matches!(item, InputItem::Item(Item::McpCall(_))));
1442 }
1443
1444 #[test]
1445 fn strict_assistant_message_still_deserializes() {
1446 let json = serde_json::json!({
1447 "type": "message",
1448 "role": "assistant",
1449 "id": "msg_1",
1450 "status": "completed",
1451 "content": [{"type": "output_text", "text": "hi", "annotations": []}]
1452 });
1453 let item: InputItem = serde_json::from_value(json).unwrap();
1454 match item {
1455 InputItem::Item(Item::Message(MessageItem::Output(out))) => {
1456 assert_eq!(out.id.as_deref(), Some("msg_1"));
1457 assert_eq!(out.status, Some(OutputStatus::Completed));
1458 }
1459 other => panic!("expected Item::Message(Output), got {other:?}"),
1460 }
1461 }
1462
1463 #[test]
1464 fn user_message_routes_to_input_variant() {
1465 let json = serde_json::json!({
1466 "type": "message",
1467 "role": "user",
1468 "content": [{"type": "input_text", "text": "hi"}]
1469 });
1470 let item: InputItem = serde_json::from_value(json).unwrap();
1471 assert!(matches!(
1472 item,
1473 InputItem::Item(Item::Message(MessageItem::Input(_)))
1474 ));
1475 }
1476
1477 #[test]
1478 fn function_call_item_still_deserializes() {
1479 let json = serde_json::json!({
1480 "type": "function_call",
1481 "call_id": "c",
1482 "name": "f",
1483 "arguments": "{}"
1484 });
1485 let item: InputItem = serde_json::from_value(json).unwrap();
1486 assert!(matches!(item, InputItem::Item(Item::FunctionCall(_))));
1487 }
1488
1489 #[test]
1490 fn easy_message_string_content_routes_to_easymessage() {
1491 let json = serde_json::json!({"role": "assistant", "content": "x"});
1492 let item: InputItem = serde_json::from_value(json).unwrap();
1493 assert!(matches!(item, InputItem::EasyMessage(_)));
1494 }
1495
1496 #[test]
1497 fn output_text_without_annotations_defaults_empty() {
1498 let json = serde_json::json!({"type": "output_text", "text": "hi"});
1499 let part: InputOutputMessageContent = serde_json::from_value(json).unwrap();
1500 match part {
1501 InputOutputMessageContent::OutputText(t) => {
1502 assert!(t.annotations.is_empty());
1503 }
1504 _ => panic!("expected OutputText"),
1505 }
1506 }
1507
1508 #[test]
1509 fn output_text_with_explicit_null_annotations_deserializes_as_empty() {
1510 let json = serde_json::json!({"type": "output_text", "text": "hi", "annotations": null});
1514 let part: InputOutputMessageContent = serde_json::from_value(json).unwrap();
1515 match part {
1516 InputOutputMessageContent::OutputText(t) => {
1517 assert!(t.annotations.is_empty());
1518 }
1519 _ => panic!("expected OutputText"),
1520 }
1521 }
1522
1523 #[test]
1524 fn assistant_message_with_explicit_null_id_and_status_deserializes() {
1525 let json = serde_json::json!({
1530 "type": "message",
1531 "role": "assistant",
1532 "id": null,
1533 "status": null,
1534 "content": [{"type": "output_text", "text": "hi", "annotations": null}]
1535 });
1536 let item: InputItem = serde_json::from_value(json).unwrap();
1537 match item {
1538 InputItem::Item(Item::Message(MessageItem::Output(out))) => {
1539 assert!(out.id.is_none());
1540 assert!(out.status.is_none());
1541 assert_eq!(out.content.len(), 1);
1542 }
1543 other => panic!("expected Item::Message(Output), got {other:?}"),
1544 }
1545 }
1546
1547 #[test]
1550 fn create_response_accepts_max_tokens_as_alias_for_max_output_tokens() {
1551 let req: CreateResponse = serde_json::from_value(serde_json::json!({
1552 "model": "m", "input": "hi", "max_tokens": 16
1553 }))
1554 .unwrap();
1555 assert_eq!(req.max_output_tokens, Some(16));
1556 let back = serde_json::to_value(&req).unwrap();
1557 assert_eq!(back["max_output_tokens"], 16);
1558 assert!(back.get("max_tokens").is_none());
1559
1560 let req: CreateResponse = serde_json::from_value(serde_json::json!({
1561 "model": "m", "input": "hi", "max_tokens": null
1562 }))
1563 .unwrap();
1564 assert_eq!(req.max_output_tokens, None);
1565
1566 let err = serde_json::from_value::<CreateResponse>(serde_json::json!({
1568 "model": "m", "input": "hi", "max_tokens": 16, "max_output_tokens": 32
1569 }))
1570 .unwrap_err();
1571 assert!(err.to_string().contains("duplicate field"), "{err}");
1572 }
1573
1574 #[test]
1575 fn create_response_roundtrip_with_relaxed_input() {
1576 let body = serde_json::json!({
1577 "model": "m",
1578 "input": [
1579 {"type": "message", "role": "user", "content": [
1580 {"type": "input_text", "text": "hi"}
1581 ]},
1582 {"type": "function_call", "call_id": "c", "name": "f", "arguments": "{}"},
1583 {"type": "message", "role": "assistant", "content": [
1584 {"type": "output_text", "text": "\n\n"}
1585 ]},
1586 {"type": "function_call_output", "call_id": "c", "output": "x"}
1587 ]
1588 });
1589
1590 let req: CreateResponse = serde_json::from_value(body).unwrap();
1591 let items = match &req.input {
1592 InputParam::Items(items) => items,
1593 _ => panic!("expected Items"),
1594 };
1595 assert_eq!(items.len(), 4);
1596 assert!(matches!(
1597 items[2],
1598 InputItem::Item(Item::Message(MessageItem::Output(_)))
1599 ));
1600 }
1601
1602 #[test]
1610 fn easy_message_multimodal_without_type_routes_to_easymessage() {
1611 let json = serde_json::json!({
1614 "role": "user",
1615 "content": [
1616 {"type": "input_image", "image_url": "data:image/png;base64,abc"}
1617 ]
1618 });
1619 let item: InputItem = serde_json::from_value(json).unwrap();
1620 match item {
1621 InputItem::EasyMessage(easy) => {
1622 assert_eq!(easy.role, Role::User);
1623 assert_eq!(easy.r#type, MessageType::Message);
1624 match easy.content {
1625 EasyInputContent::ContentList(parts) => {
1626 assert_eq!(parts.len(), 1);
1627 match &parts[0] {
1628 InputContent::InputImage(img) => {
1629 assert_eq!(img.detail, ImageDetail::Auto);
1630 assert_eq!(
1631 img.image_url.as_deref(),
1632 Some("data:image/png;base64,abc")
1633 );
1634 }
1635 other => panic!("expected InputImage, got {other:?}"),
1636 }
1637 }
1638 other => panic!("expected ContentList, got {other:?}"),
1639 }
1640 }
1641 other => panic!("expected EasyMessage, got {other:?}"),
1642 }
1643 }
1644
1645 #[test]
1646 fn easy_message_multimodal_with_explicit_null_detail() {
1647 let json = serde_json::json!({
1651 "role": "user",
1652 "content": [
1653 {"type": "input_image", "image_url": "data:image/png;base64,abc", "detail": null}
1654 ]
1655 });
1656 let item: InputItem = serde_json::from_value(json).unwrap();
1657 assert!(matches!(item, InputItem::EasyMessage(_)));
1658 }
1659
1660 #[test]
1661 fn easy_message_assistant_multimodal_without_type() {
1662 let json = serde_json::json!({
1666 "role": "assistant",
1667 "content": [
1668 {"type": "input_text", "text": "ok"}
1669 ]
1670 });
1671 let item: InputItem = serde_json::from_value(json).unwrap();
1672 match item {
1673 InputItem::EasyMessage(easy) => {
1674 assert_eq!(easy.role, Role::Assistant);
1675 }
1676 other => panic!("expected EasyMessage(assistant), got {other:?}"),
1677 }
1678 }
1679
1680 #[test]
1681 fn easy_message_text_only_without_type_unchanged() {
1682 let json = serde_json::json!({"role": "user", "content": "Hello"});
1687 let item: InputItem = serde_json::from_value(json).unwrap();
1688 match item {
1689 InputItem::EasyMessage(easy) => {
1690 assert_eq!(easy.role, Role::User);
1691 assert!(matches!(easy.content, EasyInputContent::Text(ref s) if s == "Hello"));
1692 }
1693 other => panic!("expected EasyMessage(Text), got {other:?}"),
1694 }
1695 }
1696
1697 #[test]
1698 fn easy_message_with_explicit_type_still_routes_to_item_message() {
1699 let json = serde_json::json!({
1703 "type": "message",
1704 "role": "user",
1705 "content": [
1706 {"type": "input_image", "image_url": "data:image/png;base64,abc"}
1707 ]
1708 });
1709 let item: InputItem = serde_json::from_value(json).unwrap();
1710 match item {
1711 InputItem::Item(Item::Message(MessageItem::Input(msg))) => {
1712 assert_eq!(msg.role, InputRole::User);
1713 assert_eq!(msg.content.len(), 1);
1714 }
1715 other => panic!("expected Item::Message(Input), got {other:?}"),
1716 }
1717 }
1718
1719 #[test]
1720 fn create_response_roundtrip_aiperf_pre_pr931_payload() {
1721 let body = serde_json::json!({
1726 "model": "Qwen/Qwen2-VL-2B-Instruct",
1727 "input": [
1728 {
1729 "role": "user",
1730 "content": [
1731 {"type": "input_text", "text": "Describe"},
1732 {"type": "input_image", "image_url": "data:image/png;base64,abc"}
1733 ]
1734 },
1735 {
1736 "role": "assistant",
1737 "content": [{"type": "input_text", "text": "ok"}]
1738 },
1739 {
1740 "role": "user",
1741 "content": [{"type": "input_text", "text": "Now describe a different one."}]
1742 }
1743 ]
1744 });
1745 let req: CreateResponse = serde_json::from_value(body).unwrap();
1746 let items = match &req.input {
1747 InputParam::Items(items) => items,
1748 _ => panic!("expected Items"),
1749 };
1750 assert_eq!(items.len(), 3);
1751 for (idx, item) in items.iter().enumerate() {
1753 assert!(
1754 matches!(item, InputItem::EasyMessage(_)),
1755 "turn {idx} did not route to EasyMessage: {item:?}",
1756 );
1757 }
1758 }
1759
1760 fn count(body: serde_json::Value) -> u32 {
1763 serde_json::from_value::<CountInputTokensRequest>(body)
1764 .expect("count request should deserialize")
1765 .estimate_tokens()
1766 }
1767
1768 #[test]
1769 fn count_tokens_plain_text_input() {
1770 assert_eq!(
1772 count(serde_json::json!({"model": "m", "input": "Hello, world!"})),
1773 5
1774 );
1775 }
1776
1777 #[test]
1778 fn count_tokens_input_is_optional() {
1779 assert_eq!(count(serde_json::json!({"model": "m"})), 0);
1781 }
1782
1783 #[test]
1784 fn count_tokens_empty_input_is_zero() {
1785 assert_eq!(count(serde_json::json!({"input": ""})), 0);
1786 }
1787
1788 #[test]
1789 fn count_tokens_short_input_never_rounds_to_zero() {
1790 assert_eq!(
1797 count(serde_json::json!({"tools": [{"type": "function", "name": "a"}]})),
1798 1
1799 );
1800 assert_eq!(count(serde_json::json!({"input": "Hi"})), 2);
1803 }
1804
1805 #[test]
1806 fn count_tokens_instructions_contribute() {
1807 assert_eq!(
1810 count(serde_json::json!({"input": "Hi", "instructions": "You are helpful."})),
1811 9
1812 );
1813 }
1814
1815 #[test]
1816 fn count_tokens_scores_the_two_spellings_of_a_prompt_identically() {
1817 assert_eq!(
1823 count(serde_json::json!({"input": "Hello"})),
1824 count(serde_json::json!({"input": [{"role": "user", "content": "Hello"}]})),
1825 );
1826 assert_eq!(
1827 count(serde_json::json!({
1828 "input": "Hello",
1829 "instructions": "You are helpful."
1830 })),
1831 count(serde_json::json!({"input": [
1832 {"role": "system", "content": "You are helpful."},
1833 {"role": "user", "content": "Hello"}
1834 ]})),
1835 );
1836 }
1837
1838 #[test]
1839 fn count_tokens_easy_message_counts_role_and_content() {
1840 assert_eq!(
1842 count(serde_json::json!({"input": [{"role": "user", "content": "Hello"}]})),
1843 3
1844 );
1845 }
1846
1847 #[test]
1848 fn count_tokens_structured_input_message() {
1849 assert_eq!(
1851 count(serde_json::json!({"input": [{
1852 "type": "message",
1853 "role": "user",
1854 "content": [{"type": "input_text", "text": "Hello"}],
1855 }]})),
1856 3
1857 );
1858 }
1859
1860 #[test]
1861 fn count_tokens_function_call_counts_name_and_arguments() {
1862 assert_eq!(
1866 count(serde_json::json!({"input": [{
1867 "type": "function_call",
1868 "call_id": "call_1",
1869 "name": "get_weather",
1870 "arguments": r#"{"city":"SF"}"#,
1871 }]})),
1872 11
1873 );
1874 }
1875
1876 #[test]
1877 fn count_tokens_charges_one_assistant_marker_per_coalesced_turn() {
1878 let one = serde_json::json!({"input": [
1883 {"type": "function_call", "call_id": "c1", "name": "aa", "arguments": ""}
1884 ]});
1885 let two = serde_json::json!({"input": [
1886 {"type": "function_call", "call_id": "c1", "name": "aa", "arguments": ""},
1887 {"type": "function_call", "call_id": "c2", "name": "bb", "arguments": ""}
1888 ]});
1889 assert_eq!(count(one), 3);
1892 assert_eq!(count(two), 4);
1893
1894 let mixed = serde_json::json!({"input": [
1897 {"role": "assistant", "content": "aa"},
1898 {"type": "reasoning", "summary": [{"type": "summary_text", "text": "bb"}]},
1899 {"type": "function_call", "call_id": "c1", "name": "cc", "arguments": ""}
1900 ]});
1901 assert_eq!(count(mixed), 5); }
1903
1904 #[test]
1905 fn count_tokens_reopens_the_assistant_turn_after_a_flush() {
1906 let two_turns = serde_json::json!({"input": [
1909 {"type": "function_call", "call_id": "c1", "name": "aa", "arguments": ""},
1910 {"type": "function_call_output", "call_id": "c1", "output": ""},
1911 {"type": "function_call", "call_id": "c2", "name": "bb", "arguments": ""}
1912 ]});
1913 assert_eq!(count(two_turns), 8);
1916 }
1917
1918 #[test]
1919 fn count_tokens_item_reference_does_not_split_an_assistant_turn() {
1920 let split = serde_json::json!({"input": [
1923 {"type": "function_call", "call_id": "c1", "name": "aa", "arguments": ""},
1924 {"type": "item_reference", "id": "item_abc"},
1925 {"type": "function_call", "call_id": "c2", "name": "bb", "arguments": ""}
1926 ]});
1927 let unsplit = serde_json::json!({"input": [
1928 {"type": "function_call", "call_id": "c1", "name": "aa", "arguments": ""},
1929 {"type": "function_call", "call_id": "c2", "name": "bb", "arguments": ""}
1930 ]});
1931 assert_eq!(count(split), count(unsplit));
1932 }
1933
1934 #[test]
1935 fn count_tokens_unsupported_item_splits_an_assistant_turn() {
1936 let across = serde_json::json!({"input": [
1940 {"type": "function_call", "call_id": "c1", "name": "aa", "arguments": ""},
1941 {"type": "web_search_call", "id": "ws_1", "status": "completed"},
1942 {"type": "function_call", "call_id": "c2", "name": "bb", "arguments": ""}
1943 ]});
1944 assert_eq!(count(across), 7);
1946 }
1947
1948 #[test]
1949 fn count_tokens_function_call_output_counts_text() {
1950 assert_eq!(
1953 count(serde_json::json!({"input": [{
1954 "type": "function_call_output",
1955 "call_id": "call_1",
1956 "output": "sunny",
1957 }]})),
1958 3
1959 );
1960 }
1961
1962 #[test]
1963 fn count_tokens_tools_contribute() {
1964 assert_eq!(
1967 count(serde_json::json!({
1968 "input": "",
1969 "tools": [{
1970 "type": "function",
1971 "name": "get_weather",
1972 "description": "Get weather",
1973 "parameters": {"type": "object"},
1974 }],
1975 })),
1976 13
1977 );
1978 }
1979
1980 #[test]
1981 fn count_tokens_images_contribute_nothing() {
1982 let with_image = count(serde_json::json!({"input": [{
1985 "type": "message",
1986 "role": "user",
1987 "content": [
1988 {"type": "input_text", "text": "Describe this"},
1989 {"type": "input_image", "image_url": "https://example.com/a-very-long-url.png"},
1990 ],
1991 }]}));
1992 let without_image = count(serde_json::json!({"input": [{
1993 "type": "message",
1994 "role": "user",
1995 "content": [{"type": "input_text", "text": "Describe this"}],
1996 }]}));
1997 assert_eq!(with_image, without_image);
1998 }
1999
2000 #[test]
2001 fn count_tokens_dropped_item_variants_cost_nothing() {
2002 for item in [
2006 serde_json::json!({"type": "web_search_call", "id": "ws_1", "status": "completed"}),
2007 serde_json::json!({
2008 "type": "computer_call",
2009 "call_id": "c_1",
2010 "id": "cu_1",
2011 "action": {"type": "screenshot"},
2012 "pending_safety_checks": [],
2013 "status": "completed",
2014 }),
2015 ] {
2016 assert_eq!(
2017 count(serde_json::json!({ "input": [item.clone()] })),
2018 0,
2019 "dropped item variant should not be counted: {item}"
2020 );
2021 }
2022 }
2023
2024 #[test]
2025 fn count_tokens_counts_exactly_the_variants_the_converter_renders() {
2026 for item in [
2032 serde_json::json!({"role": "user", "content": "Hello"}),
2033 serde_json::json!({
2034 "type": "message",
2035 "role": "user",
2036 "content": [{"type": "input_text", "text": "Hello"}],
2037 }),
2038 serde_json::json!({
2039 "type": "message",
2040 "role": "assistant",
2041 "content": [{"type": "output_text", "text": "Hi", "annotations": []}],
2042 }),
2043 serde_json::json!({
2044 "type": "function_call",
2045 "call_id": "c1",
2046 "name": "get_weather",
2047 "arguments": "{}",
2048 }),
2049 serde_json::json!({"type": "function_call_output", "call_id": "c1", "output": "sunny"}),
2050 serde_json::json!({
2051 "type": "reasoning",
2052 "summary": [{"type": "summary_text", "text": "thinking"}],
2053 }),
2054 ] {
2055 assert!(
2056 count(serde_json::json!({ "input": [item.clone()] })) > 0,
2057 "rendered variant should be counted: {item}"
2058 );
2059 }
2060 }
2061
2062 #[test]
2063 fn count_tokens_reasoning_counts_summary_only() {
2064 let summary_only = serde_json::json!({"input": [{
2067 "type": "reasoning",
2068 "summary": [{"type": "summary_text", "text": "thinking"}],
2069 }]});
2070 let with_dropped_fields = serde_json::json!({"input": [{
2071 "type": "reasoning",
2072 "summary": [{"type": "summary_text", "text": "thinking"}],
2073 "content": [{"type": "reasoning_text", "text": "a much longer private chain of thought"}],
2074 "encrypted_content": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
2075 }]});
2076
2077 assert_eq!(count(summary_only.clone()), 5);
2080 assert_eq!(count(with_dropped_fields), count(summary_only));
2081 }
2082
2083 #[test]
2084 fn count_tokens_hosted_tools_cost_nothing() {
2085 assert_eq!(
2088 count(serde_json::json!({
2089 "input": "",
2090 "tools": [{"type": "web_search"}],
2091 })),
2092 0
2093 );
2094 }
2095
2096 #[test]
2097 fn count_tokens_namespaced_tools_count_their_functions() {
2098 assert_eq!(
2103 count(serde_json::json!({
2104 "input": "",
2105 "tools": [{
2106 "type": "namespace",
2107 "name": "weather_ns",
2108 "description": "Weather tools",
2109 "tools": [{
2110 "type": "function",
2111 "name": "get_weather",
2112 "description": "Get weather",
2113 "parameters": {"type": "object"},
2114 }],
2115 }],
2116 })),
2117 13
2118 );
2119 }
2120
2121 #[test]
2122 fn count_tokens_item_reference_contributes_nothing() {
2123 assert_eq!(
2125 count(serde_json::json!({"input": [{"type": "item_reference", "id": "msg_1"}]})),
2126 0
2127 );
2128 }
2129
2130 #[test]
2131 fn count_tokens_ignores_unsupported_stateful_fields() {
2132 assert_eq!(
2135 count(serde_json::json!({
2136 "model": "m",
2137 "input": "Hello, world!",
2138 "previous_response_id": "resp_abc123",
2139 "conversation": {"id": "conv_1"},
2140 })),
2141 5
2142 );
2143 }
2144
2145 #[test]
2146 fn count_tokens_deserializes_the_litellm_request_shape() {
2147 let request: CountInputTokensRequest = serde_json::from_value(serde_json::json!({
2151 "model": "dynamo/deepseek-ai/deepseek-v4-pro-sglang",
2152 "input": [{"role": "user", "content": "Hello"}],
2153 "instructions": "You are helpful.",
2154 "tools": [{
2155 "type": "function",
2156 "name": "get_weather",
2157 "description": "Get weather",
2158 "parameters": {"type": "object"},
2159 }],
2160 }))
2161 .expect("LiteLLM request shape should deserialize");
2162
2163 assert_eq!(
2164 request.model.as_deref(),
2165 Some("dynamo/deepseek-ai/deepseek-v4-pro-sglang")
2166 );
2167 assert!(matches!(request.input, InputParam::Items(ref items) if items.len() == 1));
2168 assert!(request.estimate_tokens() > 0);
2169 }
2170
2171 #[test]
2172 fn count_tokens_accepts_explicit_null_input() {
2173 assert_eq!(count(serde_json::json!({"model": "m", "input": null})), 0);
2176 assert_eq!(
2177 count(serde_json::json!({
2178 "model": "m",
2179 "input": null,
2180 "instructions": "You are helpful."
2181 })),
2182 7
2183 );
2184 }
2185
2186 #[test]
2187 fn count_tokens_drops_unparseable_tools_instead_of_failing() {
2188 let request: CountInputTokensRequest = serde_json::from_value(serde_json::json!({
2192 "model": "m",
2193 "input": "Hello, world!",
2194 "tools": [{"type": "custom", "custom": {"name": "x"}}],
2195 }))
2196 .expect("an unparseable tool should be dropped, not rejected");
2197 assert_eq!(request.tools.as_deref(), Some(&[][..]));
2198 assert_eq!(request.estimate_tokens(), 5);
2201 }
2202
2203 #[test]
2204 fn count_tokens_keeps_parseable_tools_alongside_dropped_ones() {
2205 let request: CountInputTokensRequest = serde_json::from_value(serde_json::json!({
2208 "model": "m",
2209 "input": "Hello, world!",
2210 "tools": [
2211 {"type": "custom", "custom": {"name": "x"}},
2212 {"type": "function", "name": "get_weather", "description": "Get weather"},
2213 ],
2214 }))
2215 .expect("a mixed tool array should deserialize");
2216 assert_eq!(request.tools.as_ref().map(Vec::len), Some(1));
2217 assert!(
2218 request.estimate_tokens()
2219 > count(serde_json::json!({"model": "m", "input": "Hello, world!"}))
2220 );
2221 }
2222
2223 #[test]
2224 fn count_tokens_distinguishes_absent_tools_from_empty_tools() {
2225 let absent: CountInputTokensRequest =
2228 serde_json::from_value(serde_json::json!({"input": "hi"})).unwrap();
2229 assert_eq!(absent.tools, None);
2230 let empty: CountInputTokensRequest =
2231 serde_json::from_value(serde_json::json!({"input": "hi", "tools": []})).unwrap();
2232 assert_eq!(empty.tools.as_deref(), Some(&[][..]));
2233 let null: CountInputTokensRequest =
2234 serde_json::from_value(serde_json::json!({"input": "hi", "tools": null})).unwrap();
2235 assert_eq!(null.tools, None);
2236 }
2237
2238 #[test]
2239 fn count_tokens_response_serializes_to_the_openai_shape() {
2240 assert_eq!(
2241 serde_json::to_value(CountInputTokensResponse::new(42)).unwrap(),
2242 serde_json::json!({"object": "response.input_tokens", "input_tokens": 42})
2243 );
2244 }
2245}
2246
2247#[cfg(test)]
2248mod reasoning_effort_tests {
2249 use super::CreateResponse;
2250
2251 #[test]
2252 fn every_reasoning_effort_deserializes() {
2253 for effort in ["none", "minimal", "low", "medium", "high", "xhigh", "max"] {
2254 let body = serde_json::json!({
2255 "model": "m", "input": "hi", "reasoning": {"effort": effort}
2256 });
2257 serde_json::from_value::<CreateResponse>(body)
2258 .unwrap_or_else(|e| panic!("effort {effort} must deserialize: {e}"));
2259 }
2260 }
2261}