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(default, skip_serializing_if = "Option::is_none")]
104 pub reasoning_effort: Option<ferrum_types::ReasoningEffort>,
105
106 #[serde(skip_serializing_if = "Option::is_none")]
110 pub tools: Option<Vec<ChatTool>>,
111
112 #[serde(skip_serializing_if = "Option::is_none")]
114 pub tool_choice: Option<ToolChoice>,
115
116 #[serde(skip_serializing_if = "Option::is_none")]
118 pub stream_options: Option<StreamOptions>,
119
120 #[serde(skip_serializing_if = "Option::is_none")]
122 pub functions: Option<Vec<ChatFunction>>,
123
124 #[serde(skip_serializing_if = "Option::is_none")]
126 pub function_call: Option<FunctionCallChoice>,
127
128 #[serde(default, skip_serializing_if = "Option::is_none")]
132 pub metadata: Option<HashMap<String, serde_json::Value>>,
133
134 #[serde(default, skip_serializing_if = "Option::is_none")]
138 pub chat_template_kwargs: Option<HashMap<String, serde_json::Value>>,
139}
140
141#[derive(Debug, Clone, Serialize)]
143pub struct StreamOptions {
144 #[serde(default, skip_serializing_if = "Option::is_none")]
145 pub include_usage: Option<bool>,
146}
147
148impl<'de> Deserialize<'de> for StreamOptions {
149 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
150 where
151 D: serde::Deserializer<'de>,
152 {
153 #[derive(Deserialize)]
154 #[serde(deny_unknown_fields)]
155 struct Object {
156 #[serde(default)]
157 include_usage: Option<bool>,
158 }
159
160 let value = serde_json::Value::deserialize(deserializer)?;
161 if !value.is_object() {
162 return Err(de::Error::custom("stream_options must be a JSON object"));
163 }
164 let parsed = serde_json::from_value::<Object>(value).map_err(de::Error::custom)?;
165 Ok(Self {
166 include_usage: parsed.include_usage,
167 })
168 }
169}
170
171#[derive(Debug, Clone, Serialize, Deserialize)]
173pub struct ChatTool {
174 #[serde(rename = "type")]
175 pub tool_type: String,
176 pub function: ChatFunction,
177}
178
179#[derive(Debug, Clone, Serialize, Deserialize)]
181pub struct ChatFunction {
182 pub name: String,
183 #[serde(default, skip_serializing_if = "Option::is_none")]
184 pub description: Option<String>,
185 #[serde(default, skip_serializing_if = "Option::is_none")]
186 pub parameters: Option<serde_json::Value>,
187 #[serde(default, skip_serializing_if = "Option::is_none")]
188 pub strict: Option<bool>,
189}
190
191#[derive(Debug, Clone, Serialize, Deserialize)]
194#[serde(untagged)]
195pub enum ToolChoice {
196 Mode(String),
197 Function {
198 #[serde(rename = "type")]
199 tool_type: String,
200 function: ToolChoiceFunction,
201 },
202}
203
204#[derive(Debug, Clone, Serialize, Deserialize)]
205pub struct ToolChoiceFunction {
206 pub name: String,
207}
208
209#[derive(Debug, Clone, Serialize, Deserialize)]
211#[serde(untagged)]
212pub enum FunctionCallChoice {
213 Mode(String),
214 Function { name: String },
215}
216
217#[derive(Debug, Clone, Serialize, Deserialize)]
225pub struct OpenAiResponseFormat {
226 #[serde(rename = "type")]
227 pub format_type: String,
228 #[serde(default, skip_serializing_if = "Option::is_none")]
230 pub json_schema: Option<OpenAiJsonSchema>,
231}
232
233#[derive(Debug, Clone, Serialize, Deserialize)]
234pub struct OpenAiJsonSchema {
235 #[serde(default, skip_serializing_if = "Option::is_none")]
237 pub name: Option<String>,
238 #[serde(default, skip_serializing_if = "Option::is_none")]
244 pub schema: Option<serde_json::Value>,
245 #[serde(default, skip_serializing_if = "Option::is_none")]
249 pub strict: Option<bool>,
250}
251
252#[derive(Debug, Clone, Serialize, Deserialize)]
254#[serde(try_from = "ChatMessageWire")]
255pub struct ChatMessage {
256 pub role: MessageRole,
258
259 #[serde(default)]
265 #[serde(deserialize_with = "deserialize_message_content")]
266 pub content: String,
267
268 #[serde(default, skip_serializing_if = "Option::is_none")]
275 pub reasoning: Option<String>,
276
277 #[serde(skip_serializing_if = "Option::is_none")]
279 pub name: Option<String>,
280
281 #[serde(skip_serializing_if = "Option::is_none")]
283 pub tool_calls: Option<Vec<ChatToolCall>>,
284
285 #[serde(skip_serializing_if = "Option::is_none")]
287 pub tool_call_id: Option<String>,
288
289 #[serde(skip_serializing_if = "Option::is_none")]
291 pub function_call: Option<ChatFunctionCall>,
292}
293
294#[derive(Deserialize)]
298struct ChatMessageWire {
299 role: MessageRole,
300 #[serde(default, deserialize_with = "deserialize_message_content")]
301 content: String,
302 #[serde(default)]
303 reasoning: serde_json::Value,
304 #[serde(default)]
305 reasoning_content: serde_json::Value,
306 #[serde(default)]
307 name: Option<String>,
308 #[serde(default)]
309 tool_calls: Option<Vec<ChatToolCall>>,
310 #[serde(default)]
311 tool_call_id: Option<String>,
312 #[serde(default)]
313 function_call: Option<ChatFunctionCall>,
314}
315
316impl TryFrom<ChatMessageWire> for ChatMessage {
317 type Error = String;
318
319 fn try_from(message: ChatMessageWire) -> Result<Self, Self::Error> {
320 let reasoning = match message.reasoning {
321 serde_json::Value::String(reasoning) => Some(reasoning),
322 serde_json::Value::Null => match message.reasoning_content {
323 serde_json::Value::String(reasoning) => Some(reasoning),
324 serde_json::Value::Null => None,
325 _ => return Err("reasoning_content must be a string or null".to_string()),
326 },
327 _ => return Err("reasoning must be a string or null".to_string()),
328 };
329
330 Ok(Self {
331 role: message.role,
332 content: message.content,
333 reasoning,
334 name: message.name,
335 tool_calls: message.tool_calls,
336 tool_call_id: message.tool_call_id,
337 function_call: message.function_call,
338 })
339 }
340}
341
342#[derive(Debug, Clone, Serialize, Deserialize)]
344pub struct ChatToolCall {
345 #[serde(skip_serializing_if = "Option::is_none")]
346 pub index: Option<u32>,
347 pub id: String,
348 #[serde(rename = "type")]
349 pub tool_type: String,
350 pub function: ChatFunctionCall,
351}
352
353#[derive(Debug, Clone, Serialize, Deserialize)]
355pub struct ChatFunctionCall {
356 pub name: String,
357 pub arguments: String,
358}
359
360fn deserialize_message_content<'de, D>(deserializer: D) -> Result<String, D::Error>
366where
367 D: serde::Deserializer<'de>,
368{
369 let value = serde_json::Value::deserialize(deserializer)?;
370 match value {
371 serde_json::Value::Null => Ok(String::new()),
372 serde_json::Value::String(s) => Ok(s),
373 serde_json::Value::Array(parts) => {
374 let mut text_parts = Vec::with_capacity(parts.len());
375 for part in parts {
376 let ty = part
377 .get("type")
378 .and_then(|v| v.as_str())
379 .ok_or_else(|| de::Error::custom("message content part missing type"))?;
380 if ty != "text" {
381 return Err(de::Error::custom(format!(
382 "unsupported message content part type `{ty}`"
383 )));
384 }
385 if let Some(text) = part.get("text").and_then(|v| v.as_str()) {
386 text_parts.push(text.to_string());
387 }
388 }
389 Ok(text_parts.join("\n"))
390 }
391 _ => Err(de::Error::custom(
392 "message content must be a string, null, or an array of text parts",
393 )),
394 }
395}
396
397fn deserialize_stop_sequences<'de, D>(deserializer: D) -> Result<Option<Vec<String>>, D::Error>
398where
399 D: serde::Deserializer<'de>,
400{
401 let value = Option::<serde_json::Value>::deserialize(deserializer)?;
402 match value {
403 None | Some(serde_json::Value::Null) => Ok(None),
404 Some(serde_json::Value::String(stop)) => Ok(Some(vec![stop])),
405 Some(serde_json::Value::Array(values)) => {
406 let mut stops = Vec::with_capacity(values.len());
407 for value in values {
408 match value {
409 serde_json::Value::String(stop) => stops.push(stop),
410 _ => {
411 return Err(de::Error::custom(
412 "stop must be a string or an array of strings",
413 ))
414 }
415 }
416 }
417 Ok(Some(stops))
418 }
419 _ => Err(de::Error::custom(
420 "stop must be a string or an array of strings",
421 )),
422 }
423}
424
425#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
427#[serde(rename_all = "lowercase")]
428pub enum MessageRole {
429 System,
430 User,
431 Assistant,
432 Function,
433 Tool,
434}
435
436#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
439#[serde(rename_all = "snake_case")]
440pub(crate) enum AssistantMessagePhase {
441 Commentary,
442 FinalAnswer,
443}
444
445#[derive(Debug, Clone, Serialize, Deserialize)]
447pub struct ChatCompletionsResponse {
448 pub id: String,
450
451 pub object: String,
453
454 pub created: u64,
456
457 pub model: String,
459
460 pub choices: Vec<ChatChoice>,
462
463 #[serde(skip_serializing_if = "Option::is_none")]
465 pub usage: Option<Usage>,
466}
467
468#[derive(Debug, Clone, Serialize, Deserialize)]
470pub struct ChatChoice {
471 pub index: u32,
473
474 #[serde(skip_serializing_if = "Option::is_none")]
476 pub message: Option<ChatMessage>,
477
478 #[serde(skip_serializing_if = "Option::is_none")]
480 pub delta: Option<ChatMessage>,
481
482 #[serde(skip_serializing_if = "Option::is_none")]
484 pub finish_reason: Option<String>,
485}
486
487#[derive(Debug, Clone, Serialize, Deserialize)]
489pub struct CompletionsRequest {
490 pub model: String,
492
493 #[serde(default)]
497 pub prompt: CompletionPrompt,
498
499 #[serde(skip_serializing_if = "Option::is_none")]
501 pub max_tokens: Option<u32>,
502
503 #[serde(skip_serializing_if = "Option::is_none")]
505 pub temperature: Option<f32>,
506
507 #[serde(skip_serializing_if = "Option::is_none")]
509 pub top_p: Option<f32>,
510
511 #[serde(skip_serializing_if = "Option::is_none")]
514 pub n: Option<u32>,
515
516 #[serde(skip_serializing_if = "Option::is_none")]
518 pub stream: Option<bool>,
519
520 #[serde(default, deserialize_with = "deserialize_stop_sequences")]
522 #[serde(skip_serializing_if = "Option::is_none")]
523 pub stop: Option<Vec<String>>,
524
525 #[serde(skip_serializing_if = "Option::is_none")]
528 pub logprobs: Option<u32>,
529
530 #[serde(skip_serializing_if = "Option::is_none")]
532 pub logit_bias: Option<HashMap<String, f32>>,
533}
534
535#[derive(Debug, Clone, Serialize, Deserialize)]
538#[serde(untagged)]
539pub enum CompletionPrompt {
540 Text(String),
541 Unsupported(serde_json::Value),
542}
543
544impl Default for CompletionPrompt {
545 fn default() -> Self {
546 Self::Unsupported(serde_json::Value::Null)
547 }
548}
549
550impl CompletionPrompt {
551 pub fn as_text(&self) -> Option<&str> {
552 match self {
553 Self::Text(text) => Some(text),
554 Self::Unsupported(_) => None,
555 }
556 }
557}
558
559#[derive(Debug, Clone, Serialize, Deserialize)]
561pub struct CompletionsResponse {
562 pub id: String,
563 pub object: String,
564 pub created: u64,
565 pub model: String,
566 pub choices: Vec<CompletionChoice>,
567 pub usage: Option<Usage>,
568}
569
570#[derive(Debug, Clone, Serialize, Deserialize)]
572pub struct CompletionChoice {
573 pub text: String,
574 pub index: u32,
575 pub finish_reason: Option<String>,
576}
577
578#[derive(Debug, Clone, Serialize, Deserialize)]
580pub struct Usage {
581 pub prompt_tokens: u32,
582 pub completion_tokens: u32,
583 pub total_tokens: u32,
584}
585
586#[derive(Debug, Clone, Serialize, Deserialize)]
588pub struct ModelListResponse {
589 pub object: String,
590 pub data: Vec<ModelInfo>,
591}
592
593#[derive(Debug, Clone, Serialize, Deserialize)]
595pub struct ModelInfo {
596 pub id: String,
597 pub object: String,
598 pub created: u64,
599 pub owned_by: String,
600 pub modalities: Vec<String>,
601 #[serde(default, skip_serializing_if = "Option::is_none")]
603 pub max_model_len: Option<usize>,
604 #[serde(default, skip_serializing_if = "Option::is_none")]
606 pub reasoning: Option<ModelReasoningCapabilities>,
607 pub permission: Vec<ModelPermission>,
608 pub root: Option<String>,
609 pub parent: Option<String>,
610}
611
612#[derive(Debug, Clone, Serialize, Deserialize)]
614pub struct ModelReasoningCapabilities {
615 #[serde(default, skip_serializing_if = "Option::is_none")]
617 pub supported_efforts: Option<Vec<ferrum_types::ReasoningEffort>>,
618 #[serde(default, skip_serializing_if = "Option::is_none")]
620 pub thinking: Option<ModelThinkingCapability>,
621}
622
623#[derive(Debug, Clone, Serialize, Deserialize)]
624pub struct ModelThinkingCapability {
625 pub default_enabled: bool,
627}
628
629#[derive(Debug, Clone, Serialize, Deserialize)]
631pub struct ModelPermission {
632 pub id: String,
633 pub object: String,
634 pub created: u64,
635 pub allow_create_engine: bool,
636 pub allow_sampling: bool,
637 pub allow_logprobs: bool,
638 pub allow_search_indices: bool,
639 pub allow_view: bool,
640 pub allow_fine_tuning: bool,
641 pub organization: String,
642 pub group: Option<String>,
643 pub is_blocking: bool,
644}
645
646#[derive(Debug, Clone, Serialize, Deserialize)]
650pub struct EmbeddingsRequest {
651 pub model: String,
653
654 pub input: EmbeddingInput,
656
657 #[serde(skip_serializing_if = "Option::is_none")]
659 pub encoding_format: Option<String>,
660}
661
662#[derive(Debug, Clone, Serialize, Deserialize)]
665#[serde(untagged)]
666pub enum EmbeddingInput {
667 Single(String),
669 Batch(Vec<String>),
671 SingleObject(EmbeddingItem),
673 BatchObjects(Vec<EmbeddingItem>),
675}
676
677#[derive(Debug, Clone, Serialize, Deserialize)]
679pub struct EmbeddingItem {
680 #[serde(skip_serializing_if = "Option::is_none")]
682 pub text: Option<String>,
683 #[serde(skip_serializing_if = "Option::is_none")]
685 pub image: Option<String>,
686}
687
688#[derive(Debug, Clone, Serialize, Deserialize)]
690pub struct EmbeddingsResponse {
691 pub object: String,
692 pub data: Vec<EmbeddingData>,
693 pub model: String,
694 pub usage: EmbeddingUsage,
695}
696
697#[derive(Debug, Clone, Serialize, Deserialize)]
699pub struct EmbeddingData {
700 pub object: String,
701 pub embedding: Vec<f32>,
702 pub index: usize,
703}
704
705#[derive(Debug, Clone, Serialize, Deserialize)]
707pub struct EmbeddingUsage {
708 pub prompt_tokens: u32,
709 pub total_tokens: u32,
710}
711
712#[derive(Debug, Clone, Serialize, Deserialize)]
716pub struct TranscriptionResponse {
717 pub text: String,
718}
719
720#[derive(Debug, Clone, Serialize, Deserialize)]
724pub struct OpenAiError {
725 pub error: OpenAiErrorDetail,
726}
727
728#[derive(Debug, Clone, Serialize, Deserialize)]
730pub struct OpenAiErrorDetail {
731 pub message: String,
732 #[serde(rename = "type")]
733 pub error_type: String,
734 pub param: Option<String>,
735 pub code: Option<String>,
736}
737
738#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
740pub enum OpenAiErrorType {
741 InvalidRequestError,
742 AuthenticationError,
743 PermissionError,
744 NotFoundError,
745 RateLimitError,
746 InternalServerError,
747 ServiceUnavailableError,
748}
749
750#[derive(Debug, Clone)]
752pub struct SseEvent {
753 pub event: Option<String>,
754 pub data: String,
755 pub id: Option<String>,
756 pub retry: Option<u32>,
757}
758
759impl SseEvent {
760 pub fn data(data: String) -> Self {
761 Self {
762 event: None,
763 data,
764 id: None,
765 retry: None,
766 }
767 }
768
769 pub fn json(value: &serde_json::Value) -> Result<Self, serde_json::Error> {
770 Ok(Self::data(serde_json::to_string(value)?))
771 }
772
773 pub fn to_string(&self) -> String {
774 let mut result = String::new();
775
776 if let Some(event) = &self.event {
777 result.push_str(&format!("event: {}\n", event));
778 }
779
780 if let Some(id) = &self.id {
781 result.push_str(&format!("id: {}\n", id));
782 }
783
784 if let Some(retry) = self.retry {
785 result.push_str(&format!("retry: {}\n", retry));
786 }
787
788 result.push_str(&format!("data: {}\n\n", self.data));
789 result
790 }
791}
792
793#[derive(Debug, Clone, Serialize, Deserialize)]
795pub struct SpeechRequest {
796 #[serde(default = "default_tts_model")]
798 pub model: String,
799
800 pub input: String,
802
803 #[serde(default = "default_voice")]
805 pub voice: String,
806
807 #[serde(default = "default_audio_format")]
809 pub response_format: String,
810
811 #[serde(default = "default_language")]
813 pub language: String,
814
815 #[serde(default)]
817 pub stream: bool,
818}
819
820fn default_tts_model() -> String {
821 "qwen3-tts".to_string()
822}
823fn default_voice() -> String {
824 "default".to_string()
825}
826fn default_audio_format() -> String {
827 "wav".to_string()
828}
829fn default_language() -> String {
830 "auto".to_string()
831}
832
833#[cfg(test)]
834mod tests {
835 use super::*;
836
837 fn chat_request_with_assistant_fields(fields: &str) -> String {
838 format!(r#"{{"model":"test","messages":[{{"role":"assistant","content":null{fields}}}]}}"#)
839 }
840
841 #[test]
842 fn chat_request_normalizes_reasoning_content_at_the_wire_boundary() {
843 let cases = [
844 ("missing", "", None),
845 ("compatibility null", r#", "reasoning_content": null"#, None),
846 (
847 "compatibility empty",
848 r#", "reasoning_content": """#,
849 Some(""),
850 ),
851 (
852 "compatibility text",
853 r#", "reasoning_content": "compatibility""#,
854 Some("compatibility"),
855 ),
856 (
857 "canonical text",
858 r#", "reasoning": "canonical""#,
859 Some("canonical"),
860 ),
861 (
862 "compatibility then canonical",
863 r#", "reasoning_content": "compatibility", "reasoning": "canonical""#,
864 Some("canonical"),
865 ),
866 (
867 "canonical then compatibility",
868 r#", "reasoning": "canonical", "reasoning_content": "compatibility""#,
869 Some("canonical"),
870 ),
871 (
872 "canonical empty wins",
873 r#", "reasoning": "", "reasoning_content": "compatibility""#,
874 Some(""),
875 ),
876 (
877 "canonical null falls back",
878 r#", "reasoning": null, "reasoning_content": "compatibility""#,
879 Some("compatibility"),
880 ),
881 (
882 "canonical text ignores invalid compatibility",
883 r#", "reasoning_content": 7, "reasoning": "canonical""#,
884 Some("canonical"),
885 ),
886 (
887 "canonical empty ignores invalid compatibility",
888 r#", "reasoning": "", "reasoning_content": {"unexpected": true}"#,
889 Some(""),
890 ),
891 ];
892
893 for (name, fields, expected) in cases {
894 let request: ChatCompletionsRequest =
895 serde_json::from_str(&chat_request_with_assistant_fields(fields))
896 .unwrap_or_else(|error| panic!("{name}: {error}"));
897 assert_eq!(request.messages[0].reasoning.as_deref(), expected, "{name}");
898
899 let normalized = serde_json::to_value(request).expect("normalized request JSON");
900 let message = &normalized["messages"][0];
901 assert!(message.get("reasoning_content").is_none(), "{name}");
902 match expected {
903 Some(expected) => assert_eq!(message["reasoning"], expected, "{name}"),
904 None => assert!(message.get("reasoning").is_none(), "{name}"),
905 }
906 }
907 }
908
909 #[test]
910 fn chat_request_rejects_non_string_reasoning_fields() {
911 for (name, fields) in [
912 ("compatibility", r#", "reasoning_content": 7"#),
913 (
914 "canonical is not masked by compatibility",
915 r#", "reasoning": 7, "reasoning_content": "compatibility""#,
916 ),
917 (
918 "canonical null validates compatibility",
919 r#", "reasoning": null, "reasoning_content": 7"#,
920 ),
921 ] {
922 let error = serde_json::from_str::<ChatCompletionsRequest>(
923 &chat_request_with_assistant_fields(fields),
924 )
925 .expect_err(name);
926 assert!(error.to_string().contains("string"), "{name}: {error}");
927 }
928 }
929}