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