1use std::collections::HashMap;
2
3use serde::{
4 de::{self, value::SeqAccessDeserializer, SeqAccess, Visitor},
5 Deserialize, Deserializer, Serialize,
6};
7use serde_json::{Map, Value};
8use validator;
9
10pub fn default_unknown_model() -> String {
17 super::UNKNOWN_MODEL_ID.to_string()
18}
19
20pub fn default_true() -> bool {
22 true
23}
24
25#[expect(
27 clippy::trivially_copy_pass_by_ref,
28 reason = "serde skip_serializing_if passes &T"
29)]
30pub fn is_false(v: &bool) -> bool {
31 !*v
32}
33
34#[expect(
36 clippy::trivially_copy_pass_by_ref,
37 reason = "serde skip_serializing_if passes &T"
38)]
39pub fn is_true(v: &bool) -> bool {
40 *v
41}
42
43pub fn deserialize_null_as_false<'de, D>(deserializer: D) -> Result<bool, D::Error>
48where
49 D: Deserializer<'de>,
50{
51 Option::<bool>::deserialize(deserializer).map(|opt| opt.unwrap_or(false))
52}
53
54pub trait GenerationRequest: Send + Sync {
62 fn is_stream(&self) -> bool;
64
65 fn get_model(&self) -> Option<&str>;
67
68 fn extract_text_for_routing(&self) -> String;
70
71 fn routing_tokens(&self) -> Option<&[i32]> {
75 None
76 }
77
78 fn rid(&self) -> Option<&str> {
81 None
82 }
83}
84
85#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, schemars::JsonSchema)]
91#[serde(untagged)]
92pub enum StringOrArray {
93 String(String),
94 Array(Vec<String>),
95}
96
97impl StringOrArray {
98 pub fn len(&self) -> usize {
100 match self {
101 StringOrArray::String(_) => 1,
102 StringOrArray::Array(arr) => arr.len(),
103 }
104 }
105
106 pub fn is_empty(&self) -> bool {
108 match self {
109 StringOrArray::String(s) => s.is_empty(),
110 StringOrArray::Array(arr) => arr.is_empty(),
111 }
112 }
113
114 pub fn to_vec(&self) -> Vec<String> {
116 match self {
117 StringOrArray::String(s) => vec![s.clone()],
118 StringOrArray::Array(arr) => arr.clone(),
119 }
120 }
121
122 pub fn iter(&self) -> StringOrArrayIter<'_> {
125 StringOrArrayIter {
126 inner: self,
127 index: 0,
128 }
129 }
130
131 pub fn first(&self) -> Option<&str> {
133 match self {
134 StringOrArray::String(s) => {
135 if s.is_empty() {
136 None
137 } else {
138 Some(s)
139 }
140 }
141 StringOrArray::Array(arr) => arr.first().map(|s| s.as_str()),
142 }
143 }
144}
145
146pub struct StringOrArrayIter<'a> {
148 inner: &'a StringOrArray,
149 index: usize,
150}
151
152impl<'a> Iterator for StringOrArrayIter<'a> {
153 type Item = &'a str;
154
155 fn next(&mut self) -> Option<Self::Item> {
156 match self.inner {
157 StringOrArray::String(s) => {
158 if self.index == 0 {
159 self.index = 1;
160 Some(s.as_str())
161 } else {
162 None
163 }
164 }
165 StringOrArray::Array(arr) => {
166 if self.index < arr.len() {
167 let item = &arr[self.index];
168 self.index += 1;
169 Some(item.as_str())
170 } else {
171 None
172 }
173 }
174 }
175 }
176
177 fn size_hint(&self) -> (usize, Option<usize>) {
178 let remaining = match self.inner {
179 StringOrArray::String(_) => 1 - self.index,
180 StringOrArray::Array(arr) => arr.len() - self.index,
181 };
182 (remaining, Some(remaining))
183 }
184}
185
186impl<'a> ExactSizeIterator for StringOrArrayIter<'a> {}
187
188pub fn validate_stop(stop: &StringOrArray) -> Result<(), validator::ValidationError> {
191 match stop {
192 StringOrArray::String(s) => {
193 if s.is_empty() {
194 return Err(validator::ValidationError::new(
195 "stop sequences cannot be empty",
196 ));
197 }
198 }
199 StringOrArray::Array(arr) => {
200 if arr.len() > 4 {
201 return Err(validator::ValidationError::new(
202 "maximum 4 stop sequences allowed",
203 ));
204 }
205 for s in arr {
206 if s.is_empty() {
207 return Err(validator::ValidationError::new(
208 "stop sequences cannot be empty",
209 ));
210 }
211 }
212 }
213 }
214 Ok(())
215}
216
217#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, schemars::JsonSchema)]
222#[serde(tag = "type")]
223pub enum ContentPart {
224 #[serde(rename = "text")]
225 Text { text: String },
226 #[serde(rename = "image_url")]
227 ImageUrl { image_url: ImageUrl },
228 #[serde(rename = "audio_url")]
229 AudioUrl { audio_url: AudioUrl },
230 #[serde(rename = "input_audio")]
231 InputAudio { input_audio: InputAudio },
232 #[serde(rename = "video_url")]
233 VideoUrl { video_url: VideoUrl },
234}
235
236#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, schemars::JsonSchema)]
237pub struct ImageUrl {
238 pub url: String,
239 #[serde(skip_serializing_if = "Option::is_none")]
240 pub detail: Option<String>, }
242
243#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, schemars::JsonSchema)]
244pub struct AudioUrl {
245 pub url: String,
246}
247
248#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, schemars::JsonSchema)]
249pub struct InputAudio {
250 pub data: String,
252 pub format: String,
254}
255
256#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, schemars::JsonSchema)]
257pub struct VideoUrl {
258 pub url: String,
259}
260
261#[derive(Debug, Clone, Deserialize, Serialize, schemars::JsonSchema)]
266#[serde(tag = "type")]
267pub enum ResponseFormat {
268 #[serde(rename = "text")]
269 Text,
270 #[serde(rename = "json_object")]
271 JsonObject,
272 #[serde(rename = "json_schema")]
273 JsonSchema { json_schema: JsonSchemaFormat },
274}
275
276#[derive(Debug, Clone, Deserialize, Serialize, schemars::JsonSchema)]
277pub struct JsonSchemaFormat {
278 pub name: String,
279 pub schema: Value,
280 #[serde(skip_serializing_if = "Option::is_none")]
281 pub strict: Option<bool>,
282}
283
284#[derive(Debug, Clone, Default, Deserialize, Serialize, schemars::JsonSchema)]
289pub struct StreamOptions {
290 #[serde(skip_serializing_if = "Option::is_none")]
292 pub include_usage: Option<bool>,
293
294 #[serde(skip_serializing_if = "Option::is_none")]
297 pub continuous_usage_stats: Option<bool>,
298
299 #[serde(skip_serializing_if = "Option::is_none")]
302 pub include_obfuscation: Option<bool>,
303
304 #[serde(flatten)]
308 pub other: Map<String, Value>,
309}
310
311#[serde_with::skip_serializing_none]
312#[derive(Debug, Clone, Deserialize, Serialize, schemars::JsonSchema)]
313pub struct ToolCallDelta {
314 pub index: u32,
315 pub id: Option<String>,
316 #[serde(rename = "type")]
317 pub tool_type: Option<String>,
318 pub function: Option<FunctionCallDelta>,
319}
320
321#[serde_with::skip_serializing_none]
322#[derive(Debug, Clone, Deserialize, Serialize, schemars::JsonSchema)]
323pub struct FunctionCallDelta {
324 pub name: Option<String>,
325 pub arguments: Option<String>,
326}
327
328#[derive(Debug, Clone, Deserialize, Serialize, schemars::JsonSchema)]
334#[serde(rename_all = "snake_case")]
335pub enum ToolChoiceValue {
336 Auto,
337 Required,
338 None,
339}
340
341#[derive(Debug, Clone, Deserialize, Serialize, schemars::JsonSchema)]
343#[serde(untagged)]
344pub enum ToolChoice {
345 Value(ToolChoiceValue),
346 Function {
347 #[serde(rename = "type")]
348 tool_type: String, function: FunctionChoice,
350 },
351 AllowedTools {
352 #[serde(rename = "type")]
353 tool_type: String, mode: String, tools: Vec<ToolReference>,
356 },
357}
358
359impl Default for ToolChoice {
360 fn default() -> Self {
361 Self::Value(ToolChoiceValue::Auto)
362 }
363}
364
365impl ToolChoice {
366 pub fn serialize_to_string(tool_choice: Option<&ToolChoice>) -> String {
370 tool_choice
371 .map(|tc| serde_json::to_string(tc).unwrap_or_else(|_| "auto".to_string()))
372 .unwrap_or_else(|| "auto".to_string())
373 }
374}
375
376#[derive(Debug, Clone, Deserialize, Serialize, schemars::JsonSchema)]
378pub struct FunctionChoice {
379 pub name: String,
380}
381
382#[derive(Debug, Clone, Deserialize, Serialize, schemars::JsonSchema)]
387#[serde(tag = "type")]
388#[serde(rename_all = "snake_case")]
389pub enum ToolReference {
390 #[serde(rename = "function")]
392 Function { name: String },
393
394 #[serde(rename = "mcp")]
396 Mcp {
397 server_label: String,
398 #[serde(skip_serializing_if = "Option::is_none")]
399 name: Option<String>,
400 },
401
402 #[serde(rename = "file_search")]
404 FileSearch,
405
406 #[serde(rename = "web_search_preview")]
408 WebSearchPreview,
409
410 #[serde(rename = "computer_use_preview")]
412 ComputerUsePreview,
413
414 #[serde(rename = "code_interpreter")]
416 CodeInterpreter,
417
418 #[serde(rename = "image_generation")]
420 ImageGeneration,
421}
422
423impl ToolReference {
424 pub fn identifier(&self) -> String {
426 match self {
427 ToolReference::Function { name } => format!("function:{name}"),
428 ToolReference::Mcp { server_label, name } => {
429 if let Some(n) = name {
430 format!("mcp:{server_label}:{n}")
431 } else {
432 format!("mcp:{server_label}")
433 }
434 }
435 ToolReference::FileSearch => "file_search".to_string(),
436 ToolReference::WebSearchPreview => "web_search_preview".to_string(),
437 ToolReference::ComputerUsePreview => "computer_use_preview".to_string(),
438 ToolReference::CodeInterpreter => "code_interpreter".to_string(),
439 ToolReference::ImageGeneration => "image_generation".to_string(),
440 }
441 }
442
443 pub fn function_name(&self) -> Option<&str> {
445 match self {
446 ToolReference::Function { name } => Some(name.as_str()),
447 _ => None,
448 }
449 }
450}
451
452#[derive(Debug, Clone, Deserialize, Serialize, schemars::JsonSchema)]
453pub struct Tool {
454 #[serde(rename = "type")]
455 pub tool_type: String, pub function: Function,
457}
458
459fn empty_parameters_schema() -> Value {
462 Value::Object(Map::new())
463}
464
465#[serde_with::skip_serializing_none]
466#[derive(Debug, Clone, Deserialize, Serialize, schemars::JsonSchema)]
467pub struct Function {
468 pub name: String,
469 pub description: Option<String>,
470 #[serde(default = "empty_parameters_schema")]
471 pub parameters: Value, pub strict: Option<bool>,
474}
475
476#[derive(Debug, Clone, Deserialize, Serialize, schemars::JsonSchema)]
477pub struct ToolCall {
478 pub id: String,
479 #[serde(rename = "type")]
480 pub tool_type: String, pub function: FunctionCallResponse,
482}
483
484#[derive(Debug, Clone)]
487pub enum FunctionCall {
488 None,
489 Auto,
490 Function { name: String },
491}
492
493impl Serialize for FunctionCall {
494 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
495 match self {
496 FunctionCall::None => serializer.serialize_str("none"),
497 FunctionCall::Auto => serializer.serialize_str("auto"),
498 FunctionCall::Function { name } => {
499 use serde::ser::SerializeMap;
500 let mut map = serializer.serialize_map(Some(1))?;
501 map.serialize_entry("name", name)?;
502 map.end()
503 }
504 }
505 }
506}
507
508impl<'de> Deserialize<'de> for FunctionCall {
509 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
510 let value = Value::deserialize(deserializer)?;
511 match &value {
512 Value::String(s) => match s.as_str() {
513 "none" => Ok(FunctionCall::None),
514 "auto" => Ok(FunctionCall::Auto),
515 other => Err(de::Error::custom(format!(
516 "unknown function_call value: \"{other}\""
517 ))),
518 },
519 Value::Object(map) => {
520 if let Some(Value::String(name)) = map.get("name") {
521 Ok(FunctionCall::Function { name: name.clone() })
522 } else {
523 Err(de::Error::custom(
524 "function_call object must have a \"name\" string field",
525 ))
526 }
527 }
528 _ => Err(de::Error::custom(
529 "function_call must be a string or object",
530 )),
531 }
532 }
533}
534
535impl schemars::JsonSchema for FunctionCall {
536 fn schema_name() -> std::borrow::Cow<'static, str> {
537 "FunctionCall".into()
538 }
539 fn json_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
540 let name_schema = generator.subschema_for::<String>();
542 schemars::json_schema!({
543 "anyOf": [
544 {
545 "type": "string",
546 "enum": ["none", "auto"]
547 },
548 {
549 "type": "object",
550 "properties": { "name": name_schema },
551 "required": ["name"]
552 }
553 ]
554 })
555 }
556}
557
558#[derive(Debug, Clone, Deserialize, Serialize, schemars::JsonSchema)]
559pub struct FunctionCallResponse {
560 pub name: String,
561 #[serde(default)]
562 pub arguments: Option<String>, }
564
565#[serde_with::skip_serializing_none]
569#[derive(Debug, Clone, Deserialize, Serialize, schemars::JsonSchema)]
570pub struct Usage {
571 pub prompt_tokens: u32,
572 pub completion_tokens: u32,
573 pub total_tokens: u32,
574 pub prompt_tokens_details: Option<PromptTokenUsageInfo>,
575 pub completion_tokens_details: Option<CompletionTokensDetails>,
576}
577
578impl Usage {
579 pub fn from_counts(prompt_tokens: u32, completion_tokens: u32) -> Self {
581 Self {
582 prompt_tokens,
583 completion_tokens,
584 total_tokens: prompt_tokens + completion_tokens,
585 prompt_tokens_details: None,
586 completion_tokens_details: None,
587 }
588 }
589
590 pub fn with_cached_tokens(mut self, cached_tokens: u32) -> Self {
592 self.prompt_tokens_details = Some(PromptTokenUsageInfo { cached_tokens });
596 self
597 }
598
599 pub fn with_reasoning_tokens(mut self, reasoning_tokens: u32) -> Self {
601 if reasoning_tokens > 0 {
602 self.completion_tokens_details = Some(CompletionTokensDetails {
603 reasoning_tokens: Some(reasoning_tokens),
604 accepted_prediction_tokens: None,
605 rejected_prediction_tokens: None,
606 });
607 }
608 self
609 }
610}
611
612#[serde_with::skip_serializing_none]
613#[derive(Debug, Clone, Deserialize, Serialize, schemars::JsonSchema)]
614pub struct CompletionTokensDetails {
615 pub reasoning_tokens: Option<u32>,
616 pub accepted_prediction_tokens: Option<u32>,
617 pub rejected_prediction_tokens: Option<u32>,
618}
619
620#[serde_with::skip_serializing_none]
622#[derive(Debug, Clone, Deserialize, Serialize, schemars::JsonSchema)]
623pub struct UsageInfo {
624 pub prompt_tokens: u32,
625 pub completion_tokens: u32,
626 pub total_tokens: u32,
627 pub reasoning_tokens: Option<u32>,
628 pub prompt_tokens_details: Option<PromptTokenUsageInfo>,
629}
630
631#[derive(Debug, Clone, Deserialize, Serialize, schemars::JsonSchema)]
632pub struct PromptTokenUsageInfo {
633 pub cached_tokens: u32,
634}
635
636#[derive(Debug, Clone, Deserialize, Serialize, schemars::JsonSchema)]
637pub struct LogProbs {
638 pub tokens: Vec<String>,
639 pub token_logprobs: Vec<Option<f32>>,
640 pub top_logprobs: Vec<Option<HashMap<String, f32>>>,
641 pub text_offset: Vec<u32>,
642}
643
644#[derive(Debug, Clone, Deserialize, Serialize, schemars::JsonSchema)]
645#[serde(untagged)]
646pub enum ChatLogProbs {
647 Detailed {
648 #[serde(skip_serializing_if = "Option::is_none")]
649 content: Option<Vec<ChatLogProbsContent>>,
650 },
651 Raw(Value),
652}
653
654#[derive(Debug, Clone, Deserialize, Serialize, schemars::JsonSchema)]
655pub struct ChatLogProbsContent {
656 pub token: String,
657 pub logprob: f32,
658 pub bytes: Option<Vec<u8>>,
659 pub top_logprobs: Vec<TopLogProb>,
660}
661
662#[derive(Debug, Clone, Deserialize, Serialize, schemars::JsonSchema)]
663pub struct TopLogProb {
664 pub token: String,
665 pub logprob: f32,
666 pub bytes: Option<Vec<u8>>,
667}
668
669#[derive(Debug, Clone, Deserialize, Serialize, schemars::JsonSchema)]
674pub struct ErrorResponse {
675 pub error: ErrorDetail,
676}
677
678#[serde_with::skip_serializing_none]
679#[derive(Debug, Clone, Deserialize, Serialize, schemars::JsonSchema)]
680pub struct ErrorDetail {
681 pub message: String,
682 #[serde(rename = "type")]
683 pub error_type: String,
684 pub param: Option<String>,
685 pub code: Option<String>,
686}
687
688#[derive(Debug, Clone, Serialize, schemars::JsonSchema)]
693#[serde(untagged)]
694pub enum InputIds {
695 Single(Vec<i32>),
696 Batch(Vec<Vec<i32>>),
697}
698
699enum FirstInputId {
702 Id(i32),
703 Ids(Vec<i32>),
704}
705
706impl<'de> Deserialize<'de> for FirstInputId {
707 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
708 where
709 D: Deserializer<'de>,
710 {
711 struct FirstInputIdVisitor;
712
713 impl<'de> Visitor<'de> for FirstInputIdVisitor {
714 type Value = FirstInputId;
715
716 fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
717 f.write_str("a token id or an array of token ids")
718 }
719
720 fn visit_i64<E: de::Error>(self, v: i64) -> Result<Self::Value, E> {
721 i32::try_from(v)
722 .map(FirstInputId::Id)
723 .map_err(|_| E::invalid_value(de::Unexpected::Signed(v), &self))
724 }
725
726 fn visit_u64<E: de::Error>(self, v: u64) -> Result<Self::Value, E> {
727 i32::try_from(v)
728 .map(FirstInputId::Id)
729 .map_err(|_| E::invalid_value(de::Unexpected::Unsigned(v), &self))
730 }
731
732 fn visit_seq<A: SeqAccess<'de>>(self, seq: A) -> Result<Self::Value, A::Error> {
733 Deserialize::deserialize(SeqAccessDeserializer::new(seq)).map(FirstInputId::Ids)
734 }
735 }
736
737 deserializer.deserialize_any(FirstInputIdVisitor)
738 }
739}
740
741impl<'de> Deserialize<'de> for InputIds {
742 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
743 where
744 D: Deserializer<'de>,
745 {
746 struct InputIdsVisitor;
747
748 impl<'de> Visitor<'de> for InputIdsVisitor {
749 type Value = InputIds;
750
751 fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
752 f.write_str("an array of token ids or an array of token id arrays")
753 }
754
755 fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
756 where
757 A: SeqAccess<'de>,
758 {
759 let Some(first) = seq.next_element::<FirstInputId>()? else {
761 return Ok(InputIds::Single(Vec::new()));
762 };
763 let remaining = seq.size_hint().unwrap_or(0);
764 match first {
765 FirstInputId::Id(id) => {
766 let mut ids = Vec::with_capacity(remaining.saturating_add(1));
767 ids.push(id);
768 while let Some(id) = seq.next_element()? {
769 ids.push(id);
770 }
771 Ok(InputIds::Single(ids))
772 }
773 FirstInputId::Ids(head) => {
774 let mut seqs = Vec::with_capacity(remaining.saturating_add(1));
775 seqs.push(head);
776 while let Some(ids) = seq.next_element()? {
777 seqs.push(ids);
778 }
779 Ok(InputIds::Batch(seqs))
780 }
781 }
782 }
783 }
784
785 deserializer.deserialize_seq(InputIdsVisitor)
786 }
787}
788
789#[derive(Debug, Clone, Deserialize, Serialize, schemars::JsonSchema)]
791#[serde(untagged)]
792pub enum LoRAPath {
793 Single(Option<String>),
794 Batch(Vec<Option<String>>),
795}
796
797#[derive(Clone, Serialize, Deserialize, schemars::JsonSchema)]
801pub struct Redacted(pub String);
802
803impl std::fmt::Debug for Redacted {
804 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
805 f.write_str("[REDACTED]")
806 }
807}
808
809#[serde_with::skip_serializing_none]
815#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
816pub struct ResponsePrompt {
817 pub id: String,
818 pub variables: Option<HashMap<String, PromptVariable>>,
819 pub version: Option<String>,
820}
821
822#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
827#[serde(untagged)]
828pub enum PromptVariable {
829 String(String),
830 Typed(PromptVariableTyped),
831}
832
833#[serde_with::skip_serializing_none]
835#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
836#[serde(tag = "type")]
837#[expect(
838 clippy::enum_variant_names,
839 reason = "variant names match OpenAI API spec"
840)]
841pub enum PromptVariableTyped {
842 #[serde(rename = "input_text")]
843 ResponseInputText { text: String },
844 #[serde(rename = "input_image")]
845 ResponseInputImage {
846 detail: Option<Detail>,
847 file_id: Option<String>,
848 image_url: Option<String>,
849 },
850 #[serde(rename = "input_file")]
851 ResponseInputFile {
852 file_data: Option<String>,
853 file_id: Option<String>,
854 file_url: Option<String>,
855 filename: Option<String>,
856 },
857}
858
859#[derive(Debug, Clone, Serialize, Deserialize, Default, schemars::JsonSchema)]
863#[serde(rename_all = "snake_case")]
864pub enum Detail {
865 Low,
866 High,
867 #[default]
868 Auto,
869 Original,
870}
871
872#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
880pub enum PromptCacheRetention {
881 #[serde(rename = "in-memory")]
882 InMemory,
883 #[serde(rename = "24h")]
884 Duration24h,
885}
886
887#[serde_with::skip_serializing_none]
892#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
893pub struct ContextManagementEntry {
894 #[serde(rename = "type")]
895 pub r#type: ContextManagementType,
896 pub compact_threshold: Option<u32>,
897}
898
899#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
903#[serde(rename_all = "snake_case")]
904pub enum ContextManagementType {
905 Compaction,
906}
907
908#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
918#[serde(untagged)]
919pub enum ConversationRef {
920 Id(String),
921 Object { id: String },
922}
923
924impl ConversationRef {
925 pub fn as_id(&self) -> &str {
927 match self {
928 Self::Id(id) | Self::Object { id } => id.as_str(),
929 }
930 }
931
932 pub fn is_empty(&self) -> bool {
936 self.as_id().is_empty()
937 }
938}
939
940#[cfg(test)]
941mod tests {
942 use serde::Deserialize;
943 use serde_json::json;
944
945 use super::*;
946
947 #[derive(Deserialize)]
948 struct NullableBoolTest {
949 #[serde(default, deserialize_with = "deserialize_null_as_false")]
950 field: bool,
951 }
952
953 #[test]
954 fn test_deserialize_null_as_false() {
955 let cases = [
956 (json!({"field": true}), true),
957 (json!({"field": false}), false),
958 (json!({"field": null}), false),
959 (json!({}), false),
960 ];
961 for (input, expected) in cases {
962 let t: NullableBoolTest = serde_json::from_value(input).unwrap();
963 assert_eq!(t.field, expected);
964 }
965 }
966
967 #[test]
968 fn test_deserialize_null_as_false_rejects_non_bool() {
969 let result = serde_json::from_value::<NullableBoolTest>(json!({"field": "yes"}));
970 assert!(result.is_err());
971 }
972
973 #[test]
974 fn stream_options_preserve_unknown_fields() {
975 let raw = json!({
976 "include_usage": true,
977 "continuous_usage_stats": true,
978 "step_usage_chunks": "all",
979 "engine_specific": {"nested": 1},
980 });
981
982 let opts: StreamOptions = serde_json::from_value(raw.clone()).unwrap();
983 assert_eq!(opts.include_usage, Some(true));
984 assert_eq!(opts.continuous_usage_stats, Some(true));
985 assert_eq!(opts.other.get("step_usage_chunks"), Some(&json!("all")));
986
987 assert_eq!(serde_json::to_value(&opts).unwrap(), raw);
990 }
991
992 #[test]
993 fn stream_options_without_unknown_fields_stay_compact() {
994 let opts: StreamOptions = serde_json::from_value(json!({"include_usage": true})).unwrap();
995 assert!(opts.other.is_empty());
996 assert_eq!(
997 serde_json::to_value(&opts).unwrap(),
998 json!({"include_usage": true})
999 );
1000 }
1001
1002 #[test]
1003 fn cached_token_builder_preserves_explicit_zero() {
1004 let usage = Usage::from_counts(16, 1).with_cached_tokens(0);
1005 assert!(matches!(
1006 usage.prompt_tokens_details,
1007 Some(PromptTokenUsageInfo { cached_tokens: 0 })
1008 ));
1009 }
1010
1011 #[test]
1012 fn content_part_deserializes_audio_url() {
1013 let value = json!({
1014 "type": "audio_url",
1015 "audio_url": {
1016 "url": "https://example.com/audio.wav"
1017 }
1018 });
1019 let part: ContentPart = serde_json::from_value(value).expect("audio_url content part");
1020 assert_eq!(
1021 part,
1022 ContentPart::AudioUrl {
1023 audio_url: AudioUrl {
1024 url: "https://example.com/audio.wav".to_string(),
1025 },
1026 }
1027 );
1028 }
1029
1030 #[test]
1031 fn content_part_round_trips_input_audio() {
1032 let value = json!({
1033 "type": "input_audio",
1034 "input_audio": {
1035 "data": "UklGRg==",
1036 "format": "wav"
1037 }
1038 });
1039 let part: ContentPart =
1040 serde_json::from_value(value.clone()).expect("input_audio content part");
1041 assert_eq!(
1042 part,
1043 ContentPart::InputAudio {
1044 input_audio: InputAudio {
1045 data: "UklGRg==".to_string(),
1046 format: "wav".to_string(),
1047 },
1048 }
1049 );
1050 assert_eq!(serde_json::to_value(part).unwrap(), value);
1051 }
1052
1053 #[test]
1054 fn conversation_ref_deserializes_bare_string() {
1055 let v = json!("conv_abc");
1056 let r: ConversationRef = serde_json::from_value(v).expect("string form");
1057 assert!(matches!(r, ConversationRef::Id(ref s) if s == "conv_abc"));
1058 assert_eq!(r.as_id(), "conv_abc");
1059 assert_eq!(serde_json::to_value(&r).unwrap(), json!("conv_abc"));
1061 }
1062
1063 #[test]
1064 fn conversation_ref_deserializes_object() {
1065 let v = json!({"id": "conv_xyz"});
1066 let r: ConversationRef = serde_json::from_value(v).expect("object form");
1067 assert!(matches!(r, ConversationRef::Object { ref id } if id == "conv_xyz"));
1068 assert_eq!(r.as_id(), "conv_xyz");
1069 assert_eq!(serde_json::to_value(&r).unwrap(), json!({"id": "conv_xyz"}));
1071 }
1072
1073 #[test]
1074 fn conversation_ref_is_empty() {
1075 assert!(ConversationRef::Id(String::new()).is_empty());
1076 assert!(!ConversationRef::Id("conv_1".to_string()).is_empty());
1077 assert!(ConversationRef::Object { id: String::new() }.is_empty());
1078 }
1079
1080 #[test]
1081 fn input_ids_deserializes_single() {
1082 let ids: InputIds = serde_json::from_str("[1, -2, 3]").unwrap();
1083 assert!(matches!(ids, InputIds::Single(ref v) if v == &[1, -2, 3]));
1084 }
1085
1086 #[test]
1087 fn input_ids_deserializes_batch() {
1088 let ids: InputIds = serde_json::from_str("[[1, 2], [3], []]").unwrap();
1089 assert!(matches!(ids, InputIds::Batch(ref v) if v == &[vec![1, 2], vec![3], vec![]]));
1090 }
1091
1092 #[test]
1093 fn input_ids_empty_array_is_single() {
1094 let ids: InputIds = serde_json::from_str("[]").unwrap();
1095 assert!(matches!(ids, InputIds::Single(ref v) if v.is_empty()));
1096 }
1097
1098 #[test]
1099 fn input_ids_rejects_invalid_input() {
1100 for input in [
1101 "[1, [2]]",
1102 "[[1], 2]",
1103 "[\"a\"]",
1104 "[1.5]",
1105 "[5000000000]",
1106 "\"nope\"",
1107 "null",
1108 "7",
1109 ] {
1110 assert!(
1111 serde_json::from_str::<InputIds>(input).is_err(),
1112 "accepted {input}"
1113 );
1114 }
1115 }
1116
1117 #[test]
1118 fn input_ids_round_trip() {
1119 for input in [json!([1, 2, 3]), json!([[1, 2], [3]]), json!([])] {
1120 let ids: InputIds = serde_json::from_value(input.clone()).unwrap();
1121 assert_eq!(serde_json::to_value(&ids).unwrap(), input);
1122 }
1123 }
1124
1125 #[test]
1126 fn function_deserializes_without_parameters() {
1127 let value = json!({"name": "web_search", "description": ""});
1130 let function: Function = serde_json::from_value(value).expect("parameterless function");
1131 assert_eq!(function.parameters, json!({}));
1132 }
1133}