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::Mode(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(skip_serializing_if = "Option::is_none")]
550 pub max_output_tokens: Option<u32>,
551 #[serde(skip_serializing_if = "Option::is_none")]
552 pub max_tool_calls: Option<u32>,
553 #[serde(skip_serializing_if = "Option::is_none")]
554 pub metadata: Option<HashMap<String, String>>,
555 #[serde(skip_serializing_if = "Option::is_none")]
556 pub model: Option<String>,
557 #[serde(skip_serializing_if = "Option::is_none")]
558 pub parallel_tool_calls: Option<bool>,
559 #[serde(skip_serializing_if = "Option::is_none")]
560 pub previous_response_id: Option<String>,
561 #[serde(skip_serializing_if = "Option::is_none")]
562 pub prompt: Option<Prompt>,
563 #[serde(skip_serializing_if = "Option::is_none")]
564 pub prompt_cache_key: Option<String>,
565 #[serde(skip_serializing_if = "Option::is_none")]
566 pub prompt_cache_retention: Option<PromptCacheRetention>,
567 #[serde(skip_serializing_if = "Option::is_none")]
568 pub reasoning: Option<Reasoning>,
569 #[serde(skip_serializing_if = "Option::is_none")]
570 pub safety_identifier: Option<String>,
571 #[serde(skip_serializing_if = "Option::is_none")]
572 pub service_tier: Option<ServiceTier>,
573 #[serde(skip_serializing_if = "Option::is_none")]
574 pub store: Option<bool>,
575 #[serde(skip_serializing_if = "Option::is_none")]
576 pub stream: Option<bool>,
577 #[serde(skip_serializing_if = "Option::is_none")]
578 pub stream_options: Option<ResponseStreamOptions>,
579 #[serde(skip_serializing_if = "Option::is_none")]
580 pub temperature: Option<f32>,
581 #[serde(skip_serializing_if = "Option::is_none")]
582 pub text: Option<ResponseTextParam>,
583 #[serde(
584 default,
585 deserialize_with = "deserialize_tool_choice",
586 skip_serializing_if = "Option::is_none"
587 )]
588 pub tool_choice: Option<ToolChoiceParam>,
589 #[serde(skip_serializing_if = "Option::is_none")]
590 pub tools: Option<Vec<Tool>>,
591 #[serde(skip_serializing_if = "Option::is_none")]
592 pub top_logprobs: Option<u8>,
593 #[serde(skip_serializing_if = "Option::is_none")]
594 pub top_p: Option<f32>,
595 #[serde(skip_serializing_if = "Option::is_none")]
596 pub truncation: Option<Truncation>,
597}
598
599pub const RESPONSE_INPUT_TOKENS_OBJECT: &str = "response.input_tokens";
605
606#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)]
625pub struct CountInputTokensRequest {
626 #[serde(default, skip_serializing_if = "Option::is_none")]
627 pub model: Option<String>,
628 #[serde(default, deserialize_with = "deserialize_null_default_input")]
632 pub input: InputParam,
633 #[serde(default, skip_serializing_if = "Option::is_none")]
634 pub instructions: Option<String>,
635 #[serde(
636 default,
637 skip_serializing_if = "Option::is_none",
638 deserialize_with = "deserialize_lenient_tools"
639 )]
640 pub tools: Option<Vec<Tool>>,
641}
642
643fn deserialize_null_default_input<'de, D>(deserializer: D) -> Result<InputParam, D::Error>
644where
645 D: serde::Deserializer<'de>,
646{
647 Ok(Option::<InputParam>::deserialize(deserializer)?.unwrap_or_default())
648}
649
650fn deserialize_lenient_tools<'de, D>(deserializer: D) -> Result<Option<Vec<Tool>>, D::Error>
668where
669 D: serde::Deserializer<'de>,
670{
671 let Some(raw) = Option::<Vec<serde_json::Value>>::deserialize(deserializer)? else {
672 return Ok(None);
673 };
674 Ok(Some(
675 raw.into_iter()
676 .filter_map(|tool| serde_json::from_value::<Tool>(tool).ok())
677 .collect(),
678 ))
679}
680
681#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
687pub struct CountInputTokensResponse {
688 pub object: String,
690 pub input_tokens: u32,
691}
692
693impl CountInputTokensResponse {
694 pub fn new(input_tokens: u32) -> Self {
695 Self {
696 object: RESPONSE_INPUT_TOKENS_OBJECT.to_string(),
697 input_tokens,
698 }
699 }
700}
701
702impl CountInputTokensRequest {
703 pub fn estimate_tokens(&self) -> u32 {
713 let mut total_len: usize = 0;
714
715 if let Some(instructions) = &self.instructions.as_ref().filter(|text| !text.is_empty()) {
725 total_len += role_len(Role::System) + instructions.len();
726 }
727
728 match &self.input {
729 InputParam::Text(text) if text.is_empty() => {}
730 InputParam::Text(text) => total_len += role_len(Role::User) + text.len(),
731 InputParam::Items(items) => total_len += estimate_input_items_len(items),
732 }
733
734 if let Some(tools) = &self.tools {
735 for tool in tools {
736 total_len += estimate_tool_len(tool);
737 }
738 }
739
740 let tokens = total_len / 3;
741 if tokens == 0 && total_len > 0 {
742 1
743 } else {
744 tokens as u32
745 }
746 }
747}
748
749fn role_len(role: Role) -> usize {
752 match role {
753 Role::User => 4,
754 Role::Assistant => 9,
755 Role::System => 6,
756 Role::Developer => 9,
757 }
758}
759
760fn input_role_len(role: InputRole) -> usize {
761 match role {
762 InputRole::User => 4,
763 InputRole::System => 6,
764 InputRole::Developer => 9,
765 }
766}
767
768const TOOL_ROLE_LEN: usize = 4;
774
775enum GroupEffect {
777 Assistant,
780 Flush,
782 Skip,
784}
785
786fn estimate_input_items_len(items: &[InputItem]) -> usize {
799 let mut total = 0;
800 let mut assistant_open = false;
801
802 for item in items {
803 let (effect, len) = measure_input_item(item);
804 total += len;
805 match effect {
806 GroupEffect::Assistant => {
807 if !assistant_open {
808 assistant_open = true;
809 total += role_len(Role::Assistant);
810 }
811 }
812 GroupEffect::Flush => assistant_open = false,
813 GroupEffect::Skip => {}
814 }
815 }
816
817 total
818}
819
820fn measure_input_item(item: &InputItem) -> (GroupEffect, usize) {
825 match item {
826 InputItem::ItemReference(_) => (GroupEffect::Skip, 0),
830 InputItem::EasyMessage(message) => {
831 let content = estimate_easy_content_len(&message.content);
832 match message.role {
833 Role::Assistant => (GroupEffect::Assistant, content),
836 role => (GroupEffect::Flush, role_len(role) + content),
837 }
838 }
839 InputItem::Item(item) => measure_item(item),
840 }
841}
842
843fn estimate_easy_content_len(content: &EasyInputContent) -> usize {
844 match content {
845 EasyInputContent::Text(text) => text.len(),
846 EasyInputContent::ContentList(parts) => parts.iter().map(estimate_input_content_len).sum(),
847 }
848}
849
850fn estimate_input_content_len(part: &InputContent) -> usize {
854 match part {
855 InputContent::InputText(text) => text.text.len(),
856 InputContent::InputImage(_) | InputContent::InputFile(_) => 0,
857 }
858}
859
860fn measure_item(item: &Item) -> (GroupEffect, usize) {
861 match item {
862 Item::Message(MessageItem::Input(message)) => (
863 GroupEffect::Flush,
864 input_role_len(message.role)
865 + message
866 .content
867 .iter()
868 .map(estimate_input_content_len)
869 .sum::<usize>(),
870 ),
871 Item::Message(MessageItem::Output(message)) => (
874 GroupEffect::Assistant,
875 message
876 .content
877 .iter()
878 .map(|part| match part {
879 InputOutputMessageContent::OutputText(text) => text.text.len(),
880 InputOutputMessageContent::Refusal(refusal) => refusal.refusal.len(),
881 })
882 .sum::<usize>(),
883 ),
884 Item::FunctionCall(call) => (
888 GroupEffect::Assistant,
889 call.name.len() + call.arguments.len(),
890 ),
891 Item::FunctionCallOutput(output) => (
894 GroupEffect::Flush,
895 TOOL_ROLE_LEN
896 + match &output.output {
897 FunctionCallOutput::Text(text) => text.len(),
898 FunctionCallOutput::Content(parts) => {
899 parts.iter().map(estimate_input_content_len).sum()
900 }
901 },
902 ),
903 Item::Reasoning(reasoning) => (
911 GroupEffect::Assistant,
912 reasoning
913 .summary
914 .iter()
915 .map(|part| match part {
916 SummaryPart::SummaryText(text) => text.text.len(),
917 })
918 .sum(),
919 ),
920 Item::FileSearchCall(_)
937 | Item::ComputerCall(_)
938 | Item::ComputerCallOutput(_)
939 | Item::WebSearchCall(_)
940 | Item::ToolSearchCall(_)
941 | Item::ToolSearchOutput(_)
942 | Item::Compaction(_)
943 | Item::ImageGenerationCall(_)
944 | Item::CodeInterpreterCall(_)
945 | Item::LocalShellCall(_)
946 | Item::LocalShellCallOutput(_)
947 | Item::ShellCall(_)
948 | Item::ShellCallOutput(_)
949 | Item::ApplyPatchCall(_)
950 | Item::ApplyPatchCallOutput(_)
951 | Item::McpListTools(_)
952 | Item::McpApprovalRequest(_)
953 | Item::McpApprovalResponse(_)
954 | Item::McpCall(_)
955 | Item::CustomToolCallOutput(_)
956 | Item::CustomToolCall(_) => (GroupEffect::Flush, 0),
957 }
958}
959
960fn estimate_tool_len(tool: &Tool) -> usize {
971 match tool {
972 Tool::Function(function) => function_tool_len(
973 &function.name,
974 function.description.as_ref(),
975 function.parameters.as_ref(),
976 ),
977 Tool::Namespace(namespace) => namespace
978 .tools
979 .iter()
980 .map(|tool| match tool {
981 NamespaceToolParamTool::Function(function) => function_tool_len(
985 &function.name,
986 function.description.as_ref(),
987 function.parameters.as_ref(),
988 ),
989 NamespaceToolParamTool::Custom(_) => 0,
990 })
991 .sum(),
992 _ => 0,
993 }
994}
995
996fn function_tool_len(
997 name: &str,
998 description: Option<&String>,
999 parameters: Option<&serde_json::Value>,
1000) -> usize {
1001 name.len()
1002 + description.map_or(0, |description| description.len())
1003 + parameters.map_or(0, |schema| schema.to_string().len())
1004}
1005
1006#[cfg(test)]
1007mod tests {
1008 use super::*;
1009
1010 fn tool_choice_of(json: serde_json::Value) -> Option<ToolChoiceParam> {
1013 let req: CreateResponse = serde_json::from_value(serde_json::json!({
1014 "input": "hi",
1015 "tool_choice": json,
1016 }))
1017 .expect("CreateResponse should deserialize");
1018 req.tool_choice
1019 }
1020
1021 #[test]
1022 fn tool_choice_mode_object_coerces_to_mode() {
1023 assert_eq!(
1026 tool_choice_of(serde_json::json!({"type": "auto", "disable_parallel_tool_use": true})),
1027 Some(ToolChoiceParam::Mode(ToolChoiceOptions::Auto)),
1028 );
1029 assert_eq!(
1030 tool_choice_of(serde_json::json!({"type": "none"})),
1031 Some(ToolChoiceParam::Mode(ToolChoiceOptions::None)),
1032 );
1033 assert_eq!(
1034 tool_choice_of(serde_json::json!({"type": "required"})),
1035 Some(ToolChoiceParam::Mode(ToolChoiceOptions::Required)),
1036 );
1037 }
1038
1039 #[test]
1040 fn tool_choice_bare_string_still_works() {
1041 assert_eq!(
1042 tool_choice_of(serde_json::json!("auto")),
1043 Some(ToolChoiceParam::Mode(ToolChoiceOptions::Auto)),
1044 );
1045 }
1046
1047 #[test]
1048 fn tool_choice_specific_function_object_still_works() {
1049 match tool_choice_of(serde_json::json!({"type": "function", "name": "get_weather"})) {
1052 Some(ToolChoiceParam::Function(f)) => assert_eq!(f.name, "get_weather"),
1053 other => panic!("expected Function tool choice, got {other:?}"),
1054 }
1055 }
1056
1057 #[test]
1058 fn tool_choice_absent_is_none() {
1059 let req: CreateResponse =
1060 serde_json::from_value(serde_json::json!({"input": "hi"})).unwrap();
1061 assert!(req.tool_choice.is_none());
1062 }
1063
1064 #[test]
1067 fn reasoning_input_without_id_deserializes() {
1068 let json = serde_json::json!({
1070 "type": "reasoning",
1071 "summary": [{"type": "summary_text", "text": "thinking"}],
1072 });
1073 match serde_json::from_value::<InputItem>(json).expect("should deserialize") {
1074 InputItem::Item(Item::Reasoning(r)) => {
1075 assert!(r.id.is_none());
1076 assert_eq!(r.summary.len(), 1);
1077 }
1078 other => panic!("expected Item::Reasoning, got {other:?}"),
1079 }
1080 }
1081
1082 #[test]
1083 fn reasoning_input_encrypted_without_id_or_summary_deserializes() {
1084 let json = serde_json::json!({
1085 "type": "reasoning",
1086 "encrypted_content": "AB==",
1087 });
1088 match serde_json::from_value::<InputItem>(json).expect("should deserialize") {
1089 InputItem::Item(Item::Reasoning(r)) => {
1090 assert!(r.id.is_none());
1091 assert!(r.summary.is_empty());
1092 assert_eq!(r.encrypted_content.as_deref(), Some("AB=="));
1093 }
1094 other => panic!("expected Item::Reasoning, got {other:?}"),
1095 }
1096 }
1097
1098 #[test]
1099 fn reasoning_input_with_id_still_works() {
1100 let json = serde_json::json!({
1101 "type": "reasoning",
1102 "id": "rs_1",
1103 "summary": [{"type": "summary_text", "text": "x"}],
1104 "status": "completed",
1105 });
1106 match serde_json::from_value::<InputItem>(json).expect("should deserialize") {
1107 InputItem::Item(Item::Reasoning(r)) => assert_eq!(r.id.as_deref(), Some("rs_1")),
1108 other => panic!("expected Item::Reasoning, got {other:?}"),
1109 }
1110 }
1111
1112 #[test]
1113 fn full_request_with_idless_reasoning_item_deserializes() {
1114 let req: Result<CreateResponse, _> = serde_json::from_value(serde_json::json!({
1117 "model": "m",
1118 "input": [
1119 {"role": "user", "content": "hi"},
1120 {"type": "reasoning", "summary": [{"type": "summary_text", "text": "x"}]},
1121 ],
1122 }));
1123 assert!(
1124 req.is_ok(),
1125 "idless reasoning input should deserialize: {req:?}"
1126 );
1127 }
1128
1129 #[test]
1130 fn codex_agent_message_normalizes_to_user_message() {
1131 let req: CreateResponse = serde_json::from_value(serde_json::json!({
1132 "input": [{
1133 "type": "agent_message",
1134 "author": "/root",
1135 "recipient": "/root/worker",
1136 "content": [
1137 {"type": "input_text", "text": "First."},
1138 {"type": "input_text", "text": "Second."},
1139 ],
1140 }],
1141 }))
1142 .expect("Codex agent message should deserialize");
1143
1144 let InputParam::Items(items) = req.input else {
1145 panic!("expected items");
1146 };
1147 assert!(matches!(
1148 &items[0],
1149 InputItem::EasyMessage(EasyInputMessage {
1150 role: Role::User,
1151 content: EasyInputContent::Text(text),
1152 ..
1153 }) if text == "First.\nSecond."
1154 ));
1155 }
1156
1157 #[test]
1158 fn codex_agent_message_string_content_normalizes_to_user_message() {
1159 let item: InputItem = serde_json::from_value(serde_json::json!({
1160 "type": "agent_message",
1161 "author": "/root",
1162 "recipient": "/root/worker",
1163 "content": "Return exactly OK.",
1164 }))
1165 .expect("Codex agent message with string content should deserialize");
1166
1167 assert!(matches!(
1168 item,
1169 InputItem::EasyMessage(EasyInputMessage {
1170 content: EasyInputContent::Text(text),
1171 ..
1172 }) if text == "Return exactly OK."
1173 ));
1174 }
1175
1176 #[test]
1177 fn codex_agent_message_normalizes_encrypted_content() {
1178 let req: CreateResponse = serde_json::from_value(serde_json::json!({
1179 "input": [{
1180 "type": "agent_message",
1181 "content": [
1182 {"type": "input_text", "text": "Payload:"},
1183 {"type": "encrypted_content", "encrypted_content": "Return exactly OK."},
1184 ],
1185 }],
1186 }))
1187 .expect("Codex agent message with encrypted content should deserialize");
1188
1189 let InputParam::Items(items) = req.input else {
1190 panic!("expected items");
1191 };
1192 assert!(matches!(
1193 &items[0],
1194 InputItem::EasyMessage(EasyInputMessage {
1195 content: EasyInputContent::Text(text),
1196 ..
1197 }) if text == "Payload:\nReturn exactly OK."
1198 ));
1199 }
1200
1201 #[test]
1202 fn codex_agent_message_missing_content_normalizes_empty() {
1203 let item: InputItem = serde_json::from_value(serde_json::json!({
1204 "type": "agent_message",
1205 "author": "/root",
1206 "recipient": "/root/worker",
1207 }))
1208 .expect("Codex agent message without content should deserialize");
1209 assert!(matches!(
1210 item,
1211 InputItem::EasyMessage(EasyInputMessage {
1212 content: EasyInputContent::Text(text),
1213 ..
1214 }) if text.is_empty()
1215 ));
1216 }
1217
1218 #[test]
1219 fn codex_agent_message_null_content_normalizes_empty() {
1220 let item: InputItem = serde_json::from_value(serde_json::json!({
1221 "type": "agent_message",
1222 "author": "/root",
1223 "recipient": "/root/worker",
1224 "content": null,
1225 }))
1226 .expect("Codex agent message with null content should deserialize");
1227 assert!(matches!(
1228 item,
1229 InputItem::EasyMessage(EasyInputMessage {
1230 content: EasyInputContent::Text(text),
1231 ..
1232 }) if text.is_empty()
1233 ));
1234 }
1235
1236 #[test]
1237 fn relaxed_assistant_message_without_id_or_status() {
1238 let json = serde_json::json!({
1239 "type": "message",
1240 "role": "assistant",
1241 "content": [{"type": "output_text", "text": "hi"}]
1242 });
1243 let item: InputItem = serde_json::from_value(json).unwrap();
1244 match item {
1245 InputItem::Item(Item::Message(MessageItem::Output(out))) => {
1246 assert_eq!(out.role, AssistantRole::Assistant);
1247 assert!(out.id.is_none());
1248 assert!(out.status.is_none());
1249 }
1250 other => panic!("expected Item::Message(Output), got {other:?}"),
1251 }
1252 }
1253
1254 #[test]
1255 fn function_call_output_image_part_without_detail_parses() {
1256 let json = serde_json::json!({
1257 "input": [
1258 {"type": "function_call", "call_id": "c1", "name": "screenshot", "arguments": "{}"},
1259 {"type": "function_call_output", "call_id": "c1", "output": [
1260 {"type": "input_text", "text": "captured"},
1261 {"type": "input_image", "image_url": "data:image/png;base64,iVBORw0KGgo="}
1262 ]}
1263 ]
1264 });
1265 let req: CreateResponse = serde_json::from_value(json).unwrap();
1266 let InputParam::Items(items) = req.input else {
1267 panic!("expected Items")
1268 };
1269 match &items[1] {
1270 InputItem::Item(Item::FunctionCallOutput(fco)) => {
1271 assert_eq!(fco.call_id, "c1");
1272 let FunctionCallOutput::Content(parts) = &fco.output else {
1273 panic!("expected Content, got {:?}", fco.output)
1274 };
1275 assert_eq!(parts.len(), 2);
1276 match &parts[1] {
1277 InputContent::InputImage(img) => {
1278 assert_eq!(img.detail, ImageDetail::Auto);
1279 assert_eq!(
1280 img.image_url.as_deref(),
1281 Some("data:image/png;base64,iVBORw0KGgo=")
1282 );
1283 }
1284 other => panic!("expected InputImage, got {other:?}"),
1285 }
1286 }
1287 other => panic!("expected FunctionCallOutput, got {other:?}"),
1288 }
1289 }
1290
1291 #[test]
1292 fn function_call_output_image_part_with_null_detail_matches_message_content() {
1293 let part = serde_json::json!({
1296 "type": "input_image", "image_url": "data:image/png;base64,iVBORw0KGgo=", "detail": null
1297 });
1298 let message: Item = serde_json::from_value(serde_json::json!({
1299 "type": "message", "role": "user", "content": [part]
1300 }))
1301 .unwrap();
1302 assert!(matches!(message, Item::Message(_)));
1303 let output: Item = serde_json::from_value(serde_json::json!({
1304 "type": "function_call_output", "call_id": "c1", "output": [part]
1305 }))
1306 .unwrap();
1307 let Item::FunctionCallOutput(fco) = output else {
1308 panic!("expected FunctionCallOutput, got {output:?}")
1309 };
1310 match &fco.output {
1311 FunctionCallOutput::Content(parts) => match &parts[0] {
1312 InputContent::InputImage(img) => assert_eq!(img.detail, ImageDetail::Auto),
1313 other => panic!("expected InputImage, got {other:?}"),
1314 },
1315 other => panic!("expected Content, got {other:?}"),
1316 }
1317 }
1318
1319 #[test]
1320 fn function_call_output_from_conversions_match_upstream() {
1321 assert_eq!(
1322 FunctionCallOutput::from("ok"),
1323 FunctionCallOutput::Text("ok".to_string())
1324 );
1325 assert_eq!(
1326 FunctionCallOutput::from(String::from("ok")),
1327 FunctionCallOutput::Text("ok".to_string())
1328 );
1329 let parts = vec![InputContent::InputText(InputTextContent {
1330 text: "captured".to_string(),
1331 })];
1332 let item = FunctionCallOutputItemParam {
1333 call_id: "c1".to_string(),
1334 output: parts.clone().into(),
1335 id: None,
1336 status: None,
1337 };
1338 assert_eq!(item.output, FunctionCallOutput::Content(parts));
1339 }
1340
1341 #[test]
1342 fn function_call_output_string_still_parses() {
1343 let item: Item = serde_json::from_value(serde_json::json!({
1344 "type": "function_call_output", "call_id": "c1", "output": "{\"ok\":true}"
1345 }))
1346 .unwrap();
1347 match item {
1348 Item::FunctionCallOutput(fco) => {
1349 assert!(
1350 matches!(fco.output, FunctionCallOutput::Text(ref t) if t == "{\"ok\":true}")
1351 );
1352 assert!(fco.id.is_none() && fco.status.is_none());
1353 }
1354 other => panic!("expected FunctionCallOutput, got {other:?}"),
1355 }
1356 }
1357
1358 #[test]
1359 fn input_image_without_detail_defaults_to_auto() {
1360 let json = serde_json::json!({
1361 "type": "input_image",
1362 "image_url": "https://example.com/cat.jpg"
1363 });
1364 let content: InputContent = serde_json::from_value(json).unwrap();
1365 match content {
1366 InputContent::InputImage(img) => assert_eq!(img.detail, ImageDetail::Auto),
1367 other => panic!("expected InputImage, got {other:?}"),
1368 }
1369 }
1370
1371 #[test]
1372 fn input_image_with_explicit_null_detail_defaults_to_auto() {
1373 let json = serde_json::json!({
1374 "type": "input_image",
1375 "image_url": "https://example.com/cat.jpg",
1376 "detail": null
1377 });
1378 let content: InputContent = serde_json::from_value(json).unwrap();
1379 match content {
1380 InputContent::InputImage(img) => assert_eq!(img.detail, ImageDetail::Auto),
1381 other => panic!("expected InputImage, got {other:?}"),
1382 }
1383 }
1384
1385 #[test]
1386 fn assistant_message_without_content_field_deserializes() {
1387 let json = serde_json::json!({
1391 "type": "message",
1392 "role": "assistant"
1393 });
1394 let item: InputItem = serde_json::from_value(json).unwrap();
1395 match item {
1396 InputItem::Item(Item::Message(MessageItem::Output(out))) => {
1397 assert_eq!(out.role, AssistantRole::Assistant);
1398 assert!(out.content.is_empty());
1399 assert!(out.id.is_none());
1400 assert!(out.status.is_none());
1401 }
1402 other => panic!("expected Item::Message(Output), got {other:?}"),
1403 }
1404 }
1405
1406 #[test]
1407 fn assistant_message_with_explicit_null_content_deserializes() {
1408 let json = serde_json::json!({
1412 "type": "message",
1413 "role": "assistant",
1414 "content": null
1415 });
1416 let item: InputItem = serde_json::from_value(json).unwrap();
1417 match item {
1418 InputItem::Item(Item::Message(MessageItem::Output(out))) => {
1419 assert!(out.content.is_empty());
1420 }
1421 other => panic!("expected Item::Message(Output), got {other:?}"),
1422 }
1423 }
1424
1425 #[test]
1426 fn mcp_call_item_deserializes() {
1427 let json = serde_json::json!({
1430 "type": "mcp_call",
1431 "id": "mcp_1",
1432 "server_label": "srv",
1433 "name": "t",
1434 "arguments": "{}"
1435 });
1436 let item: InputItem = serde_json::from_value(json).unwrap();
1437 assert!(matches!(item, InputItem::Item(Item::McpCall(_))));
1438 }
1439
1440 #[test]
1441 fn strict_assistant_message_still_deserializes() {
1442 let json = serde_json::json!({
1443 "type": "message",
1444 "role": "assistant",
1445 "id": "msg_1",
1446 "status": "completed",
1447 "content": [{"type": "output_text", "text": "hi", "annotations": []}]
1448 });
1449 let item: InputItem = serde_json::from_value(json).unwrap();
1450 match item {
1451 InputItem::Item(Item::Message(MessageItem::Output(out))) => {
1452 assert_eq!(out.id.as_deref(), Some("msg_1"));
1453 assert_eq!(out.status, Some(OutputStatus::Completed));
1454 }
1455 other => panic!("expected Item::Message(Output), got {other:?}"),
1456 }
1457 }
1458
1459 #[test]
1460 fn user_message_routes_to_input_variant() {
1461 let json = serde_json::json!({
1462 "type": "message",
1463 "role": "user",
1464 "content": [{"type": "input_text", "text": "hi"}]
1465 });
1466 let item: InputItem = serde_json::from_value(json).unwrap();
1467 assert!(matches!(
1468 item,
1469 InputItem::Item(Item::Message(MessageItem::Input(_)))
1470 ));
1471 }
1472
1473 #[test]
1474 fn function_call_item_still_deserializes() {
1475 let json = serde_json::json!({
1476 "type": "function_call",
1477 "call_id": "c",
1478 "name": "f",
1479 "arguments": "{}"
1480 });
1481 let item: InputItem = serde_json::from_value(json).unwrap();
1482 assert!(matches!(item, InputItem::Item(Item::FunctionCall(_))));
1483 }
1484
1485 #[test]
1486 fn easy_message_string_content_routes_to_easymessage() {
1487 let json = serde_json::json!({"role": "assistant", "content": "x"});
1488 let item: InputItem = serde_json::from_value(json).unwrap();
1489 assert!(matches!(item, InputItem::EasyMessage(_)));
1490 }
1491
1492 #[test]
1493 fn output_text_without_annotations_defaults_empty() {
1494 let json = serde_json::json!({"type": "output_text", "text": "hi"});
1495 let part: InputOutputMessageContent = serde_json::from_value(json).unwrap();
1496 match part {
1497 InputOutputMessageContent::OutputText(t) => {
1498 assert!(t.annotations.is_empty());
1499 }
1500 _ => panic!("expected OutputText"),
1501 }
1502 }
1503
1504 #[test]
1505 fn output_text_with_explicit_null_annotations_deserializes_as_empty() {
1506 let json = serde_json::json!({"type": "output_text", "text": "hi", "annotations": null});
1510 let part: InputOutputMessageContent = serde_json::from_value(json).unwrap();
1511 match part {
1512 InputOutputMessageContent::OutputText(t) => {
1513 assert!(t.annotations.is_empty());
1514 }
1515 _ => panic!("expected OutputText"),
1516 }
1517 }
1518
1519 #[test]
1520 fn assistant_message_with_explicit_null_id_and_status_deserializes() {
1521 let json = serde_json::json!({
1526 "type": "message",
1527 "role": "assistant",
1528 "id": null,
1529 "status": null,
1530 "content": [{"type": "output_text", "text": "hi", "annotations": null}]
1531 });
1532 let item: InputItem = serde_json::from_value(json).unwrap();
1533 match item {
1534 InputItem::Item(Item::Message(MessageItem::Output(out))) => {
1535 assert!(out.id.is_none());
1536 assert!(out.status.is_none());
1537 assert_eq!(out.content.len(), 1);
1538 }
1539 other => panic!("expected Item::Message(Output), got {other:?}"),
1540 }
1541 }
1542
1543 #[test]
1544 fn create_response_roundtrip_with_relaxed_input() {
1545 let body = serde_json::json!({
1546 "model": "m",
1547 "input": [
1548 {"type": "message", "role": "user", "content": [
1549 {"type": "input_text", "text": "hi"}
1550 ]},
1551 {"type": "function_call", "call_id": "c", "name": "f", "arguments": "{}"},
1552 {"type": "message", "role": "assistant", "content": [
1553 {"type": "output_text", "text": "\n\n"}
1554 ]},
1555 {"type": "function_call_output", "call_id": "c", "output": "x"}
1556 ]
1557 });
1558
1559 let req: CreateResponse = serde_json::from_value(body).unwrap();
1560 let items = match &req.input {
1561 InputParam::Items(items) => items,
1562 _ => panic!("expected Items"),
1563 };
1564 assert_eq!(items.len(), 4);
1565 assert!(matches!(
1566 items[2],
1567 InputItem::Item(Item::Message(MessageItem::Output(_)))
1568 ));
1569 }
1570
1571 #[test]
1579 fn easy_message_multimodal_without_type_routes_to_easymessage() {
1580 let json = serde_json::json!({
1583 "role": "user",
1584 "content": [
1585 {"type": "input_image", "image_url": "data:image/png;base64,abc"}
1586 ]
1587 });
1588 let item: InputItem = serde_json::from_value(json).unwrap();
1589 match item {
1590 InputItem::EasyMessage(easy) => {
1591 assert_eq!(easy.role, Role::User);
1592 assert_eq!(easy.r#type, MessageType::Message);
1593 match easy.content {
1594 EasyInputContent::ContentList(parts) => {
1595 assert_eq!(parts.len(), 1);
1596 match &parts[0] {
1597 InputContent::InputImage(img) => {
1598 assert_eq!(img.detail, ImageDetail::Auto);
1599 assert_eq!(
1600 img.image_url.as_deref(),
1601 Some("data:image/png;base64,abc")
1602 );
1603 }
1604 other => panic!("expected InputImage, got {other:?}"),
1605 }
1606 }
1607 other => panic!("expected ContentList, got {other:?}"),
1608 }
1609 }
1610 other => panic!("expected EasyMessage, got {other:?}"),
1611 }
1612 }
1613
1614 #[test]
1615 fn easy_message_multimodal_with_explicit_null_detail() {
1616 let json = serde_json::json!({
1620 "role": "user",
1621 "content": [
1622 {"type": "input_image", "image_url": "data:image/png;base64,abc", "detail": null}
1623 ]
1624 });
1625 let item: InputItem = serde_json::from_value(json).unwrap();
1626 assert!(matches!(item, InputItem::EasyMessage(_)));
1627 }
1628
1629 #[test]
1630 fn easy_message_assistant_multimodal_without_type() {
1631 let json = serde_json::json!({
1635 "role": "assistant",
1636 "content": [
1637 {"type": "input_text", "text": "ok"}
1638 ]
1639 });
1640 let item: InputItem = serde_json::from_value(json).unwrap();
1641 match item {
1642 InputItem::EasyMessage(easy) => {
1643 assert_eq!(easy.role, Role::Assistant);
1644 }
1645 other => panic!("expected EasyMessage(assistant), got {other:?}"),
1646 }
1647 }
1648
1649 #[test]
1650 fn easy_message_text_only_without_type_unchanged() {
1651 let json = serde_json::json!({"role": "user", "content": "Hello"});
1656 let item: InputItem = serde_json::from_value(json).unwrap();
1657 match item {
1658 InputItem::EasyMessage(easy) => {
1659 assert_eq!(easy.role, Role::User);
1660 assert!(matches!(easy.content, EasyInputContent::Text(ref s) if s == "Hello"));
1661 }
1662 other => panic!("expected EasyMessage(Text), got {other:?}"),
1663 }
1664 }
1665
1666 #[test]
1667 fn easy_message_with_explicit_type_still_routes_to_item_message() {
1668 let json = serde_json::json!({
1672 "type": "message",
1673 "role": "user",
1674 "content": [
1675 {"type": "input_image", "image_url": "data:image/png;base64,abc"}
1676 ]
1677 });
1678 let item: InputItem = serde_json::from_value(json).unwrap();
1679 match item {
1680 InputItem::Item(Item::Message(MessageItem::Input(msg))) => {
1681 assert_eq!(msg.role, InputRole::User);
1682 assert_eq!(msg.content.len(), 1);
1683 }
1684 other => panic!("expected Item::Message(Input), got {other:?}"),
1685 }
1686 }
1687
1688 #[test]
1689 fn create_response_roundtrip_aiperf_pre_pr931_payload() {
1690 let body = serde_json::json!({
1695 "model": "Qwen/Qwen2-VL-2B-Instruct",
1696 "input": [
1697 {
1698 "role": "user",
1699 "content": [
1700 {"type": "input_text", "text": "Describe"},
1701 {"type": "input_image", "image_url": "data:image/png;base64,abc"}
1702 ]
1703 },
1704 {
1705 "role": "assistant",
1706 "content": [{"type": "input_text", "text": "ok"}]
1707 },
1708 {
1709 "role": "user",
1710 "content": [{"type": "input_text", "text": "Now describe a different one."}]
1711 }
1712 ]
1713 });
1714 let req: CreateResponse = serde_json::from_value(body).unwrap();
1715 let items = match &req.input {
1716 InputParam::Items(items) => items,
1717 _ => panic!("expected Items"),
1718 };
1719 assert_eq!(items.len(), 3);
1720 for (idx, item) in items.iter().enumerate() {
1722 assert!(
1723 matches!(item, InputItem::EasyMessage(_)),
1724 "turn {idx} did not route to EasyMessage: {item:?}",
1725 );
1726 }
1727 }
1728
1729 fn count(body: serde_json::Value) -> u32 {
1732 serde_json::from_value::<CountInputTokensRequest>(body)
1733 .expect("count request should deserialize")
1734 .estimate_tokens()
1735 }
1736
1737 #[test]
1738 fn count_tokens_plain_text_input() {
1739 assert_eq!(
1741 count(serde_json::json!({"model": "m", "input": "Hello, world!"})),
1742 5
1743 );
1744 }
1745
1746 #[test]
1747 fn count_tokens_input_is_optional() {
1748 assert_eq!(count(serde_json::json!({"model": "m"})), 0);
1750 }
1751
1752 #[test]
1753 fn count_tokens_empty_input_is_zero() {
1754 assert_eq!(count(serde_json::json!({"input": ""})), 0);
1755 }
1756
1757 #[test]
1758 fn count_tokens_short_input_never_rounds_to_zero() {
1759 assert_eq!(
1766 count(serde_json::json!({"tools": [{"type": "function", "name": "a"}]})),
1767 1
1768 );
1769 assert_eq!(count(serde_json::json!({"input": "Hi"})), 2);
1772 }
1773
1774 #[test]
1775 fn count_tokens_instructions_contribute() {
1776 assert_eq!(
1779 count(serde_json::json!({"input": "Hi", "instructions": "You are helpful."})),
1780 9
1781 );
1782 }
1783
1784 #[test]
1785 fn count_tokens_scores_the_two_spellings_of_a_prompt_identically() {
1786 assert_eq!(
1792 count(serde_json::json!({"input": "Hello"})),
1793 count(serde_json::json!({"input": [{"role": "user", "content": "Hello"}]})),
1794 );
1795 assert_eq!(
1796 count(serde_json::json!({
1797 "input": "Hello",
1798 "instructions": "You are helpful."
1799 })),
1800 count(serde_json::json!({"input": [
1801 {"role": "system", "content": "You are helpful."},
1802 {"role": "user", "content": "Hello"}
1803 ]})),
1804 );
1805 }
1806
1807 #[test]
1808 fn count_tokens_easy_message_counts_role_and_content() {
1809 assert_eq!(
1811 count(serde_json::json!({"input": [{"role": "user", "content": "Hello"}]})),
1812 3
1813 );
1814 }
1815
1816 #[test]
1817 fn count_tokens_structured_input_message() {
1818 assert_eq!(
1820 count(serde_json::json!({"input": [{
1821 "type": "message",
1822 "role": "user",
1823 "content": [{"type": "input_text", "text": "Hello"}],
1824 }]})),
1825 3
1826 );
1827 }
1828
1829 #[test]
1830 fn count_tokens_function_call_counts_name_and_arguments() {
1831 assert_eq!(
1835 count(serde_json::json!({"input": [{
1836 "type": "function_call",
1837 "call_id": "call_1",
1838 "name": "get_weather",
1839 "arguments": r#"{"city":"SF"}"#,
1840 }]})),
1841 11
1842 );
1843 }
1844
1845 #[test]
1846 fn count_tokens_charges_one_assistant_marker_per_coalesced_turn() {
1847 let one = serde_json::json!({"input": [
1852 {"type": "function_call", "call_id": "c1", "name": "aa", "arguments": ""}
1853 ]});
1854 let two = serde_json::json!({"input": [
1855 {"type": "function_call", "call_id": "c1", "name": "aa", "arguments": ""},
1856 {"type": "function_call", "call_id": "c2", "name": "bb", "arguments": ""}
1857 ]});
1858 assert_eq!(count(one), 3);
1861 assert_eq!(count(two), 4);
1862
1863 let mixed = serde_json::json!({"input": [
1866 {"role": "assistant", "content": "aa"},
1867 {"type": "reasoning", "summary": [{"type": "summary_text", "text": "bb"}]},
1868 {"type": "function_call", "call_id": "c1", "name": "cc", "arguments": ""}
1869 ]});
1870 assert_eq!(count(mixed), 5); }
1872
1873 #[test]
1874 fn count_tokens_reopens_the_assistant_turn_after_a_flush() {
1875 let two_turns = serde_json::json!({"input": [
1878 {"type": "function_call", "call_id": "c1", "name": "aa", "arguments": ""},
1879 {"type": "function_call_output", "call_id": "c1", "output": ""},
1880 {"type": "function_call", "call_id": "c2", "name": "bb", "arguments": ""}
1881 ]});
1882 assert_eq!(count(two_turns), 8);
1885 }
1886
1887 #[test]
1888 fn count_tokens_item_reference_does_not_split_an_assistant_turn() {
1889 let split = serde_json::json!({"input": [
1892 {"type": "function_call", "call_id": "c1", "name": "aa", "arguments": ""},
1893 {"type": "item_reference", "id": "item_abc"},
1894 {"type": "function_call", "call_id": "c2", "name": "bb", "arguments": ""}
1895 ]});
1896 let unsplit = serde_json::json!({"input": [
1897 {"type": "function_call", "call_id": "c1", "name": "aa", "arguments": ""},
1898 {"type": "function_call", "call_id": "c2", "name": "bb", "arguments": ""}
1899 ]});
1900 assert_eq!(count(split), count(unsplit));
1901 }
1902
1903 #[test]
1904 fn count_tokens_unsupported_item_splits_an_assistant_turn() {
1905 let across = serde_json::json!({"input": [
1909 {"type": "function_call", "call_id": "c1", "name": "aa", "arguments": ""},
1910 {"type": "web_search_call", "id": "ws_1", "status": "completed"},
1911 {"type": "function_call", "call_id": "c2", "name": "bb", "arguments": ""}
1912 ]});
1913 assert_eq!(count(across), 7);
1915 }
1916
1917 #[test]
1918 fn count_tokens_function_call_output_counts_text() {
1919 assert_eq!(
1922 count(serde_json::json!({"input": [{
1923 "type": "function_call_output",
1924 "call_id": "call_1",
1925 "output": "sunny",
1926 }]})),
1927 3
1928 );
1929 }
1930
1931 #[test]
1932 fn count_tokens_tools_contribute() {
1933 assert_eq!(
1936 count(serde_json::json!({
1937 "input": "",
1938 "tools": [{
1939 "type": "function",
1940 "name": "get_weather",
1941 "description": "Get weather",
1942 "parameters": {"type": "object"},
1943 }],
1944 })),
1945 13
1946 );
1947 }
1948
1949 #[test]
1950 fn count_tokens_images_contribute_nothing() {
1951 let with_image = count(serde_json::json!({"input": [{
1954 "type": "message",
1955 "role": "user",
1956 "content": [
1957 {"type": "input_text", "text": "Describe this"},
1958 {"type": "input_image", "image_url": "https://example.com/a-very-long-url.png"},
1959 ],
1960 }]}));
1961 let without_image = count(serde_json::json!({"input": [{
1962 "type": "message",
1963 "role": "user",
1964 "content": [{"type": "input_text", "text": "Describe this"}],
1965 }]}));
1966 assert_eq!(with_image, without_image);
1967 }
1968
1969 #[test]
1970 fn count_tokens_dropped_item_variants_cost_nothing() {
1971 for item in [
1975 serde_json::json!({"type": "web_search_call", "id": "ws_1", "status": "completed"}),
1976 serde_json::json!({
1977 "type": "computer_call",
1978 "call_id": "c_1",
1979 "id": "cu_1",
1980 "action": {"type": "screenshot"},
1981 "pending_safety_checks": [],
1982 "status": "completed",
1983 }),
1984 ] {
1985 assert_eq!(
1986 count(serde_json::json!({ "input": [item.clone()] })),
1987 0,
1988 "dropped item variant should not be counted: {item}"
1989 );
1990 }
1991 }
1992
1993 #[test]
1994 fn count_tokens_counts_exactly_the_variants_the_converter_renders() {
1995 for item in [
2001 serde_json::json!({"role": "user", "content": "Hello"}),
2002 serde_json::json!({
2003 "type": "message",
2004 "role": "user",
2005 "content": [{"type": "input_text", "text": "Hello"}],
2006 }),
2007 serde_json::json!({
2008 "type": "message",
2009 "role": "assistant",
2010 "content": [{"type": "output_text", "text": "Hi", "annotations": []}],
2011 }),
2012 serde_json::json!({
2013 "type": "function_call",
2014 "call_id": "c1",
2015 "name": "get_weather",
2016 "arguments": "{}",
2017 }),
2018 serde_json::json!({"type": "function_call_output", "call_id": "c1", "output": "sunny"}),
2019 serde_json::json!({
2020 "type": "reasoning",
2021 "summary": [{"type": "summary_text", "text": "thinking"}],
2022 }),
2023 ] {
2024 assert!(
2025 count(serde_json::json!({ "input": [item.clone()] })) > 0,
2026 "rendered variant should be counted: {item}"
2027 );
2028 }
2029 }
2030
2031 #[test]
2032 fn count_tokens_reasoning_counts_summary_only() {
2033 let summary_only = serde_json::json!({"input": [{
2036 "type": "reasoning",
2037 "summary": [{"type": "summary_text", "text": "thinking"}],
2038 }]});
2039 let with_dropped_fields = serde_json::json!({"input": [{
2040 "type": "reasoning",
2041 "summary": [{"type": "summary_text", "text": "thinking"}],
2042 "content": [{"type": "reasoning_text", "text": "a much longer private chain of thought"}],
2043 "encrypted_content": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
2044 }]});
2045
2046 assert_eq!(count(summary_only.clone()), 5);
2049 assert_eq!(count(with_dropped_fields), count(summary_only));
2050 }
2051
2052 #[test]
2053 fn count_tokens_hosted_tools_cost_nothing() {
2054 assert_eq!(
2057 count(serde_json::json!({
2058 "input": "",
2059 "tools": [{"type": "web_search"}],
2060 })),
2061 0
2062 );
2063 }
2064
2065 #[test]
2066 fn count_tokens_namespaced_tools_count_their_functions() {
2067 assert_eq!(
2072 count(serde_json::json!({
2073 "input": "",
2074 "tools": [{
2075 "type": "namespace",
2076 "name": "weather_ns",
2077 "description": "Weather tools",
2078 "tools": [{
2079 "type": "function",
2080 "name": "get_weather",
2081 "description": "Get weather",
2082 "parameters": {"type": "object"},
2083 }],
2084 }],
2085 })),
2086 13
2087 );
2088 }
2089
2090 #[test]
2091 fn count_tokens_item_reference_contributes_nothing() {
2092 assert_eq!(
2094 count(serde_json::json!({"input": [{"type": "item_reference", "id": "msg_1"}]})),
2095 0
2096 );
2097 }
2098
2099 #[test]
2100 fn count_tokens_ignores_unsupported_stateful_fields() {
2101 assert_eq!(
2104 count(serde_json::json!({
2105 "model": "m",
2106 "input": "Hello, world!",
2107 "previous_response_id": "resp_abc123",
2108 "conversation": {"id": "conv_1"},
2109 })),
2110 5
2111 );
2112 }
2113
2114 #[test]
2115 fn count_tokens_deserializes_the_litellm_request_shape() {
2116 let request: CountInputTokensRequest = serde_json::from_value(serde_json::json!({
2120 "model": "dynamo/deepseek-ai/deepseek-v4-pro-sglang",
2121 "input": [{"role": "user", "content": "Hello"}],
2122 "instructions": "You are helpful.",
2123 "tools": [{
2124 "type": "function",
2125 "name": "get_weather",
2126 "description": "Get weather",
2127 "parameters": {"type": "object"},
2128 }],
2129 }))
2130 .expect("LiteLLM request shape should deserialize");
2131
2132 assert_eq!(
2133 request.model.as_deref(),
2134 Some("dynamo/deepseek-ai/deepseek-v4-pro-sglang")
2135 );
2136 assert!(matches!(request.input, InputParam::Items(ref items) if items.len() == 1));
2137 assert!(request.estimate_tokens() > 0);
2138 }
2139
2140 #[test]
2141 fn count_tokens_accepts_explicit_null_input() {
2142 assert_eq!(count(serde_json::json!({"model": "m", "input": null})), 0);
2145 assert_eq!(
2146 count(serde_json::json!({
2147 "model": "m",
2148 "input": null,
2149 "instructions": "You are helpful."
2150 })),
2151 7
2152 );
2153 }
2154
2155 #[test]
2156 fn count_tokens_drops_unparseable_tools_instead_of_failing() {
2157 let request: CountInputTokensRequest = serde_json::from_value(serde_json::json!({
2161 "model": "m",
2162 "input": "Hello, world!",
2163 "tools": [{"type": "custom", "custom": {"name": "x"}}],
2164 }))
2165 .expect("an unparseable tool should be dropped, not rejected");
2166 assert_eq!(request.tools.as_deref(), Some(&[][..]));
2167 assert_eq!(request.estimate_tokens(), 5);
2170 }
2171
2172 #[test]
2173 fn count_tokens_keeps_parseable_tools_alongside_dropped_ones() {
2174 let request: CountInputTokensRequest = serde_json::from_value(serde_json::json!({
2177 "model": "m",
2178 "input": "Hello, world!",
2179 "tools": [
2180 {"type": "custom", "custom": {"name": "x"}},
2181 {"type": "function", "name": "get_weather", "description": "Get weather"},
2182 ],
2183 }))
2184 .expect("a mixed tool array should deserialize");
2185 assert_eq!(request.tools.as_ref().map(Vec::len), Some(1));
2186 assert!(
2187 request.estimate_tokens()
2188 > count(serde_json::json!({"model": "m", "input": "Hello, world!"}))
2189 );
2190 }
2191
2192 #[test]
2193 fn count_tokens_distinguishes_absent_tools_from_empty_tools() {
2194 let absent: CountInputTokensRequest =
2197 serde_json::from_value(serde_json::json!({"input": "hi"})).unwrap();
2198 assert_eq!(absent.tools, None);
2199 let empty: CountInputTokensRequest =
2200 serde_json::from_value(serde_json::json!({"input": "hi", "tools": []})).unwrap();
2201 assert_eq!(empty.tools.as_deref(), Some(&[][..]));
2202 let null: CountInputTokensRequest =
2203 serde_json::from_value(serde_json::json!({"input": "hi", "tools": null})).unwrap();
2204 assert_eq!(null.tools, None);
2205 }
2206
2207 #[test]
2208 fn count_tokens_response_serializes_to_the_openai_shape() {
2209 assert_eq!(
2210 serde_json::to_value(CountInputTokensResponse::new(42)).unwrap(),
2211 serde_json::json!({"object": "response.input_tokens", "input_tokens": 42})
2212 );
2213 }
2214}