1use std::pin::Pin;
9
10use derive_builder::Builder;
11use futures::Stream;
12use serde::{Deserialize, Serialize};
13use url::Url;
14use uuid::Uuid;
15
16use crate::error::OpenAIError;
17
18pub use async_openai::types::chat::{
25 ChatCompletionAudio, ChatCompletionAudioFormat, ChatCompletionAudioVoice,
26 ChatCompletionFunctionCall, ChatCompletionFunctions, ChatCompletionFunctionsArgs,
27 ChatCompletionRequestAssistantMessageAudio, ChatCompletionRequestAssistantMessageContent,
28 ChatCompletionRequestAssistantMessageContentPart, ChatCompletionRequestDeveloperMessage,
29 ChatCompletionRequestDeveloperMessageArgs, ChatCompletionRequestDeveloperMessageContent,
30 ChatCompletionRequestFunctionMessage, ChatCompletionRequestFunctionMessageArgs,
31 ChatCompletionRequestMessageContentPartAudio, ChatCompletionRequestMessageContentPartRefusal,
32 ChatCompletionRequestMessageContentPartText, ChatCompletionRequestSystemMessageContent,
33 ChatCompletionRequestSystemMessageContentPart, ChatCompletionResponseMessageAudio, Choice,
34 CompletionFinishReason, CompletionTokensDetails, CompletionUsage, FunctionObject,
35 FunctionObjectArgs, ImageDetail, InputAudio, InputAudioFormat, Logprobs, PredictionContent,
36 PredictionContentContent, Prompt, PromptTokensDetails, ResponseFormat,
37 ResponseFormatJsonSchema, Role, ServiceTier, TopLogprobs, WebSearchContextSize,
38 WebSearchLocation, WebSearchOptions, WebSearchUserLocation, WebSearchUserLocationType,
39};
40
41#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
49#[serde(untagged)]
50pub enum Stop {
51 String(String),
52 StringArray(Vec<String>),
53 TokenIdArray(Vec<u32>),
54}
55
56impl Stop {
57 pub fn strings(&self) -> Option<Vec<String>> {
58 match self {
59 Stop::String(s) => Some(vec![s.clone()]),
60 Stop::StringArray(arr) => Some(arr.clone()),
61 Stop::TokenIdArray(_) => None,
62 }
63 }
64
65 pub fn token_ids(&self) -> Option<Vec<u32>> {
66 match self {
67 Stop::TokenIdArray(arr) => Some(arr.clone()),
68 Stop::String(_) | Stop::StringArray(_) => None,
69 }
70 }
71}
72
73impl From<String> for Stop {
74 fn from(value: String) -> Self {
75 Stop::String(value)
76 }
77}
78
79impl From<&str> for Stop {
80 fn from(value: &str) -> Self {
81 Stop::String(value.to_string())
82 }
83}
84
85impl From<Vec<String>> for Stop {
86 fn from(value: Vec<String>) -> Self {
87 Stop::StringArray(value)
88 }
89}
90
91impl From<Vec<u32>> for Stop {
92 fn from(value: Vec<u32>) -> Self {
93 Stop::TokenIdArray(value)
94 }
95}
96
97impl From<async_openai::types::chat::StopConfiguration> for Stop {
98 fn from(value: async_openai::types::chat::StopConfiguration) -> Self {
99 match value {
100 async_openai::types::chat::StopConfiguration::String(value) => Stop::String(value),
101 async_openai::types::chat::StopConfiguration::StringArray(value) => {
102 Stop::StringArray(value)
103 }
104 }
105 }
106}
107
108pub use async_openai::types::chat::FinishReason;
110
111pub use async_openai::types::chat::FunctionType;
114
115#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
121#[serde(rename_all = "lowercase")]
122pub enum ReasoningEffort {
123 None,
124 Minimal,
125 Low,
126 Medium,
127 High,
128 Xhigh,
129 Max,
130}
131
132impl From<async_openai::types::chat::ReasoningEffort> for ReasoningEffort {
133 fn from(value: async_openai::types::chat::ReasoningEffort) -> Self {
134 match value {
135 async_openai::types::chat::ReasoningEffort::None => ReasoningEffort::None,
136 async_openai::types::chat::ReasoningEffort::Minimal => ReasoningEffort::Minimal,
137 async_openai::types::chat::ReasoningEffort::Low => ReasoningEffort::Low,
138 async_openai::types::chat::ReasoningEffort::Medium => ReasoningEffort::Medium,
139 async_openai::types::chat::ReasoningEffort::High => ReasoningEffort::High,
140 async_openai::types::chat::ReasoningEffort::Xhigh => ReasoningEffort::Xhigh,
141 }
142 }
143}
144
145fn deserialize_arguments<'de, D>(deserializer: D) -> Result<String, D::Error>
154where
155 D: serde::Deserializer<'de>,
156{
157 use serde::de::Error;
158 let value = serde_json::Value::deserialize(deserializer)?;
159 match value {
160 serde_json::Value::String(s) => Ok(s),
161 v @ serde_json::Value::Object(_) => {
162 Ok(serde_json::to_string(&v).unwrap())
164 }
165 other => Err(D::Error::custom(format!(
166 "expected string or object for `arguments`, got {other}"
167 ))),
168 }
169}
170
171fn deserialize_arguments_opt<'de, D>(deserializer: D) -> Result<Option<String>, D::Error>
172where
173 D: serde::Deserializer<'de>,
174{
175 use serde::de::Error;
176 let value = Option::<serde_json::Value>::deserialize(deserializer)?;
177 match value {
178 None => Ok(None),
179 Some(serde_json::Value::String(s)) => Ok(Some(s)),
180 Some(v @ serde_json::Value::Object(_)) => serde_json::to_string(&v)
181 .map(Some)
182 .map_err(|e| D::Error::custom(e.to_string())),
183 Some(other) => Err(D::Error::custom(format!(
184 "expected string or object for `arguments`, got {other}"
185 ))),
186 }
187}
188
189fn deserialize_optional_media<'de, D, T>(deserializer: D) -> Result<Option<T>, D::Error>
195where
196 D: serde::Deserializer<'de>,
197 T: serde::de::DeserializeOwned,
198{
199 use serde::de::Error;
200 match Option::<serde_json::Value>::deserialize(deserializer)? {
201 None => Ok(None),
202 Some(value) if value.get("url").and_then(serde_json::Value::as_str) == Some("") => Ok(None),
203 Some(value) => serde_json::from_value(value)
204 .map(Some)
205 .map_err(D::Error::custom),
206 }
207}
208
209#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Default)]
222pub struct FunctionCall {
223 pub name: String,
224 #[serde(deserialize_with = "deserialize_arguments")]
225 pub arguments: String,
226}
227
228#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Default)]
232pub struct FunctionCallStream {
233 #[serde(skip_serializing_if = "Option::is_none")]
234 pub name: Option<String>,
235 #[serde(
236 default,
237 skip_serializing_if = "Option::is_none",
238 deserialize_with = "deserialize_arguments_opt"
239 )]
240 pub arguments: Option<String>,
241}
242
243#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Default)]
249pub struct ChatCompletionMessageToolCallChunk {
250 pub index: u32,
251 #[serde(skip_serializing_if = "Option::is_none")]
254 pub id: Option<String>,
255 #[serde(skip_serializing_if = "Option::is_none")]
256 pub r#type: Option<FunctionType>,
257 #[serde(skip_serializing_if = "Option::is_none")]
258 pub function: Option<FunctionCallStream>,
259}
260
261#[derive(Debug, Serialize, Deserialize, Clone, Builder, PartialEq)]
274#[builder(name = "ChatCompletionRequestMessageContentPartImageArgs")]
275#[builder(pattern = "mutable")]
276#[builder(setter(into, strip_option))]
277#[builder(derive(Debug))]
278#[builder(build_fn(error = "OpenAIError"))]
279pub struct ChatCompletionRequestMessageContentPartImage {
280 #[builder(default)]
281 #[serde(default, deserialize_with = "deserialize_optional_media")]
282 pub image_url: Option<ImageUrl>,
283 #[builder(default)]
284 #[serde(skip_serializing_if = "Option::is_none")]
285 pub uuid: Option<String>,
287}
288
289#[derive(Debug, Serialize, Deserialize, Clone, Builder, PartialEq)]
294#[builder(name = "ImageUrlArgs")]
295#[builder(pattern = "mutable")]
296#[builder(setter(into, strip_option))]
297#[builder(derive(Debug))]
298#[builder(build_fn(error = "OpenAIError"))]
299pub struct ImageUrl {
300 pub url: Url,
301 pub detail: Option<ImageDetail>,
302 #[deprecated(note = "use the content-part `uuid` field for vLLM cache identities")]
303 #[serde(skip_serializing_if = "Option::is_none")]
304 pub uuid: Option<Uuid>,
305}
306
307#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
313#[serde(tag = "type")]
314#[serde(rename_all = "snake_case")]
315pub enum ChatCompletionRequestToolMessageContentPart {
316 Text(ChatCompletionRequestMessageContentPartText),
317 ImageUrl(ChatCompletionRequestMessageContentPartImage),
318 VideoUrl(ChatCompletionRequestMessageContentPartVideo),
319 AudioUrl(ChatCompletionRequestMessageContentPartAudioUrl),
320}
321
322#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
324#[serde(untagged)]
325pub enum ChatCompletionRequestToolMessageContent {
326 Text(String),
327 Array(Vec<ChatCompletionRequestToolMessageContentPart>),
328}
329
330impl Default for ChatCompletionRequestToolMessageContent {
331 fn default() -> Self {
332 Self::Text(String::new())
333 }
334}
335
336impl From<&str> for ChatCompletionRequestToolMessageContent {
337 fn from(value: &str) -> Self {
338 Self::Text(value.into())
339 }
340}
341
342impl From<String> for ChatCompletionRequestToolMessageContent {
343 fn from(value: String) -> Self {
344 Self::Text(value)
345 }
346}
347
348#[derive(Debug, Serialize, Deserialize, Default, Clone, Builder, PartialEq)]
350#[builder(name = "ChatCompletionRequestToolMessageArgs")]
351#[builder(pattern = "mutable")]
352#[builder(setter(into, strip_option), default)]
353#[builder(derive(Debug))]
354#[builder(build_fn(error = "OpenAIError"))]
355pub struct ChatCompletionRequestToolMessage {
356 pub content: ChatCompletionRequestToolMessageContent,
357 pub tool_call_id: String,
358}
359
360#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
361pub struct ChatChoiceLogprobs {
362 pub content: Option<Vec<ChatCompletionTokenLogprob>>,
363 pub refusal: Option<Vec<ChatCompletionTokenLogprob>>,
364}
365
366#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
372pub struct ChatCompletionTokenLogprob {
373 pub token: String,
374 pub logprob: f32,
375 #[serde(skip_serializing_if = "Option::is_none")]
376 pub token_id: Option<u32>,
377 pub bytes: Option<Vec<u8>>,
378 pub top_logprobs: Vec<TopLogprobs>,
379}
380
381#[derive(Clone, Serialize, Default, Debug, Deserialize, PartialEq)]
382#[serde(rename_all = "lowercase")]
383pub enum ChatCompletionToolType {
384 #[default]
385 Function,
386}
387
388#[derive(Clone, Serialize, Default, Debug, Deserialize, PartialEq)]
389pub struct FunctionName {
390 pub name: String,
391}
392
393#[derive(Clone, Serialize, Default, Debug, Deserialize, PartialEq)]
394pub struct ChatCompletionNamedToolChoice {
395 pub r#type: ChatCompletionToolType,
396 pub function: FunctionName,
397}
398
399fn default_function_type() -> FunctionType {
400 FunctionType::Function
401}
402
403#[derive(Clone, Serialize, Debug, Deserialize, PartialEq)]
409pub struct ChatCompletionMessageToolCall {
410 pub id: String,
411 #[serde(default = "default_function_type")]
412 pub r#type: FunctionType,
413 pub function: FunctionCall,
414}
415
416#[derive(Clone, Serialize, Default, Debug, Deserialize, PartialEq)]
418#[serde(rename_all = "lowercase")]
419pub enum ChatCompletionToolChoiceOption {
420 #[default]
421 None,
422 Auto,
423 Required,
424 #[serde(untagged)]
425 Named(ChatCompletionNamedToolChoice),
426}
427
428#[derive(Clone, Serialize, Default, Debug, Builder, Deserialize, PartialEq)]
429#[builder(name = "ChatCompletionToolArgs")]
430#[builder(pattern = "mutable")]
431#[builder(setter(into, strip_option), default)]
432#[builder(derive(Debug))]
433#[builder(build_fn(error = "OpenAIError"))]
434pub struct ChatCompletionTool {
435 #[builder(default = "ChatCompletionToolType::Function")]
436 pub r#type: ChatCompletionToolType,
437 pub function: FunctionObject,
438}
439
440#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
451#[serde(untagged)]
452pub enum StopReason {
453 String(String),
454 Int(i64),
455 IntArray(Vec<i64>),
456}
457
458#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
468#[serde(untagged)]
469pub enum ReasoningContent {
470 Text(String),
472 Segments(Vec<String>),
475}
476
477impl ReasoningContent {
478 pub fn to_flat_string(&self) -> String {
480 match self {
481 ReasoningContent::Text(s) => s.clone(),
482 ReasoningContent::Segments(segs) => segs
483 .iter()
484 .filter(|s| !s.is_empty())
485 .cloned()
486 .collect::<Vec<_>>()
487 .join("\n"),
488 }
489 }
490
491 pub fn segments(&self) -> Option<&[String]> {
493 match self {
494 ReasoningContent::Segments(segs) => Some(segs),
495 ReasoningContent::Text(_) => None,
496 }
497 }
498}
499
500#[derive(Clone, Serialize, Debug, Deserialize, PartialEq)]
504pub struct ChatCompletionResponseContentPartText {
505 pub text: String,
506}
507
508#[derive(Clone, Serialize, Debug, Deserialize, PartialEq)]
510pub struct ChatCompletionResponseContentPartImageUrl {
511 pub image_url: ImageUrlResponse,
512}
513
514#[derive(Clone, Serialize, Debug, Deserialize, PartialEq)]
516pub struct ChatCompletionResponseContentPartVideoUrl {
517 pub video_url: VideoUrlResponse,
518}
519
520#[derive(Clone, Serialize, Debug, Deserialize, PartialEq)]
522pub struct ChatCompletionResponseContentPartAudioUrl {
523 pub audio_url: AudioUrlResponse,
524}
525
526#[derive(Clone, Serialize, Debug, Deserialize, PartialEq)]
527pub struct ImageUrlResponse {
528 pub url: String,
529 #[serde(skip_serializing_if = "Option::is_none")]
530 pub detail: Option<String>,
531}
532
533#[derive(Clone, Serialize, Debug, Deserialize, PartialEq)]
534pub struct VideoUrlResponse {
535 pub url: String,
536}
537
538#[derive(Clone, Serialize, Debug, Deserialize, PartialEq)]
539pub struct AudioUrlResponse {
540 pub url: String,
541}
542
543#[derive(Clone, Serialize, Debug, Deserialize, PartialEq)]
545#[serde(tag = "type", rename_all = "snake_case")]
546pub enum ChatCompletionResponseContentPart {
547 Text(ChatCompletionResponseContentPartText),
548 ImageUrl(ChatCompletionResponseContentPartImageUrl),
549 VideoUrl(ChatCompletionResponseContentPartVideoUrl),
550 AudioUrl(ChatCompletionResponseContentPartAudioUrl),
551}
552
553#[derive(Clone, Serialize, Debug, Deserialize, PartialEq)]
559#[serde(untagged)]
560pub enum ChatCompletionMessageContent {
561 Text(String),
563 Parts(Vec<ChatCompletionResponseContentPart>),
565}
566
567#[derive(Debug, Serialize, Deserialize, Clone, Builder, PartialEq)]
570#[builder(name = "VideoUrlArgs")]
571#[builder(pattern = "mutable")]
572#[builder(setter(into, strip_option))]
573#[builder(derive(Debug))]
574#[builder(build_fn(error = "OpenAIError"))]
575pub struct VideoUrl {
576 pub url: Url,
577 pub detail: Option<ImageDetail>,
578 #[deprecated(note = "use the content-part `uuid` field for vLLM cache identities")]
579 #[serde(skip_serializing_if = "Option::is_none")]
580 pub uuid: Option<Uuid>,
581}
582
583#[derive(Debug, Serialize, Deserialize, Clone, Builder, PartialEq)]
584#[builder(name = "ChatCompletionRequestMessageContentPartVideoArgs")]
585#[builder(pattern = "mutable")]
586#[builder(setter(into, strip_option))]
587#[builder(derive(Debug))]
588#[builder(build_fn(error = "OpenAIError"))]
589pub struct ChatCompletionRequestMessageContentPartVideo {
590 #[builder(default)]
591 #[serde(default, deserialize_with = "deserialize_optional_media")]
592 pub video_url: Option<VideoUrl>,
593 #[builder(default)]
594 #[serde(skip_serializing_if = "Option::is_none")]
595 pub uuid: Option<String>,
597}
598
599#[derive(Debug, Serialize, Deserialize, Clone, Builder, PartialEq)]
600#[builder(name = "AudioUrlArgs")]
601#[builder(pattern = "mutable")]
602#[builder(setter(into, strip_option))]
603#[builder(derive(Debug))]
604#[builder(build_fn(error = "OpenAIError"))]
605pub struct AudioUrl {
606 pub url: Url,
607 #[deprecated(note = "use the content-part `uuid` field for vLLM cache identities")]
608 #[serde(skip_serializing_if = "Option::is_none")]
609 pub uuid: Option<Uuid>,
610}
611
612#[derive(Debug, Serialize, Deserialize, Clone, Builder, PartialEq)]
613#[builder(name = "ChatCompletionRequestMessageContentPartAudioUrlArgs")]
614#[builder(pattern = "mutable")]
615#[builder(setter(into, strip_option))]
616#[builder(derive(Debug))]
617#[builder(build_fn(error = "OpenAIError"))]
618pub struct ChatCompletionRequestMessageContentPartAudioUrl {
619 #[builder(default)]
620 #[serde(default, deserialize_with = "deserialize_optional_media")]
621 pub audio_url: Option<AudioUrl>,
622 #[builder(default)]
623 #[serde(skip_serializing_if = "Option::is_none")]
624 pub uuid: Option<String>,
626}
627
628#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
632#[serde(untagged)]
633pub enum ChatCompletionRequestUserMessageContent {
634 Text(String),
635 Array(Vec<ChatCompletionRequestUserMessageContentPart>),
636}
637
638#[derive(Debug, Serialize, Deserialize, Default, Clone, Builder, PartialEq)]
639#[builder(name = "ChatCompletionRequestUserMessageArgs")]
640#[builder(pattern = "mutable")]
641#[builder(setter(into, strip_option), default)]
642#[builder(derive(Debug))]
643#[builder(build_fn(error = "OpenAIError"))]
644pub struct ChatCompletionRequestUserMessage {
645 pub content: ChatCompletionRequestUserMessageContent,
646 #[serde(skip_serializing_if = "Option::is_none")]
647 pub name: Option<String>,
648}
649
650impl Default for ChatCompletionRequestUserMessageContent {
651 fn default() -> Self {
652 Self::Text(String::new())
653 }
654}
655
656impl From<&str> for ChatCompletionRequestUserMessageContent {
657 fn from(value: &str) -> Self {
658 Self::Text(value.into())
659 }
660}
661
662impl From<String> for ChatCompletionRequestUserMessageContent {
663 fn from(value: String) -> Self {
664 Self::Text(value)
665 }
666}
667
668impl From<Vec<ChatCompletionRequestUserMessageContentPart>>
669 for ChatCompletionRequestUserMessageContent
670{
671 fn from(value: Vec<ChatCompletionRequestUserMessageContentPart>) -> Self {
672 Self::Array(value)
673 }
674}
675
676#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
682#[serde(tag = "type")]
683#[serde(rename_all = "snake_case")]
684pub enum ChatCompletionRequestUserMessageContentPart {
685 Text(ChatCompletionRequestMessageContentPartText),
686 ImageUrl(ChatCompletionRequestMessageContentPartImage),
687 VideoUrl(ChatCompletionRequestMessageContentPartVideo),
688 AudioUrl(ChatCompletionRequestMessageContentPartAudioUrl),
689 InputAudio(ChatCompletionRequestMessageContentPartAudio),
690}
691
692#[derive(Debug, Serialize, Clone, Builder, PartialEq, Default)]
711#[builder(name = "ChatCompletionRequestSystemMessageArgs")]
712#[builder(pattern = "mutable")]
713#[builder(setter(into, strip_option), default)]
714#[builder(derive(Debug))]
715#[builder(build_fn(error = "OpenAIError"))]
716pub struct ChatCompletionRequestSystemMessage {
717 pub content: ChatCompletionRequestSystemMessageContent,
718 #[serde(skip_serializing_if = "Option::is_none")]
719 pub name: Option<String>,
720 #[serde(skip_serializing_if = "Option::is_none")]
742 pub tools: Option<Vec<serde_json::Value>>,
743}
744
745impl<'de> Deserialize<'de> for ChatCompletionRequestSystemMessage {
746 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
747 where
748 D: serde::Deserializer<'de>,
749 {
750 use serde::de::Error;
751
752 #[derive(Deserialize)]
756 struct Wire {
757 content: Option<ChatCompletionRequestSystemMessageContent>,
758 name: Option<String>,
759 tools: Option<Vec<serde_json::Value>>,
760 }
761
762 let Wire {
763 content,
764 name,
765 tools,
766 } = Wire::deserialize(deserializer)?;
767 let content = match content {
768 Some(content) => content,
769 None if tools.as_ref().is_some_and(|tools| !tools.is_empty()) => {
770 ChatCompletionRequestSystemMessageContent::Text(String::new())
771 }
772 None => {
773 return Err(D::Error::custom(
774 "missing field `content`: a system message needs `content` unless it \
775 declares non-empty Kimi-style `tools`",
776 ));
777 }
778 };
779 Ok(Self {
780 content,
781 name,
782 tools,
783 })
784 }
785}
786
787#[derive(Debug, Serialize, Deserialize, Default, Clone, Builder, PartialEq)]
795#[builder(name = "ChatCompletionRequestAssistantMessageArgs")]
796#[builder(pattern = "mutable")]
797#[builder(setter(into, strip_option), default)]
798#[builder(derive(Debug))]
799#[builder(build_fn(error = "OpenAIError"))]
800pub struct ChatCompletionRequestAssistantMessage {
801 #[serde(skip_serializing_if = "Option::is_none")]
802 pub content: Option<ChatCompletionRequestAssistantMessageContent>,
803 #[serde(default, alias = "reasoning", skip_serializing_if = "Option::is_none")]
809 pub reasoning_content: Option<ReasoningContent>,
810 #[serde(skip_serializing_if = "Option::is_none")]
811 pub refusal: Option<String>,
812 #[serde(skip_serializing_if = "Option::is_none")]
813 pub name: Option<String>,
814 #[serde(skip_serializing_if = "Option::is_none")]
815 pub audio: Option<ChatCompletionRequestAssistantMessageAudio>,
816 #[serde(skip_serializing_if = "Option::is_none")]
817 pub tool_calls: Option<Vec<ChatCompletionMessageToolCall>>,
818 #[deprecated]
819 #[serde(skip_serializing_if = "Option::is_none")]
820 pub function_call: Option<FunctionCall>,
821 #[serde(skip_serializing_if = "Option::is_none")]
822 pub partial: Option<bool>,
823}
824
825#[derive(Debug, Serialize, Clone, PartialEq)]
836#[serde(tag = "role")]
837#[serde(rename_all = "lowercase")]
838pub enum ChatCompletionRequestMessage {
839 Developer(ChatCompletionRequestDeveloperMessage),
840 System(ChatCompletionRequestSystemMessage),
841 User(ChatCompletionRequestUserMessage),
842 Assistant(ChatCompletionRequestAssistantMessage),
843 Tool(ChatCompletionRequestToolMessage),
844 Function(ChatCompletionRequestFunctionMessage),
845}
846
847impl<'de> Deserialize<'de> for ChatCompletionRequestMessage {
848 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
849 where
850 D: serde::Deserializer<'de>,
851 {
852 use serde::de::Error;
853
854 #[derive(Deserialize)]
855 struct ForbidToolsAndPartial<T> {
856 tools: Option<serde::de::IgnoredAny>,
857 partial: Option<serde::de::IgnoredAny>,
858 #[serde(flatten)]
859 message: T,
860 }
861
862 #[derive(Deserialize)]
863 struct ForbidTools<T> {
864 tools: Option<serde::de::IgnoredAny>,
865 #[serde(flatten)]
866 message: T,
867 }
868
869 #[derive(Deserialize)]
870 struct ForbidPartial<T> {
871 partial: Option<serde::de::IgnoredAny>,
872 #[serde(flatten)]
873 message: T,
874 }
875
876 #[derive(Deserialize)]
877 #[serde(tag = "role")]
878 #[serde(rename_all = "lowercase")]
879 enum Wire {
880 Developer(ForbidToolsAndPartial<ChatCompletionRequestDeveloperMessage>),
881 System(ForbidPartial<ChatCompletionRequestSystemMessage>),
882 User(ForbidToolsAndPartial<ChatCompletionRequestUserMessage>),
883 Assistant(ForbidTools<ChatCompletionRequestAssistantMessage>),
884 Tool(ForbidToolsAndPartial<ChatCompletionRequestToolMessage>),
885 Function(ForbidToolsAndPartial<ChatCompletionRequestFunctionMessage>),
886 }
887
888 fn reject_forbidden<E: Error>(
889 value: Option<serde::de::IgnoredAny>,
890 field: &str,
891 allowed_role: &str,
892 actual_role: &str,
893 ) -> Result<(), E> {
894 if value.is_some() {
895 return Err(E::custom(format!(
896 "`{field}` is only accepted on {allowed_role} messages, not on role {actual_role}"
897 )));
898 }
899 Ok(())
900 }
901
902 let wire = Wire::deserialize(deserializer)?;
903 Ok(match wire {
904 Wire::Developer(ForbidToolsAndPartial {
905 tools,
906 partial,
907 message,
908 }) => {
909 reject_forbidden::<D::Error>(tools, "tools", "system", "developer")?;
910 reject_forbidden::<D::Error>(partial, "partial", "assistant", "developer")?;
911 ChatCompletionRequestMessage::Developer(message)
912 }
913 Wire::System(ForbidPartial { partial, message }) => {
914 reject_forbidden::<D::Error>(partial, "partial", "assistant", "system")?;
915 ChatCompletionRequestMessage::System(message)
916 }
917 Wire::User(ForbidToolsAndPartial {
918 tools,
919 partial,
920 message,
921 }) => {
922 reject_forbidden::<D::Error>(tools, "tools", "system", "user")?;
923 reject_forbidden::<D::Error>(partial, "partial", "assistant", "user")?;
924 ChatCompletionRequestMessage::User(message)
925 }
926 Wire::Assistant(ForbidTools { tools, message }) => {
927 reject_forbidden::<D::Error>(tools, "tools", "system", "assistant")?;
928 ChatCompletionRequestMessage::Assistant(message)
929 }
930 Wire::Tool(ForbidToolsAndPartial {
931 tools,
932 partial,
933 message,
934 }) => {
935 reject_forbidden::<D::Error>(tools, "tools", "system", "tool")?;
936 reject_forbidden::<D::Error>(partial, "partial", "assistant", "tool")?;
937 ChatCompletionRequestMessage::Tool(message)
938 }
939 Wire::Function(ForbidToolsAndPartial {
940 tools,
941 partial,
942 message,
943 }) => {
944 reject_forbidden::<D::Error>(tools, "tools", "system", "function")?;
945 reject_forbidden::<D::Error>(partial, "partial", "assistant", "function")?;
946 ChatCompletionRequestMessage::Function(message)
947 }
948 })
949 }
950}
951
952pub type ServiceTierResponse = ServiceTier;
954
955#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
961pub struct ChatCompletionResponseMessage {
962 pub content: Option<ChatCompletionMessageContent>,
966 pub refusal: Option<String>,
970 #[serde(skip_serializing_if = "Option::is_none")]
971 pub tool_calls: Option<Vec<ChatCompletionMessageToolCall>>,
972 pub role: Role,
973 #[serde(skip_serializing_if = "Option::is_none")]
974 #[deprecated]
975 pub function_call: Option<FunctionCall>,
976 #[serde(skip_serializing_if = "Option::is_none")]
977 pub audio: Option<ChatCompletionResponseMessageAudio>,
978 #[serde(default, alias = "reasoning", skip_serializing_if = "Option::is_none")]
985 pub reasoning_content: Option<String>,
986}
987
988fn deserialize_null_as_false<'de, D>(deserializer: D) -> Result<bool, D::Error>
989where
990 D: serde::Deserializer<'de>,
991{
992 Option::<bool>::deserialize(deserializer).map(Option::unwrap_or_default)
993}
994
995#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq)]
1000pub struct ChatCompletionStreamOptions {
1001 #[serde(default, deserialize_with = "deserialize_null_as_false")]
1002 pub include_usage: bool,
1003 #[serde(default, deserialize_with = "deserialize_null_as_false")]
1006 pub continuous_usage_stats: bool,
1007}
1008
1009#[derive(Clone, Serialize, Default, Debug, Builder, Deserialize, PartialEq)]
1016#[builder(name = "CreateChatCompletionRequestArgs")]
1017#[builder(pattern = "mutable")]
1018#[builder(setter(into, strip_option), default)]
1019#[builder(derive(Debug))]
1020#[builder(build_fn(error = "OpenAIError"))]
1021pub struct CreateChatCompletionRequest {
1022 pub messages: Vec<ChatCompletionRequestMessage>,
1023 pub model: String,
1024 #[serde(skip_serializing_if = "Option::is_none")]
1026 pub mm_processor_kwargs: Option<serde_json::Value>,
1027 #[serde(skip_serializing_if = "Option::is_none")]
1028 pub store: Option<bool>,
1029 #[serde(skip_serializing_if = "Option::is_none")]
1030 pub reasoning_effort: Option<ReasoningEffort>,
1031 #[serde(skip_serializing_if = "Option::is_none")]
1032 pub metadata: Option<serde_json::Value>,
1033 #[serde(skip_serializing_if = "Option::is_none")]
1034 pub frequency_penalty: Option<f32>,
1035 #[serde(skip_serializing_if = "Option::is_none")]
1036 pub logit_bias: Option<std::collections::HashMap<String, serde_json::Value>>,
1037 #[serde(skip_serializing_if = "Option::is_none")]
1038 pub logprobs: Option<bool>,
1039 #[serde(skip_serializing_if = "Option::is_none")]
1040 pub top_logprobs: Option<u8>,
1041 #[deprecated]
1042 #[serde(skip_serializing_if = "Option::is_none")]
1043 pub max_tokens: Option<u32>,
1044 #[serde(skip_serializing_if = "Option::is_none")]
1045 pub max_completion_tokens: Option<u32>,
1046 #[serde(skip_serializing_if = "Option::is_none")]
1047 pub n: Option<u8>,
1048 #[serde(skip_serializing_if = "Option::is_none")]
1049 pub modalities: Option<Vec<async_openai::types::chat::ResponseModalities>>,
1050 #[serde(skip_serializing_if = "Option::is_none")]
1051 pub prediction: Option<PredictionContent>,
1052 #[serde(skip_serializing_if = "Option::is_none")]
1053 pub audio: Option<ChatCompletionAudio>,
1054 #[serde(skip_serializing_if = "Option::is_none")]
1055 pub presence_penalty: Option<f32>,
1056 #[serde(skip_serializing_if = "Option::is_none")]
1057 pub response_format: Option<ResponseFormat>,
1058 #[serde(skip_serializing_if = "Option::is_none")]
1059 pub seed: Option<i64>,
1060 #[serde(skip_serializing_if = "Option::is_none")]
1061 pub service_tier: Option<ServiceTier>,
1062 #[serde(skip_serializing_if = "Option::is_none")]
1063 pub stop: Option<Stop>,
1064 #[serde(default, skip_serializing_if = "Option::is_none")]
1065 pub stream: Option<bool>,
1066 #[serde(skip_serializing_if = "Option::is_none")]
1067 pub stream_options: Option<ChatCompletionStreamOptions>,
1068 #[serde(skip_serializing_if = "Option::is_none")]
1069 pub temperature: Option<f32>,
1070 #[serde(skip_serializing_if = "Option::is_none")]
1071 pub top_p: Option<f32>,
1072 #[serde(skip_serializing_if = "Option::is_none")]
1073 pub tools: Option<Vec<ChatCompletionTool>>,
1074 #[serde(skip_serializing_if = "Option::is_none")]
1075 pub tool_choice: Option<ChatCompletionToolChoiceOption>,
1076 #[serde(skip_serializing_if = "Option::is_none")]
1077 pub parallel_tool_calls: Option<bool>,
1078 #[serde(skip_serializing_if = "Option::is_none")]
1079 pub user: Option<String>,
1080 #[serde(skip_serializing_if = "Option::is_none")]
1089 pub prompt_cache_key: Option<String>,
1090 #[deprecated]
1091 #[serde(skip_serializing_if = "Option::is_none")]
1092 pub function_call: Option<ChatCompletionFunctionCall>,
1093 #[deprecated]
1094 #[serde(skip_serializing_if = "Option::is_none")]
1095 pub functions: Option<Vec<ChatCompletionFunctions>>,
1096 #[serde(skip_serializing_if = "Option::is_none")]
1097 pub web_search_options: Option<WebSearchOptions>,
1098}
1099
1100impl CreateChatCompletionRequest {
1101 pub fn dynamic_system_tools(&self) -> impl Iterator<Item = &serde_json::Value> {
1112 self.messages
1113 .iter()
1114 .filter_map(|message| match message {
1115 ChatCompletionRequestMessage::System(system) => system.tools.as_deref(),
1116 _ => None,
1117 })
1118 .flatten()
1119 }
1120
1121 pub fn has_effective_tools(&self) -> bool {
1128 self.tools.as_ref().is_some_and(|tools| !tools.is_empty())
1129 || self.dynamic_system_tools().next().is_some()
1130 }
1131
1132 pub fn effective_tool_names(&self) -> impl Iterator<Item = &str> {
1135 self.tools
1136 .iter()
1137 .flatten()
1138 .map(|tool| tool.function.name.as_str())
1139 .chain(self.dynamic_system_tools().filter_map(dynamic_tool_name))
1140 }
1141
1142 pub fn effective_tool_contains(&self, name: &str) -> bool {
1147 self.effective_tool_names().any(|tool| tool == name)
1148 }
1149}
1150
1151pub fn dynamic_tool_name(tool: &serde_json::Value) -> Option<&str> {
1158 tool.get("function")
1159 .and_then(serde_json::Value::as_object)
1160 .and_then(|function| function.get("name"))
1161 .or_else(|| tool.get("name"))
1162 .and_then(serde_json::Value::as_str)
1163}
1164
1165#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
1169pub struct ChatChoice {
1170 pub index: u32,
1171 pub message: ChatCompletionResponseMessage,
1172 pub finish_reason: Option<FinishReason>,
1173 pub logprobs: Option<ChatChoiceLogprobs>,
1174}
1175
1176fn serialize_usage_omitting_absent<S>(
1186 usage: &Option<CompletionUsage>,
1187 serializer: S,
1188) -> Result<S::Ok, S::Error>
1189where
1190 S: serde::Serializer,
1191{
1192 #[derive(Serialize)]
1193 struct PromptDetailsShadow {
1194 #[serde(skip_serializing_if = "Option::is_none")]
1195 audio_tokens: Option<u32>,
1196 #[serde(skip_serializing_if = "Option::is_none")]
1197 cached_tokens: Option<u32>,
1198 }
1199
1200 #[derive(Serialize)]
1201 struct CompletionDetailsShadow {
1202 #[serde(skip_serializing_if = "Option::is_none")]
1203 accepted_prediction_tokens: Option<u32>,
1204 #[serde(skip_serializing_if = "Option::is_none")]
1205 audio_tokens: Option<u32>,
1206 #[serde(skip_serializing_if = "Option::is_none")]
1207 reasoning_tokens: Option<u32>,
1208 #[serde(skip_serializing_if = "Option::is_none")]
1209 rejected_prediction_tokens: Option<u32>,
1210 }
1211
1212 #[derive(Serialize)]
1213 struct UsageShadow {
1214 prompt_tokens: u32,
1215 completion_tokens: u32,
1216 total_tokens: u32,
1217 #[serde(skip_serializing_if = "Option::is_none")]
1218 prompt_tokens_details: Option<PromptDetailsShadow>,
1219 #[serde(skip_serializing_if = "Option::is_none")]
1220 completion_tokens_details: Option<CompletionDetailsShadow>,
1221 }
1222
1223 match usage {
1224 None => serializer.serialize_none(),
1225 Some(u) => UsageShadow {
1226 prompt_tokens: u.prompt_tokens,
1227 completion_tokens: u.completion_tokens,
1228 total_tokens: u.total_tokens,
1229 prompt_tokens_details: u
1230 .prompt_tokens_details
1231 .as_ref()
1232 .map(|d| PromptDetailsShadow {
1233 audio_tokens: d.audio_tokens,
1234 cached_tokens: d.cached_tokens,
1235 }),
1236 completion_tokens_details: u.completion_tokens_details.as_ref().map(|d| {
1237 CompletionDetailsShadow {
1238 accepted_prediction_tokens: d.accepted_prediction_tokens,
1239 audio_tokens: d.audio_tokens,
1240 reasoning_tokens: d.reasoning_tokens,
1241 rejected_prediction_tokens: d.rejected_prediction_tokens,
1242 }
1243 }),
1244 }
1245 .serialize(serializer),
1246 }
1247}
1248
1249#[derive(Debug, Deserialize, Clone, PartialEq, Serialize)]
1256pub struct CreateChatCompletionResponse {
1257 pub id: String,
1258 pub choices: Vec<ChatChoice>,
1259 pub created: u32,
1260 pub model: String,
1261 #[serde(skip_serializing_if = "Option::is_none")]
1262 pub service_tier: Option<ServiceTierResponse>,
1263 #[serde(skip_serializing_if = "Option::is_none")]
1264 pub system_fingerprint: Option<String>,
1265 pub object: String,
1266 #[serde(
1267 skip_serializing_if = "Option::is_none",
1268 serialize_with = "serialize_usage_omitting_absent"
1269 )]
1270 pub usage: Option<CompletionUsage>,
1271}
1272
1273pub type ChatCompletionResponseStream =
1274 Pin<Box<dyn Stream<Item = Result<CreateChatCompletionStreamResponse, OpenAIError>> + Send>>;
1275
1276#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
1282pub struct ChatCompletionStreamResponseDelta {
1283 #[serde(skip_serializing_if = "Option::is_none")]
1284 pub content: Option<ChatCompletionMessageContent>,
1285 #[serde(skip_serializing_if = "Option::is_none")]
1286 pub function_call: Option<ChatCompletionStreamResponseDeltaFunctionCall>,
1287 #[serde(skip_serializing_if = "Option::is_none")]
1288 pub tool_calls: Option<Vec<ChatCompletionMessageToolCallChunk>>,
1289 #[serde(skip_serializing_if = "Option::is_none")]
1290 pub role: Option<Role>,
1291 #[serde(skip_serializing_if = "Option::is_none")]
1292 pub refusal: Option<String>,
1293 #[serde(default, alias = "reasoning", skip_serializing_if = "Option::is_none")]
1299 pub reasoning_content: Option<String>,
1300}
1301
1302#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
1303pub struct ChatCompletionStreamResponseDeltaFunctionCall {
1304 #[serde(skip_serializing_if = "Option::is_none")]
1305 pub name: Option<String>,
1306 #[serde(
1307 default,
1308 deserialize_with = "deserialize_arguments_opt",
1309 skip_serializing_if = "Option::is_none"
1310 )]
1311 pub arguments: Option<String>,
1312}
1313
1314#[derive(Debug, Deserialize, Clone, PartialEq, Serialize)]
1316pub struct ChatChoiceStream {
1317 pub index: u32,
1318 pub delta: ChatCompletionStreamResponseDelta,
1319 pub finish_reason: Option<FinishReason>,
1320 pub logprobs: Option<ChatChoiceLogprobs>,
1321}
1322
1323#[derive(Debug, Deserialize, Clone, PartialEq, Serialize)]
1331pub struct CreateChatCompletionStreamResponse {
1332 pub id: String,
1333 pub choices: Vec<ChatChoiceStream>,
1334 pub created: u32,
1335 pub model: String,
1336 #[serde(skip_serializing_if = "Option::is_none")]
1337 pub service_tier: Option<ServiceTierResponse>,
1338 #[serde(skip_serializing_if = "Option::is_none")]
1339 pub system_fingerprint: Option<String>,
1340 pub object: String,
1341 #[serde(
1342 skip_serializing_if = "Option::is_none",
1343 serialize_with = "serialize_usage_omitting_absent"
1344 )]
1345 pub usage: Option<CompletionUsage>,
1346}
1347
1348#[cfg(test)]
1349mod tests {
1350 use super::*;
1351
1352 #[test]
1353 fn stream_options_default_missing_and_null_flags_to_false() {
1354 for (payload, expected) in [
1355 (serde_json::json!({}), (false, false)),
1356 (
1357 serde_json::json!({
1358 "include_usage": null,
1359 "continuous_usage_stats": true,
1360 }),
1361 (false, true),
1362 ),
1363 (
1364 serde_json::json!({
1365 "include_usage": true,
1366 "continuous_usage_stats": null,
1367 }),
1368 (true, false),
1369 ),
1370 ] {
1371 let options: ChatCompletionStreamOptions = serde_json::from_value(payload).unwrap();
1372 assert_eq!(
1373 (options.include_usage, options.continuous_usage_stats),
1374 expected
1375 );
1376 }
1377 }
1378
1379 #[test]
1380 fn stream_options_preserve_boolean_wire_shape_and_reject_other_types() {
1381 let options: ChatCompletionStreamOptions = serde_json::from_value(serde_json::json!({
1382 "include_usage": true,
1383 "continuous_usage_stats": false,
1384 }))
1385 .unwrap();
1386 assert!(options.include_usage);
1387 assert!(!options.continuous_usage_stats);
1388 assert_eq!(
1389 serde_json::to_value(options).unwrap(),
1390 serde_json::json!({
1391 "include_usage": true,
1392 "continuous_usage_stats": false,
1393 })
1394 );
1395
1396 for payload in [
1397 serde_json::json!({"include_usage": "true"}),
1398 serde_json::json!({"continuous_usage_stats": 1}),
1399 ] {
1400 serde_json::from_value::<ChatCompletionStreamOptions>(payload).unwrap_err();
1401 }
1402 }
1403
1404 #[test]
1405 fn stop_accepts_token_id_array() {
1406 let stop: Stop = serde_json::from_value(serde_json::json!([32, 34])).unwrap();
1407
1408 assert_eq!(stop, Stop::TokenIdArray(vec![32, 34]));
1409 }
1410
1411 #[test]
1412 fn stop_accepts_string_and_string_array() {
1413 let stop: Stop = serde_json::from_value(serde_json::json!(" The")).unwrap();
1414
1415 assert_eq!(stop, Stop::String(" The".to_string()));
1416
1417 let stop: Stop = serde_json::from_value(serde_json::json!(["A", "B"])).unwrap();
1418
1419 assert_eq!(
1420 stop,
1421 Stop::StringArray(vec!["A".to_string(), "B".to_string()])
1422 );
1423 }
1424
1425 #[test]
1426 fn stop_token_id_display_string_remains_string_stop() {
1427 let stop: Stop = serde_json::from_value(serde_json::json!("token_id:576")).unwrap();
1428
1429 assert_eq!(stop, Stop::String("token_id:576".to_string()));
1430
1431 let stop: Stop = serde_json::from_value(serde_json::json!(["token_id:576"])).unwrap();
1432
1433 assert_eq!(stop, Stop::StringArray(vec!["token_id:576".to_string()]));
1434 }
1435
1436 #[test]
1437 fn stop_rejects_single_token_id() {
1438 let result = serde_json::from_value::<Stop>(serde_json::json!(576));
1439
1440 assert!(result.is_err());
1441 }
1442
1443 #[test]
1444 fn stop_converts_from_upstream_stop_configuration() {
1445 let upstream =
1446 async_openai::types::chat::StopConfiguration::StringArray(vec!["END".to_string()]);
1447
1448 assert_eq!(
1449 Stop::from(upstream),
1450 Stop::StringArray(vec!["END".to_string()])
1451 );
1452 }
1453
1454 #[test]
1455 fn request_builder_accepts_upstream_reasoning_effort() {
1456 let request = CreateChatCompletionRequestArgs::default()
1457 .reasoning_effort(async_openai::types::chat::ReasoningEffort::High)
1458 .build()
1459 .unwrap();
1460
1461 assert_eq!(request.reasoning_effort, Some(ReasoningEffort::High));
1462 }
1463
1464 #[test]
1465 fn tool_call_defaults_type_on_deserialize() {
1466 let tool_call: ChatCompletionMessageToolCall = serde_json::from_value(serde_json::json!({
1467 "id": "call_123",
1468 "function": {
1469 "name": "get_weather",
1470 "arguments": "{\"location\":\"SF\"}"
1471 }
1472 }))
1473 .unwrap();
1474
1475 assert_eq!(tool_call.r#type, FunctionType::Function);
1476 }
1477
1478 #[test]
1479 fn tool_call_serializes_type_for_wire_compat() {
1480 let tool_call = ChatCompletionMessageToolCall {
1481 id: "call_123".into(),
1482 r#type: FunctionType::Function,
1483 function: FunctionCall {
1484 name: "get_weather".into(),
1485 arguments: "{\"location\":\"SF\"}".into(),
1486 },
1487 };
1488
1489 let json = serde_json::to_value(tool_call).unwrap();
1490 assert_eq!(json["type"], "function");
1491 }
1492
1493 #[test]
1496 fn function_call_accepts_string_arguments() {
1497 let fc: FunctionCall = serde_json::from_value(serde_json::json!({
1498 "name": "get_weather",
1499 "arguments": "{\"location\":\"SF\"}"
1500 }))
1501 .unwrap();
1502 assert_eq!(fc.arguments, "{\"location\":\"SF\"}");
1503 }
1504
1505 #[test]
1506 fn function_call_accepts_dict_arguments() {
1507 let fc: FunctionCall = serde_json::from_value(serde_json::json!({
1508 "name": "get_weather",
1509 "arguments": {"location": "SF"}
1510 }))
1511 .unwrap();
1512 assert_eq!(fc.arguments, "{\"location\":\"SF\"}");
1513 }
1514
1515 #[test]
1516 fn function_call_rejects_integer_arguments() {
1517 let result = serde_json::from_value::<FunctionCall>(serde_json::json!({
1518 "name": "f",
1519 "arguments": 42
1520 }));
1521 assert!(result.is_err());
1522 }
1523
1524 #[test]
1525 fn function_call_rejects_boolean_arguments() {
1526 let result = serde_json::from_value::<FunctionCall>(serde_json::json!({
1527 "name": "f",
1528 "arguments": true
1529 }));
1530 assert!(result.is_err());
1531 }
1532
1533 #[test]
1534 fn function_call_rejects_null_arguments() {
1535 let result = serde_json::from_value::<FunctionCall>(serde_json::json!({
1536 "name": "f",
1537 "arguments": null
1538 }));
1539 assert!(result.is_err());
1540 }
1541
1542 #[test]
1543 fn function_call_rejects_array_arguments() {
1544 let result = serde_json::from_value::<FunctionCall>(serde_json::json!({
1545 "name": "f",
1546 "arguments": [1, 2, 3]
1547 }));
1548 assert!(result.is_err());
1549 }
1550
1551 #[test]
1552 fn function_call_stream_null_arguments_produces_none() {
1553 let fcs: FunctionCallStream = serde_json::from_value(serde_json::json!({
1554 "name": "f",
1555 "arguments": null
1556 }))
1557 .unwrap();
1558 assert_eq!(fcs.arguments, None);
1559 }
1560
1561 #[test]
1562 fn function_call_stream_rejects_integer_arguments() {
1563 let result = serde_json::from_value::<FunctionCallStream>(serde_json::json!({
1564 "name": "f",
1565 "arguments": 42
1566 }));
1567 assert!(result.is_err());
1568 }
1569
1570 #[test]
1571 fn function_call_stream_rejects_boolean_arguments() {
1572 let result = serde_json::from_value::<FunctionCallStream>(serde_json::json!({
1573 "name": "f",
1574 "arguments": true
1575 }));
1576 assert!(result.is_err());
1577 }
1578
1579 #[test]
1580 fn function_call_stream_accepts_dict_arguments() {
1581 let fcs: FunctionCallStream = serde_json::from_value(serde_json::json!({
1582 "name": "get_weather",
1583 "arguments": {"location": "SF"}
1584 }))
1585 .unwrap();
1586 assert_eq!(fcs.arguments.as_deref(), Some("{\"location\":\"SF\"}"));
1587 }
1588
1589 #[test]
1590 fn function_call_stream_accepts_null_arguments() {
1591 let fcs: FunctionCallStream = serde_json::from_value(serde_json::json!({
1592 "name": "get_weather"
1593 }))
1594 .unwrap();
1595 assert_eq!(fcs.arguments, None);
1596 }
1597
1598 #[test]
1599 fn tool_call_with_dict_arguments_roundtrip() {
1600 let tc: ChatCompletionMessageToolCall = serde_json::from_value(serde_json::json!({
1601 "id": "call_abc",
1602 "type": "function",
1603 "function": {
1604 "name": "search",
1605 "arguments": {"query": "hello", "limit": 10}
1606 }
1607 }))
1608 .unwrap();
1609 let parsed: serde_json::Value = serde_json::from_str(&tc.function.arguments).unwrap();
1611 assert_eq!(parsed, serde_json::json!({"query": "hello", "limit": 10}));
1612 let json = serde_json::to_value(&tc).unwrap();
1614 assert!(json["function"]["arguments"].is_string());
1615 }
1616
1617 #[test]
1618 fn stream_delta_function_call_accepts_dict_arguments() {
1619 let delta: ChatCompletionStreamResponseDeltaFunctionCall =
1620 serde_json::from_value(serde_json::json!({
1621 "name": "get_weather",
1622 "arguments": {"location": "SF"}
1623 }))
1624 .unwrap();
1625 assert_eq!(delta.arguments.as_deref(), Some("{\"location\":\"SF\"}"));
1626 }
1627
1628 fn parse_content_part(json: serde_json::Value) -> ChatCompletionRequestUserMessageContentPart {
1629 serde_json::from_value(json).expect("content part deserialization failed")
1630 }
1631
1632 #[test]
1633 fn image_url_url_and_top_level_uuid() {
1634 let part = parse_content_part(serde_json::json!({
1635 "type": "image_url",
1636 "image_url": {"url": "https://x.example/y.png"},
1637 "uuid": "image-123"
1638 }));
1639
1640 match part {
1641 ChatCompletionRequestUserMessageContentPart::ImageUrl(part) => {
1642 assert_eq!(part.uuid.as_deref(), Some("image-123"));
1643 assert_eq!(
1644 part.image_url.as_ref().map(|image| image.url.as_str()),
1645 Some("https://x.example/y.png")
1646 );
1647 }
1648 _ => panic!("expected image_url part"),
1649 }
1650 }
1651
1652 #[test]
1653 fn image_url_null_and_top_level_uuid() {
1654 let part = parse_content_part(serde_json::json!({
1655 "type": "image_url",
1656 "image_url": null,
1657 "uuid": "sku-1234-a"
1658 }));
1659
1660 match part {
1661 ChatCompletionRequestUserMessageContentPart::ImageUrl(part) => {
1662 assert!(part.image_url.is_none());
1663 assert_eq!(part.uuid.as_deref(), Some("sku-1234-a"));
1664 }
1665 _ => panic!("expected image_url part"),
1666 }
1667 }
1668
1669 #[test]
1670 fn empty_media_urls_deserialize_as_uuid_only() {
1671 for (part_type, media_field, uuid) in [
1672 ("image_url", "image_url", "image-cache-key"),
1673 ("video_url", "video_url", "video-cache-key"),
1674 ("audio_url", "audio_url", "audio-cache-key"),
1675 ] {
1676 let part = parse_content_part(serde_json::json!({
1677 "type": part_type,
1678 (media_field): {"url": ""},
1679 "uuid": uuid
1680 }));
1681 let json = serde_json::to_value(part).unwrap();
1682
1683 assert!(json[media_field].is_null());
1684 assert_eq!(json["uuid"], uuid);
1685 }
1686 }
1687
1688 #[test]
1689 fn image_url_null_without_uuid_deserializes_for_use_site_validation() {
1690 let part = parse_content_part(serde_json::json!({
1691 "type": "image_url",
1692 "image_url": null
1693 }));
1694
1695 match part {
1696 ChatCompletionRequestUserMessageContentPart::ImageUrl(part) => {
1697 assert!(part.image_url.is_none());
1698 assert!(part.uuid.is_none());
1699 }
1700 _ => panic!("expected image_url part"),
1701 }
1702 }
1703
1704 #[test]
1705 fn image_url_serialize_uuid_only_uses_null_image_url() {
1706 let part = ChatCompletionRequestMessageContentPartImage {
1707 image_url: None,
1708 uuid: Some("image-123".to_string()),
1709 };
1710 let json = serde_json::to_value(part).unwrap();
1711
1712 assert!(json["image_url"].is_null());
1713 assert_eq!(json["uuid"], "image-123");
1714 }
1715
1716 #[test]
1717 fn cached_media_builders_allow_omitting_urls() {
1718 let image = ChatCompletionRequestMessageContentPartImageArgs::default()
1719 .uuid("image-123")
1720 .build()
1721 .unwrap();
1722 let video = ChatCompletionRequestMessageContentPartVideoArgs::default()
1723 .uuid("video-123")
1724 .build()
1725 .unwrap();
1726 let audio = ChatCompletionRequestMessageContentPartAudioUrlArgs::default()
1727 .uuid("audio-123")
1728 .build()
1729 .unwrap();
1730
1731 let image_json = serde_json::to_value(image).unwrap();
1732 let video_json = serde_json::to_value(video).unwrap();
1733 let audio_json = serde_json::to_value(audio).unwrap();
1734 assert!(image_json["image_url"].is_null());
1735 assert!(video_json["video_url"].is_null());
1736 assert!(audio_json["audio_url"].is_null());
1737 }
1738
1739 #[test]
1740 fn image_url_uuid_accepts_opaque_string() {
1741 let part = parse_content_part(serde_json::json!({
1742 "type": "image_url",
1743 "image_url": {"url": "https://x.example/y.png"},
1744 "uuid": "img-ac3921de680bb217"
1745 }));
1746
1747 match part {
1748 ChatCompletionRequestUserMessageContentPart::ImageUrl(part) => {
1749 assert_eq!(part.uuid.as_deref(), Some("img-ac3921de680bb217"));
1750 }
1751 _ => panic!("expected image_url part"),
1752 }
1753 }
1754
1755 #[test]
1756 fn url_conversions_preserve_required_urls() {
1757 let image: ImageUrl = "https://x.example/image.png".into();
1758 let video: VideoUrl = "https://x.example/video.mp4".into();
1759 let audio: AudioUrl = "https://x.example/audio.wav".into();
1760
1761 assert_eq!(image.url.as_str(), "https://x.example/image.png");
1762 assert_eq!(video.url.as_str(), "https://x.example/video.mp4");
1763 assert_eq!(audio.url.as_str(), "https://x.example/audio.wav");
1764 }
1765
1766 #[test]
1767 fn invalid_media_urls_remain_rejected() {
1768 for (part_type, media_field) in [
1769 ("image_url", "image_url"),
1770 ("video_url", "video_url"),
1771 ("audio_url", "audio_url"),
1772 ] {
1773 let result = serde_json::from_value::<ChatCompletionRequestUserMessageContentPart>(
1774 serde_json::json!({
1775 "type": part_type,
1776 (media_field): {"url": "not a url"},
1777 "uuid": "cache-key"
1778 }),
1779 );
1780
1781 assert!(result.is_err(), "{part_type} accepted an invalid URL");
1782 }
1783 }
1784
1785 #[test]
1786 fn legacy_nested_media_uuids_remain_accepted() {
1787 let legacy_uuid = "92b888ad-e64a-478f-b688-5091e16544e3";
1788
1789 for (part_type, media_field, url) in [
1790 ("image_url", "image_url", "https://x.example/image.png"),
1791 ("video_url", "video_url", "https://x.example/video.mp4"),
1792 ("audio_url", "audio_url", "https://x.example/audio.wav"),
1793 ] {
1794 let part = parse_content_part(serde_json::json!({
1795 "type": part_type,
1796 (media_field): {"url": url, "uuid": legacy_uuid}
1797 }));
1798 let json = serde_json::to_value(part).unwrap();
1799
1800 assert_eq!(json[media_field]["url"], url);
1801 assert_eq!(json[media_field]["uuid"], legacy_uuid);
1802 assert!(json.get("uuid").is_none());
1803 }
1804 }
1805
1806 #[test]
1807 fn video_url_null_and_top_level_uuid() {
1808 let part = parse_content_part(serde_json::json!({
1809 "type": "video_url",
1810 "video_url": null,
1811 "uuid": "video-cache-key"
1812 }));
1813
1814 match part {
1815 ChatCompletionRequestUserMessageContentPart::VideoUrl(part) => {
1816 assert!(part.video_url.is_none());
1817 assert_eq!(part.uuid.as_deref(), Some("video-cache-key"));
1818 }
1819 _ => panic!("expected video_url part"),
1820 }
1821 }
1822
1823 #[test]
1824 fn audio_url_null_and_top_level_uuid() {
1825 let part = parse_content_part(serde_json::json!({
1826 "type": "audio_url",
1827 "audio_url": null,
1828 "uuid": "audio-cache-key"
1829 }));
1830
1831 match part {
1832 ChatCompletionRequestUserMessageContentPart::AudioUrl(part) => {
1833 assert!(part.audio_url.is_none());
1834 assert_eq!(part.uuid.as_deref(), Some("audio-cache-key"));
1835 }
1836 _ => panic!("expected audio_url part"),
1837 }
1838 }
1839
1840 #[test]
1841 fn message_content_array_preserves_uuid_alignment() {
1842 let payload = serde_json::json!({
1843 "role": "user",
1844 "content": [
1845 {"type": "text", "text": "describe these"},
1846 {
1847 "type": "image_url",
1848 "image_url": {"url": "https://x.example/img1.png"},
1849 "uuid": "image-1"
1850 },
1851 {"type": "image_url", "image_url": null, "uuid": "image-1"}
1852 ]
1853 });
1854 let message: ChatCompletionRequestUserMessage = serde_json::from_value(payload).unwrap();
1855 let ChatCompletionRequestUserMessageContent::Array(parts) = message.content else {
1856 panic!("expected content array");
1857 };
1858
1859 assert_eq!(parts.len(), 3);
1860 match &parts[1] {
1861 ChatCompletionRequestUserMessageContentPart::ImageUrl(part) => {
1862 assert!(
1863 part.image_url
1864 .as_ref()
1865 .map(|image| image.url.as_str())
1866 .is_some()
1867 );
1868 assert_eq!(part.uuid.as_deref(), Some("image-1"));
1869 }
1870 _ => panic!("parts[1] should be image_url"),
1871 }
1872 match &parts[2] {
1873 ChatCompletionRequestUserMessageContentPart::ImageUrl(part) => {
1874 assert!(part.image_url.is_none());
1875 assert_eq!(part.uuid.as_deref(), Some("image-1"));
1876 }
1877 _ => panic!("parts[2] should be image_url"),
1878 }
1879 }
1880
1881 #[test]
1882 fn tool_message_accepts_media_content() {
1883 let message: ChatCompletionRequestMessage = serde_json::from_value(serde_json::json!({
1884 "role": "tool",
1885 "tool_call_id": "call_media",
1886 "content": [
1887 {"type": "text", "text": "Screenshot captured"},
1888 {
1889 "type": "image_url",
1890 "image_url": {
1891 "url": "data:image/png;base64,aGVsbG8="
1892 }
1893 },
1894 {
1895 "type": "video_url",
1896 "video_url": {
1897 "url": "https://example.com/clip.mp4"
1898 }
1899 },
1900 {
1901 "type": "audio_url",
1902 "audio_url": {
1903 "url": "https://example.com/audio.wav"
1904 }
1905 }
1906 ]
1907 }))
1908 .unwrap();
1909
1910 let ChatCompletionRequestMessage::Tool(tool) = message else {
1911 panic!("expected tool message");
1912 };
1913 let ChatCompletionRequestToolMessageContent::Array(parts) = tool.content else {
1914 panic!("expected array content");
1915 };
1916 assert!(matches!(
1917 parts[1],
1918 ChatCompletionRequestToolMessageContentPart::ImageUrl(_)
1919 ));
1920 assert!(matches!(
1921 parts[2],
1922 ChatCompletionRequestToolMessageContentPart::VideoUrl(_)
1923 ));
1924 assert!(matches!(
1925 parts[3],
1926 ChatCompletionRequestToolMessageContentPart::AudioUrl(_)
1927 ));
1928 }
1929
1930 #[test]
1931 fn chat_logprob_serializes_token_id_when_present() {
1932 let logprob = ChatCompletionTokenLogprob {
1933 token: " hello".into(),
1934 logprob: -0.12,
1935 token_id: Some(123),
1936 bytes: Some(vec![32, 104, 101, 108, 108, 111]),
1937 top_logprobs: vec![],
1938 };
1939
1940 let json = serde_json::to_value(logprob).unwrap();
1941
1942 assert_eq!(json["token_id"], 123);
1943 }
1944
1945 #[test]
1946 fn chat_logprob_deserializes_optional_fields() {
1947 let choice_logprobs: ChatChoiceLogprobs = serde_json::from_value(serde_json::json!({
1948 "content": [{
1949 "token": " hello",
1950 "logprob": -0.12,
1951 "top_logprobs": []
1952 }]
1953 }))
1954 .unwrap();
1955 let token_logprob: ChatCompletionTokenLogprob = serde_json::from_value(serde_json::json!({
1956 "token": " hello",
1957 "logprob": -0.12,
1958 "token_id": 123,
1959 "bytes": [32, 104, 101, 108, 108, 111],
1960 "top_logprobs": []
1961 }))
1962 .unwrap();
1963
1964 assert_eq!(choice_logprobs.content.as_ref().unwrap()[0].token_id, None);
1965 assert!(choice_logprobs.refusal.is_none());
1966 assert_eq!(token_logprob.token_id, Some(123));
1967 assert_eq!(token_logprob.bytes, Some(vec![32, 104, 101, 108, 108, 111]));
1968 }
1969
1970 #[test]
1971 fn chat_logprob_preserves_nullable_fields() {
1972 let choice_logprobs = ChatChoiceLogprobs {
1973 content: None,
1974 refusal: None,
1975 };
1976 let token_logprob = ChatCompletionTokenLogprob {
1977 token: " hello".into(),
1978 logprob: -0.12,
1979 token_id: None,
1980 bytes: None,
1981 top_logprobs: vec![],
1982 };
1983
1984 let choice_json = serde_json::to_value(choice_logprobs).unwrap();
1985 let token_json = serde_json::to_value(token_logprob).unwrap();
1986
1987 assert_eq!(choice_json["content"], serde_json::Value::Null);
1988 assert_eq!(choice_json["refusal"], serde_json::Value::Null);
1989 assert!(token_json.get("token_id").is_none());
1990 assert_eq!(token_json["bytes"], serde_json::Value::Null);
1991 }
1992
1993 #[test]
1994 #[allow(deprecated)]
1995 fn chat_response_omits_absent_optional_fields() {
1996 let response = CreateChatCompletionResponse {
1997 id: "chatcmpl_dummy".into(),
1998 choices: vec![ChatChoice {
1999 index: 0,
2000 message: ChatCompletionResponseMessage {
2001 content: Some(ChatCompletionMessageContent::Text("hello".into())),
2002 refusal: None,
2003 tool_calls: None,
2004 role: Role::Assistant,
2005 function_call: None,
2006 audio: None,
2007 reasoning_content: None,
2008 },
2009 finish_reason: Some(FinishReason::Stop),
2010 logprobs: None,
2011 }],
2012 created: 0,
2013 model: "dummy-model".into(),
2014 service_tier: None,
2015 system_fingerprint: None,
2016 object: "chat.completion".into(),
2017 usage: None,
2018 };
2019
2020 let json = serde_json::to_value(response).unwrap();
2021
2022 for absent in ["usage", "service_tier", "system_fingerprint"] {
2023 assert!(json.get(absent).is_none(), "{absent} should be omitted");
2024 }
2025 let choice = &json["choices"][0];
2026 assert_eq!(choice["finish_reason"], "stop");
2027 assert_eq!(choice["logprobs"], serde_json::Value::Null);
2028 let message = &choice["message"];
2029 assert_eq!(message["refusal"], serde_json::Value::Null);
2030 for absent in ["tool_calls", "function_call", "audio", "reasoning_content"] {
2031 assert!(
2032 message.get(absent).is_none(),
2033 "message.{absent} should be omitted"
2034 );
2035 }
2036 }
2037
2038 #[test]
2039 fn stream_response_omits_absent_optional_fields() {
2040 let chunk = CreateChatCompletionStreamResponse {
2041 id: "chatcmpl_dummy".into(),
2042 choices: vec![ChatChoiceStream {
2043 index: 0,
2044 delta: ChatCompletionStreamResponseDelta {
2045 content: Some(ChatCompletionMessageContent::Text("hello".into())),
2046 function_call: None,
2047 tool_calls: None,
2048 role: None,
2049 refusal: None,
2050 reasoning_content: None,
2051 },
2052 finish_reason: None,
2053 logprobs: None,
2054 }],
2055 created: 0,
2056 model: "dummy-model".into(),
2057 service_tier: None,
2058 system_fingerprint: None,
2059 object: "chat.completion.chunk".into(),
2060 usage: None,
2061 };
2062
2063 let json = serde_json::to_value(chunk).unwrap();
2064
2065 for absent in ["usage", "service_tier", "system_fingerprint"] {
2066 assert!(json.get(absent).is_none(), "{absent} should be omitted");
2067 }
2068 }
2069
2070 #[test]
2071 fn stream_tool_call_continuation_chunk_omits_absent_fields() {
2072 let chunk = ChatCompletionMessageToolCallChunk {
2073 index: 0,
2074 id: None,
2075 r#type: None,
2076 function: Some(FunctionCallStream {
2077 name: None,
2078 arguments: Some("{\"a\":".into()),
2079 }),
2080 };
2081
2082 let json = serde_json::to_value(chunk).unwrap();
2083
2084 assert!(json.get("id").is_none());
2085 assert!(json.get("type").is_none());
2086 assert!(json["function"].get("name").is_none());
2087 assert_eq!(json["function"]["arguments"], "{\"a\":");
2088 }
2089
2090 #[test]
2091 fn stream_delta_function_call_omits_absent_fields() {
2092 let function_call = ChatCompletionStreamResponseDeltaFunctionCall {
2093 name: None,
2094 arguments: Some("{}".into()),
2095 };
2096
2097 let json = serde_json::to_value(function_call).unwrap();
2098
2099 assert!(json.get("name").is_none());
2100 assert_eq!(json["arguments"], "{}");
2101 }
2102
2103 #[test]
2104 fn usage_details_omit_absent_fields() {
2105 let response = CreateChatCompletionResponse {
2106 id: "chatcmpl_dummy".into(),
2107 choices: vec![],
2108 created: 0,
2109 model: "dummy-model".into(),
2110 service_tier: None,
2111 system_fingerprint: None,
2112 object: "chat.completion".into(),
2113 usage: Some(CompletionUsage {
2114 prompt_tokens: 10,
2115 completion_tokens: 25,
2116 total_tokens: 35,
2117 prompt_tokens_details: Some(PromptTokensDetails {
2118 audio_tokens: None,
2119 cached_tokens: Some(0),
2120 }),
2121 completion_tokens_details: Some(CompletionTokensDetails {
2122 reasoning_tokens: Some(5),
2123 ..Default::default()
2124 }),
2125 }),
2126 };
2127
2128 let json = serde_json::to_value(&response).unwrap();
2129 let usage = &json["usage"];
2130
2131 assert_eq!(usage["total_tokens"], 35);
2132 assert_eq!(usage["prompt_tokens_details"]["cached_tokens"], 0);
2133 assert!(
2134 usage["prompt_tokens_details"].get("audio_tokens").is_none(),
2135 "audio_tokens should be omitted, not null"
2136 );
2137 assert_eq!(usage["completion_tokens_details"]["reasoning_tokens"], 5);
2138 for absent in [
2139 "accepted_prediction_tokens",
2140 "audio_tokens",
2141 "rejected_prediction_tokens",
2142 ] {
2143 assert!(
2144 usage["completion_tokens_details"].get(absent).is_none(),
2145 "{absent} should be omitted"
2146 );
2147 }
2148
2149 let roundtrip: CreateChatCompletionResponse = serde_json::from_value(json).unwrap();
2150 assert_eq!(roundtrip, response);
2151 }
2152
2153 #[test]
2156 fn effective_tool_set_unions_top_level_and_dynamic_system_tools() {
2157 let request: CreateChatCompletionRequest = serde_json::from_value(serde_json::json!({
2158 "model": "dummy-kimi-model",
2159 "tools": [{
2160 "type": "function",
2161 "function": {"name": "add", "parameters": {"type": "object"}}
2162 }],
2163 "messages": [
2164 {"role": "user", "content": "start"},
2165 {
2166 "role": "system",
2167 "tools": [
2168 {
2169 "type": "function",
2170 "function": {"name": "lookup", "parameters": {"type": "object"}}
2171 },
2172 {"name": "search", "parameters": {"type": "object"}},
2173 {"description": "no name, skipped"}
2174 ]
2175 },
2176 {"role": "user", "content": "continue"}
2177 ]
2178 }))
2179 .unwrap();
2180
2181 assert!(request.has_effective_tools());
2182 assert_eq!(request.dynamic_system_tools().count(), 3);
2183 assert_eq!(
2184 request.effective_tool_names().collect::<Vec<_>>(),
2185 ["add", "lookup", "search"],
2186 "top-level first, then dynamic in message order; wrapped and bare shapes both resolve"
2187 );
2188 for name in ["add", "lookup", "search"] {
2189 assert!(
2190 request.effective_tool_contains(name),
2191 "{name} should be found"
2192 );
2193 }
2194 assert!(!request.effective_tool_contains("missing"));
2195 assert!(
2196 !request.effective_tool_contains("no name, skipped"),
2197 "a description is not a name"
2198 );
2199 }
2200
2201 #[test]
2202 fn effective_tool_set_is_empty_without_any_declaration() {
2203 for payload in [
2204 serde_json::json!({
2205 "model": "m",
2206 "messages": [{"role": "user", "content": "hi"}]
2207 }),
2208 serde_json::json!({
2209 "model": "m",
2210 "tools": [],
2211 "messages": [{"role": "system", "content": "plain system text"}]
2212 }),
2213 ] {
2214 let request: CreateChatCompletionRequest = serde_json::from_value(payload).unwrap();
2215 assert!(!request.has_effective_tools());
2216 assert_eq!(request.effective_tool_names().count(), 0);
2217 assert!(!request.effective_tool_contains("anything"));
2218 }
2219 }
2220
2221 #[test]
2222 fn dynamic_system_tools_alone_count_as_effective_tools() {
2223 let request: CreateChatCompletionRequest = serde_json::from_value(serde_json::json!({
2224 "model": "dummy-kimi-model",
2225 "messages": [
2226 {"role": "system", "tools": [{"name": "lookup"}]},
2227 {"role": "user", "content": "go"}
2228 ]
2229 }))
2230 .unwrap();
2231
2232 assert!(
2233 request.tools.is_none(),
2234 "nothing was folded into top-level tools"
2235 );
2236 assert!(request.has_effective_tools());
2237 assert!(request.effective_tool_contains("lookup"));
2238 }
2239
2240 #[test]
2241 fn dynamic_tool_name_handles_wrapped_bare_and_invalid_shapes() {
2242 assert_eq!(
2243 dynamic_tool_name(&serde_json::json!({"type": "function", "function": {"name": "a"}})),
2244 Some("a")
2245 );
2246 assert_eq!(
2247 dynamic_tool_name(&serde_json::json!({"name": "b"})),
2248 Some("b")
2249 );
2250 assert_eq!(dynamic_tool_name(&serde_json::json!({"name": 7})), None);
2251 }
2252
2253 #[test]
2254 fn system_message_without_content_is_rejected_unless_it_declares_tools() {
2255 for (label, message) in [
2258 ("nothing", serde_json::json!({"role": "system"})),
2259 (
2260 "empty tools",
2261 serde_json::json!({"role": "system", "tools": []}),
2262 ),
2263 ] {
2264 let error =
2265 serde_json::from_value::<ChatCompletionRequestMessage>(message).expect_err(label);
2266 assert!(
2267 error.to_string().starts_with("missing field `content`"),
2268 "{label}: unexpected error {error}"
2269 );
2270 }
2271 }
2272
2273 #[test]
2274 fn system_message_guard_leaves_valid_shapes_alone() {
2275 for (label, message) in [
2276 (
2277 "content only",
2278 serde_json::json!({"role": "system", "content": "hi"}),
2279 ),
2280 (
2281 "content parts",
2282 serde_json::json!({"role": "system", "content": [{"type": "text", "text": "hi"}]}),
2283 ),
2284 (
2285 "tools only",
2286 serde_json::json!({"role": "system", "tools": [{"name": "lookup"}]}),
2287 ),
2288 (
2289 "content and tools (renderer decides)",
2290 serde_json::json!({"role": "system", "content": "hi", "tools": [{"name": "lookup"}]}),
2291 ),
2292 ] {
2293 let parsed: ChatCompletionRequestMessage =
2294 serde_json::from_value(message).unwrap_or_else(|e| panic!("{label}: {e}"));
2295 assert!(
2296 matches!(parsed, ChatCompletionRequestMessage::System(_)),
2297 "{label}"
2298 );
2299 }
2300 }
2301
2302 #[test]
2303 fn message_rejects_tools_and_partial_on_wrong_roles() {
2304 let tools = serde_json::json!([{"name": "lookup"}]);
2305 for (label, message, needle) in [
2306 (
2307 "tools on user",
2308 serde_json::json!({"role": "user", "content": "hi", "tools": tools}),
2309 "`tools` is only accepted on system messages, not on role user",
2310 ),
2311 (
2312 "tools on assistant",
2313 serde_json::json!({"role": "assistant", "content": "hi", "tools": tools}),
2314 "`tools` is only accepted on system messages, not on role assistant",
2315 ),
2316 (
2317 "tools on developer",
2319 serde_json::json!({"role": "developer", "content": "hi", "tools": tools}),
2320 "`tools` is only accepted on system messages, not on role developer",
2321 ),
2322 (
2323 "partial on user",
2324 serde_json::json!({"role": "user", "content": "hi", "partial": true}),
2325 "`partial` is only accepted on assistant messages, not on role user",
2326 ),
2327 (
2328 "partial on system",
2329 serde_json::json!({"role": "system", "content": "hi", "partial": false}),
2330 "`partial` is only accepted on assistant messages, not on role system",
2331 ),
2332 ] {
2333 let error = serde_json::from_value::<ChatCompletionRequestMessage>(message)
2334 .expect_err(label)
2335 .to_string();
2336 assert!(error.contains(needle), "{label}: {error}");
2337 }
2338
2339 for message in [
2340 serde_json::json!({"role": "user", "content": "hi", "tools": null}),
2341 serde_json::json!({"role": "user", "content": "hi", "partial": null}),
2342 ] {
2343 serde_json::from_value::<ChatCompletionRequestMessage>(message).unwrap();
2344 }
2345
2346 for message in [
2347 serde_json::json!({"role": "system", "tools": tools}),
2348 serde_json::json!({"role": "assistant", "content": "seed", "partial": true}),
2349 serde_json::json!({"role": "user", "content": "hi", "x_vendor": 1}),
2350 ] {
2351 serde_json::from_value::<ChatCompletionRequestMessage>(message).unwrap();
2352 }
2353 }
2354
2355 #[test]
2356 fn message_rejects_duplicate_top_level_keys() {
2357 for (label, raw) in [
2358 (
2359 "role twice",
2360 r#"{"role":"user","content":"hi","role":"system"}"#,
2361 ),
2362 (
2363 "content twice",
2364 r#"{"role":"user","content":"a","content":"b"}"#,
2365 ),
2366 ] {
2367 let error = serde_json::from_str::<ChatCompletionRequestMessage>(raw)
2368 .expect_err(label)
2369 .to_string();
2370 assert!(error.contains("duplicate field"), "{label}: {error}");
2371 }
2372 }
2373
2374 #[test]
2375 fn message_rejects_duplicate_fields_in_nested_typed_objects() {
2376 let tool_call = r#"{
2377 "role":"assistant",
2378 "content":null,
2379 "tool_calls":[{
2380 "id":"first",
2381 "id":"second",
2382 "type":"function",
2383 "function":{"name":"lookup","arguments":"{}"}
2384 }]
2385 }"#;
2386 let error = serde_json::from_str::<ChatCompletionRequestMessage>(tool_call)
2387 .unwrap_err()
2388 .to_string();
2389 assert!(error.contains("duplicate field `id`"), "{error}");
2390
2391 let content_part = r#"{
2392 "role":"user",
2393 "content":[{"type":"text","text":"first","text":"second"}]
2394 }"#;
2395 assert!(serde_json::from_str::<ChatCompletionRequestMessage>(content_part).is_err());
2396 }
2397
2398 #[test]
2399 fn default_system_message_round_trips() {
2400 let message = ChatCompletionRequestSystemMessage::default();
2401 let json = serde_json::to_value(&message).unwrap();
2402 assert_eq!(json, serde_json::json!({"content": ""}));
2403 let back: ChatCompletionRequestSystemMessage = serde_json::from_value(json).unwrap();
2404 assert_eq!(back, message);
2405
2406 let built = ChatCompletionRequestSystemMessageArgs::default()
2407 .name("ops")
2408 .build()
2409 .unwrap();
2410 let json = serde_json::to_value(&built).unwrap();
2411 assert_eq!(json, serde_json::json!({"content": "", "name": "ops"}));
2412 serde_json::from_value::<ChatCompletionRequestSystemMessage>(json).unwrap();
2413 }
2414
2415 #[test]
2416 fn system_message_guard_keeps_field_level_errors() {
2417 let error = serde_json::from_value::<ChatCompletionRequestMessage>(serde_json::json!({
2418 "role": "system",
2419 "tools": "lookup"
2420 }))
2421 .unwrap_err();
2422 assert!(
2423 !error.to_string().starts_with("missing field `content`"),
2424 "field error expected, got {error}"
2425 );
2426 }
2427
2428 #[test]
2429 fn system_message_canonicalizes_missing_content_with_tools() {
2430 let request: CreateChatCompletionRequest = serde_json::from_value(serde_json::json!({
2431 "model": "dummy-kimi-model",
2432 "messages": [
2433 {
2434 "role": "system",
2435 "tools": [
2436 {
2437 "name": "lookup",
2438 "description": "dummy lookup tool",
2439 "parameters": {
2440 "type": "object",
2441 "properties": {
2442 "query": { "type": "string" }
2443 }
2444 }
2445 }
2446 ]
2447 },
2448 {
2449 "role": "assistant",
2450 "content": "synthetic prefill",
2451 "partial": true
2452 },
2453 {
2454 "role": "user",
2455 "content": "continue"
2456 }
2457 ]
2458 }))
2459 .unwrap();
2460
2461 match &request.messages[0] {
2462 ChatCompletionRequestMessage::System(system) => {
2463 assert_eq!(
2464 system.content,
2465 ChatCompletionRequestSystemMessageContent::Text(String::new())
2466 );
2467 let tools = system.tools.as_ref().expect("tools should be present");
2468 assert_eq!(tools.len(), 1);
2469 assert_eq!(tools[0]["name"], "lookup");
2470 }
2471 other => panic!("expected system message, got {other:?}"),
2472 }
2473
2474 match &request.messages[1] {
2475 ChatCompletionRequestMessage::Assistant(assistant) => {
2476 assert_eq!(assistant.partial, Some(true));
2477 }
2478 other => panic!("expected assistant message, got {other:?}"),
2479 }
2480
2481 let message: ChatCompletionRequestMessage = serde_json::from_value(serde_json::json!({
2484 "role": "system",
2485 "content": null,
2486 "tools": [{"name": "lookup"}]
2487 }))
2488 .unwrap();
2489 let ChatCompletionRequestMessage::System(system) = &message else {
2490 panic!("expected system message");
2491 };
2492 assert_eq!(
2493 system.content,
2494 ChatCompletionRequestSystemMessageContent::Text(String::new())
2495 );
2496 assert_eq!(
2497 serde_json::to_value(message).unwrap(),
2498 serde_json::json!({
2499 "role": "system",
2500 "content": "",
2501 "tools": [{"name": "lookup"}]
2502 })
2503 );
2504 }
2505
2506 #[test]
2507 fn kimi_style_request_preserves_tools_and_canonicalizes_content() {
2508 let payload = serde_json::json!({
2509 "model": "dummy-kimi-model",
2510 "messages": [
2511 {
2512 "role": "system",
2513 "tools": [
2514 {
2515 "name": "lookup",
2516 "description": "dummy lookup tool",
2517 "parameters": {
2518 "type": "object",
2519 "properties": {
2520 "query": { "type": "string" }
2521 }
2522 },
2523 "vendor_hint": { "priority": 3 }
2524 }
2525 ]
2526 },
2527 {
2528 "role": "assistant",
2529 "content": "synthetic prefill",
2530 "partial": true
2531 },
2532 {
2533 "role": "user",
2534 "content": "continue"
2535 }
2536 ]
2537 });
2538
2539 let request: CreateChatCompletionRequest = serde_json::from_value(payload.clone()).unwrap();
2540 let serialized = serde_json::to_value(request).unwrap();
2541 let mut canonical = payload;
2542 canonical["messages"][0]["content"] = serde_json::json!("");
2543
2544 assert_eq!(serialized, canonical);
2545 }
2546
2547 #[test]
2548 fn system_message_tools_preserve_official_wrapped_shape() {
2549 let payload = serde_json::json!({
2550 "model": "dummy-kimi-model",
2551 "messages": [
2552 {
2553 "role": "system",
2554 "tools": [
2555 {
2556 "type": "function",
2557 "function": {
2558 "name": "lookup",
2559 "description": "dummy lookup tool",
2560 "parameters": {
2561 "type": "object",
2562 "properties": {
2563 "query": { "type": "string" }
2564 },
2565 "required": ["query"]
2566 },
2567 "strict": true
2568 }
2569 }
2570 ]
2571 },
2572 { "role": "user", "content": "continue" }
2573 ]
2574 });
2575
2576 let request: CreateChatCompletionRequest = serde_json::from_value(payload.clone()).unwrap();
2577 match &request.messages[0] {
2578 ChatCompletionRequestMessage::System(system) => {
2579 let tools = system.tools.as_ref().expect("tools should be present");
2580 assert_eq!(tools[0]["type"], "function");
2581 assert_eq!(tools[0]["function"]["name"], "lookup");
2582 }
2583 other => panic!("expected system message, got {other:?}"),
2584 }
2585
2586 let mut canonical = payload;
2587 canonical["messages"][0]["content"] = serde_json::json!("");
2588 assert_eq!(serde_json::to_value(request).unwrap(), canonical);
2589 }
2590
2591 #[test]
2592 fn assistant_message_omits_partial_when_absent() {
2593 let assistant = ChatCompletionRequestAssistantMessageArgs::default()
2594 .content("hello")
2595 .build()
2596 .unwrap();
2597
2598 assert_eq!(assistant.partial, None);
2599 let json = serde_json::to_value(&assistant).unwrap();
2600 assert!(
2601 json.get("partial").is_none(),
2602 "partial should be omitted when absent"
2603 );
2604 }
2605
2606 #[test]
2607 fn assistant_message_serializes_partial_when_present() {
2608 let assistant = ChatCompletionRequestAssistantMessageArgs::default()
2609 .content("synthetic prefill")
2610 .partial(true)
2611 .build()
2612 .unwrap();
2613
2614 let json = serde_json::to_value(&assistant).unwrap();
2615 assert_eq!(json["partial"], true);
2616
2617 let roundtrip: ChatCompletionRequestAssistantMessage =
2618 serde_json::from_value(json).unwrap();
2619 assert_eq!(roundtrip, assistant);
2620 }
2621
2622 #[test]
2623 fn system_message_from_upstream_preserves_content_and_leaves_tools_none() {
2624 let upstream = async_openai::types::chat::ChatCompletionRequestSystemMessage {
2625 content: async_openai::types::chat::ChatCompletionRequestSystemMessageContent::Text(
2626 "hi".into(),
2627 ),
2628 name: None,
2629 };
2630
2631 let owned: ChatCompletionRequestSystemMessage = upstream.into();
2632 assert!(owned.tools.is_none());
2633 match owned.content {
2634 ChatCompletionRequestSystemMessageContent::Text(text) => assert_eq!(text, "hi"),
2635 other => panic!("expected text content, got {other:?}"),
2636 }
2637 }
2638
2639 #[test]
2640 fn system_message_restores_upstream_convenience_conversions() {
2641 let from_content = ChatCompletionRequestSystemMessage::from(
2642 ChatCompletionRequestSystemMessageContent::Text("from content".into()),
2643 );
2644 let from_str = ChatCompletionRequestSystemMessage::from("from str");
2645 let from_string = ChatCompletionRequestSystemMessage::from(String::from("from string"));
2646
2647 for (message, expected) in [
2648 (from_content, "from content"),
2649 (from_str, "from str"),
2650 (from_string, "from string"),
2651 ] {
2652 assert_eq!(
2653 message.content,
2654 ChatCompletionRequestSystemMessageContent::Text(expected.into())
2655 );
2656 assert!(message.name.is_none());
2657 assert!(message.tools.is_none());
2658 }
2659 }
2660}