1use serde::{de, Deserialize, Serialize};
7use std::collections::HashMap;
8
9#[derive(Debug, Clone, Serialize, Deserialize)]
11pub struct ChatCompletionsRequest {
12 pub model: String,
14
15 pub messages: Vec<ChatMessage>,
17
18 #[serde(skip_serializing_if = "Option::is_none")]
20 pub max_tokens: Option<u32>,
21
22 #[serde(skip_serializing_if = "Option::is_none")]
25 pub max_completion_tokens: Option<u32>,
26
27 #[serde(skip_serializing_if = "Option::is_none")]
29 pub temperature: Option<f32>,
30
31 #[serde(skip_serializing_if = "Option::is_none")]
33 pub top_p: Option<f32>,
34
35 #[serde(skip_serializing_if = "Option::is_none")]
38 pub top_k: Option<i64>,
39
40 #[serde(skip_serializing_if = "Option::is_none")]
43 pub min_p: Option<f32>,
44
45 #[serde(skip_serializing_if = "Option::is_none")]
47 pub repetition_penalty: Option<f32>,
48
49 #[serde(skip_serializing_if = "Option::is_none")]
51 pub n: Option<u32>,
52
53 #[serde(skip_serializing_if = "Option::is_none")]
55 pub stream: Option<bool>,
56
57 #[serde(skip_serializing_if = "Option::is_none")]
61 pub ignore_eos: Option<bool>,
62
63 #[serde(default, deserialize_with = "deserialize_stop_sequences")]
65 #[serde(skip_serializing_if = "Option::is_none")]
66 pub stop: Option<Vec<String>>,
67
68 #[serde(skip_serializing_if = "Option::is_none")]
70 pub presence_penalty: Option<f32>,
71
72 #[serde(skip_serializing_if = "Option::is_none")]
74 pub frequency_penalty: Option<f32>,
75
76 #[serde(skip_serializing_if = "Option::is_none")]
78 pub logit_bias: Option<HashMap<String, f32>>,
79
80 #[serde(skip_serializing_if = "Option::is_none")]
83 pub logprobs: Option<bool>,
84
85 #[serde(skip_serializing_if = "Option::is_none")]
87 pub top_logprobs: Option<u32>,
88
89 #[serde(skip_serializing_if = "Option::is_none")]
91 pub user: Option<String>,
92
93 #[serde(skip_serializing_if = "Option::is_none")]
95 pub seed: Option<u64>,
96
97 #[serde(skip_serializing_if = "Option::is_none")]
99 pub response_format: Option<OpenAiResponseFormat>,
100
101 #[serde(skip_serializing_if = "Option::is_none")]
105 pub tools: Option<Vec<ChatTool>>,
106
107 #[serde(skip_serializing_if = "Option::is_none")]
109 pub tool_choice: Option<ToolChoice>,
110
111 #[serde(skip_serializing_if = "Option::is_none")]
113 pub stream_options: Option<StreamOptions>,
114
115 #[serde(skip_serializing_if = "Option::is_none")]
117 pub functions: Option<Vec<ChatFunction>>,
118
119 #[serde(skip_serializing_if = "Option::is_none")]
121 pub function_call: Option<FunctionCallChoice>,
122
123 #[serde(default, skip_serializing_if = "Option::is_none")]
127 pub metadata: Option<HashMap<String, serde_json::Value>>,
128
129 #[serde(default, skip_serializing_if = "Option::is_none")]
133 pub chat_template_kwargs: Option<HashMap<String, serde_json::Value>>,
134}
135
136#[derive(Debug, Clone, Serialize)]
138pub struct StreamOptions {
139 #[serde(default, skip_serializing_if = "Option::is_none")]
140 pub include_usage: Option<bool>,
141}
142
143impl<'de> Deserialize<'de> for StreamOptions {
144 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
145 where
146 D: serde::Deserializer<'de>,
147 {
148 #[derive(Deserialize)]
149 #[serde(deny_unknown_fields)]
150 struct Object {
151 #[serde(default)]
152 include_usage: Option<bool>,
153 }
154
155 let value = serde_json::Value::deserialize(deserializer)?;
156 if !value.is_object() {
157 return Err(de::Error::custom("stream_options must be a JSON object"));
158 }
159 let parsed = serde_json::from_value::<Object>(value).map_err(de::Error::custom)?;
160 Ok(Self {
161 include_usage: parsed.include_usage,
162 })
163 }
164}
165
166#[derive(Debug, Clone, Serialize, Deserialize)]
168pub struct ChatTool {
169 #[serde(rename = "type")]
170 pub tool_type: String,
171 pub function: ChatFunction,
172}
173
174#[derive(Debug, Clone, Serialize, Deserialize)]
176pub struct ChatFunction {
177 pub name: String,
178 #[serde(default, skip_serializing_if = "Option::is_none")]
179 pub description: Option<String>,
180 #[serde(default, skip_serializing_if = "Option::is_none")]
181 pub parameters: Option<serde_json::Value>,
182 #[serde(default, skip_serializing_if = "Option::is_none")]
183 pub strict: Option<bool>,
184}
185
186#[derive(Debug, Clone, Serialize, Deserialize)]
189#[serde(untagged)]
190pub enum ToolChoice {
191 Mode(String),
192 Function {
193 #[serde(rename = "type")]
194 tool_type: String,
195 function: ToolChoiceFunction,
196 },
197}
198
199#[derive(Debug, Clone, Serialize, Deserialize)]
200pub struct ToolChoiceFunction {
201 pub name: String,
202}
203
204#[derive(Debug, Clone, Serialize, Deserialize)]
206#[serde(untagged)]
207pub enum FunctionCallChoice {
208 Mode(String),
209 Function { name: String },
210}
211
212#[derive(Debug, Clone, Serialize, Deserialize)]
220pub struct OpenAiResponseFormat {
221 #[serde(rename = "type")]
222 pub format_type: String,
223 #[serde(default, skip_serializing_if = "Option::is_none")]
225 pub json_schema: Option<OpenAiJsonSchema>,
226}
227
228#[derive(Debug, Clone, Serialize, Deserialize)]
229pub struct OpenAiJsonSchema {
230 #[serde(default, skip_serializing_if = "Option::is_none")]
232 pub name: Option<String>,
233 #[serde(default, skip_serializing_if = "Option::is_none")]
239 pub schema: Option<serde_json::Value>,
240 #[serde(default, skip_serializing_if = "Option::is_none")]
244 pub strict: Option<bool>,
245}
246
247#[derive(Debug, Clone, Serialize, Deserialize)]
249#[serde(try_from = "ChatMessageWire")]
250pub struct ChatMessage {
251 pub role: MessageRole,
253
254 #[serde(default)]
260 #[serde(deserialize_with = "deserialize_message_content")]
261 pub content: String,
262
263 #[serde(default, skip_serializing_if = "Option::is_none")]
270 pub reasoning: Option<String>,
271
272 #[serde(skip_serializing_if = "Option::is_none")]
274 pub name: Option<String>,
275
276 #[serde(skip_serializing_if = "Option::is_none")]
278 pub tool_calls: Option<Vec<ChatToolCall>>,
279
280 #[serde(skip_serializing_if = "Option::is_none")]
282 pub tool_call_id: Option<String>,
283
284 #[serde(skip_serializing_if = "Option::is_none")]
286 pub function_call: Option<ChatFunctionCall>,
287}
288
289#[derive(Deserialize)]
293struct ChatMessageWire {
294 role: MessageRole,
295 #[serde(default, deserialize_with = "deserialize_message_content")]
296 content: String,
297 #[serde(default)]
298 reasoning: serde_json::Value,
299 #[serde(default)]
300 reasoning_content: serde_json::Value,
301 #[serde(default)]
302 name: Option<String>,
303 #[serde(default)]
304 tool_calls: Option<Vec<ChatToolCall>>,
305 #[serde(default)]
306 tool_call_id: Option<String>,
307 #[serde(default)]
308 function_call: Option<ChatFunctionCall>,
309}
310
311impl TryFrom<ChatMessageWire> for ChatMessage {
312 type Error = String;
313
314 fn try_from(message: ChatMessageWire) -> Result<Self, Self::Error> {
315 let reasoning = match message.reasoning {
316 serde_json::Value::String(reasoning) => Some(reasoning),
317 serde_json::Value::Null => match message.reasoning_content {
318 serde_json::Value::String(reasoning) => Some(reasoning),
319 serde_json::Value::Null => None,
320 _ => return Err("reasoning_content must be a string or null".to_string()),
321 },
322 _ => return Err("reasoning must be a string or null".to_string()),
323 };
324
325 Ok(Self {
326 role: message.role,
327 content: message.content,
328 reasoning,
329 name: message.name,
330 tool_calls: message.tool_calls,
331 tool_call_id: message.tool_call_id,
332 function_call: message.function_call,
333 })
334 }
335}
336
337#[derive(Debug, Clone, Serialize, Deserialize)]
339pub struct ChatToolCall {
340 #[serde(skip_serializing_if = "Option::is_none")]
341 pub index: Option<u32>,
342 pub id: String,
343 #[serde(rename = "type")]
344 pub tool_type: String,
345 pub function: ChatFunctionCall,
346}
347
348#[derive(Debug, Clone, Serialize, Deserialize)]
350pub struct ChatFunctionCall {
351 pub name: String,
352 pub arguments: String,
353}
354
355fn deserialize_message_content<'de, D>(deserializer: D) -> Result<String, D::Error>
361where
362 D: serde::Deserializer<'de>,
363{
364 let value = serde_json::Value::deserialize(deserializer)?;
365 match value {
366 serde_json::Value::Null => Ok(String::new()),
367 serde_json::Value::String(s) => Ok(s),
368 serde_json::Value::Array(parts) => {
369 let mut text_parts = Vec::with_capacity(parts.len());
370 for part in parts {
371 let ty = part
372 .get("type")
373 .and_then(|v| v.as_str())
374 .ok_or_else(|| de::Error::custom("message content part missing type"))?;
375 if ty != "text" {
376 return Err(de::Error::custom(format!(
377 "unsupported message content part type `{ty}`"
378 )));
379 }
380 if let Some(text) = part.get("text").and_then(|v| v.as_str()) {
381 text_parts.push(text.to_string());
382 }
383 }
384 Ok(text_parts.join("\n"))
385 }
386 _ => Err(de::Error::custom(
387 "message content must be a string, null, or an array of text parts",
388 )),
389 }
390}
391
392fn deserialize_stop_sequences<'de, D>(deserializer: D) -> Result<Option<Vec<String>>, D::Error>
393where
394 D: serde::Deserializer<'de>,
395{
396 let value = Option::<serde_json::Value>::deserialize(deserializer)?;
397 match value {
398 None | Some(serde_json::Value::Null) => Ok(None),
399 Some(serde_json::Value::String(stop)) => Ok(Some(vec![stop])),
400 Some(serde_json::Value::Array(values)) => {
401 let mut stops = Vec::with_capacity(values.len());
402 for value in values {
403 match value {
404 serde_json::Value::String(stop) => stops.push(stop),
405 _ => {
406 return Err(de::Error::custom(
407 "stop must be a string or an array of strings",
408 ))
409 }
410 }
411 }
412 Ok(Some(stops))
413 }
414 _ => Err(de::Error::custom(
415 "stop must be a string or an array of strings",
416 )),
417 }
418}
419
420#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
422#[serde(rename_all = "lowercase")]
423pub enum MessageRole {
424 System,
425 User,
426 Assistant,
427 Function,
428 Tool,
429}
430
431#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
434#[serde(rename_all = "snake_case")]
435pub(crate) enum AssistantMessagePhase {
436 Commentary,
437 FinalAnswer,
438}
439
440#[derive(Debug, Clone, Serialize, Deserialize)]
442pub struct ChatCompletionsResponse {
443 pub id: String,
445
446 pub object: String,
448
449 pub created: u64,
451
452 pub model: String,
454
455 pub choices: Vec<ChatChoice>,
457
458 #[serde(skip_serializing_if = "Option::is_none")]
460 pub usage: Option<Usage>,
461}
462
463#[derive(Debug, Clone, Serialize, Deserialize)]
465pub struct ChatChoice {
466 pub index: u32,
468
469 #[serde(skip_serializing_if = "Option::is_none")]
471 pub message: Option<ChatMessage>,
472
473 #[serde(skip_serializing_if = "Option::is_none")]
475 pub delta: Option<ChatMessage>,
476
477 #[serde(skip_serializing_if = "Option::is_none")]
479 pub finish_reason: Option<String>,
480}
481
482#[derive(Debug, Clone, Serialize, Deserialize)]
484pub struct CompletionsRequest {
485 pub model: String,
487
488 #[serde(default)]
492 pub prompt: CompletionPrompt,
493
494 #[serde(skip_serializing_if = "Option::is_none")]
496 pub max_tokens: Option<u32>,
497
498 #[serde(skip_serializing_if = "Option::is_none")]
500 pub temperature: Option<f32>,
501
502 #[serde(skip_serializing_if = "Option::is_none")]
504 pub top_p: Option<f32>,
505
506 #[serde(skip_serializing_if = "Option::is_none")]
509 pub n: Option<u32>,
510
511 #[serde(skip_serializing_if = "Option::is_none")]
513 pub stream: Option<bool>,
514
515 #[serde(default, deserialize_with = "deserialize_stop_sequences")]
517 #[serde(skip_serializing_if = "Option::is_none")]
518 pub stop: Option<Vec<String>>,
519
520 #[serde(skip_serializing_if = "Option::is_none")]
523 pub logprobs: Option<u32>,
524
525 #[serde(skip_serializing_if = "Option::is_none")]
527 pub logit_bias: Option<HashMap<String, f32>>,
528}
529
530#[derive(Debug, Clone, Serialize, Deserialize)]
533#[serde(untagged)]
534pub enum CompletionPrompt {
535 Text(String),
536 Unsupported(serde_json::Value),
537}
538
539impl Default for CompletionPrompt {
540 fn default() -> Self {
541 Self::Unsupported(serde_json::Value::Null)
542 }
543}
544
545impl CompletionPrompt {
546 pub fn as_text(&self) -> Option<&str> {
547 match self {
548 Self::Text(text) => Some(text),
549 Self::Unsupported(_) => None,
550 }
551 }
552}
553
554#[derive(Debug, Clone, Serialize, Deserialize)]
556pub struct CompletionsResponse {
557 pub id: String,
558 pub object: String,
559 pub created: u64,
560 pub model: String,
561 pub choices: Vec<CompletionChoice>,
562 pub usage: Option<Usage>,
563}
564
565#[derive(Debug, Clone, Serialize, Deserialize)]
567pub struct CompletionChoice {
568 pub text: String,
569 pub index: u32,
570 pub finish_reason: Option<String>,
571}
572
573#[derive(Debug, Clone, Serialize, Deserialize)]
575pub struct Usage {
576 pub prompt_tokens: u32,
577 pub completion_tokens: u32,
578 pub total_tokens: u32,
579}
580
581#[derive(Debug, Clone, Serialize, Deserialize)]
583pub struct ModelListResponse {
584 pub object: String,
585 pub data: Vec<ModelInfo>,
586}
587
588#[derive(Debug, Clone, Serialize, Deserialize)]
590pub struct ModelInfo {
591 pub id: String,
592 pub object: String,
593 pub created: u64,
594 pub owned_by: String,
595 pub modalities: Vec<String>,
596 pub permission: Vec<ModelPermission>,
597 pub root: Option<String>,
598 pub parent: Option<String>,
599}
600
601#[derive(Debug, Clone, Serialize, Deserialize)]
603pub struct ModelPermission {
604 pub id: String,
605 pub object: String,
606 pub created: u64,
607 pub allow_create_engine: bool,
608 pub allow_sampling: bool,
609 pub allow_logprobs: bool,
610 pub allow_search_indices: bool,
611 pub allow_view: bool,
612 pub allow_fine_tuning: bool,
613 pub organization: String,
614 pub group: Option<String>,
615 pub is_blocking: bool,
616}
617
618#[derive(Debug, Clone, Serialize, Deserialize)]
622pub struct EmbeddingsRequest {
623 pub model: String,
625
626 pub input: EmbeddingInput,
628
629 #[serde(skip_serializing_if = "Option::is_none")]
631 pub encoding_format: Option<String>,
632}
633
634#[derive(Debug, Clone, Serialize, Deserialize)]
637#[serde(untagged)]
638pub enum EmbeddingInput {
639 Single(String),
641 Batch(Vec<String>),
643 SingleObject(EmbeddingItem),
645 BatchObjects(Vec<EmbeddingItem>),
647}
648
649#[derive(Debug, Clone, Serialize, Deserialize)]
651pub struct EmbeddingItem {
652 #[serde(skip_serializing_if = "Option::is_none")]
654 pub text: Option<String>,
655 #[serde(skip_serializing_if = "Option::is_none")]
657 pub image: Option<String>,
658}
659
660#[derive(Debug, Clone, Serialize, Deserialize)]
662pub struct EmbeddingsResponse {
663 pub object: String,
664 pub data: Vec<EmbeddingData>,
665 pub model: String,
666 pub usage: EmbeddingUsage,
667}
668
669#[derive(Debug, Clone, Serialize, Deserialize)]
671pub struct EmbeddingData {
672 pub object: String,
673 pub embedding: Vec<f32>,
674 pub index: usize,
675}
676
677#[derive(Debug, Clone, Serialize, Deserialize)]
679pub struct EmbeddingUsage {
680 pub prompt_tokens: u32,
681 pub total_tokens: u32,
682}
683
684#[derive(Debug, Clone, Serialize, Deserialize)]
688pub struct TranscriptionResponse {
689 pub text: String,
690}
691
692#[derive(Debug, Clone, Serialize, Deserialize)]
696pub struct OpenAiError {
697 pub error: OpenAiErrorDetail,
698}
699
700#[derive(Debug, Clone, Serialize, Deserialize)]
702pub struct OpenAiErrorDetail {
703 pub message: String,
704 #[serde(rename = "type")]
705 pub error_type: String,
706 pub param: Option<String>,
707 pub code: Option<String>,
708}
709
710#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
712pub enum OpenAiErrorType {
713 InvalidRequestError,
714 AuthenticationError,
715 PermissionError,
716 NotFoundError,
717 RateLimitError,
718 InternalServerError,
719 ServiceUnavailableError,
720}
721
722#[derive(Debug, Clone)]
724pub struct SseEvent {
725 pub event: Option<String>,
726 pub data: String,
727 pub id: Option<String>,
728 pub retry: Option<u32>,
729}
730
731impl SseEvent {
732 pub fn data(data: String) -> Self {
733 Self {
734 event: None,
735 data,
736 id: None,
737 retry: None,
738 }
739 }
740
741 pub fn json(value: &serde_json::Value) -> Result<Self, serde_json::Error> {
742 Ok(Self::data(serde_json::to_string(value)?))
743 }
744
745 pub fn to_string(&self) -> String {
746 let mut result = String::new();
747
748 if let Some(event) = &self.event {
749 result.push_str(&format!("event: {}\n", event));
750 }
751
752 if let Some(id) = &self.id {
753 result.push_str(&format!("id: {}\n", id));
754 }
755
756 if let Some(retry) = self.retry {
757 result.push_str(&format!("retry: {}\n", retry));
758 }
759
760 result.push_str(&format!("data: {}\n\n", self.data));
761 result
762 }
763}
764
765#[derive(Debug, Clone, Serialize, Deserialize)]
767pub struct SpeechRequest {
768 #[serde(default = "default_tts_model")]
770 pub model: String,
771
772 pub input: String,
774
775 #[serde(default = "default_voice")]
777 pub voice: String,
778
779 #[serde(default = "default_audio_format")]
781 pub response_format: String,
782
783 #[serde(default = "default_language")]
785 pub language: String,
786
787 #[serde(default)]
789 pub stream: bool,
790}
791
792fn default_tts_model() -> String {
793 "qwen3-tts".to_string()
794}
795fn default_voice() -> String {
796 "default".to_string()
797}
798fn default_audio_format() -> String {
799 "wav".to_string()
800}
801fn default_language() -> String {
802 "auto".to_string()
803}
804
805#[cfg(test)]
806mod tests {
807 use super::*;
808
809 fn chat_request_with_assistant_fields(fields: &str) -> String {
810 format!(r#"{{"model":"test","messages":[{{"role":"assistant","content":null{fields}}}]}}"#)
811 }
812
813 #[test]
814 fn chat_request_normalizes_reasoning_content_at_the_wire_boundary() {
815 let cases = [
816 ("missing", "", None),
817 ("compatibility null", r#", "reasoning_content": null"#, None),
818 (
819 "compatibility empty",
820 r#", "reasoning_content": """#,
821 Some(""),
822 ),
823 (
824 "compatibility text",
825 r#", "reasoning_content": "compatibility""#,
826 Some("compatibility"),
827 ),
828 (
829 "canonical text",
830 r#", "reasoning": "canonical""#,
831 Some("canonical"),
832 ),
833 (
834 "compatibility then canonical",
835 r#", "reasoning_content": "compatibility", "reasoning": "canonical""#,
836 Some("canonical"),
837 ),
838 (
839 "canonical then compatibility",
840 r#", "reasoning": "canonical", "reasoning_content": "compatibility""#,
841 Some("canonical"),
842 ),
843 (
844 "canonical empty wins",
845 r#", "reasoning": "", "reasoning_content": "compatibility""#,
846 Some(""),
847 ),
848 (
849 "canonical null falls back",
850 r#", "reasoning": null, "reasoning_content": "compatibility""#,
851 Some("compatibility"),
852 ),
853 (
854 "canonical text ignores invalid compatibility",
855 r#", "reasoning_content": 7, "reasoning": "canonical""#,
856 Some("canonical"),
857 ),
858 (
859 "canonical empty ignores invalid compatibility",
860 r#", "reasoning": "", "reasoning_content": {"unexpected": true}"#,
861 Some(""),
862 ),
863 ];
864
865 for (name, fields, expected) in cases {
866 let request: ChatCompletionsRequest =
867 serde_json::from_str(&chat_request_with_assistant_fields(fields))
868 .unwrap_or_else(|error| panic!("{name}: {error}"));
869 assert_eq!(request.messages[0].reasoning.as_deref(), expected, "{name}");
870
871 let normalized = serde_json::to_value(request).expect("normalized request JSON");
872 let message = &normalized["messages"][0];
873 assert!(message.get("reasoning_content").is_none(), "{name}");
874 match expected {
875 Some(expected) => assert_eq!(message["reasoning"], expected, "{name}"),
876 None => assert!(message.get("reasoning").is_none(), "{name}"),
877 }
878 }
879 }
880
881 #[test]
882 fn chat_request_rejects_non_string_reasoning_fields() {
883 for (name, fields) in [
884 ("compatibility", r#", "reasoning_content": 7"#),
885 (
886 "canonical is not masked by compatibility",
887 r#", "reasoning": 7, "reasoning_content": "compatibility""#,
888 ),
889 (
890 "canonical null validates compatibility",
891 r#", "reasoning": null, "reasoning_content": 7"#,
892 ),
893 ] {
894 let error = serde_json::from_str::<ChatCompletionsRequest>(
895 &chat_request_with_assistant_fields(fields),
896 )
897 .expect_err(name);
898 assert!(error.to_string().contains("string"), "{name}: {error}");
899 }
900 }
901}