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