1use std::collections::HashMap;
8
9use serde::{Deserialize, Serialize};
10
11#[derive(Debug, Deserialize)]
21pub struct ChatCompletionRequest {
22 pub model: ModelField,
24 pub messages: Vec<ChatCompletionMessage>,
26 #[serde(default)]
28 pub stream: bool,
29 #[serde(default)]
31 pub temperature: Option<f32>,
32 #[serde(default)]
34 pub max_tokens: Option<u32>,
35 #[serde(default)]
37 pub strict_capabilities: Option<bool>,
38 #[serde(default)]
40 pub tools: Option<Vec<ToolDefinition>>,
41 #[serde(default)]
43 pub tool_choice: Option<ToolChoice>,
44 #[serde(default)]
46 pub response_format: Option<ResponseFormatRequest>,
47 #[serde(default)]
49 pub top_p: Option<f32>,
50 #[serde(default)]
52 pub stop: Option<StopField>,
53}
54
55#[derive(Debug, Clone, Deserialize)]
60#[serde(untagged)]
61pub enum StopField {
62 Single(String),
64 Multiple(Vec<String>),
66}
67
68const MAX_STOP_SEQUENCES: usize = 4;
70
71impl StopField {
72 pub const fn len(&self) -> usize {
74 match self {
75 Self::Single(_) => 1,
76 Self::Multiple(v) => v.len(),
77 }
78 }
79
80 pub const fn is_empty(&self) -> bool {
82 matches!(self, Self::Multiple(v) if v.is_empty())
83 }
84
85 pub fn into_vec(self) -> Vec<String> {
88 match self {
89 Self::Single(s) => vec![s],
90 Self::Multiple(v) => v.into_iter().take(MAX_STOP_SEQUENCES).collect(),
91 }
92 }
93
94 pub fn to_bounded_vec(&self) -> Vec<String> {
97 match self {
98 Self::Single(s) => vec![s.clone()],
99 Self::Multiple(v) => v.iter().take(MAX_STOP_SEQUENCES).cloned().collect(),
100 }
101 }
102}
103
104#[derive(Debug, Clone, Deserialize)]
106#[serde(tag = "type")]
107pub enum ResponseFormatRequest {
108 #[serde(rename = "text")]
110 Text,
111 #[serde(rename = "json_object")]
113 JsonObject,
114 #[serde(rename = "json_schema")]
116 JsonSchema {
117 json_schema: JsonSchemaSpec,
119 },
120}
121
122#[derive(Debug, Clone, Deserialize)]
124pub struct JsonSchemaSpec {
125 pub name: String,
127 pub schema: serde_json::Value,
129}
130
131#[derive(Debug, Clone, Deserialize)]
133#[serde(untagged)]
134pub enum ModelField {
135 Single(String),
137 Multiple(Vec<String>),
139}
140
141#[derive(Debug, Clone, Deserialize)]
146#[serde(untagged)]
147pub enum MessageContent {
148 Text(String),
150 Parts(Vec<ContentPart>),
152}
153
154impl MessageContent {
155 pub fn as_text(&self) -> String {
157 match self {
158 Self::Text(s) => s.clone(),
159 Self::Parts(parts) => parts
160 .iter()
161 .filter_map(|p| match p {
162 ContentPart::Text { text } => Some(text.as_str()),
163 ContentPart::ImageUrl { .. } => None,
164 })
165 .collect::<Vec<_>>()
166 .join(""),
167 }
168 }
169}
170
171#[derive(Debug, Clone, Deserialize)]
173#[serde(tag = "type")]
174pub enum ContentPart {
175 #[serde(rename = "text")]
177 Text {
178 text: String,
180 },
181 #[serde(rename = "image_url")]
183 ImageUrl {
184 image_url: ImageUrlDetail,
186 },
187}
188
189#[derive(Debug, Clone, Deserialize)]
191pub struct ImageUrlDetail {
192 pub url: String,
194}
195
196#[derive(Debug, Clone, Deserialize)]
198pub struct ChatCompletionMessage {
199 pub role: String,
201 pub content: Option<MessageContent>,
203 #[serde(default)]
205 pub tool_calls: Option<Vec<ToolCall>>,
206 #[serde(default)]
208 pub tool_call_id: Option<String>,
209 #[serde(default)]
211 pub name: Option<String>,
212}
213
214#[derive(Debug, Clone, Deserialize)]
220pub struct ToolDefinition {
221 #[serde(rename = "type")]
223 pub tool_type: String,
224 pub function: FunctionObject,
226}
227
228#[derive(Debug, Clone, Deserialize)]
230pub struct FunctionObject {
231 pub name: String,
233 #[serde(default)]
235 pub description: Option<String>,
236 #[serde(default)]
238 pub parameters: Option<serde_json::Value>,
239}
240
241#[derive(Debug, Clone, Deserialize)]
243#[serde(untagged)]
244pub enum ToolChoice {
245 Mode(String),
247 Specific(ToolChoiceSpecific),
249}
250
251#[derive(Debug, Clone, Deserialize)]
253pub struct ToolChoiceSpecific {
254 #[serde(rename = "type")]
256 pub tool_type: String,
257 pub function: ToolChoiceFunction,
259}
260
261#[derive(Debug, Clone, Deserialize)]
263pub struct ToolChoiceFunction {
264 pub name: String,
266}
267
268#[derive(Debug, Clone, Serialize, Deserialize)]
270pub struct ToolCall {
271 #[serde(default)]
273 pub index: usize,
274 pub id: String,
276 #[serde(rename = "type")]
278 pub tool_type: String,
279 pub function: ToolCallFunction,
281}
282
283#[derive(Debug, Clone, Serialize, Deserialize)]
285pub struct ToolCallFunction {
286 pub name: String,
288 pub arguments: String,
290}
291
292#[derive(Debug, Serialize)]
298pub struct ChatCompletionResponse {
299 pub id: String,
301 pub object: &'static str,
303 pub created: u64,
305 pub model: String,
307 pub choices: Vec<Choice>,
309 #[serde(skip_serializing_if = "Option::is_none")]
311 pub usage: Option<Usage>,
312 #[serde(skip_serializing_if = "Option::is_none")]
314 pub warnings: Option<Vec<String>>,
315}
316
317#[derive(Debug, Serialize)]
319pub struct Choice {
320 pub index: u32,
322 pub message: ResponseMessage,
324 pub finish_reason: Option<String>,
326}
327
328#[derive(Debug, Serialize)]
330pub struct ResponseMessage {
331 pub role: &'static str,
333 #[serde(skip_serializing_if = "Option::is_none")]
335 pub content: Option<String>,
336 #[serde(skip_serializing_if = "Option::is_none")]
338 pub tool_calls: Option<Vec<ToolCall>>,
339}
340
341#[derive(Debug, Serialize)]
343pub struct Usage {
344 #[serde(rename = "prompt_tokens")]
346 pub prompt: u32,
347 #[serde(rename = "completion_tokens")]
349 pub completion: u32,
350 #[serde(rename = "total_tokens")]
352 pub total: u32,
353}
354
355#[derive(Debug, Serialize)]
361pub struct ChatCompletionChunk {
362 pub id: String,
364 pub object: &'static str,
366 pub created: u64,
368 pub model: String,
370 pub choices: Vec<ChunkChoice>,
372}
373
374#[derive(Debug, Serialize)]
376pub struct ChunkChoice {
377 pub index: u32,
379 pub delta: Delta,
381 pub finish_reason: Option<String>,
383}
384
385#[derive(Debug, Serialize)]
387pub struct Delta {
388 #[serde(skip_serializing_if = "Option::is_none")]
390 pub role: Option<&'static str>,
391 #[serde(skip_serializing_if = "Option::is_none")]
393 pub content: Option<String>,
394 #[serde(skip_serializing_if = "Option::is_none")]
396 pub tool_calls: Option<Vec<ToolCall>>,
397}
398
399#[derive(Debug, Serialize)]
405pub struct MultiplexResponse {
406 pub id: String,
408 pub object: &'static str,
410 pub created: u64,
412 pub results: Vec<MultiplexProviderResult>,
414 pub summary: String,
416}
417
418#[derive(Debug, Serialize)]
420pub struct MultiplexProviderResult {
421 pub provider: String,
423 #[serde(skip_serializing_if = "Option::is_none")]
425 pub model: Option<String>,
426 #[serde(skip_serializing_if = "Option::is_none")]
428 pub content: Option<String>,
429 #[serde(skip_serializing_if = "Option::is_none")]
431 pub error: Option<String>,
432 pub duration_ms: u64,
434}
435
436#[derive(Debug, Serialize)]
442pub struct ModelsResponse {
443 pub object: &'static str,
445 pub data: Vec<ModelObject>,
447}
448
449#[derive(Debug, Serialize)]
451pub struct ModelObject {
452 pub id: String,
454 pub object: &'static str,
456 pub owned_by: String,
458}
459
460#[derive(Debug, Serialize)]
466pub struct HealthResponse {
467 pub status: &'static str,
469 pub providers: HashMap<String, String>,
471}
472
473#[derive(Debug, Serialize)]
479pub struct ErrorResponse {
480 pub error: ErrorDetail,
482}
483
484#[derive(Debug, Serialize)]
486pub struct ErrorDetail {
487 pub message: String,
489 #[serde(rename = "type")]
491 pub error_type: String,
492 #[serde(skip_serializing_if = "Option::is_none")]
494 pub param: Option<String>,
495 #[serde(skip_serializing_if = "Option::is_none")]
497 pub code: Option<String>,
498}
499
500impl ErrorResponse {
501 pub fn new(error_type: impl Into<String>, message: impl Into<String>) -> Self {
503 Self {
504 error: ErrorDetail {
505 message: message.into(),
506 error_type: error_type.into(),
507 param: None,
508 code: None,
509 },
510 }
511 }
512}
513
514#[cfg(test)]
515mod tests {
516 use super::*;
517
518 #[test]
519 fn deserialize_single_model() {
520 let json = r#"{"model":"copilot:gpt-4o","messages":[{"role":"user","content":"hi"}]}"#;
521 let req: ChatCompletionRequest = serde_json::from_str(json).expect("deserialize"); match req.model {
523 ModelField::Single(m) => assert_eq!(m, "copilot:gpt-4o"),
524 ModelField::Multiple(_) => unreachable!("expected single"), }
526 assert!(!req.stream);
527 }
528
529 #[test]
530 fn deserialize_multiple_models() {
531 let json = r#"{"model":["copilot:gpt-4o","claude:opus"],"messages":[{"role":"user","content":"hi"}]}"#;
532 let req: ChatCompletionRequest = serde_json::from_str(json).expect("deserialize"); match req.model {
534 ModelField::Multiple(models) => {
535 assert_eq!(models.len(), 2);
536 assert_eq!(models[0], "copilot:gpt-4o");
537 assert_eq!(models[1], "claude:opus");
538 }
539 ModelField::Single(_) => unreachable!("expected multiple"), }
541 }
542
543 #[test]
544 fn deserialize_with_stream_flag() {
545 let json =
546 r#"{"model":"copilot","messages":[{"role":"user","content":"hi"}],"stream":true}"#;
547 let req: ChatCompletionRequest = serde_json::from_str(json).expect("deserialize"); assert!(req.stream);
549 }
550
551 #[test]
552 fn deserialize_message_with_null_content() {
553 let json = r#"{"model":"copilot","messages":[{"role":"assistant","content":null,"tool_calls":[{"id":"call_1","type":"function","function":{"name":"search","arguments":"{}"}}]}]}"#;
554 let req: ChatCompletionRequest = serde_json::from_str(json).expect("deserialize"); assert!(req.messages[0].content.is_none());
556 assert!(req.messages[0].tool_calls.is_some());
557 }
558
559 #[test]
560 fn deserialize_message_without_content_field() {
561 let json = r#"{"model":"copilot","messages":[{"role":"tool","tool_call_id":"call_1","name":"search","content":"{\"result\":\"found\"}"}]}"#;
562 let req: ChatCompletionRequest = serde_json::from_str(json).expect("deserialize"); assert_eq!(req.messages[0].role, "tool");
564 assert_eq!(req.messages[0].tool_call_id.as_deref(), Some("call_1"));
565 assert_eq!(req.messages[0].name.as_deref(), Some("search"));
566 }
567
568 #[test]
569 fn deserialize_multipart_content() {
570 let json = r#"{
571 "model": "copilot",
572 "messages": [{
573 "role": "user",
574 "content": [
575 {"type": "text", "text": "What is in this image?"},
576 {"type": "image_url", "image_url": {"url": "data:image/png;base64,aGVsbG8="}}
577 ]
578 }]
579 }"#;
580 let req: ChatCompletionRequest = serde_json::from_str(json).expect("deserialize"); let content = req.messages[0].content.as_ref().expect("content present"); match content {
583 MessageContent::Parts(parts) => {
584 assert_eq!(parts.len(), 2);
585 assert!(
586 matches!(&parts[0], ContentPart::Text { text } if text == "What is in this image?")
587 );
588 assert!(
589 matches!(&parts[1], ContentPart::ImageUrl { image_url } if image_url.url.contains("base64"))
590 );
591 }
592 MessageContent::Text(_) => unreachable!("expected Parts variant"), }
594 }
595
596 #[test]
597 fn message_content_as_text_plain_string() {
598 let content = MessageContent::Text("hello".to_owned());
599 assert_eq!(content.as_text(), "hello");
600 }
601
602 #[test]
603 fn message_content_as_text_multipart() {
604 let content = MessageContent::Parts(vec![
605 ContentPart::Text {
606 text: "describe ".to_owned(),
607 },
608 ContentPart::ImageUrl {
609 image_url: ImageUrlDetail {
610 url: "data:image/png;base64,abc".to_owned(),
611 },
612 },
613 ContentPart::Text {
614 text: "this image".to_owned(),
615 },
616 ]);
617 assert_eq!(content.as_text(), "describe this image");
618 }
619
620 #[test]
621 fn deserialize_plain_string_content_backward_compat() {
622 let json = r#"{"model":"copilot","messages":[{"role":"user","content":"hi"}]}"#;
623 let req: ChatCompletionRequest = serde_json::from_str(json).expect("deserialize"); let content = req.messages[0].content.as_ref().expect("content present"); match content {
626 MessageContent::Text(s) => assert_eq!(s, "hi"),
627 MessageContent::Parts(_) => unreachable!("expected Text variant"), }
629 }
630
631 #[test]
632 fn deserialize_tool_definitions() {
633 let json = r#"{
634 "model": "copilot",
635 "messages": [{"role": "user", "content": "hi"}],
636 "tools": [{
637 "type": "function",
638 "function": {
639 "name": "get_weather",
640 "description": "Get weather for a city",
641 "parameters": {"type": "object", "properties": {"city": {"type": "string"}}, "required": ["city"]}
642 }
643 }]
644 }"#;
645 let req: ChatCompletionRequest = serde_json::from_str(json).expect("deserialize"); let tools = req.tools.expect("tools present"); assert_eq!(tools.len(), 1);
648 assert_eq!(tools[0].tool_type, "function");
649 assert_eq!(tools[0].function.name, "get_weather");
650 assert!(tools[0].function.parameters.is_some());
651 }
652
653 #[test]
654 fn deserialize_tool_choice_auto() {
655 let json = r#"{"model":"copilot","messages":[{"role":"user","content":"hi"}],"tool_choice":"auto"}"#;
656 let req: ChatCompletionRequest = serde_json::from_str(json).expect("deserialize"); let tool_choice = req.tool_choice.expect("tool_choice present"); match tool_choice {
659 ToolChoice::Mode(m) => assert_eq!(m, "auto"),
660 ToolChoice::Specific(_) => unreachable!("expected mode"), }
662 }
663
664 #[test]
665 fn deserialize_tool_choice_specific() {
666 let json = r#"{"model":"copilot","messages":[{"role":"user","content":"hi"}],"tool_choice":{"type":"function","function":{"name":"get_weather"}}}"#;
667 let req: ChatCompletionRequest = serde_json::from_str(json).expect("deserialize"); let tool_choice = req.tool_choice.expect("tool_choice present"); match tool_choice {
670 ToolChoice::Specific(s) => assert_eq!(s.function.name, "get_weather"),
671 ToolChoice::Mode(_) => unreachable!("expected specific"), }
673 }
674
675 #[test]
676 fn serialize_completion_response() {
677 let resp = ChatCompletionResponse {
678 id: "chatcmpl-test".to_owned(),
679 object: "chat.completion",
680 created: 1_700_000_000,
681 model: "copilot:gpt-4o".to_owned(),
682 choices: vec![Choice {
683 index: 0,
684 message: ResponseMessage {
685 role: "assistant",
686 content: Some("Hello!".to_owned()),
687 tool_calls: None,
688 },
689 finish_reason: Some("stop".to_owned()),
690 }],
691 usage: None,
692 warnings: None,
693 };
694 let json = serde_json::to_string(&resp).expect("serialize"); assert!(json.contains("chat.completion"));
696 assert!(json.contains("Hello!"));
697 assert!(!json.contains("tool_calls"));
698 }
699
700 #[test]
701 fn serialize_response_with_tool_calls() {
702 let resp = ChatCompletionResponse {
703 id: "chatcmpl-test".to_owned(),
704 object: "chat.completion",
705 created: 1_700_000_000,
706 model: "copilot:gpt-4o".to_owned(),
707 choices: vec![Choice {
708 index: 0,
709 message: ResponseMessage {
710 role: "assistant",
711 content: None,
712 tool_calls: Some(vec![ToolCall {
713 index: 0,
714 id: "call_abc123".to_owned(),
715 tool_type: "function".to_owned(),
716 function: ToolCallFunction {
717 name: "get_weather".to_owned(),
718 arguments: r#"{"city":"Paris"}"#.to_owned(),
719 },
720 }]),
721 },
722 finish_reason: Some("tool_calls".to_owned()),
723 }],
724 usage: None,
725 warnings: None,
726 };
727 let json = serde_json::to_string(&resp).expect("serialize"); assert!(json.contains("tool_calls"));
729 assert!(json.contains("call_abc123"));
730 assert!(json.contains("get_weather"));
731 assert!(!json.contains(r#""content""#));
732 }
733
734 #[test]
735 fn serialize_error_response() {
736 let resp = ErrorResponse::new("invalid_request_error", "Unknown model");
737 let json = serde_json::to_string(&resp).expect("serialize"); assert!(json.contains("invalid_request_error"));
739 assert!(json.contains("Unknown model"));
740 }
741
742 #[test]
743 fn serialize_chunk_response() {
744 let chunk = ChatCompletionChunk {
745 id: "chatcmpl-test".to_owned(),
746 object: "chat.completion.chunk",
747 created: 1_700_000_000,
748 model: "copilot".to_owned(),
749 choices: vec![ChunkChoice {
750 index: 0,
751 delta: Delta {
752 role: None,
753 content: Some("token".to_owned()),
754 tool_calls: None,
755 },
756 finish_reason: None,
757 }],
758 };
759 let json = serde_json::to_string(&chunk).expect("serialize"); assert!(json.contains("chat.completion.chunk"));
761 assert!(json.contains("token"));
762 assert!(!json.contains("tool_calls"));
763 }
764
765 #[test]
766 fn deserialize_tool_choice_none() {
767 let json = r#"{"model":"copilot","messages":[{"role":"user","content":"hi"}],"tool_choice":"none"}"#;
768 let req: ChatCompletionRequest = serde_json::from_str(json).expect("deserialize"); let tool_choice = req.tool_choice.expect("tool_choice present"); match tool_choice {
771 ToolChoice::Mode(m) => assert_eq!(m, "none"),
772 ToolChoice::Specific(_) => unreachable!("expected mode"), }
774 }
775
776 #[test]
777 fn deserialize_tool_choice_required() {
778 let json = r#"{"model":"copilot","messages":[{"role":"user","content":"hi"}],"tool_choice":"required"}"#;
779 let req: ChatCompletionRequest = serde_json::from_str(json).expect("deserialize"); let tool_choice = req.tool_choice.expect("tool_choice present"); match tool_choice {
782 ToolChoice::Mode(m) => assert_eq!(m, "required"),
783 ToolChoice::Specific(_) => unreachable!("expected mode"), }
785 }
786
787 #[test]
788 fn deserialize_response_format_text() {
789 let json = r#"{"model":"copilot","messages":[{"role":"user","content":"hi"}],"response_format":{"type":"text"}}"#;
790 let req: ChatCompletionRequest = serde_json::from_str(json).expect("deserialize"); assert!(matches!(
792 req.response_format,
793 Some(ResponseFormatRequest::Text)
794 ));
795 }
796
797 #[test]
798 fn deserialize_response_format_json_object() {
799 let json = r#"{"model":"copilot","messages":[{"role":"user","content":"hi"}],"response_format":{"type":"json_object"}}"#;
800 let req: ChatCompletionRequest = serde_json::from_str(json).expect("deserialize"); assert!(matches!(
802 req.response_format,
803 Some(ResponseFormatRequest::JsonObject)
804 ));
805 }
806
807 #[test]
808 fn deserialize_response_format_json_schema() {
809 let json = r#"{
810 "model": "copilot",
811 "messages": [{"role": "user", "content": "hi"}],
812 "response_format": {
813 "type": "json_schema",
814 "json_schema": {
815 "name": "weather",
816 "schema": {"type": "object", "properties": {"temp": {"type": "number"}}}
817 }
818 }
819 }"#;
820 let req: ChatCompletionRequest = serde_json::from_str(json).expect("deserialize"); match req.response_format {
822 Some(ResponseFormatRequest::JsonSchema { json_schema }) => {
823 assert_eq!(json_schema.name, "weather");
824 assert!(json_schema.schema["properties"]["temp"].is_object());
825 }
826 other => unreachable!("expected JsonSchema, got: {other:?}"), }
828 }
829
830 #[test]
831 fn deserialize_top_p() {
832 let json = r#"{"model":"copilot","messages":[{"role":"user","content":"hi"}],"top_p":0.9}"#;
833 let req: ChatCompletionRequest = serde_json::from_str(json).expect("deserialize"); assert_eq!(req.top_p, Some(0.9));
835 }
836
837 #[test]
838 fn deserialize_stop_single() {
839 let json =
840 r#"{"model":"copilot","messages":[{"role":"user","content":"hi"}],"stop":"END"}"#;
841 let req: ChatCompletionRequest = serde_json::from_str(json).expect("deserialize"); let stop = req.stop.expect("stop present"); assert_eq!(stop.into_vec(), vec!["END"]);
844 }
845
846 #[test]
847 fn deserialize_stop_array() {
848 let json = r#"{"model":"copilot","messages":[{"role":"user","content":"hi"}],"stop":["END","STOP"]}"#;
849 let req: ChatCompletionRequest = serde_json::from_str(json).expect("deserialize"); let stop = req.stop.expect("stop present"); assert_eq!(stop.into_vec(), vec!["END", "STOP"]);
852 }
853
854 #[test]
855 fn stop_field_len() {
856 let single = StopField::Single("END".to_owned());
857 assert_eq!(single.len(), 1);
858 let multiple = StopField::Multiple(vec!["A".to_owned(), "B".to_owned(), "C".to_owned()]);
859 assert_eq!(multiple.len(), 3);
860 }
861
862 #[test]
863 fn stop_field_into_vec_truncates_at_four() {
864 let oversized = StopField::Multiple((0..10).map(|i| format!("stop_{i}")).collect());
865 let result = oversized.into_vec();
866 assert_eq!(result.len(), 4);
867 assert_eq!(result[0], "stop_0");
868 assert_eq!(result[3], "stop_3");
869 }
870
871 #[test]
872 fn deserialize_all_optional_fields() {
873 let json = r#"{
874 "model": "copilot",
875 "messages": [{"role": "user", "content": "hi"}],
876 "temperature": 0.7,
877 "max_tokens": 100,
878 "top_p": 0.95,
879 "stop": ["END"],
880 "stream": true
881 }"#;
882 let req: ChatCompletionRequest = serde_json::from_str(json).expect("deserialize"); assert_eq!(req.temperature, Some(0.7));
884 assert_eq!(req.max_tokens, Some(100));
885 assert_eq!(req.top_p, Some(0.95));
886 assert!(req.stop.is_some());
887 assert!(req.stream);
888 }
889
890 #[test]
891 fn serialize_models_response() {
892 let resp = ModelsResponse {
893 object: "list",
894 data: vec![ModelObject {
895 id: "copilot:gpt-4o".to_owned(),
896 object: "model",
897 owned_by: "copilot".to_owned(),
898 }],
899 };
900 let json = serde_json::to_string(&resp).expect("serialize"); assert!(json.contains("copilot:gpt-4o"));
902 }
903}