1use std::collections::HashMap;
32
33use serde::{Deserialize, Serialize, de};
34
35pub use async_openai::types::responses::*;
39
40pub use async_openai::types::responses::InputContent as UpstreamInputContent;
46
47pub use crate::types::ImageDetail;
49pub use crate::types::ReasoningEffort;
50pub use crate::types::ResponseFormatJsonSchema;
51
52pub type Input = InputParam;
54pub type PromptConfig = Prompt;
55pub type TextConfig = ResponseTextParam;
56pub type TextResponseFormat = TextResponseFormatConfiguration;
57
58pub type ResponseStream = std::pin::Pin<
60 Box<dyn futures::Stream<Item = Result<ResponseStreamEvent, crate::error::OpenAIError>> + Send>,
61>;
62
63pub const SPEC_NULLABLE_REQUIRED_RESPONSE_FIELDS: &[&str] = &[
80 "billing",
81 "completed_at",
82 "conversation",
83 "error",
84 "incomplete_details",
85 "instructions",
86 "max_output_tokens",
87 "max_tool_calls",
88 "previous_response_id",
89 "prompt",
90 "prompt_cache_key",
91 "prompt_cache_retention",
92 "reasoning",
93 "safety_identifier",
94 "usage",
95];
96
97fn deserialize_null_as_empty_vec<'de, T, D>(deserializer: D) -> Result<Vec<T>, D::Error>
107where
108 T: Deserialize<'de>,
109 D: serde::Deserializer<'de>,
110{
111 Option::<Vec<T>>::deserialize(deserializer).map(Option::unwrap_or_default)
112}
113
114fn deserialize_null_as_default<'de, T, D>(deserializer: D) -> Result<T, D::Error>
120where
121 T: Deserialize<'de> + Default,
122 D: serde::Deserializer<'de>,
123{
124 Option::<T>::deserialize(deserializer).map(Option::unwrap_or_default)
125}
126
127fn deserialize_tool_choice<'de, D>(deserializer: D) -> Result<Option<ToolChoiceParam>, D::Error>
143where
144 D: serde::Deserializer<'de>,
145{
146 let Some(value) = Option::<serde_json::Value>::deserialize(deserializer)? else {
147 return Ok(None);
148 };
149 if let Some(serde_json::Value::String(t)) = value.get("type") {
150 let mode = match t.as_str() {
151 "auto" => Some(ToolChoiceOptions::Auto),
152 "none" => Some(ToolChoiceOptions::None),
153 "required" => Some(ToolChoiceOptions::Required),
154 _ => None,
155 };
156 if let Some(mode) = mode {
157 return Ok(Some(ToolChoiceParam::Mode(mode)));
158 }
159 }
160 ToolChoiceParam::deserialize(value)
161 .map(Some)
162 .map_err(serde::de::Error::custom)
163}
164
165#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
169pub struct InputOutputTextContent {
170 #[serde(default, deserialize_with = "deserialize_null_as_empty_vec")]
171 pub annotations: Vec<Annotation>,
172 #[serde(default, skip_serializing_if = "Option::is_none")]
173 pub logprobs: Option<Vec<LogProb>>,
174 pub text: String,
175}
176
177#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
179#[serde(tag = "type", rename_all = "snake_case")]
180pub enum InputOutputMessageContent {
181 OutputText(InputOutputTextContent),
182 Refusal(RefusalContent),
183}
184
185#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
192pub struct InputOutputMessage {
193 #[serde(default, deserialize_with = "deserialize_null_as_empty_vec")]
194 pub content: Vec<InputOutputMessageContent>,
195 #[serde(default, skip_serializing_if = "Option::is_none")]
196 pub id: Option<String>,
197 pub role: AssistantRole,
198 #[serde(default, skip_serializing_if = "Option::is_none")]
199 pub phase: Option<MessagePhase>,
200 #[serde(default, skip_serializing_if = "Option::is_none")]
201 pub status: Option<OutputStatus>,
202}
203
204#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
213pub struct InputImageContent {
214 #[serde(default, deserialize_with = "deserialize_null_as_default")]
215 pub detail: ImageDetail,
216 #[serde(default, skip_serializing_if = "Option::is_none")]
217 pub file_id: Option<String>,
218 #[serde(default, skip_serializing_if = "Option::is_none")]
219 pub image_url: Option<String>,
220}
221
222#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
226#[serde(tag = "type", rename_all = "snake_case")]
227pub enum InputContent {
228 InputText(InputTextContent),
229 InputImage(InputImageContent),
230 InputFile(InputFileContent),
231}
232
233#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)]
236pub struct InputMessage {
237 pub content: Vec<InputContent>,
238 pub role: InputRole,
239 #[serde(default, skip_serializing_if = "Option::is_none")]
240 pub status: Option<OutputStatus>,
241}
242
243#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
254#[serde(untagged)]
255pub enum EasyInputContent {
256 Text(String),
258 ContentList(Vec<InputContent>),
260}
261
262impl Default for EasyInputContent {
263 fn default() -> Self {
264 Self::Text(String::new())
265 }
266}
267
268#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)]
274pub struct EasyInputMessage {
275 #[serde(default)]
279 pub r#type: MessageType,
280 pub role: Role,
281 pub content: EasyInputContent,
282 #[serde(default, skip_serializing_if = "Option::is_none")]
283 pub phase: Option<MessagePhase>,
284}
285
286#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
299#[serde(untagged)]
300pub enum MessageItem {
301 Output(InputOutputMessage),
304 Input(InputMessage),
306}
307
308#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
324pub struct InputReasoningItem {
325 #[serde(default, skip_serializing_if = "Option::is_none")]
327 pub id: Option<String>,
328 #[serde(default)]
330 pub summary: Vec<SummaryPart>,
331 #[serde(default, skip_serializing_if = "Option::is_none")]
332 pub content: Option<Vec<ReasoningTextContent>>,
333 #[serde(default, skip_serializing_if = "Option::is_none")]
334 pub encrypted_content: Option<String>,
335 #[serde(default, skip_serializing_if = "Option::is_none")]
336 pub status: Option<OutputStatus>,
337}
338
339#[derive(Deserialize)]
341struct CodexAgentMessage {
342 #[serde(default)]
343 content: Option<CodexAgentMessageContent>,
344}
345
346#[derive(Deserialize)]
347#[serde(untagged)]
348enum CodexAgentMessageContent {
349 Text(String),
350 Parts(Vec<CodexAgentMessageInputContent>),
351}
352
353#[derive(Deserialize)]
354#[serde(tag = "type", rename_all = "snake_case")]
355enum CodexAgentMessageInputContent {
356 InputText(InputTextContent),
357 EncryptedContent { encrypted_content: String },
358}
359
360#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
363#[serde(tag = "type", rename_all = "snake_case")]
364pub enum Item {
365 Message(MessageItem),
366 FileSearchCall(FileSearchToolCall),
367 ComputerCall(ComputerToolCall),
368 ComputerCallOutput(ComputerCallOutputItemParam),
369 WebSearchCall(WebSearchToolCall),
370 FunctionCall(FunctionToolCall),
371 FunctionCallOutput(FunctionCallOutputItemParam),
372 ToolSearchCall(ToolSearchCallItemParam),
373 ToolSearchOutput(ToolSearchOutputItemParam),
374 Reasoning(InputReasoningItem),
375 Compaction(CompactionSummaryItemParam),
376 ImageGenerationCall(ImageGenToolCall),
377 CodeInterpreterCall(CodeInterpreterToolCall),
378 LocalShellCall(LocalShellToolCall),
379 LocalShellCallOutput(LocalShellToolCallOutput),
380 ShellCall(FunctionShellCallItemParam),
381 ShellCallOutput(FunctionShellCallOutputItemParam),
382 ApplyPatchCall(ApplyPatchToolCallItemParam),
383 ApplyPatchCallOutput(ApplyPatchToolCallOutputItemParam),
384 McpListTools(MCPListTools),
385 McpApprovalRequest(MCPApprovalRequest),
386 McpApprovalResponse(MCPApprovalResponse),
387 McpCall(MCPToolCall),
388 CustomToolCallOutput(CustomToolCallOutput),
389 CustomToolCall(CustomToolCall),
390}
391
392#[derive(Debug, Serialize, Clone, PartialEq)]
394#[serde(untagged)]
395pub enum InputItem {
396 ItemReference(ItemReference),
397 Item(Item),
398 EasyMessage(EasyInputMessage),
399}
400
401#[derive(Deserialize)]
402#[serde(untagged)]
403enum InputItemWire {
404 ItemReference(ItemReference),
405 Item(Item),
406 EasyMessage(EasyInputMessage),
407}
408
409impl<'de> Deserialize<'de> for InputItem {
410 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
411 where
412 D: serde::Deserializer<'de>,
413 {
414 let value = serde_json::Value::deserialize(deserializer)?;
415 if value.get("type").and_then(serde_json::Value::as_str) == Some("agent_message") {
416 let message = CodexAgentMessage::deserialize(value).map_err(de::Error::custom)?;
417 return Ok(normalize_codex_agent_message(message));
418 }
419
420 match InputItemWire::deserialize(value).map_err(de::Error::custom)? {
421 InputItemWire::ItemReference(item) => Ok(Self::ItemReference(item)),
422 InputItemWire::Item(item) => Ok(Self::Item(item)),
423 InputItemWire::EasyMessage(message) => Ok(Self::EasyMessage(message)),
424 }
425 }
426}
427
428fn normalize_codex_agent_message(message: CodexAgentMessage) -> InputItem {
429 let content = match message.content {
430 None => String::new(),
431 Some(CodexAgentMessageContent::Text(text)) => text,
432 Some(CodexAgentMessageContent::Parts(parts)) => parts
433 .into_iter()
434 .map(|part| match part {
435 CodexAgentMessageInputContent::InputText(part) => part.text,
436 CodexAgentMessageInputContent::EncryptedContent { encrypted_content } => {
437 encrypted_content
438 }
439 })
440 .collect::<Vec<_>>()
441 .join("\n"),
442 };
443 InputItem::EasyMessage(EasyInputMessage {
444 r#type: MessageType::Message,
445 role: Role::User,
446 content: EasyInputContent::Text(content),
447 phase: None,
448 })
449}
450
451#[derive(Debug, Serialize, Clone, PartialEq)]
453#[serde(untagged)]
454pub enum InputParam {
455 Text(String),
456 Items(Vec<InputItem>),
457}
458
459impl<'de> Deserialize<'de> for InputParam {
460 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
461 where
462 D: serde::Deserializer<'de>,
463 {
464 match serde_json::Value::deserialize(deserializer)? {
465 serde_json::Value::String(text) => Ok(Self::Text(text)),
466 serde_json::Value::Array(items) => {
467 serde_json::from_value(serde_json::Value::Array(items))
468 .map(Self::Items)
469 .map_err(de::Error::custom)
470 }
471 _ => Err(de::Error::custom(
472 "input must be a string or an array of input items",
473 )),
474 }
475 }
476}
477
478impl Default for InputParam {
479 fn default() -> Self {
480 Self::Text(String::new())
481 }
482}
483
484#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)]
493pub struct CreateResponse {
494 #[serde(skip_serializing_if = "Option::is_none")]
495 pub background: Option<bool>,
496 #[serde(skip_serializing_if = "Option::is_none")]
497 pub conversation: Option<ConversationParam>,
498 #[serde(skip_serializing_if = "Option::is_none")]
499 pub include: Option<Vec<IncludeEnum>>,
500 pub input: InputParam,
501 #[serde(skip_serializing_if = "Option::is_none")]
502 pub instructions: Option<String>,
503 #[serde(skip_serializing_if = "Option::is_none")]
504 pub max_output_tokens: Option<u32>,
505 #[serde(skip_serializing_if = "Option::is_none")]
506 pub max_tool_calls: Option<u32>,
507 #[serde(skip_serializing_if = "Option::is_none")]
508 pub metadata: Option<HashMap<String, String>>,
509 #[serde(skip_serializing_if = "Option::is_none")]
510 pub model: Option<String>,
511 #[serde(skip_serializing_if = "Option::is_none")]
512 pub parallel_tool_calls: Option<bool>,
513 #[serde(skip_serializing_if = "Option::is_none")]
514 pub previous_response_id: Option<String>,
515 #[serde(skip_serializing_if = "Option::is_none")]
516 pub prompt: Option<Prompt>,
517 #[serde(skip_serializing_if = "Option::is_none")]
518 pub prompt_cache_key: Option<String>,
519 #[serde(skip_serializing_if = "Option::is_none")]
520 pub prompt_cache_retention: Option<PromptCacheRetention>,
521 #[serde(skip_serializing_if = "Option::is_none")]
522 pub reasoning: Option<Reasoning>,
523 #[serde(skip_serializing_if = "Option::is_none")]
524 pub safety_identifier: Option<String>,
525 #[serde(skip_serializing_if = "Option::is_none")]
526 pub service_tier: Option<ServiceTier>,
527 #[serde(skip_serializing_if = "Option::is_none")]
528 pub store: Option<bool>,
529 #[serde(skip_serializing_if = "Option::is_none")]
530 pub stream: Option<bool>,
531 #[serde(skip_serializing_if = "Option::is_none")]
532 pub stream_options: Option<ResponseStreamOptions>,
533 #[serde(skip_serializing_if = "Option::is_none")]
534 pub temperature: Option<f32>,
535 #[serde(skip_serializing_if = "Option::is_none")]
536 pub text: Option<ResponseTextParam>,
537 #[serde(
538 default,
539 deserialize_with = "deserialize_tool_choice",
540 skip_serializing_if = "Option::is_none"
541 )]
542 pub tool_choice: Option<ToolChoiceParam>,
543 #[serde(skip_serializing_if = "Option::is_none")]
544 pub tools: Option<Vec<Tool>>,
545 #[serde(skip_serializing_if = "Option::is_none")]
546 pub top_logprobs: Option<u8>,
547 #[serde(skip_serializing_if = "Option::is_none")]
548 pub top_p: Option<f32>,
549 #[serde(skip_serializing_if = "Option::is_none")]
550 pub truncation: Option<Truncation>,
551}
552
553pub const RESPONSE_INPUT_TOKENS_OBJECT: &str = "response.input_tokens";
559
560#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)]
579pub struct CountInputTokensRequest {
580 #[serde(default, skip_serializing_if = "Option::is_none")]
581 pub model: Option<String>,
582 #[serde(default, deserialize_with = "deserialize_null_default_input")]
586 pub input: InputParam,
587 #[serde(default, skip_serializing_if = "Option::is_none")]
588 pub instructions: Option<String>,
589 #[serde(
590 default,
591 skip_serializing_if = "Option::is_none",
592 deserialize_with = "deserialize_lenient_tools"
593 )]
594 pub tools: Option<Vec<Tool>>,
595}
596
597fn deserialize_null_default_input<'de, D>(deserializer: D) -> Result<InputParam, D::Error>
598where
599 D: serde::Deserializer<'de>,
600{
601 Ok(Option::<InputParam>::deserialize(deserializer)?.unwrap_or_default())
602}
603
604fn deserialize_lenient_tools<'de, D>(deserializer: D) -> Result<Option<Vec<Tool>>, D::Error>
622where
623 D: serde::Deserializer<'de>,
624{
625 let Some(raw) = Option::<Vec<serde_json::Value>>::deserialize(deserializer)? else {
626 return Ok(None);
627 };
628 Ok(Some(
629 raw.into_iter()
630 .filter_map(|tool| serde_json::from_value::<Tool>(tool).ok())
631 .collect(),
632 ))
633}
634
635#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
641pub struct CountInputTokensResponse {
642 pub object: String,
644 pub input_tokens: u32,
645}
646
647impl CountInputTokensResponse {
648 pub fn new(input_tokens: u32) -> Self {
649 Self {
650 object: RESPONSE_INPUT_TOKENS_OBJECT.to_string(),
651 input_tokens,
652 }
653 }
654}
655
656impl CountInputTokensRequest {
657 pub fn estimate_tokens(&self) -> u32 {
667 let mut total_len: usize = 0;
668
669 if let Some(instructions) = &self.instructions.as_ref().filter(|text| !text.is_empty()) {
679 total_len += role_len(Role::System) + instructions.len();
680 }
681
682 match &self.input {
683 InputParam::Text(text) if text.is_empty() => {}
684 InputParam::Text(text) => total_len += role_len(Role::User) + text.len(),
685 InputParam::Items(items) => total_len += estimate_input_items_len(items),
686 }
687
688 if let Some(tools) = &self.tools {
689 for tool in tools {
690 total_len += estimate_tool_len(tool);
691 }
692 }
693
694 let tokens = total_len / 3;
695 if tokens == 0 && total_len > 0 {
696 1
697 } else {
698 tokens as u32
699 }
700 }
701}
702
703fn role_len(role: Role) -> usize {
706 match role {
707 Role::User => 4,
708 Role::Assistant => 9,
709 Role::System => 6,
710 Role::Developer => 9,
711 }
712}
713
714fn input_role_len(role: InputRole) -> usize {
715 match role {
716 InputRole::User => 4,
717 InputRole::System => 6,
718 InputRole::Developer => 9,
719 }
720}
721
722const TOOL_ROLE_LEN: usize = 4;
728
729enum GroupEffect {
731 Assistant,
734 Flush,
736 Skip,
738}
739
740fn estimate_input_items_len(items: &[InputItem]) -> usize {
753 let mut total = 0;
754 let mut assistant_open = false;
755
756 for item in items {
757 let (effect, len) = measure_input_item(item);
758 total += len;
759 match effect {
760 GroupEffect::Assistant => {
761 if !assistant_open {
762 assistant_open = true;
763 total += role_len(Role::Assistant);
764 }
765 }
766 GroupEffect::Flush => assistant_open = false,
767 GroupEffect::Skip => {}
768 }
769 }
770
771 total
772}
773
774fn measure_input_item(item: &InputItem) -> (GroupEffect, usize) {
779 match item {
780 InputItem::ItemReference(_) => (GroupEffect::Skip, 0),
784 InputItem::EasyMessage(message) => {
785 let content = estimate_easy_content_len(&message.content);
786 match message.role {
787 Role::Assistant => (GroupEffect::Assistant, content),
790 role => (GroupEffect::Flush, role_len(role) + content),
791 }
792 }
793 InputItem::Item(item) => measure_item(item),
794 }
795}
796
797fn estimate_easy_content_len(content: &EasyInputContent) -> usize {
798 match content {
799 EasyInputContent::Text(text) => text.len(),
800 EasyInputContent::ContentList(parts) => parts.iter().map(estimate_input_content_len).sum(),
801 }
802}
803
804fn estimate_input_content_len(part: &InputContent) -> usize {
808 match part {
809 InputContent::InputText(text) => text.text.len(),
810 InputContent::InputImage(_) | InputContent::InputFile(_) => 0,
811 }
812}
813
814fn measure_item(item: &Item) -> (GroupEffect, usize) {
815 match item {
816 Item::Message(MessageItem::Input(message)) => (
817 GroupEffect::Flush,
818 input_role_len(message.role)
819 + message
820 .content
821 .iter()
822 .map(estimate_input_content_len)
823 .sum::<usize>(),
824 ),
825 Item::Message(MessageItem::Output(message)) => (
828 GroupEffect::Assistant,
829 message
830 .content
831 .iter()
832 .map(|part| match part {
833 InputOutputMessageContent::OutputText(text) => text.text.len(),
834 InputOutputMessageContent::Refusal(refusal) => refusal.refusal.len(),
835 })
836 .sum::<usize>(),
837 ),
838 Item::FunctionCall(call) => (
842 GroupEffect::Assistant,
843 call.name.len() + call.arguments.len(),
844 ),
845 Item::FunctionCallOutput(output) => (
848 GroupEffect::Flush,
849 TOOL_ROLE_LEN
850 + match &output.output {
851 FunctionCallOutput::Text(text) => text.len(),
852 FunctionCallOutput::Content(parts) => parts
853 .iter()
854 .map(|part| match part {
855 UpstreamInputContent::InputText(text) => text.text.len(),
856 UpstreamInputContent::InputImage(_)
857 | UpstreamInputContent::InputFile(_) => 0,
858 })
859 .sum(),
860 },
861 ),
862 Item::Reasoning(reasoning) => (
870 GroupEffect::Assistant,
871 reasoning
872 .summary
873 .iter()
874 .map(|part| match part {
875 SummaryPart::SummaryText(text) => text.text.len(),
876 })
877 .sum(),
878 ),
879 Item::FileSearchCall(_)
896 | Item::ComputerCall(_)
897 | Item::ComputerCallOutput(_)
898 | Item::WebSearchCall(_)
899 | Item::ToolSearchCall(_)
900 | Item::ToolSearchOutput(_)
901 | Item::Compaction(_)
902 | Item::ImageGenerationCall(_)
903 | Item::CodeInterpreterCall(_)
904 | Item::LocalShellCall(_)
905 | Item::LocalShellCallOutput(_)
906 | Item::ShellCall(_)
907 | Item::ShellCallOutput(_)
908 | Item::ApplyPatchCall(_)
909 | Item::ApplyPatchCallOutput(_)
910 | Item::McpListTools(_)
911 | Item::McpApprovalRequest(_)
912 | Item::McpApprovalResponse(_)
913 | Item::McpCall(_)
914 | Item::CustomToolCallOutput(_)
915 | Item::CustomToolCall(_) => (GroupEffect::Flush, 0),
916 }
917}
918
919fn estimate_tool_len(tool: &Tool) -> usize {
930 match tool {
931 Tool::Function(function) => function_tool_len(
932 &function.name,
933 function.description.as_ref(),
934 function.parameters.as_ref(),
935 ),
936 Tool::Namespace(namespace) => namespace
937 .tools
938 .iter()
939 .map(|tool| match tool {
940 NamespaceToolParamTool::Function(function) => function_tool_len(
944 &function.name,
945 function.description.as_ref(),
946 function.parameters.as_ref(),
947 ),
948 NamespaceToolParamTool::Custom(_) => 0,
949 })
950 .sum(),
951 _ => 0,
952 }
953}
954
955fn function_tool_len(
956 name: &str,
957 description: Option<&String>,
958 parameters: Option<&serde_json::Value>,
959) -> usize {
960 name.len()
961 + description.map_or(0, |description| description.len())
962 + parameters.map_or(0, |schema| schema.to_string().len())
963}
964
965#[cfg(test)]
966mod tests {
967 use super::*;
968
969 fn tool_choice_of(json: serde_json::Value) -> Option<ToolChoiceParam> {
972 let req: CreateResponse = serde_json::from_value(serde_json::json!({
973 "input": "hi",
974 "tool_choice": json,
975 }))
976 .expect("CreateResponse should deserialize");
977 req.tool_choice
978 }
979
980 #[test]
981 fn tool_choice_mode_object_coerces_to_mode() {
982 assert_eq!(
985 tool_choice_of(serde_json::json!({"type": "auto", "disable_parallel_tool_use": true})),
986 Some(ToolChoiceParam::Mode(ToolChoiceOptions::Auto)),
987 );
988 assert_eq!(
989 tool_choice_of(serde_json::json!({"type": "none"})),
990 Some(ToolChoiceParam::Mode(ToolChoiceOptions::None)),
991 );
992 assert_eq!(
993 tool_choice_of(serde_json::json!({"type": "required"})),
994 Some(ToolChoiceParam::Mode(ToolChoiceOptions::Required)),
995 );
996 }
997
998 #[test]
999 fn tool_choice_bare_string_still_works() {
1000 assert_eq!(
1001 tool_choice_of(serde_json::json!("auto")),
1002 Some(ToolChoiceParam::Mode(ToolChoiceOptions::Auto)),
1003 );
1004 }
1005
1006 #[test]
1007 fn tool_choice_specific_function_object_still_works() {
1008 match tool_choice_of(serde_json::json!({"type": "function", "name": "get_weather"})) {
1011 Some(ToolChoiceParam::Function(f)) => assert_eq!(f.name, "get_weather"),
1012 other => panic!("expected Function tool choice, got {other:?}"),
1013 }
1014 }
1015
1016 #[test]
1017 fn tool_choice_absent_is_none() {
1018 let req: CreateResponse =
1019 serde_json::from_value(serde_json::json!({"input": "hi"})).unwrap();
1020 assert!(req.tool_choice.is_none());
1021 }
1022
1023 #[test]
1026 fn reasoning_input_without_id_deserializes() {
1027 let json = serde_json::json!({
1029 "type": "reasoning",
1030 "summary": [{"type": "summary_text", "text": "thinking"}],
1031 });
1032 match serde_json::from_value::<InputItem>(json).expect("should deserialize") {
1033 InputItem::Item(Item::Reasoning(r)) => {
1034 assert!(r.id.is_none());
1035 assert_eq!(r.summary.len(), 1);
1036 }
1037 other => panic!("expected Item::Reasoning, got {other:?}"),
1038 }
1039 }
1040
1041 #[test]
1042 fn reasoning_input_encrypted_without_id_or_summary_deserializes() {
1043 let json = serde_json::json!({
1044 "type": "reasoning",
1045 "encrypted_content": "AB==",
1046 });
1047 match serde_json::from_value::<InputItem>(json).expect("should deserialize") {
1048 InputItem::Item(Item::Reasoning(r)) => {
1049 assert!(r.id.is_none());
1050 assert!(r.summary.is_empty());
1051 assert_eq!(r.encrypted_content.as_deref(), Some("AB=="));
1052 }
1053 other => panic!("expected Item::Reasoning, got {other:?}"),
1054 }
1055 }
1056
1057 #[test]
1058 fn reasoning_input_with_id_still_works() {
1059 let json = serde_json::json!({
1060 "type": "reasoning",
1061 "id": "rs_1",
1062 "summary": [{"type": "summary_text", "text": "x"}],
1063 "status": "completed",
1064 });
1065 match serde_json::from_value::<InputItem>(json).expect("should deserialize") {
1066 InputItem::Item(Item::Reasoning(r)) => assert_eq!(r.id.as_deref(), Some("rs_1")),
1067 other => panic!("expected Item::Reasoning, got {other:?}"),
1068 }
1069 }
1070
1071 #[test]
1072 fn full_request_with_idless_reasoning_item_deserializes() {
1073 let req: Result<CreateResponse, _> = serde_json::from_value(serde_json::json!({
1076 "model": "m",
1077 "input": [
1078 {"role": "user", "content": "hi"},
1079 {"type": "reasoning", "summary": [{"type": "summary_text", "text": "x"}]},
1080 ],
1081 }));
1082 assert!(
1083 req.is_ok(),
1084 "idless reasoning input should deserialize: {req:?}"
1085 );
1086 }
1087
1088 #[test]
1089 fn codex_agent_message_normalizes_to_user_message() {
1090 let req: CreateResponse = serde_json::from_value(serde_json::json!({
1091 "input": [{
1092 "type": "agent_message",
1093 "author": "/root",
1094 "recipient": "/root/worker",
1095 "content": [
1096 {"type": "input_text", "text": "First."},
1097 {"type": "input_text", "text": "Second."},
1098 ],
1099 }],
1100 }))
1101 .expect("Codex agent message should deserialize");
1102
1103 let InputParam::Items(items) = req.input else {
1104 panic!("expected items");
1105 };
1106 assert!(matches!(
1107 &items[0],
1108 InputItem::EasyMessage(EasyInputMessage {
1109 role: Role::User,
1110 content: EasyInputContent::Text(text),
1111 ..
1112 }) if text == "First.\nSecond."
1113 ));
1114 }
1115
1116 #[test]
1117 fn codex_agent_message_string_content_normalizes_to_user_message() {
1118 let item: InputItem = serde_json::from_value(serde_json::json!({
1119 "type": "agent_message",
1120 "author": "/root",
1121 "recipient": "/root/worker",
1122 "content": "Return exactly OK.",
1123 }))
1124 .expect("Codex agent message with string content should deserialize");
1125
1126 assert!(matches!(
1127 item,
1128 InputItem::EasyMessage(EasyInputMessage {
1129 content: EasyInputContent::Text(text),
1130 ..
1131 }) if text == "Return exactly OK."
1132 ));
1133 }
1134
1135 #[test]
1136 fn codex_agent_message_normalizes_encrypted_content() {
1137 let req: CreateResponse = serde_json::from_value(serde_json::json!({
1138 "input": [{
1139 "type": "agent_message",
1140 "content": [
1141 {"type": "input_text", "text": "Payload:"},
1142 {"type": "encrypted_content", "encrypted_content": "Return exactly OK."},
1143 ],
1144 }],
1145 }))
1146 .expect("Codex agent message with encrypted content should deserialize");
1147
1148 let InputParam::Items(items) = req.input else {
1149 panic!("expected items");
1150 };
1151 assert!(matches!(
1152 &items[0],
1153 InputItem::EasyMessage(EasyInputMessage {
1154 content: EasyInputContent::Text(text),
1155 ..
1156 }) if text == "Payload:\nReturn exactly OK."
1157 ));
1158 }
1159
1160 #[test]
1161 fn codex_agent_message_missing_content_normalizes_empty() {
1162 let item: InputItem = serde_json::from_value(serde_json::json!({
1163 "type": "agent_message",
1164 "author": "/root",
1165 "recipient": "/root/worker",
1166 }))
1167 .expect("Codex agent message without content should deserialize");
1168 assert!(matches!(
1169 item,
1170 InputItem::EasyMessage(EasyInputMessage {
1171 content: EasyInputContent::Text(text),
1172 ..
1173 }) if text.is_empty()
1174 ));
1175 }
1176
1177 #[test]
1178 fn codex_agent_message_null_content_normalizes_empty() {
1179 let item: InputItem = serde_json::from_value(serde_json::json!({
1180 "type": "agent_message",
1181 "author": "/root",
1182 "recipient": "/root/worker",
1183 "content": null,
1184 }))
1185 .expect("Codex agent message with null content should deserialize");
1186 assert!(matches!(
1187 item,
1188 InputItem::EasyMessage(EasyInputMessage {
1189 content: EasyInputContent::Text(text),
1190 ..
1191 }) if text.is_empty()
1192 ));
1193 }
1194
1195 #[test]
1196 fn relaxed_assistant_message_without_id_or_status() {
1197 let json = serde_json::json!({
1198 "type": "message",
1199 "role": "assistant",
1200 "content": [{"type": "output_text", "text": "hi"}]
1201 });
1202 let item: InputItem = serde_json::from_value(json).unwrap();
1203 match item {
1204 InputItem::Item(Item::Message(MessageItem::Output(out))) => {
1205 assert_eq!(out.role, AssistantRole::Assistant);
1206 assert!(out.id.is_none());
1207 assert!(out.status.is_none());
1208 }
1209 other => panic!("expected Item::Message(Output), got {other:?}"),
1210 }
1211 }
1212
1213 #[test]
1214 fn input_image_without_detail_defaults_to_auto() {
1215 let json = serde_json::json!({
1216 "type": "input_image",
1217 "image_url": "https://example.com/cat.jpg"
1218 });
1219 let content: InputContent = serde_json::from_value(json).unwrap();
1220 match content {
1221 InputContent::InputImage(img) => assert_eq!(img.detail, ImageDetail::Auto),
1222 other => panic!("expected InputImage, got {other:?}"),
1223 }
1224 }
1225
1226 #[test]
1227 fn input_image_with_explicit_null_detail_defaults_to_auto() {
1228 let json = serde_json::json!({
1229 "type": "input_image",
1230 "image_url": "https://example.com/cat.jpg",
1231 "detail": null
1232 });
1233 let content: InputContent = serde_json::from_value(json).unwrap();
1234 match content {
1235 InputContent::InputImage(img) => assert_eq!(img.detail, ImageDetail::Auto),
1236 other => panic!("expected InputImage, got {other:?}"),
1237 }
1238 }
1239
1240 #[test]
1241 fn assistant_message_without_content_field_deserializes() {
1242 let json = serde_json::json!({
1246 "type": "message",
1247 "role": "assistant"
1248 });
1249 let item: InputItem = serde_json::from_value(json).unwrap();
1250 match item {
1251 InputItem::Item(Item::Message(MessageItem::Output(out))) => {
1252 assert_eq!(out.role, AssistantRole::Assistant);
1253 assert!(out.content.is_empty());
1254 assert!(out.id.is_none());
1255 assert!(out.status.is_none());
1256 }
1257 other => panic!("expected Item::Message(Output), got {other:?}"),
1258 }
1259 }
1260
1261 #[test]
1262 fn assistant_message_with_explicit_null_content_deserializes() {
1263 let json = serde_json::json!({
1267 "type": "message",
1268 "role": "assistant",
1269 "content": null
1270 });
1271 let item: InputItem = serde_json::from_value(json).unwrap();
1272 match item {
1273 InputItem::Item(Item::Message(MessageItem::Output(out))) => {
1274 assert!(out.content.is_empty());
1275 }
1276 other => panic!("expected Item::Message(Output), got {other:?}"),
1277 }
1278 }
1279
1280 #[test]
1281 fn mcp_call_item_deserializes() {
1282 let json = serde_json::json!({
1285 "type": "mcp_call",
1286 "id": "mcp_1",
1287 "server_label": "srv",
1288 "name": "t",
1289 "arguments": "{}"
1290 });
1291 let item: InputItem = serde_json::from_value(json).unwrap();
1292 assert!(matches!(item, InputItem::Item(Item::McpCall(_))));
1293 }
1294
1295 #[test]
1296 fn strict_assistant_message_still_deserializes() {
1297 let json = serde_json::json!({
1298 "type": "message",
1299 "role": "assistant",
1300 "id": "msg_1",
1301 "status": "completed",
1302 "content": [{"type": "output_text", "text": "hi", "annotations": []}]
1303 });
1304 let item: InputItem = serde_json::from_value(json).unwrap();
1305 match item {
1306 InputItem::Item(Item::Message(MessageItem::Output(out))) => {
1307 assert_eq!(out.id.as_deref(), Some("msg_1"));
1308 assert_eq!(out.status, Some(OutputStatus::Completed));
1309 }
1310 other => panic!("expected Item::Message(Output), got {other:?}"),
1311 }
1312 }
1313
1314 #[test]
1315 fn user_message_routes_to_input_variant() {
1316 let json = serde_json::json!({
1317 "type": "message",
1318 "role": "user",
1319 "content": [{"type": "input_text", "text": "hi"}]
1320 });
1321 let item: InputItem = serde_json::from_value(json).unwrap();
1322 assert!(matches!(
1323 item,
1324 InputItem::Item(Item::Message(MessageItem::Input(_)))
1325 ));
1326 }
1327
1328 #[test]
1329 fn function_call_item_still_deserializes() {
1330 let json = serde_json::json!({
1331 "type": "function_call",
1332 "call_id": "c",
1333 "name": "f",
1334 "arguments": "{}"
1335 });
1336 let item: InputItem = serde_json::from_value(json).unwrap();
1337 assert!(matches!(item, InputItem::Item(Item::FunctionCall(_))));
1338 }
1339
1340 #[test]
1341 fn easy_message_string_content_routes_to_easymessage() {
1342 let json = serde_json::json!({"role": "assistant", "content": "x"});
1343 let item: InputItem = serde_json::from_value(json).unwrap();
1344 assert!(matches!(item, InputItem::EasyMessage(_)));
1345 }
1346
1347 #[test]
1348 fn output_text_without_annotations_defaults_empty() {
1349 let json = serde_json::json!({"type": "output_text", "text": "hi"});
1350 let part: InputOutputMessageContent = serde_json::from_value(json).unwrap();
1351 match part {
1352 InputOutputMessageContent::OutputText(t) => {
1353 assert!(t.annotations.is_empty());
1354 }
1355 _ => panic!("expected OutputText"),
1356 }
1357 }
1358
1359 #[test]
1360 fn output_text_with_explicit_null_annotations_deserializes_as_empty() {
1361 let json = serde_json::json!({"type": "output_text", "text": "hi", "annotations": null});
1365 let part: InputOutputMessageContent = serde_json::from_value(json).unwrap();
1366 match part {
1367 InputOutputMessageContent::OutputText(t) => {
1368 assert!(t.annotations.is_empty());
1369 }
1370 _ => panic!("expected OutputText"),
1371 }
1372 }
1373
1374 #[test]
1375 fn assistant_message_with_explicit_null_id_and_status_deserializes() {
1376 let json = serde_json::json!({
1381 "type": "message",
1382 "role": "assistant",
1383 "id": null,
1384 "status": null,
1385 "content": [{"type": "output_text", "text": "hi", "annotations": null}]
1386 });
1387 let item: InputItem = serde_json::from_value(json).unwrap();
1388 match item {
1389 InputItem::Item(Item::Message(MessageItem::Output(out))) => {
1390 assert!(out.id.is_none());
1391 assert!(out.status.is_none());
1392 assert_eq!(out.content.len(), 1);
1393 }
1394 other => panic!("expected Item::Message(Output), got {other:?}"),
1395 }
1396 }
1397
1398 #[test]
1399 fn create_response_roundtrip_with_relaxed_input() {
1400 let body = serde_json::json!({
1401 "model": "m",
1402 "input": [
1403 {"type": "message", "role": "user", "content": [
1404 {"type": "input_text", "text": "hi"}
1405 ]},
1406 {"type": "function_call", "call_id": "c", "name": "f", "arguments": "{}"},
1407 {"type": "message", "role": "assistant", "content": [
1408 {"type": "output_text", "text": "\n\n"}
1409 ]},
1410 {"type": "function_call_output", "call_id": "c", "output": "x"}
1411 ]
1412 });
1413
1414 let req: CreateResponse = serde_json::from_value(body).unwrap();
1415 let items = match &req.input {
1416 InputParam::Items(items) => items,
1417 _ => panic!("expected Items"),
1418 };
1419 assert_eq!(items.len(), 4);
1420 assert!(matches!(
1421 items[2],
1422 InputItem::Item(Item::Message(MessageItem::Output(_)))
1423 ));
1424 }
1425
1426 #[test]
1434 fn easy_message_multimodal_without_type_routes_to_easymessage() {
1435 let json = serde_json::json!({
1438 "role": "user",
1439 "content": [
1440 {"type": "input_image", "image_url": "data:image/png;base64,abc"}
1441 ]
1442 });
1443 let item: InputItem = serde_json::from_value(json).unwrap();
1444 match item {
1445 InputItem::EasyMessage(easy) => {
1446 assert_eq!(easy.role, Role::User);
1447 assert_eq!(easy.r#type, MessageType::Message);
1448 match easy.content {
1449 EasyInputContent::ContentList(parts) => {
1450 assert_eq!(parts.len(), 1);
1451 match &parts[0] {
1452 InputContent::InputImage(img) => {
1453 assert_eq!(img.detail, ImageDetail::Auto);
1454 assert_eq!(
1455 img.image_url.as_deref(),
1456 Some("data:image/png;base64,abc")
1457 );
1458 }
1459 other => panic!("expected InputImage, got {other:?}"),
1460 }
1461 }
1462 other => panic!("expected ContentList, got {other:?}"),
1463 }
1464 }
1465 other => panic!("expected EasyMessage, got {other:?}"),
1466 }
1467 }
1468
1469 #[test]
1470 fn easy_message_multimodal_with_explicit_null_detail() {
1471 let json = serde_json::json!({
1475 "role": "user",
1476 "content": [
1477 {"type": "input_image", "image_url": "data:image/png;base64,abc", "detail": null}
1478 ]
1479 });
1480 let item: InputItem = serde_json::from_value(json).unwrap();
1481 assert!(matches!(item, InputItem::EasyMessage(_)));
1482 }
1483
1484 #[test]
1485 fn easy_message_assistant_multimodal_without_type() {
1486 let json = serde_json::json!({
1490 "role": "assistant",
1491 "content": [
1492 {"type": "input_text", "text": "ok"}
1493 ]
1494 });
1495 let item: InputItem = serde_json::from_value(json).unwrap();
1496 match item {
1497 InputItem::EasyMessage(easy) => {
1498 assert_eq!(easy.role, Role::Assistant);
1499 }
1500 other => panic!("expected EasyMessage(assistant), got {other:?}"),
1501 }
1502 }
1503
1504 #[test]
1505 fn easy_message_text_only_without_type_unchanged() {
1506 let json = serde_json::json!({"role": "user", "content": "Hello"});
1511 let item: InputItem = serde_json::from_value(json).unwrap();
1512 match item {
1513 InputItem::EasyMessage(easy) => {
1514 assert_eq!(easy.role, Role::User);
1515 assert!(matches!(easy.content, EasyInputContent::Text(ref s) if s == "Hello"));
1516 }
1517 other => panic!("expected EasyMessage(Text), got {other:?}"),
1518 }
1519 }
1520
1521 #[test]
1522 fn easy_message_with_explicit_type_still_routes_to_item_message() {
1523 let json = serde_json::json!({
1527 "type": "message",
1528 "role": "user",
1529 "content": [
1530 {"type": "input_image", "image_url": "data:image/png;base64,abc"}
1531 ]
1532 });
1533 let item: InputItem = serde_json::from_value(json).unwrap();
1534 match item {
1535 InputItem::Item(Item::Message(MessageItem::Input(msg))) => {
1536 assert_eq!(msg.role, InputRole::User);
1537 assert_eq!(msg.content.len(), 1);
1538 }
1539 other => panic!("expected Item::Message(Input), got {other:?}"),
1540 }
1541 }
1542
1543 #[test]
1544 fn create_response_roundtrip_aiperf_pre_pr931_payload() {
1545 let body = serde_json::json!({
1550 "model": "Qwen/Qwen2-VL-2B-Instruct",
1551 "input": [
1552 {
1553 "role": "user",
1554 "content": [
1555 {"type": "input_text", "text": "Describe"},
1556 {"type": "input_image", "image_url": "data:image/png;base64,abc"}
1557 ]
1558 },
1559 {
1560 "role": "assistant",
1561 "content": [{"type": "input_text", "text": "ok"}]
1562 },
1563 {
1564 "role": "user",
1565 "content": [{"type": "input_text", "text": "Now describe a different one."}]
1566 }
1567 ]
1568 });
1569 let req: CreateResponse = serde_json::from_value(body).unwrap();
1570 let items = match &req.input {
1571 InputParam::Items(items) => items,
1572 _ => panic!("expected Items"),
1573 };
1574 assert_eq!(items.len(), 3);
1575 for (idx, item) in items.iter().enumerate() {
1577 assert!(
1578 matches!(item, InputItem::EasyMessage(_)),
1579 "turn {idx} did not route to EasyMessage: {item:?}",
1580 );
1581 }
1582 }
1583
1584 fn count(body: serde_json::Value) -> u32 {
1587 serde_json::from_value::<CountInputTokensRequest>(body)
1588 .expect("count request should deserialize")
1589 .estimate_tokens()
1590 }
1591
1592 #[test]
1593 fn count_tokens_plain_text_input() {
1594 assert_eq!(
1596 count(serde_json::json!({"model": "m", "input": "Hello, world!"})),
1597 5
1598 );
1599 }
1600
1601 #[test]
1602 fn count_tokens_input_is_optional() {
1603 assert_eq!(count(serde_json::json!({"model": "m"})), 0);
1605 }
1606
1607 #[test]
1608 fn count_tokens_empty_input_is_zero() {
1609 assert_eq!(count(serde_json::json!({"input": ""})), 0);
1610 }
1611
1612 #[test]
1613 fn count_tokens_short_input_never_rounds_to_zero() {
1614 assert_eq!(
1621 count(serde_json::json!({"tools": [{"type": "function", "name": "a"}]})),
1622 1
1623 );
1624 assert_eq!(count(serde_json::json!({"input": "Hi"})), 2);
1627 }
1628
1629 #[test]
1630 fn count_tokens_instructions_contribute() {
1631 assert_eq!(
1634 count(serde_json::json!({"input": "Hi", "instructions": "You are helpful."})),
1635 9
1636 );
1637 }
1638
1639 #[test]
1640 fn count_tokens_scores_the_two_spellings_of_a_prompt_identically() {
1641 assert_eq!(
1647 count(serde_json::json!({"input": "Hello"})),
1648 count(serde_json::json!({"input": [{"role": "user", "content": "Hello"}]})),
1649 );
1650 assert_eq!(
1651 count(serde_json::json!({
1652 "input": "Hello",
1653 "instructions": "You are helpful."
1654 })),
1655 count(serde_json::json!({"input": [
1656 {"role": "system", "content": "You are helpful."},
1657 {"role": "user", "content": "Hello"}
1658 ]})),
1659 );
1660 }
1661
1662 #[test]
1663 fn count_tokens_easy_message_counts_role_and_content() {
1664 assert_eq!(
1666 count(serde_json::json!({"input": [{"role": "user", "content": "Hello"}]})),
1667 3
1668 );
1669 }
1670
1671 #[test]
1672 fn count_tokens_structured_input_message() {
1673 assert_eq!(
1675 count(serde_json::json!({"input": [{
1676 "type": "message",
1677 "role": "user",
1678 "content": [{"type": "input_text", "text": "Hello"}],
1679 }]})),
1680 3
1681 );
1682 }
1683
1684 #[test]
1685 fn count_tokens_function_call_counts_name_and_arguments() {
1686 assert_eq!(
1690 count(serde_json::json!({"input": [{
1691 "type": "function_call",
1692 "call_id": "call_1",
1693 "name": "get_weather",
1694 "arguments": r#"{"city":"SF"}"#,
1695 }]})),
1696 11
1697 );
1698 }
1699
1700 #[test]
1701 fn count_tokens_charges_one_assistant_marker_per_coalesced_turn() {
1702 let one = serde_json::json!({"input": [
1707 {"type": "function_call", "call_id": "c1", "name": "aa", "arguments": ""}
1708 ]});
1709 let two = serde_json::json!({"input": [
1710 {"type": "function_call", "call_id": "c1", "name": "aa", "arguments": ""},
1711 {"type": "function_call", "call_id": "c2", "name": "bb", "arguments": ""}
1712 ]});
1713 assert_eq!(count(one), 3);
1716 assert_eq!(count(two), 4);
1717
1718 let mixed = serde_json::json!({"input": [
1721 {"role": "assistant", "content": "aa"},
1722 {"type": "reasoning", "summary": [{"type": "summary_text", "text": "bb"}]},
1723 {"type": "function_call", "call_id": "c1", "name": "cc", "arguments": ""}
1724 ]});
1725 assert_eq!(count(mixed), 5); }
1727
1728 #[test]
1729 fn count_tokens_reopens_the_assistant_turn_after_a_flush() {
1730 let two_turns = serde_json::json!({"input": [
1733 {"type": "function_call", "call_id": "c1", "name": "aa", "arguments": ""},
1734 {"type": "function_call_output", "call_id": "c1", "output": ""},
1735 {"type": "function_call", "call_id": "c2", "name": "bb", "arguments": ""}
1736 ]});
1737 assert_eq!(count(two_turns), 8);
1740 }
1741
1742 #[test]
1743 fn count_tokens_item_reference_does_not_split_an_assistant_turn() {
1744 let split = serde_json::json!({"input": [
1747 {"type": "function_call", "call_id": "c1", "name": "aa", "arguments": ""},
1748 {"type": "item_reference", "id": "item_abc"},
1749 {"type": "function_call", "call_id": "c2", "name": "bb", "arguments": ""}
1750 ]});
1751 let unsplit = serde_json::json!({"input": [
1752 {"type": "function_call", "call_id": "c1", "name": "aa", "arguments": ""},
1753 {"type": "function_call", "call_id": "c2", "name": "bb", "arguments": ""}
1754 ]});
1755 assert_eq!(count(split), count(unsplit));
1756 }
1757
1758 #[test]
1759 fn count_tokens_unsupported_item_splits_an_assistant_turn() {
1760 let across = serde_json::json!({"input": [
1764 {"type": "function_call", "call_id": "c1", "name": "aa", "arguments": ""},
1765 {"type": "web_search_call", "id": "ws_1", "status": "completed"},
1766 {"type": "function_call", "call_id": "c2", "name": "bb", "arguments": ""}
1767 ]});
1768 assert_eq!(count(across), 7);
1770 }
1771
1772 #[test]
1773 fn count_tokens_function_call_output_counts_text() {
1774 assert_eq!(
1777 count(serde_json::json!({"input": [{
1778 "type": "function_call_output",
1779 "call_id": "call_1",
1780 "output": "sunny",
1781 }]})),
1782 3
1783 );
1784 }
1785
1786 #[test]
1787 fn count_tokens_tools_contribute() {
1788 assert_eq!(
1791 count(serde_json::json!({
1792 "input": "",
1793 "tools": [{
1794 "type": "function",
1795 "name": "get_weather",
1796 "description": "Get weather",
1797 "parameters": {"type": "object"},
1798 }],
1799 })),
1800 13
1801 );
1802 }
1803
1804 #[test]
1805 fn count_tokens_images_contribute_nothing() {
1806 let with_image = count(serde_json::json!({"input": [{
1809 "type": "message",
1810 "role": "user",
1811 "content": [
1812 {"type": "input_text", "text": "Describe this"},
1813 {"type": "input_image", "image_url": "https://example.com/a-very-long-url.png"},
1814 ],
1815 }]}));
1816 let without_image = count(serde_json::json!({"input": [{
1817 "type": "message",
1818 "role": "user",
1819 "content": [{"type": "input_text", "text": "Describe this"}],
1820 }]}));
1821 assert_eq!(with_image, without_image);
1822 }
1823
1824 #[test]
1825 fn count_tokens_dropped_item_variants_cost_nothing() {
1826 for item in [
1830 serde_json::json!({"type": "web_search_call", "id": "ws_1", "status": "completed"}),
1831 serde_json::json!({
1832 "type": "computer_call",
1833 "call_id": "c_1",
1834 "id": "cu_1",
1835 "action": {"type": "screenshot"},
1836 "pending_safety_checks": [],
1837 "status": "completed",
1838 }),
1839 ] {
1840 assert_eq!(
1841 count(serde_json::json!({ "input": [item.clone()] })),
1842 0,
1843 "dropped item variant should not be counted: {item}"
1844 );
1845 }
1846 }
1847
1848 #[test]
1849 fn count_tokens_counts_exactly_the_variants_the_converter_renders() {
1850 for item in [
1856 serde_json::json!({"role": "user", "content": "Hello"}),
1857 serde_json::json!({
1858 "type": "message",
1859 "role": "user",
1860 "content": [{"type": "input_text", "text": "Hello"}],
1861 }),
1862 serde_json::json!({
1863 "type": "message",
1864 "role": "assistant",
1865 "content": [{"type": "output_text", "text": "Hi", "annotations": []}],
1866 }),
1867 serde_json::json!({
1868 "type": "function_call",
1869 "call_id": "c1",
1870 "name": "get_weather",
1871 "arguments": "{}",
1872 }),
1873 serde_json::json!({"type": "function_call_output", "call_id": "c1", "output": "sunny"}),
1874 serde_json::json!({
1875 "type": "reasoning",
1876 "summary": [{"type": "summary_text", "text": "thinking"}],
1877 }),
1878 ] {
1879 assert!(
1880 count(serde_json::json!({ "input": [item.clone()] })) > 0,
1881 "rendered variant should be counted: {item}"
1882 );
1883 }
1884 }
1885
1886 #[test]
1887 fn count_tokens_reasoning_counts_summary_only() {
1888 let summary_only = serde_json::json!({"input": [{
1891 "type": "reasoning",
1892 "summary": [{"type": "summary_text", "text": "thinking"}],
1893 }]});
1894 let with_dropped_fields = serde_json::json!({"input": [{
1895 "type": "reasoning",
1896 "summary": [{"type": "summary_text", "text": "thinking"}],
1897 "content": [{"type": "reasoning_text", "text": "a much longer private chain of thought"}],
1898 "encrypted_content": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
1899 }]});
1900
1901 assert_eq!(count(summary_only.clone()), 5);
1904 assert_eq!(count(with_dropped_fields), count(summary_only));
1905 }
1906
1907 #[test]
1908 fn count_tokens_hosted_tools_cost_nothing() {
1909 assert_eq!(
1912 count(serde_json::json!({
1913 "input": "",
1914 "tools": [{"type": "web_search"}],
1915 })),
1916 0
1917 );
1918 }
1919
1920 #[test]
1921 fn count_tokens_namespaced_tools_count_their_functions() {
1922 assert_eq!(
1927 count(serde_json::json!({
1928 "input": "",
1929 "tools": [{
1930 "type": "namespace",
1931 "name": "weather_ns",
1932 "description": "Weather tools",
1933 "tools": [{
1934 "type": "function",
1935 "name": "get_weather",
1936 "description": "Get weather",
1937 "parameters": {"type": "object"},
1938 }],
1939 }],
1940 })),
1941 13
1942 );
1943 }
1944
1945 #[test]
1946 fn count_tokens_item_reference_contributes_nothing() {
1947 assert_eq!(
1949 count(serde_json::json!({"input": [{"type": "item_reference", "id": "msg_1"}]})),
1950 0
1951 );
1952 }
1953
1954 #[test]
1955 fn count_tokens_ignores_unsupported_stateful_fields() {
1956 assert_eq!(
1959 count(serde_json::json!({
1960 "model": "m",
1961 "input": "Hello, world!",
1962 "previous_response_id": "resp_abc123",
1963 "conversation": {"id": "conv_1"},
1964 })),
1965 5
1966 );
1967 }
1968
1969 #[test]
1970 fn count_tokens_deserializes_the_litellm_request_shape() {
1971 let request: CountInputTokensRequest = serde_json::from_value(serde_json::json!({
1975 "model": "dynamo/deepseek-ai/deepseek-v4-pro-sglang",
1976 "input": [{"role": "user", "content": "Hello"}],
1977 "instructions": "You are helpful.",
1978 "tools": [{
1979 "type": "function",
1980 "name": "get_weather",
1981 "description": "Get weather",
1982 "parameters": {"type": "object"},
1983 }],
1984 }))
1985 .expect("LiteLLM request shape should deserialize");
1986
1987 assert_eq!(
1988 request.model.as_deref(),
1989 Some("dynamo/deepseek-ai/deepseek-v4-pro-sglang")
1990 );
1991 assert!(matches!(request.input, InputParam::Items(ref items) if items.len() == 1));
1992 assert!(request.estimate_tokens() > 0);
1993 }
1994
1995 #[test]
1996 fn count_tokens_accepts_explicit_null_input() {
1997 assert_eq!(count(serde_json::json!({"model": "m", "input": null})), 0);
2000 assert_eq!(
2001 count(serde_json::json!({
2002 "model": "m",
2003 "input": null,
2004 "instructions": "You are helpful."
2005 })),
2006 7
2007 );
2008 }
2009
2010 #[test]
2011 fn count_tokens_drops_unparseable_tools_instead_of_failing() {
2012 let request: CountInputTokensRequest = serde_json::from_value(serde_json::json!({
2016 "model": "m",
2017 "input": "Hello, world!",
2018 "tools": [{"type": "custom", "custom": {"name": "x"}}],
2019 }))
2020 .expect("an unparseable tool should be dropped, not rejected");
2021 assert_eq!(request.tools.as_deref(), Some(&[][..]));
2022 assert_eq!(request.estimate_tokens(), 5);
2025 }
2026
2027 #[test]
2028 fn count_tokens_keeps_parseable_tools_alongside_dropped_ones() {
2029 let request: CountInputTokensRequest = serde_json::from_value(serde_json::json!({
2032 "model": "m",
2033 "input": "Hello, world!",
2034 "tools": [
2035 {"type": "custom", "custom": {"name": "x"}},
2036 {"type": "function", "name": "get_weather", "description": "Get weather"},
2037 ],
2038 }))
2039 .expect("a mixed tool array should deserialize");
2040 assert_eq!(request.tools.as_ref().map(Vec::len), Some(1));
2041 assert!(
2042 request.estimate_tokens()
2043 > count(serde_json::json!({"model": "m", "input": "Hello, world!"}))
2044 );
2045 }
2046
2047 #[test]
2048 fn count_tokens_distinguishes_absent_tools_from_empty_tools() {
2049 let absent: CountInputTokensRequest =
2052 serde_json::from_value(serde_json::json!({"input": "hi"})).unwrap();
2053 assert_eq!(absent.tools, None);
2054 let empty: CountInputTokensRequest =
2055 serde_json::from_value(serde_json::json!({"input": "hi", "tools": []})).unwrap();
2056 assert_eq!(empty.tools.as_deref(), Some(&[][..]));
2057 let null: CountInputTokensRequest =
2058 serde_json::from_value(serde_json::json!({"input": "hi", "tools": null})).unwrap();
2059 assert_eq!(null.tools, None);
2060 }
2061
2062 #[test]
2063 fn count_tokens_response_serializes_to_the_openai_shape() {
2064 assert_eq!(
2065 serde_json::to_value(CountInputTokensResponse::new(42)).unwrap(),
2066 serde_json::json!({"object": "response.input_tokens", "input_tokens": 42})
2067 );
2068 }
2069}