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)]
263pub struct FunctionCallStream {
264 pub name: Option<String>,
265 #[serde(default, deserialize_with = "deserialize_arguments_opt")]
266 pub arguments: Option<String>,
267}
268
269#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Default)]
275pub struct ChatCompletionMessageToolCallChunk {
276 pub index: u32,
277 pub id: Option<String>,
278 pub r#type: Option<FunctionType>,
279 pub function: Option<FunctionCallStream>,
280}
281
282#[derive(Debug, Serialize, Deserialize, Clone, Builder, PartialEq)]
295#[builder(name = "ChatCompletionRequestMessageContentPartImageArgs")]
296#[builder(pattern = "mutable")]
297#[builder(setter(into, strip_option))]
298#[builder(derive(Debug))]
299#[builder(build_fn(error = "OpenAIError"))]
300pub struct ChatCompletionRequestMessageContentPartImage {
301 #[builder(default)]
302 #[serde(default, deserialize_with = "deserialize_optional_media")]
303 pub image_url: Option<ImageUrl>,
304 #[builder(default)]
305 #[serde(skip_serializing_if = "Option::is_none")]
306 pub uuid: Option<String>,
308}
309
310#[derive(Debug, Serialize, Deserialize, Clone, Builder, PartialEq)]
315#[builder(name = "ImageUrlArgs")]
316#[builder(pattern = "mutable")]
317#[builder(setter(into, strip_option))]
318#[builder(derive(Debug))]
319#[builder(build_fn(error = "OpenAIError"))]
320pub struct ImageUrl {
321 pub url: Url,
322 pub detail: Option<ImageDetail>,
323 #[deprecated(note = "use the content-part `uuid` field for vLLM cache identities")]
324 #[serde(skip_serializing_if = "Option::is_none")]
325 pub uuid: Option<Uuid>,
326}
327
328#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
334#[serde(tag = "type")]
335#[serde(rename_all = "snake_case")]
336pub enum ChatCompletionRequestToolMessageContentPart {
337 Text(ChatCompletionRequestMessageContentPartText),
338 ImageUrl(ChatCompletionRequestMessageContentPartImage),
339 VideoUrl(ChatCompletionRequestMessageContentPartVideo),
340 AudioUrl(ChatCompletionRequestMessageContentPartAudioUrl),
341}
342
343#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
345#[serde(untagged)]
346pub enum ChatCompletionRequestToolMessageContent {
347 Text(String),
348 Array(Vec<ChatCompletionRequestToolMessageContentPart>),
349}
350
351impl Default for ChatCompletionRequestToolMessageContent {
352 fn default() -> Self {
353 Self::Text(String::new())
354 }
355}
356
357impl From<&str> for ChatCompletionRequestToolMessageContent {
358 fn from(value: &str) -> Self {
359 Self::Text(value.into())
360 }
361}
362
363impl From<String> for ChatCompletionRequestToolMessageContent {
364 fn from(value: String) -> Self {
365 Self::Text(value)
366 }
367}
368
369#[derive(Debug, Serialize, Deserialize, Default, Clone, Builder, PartialEq)]
371#[builder(name = "ChatCompletionRequestToolMessageArgs")]
372#[builder(pattern = "mutable")]
373#[builder(setter(into, strip_option), default)]
374#[builder(derive(Debug))]
375#[builder(build_fn(error = "OpenAIError"))]
376pub struct ChatCompletionRequestToolMessage {
377 pub content: ChatCompletionRequestToolMessageContent,
378 pub tool_call_id: String,
379}
380
381#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
382pub struct ChatChoiceLogprobs {
383 pub content: Option<Vec<ChatCompletionTokenLogprob>>,
384 pub refusal: Option<Vec<ChatCompletionTokenLogprob>>,
385}
386
387#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
393pub struct ChatCompletionTokenLogprob {
394 pub token: String,
395 pub logprob: f32,
396 #[serde(skip_serializing_if = "Option::is_none")]
397 pub token_id: Option<u32>,
398 pub bytes: Option<Vec<u8>>,
399 pub top_logprobs: Vec<TopLogprobs>,
400}
401
402#[derive(Clone, Serialize, Default, Debug, Deserialize, PartialEq)]
403#[serde(rename_all = "lowercase")]
404pub enum ChatCompletionToolType {
405 #[default]
406 Function,
407}
408
409#[derive(Clone, Serialize, Default, Debug, Deserialize, PartialEq)]
410pub struct FunctionName {
411 pub name: String,
412}
413
414#[derive(Clone, Serialize, Default, Debug, Deserialize, PartialEq)]
415pub struct ChatCompletionNamedToolChoice {
416 pub r#type: ChatCompletionToolType,
417 pub function: FunctionName,
418}
419
420fn default_function_type() -> FunctionType {
421 FunctionType::Function
422}
423
424#[derive(Clone, Serialize, Debug, Deserialize, PartialEq)]
430pub struct ChatCompletionMessageToolCall {
431 pub id: String,
432 #[serde(default = "default_function_type")]
433 pub r#type: FunctionType,
434 pub function: FunctionCall,
435}
436
437#[derive(Clone, Serialize, Default, Debug, Deserialize, PartialEq)]
439#[serde(rename_all = "lowercase")]
440pub enum ChatCompletionToolChoiceOption {
441 #[default]
442 None,
443 Auto,
444 Required,
445 #[serde(untagged)]
446 Named(ChatCompletionNamedToolChoice),
447}
448
449#[derive(Clone, Serialize, Default, Debug, Builder, Deserialize, PartialEq)]
450#[builder(name = "ChatCompletionToolArgs")]
451#[builder(pattern = "mutable")]
452#[builder(setter(into, strip_option), default)]
453#[builder(derive(Debug))]
454#[builder(build_fn(error = "OpenAIError"))]
455pub struct ChatCompletionTool {
456 #[builder(default = "ChatCompletionToolType::Function")]
457 pub r#type: ChatCompletionToolType,
458 pub function: FunctionObject,
459}
460
461#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
472#[serde(untagged)]
473pub enum StopReason {
474 String(String),
475 Int(i64),
476 IntArray(Vec<i64>),
477}
478
479#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
489#[serde(untagged)]
490pub enum ReasoningContent {
491 Text(String),
493 Segments(Vec<String>),
496}
497
498impl ReasoningContent {
499 pub fn to_flat_string(&self) -> String {
501 match self {
502 ReasoningContent::Text(s) => s.clone(),
503 ReasoningContent::Segments(segs) => segs
504 .iter()
505 .filter(|s| !s.is_empty())
506 .cloned()
507 .collect::<Vec<_>>()
508 .join("\n"),
509 }
510 }
511
512 pub fn segments(&self) -> Option<&[String]> {
514 match self {
515 ReasoningContent::Segments(segs) => Some(segs),
516 ReasoningContent::Text(_) => None,
517 }
518 }
519}
520
521#[derive(Clone, Serialize, Debug, Deserialize, PartialEq)]
525pub struct ChatCompletionResponseContentPartText {
526 pub text: String,
527}
528
529#[derive(Clone, Serialize, Debug, Deserialize, PartialEq)]
531pub struct ChatCompletionResponseContentPartImageUrl {
532 pub image_url: ImageUrlResponse,
533}
534
535#[derive(Clone, Serialize, Debug, Deserialize, PartialEq)]
537pub struct ChatCompletionResponseContentPartVideoUrl {
538 pub video_url: VideoUrlResponse,
539}
540
541#[derive(Clone, Serialize, Debug, Deserialize, PartialEq)]
543pub struct ChatCompletionResponseContentPartAudioUrl {
544 pub audio_url: AudioUrlResponse,
545}
546
547#[derive(Clone, Serialize, Debug, Deserialize, PartialEq)]
548pub struct ImageUrlResponse {
549 pub url: String,
550 #[serde(skip_serializing_if = "Option::is_none")]
551 pub detail: Option<String>,
552}
553
554#[derive(Clone, Serialize, Debug, Deserialize, PartialEq)]
555pub struct VideoUrlResponse {
556 pub url: String,
557}
558
559#[derive(Clone, Serialize, Debug, Deserialize, PartialEq)]
560pub struct AudioUrlResponse {
561 pub url: String,
562}
563
564#[derive(Clone, Serialize, Debug, Deserialize, PartialEq)]
566#[serde(tag = "type", rename_all = "snake_case")]
567pub enum ChatCompletionResponseContentPart {
568 Text(ChatCompletionResponseContentPartText),
569 ImageUrl(ChatCompletionResponseContentPartImageUrl),
570 VideoUrl(ChatCompletionResponseContentPartVideoUrl),
571 AudioUrl(ChatCompletionResponseContentPartAudioUrl),
572}
573
574#[derive(Clone, Serialize, Debug, Deserialize, PartialEq)]
580#[serde(untagged)]
581pub enum ChatCompletionMessageContent {
582 Text(String),
584 Parts(Vec<ChatCompletionResponseContentPart>),
586}
587
588#[derive(Debug, Serialize, Deserialize, Clone, Builder, PartialEq)]
591#[builder(name = "VideoUrlArgs")]
592#[builder(pattern = "mutable")]
593#[builder(setter(into, strip_option))]
594#[builder(derive(Debug))]
595#[builder(build_fn(error = "OpenAIError"))]
596pub struct VideoUrl {
597 pub url: Url,
598 pub detail: Option<ImageDetail>,
599 #[deprecated(note = "use the content-part `uuid` field for vLLM cache identities")]
600 #[serde(skip_serializing_if = "Option::is_none")]
601 pub uuid: Option<Uuid>,
602}
603
604#[derive(Debug, Serialize, Deserialize, Clone, Builder, PartialEq)]
605#[builder(name = "ChatCompletionRequestMessageContentPartVideoArgs")]
606#[builder(pattern = "mutable")]
607#[builder(setter(into, strip_option))]
608#[builder(derive(Debug))]
609#[builder(build_fn(error = "OpenAIError"))]
610pub struct ChatCompletionRequestMessageContentPartVideo {
611 #[builder(default)]
612 #[serde(default, deserialize_with = "deserialize_optional_media")]
613 pub video_url: Option<VideoUrl>,
614 #[builder(default)]
615 #[serde(skip_serializing_if = "Option::is_none")]
616 pub uuid: Option<String>,
618}
619
620#[derive(Debug, Serialize, Deserialize, Clone, Builder, PartialEq)]
621#[builder(name = "AudioUrlArgs")]
622#[builder(pattern = "mutable")]
623#[builder(setter(into, strip_option))]
624#[builder(derive(Debug))]
625#[builder(build_fn(error = "OpenAIError"))]
626pub struct AudioUrl {
627 pub url: Url,
628 #[deprecated(note = "use the content-part `uuid` field for vLLM cache identities")]
629 #[serde(skip_serializing_if = "Option::is_none")]
630 pub uuid: Option<Uuid>,
631}
632
633#[derive(Debug, Serialize, Deserialize, Clone, Builder, PartialEq)]
634#[builder(name = "ChatCompletionRequestMessageContentPartAudioUrlArgs")]
635#[builder(pattern = "mutable")]
636#[builder(setter(into, strip_option))]
637#[builder(derive(Debug))]
638#[builder(build_fn(error = "OpenAIError"))]
639pub struct ChatCompletionRequestMessageContentPartAudioUrl {
640 #[builder(default)]
641 #[serde(default, deserialize_with = "deserialize_optional_media")]
642 pub audio_url: Option<AudioUrl>,
643 #[builder(default)]
644 #[serde(skip_serializing_if = "Option::is_none")]
645 pub uuid: Option<String>,
647}
648
649#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
653#[serde(untagged)]
654pub enum ChatCompletionRequestUserMessageContent {
655 Text(String),
656 Array(Vec<ChatCompletionRequestUserMessageContentPart>),
657}
658
659#[derive(Debug, Serialize, Deserialize, Default, Clone, Builder, PartialEq)]
660#[builder(name = "ChatCompletionRequestUserMessageArgs")]
661#[builder(pattern = "mutable")]
662#[builder(setter(into, strip_option), default)]
663#[builder(derive(Debug))]
664#[builder(build_fn(error = "OpenAIError"))]
665pub struct ChatCompletionRequestUserMessage {
666 pub content: ChatCompletionRequestUserMessageContent,
667 #[serde(skip_serializing_if = "Option::is_none")]
668 pub name: Option<String>,
669}
670
671impl Default for ChatCompletionRequestUserMessageContent {
672 fn default() -> Self {
673 Self::Text(String::new())
674 }
675}
676
677impl From<&str> for ChatCompletionRequestUserMessageContent {
678 fn from(value: &str) -> Self {
679 Self::Text(value.into())
680 }
681}
682
683impl From<String> for ChatCompletionRequestUserMessageContent {
684 fn from(value: String) -> Self {
685 Self::Text(value)
686 }
687}
688
689impl From<Vec<ChatCompletionRequestUserMessageContentPart>>
690 for ChatCompletionRequestUserMessageContent
691{
692 fn from(value: Vec<ChatCompletionRequestUserMessageContentPart>) -> Self {
693 Self::Array(value)
694 }
695}
696
697#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
703#[serde(tag = "type")]
704#[serde(rename_all = "snake_case")]
705pub enum ChatCompletionRequestUserMessageContentPart {
706 Text(ChatCompletionRequestMessageContentPartText),
707 ImageUrl(ChatCompletionRequestMessageContentPartImage),
708 VideoUrl(ChatCompletionRequestMessageContentPartVideo),
709 AudioUrl(ChatCompletionRequestMessageContentPartAudioUrl),
710 InputAudio(ChatCompletionRequestMessageContentPartAudio),
711}
712
713#[derive(Debug, Serialize, Deserialize, Default, Clone, Builder, PartialEq)]
719#[builder(name = "ChatCompletionRequestAssistantMessageArgs")]
720#[builder(pattern = "mutable")]
721#[builder(setter(into, strip_option), default)]
722#[builder(derive(Debug))]
723#[builder(build_fn(error = "OpenAIError"))]
724pub struct ChatCompletionRequestAssistantMessage {
725 #[serde(skip_serializing_if = "Option::is_none")]
726 pub content: Option<ChatCompletionRequestAssistantMessageContent>,
727 #[serde(default, alias = "reasoning", skip_serializing_if = "Option::is_none")]
733 pub reasoning_content: Option<ReasoningContent>,
734 #[serde(skip_serializing_if = "Option::is_none")]
735 pub refusal: Option<String>,
736 #[serde(skip_serializing_if = "Option::is_none")]
737 pub name: Option<String>,
738 #[serde(skip_serializing_if = "Option::is_none")]
739 pub audio: Option<ChatCompletionRequestAssistantMessageAudio>,
740 #[serde(skip_serializing_if = "Option::is_none")]
741 pub tool_calls: Option<Vec<ChatCompletionMessageToolCall>>,
742 #[deprecated]
743 #[serde(skip_serializing_if = "Option::is_none")]
744 pub function_call: Option<FunctionCall>,
745}
746
747#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
753#[serde(tag = "role")]
754#[serde(rename_all = "lowercase")]
755pub enum ChatCompletionRequestMessage {
756 Developer(ChatCompletionRequestDeveloperMessage),
757 System(ChatCompletionRequestSystemMessage),
758 User(ChatCompletionRequestUserMessage),
759 Assistant(ChatCompletionRequestAssistantMessage),
760 Tool(ChatCompletionRequestToolMessage),
761 Function(ChatCompletionRequestFunctionMessage),
762}
763
764pub type ServiceTierResponse = ServiceTier;
766
767#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
773pub struct ChatCompletionResponseMessage {
774 pub content: Option<ChatCompletionMessageContent>,
778 #[serde(skip_serializing_if = "Option::is_none")]
779 pub refusal: Option<String>,
780 #[serde(skip_serializing_if = "Option::is_none")]
781 pub tool_calls: Option<Vec<ChatCompletionMessageToolCall>>,
782 pub role: Role,
783 #[serde(skip_serializing_if = "Option::is_none")]
784 #[deprecated]
785 pub function_call: Option<FunctionCall>,
786 #[serde(skip_serializing_if = "Option::is_none")]
787 pub audio: Option<ChatCompletionResponseMessageAudio>,
788 #[serde(default, alias = "reasoning")]
794 pub reasoning_content: Option<String>,
795}
796
797#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq)]
802pub struct ChatCompletionStreamOptions {
803 pub include_usage: bool,
804 #[serde(default)]
807 pub continuous_usage_stats: bool,
808}
809
810#[derive(Clone, Serialize, Default, Debug, Builder, Deserialize, PartialEq)]
817#[builder(name = "CreateChatCompletionRequestArgs")]
818#[builder(pattern = "mutable")]
819#[builder(setter(into, strip_option), default)]
820#[builder(derive(Debug))]
821#[builder(build_fn(error = "OpenAIError"))]
822pub struct CreateChatCompletionRequest {
823 pub messages: Vec<ChatCompletionRequestMessage>,
824 pub model: String,
825 #[serde(skip_serializing_if = "Option::is_none")]
827 pub mm_processor_kwargs: Option<serde_json::Value>,
828 #[serde(skip_serializing_if = "Option::is_none")]
829 pub store: Option<bool>,
830 #[serde(skip_serializing_if = "Option::is_none")]
831 pub reasoning_effort: Option<ReasoningEffort>,
832 #[serde(skip_serializing_if = "Option::is_none")]
833 pub metadata: Option<serde_json::Value>,
834 #[serde(skip_serializing_if = "Option::is_none")]
835 pub frequency_penalty: Option<f32>,
836 #[serde(skip_serializing_if = "Option::is_none")]
837 pub logit_bias: Option<std::collections::HashMap<String, serde_json::Value>>,
838 #[serde(skip_serializing_if = "Option::is_none")]
839 pub logprobs: Option<bool>,
840 #[serde(skip_serializing_if = "Option::is_none")]
841 pub top_logprobs: Option<u8>,
842 #[deprecated]
843 #[serde(skip_serializing_if = "Option::is_none")]
844 pub max_tokens: Option<u32>,
845 #[serde(skip_serializing_if = "Option::is_none")]
846 pub max_completion_tokens: Option<u32>,
847 #[serde(skip_serializing_if = "Option::is_none")]
848 pub n: Option<u8>,
849 #[serde(skip_serializing_if = "Option::is_none")]
850 pub modalities: Option<Vec<async_openai::types::chat::ResponseModalities>>,
851 #[serde(skip_serializing_if = "Option::is_none")]
852 pub prediction: Option<PredictionContent>,
853 #[serde(skip_serializing_if = "Option::is_none")]
854 pub audio: Option<ChatCompletionAudio>,
855 #[serde(skip_serializing_if = "Option::is_none")]
856 pub presence_penalty: Option<f32>,
857 #[serde(skip_serializing_if = "Option::is_none")]
858 pub response_format: Option<ResponseFormat>,
859 #[serde(skip_serializing_if = "Option::is_none")]
860 pub seed: Option<i64>,
861 #[serde(skip_serializing_if = "Option::is_none")]
862 pub service_tier: Option<ServiceTier>,
863 #[serde(skip_serializing_if = "Option::is_none")]
864 pub stop: Option<Stop>,
865 #[serde(default, skip_serializing_if = "Option::is_none")]
866 pub stream: Option<bool>,
867 #[serde(skip_serializing_if = "Option::is_none")]
868 pub stream_options: Option<ChatCompletionStreamOptions>,
869 #[serde(skip_serializing_if = "Option::is_none")]
870 pub temperature: Option<f32>,
871 #[serde(skip_serializing_if = "Option::is_none")]
872 pub top_p: Option<f32>,
873 #[serde(skip_serializing_if = "Option::is_none")]
874 pub tools: Option<Vec<ChatCompletionTool>>,
875 #[serde(skip_serializing_if = "Option::is_none")]
876 pub tool_choice: Option<ChatCompletionToolChoiceOption>,
877 #[serde(skip_serializing_if = "Option::is_none")]
878 pub parallel_tool_calls: Option<bool>,
879 #[serde(skip_serializing_if = "Option::is_none")]
880 pub user: Option<String>,
881 #[deprecated]
882 #[serde(skip_serializing_if = "Option::is_none")]
883 pub function_call: Option<ChatCompletionFunctionCall>,
884 #[deprecated]
885 #[serde(skip_serializing_if = "Option::is_none")]
886 pub functions: Option<Vec<ChatCompletionFunctions>>,
887 #[serde(skip_serializing_if = "Option::is_none")]
888 pub web_search_options: Option<WebSearchOptions>,
889}
890
891#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
895pub struct ChatChoice {
896 pub index: u32,
897 pub message: ChatCompletionResponseMessage,
898 pub finish_reason: Option<FinishReason>,
899 pub logprobs: Option<ChatChoiceLogprobs>,
900}
901
902#[derive(Debug, Deserialize, Clone, PartialEq, Serialize)]
904pub struct CreateChatCompletionResponse {
905 pub id: String,
906 pub choices: Vec<ChatChoice>,
907 pub created: u32,
908 pub model: String,
909 pub service_tier: Option<ServiceTierResponse>,
910 pub system_fingerprint: Option<String>,
911 pub object: String,
912 pub usage: Option<CompletionUsage>,
913}
914
915pub type ChatCompletionResponseStream =
916 Pin<Box<dyn Stream<Item = Result<CreateChatCompletionStreamResponse, OpenAIError>> + Send>>;
917
918#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
924pub struct ChatCompletionStreamResponseDelta {
925 #[serde(skip_serializing_if = "Option::is_none")]
926 pub content: Option<ChatCompletionMessageContent>,
927 #[serde(skip_serializing_if = "Option::is_none")]
928 pub function_call: Option<ChatCompletionStreamResponseDeltaFunctionCall>,
929 #[serde(skip_serializing_if = "Option::is_none")]
930 pub tool_calls: Option<Vec<ChatCompletionMessageToolCallChunk>>,
931 #[serde(skip_serializing_if = "Option::is_none")]
932 pub role: Option<Role>,
933 #[serde(skip_serializing_if = "Option::is_none")]
934 pub refusal: Option<String>,
935 #[serde(default, alias = "reasoning", skip_serializing_if = "Option::is_none")]
941 pub reasoning_content: Option<String>,
942}
943
944#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
945pub struct ChatCompletionStreamResponseDeltaFunctionCall {
946 pub name: Option<String>,
947 #[serde(default, deserialize_with = "deserialize_arguments_opt")]
948 pub arguments: Option<String>,
949}
950
951#[derive(Debug, Deserialize, Clone, PartialEq, Serialize)]
953pub struct ChatChoiceStream {
954 pub index: u32,
955 pub delta: ChatCompletionStreamResponseDelta,
956 pub finish_reason: Option<FinishReason>,
957 pub logprobs: Option<ChatChoiceLogprobs>,
958}
959
960#[derive(Debug, Deserialize, Clone, PartialEq, Serialize)]
962pub struct CreateChatCompletionStreamResponse {
963 pub id: String,
964 pub choices: Vec<ChatChoiceStream>,
965 pub created: u32,
966 pub model: String,
967 pub service_tier: Option<ServiceTierResponse>,
968 pub system_fingerprint: Option<String>,
969 pub object: String,
970 pub usage: Option<CompletionUsage>,
971}
972
973#[cfg(test)]
974mod tests {
975 use super::*;
976
977 #[test]
978 fn stop_accepts_token_id_array() {
979 let stop: Stop = serde_json::from_value(serde_json::json!([32, 34])).unwrap();
980
981 assert_eq!(stop, Stop::TokenIdArray(vec![32, 34]));
982 }
983
984 #[test]
985 fn stop_accepts_string_and_string_array() {
986 let stop: Stop = serde_json::from_value(serde_json::json!(" The")).unwrap();
987
988 assert_eq!(stop, Stop::String(" The".to_string()));
989
990 let stop: Stop = serde_json::from_value(serde_json::json!(["A", "B"])).unwrap();
991
992 assert_eq!(
993 stop,
994 Stop::StringArray(vec!["A".to_string(), "B".to_string()])
995 );
996 }
997
998 #[test]
999 fn stop_token_id_display_string_remains_string_stop() {
1000 let stop: Stop = serde_json::from_value(serde_json::json!("token_id:576")).unwrap();
1001
1002 assert_eq!(stop, Stop::String("token_id:576".to_string()));
1003
1004 let stop: Stop = serde_json::from_value(serde_json::json!(["token_id:576"])).unwrap();
1005
1006 assert_eq!(stop, Stop::StringArray(vec!["token_id:576".to_string()]));
1007 }
1008
1009 #[test]
1010 fn stop_rejects_single_token_id() {
1011 let result = serde_json::from_value::<Stop>(serde_json::json!(576));
1012
1013 assert!(result.is_err());
1014 }
1015
1016 #[test]
1017 fn stop_converts_from_upstream_stop_configuration() {
1018 let upstream =
1019 async_openai::types::chat::StopConfiguration::StringArray(vec!["END".to_string()]);
1020
1021 assert_eq!(
1022 Stop::from(upstream),
1023 Stop::StringArray(vec!["END".to_string()])
1024 );
1025 }
1026
1027 #[test]
1028 fn request_builder_accepts_upstream_reasoning_effort() {
1029 let request = CreateChatCompletionRequestArgs::default()
1030 .reasoning_effort(async_openai::types::chat::ReasoningEffort::High)
1031 .build()
1032 .unwrap();
1033
1034 assert_eq!(request.reasoning_effort, Some(ReasoningEffort::High));
1035 }
1036
1037 #[test]
1038 fn tool_call_defaults_type_on_deserialize() {
1039 let tool_call: ChatCompletionMessageToolCall = serde_json::from_value(serde_json::json!({
1040 "id": "call_123",
1041 "function": {
1042 "name": "get_weather",
1043 "arguments": "{\"location\":\"SF\"}"
1044 }
1045 }))
1046 .unwrap();
1047
1048 assert_eq!(tool_call.r#type, FunctionType::Function);
1049 }
1050
1051 #[test]
1052 fn tool_call_serializes_type_for_wire_compat() {
1053 let tool_call = ChatCompletionMessageToolCall {
1054 id: "call_123".into(),
1055 r#type: FunctionType::Function,
1056 function: FunctionCall {
1057 name: "get_weather".into(),
1058 arguments: "{\"location\":\"SF\"}".into(),
1059 },
1060 };
1061
1062 let json = serde_json::to_value(tool_call).unwrap();
1063 assert_eq!(json["type"], "function");
1064 }
1065
1066 #[test]
1069 fn function_call_accepts_string_arguments() {
1070 let fc: FunctionCall = serde_json::from_value(serde_json::json!({
1071 "name": "get_weather",
1072 "arguments": "{\"location\":\"SF\"}"
1073 }))
1074 .unwrap();
1075 assert_eq!(fc.arguments, "{\"location\":\"SF\"}");
1076 }
1077
1078 #[test]
1079 fn function_call_accepts_dict_arguments() {
1080 let fc: FunctionCall = serde_json::from_value(serde_json::json!({
1081 "name": "get_weather",
1082 "arguments": {"location": "SF"}
1083 }))
1084 .unwrap();
1085 assert_eq!(fc.arguments, "{\"location\":\"SF\"}");
1086 }
1087
1088 #[test]
1089 fn function_call_rejects_integer_arguments() {
1090 let result = serde_json::from_value::<FunctionCall>(serde_json::json!({
1091 "name": "f",
1092 "arguments": 42
1093 }));
1094 assert!(result.is_err());
1095 }
1096
1097 #[test]
1098 fn function_call_rejects_boolean_arguments() {
1099 let result = serde_json::from_value::<FunctionCall>(serde_json::json!({
1100 "name": "f",
1101 "arguments": true
1102 }));
1103 assert!(result.is_err());
1104 }
1105
1106 #[test]
1107 fn function_call_rejects_null_arguments() {
1108 let result = serde_json::from_value::<FunctionCall>(serde_json::json!({
1109 "name": "f",
1110 "arguments": null
1111 }));
1112 assert!(result.is_err());
1113 }
1114
1115 #[test]
1116 fn function_call_rejects_array_arguments() {
1117 let result = serde_json::from_value::<FunctionCall>(serde_json::json!({
1118 "name": "f",
1119 "arguments": [1, 2, 3]
1120 }));
1121 assert!(result.is_err());
1122 }
1123
1124 #[test]
1125 fn function_call_stream_null_arguments_produces_none() {
1126 let fcs: FunctionCallStream = serde_json::from_value(serde_json::json!({
1127 "name": "f",
1128 "arguments": null
1129 }))
1130 .unwrap();
1131 assert_eq!(fcs.arguments, None);
1132 }
1133
1134 #[test]
1135 fn function_call_stream_rejects_integer_arguments() {
1136 let result = serde_json::from_value::<FunctionCallStream>(serde_json::json!({
1137 "name": "f",
1138 "arguments": 42
1139 }));
1140 assert!(result.is_err());
1141 }
1142
1143 #[test]
1144 fn function_call_stream_rejects_boolean_arguments() {
1145 let result = serde_json::from_value::<FunctionCallStream>(serde_json::json!({
1146 "name": "f",
1147 "arguments": true
1148 }));
1149 assert!(result.is_err());
1150 }
1151
1152 #[test]
1153 fn function_call_stream_accepts_dict_arguments() {
1154 let fcs: FunctionCallStream = serde_json::from_value(serde_json::json!({
1155 "name": "get_weather",
1156 "arguments": {"location": "SF"}
1157 }))
1158 .unwrap();
1159 assert_eq!(fcs.arguments.as_deref(), Some("{\"location\":\"SF\"}"));
1160 }
1161
1162 #[test]
1163 fn function_call_stream_accepts_null_arguments() {
1164 let fcs: FunctionCallStream = serde_json::from_value(serde_json::json!({
1165 "name": "get_weather"
1166 }))
1167 .unwrap();
1168 assert_eq!(fcs.arguments, None);
1169 }
1170
1171 #[test]
1172 fn tool_call_with_dict_arguments_roundtrip() {
1173 let tc: ChatCompletionMessageToolCall = serde_json::from_value(serde_json::json!({
1174 "id": "call_abc",
1175 "type": "function",
1176 "function": {
1177 "name": "search",
1178 "arguments": {"query": "hello", "limit": 10}
1179 }
1180 }))
1181 .unwrap();
1182 let parsed: serde_json::Value = serde_json::from_str(&tc.function.arguments).unwrap();
1184 assert_eq!(parsed, serde_json::json!({"query": "hello", "limit": 10}));
1185 let json = serde_json::to_value(&tc).unwrap();
1187 assert!(json["function"]["arguments"].is_string());
1188 }
1189
1190 #[test]
1191 fn stream_delta_function_call_accepts_dict_arguments() {
1192 let delta: ChatCompletionStreamResponseDeltaFunctionCall =
1193 serde_json::from_value(serde_json::json!({
1194 "name": "get_weather",
1195 "arguments": {"location": "SF"}
1196 }))
1197 .unwrap();
1198 assert_eq!(delta.arguments.as_deref(), Some("{\"location\":\"SF\"}"));
1199 }
1200
1201 fn parse_content_part(json: serde_json::Value) -> ChatCompletionRequestUserMessageContentPart {
1202 serde_json::from_value(json).expect("content part deserialization failed")
1203 }
1204
1205 #[test]
1206 fn image_url_url_and_top_level_uuid() {
1207 let part = parse_content_part(serde_json::json!({
1208 "type": "image_url",
1209 "image_url": {"url": "https://x.example/y.png"},
1210 "uuid": "image-123"
1211 }));
1212
1213 match part {
1214 ChatCompletionRequestUserMessageContentPart::ImageUrl(part) => {
1215 assert_eq!(part.uuid.as_deref(), Some("image-123"));
1216 assert_eq!(
1217 part.image_url.as_ref().map(|image| image.url.as_str()),
1218 Some("https://x.example/y.png")
1219 );
1220 }
1221 _ => panic!("expected image_url part"),
1222 }
1223 }
1224
1225 #[test]
1226 fn image_url_null_and_top_level_uuid() {
1227 let part = parse_content_part(serde_json::json!({
1228 "type": "image_url",
1229 "image_url": null,
1230 "uuid": "sku-1234-a"
1231 }));
1232
1233 match part {
1234 ChatCompletionRequestUserMessageContentPart::ImageUrl(part) => {
1235 assert!(part.image_url.is_none());
1236 assert_eq!(part.uuid.as_deref(), Some("sku-1234-a"));
1237 }
1238 _ => panic!("expected image_url part"),
1239 }
1240 }
1241
1242 #[test]
1243 fn empty_media_urls_deserialize_as_uuid_only() {
1244 for (part_type, media_field, uuid) in [
1245 ("image_url", "image_url", "image-cache-key"),
1246 ("video_url", "video_url", "video-cache-key"),
1247 ("audio_url", "audio_url", "audio-cache-key"),
1248 ] {
1249 let part = parse_content_part(serde_json::json!({
1250 "type": part_type,
1251 (media_field): {"url": ""},
1252 "uuid": uuid
1253 }));
1254 let json = serde_json::to_value(part).unwrap();
1255
1256 assert!(json[media_field].is_null());
1257 assert_eq!(json["uuid"], uuid);
1258 }
1259 }
1260
1261 #[test]
1262 fn image_url_null_without_uuid_deserializes_for_use_site_validation() {
1263 let part = parse_content_part(serde_json::json!({
1264 "type": "image_url",
1265 "image_url": null
1266 }));
1267
1268 match part {
1269 ChatCompletionRequestUserMessageContentPart::ImageUrl(part) => {
1270 assert!(part.image_url.is_none());
1271 assert!(part.uuid.is_none());
1272 }
1273 _ => panic!("expected image_url part"),
1274 }
1275 }
1276
1277 #[test]
1278 fn image_url_serialize_uuid_only_uses_null_image_url() {
1279 let part = ChatCompletionRequestMessageContentPartImage {
1280 image_url: None,
1281 uuid: Some("image-123".to_string()),
1282 };
1283 let json = serde_json::to_value(part).unwrap();
1284
1285 assert!(json["image_url"].is_null());
1286 assert_eq!(json["uuid"], "image-123");
1287 }
1288
1289 #[test]
1290 fn cached_media_builders_allow_omitting_urls() {
1291 let image = ChatCompletionRequestMessageContentPartImageArgs::default()
1292 .uuid("image-123")
1293 .build()
1294 .unwrap();
1295 let video = ChatCompletionRequestMessageContentPartVideoArgs::default()
1296 .uuid("video-123")
1297 .build()
1298 .unwrap();
1299 let audio = ChatCompletionRequestMessageContentPartAudioUrlArgs::default()
1300 .uuid("audio-123")
1301 .build()
1302 .unwrap();
1303
1304 let image_json = serde_json::to_value(image).unwrap();
1305 let video_json = serde_json::to_value(video).unwrap();
1306 let audio_json = serde_json::to_value(audio).unwrap();
1307 assert!(image_json["image_url"].is_null());
1308 assert!(video_json["video_url"].is_null());
1309 assert!(audio_json["audio_url"].is_null());
1310 }
1311
1312 #[test]
1313 fn image_url_uuid_accepts_opaque_string() {
1314 let part = parse_content_part(serde_json::json!({
1315 "type": "image_url",
1316 "image_url": {"url": "https://x.example/y.png"},
1317 "uuid": "img-ac3921de680bb217"
1318 }));
1319
1320 match part {
1321 ChatCompletionRequestUserMessageContentPart::ImageUrl(part) => {
1322 assert_eq!(part.uuid.as_deref(), Some("img-ac3921de680bb217"));
1323 }
1324 _ => panic!("expected image_url part"),
1325 }
1326 }
1327
1328 #[test]
1329 fn url_conversions_preserve_required_urls() {
1330 let image: ImageUrl = "https://x.example/image.png".into();
1331 let video: VideoUrl = "https://x.example/video.mp4".into();
1332 let audio: AudioUrl = "https://x.example/audio.wav".into();
1333
1334 assert_eq!(image.url.as_str(), "https://x.example/image.png");
1335 assert_eq!(video.url.as_str(), "https://x.example/video.mp4");
1336 assert_eq!(audio.url.as_str(), "https://x.example/audio.wav");
1337 }
1338
1339 #[test]
1340 fn invalid_media_urls_remain_rejected() {
1341 for (part_type, media_field) in [
1342 ("image_url", "image_url"),
1343 ("video_url", "video_url"),
1344 ("audio_url", "audio_url"),
1345 ] {
1346 let result = serde_json::from_value::<ChatCompletionRequestUserMessageContentPart>(
1347 serde_json::json!({
1348 "type": part_type,
1349 (media_field): {"url": "not a url"},
1350 "uuid": "cache-key"
1351 }),
1352 );
1353
1354 assert!(result.is_err(), "{part_type} accepted an invalid URL");
1355 }
1356 }
1357
1358 #[test]
1359 fn legacy_nested_media_uuids_remain_accepted() {
1360 let legacy_uuid = "92b888ad-e64a-478f-b688-5091e16544e3";
1361
1362 for (part_type, media_field, url) in [
1363 ("image_url", "image_url", "https://x.example/image.png"),
1364 ("video_url", "video_url", "https://x.example/video.mp4"),
1365 ("audio_url", "audio_url", "https://x.example/audio.wav"),
1366 ] {
1367 let part = parse_content_part(serde_json::json!({
1368 "type": part_type,
1369 (media_field): {"url": url, "uuid": legacy_uuid}
1370 }));
1371 let json = serde_json::to_value(part).unwrap();
1372
1373 assert_eq!(json[media_field]["url"], url);
1374 assert_eq!(json[media_field]["uuid"], legacy_uuid);
1375 assert!(json.get("uuid").is_none());
1376 }
1377 }
1378
1379 #[test]
1380 fn video_url_null_and_top_level_uuid() {
1381 let part = parse_content_part(serde_json::json!({
1382 "type": "video_url",
1383 "video_url": null,
1384 "uuid": "video-cache-key"
1385 }));
1386
1387 match part {
1388 ChatCompletionRequestUserMessageContentPart::VideoUrl(part) => {
1389 assert!(part.video_url.is_none());
1390 assert_eq!(part.uuid.as_deref(), Some("video-cache-key"));
1391 }
1392 _ => panic!("expected video_url part"),
1393 }
1394 }
1395
1396 #[test]
1397 fn audio_url_null_and_top_level_uuid() {
1398 let part = parse_content_part(serde_json::json!({
1399 "type": "audio_url",
1400 "audio_url": null,
1401 "uuid": "audio-cache-key"
1402 }));
1403
1404 match part {
1405 ChatCompletionRequestUserMessageContentPart::AudioUrl(part) => {
1406 assert!(part.audio_url.is_none());
1407 assert_eq!(part.uuid.as_deref(), Some("audio-cache-key"));
1408 }
1409 _ => panic!("expected audio_url part"),
1410 }
1411 }
1412
1413 #[test]
1414 fn message_content_array_preserves_uuid_alignment() {
1415 let payload = serde_json::json!({
1416 "role": "user",
1417 "content": [
1418 {"type": "text", "text": "describe these"},
1419 {
1420 "type": "image_url",
1421 "image_url": {"url": "https://x.example/img1.png"},
1422 "uuid": "image-1"
1423 },
1424 {"type": "image_url", "image_url": null, "uuid": "image-1"}
1425 ]
1426 });
1427 let message: ChatCompletionRequestUserMessage = serde_json::from_value(payload).unwrap();
1428 let ChatCompletionRequestUserMessageContent::Array(parts) = message.content else {
1429 panic!("expected content array");
1430 };
1431
1432 assert_eq!(parts.len(), 3);
1433 match &parts[1] {
1434 ChatCompletionRequestUserMessageContentPart::ImageUrl(part) => {
1435 assert!(
1436 part.image_url
1437 .as_ref()
1438 .map(|image| image.url.as_str())
1439 .is_some()
1440 );
1441 assert_eq!(part.uuid.as_deref(), Some("image-1"));
1442 }
1443 _ => panic!("parts[1] should be image_url"),
1444 }
1445 match &parts[2] {
1446 ChatCompletionRequestUserMessageContentPart::ImageUrl(part) => {
1447 assert!(part.image_url.is_none());
1448 assert_eq!(part.uuid.as_deref(), Some("image-1"));
1449 }
1450 _ => panic!("parts[2] should be image_url"),
1451 }
1452 }
1453
1454 #[test]
1455 fn tool_message_accepts_media_content() {
1456 let message: ChatCompletionRequestMessage = serde_json::from_value(serde_json::json!({
1457 "role": "tool",
1458 "tool_call_id": "call_media",
1459 "content": [
1460 {"type": "text", "text": "Screenshot captured"},
1461 {
1462 "type": "image_url",
1463 "image_url": {
1464 "url": "data:image/png;base64,aGVsbG8="
1465 }
1466 },
1467 {
1468 "type": "video_url",
1469 "video_url": {
1470 "url": "https://example.com/clip.mp4"
1471 }
1472 },
1473 {
1474 "type": "audio_url",
1475 "audio_url": {
1476 "url": "https://example.com/audio.wav"
1477 }
1478 }
1479 ]
1480 }))
1481 .unwrap();
1482
1483 let ChatCompletionRequestMessage::Tool(tool) = message else {
1484 panic!("expected tool message");
1485 };
1486 let ChatCompletionRequestToolMessageContent::Array(parts) = tool.content else {
1487 panic!("expected array content");
1488 };
1489 assert!(matches!(
1490 parts[1],
1491 ChatCompletionRequestToolMessageContentPart::ImageUrl(_)
1492 ));
1493 assert!(matches!(
1494 parts[2],
1495 ChatCompletionRequestToolMessageContentPart::VideoUrl(_)
1496 ));
1497 assert!(matches!(
1498 parts[3],
1499 ChatCompletionRequestToolMessageContentPart::AudioUrl(_)
1500 ));
1501 }
1502
1503 #[test]
1504 fn chat_logprob_serializes_token_id_when_present() {
1505 let logprob = ChatCompletionTokenLogprob {
1506 token: " hello".into(),
1507 logprob: -0.12,
1508 token_id: Some(123),
1509 bytes: Some(vec![32, 104, 101, 108, 108, 111]),
1510 top_logprobs: vec![],
1511 };
1512
1513 let json = serde_json::to_value(logprob).unwrap();
1514
1515 assert_eq!(json["token_id"], 123);
1516 }
1517
1518 #[test]
1519 fn chat_logprob_deserializes_optional_fields() {
1520 let choice_logprobs: ChatChoiceLogprobs = serde_json::from_value(serde_json::json!({
1521 "content": [{
1522 "token": " hello",
1523 "logprob": -0.12,
1524 "top_logprobs": []
1525 }]
1526 }))
1527 .unwrap();
1528 let token_logprob: ChatCompletionTokenLogprob = serde_json::from_value(serde_json::json!({
1529 "token": " hello",
1530 "logprob": -0.12,
1531 "token_id": 123,
1532 "bytes": [32, 104, 101, 108, 108, 111],
1533 "top_logprobs": []
1534 }))
1535 .unwrap();
1536
1537 assert_eq!(choice_logprobs.content.as_ref().unwrap()[0].token_id, None);
1538 assert!(choice_logprobs.refusal.is_none());
1539 assert_eq!(token_logprob.token_id, Some(123));
1540 assert_eq!(token_logprob.bytes, Some(vec![32, 104, 101, 108, 108, 111]));
1541 }
1542
1543 #[test]
1544 fn chat_logprob_preserves_nullable_fields() {
1545 let choice_logprobs = ChatChoiceLogprobs {
1546 content: None,
1547 refusal: None,
1548 };
1549 let token_logprob = ChatCompletionTokenLogprob {
1550 token: " hello".into(),
1551 logprob: -0.12,
1552 token_id: None,
1553 bytes: None,
1554 top_logprobs: vec![],
1555 };
1556
1557 let choice_json = serde_json::to_value(choice_logprobs).unwrap();
1558 let token_json = serde_json::to_value(token_logprob).unwrap();
1559
1560 assert_eq!(choice_json["content"], serde_json::Value::Null);
1561 assert_eq!(choice_json["refusal"], serde_json::Value::Null);
1562 assert!(token_json.get("token_id").is_none());
1563 assert_eq!(token_json["bytes"], serde_json::Value::Null);
1564 }
1565}