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
988#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq)]
993pub struct ChatCompletionStreamOptions {
994 pub include_usage: bool,
995 #[serde(default)]
998 pub continuous_usage_stats: bool,
999}
1000
1001#[derive(Clone, Serialize, Default, Debug, Builder, Deserialize, PartialEq)]
1008#[builder(name = "CreateChatCompletionRequestArgs")]
1009#[builder(pattern = "mutable")]
1010#[builder(setter(into, strip_option), default)]
1011#[builder(derive(Debug))]
1012#[builder(build_fn(error = "OpenAIError"))]
1013pub struct CreateChatCompletionRequest {
1014 pub messages: Vec<ChatCompletionRequestMessage>,
1015 pub model: String,
1016 #[serde(skip_serializing_if = "Option::is_none")]
1018 pub mm_processor_kwargs: Option<serde_json::Value>,
1019 #[serde(skip_serializing_if = "Option::is_none")]
1020 pub store: Option<bool>,
1021 #[serde(skip_serializing_if = "Option::is_none")]
1022 pub reasoning_effort: Option<ReasoningEffort>,
1023 #[serde(skip_serializing_if = "Option::is_none")]
1024 pub metadata: Option<serde_json::Value>,
1025 #[serde(skip_serializing_if = "Option::is_none")]
1026 pub frequency_penalty: Option<f32>,
1027 #[serde(skip_serializing_if = "Option::is_none")]
1028 pub logit_bias: Option<std::collections::HashMap<String, serde_json::Value>>,
1029 #[serde(skip_serializing_if = "Option::is_none")]
1030 pub logprobs: Option<bool>,
1031 #[serde(skip_serializing_if = "Option::is_none")]
1032 pub top_logprobs: Option<u8>,
1033 #[deprecated]
1034 #[serde(skip_serializing_if = "Option::is_none")]
1035 pub max_tokens: Option<u32>,
1036 #[serde(skip_serializing_if = "Option::is_none")]
1037 pub max_completion_tokens: Option<u32>,
1038 #[serde(skip_serializing_if = "Option::is_none")]
1039 pub n: Option<u8>,
1040 #[serde(skip_serializing_if = "Option::is_none")]
1041 pub modalities: Option<Vec<async_openai::types::chat::ResponseModalities>>,
1042 #[serde(skip_serializing_if = "Option::is_none")]
1043 pub prediction: Option<PredictionContent>,
1044 #[serde(skip_serializing_if = "Option::is_none")]
1045 pub audio: Option<ChatCompletionAudio>,
1046 #[serde(skip_serializing_if = "Option::is_none")]
1047 pub presence_penalty: Option<f32>,
1048 #[serde(skip_serializing_if = "Option::is_none")]
1049 pub response_format: Option<ResponseFormat>,
1050 #[serde(skip_serializing_if = "Option::is_none")]
1051 pub seed: Option<i64>,
1052 #[serde(skip_serializing_if = "Option::is_none")]
1053 pub service_tier: Option<ServiceTier>,
1054 #[serde(skip_serializing_if = "Option::is_none")]
1055 pub stop: Option<Stop>,
1056 #[serde(default, skip_serializing_if = "Option::is_none")]
1057 pub stream: Option<bool>,
1058 #[serde(skip_serializing_if = "Option::is_none")]
1059 pub stream_options: Option<ChatCompletionStreamOptions>,
1060 #[serde(skip_serializing_if = "Option::is_none")]
1061 pub temperature: Option<f32>,
1062 #[serde(skip_serializing_if = "Option::is_none")]
1063 pub top_p: Option<f32>,
1064 #[serde(skip_serializing_if = "Option::is_none")]
1065 pub tools: Option<Vec<ChatCompletionTool>>,
1066 #[serde(skip_serializing_if = "Option::is_none")]
1067 pub tool_choice: Option<ChatCompletionToolChoiceOption>,
1068 #[serde(skip_serializing_if = "Option::is_none")]
1069 pub parallel_tool_calls: Option<bool>,
1070 #[serde(skip_serializing_if = "Option::is_none")]
1071 pub user: Option<String>,
1072 #[serde(skip_serializing_if = "Option::is_none")]
1081 pub prompt_cache_key: Option<String>,
1082 #[deprecated]
1083 #[serde(skip_serializing_if = "Option::is_none")]
1084 pub function_call: Option<ChatCompletionFunctionCall>,
1085 #[deprecated]
1086 #[serde(skip_serializing_if = "Option::is_none")]
1087 pub functions: Option<Vec<ChatCompletionFunctions>>,
1088 #[serde(skip_serializing_if = "Option::is_none")]
1089 pub web_search_options: Option<WebSearchOptions>,
1090}
1091
1092impl CreateChatCompletionRequest {
1093 pub fn dynamic_system_tools(&self) -> impl Iterator<Item = &serde_json::Value> {
1104 self.messages
1105 .iter()
1106 .filter_map(|message| match message {
1107 ChatCompletionRequestMessage::System(system) => system.tools.as_deref(),
1108 _ => None,
1109 })
1110 .flatten()
1111 }
1112
1113 pub fn has_effective_tools(&self) -> bool {
1120 self.tools.as_ref().is_some_and(|tools| !tools.is_empty())
1121 || self.dynamic_system_tools().next().is_some()
1122 }
1123
1124 pub fn effective_tool_names(&self) -> impl Iterator<Item = &str> {
1127 self.tools
1128 .iter()
1129 .flatten()
1130 .map(|tool| tool.function.name.as_str())
1131 .chain(self.dynamic_system_tools().filter_map(dynamic_tool_name))
1132 }
1133
1134 pub fn effective_tool_contains(&self, name: &str) -> bool {
1139 self.effective_tool_names().any(|tool| tool == name)
1140 }
1141}
1142
1143pub fn dynamic_tool_name(tool: &serde_json::Value) -> Option<&str> {
1150 tool.get("function")
1151 .and_then(serde_json::Value::as_object)
1152 .and_then(|function| function.get("name"))
1153 .or_else(|| tool.get("name"))
1154 .and_then(serde_json::Value::as_str)
1155}
1156
1157#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
1161pub struct ChatChoice {
1162 pub index: u32,
1163 pub message: ChatCompletionResponseMessage,
1164 pub finish_reason: Option<FinishReason>,
1165 pub logprobs: Option<ChatChoiceLogprobs>,
1166}
1167
1168fn serialize_usage_omitting_absent<S>(
1178 usage: &Option<CompletionUsage>,
1179 serializer: S,
1180) -> Result<S::Ok, S::Error>
1181where
1182 S: serde::Serializer,
1183{
1184 #[derive(Serialize)]
1185 struct PromptDetailsShadow {
1186 #[serde(skip_serializing_if = "Option::is_none")]
1187 audio_tokens: Option<u32>,
1188 #[serde(skip_serializing_if = "Option::is_none")]
1189 cached_tokens: Option<u32>,
1190 }
1191
1192 #[derive(Serialize)]
1193 struct CompletionDetailsShadow {
1194 #[serde(skip_serializing_if = "Option::is_none")]
1195 accepted_prediction_tokens: Option<u32>,
1196 #[serde(skip_serializing_if = "Option::is_none")]
1197 audio_tokens: Option<u32>,
1198 #[serde(skip_serializing_if = "Option::is_none")]
1199 reasoning_tokens: Option<u32>,
1200 #[serde(skip_serializing_if = "Option::is_none")]
1201 rejected_prediction_tokens: Option<u32>,
1202 }
1203
1204 #[derive(Serialize)]
1205 struct UsageShadow {
1206 prompt_tokens: u32,
1207 completion_tokens: u32,
1208 total_tokens: u32,
1209 #[serde(skip_serializing_if = "Option::is_none")]
1210 prompt_tokens_details: Option<PromptDetailsShadow>,
1211 #[serde(skip_serializing_if = "Option::is_none")]
1212 completion_tokens_details: Option<CompletionDetailsShadow>,
1213 }
1214
1215 match usage {
1216 None => serializer.serialize_none(),
1217 Some(u) => UsageShadow {
1218 prompt_tokens: u.prompt_tokens,
1219 completion_tokens: u.completion_tokens,
1220 total_tokens: u.total_tokens,
1221 prompt_tokens_details: u
1222 .prompt_tokens_details
1223 .as_ref()
1224 .map(|d| PromptDetailsShadow {
1225 audio_tokens: d.audio_tokens,
1226 cached_tokens: d.cached_tokens,
1227 }),
1228 completion_tokens_details: u.completion_tokens_details.as_ref().map(|d| {
1229 CompletionDetailsShadow {
1230 accepted_prediction_tokens: d.accepted_prediction_tokens,
1231 audio_tokens: d.audio_tokens,
1232 reasoning_tokens: d.reasoning_tokens,
1233 rejected_prediction_tokens: d.rejected_prediction_tokens,
1234 }
1235 }),
1236 }
1237 .serialize(serializer),
1238 }
1239}
1240
1241#[derive(Debug, Deserialize, Clone, PartialEq, Serialize)]
1248pub struct CreateChatCompletionResponse {
1249 pub id: String,
1250 pub choices: Vec<ChatChoice>,
1251 pub created: u32,
1252 pub model: String,
1253 #[serde(skip_serializing_if = "Option::is_none")]
1254 pub service_tier: Option<ServiceTierResponse>,
1255 #[serde(skip_serializing_if = "Option::is_none")]
1256 pub system_fingerprint: Option<String>,
1257 pub object: String,
1258 #[serde(
1259 skip_serializing_if = "Option::is_none",
1260 serialize_with = "serialize_usage_omitting_absent"
1261 )]
1262 pub usage: Option<CompletionUsage>,
1263}
1264
1265pub type ChatCompletionResponseStream =
1266 Pin<Box<dyn Stream<Item = Result<CreateChatCompletionStreamResponse, OpenAIError>> + Send>>;
1267
1268#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
1274pub struct ChatCompletionStreamResponseDelta {
1275 #[serde(skip_serializing_if = "Option::is_none")]
1276 pub content: Option<ChatCompletionMessageContent>,
1277 #[serde(skip_serializing_if = "Option::is_none")]
1278 pub function_call: Option<ChatCompletionStreamResponseDeltaFunctionCall>,
1279 #[serde(skip_serializing_if = "Option::is_none")]
1280 pub tool_calls: Option<Vec<ChatCompletionMessageToolCallChunk>>,
1281 #[serde(skip_serializing_if = "Option::is_none")]
1282 pub role: Option<Role>,
1283 #[serde(skip_serializing_if = "Option::is_none")]
1284 pub refusal: Option<String>,
1285 #[serde(default, alias = "reasoning", skip_serializing_if = "Option::is_none")]
1291 pub reasoning_content: Option<String>,
1292}
1293
1294#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
1295pub struct ChatCompletionStreamResponseDeltaFunctionCall {
1296 #[serde(skip_serializing_if = "Option::is_none")]
1297 pub name: Option<String>,
1298 #[serde(
1299 default,
1300 deserialize_with = "deserialize_arguments_opt",
1301 skip_serializing_if = "Option::is_none"
1302 )]
1303 pub arguments: Option<String>,
1304}
1305
1306#[derive(Debug, Deserialize, Clone, PartialEq, Serialize)]
1308pub struct ChatChoiceStream {
1309 pub index: u32,
1310 pub delta: ChatCompletionStreamResponseDelta,
1311 pub finish_reason: Option<FinishReason>,
1312 pub logprobs: Option<ChatChoiceLogprobs>,
1313}
1314
1315#[derive(Debug, Deserialize, Clone, PartialEq, Serialize)]
1323pub struct CreateChatCompletionStreamResponse {
1324 pub id: String,
1325 pub choices: Vec<ChatChoiceStream>,
1326 pub created: u32,
1327 pub model: String,
1328 #[serde(skip_serializing_if = "Option::is_none")]
1329 pub service_tier: Option<ServiceTierResponse>,
1330 #[serde(skip_serializing_if = "Option::is_none")]
1331 pub system_fingerprint: Option<String>,
1332 pub object: String,
1333 #[serde(
1334 skip_serializing_if = "Option::is_none",
1335 serialize_with = "serialize_usage_omitting_absent"
1336 )]
1337 pub usage: Option<CompletionUsage>,
1338}
1339
1340#[cfg(test)]
1341mod tests {
1342 use super::*;
1343
1344 #[test]
1345 fn stop_accepts_token_id_array() {
1346 let stop: Stop = serde_json::from_value(serde_json::json!([32, 34])).unwrap();
1347
1348 assert_eq!(stop, Stop::TokenIdArray(vec![32, 34]));
1349 }
1350
1351 #[test]
1352 fn stop_accepts_string_and_string_array() {
1353 let stop: Stop = serde_json::from_value(serde_json::json!(" The")).unwrap();
1354
1355 assert_eq!(stop, Stop::String(" The".to_string()));
1356
1357 let stop: Stop = serde_json::from_value(serde_json::json!(["A", "B"])).unwrap();
1358
1359 assert_eq!(
1360 stop,
1361 Stop::StringArray(vec!["A".to_string(), "B".to_string()])
1362 );
1363 }
1364
1365 #[test]
1366 fn stop_token_id_display_string_remains_string_stop() {
1367 let stop: Stop = serde_json::from_value(serde_json::json!("token_id:576")).unwrap();
1368
1369 assert_eq!(stop, Stop::String("token_id:576".to_string()));
1370
1371 let stop: Stop = serde_json::from_value(serde_json::json!(["token_id:576"])).unwrap();
1372
1373 assert_eq!(stop, Stop::StringArray(vec!["token_id:576".to_string()]));
1374 }
1375
1376 #[test]
1377 fn stop_rejects_single_token_id() {
1378 let result = serde_json::from_value::<Stop>(serde_json::json!(576));
1379
1380 assert!(result.is_err());
1381 }
1382
1383 #[test]
1384 fn stop_converts_from_upstream_stop_configuration() {
1385 let upstream =
1386 async_openai::types::chat::StopConfiguration::StringArray(vec!["END".to_string()]);
1387
1388 assert_eq!(
1389 Stop::from(upstream),
1390 Stop::StringArray(vec!["END".to_string()])
1391 );
1392 }
1393
1394 #[test]
1395 fn request_builder_accepts_upstream_reasoning_effort() {
1396 let request = CreateChatCompletionRequestArgs::default()
1397 .reasoning_effort(async_openai::types::chat::ReasoningEffort::High)
1398 .build()
1399 .unwrap();
1400
1401 assert_eq!(request.reasoning_effort, Some(ReasoningEffort::High));
1402 }
1403
1404 #[test]
1405 fn tool_call_defaults_type_on_deserialize() {
1406 let tool_call: ChatCompletionMessageToolCall = serde_json::from_value(serde_json::json!({
1407 "id": "call_123",
1408 "function": {
1409 "name": "get_weather",
1410 "arguments": "{\"location\":\"SF\"}"
1411 }
1412 }))
1413 .unwrap();
1414
1415 assert_eq!(tool_call.r#type, FunctionType::Function);
1416 }
1417
1418 #[test]
1419 fn tool_call_serializes_type_for_wire_compat() {
1420 let tool_call = ChatCompletionMessageToolCall {
1421 id: "call_123".into(),
1422 r#type: FunctionType::Function,
1423 function: FunctionCall {
1424 name: "get_weather".into(),
1425 arguments: "{\"location\":\"SF\"}".into(),
1426 },
1427 };
1428
1429 let json = serde_json::to_value(tool_call).unwrap();
1430 assert_eq!(json["type"], "function");
1431 }
1432
1433 #[test]
1436 fn function_call_accepts_string_arguments() {
1437 let fc: FunctionCall = serde_json::from_value(serde_json::json!({
1438 "name": "get_weather",
1439 "arguments": "{\"location\":\"SF\"}"
1440 }))
1441 .unwrap();
1442 assert_eq!(fc.arguments, "{\"location\":\"SF\"}");
1443 }
1444
1445 #[test]
1446 fn function_call_accepts_dict_arguments() {
1447 let fc: FunctionCall = serde_json::from_value(serde_json::json!({
1448 "name": "get_weather",
1449 "arguments": {"location": "SF"}
1450 }))
1451 .unwrap();
1452 assert_eq!(fc.arguments, "{\"location\":\"SF\"}");
1453 }
1454
1455 #[test]
1456 fn function_call_rejects_integer_arguments() {
1457 let result = serde_json::from_value::<FunctionCall>(serde_json::json!({
1458 "name": "f",
1459 "arguments": 42
1460 }));
1461 assert!(result.is_err());
1462 }
1463
1464 #[test]
1465 fn function_call_rejects_boolean_arguments() {
1466 let result = serde_json::from_value::<FunctionCall>(serde_json::json!({
1467 "name": "f",
1468 "arguments": true
1469 }));
1470 assert!(result.is_err());
1471 }
1472
1473 #[test]
1474 fn function_call_rejects_null_arguments() {
1475 let result = serde_json::from_value::<FunctionCall>(serde_json::json!({
1476 "name": "f",
1477 "arguments": null
1478 }));
1479 assert!(result.is_err());
1480 }
1481
1482 #[test]
1483 fn function_call_rejects_array_arguments() {
1484 let result = serde_json::from_value::<FunctionCall>(serde_json::json!({
1485 "name": "f",
1486 "arguments": [1, 2, 3]
1487 }));
1488 assert!(result.is_err());
1489 }
1490
1491 #[test]
1492 fn function_call_stream_null_arguments_produces_none() {
1493 let fcs: FunctionCallStream = serde_json::from_value(serde_json::json!({
1494 "name": "f",
1495 "arguments": null
1496 }))
1497 .unwrap();
1498 assert_eq!(fcs.arguments, None);
1499 }
1500
1501 #[test]
1502 fn function_call_stream_rejects_integer_arguments() {
1503 let result = serde_json::from_value::<FunctionCallStream>(serde_json::json!({
1504 "name": "f",
1505 "arguments": 42
1506 }));
1507 assert!(result.is_err());
1508 }
1509
1510 #[test]
1511 fn function_call_stream_rejects_boolean_arguments() {
1512 let result = serde_json::from_value::<FunctionCallStream>(serde_json::json!({
1513 "name": "f",
1514 "arguments": true
1515 }));
1516 assert!(result.is_err());
1517 }
1518
1519 #[test]
1520 fn function_call_stream_accepts_dict_arguments() {
1521 let fcs: FunctionCallStream = serde_json::from_value(serde_json::json!({
1522 "name": "get_weather",
1523 "arguments": {"location": "SF"}
1524 }))
1525 .unwrap();
1526 assert_eq!(fcs.arguments.as_deref(), Some("{\"location\":\"SF\"}"));
1527 }
1528
1529 #[test]
1530 fn function_call_stream_accepts_null_arguments() {
1531 let fcs: FunctionCallStream = serde_json::from_value(serde_json::json!({
1532 "name": "get_weather"
1533 }))
1534 .unwrap();
1535 assert_eq!(fcs.arguments, None);
1536 }
1537
1538 #[test]
1539 fn tool_call_with_dict_arguments_roundtrip() {
1540 let tc: ChatCompletionMessageToolCall = serde_json::from_value(serde_json::json!({
1541 "id": "call_abc",
1542 "type": "function",
1543 "function": {
1544 "name": "search",
1545 "arguments": {"query": "hello", "limit": 10}
1546 }
1547 }))
1548 .unwrap();
1549 let parsed: serde_json::Value = serde_json::from_str(&tc.function.arguments).unwrap();
1551 assert_eq!(parsed, serde_json::json!({"query": "hello", "limit": 10}));
1552 let json = serde_json::to_value(&tc).unwrap();
1554 assert!(json["function"]["arguments"].is_string());
1555 }
1556
1557 #[test]
1558 fn stream_delta_function_call_accepts_dict_arguments() {
1559 let delta: ChatCompletionStreamResponseDeltaFunctionCall =
1560 serde_json::from_value(serde_json::json!({
1561 "name": "get_weather",
1562 "arguments": {"location": "SF"}
1563 }))
1564 .unwrap();
1565 assert_eq!(delta.arguments.as_deref(), Some("{\"location\":\"SF\"}"));
1566 }
1567
1568 fn parse_content_part(json: serde_json::Value) -> ChatCompletionRequestUserMessageContentPart {
1569 serde_json::from_value(json).expect("content part deserialization failed")
1570 }
1571
1572 #[test]
1573 fn image_url_url_and_top_level_uuid() {
1574 let part = parse_content_part(serde_json::json!({
1575 "type": "image_url",
1576 "image_url": {"url": "https://x.example/y.png"},
1577 "uuid": "image-123"
1578 }));
1579
1580 match part {
1581 ChatCompletionRequestUserMessageContentPart::ImageUrl(part) => {
1582 assert_eq!(part.uuid.as_deref(), Some("image-123"));
1583 assert_eq!(
1584 part.image_url.as_ref().map(|image| image.url.as_str()),
1585 Some("https://x.example/y.png")
1586 );
1587 }
1588 _ => panic!("expected image_url part"),
1589 }
1590 }
1591
1592 #[test]
1593 fn image_url_null_and_top_level_uuid() {
1594 let part = parse_content_part(serde_json::json!({
1595 "type": "image_url",
1596 "image_url": null,
1597 "uuid": "sku-1234-a"
1598 }));
1599
1600 match part {
1601 ChatCompletionRequestUserMessageContentPart::ImageUrl(part) => {
1602 assert!(part.image_url.is_none());
1603 assert_eq!(part.uuid.as_deref(), Some("sku-1234-a"));
1604 }
1605 _ => panic!("expected image_url part"),
1606 }
1607 }
1608
1609 #[test]
1610 fn empty_media_urls_deserialize_as_uuid_only() {
1611 for (part_type, media_field, uuid) in [
1612 ("image_url", "image_url", "image-cache-key"),
1613 ("video_url", "video_url", "video-cache-key"),
1614 ("audio_url", "audio_url", "audio-cache-key"),
1615 ] {
1616 let part = parse_content_part(serde_json::json!({
1617 "type": part_type,
1618 (media_field): {"url": ""},
1619 "uuid": uuid
1620 }));
1621 let json = serde_json::to_value(part).unwrap();
1622
1623 assert!(json[media_field].is_null());
1624 assert_eq!(json["uuid"], uuid);
1625 }
1626 }
1627
1628 #[test]
1629 fn image_url_null_without_uuid_deserializes_for_use_site_validation() {
1630 let part = parse_content_part(serde_json::json!({
1631 "type": "image_url",
1632 "image_url": null
1633 }));
1634
1635 match part {
1636 ChatCompletionRequestUserMessageContentPart::ImageUrl(part) => {
1637 assert!(part.image_url.is_none());
1638 assert!(part.uuid.is_none());
1639 }
1640 _ => panic!("expected image_url part"),
1641 }
1642 }
1643
1644 #[test]
1645 fn image_url_serialize_uuid_only_uses_null_image_url() {
1646 let part = ChatCompletionRequestMessageContentPartImage {
1647 image_url: None,
1648 uuid: Some("image-123".to_string()),
1649 };
1650 let json = serde_json::to_value(part).unwrap();
1651
1652 assert!(json["image_url"].is_null());
1653 assert_eq!(json["uuid"], "image-123");
1654 }
1655
1656 #[test]
1657 fn cached_media_builders_allow_omitting_urls() {
1658 let image = ChatCompletionRequestMessageContentPartImageArgs::default()
1659 .uuid("image-123")
1660 .build()
1661 .unwrap();
1662 let video = ChatCompletionRequestMessageContentPartVideoArgs::default()
1663 .uuid("video-123")
1664 .build()
1665 .unwrap();
1666 let audio = ChatCompletionRequestMessageContentPartAudioUrlArgs::default()
1667 .uuid("audio-123")
1668 .build()
1669 .unwrap();
1670
1671 let image_json = serde_json::to_value(image).unwrap();
1672 let video_json = serde_json::to_value(video).unwrap();
1673 let audio_json = serde_json::to_value(audio).unwrap();
1674 assert!(image_json["image_url"].is_null());
1675 assert!(video_json["video_url"].is_null());
1676 assert!(audio_json["audio_url"].is_null());
1677 }
1678
1679 #[test]
1680 fn image_url_uuid_accepts_opaque_string() {
1681 let part = parse_content_part(serde_json::json!({
1682 "type": "image_url",
1683 "image_url": {"url": "https://x.example/y.png"},
1684 "uuid": "img-ac3921de680bb217"
1685 }));
1686
1687 match part {
1688 ChatCompletionRequestUserMessageContentPart::ImageUrl(part) => {
1689 assert_eq!(part.uuid.as_deref(), Some("img-ac3921de680bb217"));
1690 }
1691 _ => panic!("expected image_url part"),
1692 }
1693 }
1694
1695 #[test]
1696 fn url_conversions_preserve_required_urls() {
1697 let image: ImageUrl = "https://x.example/image.png".into();
1698 let video: VideoUrl = "https://x.example/video.mp4".into();
1699 let audio: AudioUrl = "https://x.example/audio.wav".into();
1700
1701 assert_eq!(image.url.as_str(), "https://x.example/image.png");
1702 assert_eq!(video.url.as_str(), "https://x.example/video.mp4");
1703 assert_eq!(audio.url.as_str(), "https://x.example/audio.wav");
1704 }
1705
1706 #[test]
1707 fn invalid_media_urls_remain_rejected() {
1708 for (part_type, media_field) in [
1709 ("image_url", "image_url"),
1710 ("video_url", "video_url"),
1711 ("audio_url", "audio_url"),
1712 ] {
1713 let result = serde_json::from_value::<ChatCompletionRequestUserMessageContentPart>(
1714 serde_json::json!({
1715 "type": part_type,
1716 (media_field): {"url": "not a url"},
1717 "uuid": "cache-key"
1718 }),
1719 );
1720
1721 assert!(result.is_err(), "{part_type} accepted an invalid URL");
1722 }
1723 }
1724
1725 #[test]
1726 fn legacy_nested_media_uuids_remain_accepted() {
1727 let legacy_uuid = "92b888ad-e64a-478f-b688-5091e16544e3";
1728
1729 for (part_type, media_field, url) in [
1730 ("image_url", "image_url", "https://x.example/image.png"),
1731 ("video_url", "video_url", "https://x.example/video.mp4"),
1732 ("audio_url", "audio_url", "https://x.example/audio.wav"),
1733 ] {
1734 let part = parse_content_part(serde_json::json!({
1735 "type": part_type,
1736 (media_field): {"url": url, "uuid": legacy_uuid}
1737 }));
1738 let json = serde_json::to_value(part).unwrap();
1739
1740 assert_eq!(json[media_field]["url"], url);
1741 assert_eq!(json[media_field]["uuid"], legacy_uuid);
1742 assert!(json.get("uuid").is_none());
1743 }
1744 }
1745
1746 #[test]
1747 fn video_url_null_and_top_level_uuid() {
1748 let part = parse_content_part(serde_json::json!({
1749 "type": "video_url",
1750 "video_url": null,
1751 "uuid": "video-cache-key"
1752 }));
1753
1754 match part {
1755 ChatCompletionRequestUserMessageContentPart::VideoUrl(part) => {
1756 assert!(part.video_url.is_none());
1757 assert_eq!(part.uuid.as_deref(), Some("video-cache-key"));
1758 }
1759 _ => panic!("expected video_url part"),
1760 }
1761 }
1762
1763 #[test]
1764 fn audio_url_null_and_top_level_uuid() {
1765 let part = parse_content_part(serde_json::json!({
1766 "type": "audio_url",
1767 "audio_url": null,
1768 "uuid": "audio-cache-key"
1769 }));
1770
1771 match part {
1772 ChatCompletionRequestUserMessageContentPart::AudioUrl(part) => {
1773 assert!(part.audio_url.is_none());
1774 assert_eq!(part.uuid.as_deref(), Some("audio-cache-key"));
1775 }
1776 _ => panic!("expected audio_url part"),
1777 }
1778 }
1779
1780 #[test]
1781 fn message_content_array_preserves_uuid_alignment() {
1782 let payload = serde_json::json!({
1783 "role": "user",
1784 "content": [
1785 {"type": "text", "text": "describe these"},
1786 {
1787 "type": "image_url",
1788 "image_url": {"url": "https://x.example/img1.png"},
1789 "uuid": "image-1"
1790 },
1791 {"type": "image_url", "image_url": null, "uuid": "image-1"}
1792 ]
1793 });
1794 let message: ChatCompletionRequestUserMessage = serde_json::from_value(payload).unwrap();
1795 let ChatCompletionRequestUserMessageContent::Array(parts) = message.content else {
1796 panic!("expected content array");
1797 };
1798
1799 assert_eq!(parts.len(), 3);
1800 match &parts[1] {
1801 ChatCompletionRequestUserMessageContentPart::ImageUrl(part) => {
1802 assert!(
1803 part.image_url
1804 .as_ref()
1805 .map(|image| image.url.as_str())
1806 .is_some()
1807 );
1808 assert_eq!(part.uuid.as_deref(), Some("image-1"));
1809 }
1810 _ => panic!("parts[1] should be image_url"),
1811 }
1812 match &parts[2] {
1813 ChatCompletionRequestUserMessageContentPart::ImageUrl(part) => {
1814 assert!(part.image_url.is_none());
1815 assert_eq!(part.uuid.as_deref(), Some("image-1"));
1816 }
1817 _ => panic!("parts[2] should be image_url"),
1818 }
1819 }
1820
1821 #[test]
1822 fn tool_message_accepts_media_content() {
1823 let message: ChatCompletionRequestMessage = serde_json::from_value(serde_json::json!({
1824 "role": "tool",
1825 "tool_call_id": "call_media",
1826 "content": [
1827 {"type": "text", "text": "Screenshot captured"},
1828 {
1829 "type": "image_url",
1830 "image_url": {
1831 "url": "data:image/png;base64,aGVsbG8="
1832 }
1833 },
1834 {
1835 "type": "video_url",
1836 "video_url": {
1837 "url": "https://example.com/clip.mp4"
1838 }
1839 },
1840 {
1841 "type": "audio_url",
1842 "audio_url": {
1843 "url": "https://example.com/audio.wav"
1844 }
1845 }
1846 ]
1847 }))
1848 .unwrap();
1849
1850 let ChatCompletionRequestMessage::Tool(tool) = message else {
1851 panic!("expected tool message");
1852 };
1853 let ChatCompletionRequestToolMessageContent::Array(parts) = tool.content else {
1854 panic!("expected array content");
1855 };
1856 assert!(matches!(
1857 parts[1],
1858 ChatCompletionRequestToolMessageContentPart::ImageUrl(_)
1859 ));
1860 assert!(matches!(
1861 parts[2],
1862 ChatCompletionRequestToolMessageContentPart::VideoUrl(_)
1863 ));
1864 assert!(matches!(
1865 parts[3],
1866 ChatCompletionRequestToolMessageContentPart::AudioUrl(_)
1867 ));
1868 }
1869
1870 #[test]
1871 fn chat_logprob_serializes_token_id_when_present() {
1872 let logprob = ChatCompletionTokenLogprob {
1873 token: " hello".into(),
1874 logprob: -0.12,
1875 token_id: Some(123),
1876 bytes: Some(vec![32, 104, 101, 108, 108, 111]),
1877 top_logprobs: vec![],
1878 };
1879
1880 let json = serde_json::to_value(logprob).unwrap();
1881
1882 assert_eq!(json["token_id"], 123);
1883 }
1884
1885 #[test]
1886 fn chat_logprob_deserializes_optional_fields() {
1887 let choice_logprobs: ChatChoiceLogprobs = serde_json::from_value(serde_json::json!({
1888 "content": [{
1889 "token": " hello",
1890 "logprob": -0.12,
1891 "top_logprobs": []
1892 }]
1893 }))
1894 .unwrap();
1895 let token_logprob: ChatCompletionTokenLogprob = serde_json::from_value(serde_json::json!({
1896 "token": " hello",
1897 "logprob": -0.12,
1898 "token_id": 123,
1899 "bytes": [32, 104, 101, 108, 108, 111],
1900 "top_logprobs": []
1901 }))
1902 .unwrap();
1903
1904 assert_eq!(choice_logprobs.content.as_ref().unwrap()[0].token_id, None);
1905 assert!(choice_logprobs.refusal.is_none());
1906 assert_eq!(token_logprob.token_id, Some(123));
1907 assert_eq!(token_logprob.bytes, Some(vec![32, 104, 101, 108, 108, 111]));
1908 }
1909
1910 #[test]
1911 fn chat_logprob_preserves_nullable_fields() {
1912 let choice_logprobs = ChatChoiceLogprobs {
1913 content: None,
1914 refusal: None,
1915 };
1916 let token_logprob = ChatCompletionTokenLogprob {
1917 token: " hello".into(),
1918 logprob: -0.12,
1919 token_id: None,
1920 bytes: None,
1921 top_logprobs: vec![],
1922 };
1923
1924 let choice_json = serde_json::to_value(choice_logprobs).unwrap();
1925 let token_json = serde_json::to_value(token_logprob).unwrap();
1926
1927 assert_eq!(choice_json["content"], serde_json::Value::Null);
1928 assert_eq!(choice_json["refusal"], serde_json::Value::Null);
1929 assert!(token_json.get("token_id").is_none());
1930 assert_eq!(token_json["bytes"], serde_json::Value::Null);
1931 }
1932
1933 #[test]
1934 #[allow(deprecated)]
1935 fn chat_response_omits_absent_optional_fields() {
1936 let response = CreateChatCompletionResponse {
1937 id: "chatcmpl_dummy".into(),
1938 choices: vec![ChatChoice {
1939 index: 0,
1940 message: ChatCompletionResponseMessage {
1941 content: Some(ChatCompletionMessageContent::Text("hello".into())),
1942 refusal: None,
1943 tool_calls: None,
1944 role: Role::Assistant,
1945 function_call: None,
1946 audio: None,
1947 reasoning_content: None,
1948 },
1949 finish_reason: Some(FinishReason::Stop),
1950 logprobs: None,
1951 }],
1952 created: 0,
1953 model: "dummy-model".into(),
1954 service_tier: None,
1955 system_fingerprint: None,
1956 object: "chat.completion".into(),
1957 usage: None,
1958 };
1959
1960 let json = serde_json::to_value(response).unwrap();
1961
1962 for absent in ["usage", "service_tier", "system_fingerprint"] {
1963 assert!(json.get(absent).is_none(), "{absent} should be omitted");
1964 }
1965 let choice = &json["choices"][0];
1966 assert_eq!(choice["finish_reason"], "stop");
1967 assert_eq!(choice["logprobs"], serde_json::Value::Null);
1968 let message = &choice["message"];
1969 assert_eq!(message["refusal"], serde_json::Value::Null);
1970 for absent in ["tool_calls", "function_call", "audio", "reasoning_content"] {
1971 assert!(
1972 message.get(absent).is_none(),
1973 "message.{absent} should be omitted"
1974 );
1975 }
1976 }
1977
1978 #[test]
1979 fn stream_response_omits_absent_optional_fields() {
1980 let chunk = CreateChatCompletionStreamResponse {
1981 id: "chatcmpl_dummy".into(),
1982 choices: vec![ChatChoiceStream {
1983 index: 0,
1984 delta: ChatCompletionStreamResponseDelta {
1985 content: Some(ChatCompletionMessageContent::Text("hello".into())),
1986 function_call: None,
1987 tool_calls: None,
1988 role: None,
1989 refusal: None,
1990 reasoning_content: None,
1991 },
1992 finish_reason: None,
1993 logprobs: None,
1994 }],
1995 created: 0,
1996 model: "dummy-model".into(),
1997 service_tier: None,
1998 system_fingerprint: None,
1999 object: "chat.completion.chunk".into(),
2000 usage: None,
2001 };
2002
2003 let json = serde_json::to_value(chunk).unwrap();
2004
2005 for absent in ["usage", "service_tier", "system_fingerprint"] {
2006 assert!(json.get(absent).is_none(), "{absent} should be omitted");
2007 }
2008 }
2009
2010 #[test]
2011 fn stream_tool_call_continuation_chunk_omits_absent_fields() {
2012 let chunk = ChatCompletionMessageToolCallChunk {
2013 index: 0,
2014 id: None,
2015 r#type: None,
2016 function: Some(FunctionCallStream {
2017 name: None,
2018 arguments: Some("{\"a\":".into()),
2019 }),
2020 };
2021
2022 let json = serde_json::to_value(chunk).unwrap();
2023
2024 assert!(json.get("id").is_none());
2025 assert!(json.get("type").is_none());
2026 assert!(json["function"].get("name").is_none());
2027 assert_eq!(json["function"]["arguments"], "{\"a\":");
2028 }
2029
2030 #[test]
2031 fn stream_delta_function_call_omits_absent_fields() {
2032 let function_call = ChatCompletionStreamResponseDeltaFunctionCall {
2033 name: None,
2034 arguments: Some("{}".into()),
2035 };
2036
2037 let json = serde_json::to_value(function_call).unwrap();
2038
2039 assert!(json.get("name").is_none());
2040 assert_eq!(json["arguments"], "{}");
2041 }
2042
2043 #[test]
2044 fn usage_details_omit_absent_fields() {
2045 let response = CreateChatCompletionResponse {
2046 id: "chatcmpl_dummy".into(),
2047 choices: vec![],
2048 created: 0,
2049 model: "dummy-model".into(),
2050 service_tier: None,
2051 system_fingerprint: None,
2052 object: "chat.completion".into(),
2053 usage: Some(CompletionUsage {
2054 prompt_tokens: 10,
2055 completion_tokens: 25,
2056 total_tokens: 35,
2057 prompt_tokens_details: Some(PromptTokensDetails {
2058 audio_tokens: None,
2059 cached_tokens: Some(0),
2060 }),
2061 completion_tokens_details: Some(CompletionTokensDetails {
2062 reasoning_tokens: Some(5),
2063 ..Default::default()
2064 }),
2065 }),
2066 };
2067
2068 let json = serde_json::to_value(&response).unwrap();
2069 let usage = &json["usage"];
2070
2071 assert_eq!(usage["total_tokens"], 35);
2072 assert_eq!(usage["prompt_tokens_details"]["cached_tokens"], 0);
2073 assert!(
2074 usage["prompt_tokens_details"].get("audio_tokens").is_none(),
2075 "audio_tokens should be omitted, not null"
2076 );
2077 assert_eq!(usage["completion_tokens_details"]["reasoning_tokens"], 5);
2078 for absent in [
2079 "accepted_prediction_tokens",
2080 "audio_tokens",
2081 "rejected_prediction_tokens",
2082 ] {
2083 assert!(
2084 usage["completion_tokens_details"].get(absent).is_none(),
2085 "{absent} should be omitted"
2086 );
2087 }
2088
2089 let roundtrip: CreateChatCompletionResponse = serde_json::from_value(json).unwrap();
2090 assert_eq!(roundtrip, response);
2091 }
2092
2093 #[test]
2096 fn effective_tool_set_unions_top_level_and_dynamic_system_tools() {
2097 let request: CreateChatCompletionRequest = serde_json::from_value(serde_json::json!({
2098 "model": "dummy-kimi-model",
2099 "tools": [{
2100 "type": "function",
2101 "function": {"name": "add", "parameters": {"type": "object"}}
2102 }],
2103 "messages": [
2104 {"role": "user", "content": "start"},
2105 {
2106 "role": "system",
2107 "tools": [
2108 {
2109 "type": "function",
2110 "function": {"name": "lookup", "parameters": {"type": "object"}}
2111 },
2112 {"name": "search", "parameters": {"type": "object"}},
2113 {"description": "no name, skipped"}
2114 ]
2115 },
2116 {"role": "user", "content": "continue"}
2117 ]
2118 }))
2119 .unwrap();
2120
2121 assert!(request.has_effective_tools());
2122 assert_eq!(request.dynamic_system_tools().count(), 3);
2123 assert_eq!(
2124 request.effective_tool_names().collect::<Vec<_>>(),
2125 ["add", "lookup", "search"],
2126 "top-level first, then dynamic in message order; wrapped and bare shapes both resolve"
2127 );
2128 for name in ["add", "lookup", "search"] {
2129 assert!(
2130 request.effective_tool_contains(name),
2131 "{name} should be found"
2132 );
2133 }
2134 assert!(!request.effective_tool_contains("missing"));
2135 assert!(
2136 !request.effective_tool_contains("no name, skipped"),
2137 "a description is not a name"
2138 );
2139 }
2140
2141 #[test]
2142 fn effective_tool_set_is_empty_without_any_declaration() {
2143 for payload in [
2144 serde_json::json!({
2145 "model": "m",
2146 "messages": [{"role": "user", "content": "hi"}]
2147 }),
2148 serde_json::json!({
2149 "model": "m",
2150 "tools": [],
2151 "messages": [{"role": "system", "content": "plain system text"}]
2152 }),
2153 ] {
2154 let request: CreateChatCompletionRequest = serde_json::from_value(payload).unwrap();
2155 assert!(!request.has_effective_tools());
2156 assert_eq!(request.effective_tool_names().count(), 0);
2157 assert!(!request.effective_tool_contains("anything"));
2158 }
2159 }
2160
2161 #[test]
2162 fn dynamic_system_tools_alone_count_as_effective_tools() {
2163 let request: CreateChatCompletionRequest = serde_json::from_value(serde_json::json!({
2164 "model": "dummy-kimi-model",
2165 "messages": [
2166 {"role": "system", "tools": [{"name": "lookup"}]},
2167 {"role": "user", "content": "go"}
2168 ]
2169 }))
2170 .unwrap();
2171
2172 assert!(
2173 request.tools.is_none(),
2174 "nothing was folded into top-level tools"
2175 );
2176 assert!(request.has_effective_tools());
2177 assert!(request.effective_tool_contains("lookup"));
2178 }
2179
2180 #[test]
2181 fn dynamic_tool_name_handles_wrapped_bare_and_invalid_shapes() {
2182 assert_eq!(
2183 dynamic_tool_name(&serde_json::json!({"type": "function", "function": {"name": "a"}})),
2184 Some("a")
2185 );
2186 assert_eq!(
2187 dynamic_tool_name(&serde_json::json!({"name": "b"})),
2188 Some("b")
2189 );
2190 assert_eq!(dynamic_tool_name(&serde_json::json!({"name": 7})), None);
2191 }
2192
2193 #[test]
2194 fn system_message_without_content_is_rejected_unless_it_declares_tools() {
2195 for (label, message) in [
2198 ("nothing", serde_json::json!({"role": "system"})),
2199 (
2200 "empty tools",
2201 serde_json::json!({"role": "system", "tools": []}),
2202 ),
2203 ] {
2204 let error =
2205 serde_json::from_value::<ChatCompletionRequestMessage>(message).expect_err(label);
2206 assert!(
2207 error.to_string().starts_with("missing field `content`"),
2208 "{label}: unexpected error {error}"
2209 );
2210 }
2211 }
2212
2213 #[test]
2214 fn system_message_guard_leaves_valid_shapes_alone() {
2215 for (label, message) in [
2216 (
2217 "content only",
2218 serde_json::json!({"role": "system", "content": "hi"}),
2219 ),
2220 (
2221 "content parts",
2222 serde_json::json!({"role": "system", "content": [{"type": "text", "text": "hi"}]}),
2223 ),
2224 (
2225 "tools only",
2226 serde_json::json!({"role": "system", "tools": [{"name": "lookup"}]}),
2227 ),
2228 (
2229 "content and tools (renderer decides)",
2230 serde_json::json!({"role": "system", "content": "hi", "tools": [{"name": "lookup"}]}),
2231 ),
2232 ] {
2233 let parsed: ChatCompletionRequestMessage =
2234 serde_json::from_value(message).unwrap_or_else(|e| panic!("{label}: {e}"));
2235 assert!(
2236 matches!(parsed, ChatCompletionRequestMessage::System(_)),
2237 "{label}"
2238 );
2239 }
2240 }
2241
2242 #[test]
2243 fn message_rejects_tools_and_partial_on_wrong_roles() {
2244 let tools = serde_json::json!([{"name": "lookup"}]);
2245 for (label, message, needle) in [
2246 (
2247 "tools on user",
2248 serde_json::json!({"role": "user", "content": "hi", "tools": tools}),
2249 "`tools` is only accepted on system messages, not on role user",
2250 ),
2251 (
2252 "tools on assistant",
2253 serde_json::json!({"role": "assistant", "content": "hi", "tools": tools}),
2254 "`tools` is only accepted on system messages, not on role assistant",
2255 ),
2256 (
2257 "tools on developer",
2259 serde_json::json!({"role": "developer", "content": "hi", "tools": tools}),
2260 "`tools` is only accepted on system messages, not on role developer",
2261 ),
2262 (
2263 "partial on user",
2264 serde_json::json!({"role": "user", "content": "hi", "partial": true}),
2265 "`partial` is only accepted on assistant messages, not on role user",
2266 ),
2267 (
2268 "partial on system",
2269 serde_json::json!({"role": "system", "content": "hi", "partial": false}),
2270 "`partial` is only accepted on assistant messages, not on role system",
2271 ),
2272 ] {
2273 let error = serde_json::from_value::<ChatCompletionRequestMessage>(message)
2274 .expect_err(label)
2275 .to_string();
2276 assert!(error.contains(needle), "{label}: {error}");
2277 }
2278
2279 for message in [
2280 serde_json::json!({"role": "user", "content": "hi", "tools": null}),
2281 serde_json::json!({"role": "user", "content": "hi", "partial": null}),
2282 ] {
2283 serde_json::from_value::<ChatCompletionRequestMessage>(message).unwrap();
2284 }
2285
2286 for message in [
2287 serde_json::json!({"role": "system", "tools": tools}),
2288 serde_json::json!({"role": "assistant", "content": "seed", "partial": true}),
2289 serde_json::json!({"role": "user", "content": "hi", "x_vendor": 1}),
2290 ] {
2291 serde_json::from_value::<ChatCompletionRequestMessage>(message).unwrap();
2292 }
2293 }
2294
2295 #[test]
2296 fn message_rejects_duplicate_top_level_keys() {
2297 for (label, raw) in [
2298 (
2299 "role twice",
2300 r#"{"role":"user","content":"hi","role":"system"}"#,
2301 ),
2302 (
2303 "content twice",
2304 r#"{"role":"user","content":"a","content":"b"}"#,
2305 ),
2306 ] {
2307 let error = serde_json::from_str::<ChatCompletionRequestMessage>(raw)
2308 .expect_err(label)
2309 .to_string();
2310 assert!(error.contains("duplicate field"), "{label}: {error}");
2311 }
2312 }
2313
2314 #[test]
2315 fn message_rejects_duplicate_fields_in_nested_typed_objects() {
2316 let tool_call = r#"{
2317 "role":"assistant",
2318 "content":null,
2319 "tool_calls":[{
2320 "id":"first",
2321 "id":"second",
2322 "type":"function",
2323 "function":{"name":"lookup","arguments":"{}"}
2324 }]
2325 }"#;
2326 let error = serde_json::from_str::<ChatCompletionRequestMessage>(tool_call)
2327 .unwrap_err()
2328 .to_string();
2329 assert!(error.contains("duplicate field `id`"), "{error}");
2330
2331 let content_part = r#"{
2332 "role":"user",
2333 "content":[{"type":"text","text":"first","text":"second"}]
2334 }"#;
2335 assert!(serde_json::from_str::<ChatCompletionRequestMessage>(content_part).is_err());
2336 }
2337
2338 #[test]
2339 fn default_system_message_round_trips() {
2340 let message = ChatCompletionRequestSystemMessage::default();
2341 let json = serde_json::to_value(&message).unwrap();
2342 assert_eq!(json, serde_json::json!({"content": ""}));
2343 let back: ChatCompletionRequestSystemMessage = serde_json::from_value(json).unwrap();
2344 assert_eq!(back, message);
2345
2346 let built = ChatCompletionRequestSystemMessageArgs::default()
2347 .name("ops")
2348 .build()
2349 .unwrap();
2350 let json = serde_json::to_value(&built).unwrap();
2351 assert_eq!(json, serde_json::json!({"content": "", "name": "ops"}));
2352 serde_json::from_value::<ChatCompletionRequestSystemMessage>(json).unwrap();
2353 }
2354
2355 #[test]
2356 fn system_message_guard_keeps_field_level_errors() {
2357 let error = serde_json::from_value::<ChatCompletionRequestMessage>(serde_json::json!({
2358 "role": "system",
2359 "tools": "lookup"
2360 }))
2361 .unwrap_err();
2362 assert!(
2363 !error.to_string().starts_with("missing field `content`"),
2364 "field error expected, got {error}"
2365 );
2366 }
2367
2368 #[test]
2369 fn system_message_canonicalizes_missing_content_with_tools() {
2370 let request: CreateChatCompletionRequest = serde_json::from_value(serde_json::json!({
2371 "model": "dummy-kimi-model",
2372 "messages": [
2373 {
2374 "role": "system",
2375 "tools": [
2376 {
2377 "name": "lookup",
2378 "description": "dummy lookup tool",
2379 "parameters": {
2380 "type": "object",
2381 "properties": {
2382 "query": { "type": "string" }
2383 }
2384 }
2385 }
2386 ]
2387 },
2388 {
2389 "role": "assistant",
2390 "content": "synthetic prefill",
2391 "partial": true
2392 },
2393 {
2394 "role": "user",
2395 "content": "continue"
2396 }
2397 ]
2398 }))
2399 .unwrap();
2400
2401 match &request.messages[0] {
2402 ChatCompletionRequestMessage::System(system) => {
2403 assert_eq!(
2404 system.content,
2405 ChatCompletionRequestSystemMessageContent::Text(String::new())
2406 );
2407 let tools = system.tools.as_ref().expect("tools should be present");
2408 assert_eq!(tools.len(), 1);
2409 assert_eq!(tools[0]["name"], "lookup");
2410 }
2411 other => panic!("expected system message, got {other:?}"),
2412 }
2413
2414 match &request.messages[1] {
2415 ChatCompletionRequestMessage::Assistant(assistant) => {
2416 assert_eq!(assistant.partial, Some(true));
2417 }
2418 other => panic!("expected assistant message, got {other:?}"),
2419 }
2420
2421 let message: ChatCompletionRequestMessage = serde_json::from_value(serde_json::json!({
2424 "role": "system",
2425 "content": null,
2426 "tools": [{"name": "lookup"}]
2427 }))
2428 .unwrap();
2429 let ChatCompletionRequestMessage::System(system) = &message else {
2430 panic!("expected system message");
2431 };
2432 assert_eq!(
2433 system.content,
2434 ChatCompletionRequestSystemMessageContent::Text(String::new())
2435 );
2436 assert_eq!(
2437 serde_json::to_value(message).unwrap(),
2438 serde_json::json!({
2439 "role": "system",
2440 "content": "",
2441 "tools": [{"name": "lookup"}]
2442 })
2443 );
2444 }
2445
2446 #[test]
2447 fn kimi_style_request_preserves_tools_and_canonicalizes_content() {
2448 let payload = serde_json::json!({
2449 "model": "dummy-kimi-model",
2450 "messages": [
2451 {
2452 "role": "system",
2453 "tools": [
2454 {
2455 "name": "lookup",
2456 "description": "dummy lookup tool",
2457 "parameters": {
2458 "type": "object",
2459 "properties": {
2460 "query": { "type": "string" }
2461 }
2462 },
2463 "vendor_hint": { "priority": 3 }
2464 }
2465 ]
2466 },
2467 {
2468 "role": "assistant",
2469 "content": "synthetic prefill",
2470 "partial": true
2471 },
2472 {
2473 "role": "user",
2474 "content": "continue"
2475 }
2476 ]
2477 });
2478
2479 let request: CreateChatCompletionRequest = serde_json::from_value(payload.clone()).unwrap();
2480 let serialized = serde_json::to_value(request).unwrap();
2481 let mut canonical = payload;
2482 canonical["messages"][0]["content"] = serde_json::json!("");
2483
2484 assert_eq!(serialized, canonical);
2485 }
2486
2487 #[test]
2488 fn system_message_tools_preserve_official_wrapped_shape() {
2489 let payload = serde_json::json!({
2490 "model": "dummy-kimi-model",
2491 "messages": [
2492 {
2493 "role": "system",
2494 "tools": [
2495 {
2496 "type": "function",
2497 "function": {
2498 "name": "lookup",
2499 "description": "dummy lookup tool",
2500 "parameters": {
2501 "type": "object",
2502 "properties": {
2503 "query": { "type": "string" }
2504 },
2505 "required": ["query"]
2506 },
2507 "strict": true
2508 }
2509 }
2510 ]
2511 },
2512 { "role": "user", "content": "continue" }
2513 ]
2514 });
2515
2516 let request: CreateChatCompletionRequest = serde_json::from_value(payload.clone()).unwrap();
2517 match &request.messages[0] {
2518 ChatCompletionRequestMessage::System(system) => {
2519 let tools = system.tools.as_ref().expect("tools should be present");
2520 assert_eq!(tools[0]["type"], "function");
2521 assert_eq!(tools[0]["function"]["name"], "lookup");
2522 }
2523 other => panic!("expected system message, got {other:?}"),
2524 }
2525
2526 let mut canonical = payload;
2527 canonical["messages"][0]["content"] = serde_json::json!("");
2528 assert_eq!(serde_json::to_value(request).unwrap(), canonical);
2529 }
2530
2531 #[test]
2532 fn assistant_message_omits_partial_when_absent() {
2533 let assistant = ChatCompletionRequestAssistantMessageArgs::default()
2534 .content("hello")
2535 .build()
2536 .unwrap();
2537
2538 assert_eq!(assistant.partial, None);
2539 let json = serde_json::to_value(&assistant).unwrap();
2540 assert!(
2541 json.get("partial").is_none(),
2542 "partial should be omitted when absent"
2543 );
2544 }
2545
2546 #[test]
2547 fn assistant_message_serializes_partial_when_present() {
2548 let assistant = ChatCompletionRequestAssistantMessageArgs::default()
2549 .content("synthetic prefill")
2550 .partial(true)
2551 .build()
2552 .unwrap();
2553
2554 let json = serde_json::to_value(&assistant).unwrap();
2555 assert_eq!(json["partial"], true);
2556
2557 let roundtrip: ChatCompletionRequestAssistantMessage =
2558 serde_json::from_value(json).unwrap();
2559 assert_eq!(roundtrip, assistant);
2560 }
2561
2562 #[test]
2563 fn system_message_from_upstream_preserves_content_and_leaves_tools_none() {
2564 let upstream = async_openai::types::chat::ChatCompletionRequestSystemMessage {
2565 content: async_openai::types::chat::ChatCompletionRequestSystemMessageContent::Text(
2566 "hi".into(),
2567 ),
2568 name: None,
2569 };
2570
2571 let owned: ChatCompletionRequestSystemMessage = upstream.into();
2572 assert!(owned.tools.is_none());
2573 match owned.content {
2574 ChatCompletionRequestSystemMessageContent::Text(text) => assert_eq!(text, "hi"),
2575 other => panic!("expected text content, got {other:?}"),
2576 }
2577 }
2578
2579 #[test]
2580 fn system_message_restores_upstream_convenience_conversions() {
2581 let from_content = ChatCompletionRequestSystemMessage::from(
2582 ChatCompletionRequestSystemMessageContent::Text("from content".into()),
2583 );
2584 let from_str = ChatCompletionRequestSystemMessage::from("from str");
2585 let from_string = ChatCompletionRequestSystemMessage::from(String::from("from string"));
2586
2587 for (message, expected) in [
2588 (from_content, "from content"),
2589 (from_str, "from str"),
2590 (from_string, "from string"),
2591 ] {
2592 assert_eq!(
2593 message.content,
2594 ChatCompletionRequestSystemMessageContent::Text(expected.into())
2595 );
2596 assert!(message.name.is_none());
2597 assert!(message.tools.is_none());
2598 }
2599 }
2600}