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,
26 ChatCompletionAudioFormat,
27 ChatCompletionAudioVoice,
28 ChatCompletionFunctionCall,
29 ChatCompletionFunctions,
30 ChatCompletionFunctionsArgs,
31 ChatCompletionRequestAssistantMessageAudio,
32 ChatCompletionRequestAssistantMessageContent,
33 ChatCompletionRequestAssistantMessageContentPart,
34 ChatCompletionRequestDeveloperMessage,
35 ChatCompletionRequestDeveloperMessageArgs,
36 ChatCompletionRequestDeveloperMessageContent,
37 ChatCompletionRequestFunctionMessage,
38 ChatCompletionRequestFunctionMessageArgs,
39 ChatCompletionRequestMessageContentPartAudio,
40 ChatCompletionRequestMessageContentPartRefusal,
41 ChatCompletionRequestMessageContentPartText,
42 ChatCompletionRequestSystemMessage,
43 ChatCompletionRequestSystemMessageArgs,
45 ChatCompletionRequestSystemMessageContent,
46 ChatCompletionRequestSystemMessageContentPart,
47 ChatCompletionResponseMessageAudio,
48 Choice,
49 CompletionFinishReason,
50 CompletionTokensDetails,
51 CompletionUsage,
52 FunctionObject,
53 FunctionObjectArgs,
54 ImageDetail,
55 InputAudio,
56 InputAudioFormat,
57 Logprobs,
58 PredictionContent,
59 PredictionContentContent,
60 Prompt,
61 PromptTokensDetails,
62 ResponseFormat,
63 ResponseFormatJsonSchema,
64 Role,
65 ServiceTier,
66 TopLogprobs,
67 WebSearchContextSize,
68 WebSearchLocation,
69 WebSearchOptions,
70 WebSearchUserLocation,
71 WebSearchUserLocationType,
72};
73
74#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
82#[serde(untagged)]
83pub enum Stop {
84 String(String),
85 StringArray(Vec<String>),
86 TokenIdArray(Vec<u32>),
87}
88
89impl Stop {
90 pub fn strings(&self) -> Option<Vec<String>> {
91 match self {
92 Stop::String(s) => Some(vec![s.clone()]),
93 Stop::StringArray(arr) => Some(arr.clone()),
94 Stop::TokenIdArray(_) => None,
95 }
96 }
97
98 pub fn token_ids(&self) -> Option<Vec<u32>> {
99 match self {
100 Stop::TokenIdArray(arr) => Some(arr.clone()),
101 Stop::String(_) | Stop::StringArray(_) => None,
102 }
103 }
104}
105
106impl From<String> for Stop {
107 fn from(value: String) -> Self {
108 Stop::String(value)
109 }
110}
111
112impl From<&str> for Stop {
113 fn from(value: &str) -> Self {
114 Stop::String(value.to_string())
115 }
116}
117
118impl From<Vec<String>> for Stop {
119 fn from(value: Vec<String>) -> Self {
120 Stop::StringArray(value)
121 }
122}
123
124impl From<Vec<u32>> for Stop {
125 fn from(value: Vec<u32>) -> Self {
126 Stop::TokenIdArray(value)
127 }
128}
129
130impl From<async_openai::types::chat::StopConfiguration> for Stop {
131 fn from(value: async_openai::types::chat::StopConfiguration) -> Self {
132 match value {
133 async_openai::types::chat::StopConfiguration::String(value) => Stop::String(value),
134 async_openai::types::chat::StopConfiguration::StringArray(value) => {
135 Stop::StringArray(value)
136 }
137 }
138 }
139}
140
141pub use async_openai::types::chat::FinishReason;
143
144pub use async_openai::types::chat::FunctionType;
147
148#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
154#[serde(rename_all = "lowercase")]
155pub enum ReasoningEffort {
156 None,
157 Minimal,
158 Low,
159 Medium,
160 High,
161 Xhigh,
162 Max,
163}
164
165impl From<async_openai::types::chat::ReasoningEffort> for ReasoningEffort {
166 fn from(value: async_openai::types::chat::ReasoningEffort) -> Self {
167 match value {
168 async_openai::types::chat::ReasoningEffort::None => ReasoningEffort::None,
169 async_openai::types::chat::ReasoningEffort::Minimal => ReasoningEffort::Minimal,
170 async_openai::types::chat::ReasoningEffort::Low => ReasoningEffort::Low,
171 async_openai::types::chat::ReasoningEffort::Medium => ReasoningEffort::Medium,
172 async_openai::types::chat::ReasoningEffort::High => ReasoningEffort::High,
173 async_openai::types::chat::ReasoningEffort::Xhigh => ReasoningEffort::Xhigh,
174 }
175 }
176}
177
178fn deserialize_arguments<'de, D>(deserializer: D) -> Result<String, D::Error>
187where
188 D: serde::Deserializer<'de>,
189{
190 use serde::de::Error;
191 let value = serde_json::Value::deserialize(deserializer)?;
192 match value {
193 serde_json::Value::String(s) => Ok(s),
194 v @ serde_json::Value::Object(_) => {
195 Ok(serde_json::to_string(&v).unwrap())
197 }
198 other => Err(D::Error::custom(format!(
199 "expected string or object for `arguments`, got {other}"
200 ))),
201 }
202}
203
204fn deserialize_arguments_opt<'de, D>(deserializer: D) -> Result<Option<String>, D::Error>
205where
206 D: serde::Deserializer<'de>,
207{
208 use serde::de::Error;
209 let value = Option::<serde_json::Value>::deserialize(deserializer)?;
210 match value {
211 None => Ok(None),
212 Some(serde_json::Value::String(s)) => Ok(Some(s)),
213 Some(v @ serde_json::Value::Object(_)) => serde_json::to_string(&v)
214 .map(Some)
215 .map_err(|e| D::Error::custom(e.to_string())),
216 Some(other) => Err(D::Error::custom(format!(
217 "expected string or object for `arguments`, got {other}"
218 ))),
219 }
220}
221
222fn deserialize_optional_media<'de, D, T>(deserializer: D) -> Result<Option<T>, D::Error>
228where
229 D: serde::Deserializer<'de>,
230 T: serde::de::DeserializeOwned,
231{
232 use serde::de::Error;
233 match Option::<serde_json::Value>::deserialize(deserializer)? {
234 None => Ok(None),
235 Some(value) if value.get("url").and_then(serde_json::Value::as_str) == Some("") => Ok(None),
236 Some(value) => serde_json::from_value(value)
237 .map(Some)
238 .map_err(D::Error::custom),
239 }
240}
241
242#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Default)]
255pub struct FunctionCall {
256 pub name: String,
257 #[serde(deserialize_with = "deserialize_arguments")]
258 pub arguments: String,
259}
260
261#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Default)]
265pub struct FunctionCallStream {
266 #[serde(skip_serializing_if = "Option::is_none")]
267 pub name: Option<String>,
268 #[serde(
269 default,
270 skip_serializing_if = "Option::is_none",
271 deserialize_with = "deserialize_arguments_opt"
272 )]
273 pub arguments: Option<String>,
274}
275
276#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Default)]
282pub struct ChatCompletionMessageToolCallChunk {
283 pub index: u32,
284 #[serde(skip_serializing_if = "Option::is_none")]
287 pub id: Option<String>,
288 #[serde(skip_serializing_if = "Option::is_none")]
289 pub r#type: Option<FunctionType>,
290 #[serde(skip_serializing_if = "Option::is_none")]
291 pub function: Option<FunctionCallStream>,
292}
293
294#[derive(Debug, Serialize, Deserialize, Clone, Builder, PartialEq)]
307#[builder(name = "ChatCompletionRequestMessageContentPartImageArgs")]
308#[builder(pattern = "mutable")]
309#[builder(setter(into, strip_option))]
310#[builder(derive(Debug))]
311#[builder(build_fn(error = "OpenAIError"))]
312pub struct ChatCompletionRequestMessageContentPartImage {
313 #[builder(default)]
314 #[serde(default, deserialize_with = "deserialize_optional_media")]
315 pub image_url: Option<ImageUrl>,
316 #[builder(default)]
317 #[serde(skip_serializing_if = "Option::is_none")]
318 pub uuid: Option<String>,
320}
321
322#[derive(Debug, Serialize, Deserialize, Clone, Builder, PartialEq)]
327#[builder(name = "ImageUrlArgs")]
328#[builder(pattern = "mutable")]
329#[builder(setter(into, strip_option))]
330#[builder(derive(Debug))]
331#[builder(build_fn(error = "OpenAIError"))]
332pub struct ImageUrl {
333 pub url: Url,
334 pub detail: Option<ImageDetail>,
335 #[deprecated(note = "use the content-part `uuid` field for vLLM cache identities")]
336 #[serde(skip_serializing_if = "Option::is_none")]
337 pub uuid: Option<Uuid>,
338}
339
340#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
346#[serde(tag = "type")]
347#[serde(rename_all = "snake_case")]
348pub enum ChatCompletionRequestToolMessageContentPart {
349 Text(ChatCompletionRequestMessageContentPartText),
350 ImageUrl(ChatCompletionRequestMessageContentPartImage),
351 VideoUrl(ChatCompletionRequestMessageContentPartVideo),
352 AudioUrl(ChatCompletionRequestMessageContentPartAudioUrl),
353}
354
355#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
357#[serde(untagged)]
358pub enum ChatCompletionRequestToolMessageContent {
359 Text(String),
360 Array(Vec<ChatCompletionRequestToolMessageContentPart>),
361}
362
363impl Default for ChatCompletionRequestToolMessageContent {
364 fn default() -> Self {
365 Self::Text(String::new())
366 }
367}
368
369impl From<&str> for ChatCompletionRequestToolMessageContent {
370 fn from(value: &str) -> Self {
371 Self::Text(value.into())
372 }
373}
374
375impl From<String> for ChatCompletionRequestToolMessageContent {
376 fn from(value: String) -> Self {
377 Self::Text(value)
378 }
379}
380
381#[derive(Debug, Serialize, Deserialize, Default, Clone, Builder, PartialEq)]
383#[builder(name = "ChatCompletionRequestToolMessageArgs")]
384#[builder(pattern = "mutable")]
385#[builder(setter(into, strip_option), default)]
386#[builder(derive(Debug))]
387#[builder(build_fn(error = "OpenAIError"))]
388pub struct ChatCompletionRequestToolMessage {
389 pub content: ChatCompletionRequestToolMessageContent,
390 pub tool_call_id: String,
391}
392
393#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
394pub struct ChatChoiceLogprobs {
395 pub content: Option<Vec<ChatCompletionTokenLogprob>>,
396 pub refusal: Option<Vec<ChatCompletionTokenLogprob>>,
397}
398
399#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
405pub struct ChatCompletionTokenLogprob {
406 pub token: String,
407 pub logprob: f32,
408 #[serde(skip_serializing_if = "Option::is_none")]
409 pub token_id: Option<u32>,
410 pub bytes: Option<Vec<u8>>,
411 pub top_logprobs: Vec<TopLogprobs>,
412}
413
414#[derive(Clone, Serialize, Default, Debug, Deserialize, PartialEq)]
415#[serde(rename_all = "lowercase")]
416pub enum ChatCompletionToolType {
417 #[default]
418 Function,
419}
420
421#[derive(Clone, Serialize, Default, Debug, Deserialize, PartialEq)]
422pub struct FunctionName {
423 pub name: String,
424}
425
426#[derive(Clone, Serialize, Default, Debug, Deserialize, PartialEq)]
427pub struct ChatCompletionNamedToolChoice {
428 pub r#type: ChatCompletionToolType,
429 pub function: FunctionName,
430}
431
432fn default_function_type() -> FunctionType {
433 FunctionType::Function
434}
435
436#[derive(Clone, Serialize, Debug, Deserialize, PartialEq)]
442pub struct ChatCompletionMessageToolCall {
443 pub id: String,
444 #[serde(default = "default_function_type")]
445 pub r#type: FunctionType,
446 pub function: FunctionCall,
447}
448
449#[derive(Clone, Serialize, Default, Debug, Deserialize, PartialEq)]
451#[serde(rename_all = "lowercase")]
452pub enum ChatCompletionToolChoiceOption {
453 #[default]
454 None,
455 Auto,
456 Required,
457 #[serde(untagged)]
458 Named(ChatCompletionNamedToolChoice),
459}
460
461#[derive(Clone, Serialize, Default, Debug, Builder, Deserialize, PartialEq)]
462#[builder(name = "ChatCompletionToolArgs")]
463#[builder(pattern = "mutable")]
464#[builder(setter(into, strip_option), default)]
465#[builder(derive(Debug))]
466#[builder(build_fn(error = "OpenAIError"))]
467pub struct ChatCompletionTool {
468 #[builder(default = "ChatCompletionToolType::Function")]
469 pub r#type: ChatCompletionToolType,
470 pub function: FunctionObject,
471}
472
473#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
484#[serde(untagged)]
485pub enum StopReason {
486 String(String),
487 Int(i64),
488 IntArray(Vec<i64>),
489}
490
491#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
501#[serde(untagged)]
502pub enum ReasoningContent {
503 Text(String),
505 Segments(Vec<String>),
508}
509
510impl ReasoningContent {
511 pub fn to_flat_string(&self) -> String {
513 match self {
514 ReasoningContent::Text(s) => s.clone(),
515 ReasoningContent::Segments(segs) => segs
516 .iter()
517 .filter(|s| !s.is_empty())
518 .cloned()
519 .collect::<Vec<_>>()
520 .join("\n"),
521 }
522 }
523
524 pub fn segments(&self) -> Option<&[String]> {
526 match self {
527 ReasoningContent::Segments(segs) => Some(segs),
528 ReasoningContent::Text(_) => None,
529 }
530 }
531}
532
533#[derive(Clone, Serialize, Debug, Deserialize, PartialEq)]
537pub struct ChatCompletionResponseContentPartText {
538 pub text: String,
539}
540
541#[derive(Clone, Serialize, Debug, Deserialize, PartialEq)]
543pub struct ChatCompletionResponseContentPartImageUrl {
544 pub image_url: ImageUrlResponse,
545}
546
547#[derive(Clone, Serialize, Debug, Deserialize, PartialEq)]
549pub struct ChatCompletionResponseContentPartVideoUrl {
550 pub video_url: VideoUrlResponse,
551}
552
553#[derive(Clone, Serialize, Debug, Deserialize, PartialEq)]
555pub struct ChatCompletionResponseContentPartAudioUrl {
556 pub audio_url: AudioUrlResponse,
557}
558
559#[derive(Clone, Serialize, Debug, Deserialize, PartialEq)]
560pub struct ImageUrlResponse {
561 pub url: String,
562 #[serde(skip_serializing_if = "Option::is_none")]
563 pub detail: Option<String>,
564}
565
566#[derive(Clone, Serialize, Debug, Deserialize, PartialEq)]
567pub struct VideoUrlResponse {
568 pub url: String,
569}
570
571#[derive(Clone, Serialize, Debug, Deserialize, PartialEq)]
572pub struct AudioUrlResponse {
573 pub url: String,
574}
575
576#[derive(Clone, Serialize, Debug, Deserialize, PartialEq)]
578#[serde(tag = "type", rename_all = "snake_case")]
579pub enum ChatCompletionResponseContentPart {
580 Text(ChatCompletionResponseContentPartText),
581 ImageUrl(ChatCompletionResponseContentPartImageUrl),
582 VideoUrl(ChatCompletionResponseContentPartVideoUrl),
583 AudioUrl(ChatCompletionResponseContentPartAudioUrl),
584}
585
586#[derive(Clone, Serialize, Debug, Deserialize, PartialEq)]
592#[serde(untagged)]
593pub enum ChatCompletionMessageContent {
594 Text(String),
596 Parts(Vec<ChatCompletionResponseContentPart>),
598}
599
600#[derive(Debug, Serialize, Deserialize, Clone, Builder, PartialEq)]
603#[builder(name = "VideoUrlArgs")]
604#[builder(pattern = "mutable")]
605#[builder(setter(into, strip_option))]
606#[builder(derive(Debug))]
607#[builder(build_fn(error = "OpenAIError"))]
608pub struct VideoUrl {
609 pub url: Url,
610 pub detail: Option<ImageDetail>,
611 #[deprecated(note = "use the content-part `uuid` field for vLLM cache identities")]
612 #[serde(skip_serializing_if = "Option::is_none")]
613 pub uuid: Option<Uuid>,
614}
615
616#[derive(Debug, Serialize, Deserialize, Clone, Builder, PartialEq)]
617#[builder(name = "ChatCompletionRequestMessageContentPartVideoArgs")]
618#[builder(pattern = "mutable")]
619#[builder(setter(into, strip_option))]
620#[builder(derive(Debug))]
621#[builder(build_fn(error = "OpenAIError"))]
622pub struct ChatCompletionRequestMessageContentPartVideo {
623 #[builder(default)]
624 #[serde(default, deserialize_with = "deserialize_optional_media")]
625 pub video_url: Option<VideoUrl>,
626 #[builder(default)]
627 #[serde(skip_serializing_if = "Option::is_none")]
628 pub uuid: Option<String>,
630}
631
632#[derive(Debug, Serialize, Deserialize, Clone, Builder, PartialEq)]
633#[builder(name = "AudioUrlArgs")]
634#[builder(pattern = "mutable")]
635#[builder(setter(into, strip_option))]
636#[builder(derive(Debug))]
637#[builder(build_fn(error = "OpenAIError"))]
638pub struct AudioUrl {
639 pub url: Url,
640 #[deprecated(note = "use the content-part `uuid` field for vLLM cache identities")]
641 #[serde(skip_serializing_if = "Option::is_none")]
642 pub uuid: Option<Uuid>,
643}
644
645#[derive(Debug, Serialize, Deserialize, Clone, Builder, PartialEq)]
646#[builder(name = "ChatCompletionRequestMessageContentPartAudioUrlArgs")]
647#[builder(pattern = "mutable")]
648#[builder(setter(into, strip_option))]
649#[builder(derive(Debug))]
650#[builder(build_fn(error = "OpenAIError"))]
651pub struct ChatCompletionRequestMessageContentPartAudioUrl {
652 #[builder(default)]
653 #[serde(default, deserialize_with = "deserialize_optional_media")]
654 pub audio_url: Option<AudioUrl>,
655 #[builder(default)]
656 #[serde(skip_serializing_if = "Option::is_none")]
657 pub uuid: Option<String>,
659}
660
661#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
665#[serde(untagged)]
666pub enum ChatCompletionRequestUserMessageContent {
667 Text(String),
668 Array(Vec<ChatCompletionRequestUserMessageContentPart>),
669}
670
671#[derive(Debug, Serialize, Deserialize, Default, Clone, Builder, PartialEq)]
672#[builder(name = "ChatCompletionRequestUserMessageArgs")]
673#[builder(pattern = "mutable")]
674#[builder(setter(into, strip_option), default)]
675#[builder(derive(Debug))]
676#[builder(build_fn(error = "OpenAIError"))]
677pub struct ChatCompletionRequestUserMessage {
678 pub content: ChatCompletionRequestUserMessageContent,
679 #[serde(skip_serializing_if = "Option::is_none")]
680 pub name: Option<String>,
681}
682
683impl Default for ChatCompletionRequestUserMessageContent {
684 fn default() -> Self {
685 Self::Text(String::new())
686 }
687}
688
689impl From<&str> for ChatCompletionRequestUserMessageContent {
690 fn from(value: &str) -> Self {
691 Self::Text(value.into())
692 }
693}
694
695impl From<String> for ChatCompletionRequestUserMessageContent {
696 fn from(value: String) -> Self {
697 Self::Text(value)
698 }
699}
700
701impl From<Vec<ChatCompletionRequestUserMessageContentPart>>
702 for ChatCompletionRequestUserMessageContent
703{
704 fn from(value: Vec<ChatCompletionRequestUserMessageContentPart>) -> Self {
705 Self::Array(value)
706 }
707}
708
709#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
715#[serde(tag = "type")]
716#[serde(rename_all = "snake_case")]
717pub enum ChatCompletionRequestUserMessageContentPart {
718 Text(ChatCompletionRequestMessageContentPartText),
719 ImageUrl(ChatCompletionRequestMessageContentPartImage),
720 VideoUrl(ChatCompletionRequestMessageContentPartVideo),
721 AudioUrl(ChatCompletionRequestMessageContentPartAudioUrl),
722 InputAudio(ChatCompletionRequestMessageContentPartAudio),
723}
724
725#[derive(Debug, Serialize, Deserialize, Default, Clone, Builder, PartialEq)]
731#[builder(name = "ChatCompletionRequestAssistantMessageArgs")]
732#[builder(pattern = "mutable")]
733#[builder(setter(into, strip_option), default)]
734#[builder(derive(Debug))]
735#[builder(build_fn(error = "OpenAIError"))]
736pub struct ChatCompletionRequestAssistantMessage {
737 #[serde(skip_serializing_if = "Option::is_none")]
738 pub content: Option<ChatCompletionRequestAssistantMessageContent>,
739 #[serde(default, alias = "reasoning", skip_serializing_if = "Option::is_none")]
745 pub reasoning_content: Option<ReasoningContent>,
746 #[serde(skip_serializing_if = "Option::is_none")]
747 pub refusal: Option<String>,
748 #[serde(skip_serializing_if = "Option::is_none")]
749 pub name: Option<String>,
750 #[serde(skip_serializing_if = "Option::is_none")]
751 pub audio: Option<ChatCompletionRequestAssistantMessageAudio>,
752 #[serde(skip_serializing_if = "Option::is_none")]
753 pub tool_calls: Option<Vec<ChatCompletionMessageToolCall>>,
754 #[deprecated]
755 #[serde(skip_serializing_if = "Option::is_none")]
756 pub function_call: Option<FunctionCall>,
757}
758
759#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
765#[serde(tag = "role")]
766#[serde(rename_all = "lowercase")]
767pub enum ChatCompletionRequestMessage {
768 Developer(ChatCompletionRequestDeveloperMessage),
769 System(ChatCompletionRequestSystemMessage),
770 User(ChatCompletionRequestUserMessage),
771 Assistant(ChatCompletionRequestAssistantMessage),
772 Tool(ChatCompletionRequestToolMessage),
773 Function(ChatCompletionRequestFunctionMessage),
774}
775
776pub type ServiceTierResponse = ServiceTier;
778
779#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
785pub struct ChatCompletionResponseMessage {
786 pub content: Option<ChatCompletionMessageContent>,
790 pub refusal: Option<String>,
794 #[serde(skip_serializing_if = "Option::is_none")]
795 pub tool_calls: Option<Vec<ChatCompletionMessageToolCall>>,
796 pub role: Role,
797 #[serde(skip_serializing_if = "Option::is_none")]
798 #[deprecated]
799 pub function_call: Option<FunctionCall>,
800 #[serde(skip_serializing_if = "Option::is_none")]
801 pub audio: Option<ChatCompletionResponseMessageAudio>,
802 #[serde(default, alias = "reasoning", skip_serializing_if = "Option::is_none")]
809 pub reasoning_content: Option<String>,
810}
811
812#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq)]
817pub struct ChatCompletionStreamOptions {
818 pub include_usage: bool,
819 #[serde(default)]
822 pub continuous_usage_stats: bool,
823}
824
825#[derive(Clone, Serialize, Default, Debug, Builder, Deserialize, PartialEq)]
832#[builder(name = "CreateChatCompletionRequestArgs")]
833#[builder(pattern = "mutable")]
834#[builder(setter(into, strip_option), default)]
835#[builder(derive(Debug))]
836#[builder(build_fn(error = "OpenAIError"))]
837pub struct CreateChatCompletionRequest {
838 pub messages: Vec<ChatCompletionRequestMessage>,
839 pub model: String,
840 #[serde(skip_serializing_if = "Option::is_none")]
842 pub mm_processor_kwargs: Option<serde_json::Value>,
843 #[serde(skip_serializing_if = "Option::is_none")]
844 pub store: Option<bool>,
845 #[serde(skip_serializing_if = "Option::is_none")]
846 pub reasoning_effort: Option<ReasoningEffort>,
847 #[serde(skip_serializing_if = "Option::is_none")]
848 pub metadata: Option<serde_json::Value>,
849 #[serde(skip_serializing_if = "Option::is_none")]
850 pub frequency_penalty: Option<f32>,
851 #[serde(skip_serializing_if = "Option::is_none")]
852 pub logit_bias: Option<std::collections::HashMap<String, serde_json::Value>>,
853 #[serde(skip_serializing_if = "Option::is_none")]
854 pub logprobs: Option<bool>,
855 #[serde(skip_serializing_if = "Option::is_none")]
856 pub top_logprobs: Option<u8>,
857 #[deprecated]
858 #[serde(skip_serializing_if = "Option::is_none")]
859 pub max_tokens: Option<u32>,
860 #[serde(skip_serializing_if = "Option::is_none")]
861 pub max_completion_tokens: Option<u32>,
862 #[serde(skip_serializing_if = "Option::is_none")]
863 pub n: Option<u8>,
864 #[serde(skip_serializing_if = "Option::is_none")]
865 pub modalities: Option<Vec<async_openai::types::chat::ResponseModalities>>,
866 #[serde(skip_serializing_if = "Option::is_none")]
867 pub prediction: Option<PredictionContent>,
868 #[serde(skip_serializing_if = "Option::is_none")]
869 pub audio: Option<ChatCompletionAudio>,
870 #[serde(skip_serializing_if = "Option::is_none")]
871 pub presence_penalty: Option<f32>,
872 #[serde(skip_serializing_if = "Option::is_none")]
873 pub response_format: Option<ResponseFormat>,
874 #[serde(skip_serializing_if = "Option::is_none")]
875 pub seed: Option<i64>,
876 #[serde(skip_serializing_if = "Option::is_none")]
877 pub service_tier: Option<ServiceTier>,
878 #[serde(skip_serializing_if = "Option::is_none")]
879 pub stop: Option<Stop>,
880 #[serde(default, skip_serializing_if = "Option::is_none")]
881 pub stream: Option<bool>,
882 #[serde(skip_serializing_if = "Option::is_none")]
883 pub stream_options: Option<ChatCompletionStreamOptions>,
884 #[serde(skip_serializing_if = "Option::is_none")]
885 pub temperature: Option<f32>,
886 #[serde(skip_serializing_if = "Option::is_none")]
887 pub top_p: Option<f32>,
888 #[serde(skip_serializing_if = "Option::is_none")]
889 pub tools: Option<Vec<ChatCompletionTool>>,
890 #[serde(skip_serializing_if = "Option::is_none")]
891 pub tool_choice: Option<ChatCompletionToolChoiceOption>,
892 #[serde(skip_serializing_if = "Option::is_none")]
893 pub parallel_tool_calls: Option<bool>,
894 #[serde(skip_serializing_if = "Option::is_none")]
895 pub user: Option<String>,
896 #[deprecated]
897 #[serde(skip_serializing_if = "Option::is_none")]
898 pub function_call: Option<ChatCompletionFunctionCall>,
899 #[deprecated]
900 #[serde(skip_serializing_if = "Option::is_none")]
901 pub functions: Option<Vec<ChatCompletionFunctions>>,
902 #[serde(skip_serializing_if = "Option::is_none")]
903 pub web_search_options: Option<WebSearchOptions>,
904}
905
906#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
910pub struct ChatChoice {
911 pub index: u32,
912 pub message: ChatCompletionResponseMessage,
913 pub finish_reason: Option<FinishReason>,
914 pub logprobs: Option<ChatChoiceLogprobs>,
915}
916
917fn serialize_usage_omitting_absent<S>(
927 usage: &Option<CompletionUsage>,
928 serializer: S,
929) -> Result<S::Ok, S::Error>
930where
931 S: serde::Serializer,
932{
933 #[derive(Serialize)]
934 struct PromptDetailsShadow {
935 #[serde(skip_serializing_if = "Option::is_none")]
936 audio_tokens: Option<u32>,
937 #[serde(skip_serializing_if = "Option::is_none")]
938 cached_tokens: Option<u32>,
939 }
940
941 #[derive(Serialize)]
942 struct CompletionDetailsShadow {
943 #[serde(skip_serializing_if = "Option::is_none")]
944 accepted_prediction_tokens: Option<u32>,
945 #[serde(skip_serializing_if = "Option::is_none")]
946 audio_tokens: Option<u32>,
947 #[serde(skip_serializing_if = "Option::is_none")]
948 reasoning_tokens: Option<u32>,
949 #[serde(skip_serializing_if = "Option::is_none")]
950 rejected_prediction_tokens: Option<u32>,
951 }
952
953 #[derive(Serialize)]
954 struct UsageShadow {
955 prompt_tokens: u32,
956 completion_tokens: u32,
957 total_tokens: u32,
958 #[serde(skip_serializing_if = "Option::is_none")]
959 prompt_tokens_details: Option<PromptDetailsShadow>,
960 #[serde(skip_serializing_if = "Option::is_none")]
961 completion_tokens_details: Option<CompletionDetailsShadow>,
962 }
963
964 match usage {
965 None => serializer.serialize_none(),
966 Some(u) => UsageShadow {
967 prompt_tokens: u.prompt_tokens,
968 completion_tokens: u.completion_tokens,
969 total_tokens: u.total_tokens,
970 prompt_tokens_details: u
971 .prompt_tokens_details
972 .as_ref()
973 .map(|d| PromptDetailsShadow {
974 audio_tokens: d.audio_tokens,
975 cached_tokens: d.cached_tokens,
976 }),
977 completion_tokens_details: u.completion_tokens_details.as_ref().map(|d| {
978 CompletionDetailsShadow {
979 accepted_prediction_tokens: d.accepted_prediction_tokens,
980 audio_tokens: d.audio_tokens,
981 reasoning_tokens: d.reasoning_tokens,
982 rejected_prediction_tokens: d.rejected_prediction_tokens,
983 }
984 }),
985 }
986 .serialize(serializer),
987 }
988}
989
990#[derive(Debug, Deserialize, Clone, PartialEq, Serialize)]
997pub struct CreateChatCompletionResponse {
998 pub id: String,
999 pub choices: Vec<ChatChoice>,
1000 pub created: u32,
1001 pub model: String,
1002 #[serde(skip_serializing_if = "Option::is_none")]
1003 pub service_tier: Option<ServiceTierResponse>,
1004 #[serde(skip_serializing_if = "Option::is_none")]
1005 pub system_fingerprint: Option<String>,
1006 pub object: String,
1007 #[serde(
1008 skip_serializing_if = "Option::is_none",
1009 serialize_with = "serialize_usage_omitting_absent"
1010 )]
1011 pub usage: Option<CompletionUsage>,
1012}
1013
1014pub type ChatCompletionResponseStream =
1015 Pin<Box<dyn Stream<Item = Result<CreateChatCompletionStreamResponse, OpenAIError>> + Send>>;
1016
1017#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
1023pub struct ChatCompletionStreamResponseDelta {
1024 #[serde(skip_serializing_if = "Option::is_none")]
1025 pub content: Option<ChatCompletionMessageContent>,
1026 #[serde(skip_serializing_if = "Option::is_none")]
1027 pub function_call: Option<ChatCompletionStreamResponseDeltaFunctionCall>,
1028 #[serde(skip_serializing_if = "Option::is_none")]
1029 pub tool_calls: Option<Vec<ChatCompletionMessageToolCallChunk>>,
1030 #[serde(skip_serializing_if = "Option::is_none")]
1031 pub role: Option<Role>,
1032 #[serde(skip_serializing_if = "Option::is_none")]
1033 pub refusal: Option<String>,
1034 #[serde(default, alias = "reasoning", skip_serializing_if = "Option::is_none")]
1040 pub reasoning_content: Option<String>,
1041}
1042
1043#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
1044pub struct ChatCompletionStreamResponseDeltaFunctionCall {
1045 #[serde(skip_serializing_if = "Option::is_none")]
1046 pub name: Option<String>,
1047 #[serde(
1048 default,
1049 deserialize_with = "deserialize_arguments_opt",
1050 skip_serializing_if = "Option::is_none"
1051 )]
1052 pub arguments: Option<String>,
1053}
1054
1055#[derive(Debug, Deserialize, Clone, PartialEq, Serialize)]
1057pub struct ChatChoiceStream {
1058 pub index: u32,
1059 pub delta: ChatCompletionStreamResponseDelta,
1060 pub finish_reason: Option<FinishReason>,
1061 pub logprobs: Option<ChatChoiceLogprobs>,
1062}
1063
1064#[derive(Debug, Deserialize, Clone, PartialEq, Serialize)]
1072pub struct CreateChatCompletionStreamResponse {
1073 pub id: String,
1074 pub choices: Vec<ChatChoiceStream>,
1075 pub created: u32,
1076 pub model: String,
1077 #[serde(skip_serializing_if = "Option::is_none")]
1078 pub service_tier: Option<ServiceTierResponse>,
1079 #[serde(skip_serializing_if = "Option::is_none")]
1080 pub system_fingerprint: Option<String>,
1081 pub object: String,
1082 #[serde(
1083 skip_serializing_if = "Option::is_none",
1084 serialize_with = "serialize_usage_omitting_absent"
1085 )]
1086 pub usage: Option<CompletionUsage>,
1087}
1088
1089#[cfg(test)]
1090mod tests {
1091 use super::*;
1092
1093 #[test]
1094 fn stop_accepts_token_id_array() {
1095 let stop: Stop = serde_json::from_value(serde_json::json!([32, 34])).unwrap();
1096
1097 assert_eq!(stop, Stop::TokenIdArray(vec![32, 34]));
1098 }
1099
1100 #[test]
1101 fn stop_accepts_string_and_string_array() {
1102 let stop: Stop = serde_json::from_value(serde_json::json!(" The")).unwrap();
1103
1104 assert_eq!(stop, Stop::String(" The".to_string()));
1105
1106 let stop: Stop = serde_json::from_value(serde_json::json!(["A", "B"])).unwrap();
1107
1108 assert_eq!(
1109 stop,
1110 Stop::StringArray(vec!["A".to_string(), "B".to_string()])
1111 );
1112 }
1113
1114 #[test]
1115 fn stop_token_id_display_string_remains_string_stop() {
1116 let stop: Stop = serde_json::from_value(serde_json::json!("token_id:576")).unwrap();
1117
1118 assert_eq!(stop, Stop::String("token_id:576".to_string()));
1119
1120 let stop: Stop = serde_json::from_value(serde_json::json!(["token_id:576"])).unwrap();
1121
1122 assert_eq!(stop, Stop::StringArray(vec!["token_id:576".to_string()]));
1123 }
1124
1125 #[test]
1126 fn stop_rejects_single_token_id() {
1127 let result = serde_json::from_value::<Stop>(serde_json::json!(576));
1128
1129 assert!(result.is_err());
1130 }
1131
1132 #[test]
1133 fn stop_converts_from_upstream_stop_configuration() {
1134 let upstream =
1135 async_openai::types::chat::StopConfiguration::StringArray(vec!["END".to_string()]);
1136
1137 assert_eq!(
1138 Stop::from(upstream),
1139 Stop::StringArray(vec!["END".to_string()])
1140 );
1141 }
1142
1143 #[test]
1144 fn request_builder_accepts_upstream_reasoning_effort() {
1145 let request = CreateChatCompletionRequestArgs::default()
1146 .reasoning_effort(async_openai::types::chat::ReasoningEffort::High)
1147 .build()
1148 .unwrap();
1149
1150 assert_eq!(request.reasoning_effort, Some(ReasoningEffort::High));
1151 }
1152
1153 #[test]
1154 fn tool_call_defaults_type_on_deserialize() {
1155 let tool_call: ChatCompletionMessageToolCall = serde_json::from_value(serde_json::json!({
1156 "id": "call_123",
1157 "function": {
1158 "name": "get_weather",
1159 "arguments": "{\"location\":\"SF\"}"
1160 }
1161 }))
1162 .unwrap();
1163
1164 assert_eq!(tool_call.r#type, FunctionType::Function);
1165 }
1166
1167 #[test]
1168 fn tool_call_serializes_type_for_wire_compat() {
1169 let tool_call = ChatCompletionMessageToolCall {
1170 id: "call_123".into(),
1171 r#type: FunctionType::Function,
1172 function: FunctionCall {
1173 name: "get_weather".into(),
1174 arguments: "{\"location\":\"SF\"}".into(),
1175 },
1176 };
1177
1178 let json = serde_json::to_value(tool_call).unwrap();
1179 assert_eq!(json["type"], "function");
1180 }
1181
1182 #[test]
1185 fn function_call_accepts_string_arguments() {
1186 let fc: FunctionCall = serde_json::from_value(serde_json::json!({
1187 "name": "get_weather",
1188 "arguments": "{\"location\":\"SF\"}"
1189 }))
1190 .unwrap();
1191 assert_eq!(fc.arguments, "{\"location\":\"SF\"}");
1192 }
1193
1194 #[test]
1195 fn function_call_accepts_dict_arguments() {
1196 let fc: FunctionCall = serde_json::from_value(serde_json::json!({
1197 "name": "get_weather",
1198 "arguments": {"location": "SF"}
1199 }))
1200 .unwrap();
1201 assert_eq!(fc.arguments, "{\"location\":\"SF\"}");
1202 }
1203
1204 #[test]
1205 fn function_call_rejects_integer_arguments() {
1206 let result = serde_json::from_value::<FunctionCall>(serde_json::json!({
1207 "name": "f",
1208 "arguments": 42
1209 }));
1210 assert!(result.is_err());
1211 }
1212
1213 #[test]
1214 fn function_call_rejects_boolean_arguments() {
1215 let result = serde_json::from_value::<FunctionCall>(serde_json::json!({
1216 "name": "f",
1217 "arguments": true
1218 }));
1219 assert!(result.is_err());
1220 }
1221
1222 #[test]
1223 fn function_call_rejects_null_arguments() {
1224 let result = serde_json::from_value::<FunctionCall>(serde_json::json!({
1225 "name": "f",
1226 "arguments": null
1227 }));
1228 assert!(result.is_err());
1229 }
1230
1231 #[test]
1232 fn function_call_rejects_array_arguments() {
1233 let result = serde_json::from_value::<FunctionCall>(serde_json::json!({
1234 "name": "f",
1235 "arguments": [1, 2, 3]
1236 }));
1237 assert!(result.is_err());
1238 }
1239
1240 #[test]
1241 fn function_call_stream_null_arguments_produces_none() {
1242 let fcs: FunctionCallStream = serde_json::from_value(serde_json::json!({
1243 "name": "f",
1244 "arguments": null
1245 }))
1246 .unwrap();
1247 assert_eq!(fcs.arguments, None);
1248 }
1249
1250 #[test]
1251 fn function_call_stream_rejects_integer_arguments() {
1252 let result = serde_json::from_value::<FunctionCallStream>(serde_json::json!({
1253 "name": "f",
1254 "arguments": 42
1255 }));
1256 assert!(result.is_err());
1257 }
1258
1259 #[test]
1260 fn function_call_stream_rejects_boolean_arguments() {
1261 let result = serde_json::from_value::<FunctionCallStream>(serde_json::json!({
1262 "name": "f",
1263 "arguments": true
1264 }));
1265 assert!(result.is_err());
1266 }
1267
1268 #[test]
1269 fn function_call_stream_accepts_dict_arguments() {
1270 let fcs: FunctionCallStream = serde_json::from_value(serde_json::json!({
1271 "name": "get_weather",
1272 "arguments": {"location": "SF"}
1273 }))
1274 .unwrap();
1275 assert_eq!(fcs.arguments.as_deref(), Some("{\"location\":\"SF\"}"));
1276 }
1277
1278 #[test]
1279 fn function_call_stream_accepts_null_arguments() {
1280 let fcs: FunctionCallStream = serde_json::from_value(serde_json::json!({
1281 "name": "get_weather"
1282 }))
1283 .unwrap();
1284 assert_eq!(fcs.arguments, None);
1285 }
1286
1287 #[test]
1288 fn tool_call_with_dict_arguments_roundtrip() {
1289 let tc: ChatCompletionMessageToolCall = serde_json::from_value(serde_json::json!({
1290 "id": "call_abc",
1291 "type": "function",
1292 "function": {
1293 "name": "search",
1294 "arguments": {"query": "hello", "limit": 10}
1295 }
1296 }))
1297 .unwrap();
1298 let parsed: serde_json::Value = serde_json::from_str(&tc.function.arguments).unwrap();
1300 assert_eq!(parsed, serde_json::json!({"query": "hello", "limit": 10}));
1301 let json = serde_json::to_value(&tc).unwrap();
1303 assert!(json["function"]["arguments"].is_string());
1304 }
1305
1306 #[test]
1307 fn stream_delta_function_call_accepts_dict_arguments() {
1308 let delta: ChatCompletionStreamResponseDeltaFunctionCall =
1309 serde_json::from_value(serde_json::json!({
1310 "name": "get_weather",
1311 "arguments": {"location": "SF"}
1312 }))
1313 .unwrap();
1314 assert_eq!(delta.arguments.as_deref(), Some("{\"location\":\"SF\"}"));
1315 }
1316
1317 fn parse_content_part(json: serde_json::Value) -> ChatCompletionRequestUserMessageContentPart {
1318 serde_json::from_value(json).expect("content part deserialization failed")
1319 }
1320
1321 #[test]
1322 fn image_url_url_and_top_level_uuid() {
1323 let part = parse_content_part(serde_json::json!({
1324 "type": "image_url",
1325 "image_url": {"url": "https://x.example/y.png"},
1326 "uuid": "image-123"
1327 }));
1328
1329 match part {
1330 ChatCompletionRequestUserMessageContentPart::ImageUrl(part) => {
1331 assert_eq!(part.uuid.as_deref(), Some("image-123"));
1332 assert_eq!(
1333 part.image_url.as_ref().map(|image| image.url.as_str()),
1334 Some("https://x.example/y.png")
1335 );
1336 }
1337 _ => panic!("expected image_url part"),
1338 }
1339 }
1340
1341 #[test]
1342 fn image_url_null_and_top_level_uuid() {
1343 let part = parse_content_part(serde_json::json!({
1344 "type": "image_url",
1345 "image_url": null,
1346 "uuid": "sku-1234-a"
1347 }));
1348
1349 match part {
1350 ChatCompletionRequestUserMessageContentPart::ImageUrl(part) => {
1351 assert!(part.image_url.is_none());
1352 assert_eq!(part.uuid.as_deref(), Some("sku-1234-a"));
1353 }
1354 _ => panic!("expected image_url part"),
1355 }
1356 }
1357
1358 #[test]
1359 fn empty_media_urls_deserialize_as_uuid_only() {
1360 for (part_type, media_field, uuid) in [
1361 ("image_url", "image_url", "image-cache-key"),
1362 ("video_url", "video_url", "video-cache-key"),
1363 ("audio_url", "audio_url", "audio-cache-key"),
1364 ] {
1365 let part = parse_content_part(serde_json::json!({
1366 "type": part_type,
1367 (media_field): {"url": ""},
1368 "uuid": uuid
1369 }));
1370 let json = serde_json::to_value(part).unwrap();
1371
1372 assert!(json[media_field].is_null());
1373 assert_eq!(json["uuid"], uuid);
1374 }
1375 }
1376
1377 #[test]
1378 fn image_url_null_without_uuid_deserializes_for_use_site_validation() {
1379 let part = parse_content_part(serde_json::json!({
1380 "type": "image_url",
1381 "image_url": null
1382 }));
1383
1384 match part {
1385 ChatCompletionRequestUserMessageContentPart::ImageUrl(part) => {
1386 assert!(part.image_url.is_none());
1387 assert!(part.uuid.is_none());
1388 }
1389 _ => panic!("expected image_url part"),
1390 }
1391 }
1392
1393 #[test]
1394 fn image_url_serialize_uuid_only_uses_null_image_url() {
1395 let part = ChatCompletionRequestMessageContentPartImage {
1396 image_url: None,
1397 uuid: Some("image-123".to_string()),
1398 };
1399 let json = serde_json::to_value(part).unwrap();
1400
1401 assert!(json["image_url"].is_null());
1402 assert_eq!(json["uuid"], "image-123");
1403 }
1404
1405 #[test]
1406 fn cached_media_builders_allow_omitting_urls() {
1407 let image = ChatCompletionRequestMessageContentPartImageArgs::default()
1408 .uuid("image-123")
1409 .build()
1410 .unwrap();
1411 let video = ChatCompletionRequestMessageContentPartVideoArgs::default()
1412 .uuid("video-123")
1413 .build()
1414 .unwrap();
1415 let audio = ChatCompletionRequestMessageContentPartAudioUrlArgs::default()
1416 .uuid("audio-123")
1417 .build()
1418 .unwrap();
1419
1420 let image_json = serde_json::to_value(image).unwrap();
1421 let video_json = serde_json::to_value(video).unwrap();
1422 let audio_json = serde_json::to_value(audio).unwrap();
1423 assert!(image_json["image_url"].is_null());
1424 assert!(video_json["video_url"].is_null());
1425 assert!(audio_json["audio_url"].is_null());
1426 }
1427
1428 #[test]
1429 fn image_url_uuid_accepts_opaque_string() {
1430 let part = parse_content_part(serde_json::json!({
1431 "type": "image_url",
1432 "image_url": {"url": "https://x.example/y.png"},
1433 "uuid": "img-ac3921de680bb217"
1434 }));
1435
1436 match part {
1437 ChatCompletionRequestUserMessageContentPart::ImageUrl(part) => {
1438 assert_eq!(part.uuid.as_deref(), Some("img-ac3921de680bb217"));
1439 }
1440 _ => panic!("expected image_url part"),
1441 }
1442 }
1443
1444 #[test]
1445 fn url_conversions_preserve_required_urls() {
1446 let image: ImageUrl = "https://x.example/image.png".into();
1447 let video: VideoUrl = "https://x.example/video.mp4".into();
1448 let audio: AudioUrl = "https://x.example/audio.wav".into();
1449
1450 assert_eq!(image.url.as_str(), "https://x.example/image.png");
1451 assert_eq!(video.url.as_str(), "https://x.example/video.mp4");
1452 assert_eq!(audio.url.as_str(), "https://x.example/audio.wav");
1453 }
1454
1455 #[test]
1456 fn invalid_media_urls_remain_rejected() {
1457 for (part_type, media_field) in [
1458 ("image_url", "image_url"),
1459 ("video_url", "video_url"),
1460 ("audio_url", "audio_url"),
1461 ] {
1462 let result = serde_json::from_value::<ChatCompletionRequestUserMessageContentPart>(
1463 serde_json::json!({
1464 "type": part_type,
1465 (media_field): {"url": "not a url"},
1466 "uuid": "cache-key"
1467 }),
1468 );
1469
1470 assert!(result.is_err(), "{part_type} accepted an invalid URL");
1471 }
1472 }
1473
1474 #[test]
1475 fn legacy_nested_media_uuids_remain_accepted() {
1476 let legacy_uuid = "92b888ad-e64a-478f-b688-5091e16544e3";
1477
1478 for (part_type, media_field, url) in [
1479 ("image_url", "image_url", "https://x.example/image.png"),
1480 ("video_url", "video_url", "https://x.example/video.mp4"),
1481 ("audio_url", "audio_url", "https://x.example/audio.wav"),
1482 ] {
1483 let part = parse_content_part(serde_json::json!({
1484 "type": part_type,
1485 (media_field): {"url": url, "uuid": legacy_uuid}
1486 }));
1487 let json = serde_json::to_value(part).unwrap();
1488
1489 assert_eq!(json[media_field]["url"], url);
1490 assert_eq!(json[media_field]["uuid"], legacy_uuid);
1491 assert!(json.get("uuid").is_none());
1492 }
1493 }
1494
1495 #[test]
1496 fn video_url_null_and_top_level_uuid() {
1497 let part = parse_content_part(serde_json::json!({
1498 "type": "video_url",
1499 "video_url": null,
1500 "uuid": "video-cache-key"
1501 }));
1502
1503 match part {
1504 ChatCompletionRequestUserMessageContentPart::VideoUrl(part) => {
1505 assert!(part.video_url.is_none());
1506 assert_eq!(part.uuid.as_deref(), Some("video-cache-key"));
1507 }
1508 _ => panic!("expected video_url part"),
1509 }
1510 }
1511
1512 #[test]
1513 fn audio_url_null_and_top_level_uuid() {
1514 let part = parse_content_part(serde_json::json!({
1515 "type": "audio_url",
1516 "audio_url": null,
1517 "uuid": "audio-cache-key"
1518 }));
1519
1520 match part {
1521 ChatCompletionRequestUserMessageContentPart::AudioUrl(part) => {
1522 assert!(part.audio_url.is_none());
1523 assert_eq!(part.uuid.as_deref(), Some("audio-cache-key"));
1524 }
1525 _ => panic!("expected audio_url part"),
1526 }
1527 }
1528
1529 #[test]
1530 fn message_content_array_preserves_uuid_alignment() {
1531 let payload = serde_json::json!({
1532 "role": "user",
1533 "content": [
1534 {"type": "text", "text": "describe these"},
1535 {
1536 "type": "image_url",
1537 "image_url": {"url": "https://x.example/img1.png"},
1538 "uuid": "image-1"
1539 },
1540 {"type": "image_url", "image_url": null, "uuid": "image-1"}
1541 ]
1542 });
1543 let message: ChatCompletionRequestUserMessage = serde_json::from_value(payload).unwrap();
1544 let ChatCompletionRequestUserMessageContent::Array(parts) = message.content else {
1545 panic!("expected content array");
1546 };
1547
1548 assert_eq!(parts.len(), 3);
1549 match &parts[1] {
1550 ChatCompletionRequestUserMessageContentPart::ImageUrl(part) => {
1551 assert!(
1552 part.image_url
1553 .as_ref()
1554 .map(|image| image.url.as_str())
1555 .is_some()
1556 );
1557 assert_eq!(part.uuid.as_deref(), Some("image-1"));
1558 }
1559 _ => panic!("parts[1] should be image_url"),
1560 }
1561 match &parts[2] {
1562 ChatCompletionRequestUserMessageContentPart::ImageUrl(part) => {
1563 assert!(part.image_url.is_none());
1564 assert_eq!(part.uuid.as_deref(), Some("image-1"));
1565 }
1566 _ => panic!("parts[2] should be image_url"),
1567 }
1568 }
1569
1570 #[test]
1571 fn tool_message_accepts_media_content() {
1572 let message: ChatCompletionRequestMessage = serde_json::from_value(serde_json::json!({
1573 "role": "tool",
1574 "tool_call_id": "call_media",
1575 "content": [
1576 {"type": "text", "text": "Screenshot captured"},
1577 {
1578 "type": "image_url",
1579 "image_url": {
1580 "url": "data:image/png;base64,aGVsbG8="
1581 }
1582 },
1583 {
1584 "type": "video_url",
1585 "video_url": {
1586 "url": "https://example.com/clip.mp4"
1587 }
1588 },
1589 {
1590 "type": "audio_url",
1591 "audio_url": {
1592 "url": "https://example.com/audio.wav"
1593 }
1594 }
1595 ]
1596 }))
1597 .unwrap();
1598
1599 let ChatCompletionRequestMessage::Tool(tool) = message else {
1600 panic!("expected tool message");
1601 };
1602 let ChatCompletionRequestToolMessageContent::Array(parts) = tool.content else {
1603 panic!("expected array content");
1604 };
1605 assert!(matches!(
1606 parts[1],
1607 ChatCompletionRequestToolMessageContentPart::ImageUrl(_)
1608 ));
1609 assert!(matches!(
1610 parts[2],
1611 ChatCompletionRequestToolMessageContentPart::VideoUrl(_)
1612 ));
1613 assert!(matches!(
1614 parts[3],
1615 ChatCompletionRequestToolMessageContentPart::AudioUrl(_)
1616 ));
1617 }
1618
1619 #[test]
1620 fn chat_logprob_serializes_token_id_when_present() {
1621 let logprob = ChatCompletionTokenLogprob {
1622 token: " hello".into(),
1623 logprob: -0.12,
1624 token_id: Some(123),
1625 bytes: Some(vec![32, 104, 101, 108, 108, 111]),
1626 top_logprobs: vec![],
1627 };
1628
1629 let json = serde_json::to_value(logprob).unwrap();
1630
1631 assert_eq!(json["token_id"], 123);
1632 }
1633
1634 #[test]
1635 fn chat_logprob_deserializes_optional_fields() {
1636 let choice_logprobs: ChatChoiceLogprobs = serde_json::from_value(serde_json::json!({
1637 "content": [{
1638 "token": " hello",
1639 "logprob": -0.12,
1640 "top_logprobs": []
1641 }]
1642 }))
1643 .unwrap();
1644 let token_logprob: ChatCompletionTokenLogprob = serde_json::from_value(serde_json::json!({
1645 "token": " hello",
1646 "logprob": -0.12,
1647 "token_id": 123,
1648 "bytes": [32, 104, 101, 108, 108, 111],
1649 "top_logprobs": []
1650 }))
1651 .unwrap();
1652
1653 assert_eq!(choice_logprobs.content.as_ref().unwrap()[0].token_id, None);
1654 assert!(choice_logprobs.refusal.is_none());
1655 assert_eq!(token_logprob.token_id, Some(123));
1656 assert_eq!(token_logprob.bytes, Some(vec![32, 104, 101, 108, 108, 111]));
1657 }
1658
1659 #[test]
1660 fn chat_logprob_preserves_nullable_fields() {
1661 let choice_logprobs = ChatChoiceLogprobs {
1662 content: None,
1663 refusal: None,
1664 };
1665 let token_logprob = ChatCompletionTokenLogprob {
1666 token: " hello".into(),
1667 logprob: -0.12,
1668 token_id: None,
1669 bytes: None,
1670 top_logprobs: vec![],
1671 };
1672
1673 let choice_json = serde_json::to_value(choice_logprobs).unwrap();
1674 let token_json = serde_json::to_value(token_logprob).unwrap();
1675
1676 assert_eq!(choice_json["content"], serde_json::Value::Null);
1677 assert_eq!(choice_json["refusal"], serde_json::Value::Null);
1678 assert!(token_json.get("token_id").is_none());
1679 assert_eq!(token_json["bytes"], serde_json::Value::Null);
1680 }
1681
1682 #[test]
1683 #[allow(deprecated)]
1684 fn chat_response_omits_absent_optional_fields() {
1685 let response = CreateChatCompletionResponse {
1686 id: "chatcmpl_dummy".into(),
1687 choices: vec![ChatChoice {
1688 index: 0,
1689 message: ChatCompletionResponseMessage {
1690 content: Some(ChatCompletionMessageContent::Text("hello".into())),
1691 refusal: None,
1692 tool_calls: None,
1693 role: Role::Assistant,
1694 function_call: None,
1695 audio: None,
1696 reasoning_content: None,
1697 },
1698 finish_reason: Some(FinishReason::Stop),
1699 logprobs: None,
1700 }],
1701 created: 0,
1702 model: "dummy-model".into(),
1703 service_tier: None,
1704 system_fingerprint: None,
1705 object: "chat.completion".into(),
1706 usage: None,
1707 };
1708
1709 let json = serde_json::to_value(response).unwrap();
1710
1711 for absent in ["usage", "service_tier", "system_fingerprint"] {
1712 assert!(json.get(absent).is_none(), "{absent} should be omitted");
1713 }
1714 let choice = &json["choices"][0];
1715 assert_eq!(choice["finish_reason"], "stop");
1716 assert_eq!(choice["logprobs"], serde_json::Value::Null);
1717 let message = &choice["message"];
1718 assert_eq!(message["refusal"], serde_json::Value::Null);
1719 for absent in ["tool_calls", "function_call", "audio", "reasoning_content"] {
1720 assert!(
1721 message.get(absent).is_none(),
1722 "message.{absent} should be omitted"
1723 );
1724 }
1725 }
1726
1727 #[test]
1728 fn stream_response_omits_absent_optional_fields() {
1729 let chunk = CreateChatCompletionStreamResponse {
1730 id: "chatcmpl_dummy".into(),
1731 choices: vec![ChatChoiceStream {
1732 index: 0,
1733 delta: ChatCompletionStreamResponseDelta {
1734 content: Some(ChatCompletionMessageContent::Text("hello".into())),
1735 function_call: None,
1736 tool_calls: None,
1737 role: None,
1738 refusal: None,
1739 reasoning_content: None,
1740 },
1741 finish_reason: None,
1742 logprobs: None,
1743 }],
1744 created: 0,
1745 model: "dummy-model".into(),
1746 service_tier: None,
1747 system_fingerprint: None,
1748 object: "chat.completion.chunk".into(),
1749 usage: None,
1750 };
1751
1752 let json = serde_json::to_value(chunk).unwrap();
1753
1754 for absent in ["usage", "service_tier", "system_fingerprint"] {
1755 assert!(json.get(absent).is_none(), "{absent} should be omitted");
1756 }
1757 }
1758
1759 #[test]
1760 fn stream_tool_call_continuation_chunk_omits_absent_fields() {
1761 let chunk = ChatCompletionMessageToolCallChunk {
1762 index: 0,
1763 id: None,
1764 r#type: None,
1765 function: Some(FunctionCallStream {
1766 name: None,
1767 arguments: Some("{\"a\":".into()),
1768 }),
1769 };
1770
1771 let json = serde_json::to_value(chunk).unwrap();
1772
1773 assert!(json.get("id").is_none());
1774 assert!(json.get("type").is_none());
1775 assert!(json["function"].get("name").is_none());
1776 assert_eq!(json["function"]["arguments"], "{\"a\":");
1777 }
1778
1779 #[test]
1780 fn stream_delta_function_call_omits_absent_fields() {
1781 let function_call = ChatCompletionStreamResponseDeltaFunctionCall {
1782 name: None,
1783 arguments: Some("{}".into()),
1784 };
1785
1786 let json = serde_json::to_value(function_call).unwrap();
1787
1788 assert!(json.get("name").is_none());
1789 assert_eq!(json["arguments"], "{}");
1790 }
1791
1792 #[test]
1793 fn usage_details_omit_absent_fields() {
1794 let response = CreateChatCompletionResponse {
1795 id: "chatcmpl_dummy".into(),
1796 choices: vec![],
1797 created: 0,
1798 model: "dummy-model".into(),
1799 service_tier: None,
1800 system_fingerprint: None,
1801 object: "chat.completion".into(),
1802 usage: Some(CompletionUsage {
1803 prompt_tokens: 10,
1804 completion_tokens: 25,
1805 total_tokens: 35,
1806 prompt_tokens_details: Some(PromptTokensDetails {
1807 audio_tokens: None,
1808 cached_tokens: Some(0),
1809 }),
1810 completion_tokens_details: Some(CompletionTokensDetails {
1811 reasoning_tokens: Some(5),
1812 ..Default::default()
1813 }),
1814 }),
1815 };
1816
1817 let json = serde_json::to_value(&response).unwrap();
1818 let usage = &json["usage"];
1819
1820 assert_eq!(usage["total_tokens"], 35);
1821 assert_eq!(usage["prompt_tokens_details"]["cached_tokens"], 0);
1822 assert!(
1823 usage["prompt_tokens_details"].get("audio_tokens").is_none(),
1824 "audio_tokens should be omitted, not null"
1825 );
1826 assert_eq!(usage["completion_tokens_details"]["reasoning_tokens"], 5);
1827 for absent in [
1828 "accepted_prediction_tokens",
1829 "audio_tokens",
1830 "rejected_prediction_tokens",
1831 ] {
1832 assert!(
1833 usage["completion_tokens_details"].get(absent).is_none(),
1834 "{absent} should be omitted"
1835 );
1836 }
1837
1838 let roundtrip: CreateChatCompletionResponse = serde_json::from_value(json).unwrap();
1839 assert_eq!(roundtrip, response);
1840 }
1841}