1use serde::{Deserialize, Serialize};
8
9#[derive(Debug, Deserialize)]
19pub struct ChatCompletionRequest {
20 pub model: ModelField,
22 pub messages: Vec<ChatCompletionMessage>,
24 #[serde(default)]
26 pub stream: bool,
27 #[serde(default)]
29 pub temperature: Option<f32>,
30 #[serde(default)]
32 pub max_tokens: Option<u32>,
33 #[serde(default)]
35 pub strict_capabilities: Option<bool>,
36 #[serde(default)]
38 pub tools: Option<Vec<ToolDefinition>>,
39 #[serde(default)]
41 pub tool_choice: Option<ToolChoice>,
42 #[serde(default)]
44 pub response_format: Option<ResponseFormatRequest>,
45 #[serde(default)]
47 pub top_p: Option<f32>,
48 #[serde(default)]
50 pub stop: Option<StopField>,
51}
52
53#[derive(Debug, Clone, Deserialize)]
58#[serde(untagged)]
59pub enum StopField {
60 Single(String),
62 Multiple(Vec<String>),
64}
65
66const MAX_STOP_SEQUENCES: usize = 4;
68
69impl StopField {
70 pub const fn len(&self) -> usize {
72 match self {
73 Self::Single(_) => 1,
74 Self::Multiple(v) => v.len(),
75 }
76 }
77
78 pub const fn is_empty(&self) -> bool {
80 matches!(self, Self::Multiple(v) if v.is_empty())
81 }
82
83 pub fn into_vec(self) -> Vec<String> {
86 match self {
87 Self::Single(s) => vec![s],
88 Self::Multiple(v) => v.into_iter().take(MAX_STOP_SEQUENCES).collect(),
89 }
90 }
91
92 pub fn to_bounded_vec(&self) -> Vec<String> {
95 match self {
96 Self::Single(s) => vec![s.clone()],
97 Self::Multiple(v) => v.iter().take(MAX_STOP_SEQUENCES).cloned().collect(),
98 }
99 }
100}
101
102#[derive(Debug, Clone, Deserialize)]
104#[serde(tag = "type")]
105pub enum ResponseFormatRequest {
106 #[serde(rename = "text")]
108 Text,
109 #[serde(rename = "json_object")]
111 JsonObject,
112 #[serde(rename = "json_schema")]
114 JsonSchema {
115 json_schema: JsonSchemaSpec,
117 },
118}
119
120#[derive(Debug, Clone, Deserialize)]
122pub struct JsonSchemaSpec {
123 pub name: String,
125 pub schema: serde_json::Value,
127}
128
129#[derive(Debug, Clone, Deserialize)]
131#[serde(untagged)]
132pub enum ModelField {
133 Single(String),
135 Multiple(Vec<String>),
137}
138
139#[derive(Debug, Clone, Deserialize)]
144#[serde(untagged)]
145pub enum MessageContent {
146 Text(String),
148 Parts(Vec<ContentPart>),
150}
151
152impl MessageContent {
153 pub fn as_text(&self) -> String {
155 match self {
156 Self::Text(s) => s.clone(),
157 Self::Parts(parts) => parts
158 .iter()
159 .filter_map(|p| match p {
160 ContentPart::Text { text } => Some(text.as_str()),
161 ContentPart::ImageUrl { .. } => None,
162 })
163 .collect::<Vec<_>>()
164 .join(""),
165 }
166 }
167}
168
169#[derive(Debug, Clone, Deserialize)]
171#[serde(tag = "type")]
172pub enum ContentPart {
173 #[serde(rename = "text")]
175 Text {
176 text: String,
178 },
179 #[serde(rename = "image_url")]
181 ImageUrl {
182 image_url: ImageUrlDetail,
184 },
185}
186
187#[derive(Debug, Clone, Deserialize)]
189pub struct ImageUrlDetail {
190 pub url: String,
192}
193
194#[derive(Debug, Clone, Deserialize)]
196pub struct ChatCompletionMessage {
197 pub role: String,
199 pub content: Option<MessageContent>,
201 #[serde(default)]
203 pub tool_calls: Option<Vec<ToolCall>>,
204 #[serde(default)]
206 pub tool_call_id: Option<String>,
207 #[serde(default)]
209 pub name: Option<String>,
210}
211
212#[derive(Debug, Clone, Deserialize)]
218pub struct ToolDefinition {
219 #[serde(rename = "type")]
221 pub tool_type: String,
222 pub function: FunctionObject,
224}
225
226#[derive(Debug, Clone, Deserialize)]
228pub struct FunctionObject {
229 pub name: String,
231 #[serde(default)]
233 pub description: Option<String>,
234 #[serde(default)]
236 pub parameters: Option<serde_json::Value>,
237}
238
239#[derive(Debug, Clone, Deserialize)]
241#[serde(untagged)]
242pub enum ToolChoice {
243 Mode(String),
245 Specific(ToolChoiceSpecific),
247}
248
249#[derive(Debug, Clone, Deserialize)]
251pub struct ToolChoiceSpecific {
252 #[serde(rename = "type")]
254 pub tool_type: String,
255 pub function: ToolChoiceFunction,
257}
258
259#[derive(Debug, Clone, Deserialize)]
261pub struct ToolChoiceFunction {
262 pub name: String,
264}
265
266#[derive(Debug, Clone, Serialize, Deserialize)]
268pub struct ToolCall {
269 #[serde(default)]
271 pub index: usize,
272 pub id: String,
274 #[serde(rename = "type")]
276 pub tool_type: String,
277 pub function: ToolCallFunction,
279}
280
281#[derive(Debug, Clone, Serialize, Deserialize)]
283pub struct ToolCallFunction {
284 pub name: String,
286 pub arguments: String,
288}
289
290#[derive(Debug, Serialize)]
296pub struct ChatCompletionResponse {
297 pub id: String,
299 pub object: &'static str,
301 pub created: u64,
303 pub model: String,
305 pub choices: Vec<Choice>,
307 #[serde(skip_serializing_if = "Option::is_none")]
309 pub usage: Option<Usage>,
310 #[serde(skip_serializing_if = "Option::is_none")]
312 pub warnings: Option<Vec<String>>,
313}
314
315#[derive(Debug, Serialize)]
317pub struct Choice {
318 pub index: u32,
320 pub message: ResponseMessage,
322 pub finish_reason: Option<String>,
324}
325
326#[derive(Debug, Serialize)]
328pub struct ResponseMessage {
329 pub role: &'static str,
331 #[serde(skip_serializing_if = "Option::is_none")]
333 pub content: Option<String>,
334 #[serde(skip_serializing_if = "Option::is_none")]
336 pub tool_calls: Option<Vec<ToolCall>>,
337}
338
339#[derive(Debug, Serialize)]
341pub struct Usage {
342 #[serde(rename = "prompt_tokens")]
344 pub prompt: u32,
345 #[serde(rename = "completion_tokens")]
347 pub completion: u32,
348 #[serde(rename = "total_tokens")]
350 pub total: u32,
351}
352
353#[derive(Debug, Serialize)]
359pub struct ChatCompletionChunk {
360 pub id: String,
362 pub object: &'static str,
364 pub created: u64,
366 pub model: String,
368 pub choices: Vec<ChunkChoice>,
370}
371
372#[derive(Debug, Serialize)]
374pub struct ChunkChoice {
375 pub index: u32,
377 pub delta: Delta,
379 pub finish_reason: Option<String>,
381}
382
383#[derive(Debug, Serialize)]
385pub struct Delta {
386 #[serde(skip_serializing_if = "Option::is_none")]
388 pub role: Option<&'static str>,
389 #[serde(skip_serializing_if = "Option::is_none")]
391 pub content: Option<String>,
392 #[serde(skip_serializing_if = "Option::is_none")]
394 pub tool_calls: Option<Vec<ToolCall>>,
395}
396
397#[derive(Debug, Serialize)]
403pub struct MultiplexResponse {
404 pub id: String,
406 pub object: &'static str,
408 pub created: u64,
410 pub results: Vec<MultiplexProviderResult>,
412 pub summary: String,
414}
415
416#[derive(Debug, Serialize)]
418pub struct MultiplexProviderResult {
419 pub provider: String,
421 #[serde(skip_serializing_if = "Option::is_none")]
423 pub model: Option<String>,
424 #[serde(skip_serializing_if = "Option::is_none")]
426 pub content: Option<String>,
427 #[serde(skip_serializing_if = "Option::is_none")]
429 pub error: Option<String>,
430 pub duration_ms: u64,
432}
433
434#[derive(Debug, Serialize)]
440pub struct ModelsResponse {
441 pub object: &'static str,
443 pub data: Vec<ModelObject>,
445}
446
447#[derive(Debug, Serialize)]
449pub struct ModelObject {
450 pub id: String,
452 pub object: &'static str,
454 pub owned_by: String,
456}
457
458#[derive(Debug, Serialize)]
464pub struct HealthResponse {
465 pub status: &'static str,
467 pub providers: std::collections::HashMap<String, String>,
469}
470
471#[derive(Debug, Serialize)]
477pub struct ErrorResponse {
478 pub error: ErrorDetail,
480}
481
482#[derive(Debug, Serialize)]
484pub struct ErrorDetail {
485 pub message: String,
487 #[serde(rename = "type")]
489 pub error_type: String,
490 #[serde(skip_serializing_if = "Option::is_none")]
492 pub param: Option<String>,
493 #[serde(skip_serializing_if = "Option::is_none")]
495 pub code: Option<String>,
496}
497
498impl ErrorResponse {
499 pub fn new(error_type: impl Into<String>, message: impl Into<String>) -> Self {
501 Self {
502 error: ErrorDetail {
503 message: message.into(),
504 error_type: error_type.into(),
505 param: None,
506 code: None,
507 },
508 }
509 }
510}
511
512#[cfg(test)]
513mod tests {
514 use super::*;
515
516 #[test]
517 fn deserialize_single_model() {
518 let json = r#"{"model":"copilot:gpt-4o","messages":[{"role":"user","content":"hi"}]}"#;
519 let req: ChatCompletionRequest = serde_json::from_str(json).expect("deserialize");
520 match req.model {
521 ModelField::Single(m) => assert_eq!(m, "copilot:gpt-4o"),
522 ModelField::Multiple(_) => panic!("expected single"),
523 }
524 assert!(!req.stream);
525 }
526
527 #[test]
528 fn deserialize_multiple_models() {
529 let json = r#"{"model":["copilot:gpt-4o","claude:opus"],"messages":[{"role":"user","content":"hi"}]}"#;
530 let req: ChatCompletionRequest = serde_json::from_str(json).expect("deserialize");
531 match req.model {
532 ModelField::Multiple(models) => {
533 assert_eq!(models.len(), 2);
534 assert_eq!(models[0], "copilot:gpt-4o");
535 assert_eq!(models[1], "claude:opus");
536 }
537 ModelField::Single(_) => panic!("expected multiple"),
538 }
539 }
540
541 #[test]
542 fn deserialize_with_stream_flag() {
543 let json =
544 r#"{"model":"copilot","messages":[{"role":"user","content":"hi"}],"stream":true}"#;
545 let req: ChatCompletionRequest = serde_json::from_str(json).expect("deserialize");
546 assert!(req.stream);
547 }
548
549 #[test]
550 fn deserialize_message_with_null_content() {
551 let json = r#"{"model":"copilot","messages":[{"role":"assistant","content":null,"tool_calls":[{"id":"call_1","type":"function","function":{"name":"search","arguments":"{}"}}]}]}"#;
552 let req: ChatCompletionRequest = serde_json::from_str(json).expect("deserialize");
553 assert!(req.messages[0].content.is_none());
554 assert!(req.messages[0].tool_calls.is_some());
555 }
556
557 #[test]
558 fn deserialize_message_without_content_field() {
559 let json = r#"{"model":"copilot","messages":[{"role":"tool","tool_call_id":"call_1","name":"search","content":"{\"result\":\"found\"}"}]}"#;
560 let req: ChatCompletionRequest = serde_json::from_str(json).expect("deserialize");
561 assert_eq!(req.messages[0].role, "tool");
562 assert_eq!(req.messages[0].tool_call_id.as_deref(), Some("call_1"));
563 assert_eq!(req.messages[0].name.as_deref(), Some("search"));
564 }
565
566 #[test]
567 fn deserialize_multipart_content() {
568 let json = r#"{
569 "model": "copilot",
570 "messages": [{
571 "role": "user",
572 "content": [
573 {"type": "text", "text": "What is in this image?"},
574 {"type": "image_url", "image_url": {"url": "data:image/png;base64,aGVsbG8="}}
575 ]
576 }]
577 }"#;
578 let req: ChatCompletionRequest = serde_json::from_str(json).expect("deserialize");
579 let content = req.messages[0].content.as_ref().expect("content present");
580 match content {
581 MessageContent::Parts(parts) => {
582 assert_eq!(parts.len(), 2);
583 assert!(
584 matches!(&parts[0], ContentPart::Text { text } if text == "What is in this image?")
585 );
586 assert!(
587 matches!(&parts[1], ContentPart::ImageUrl { image_url } if image_url.url.contains("base64"))
588 );
589 }
590 MessageContent::Text(_) => panic!("expected Parts variant"),
591 }
592 }
593
594 #[test]
595 fn message_content_as_text_plain_string() {
596 let content = MessageContent::Text("hello".to_owned());
597 assert_eq!(content.as_text(), "hello");
598 }
599
600 #[test]
601 fn message_content_as_text_multipart() {
602 let content = MessageContent::Parts(vec![
603 ContentPart::Text {
604 text: "describe ".to_owned(),
605 },
606 ContentPart::ImageUrl {
607 image_url: ImageUrlDetail {
608 url: "data:image/png;base64,abc".to_owned(),
609 },
610 },
611 ContentPart::Text {
612 text: "this image".to_owned(),
613 },
614 ]);
615 assert_eq!(content.as_text(), "describe this image");
616 }
617
618 #[test]
619 fn deserialize_plain_string_content_backward_compat() {
620 let json = r#"{"model":"copilot","messages":[{"role":"user","content":"hi"}]}"#;
621 let req: ChatCompletionRequest = serde_json::from_str(json).expect("deserialize");
622 match req.messages[0].content.as_ref().expect("content present") {
623 MessageContent::Text(s) => assert_eq!(s, "hi"),
624 MessageContent::Parts(_) => panic!("expected Text variant"),
625 }
626 }
627
628 #[test]
629 fn deserialize_tool_definitions() {
630 let json = r#"{
631 "model": "copilot",
632 "messages": [{"role": "user", "content": "hi"}],
633 "tools": [{
634 "type": "function",
635 "function": {
636 "name": "get_weather",
637 "description": "Get weather for a city",
638 "parameters": {"type": "object", "properties": {"city": {"type": "string"}}, "required": ["city"]}
639 }
640 }]
641 }"#;
642 let req: ChatCompletionRequest = serde_json::from_str(json).expect("deserialize");
643 let tools = req.tools.expect("tools present");
644 assert_eq!(tools.len(), 1);
645 assert_eq!(tools[0].tool_type, "function");
646 assert_eq!(tools[0].function.name, "get_weather");
647 assert!(tools[0].function.parameters.is_some());
648 }
649
650 #[test]
651 fn deserialize_tool_choice_auto() {
652 let json = r#"{"model":"copilot","messages":[{"role":"user","content":"hi"}],"tool_choice":"auto"}"#;
653 let req: ChatCompletionRequest = serde_json::from_str(json).expect("deserialize");
654 match req.tool_choice.expect("tool_choice present") {
655 ToolChoice::Mode(m) => assert_eq!(m, "auto"),
656 ToolChoice::Specific(_) => panic!("expected mode"),
657 }
658 }
659
660 #[test]
661 fn deserialize_tool_choice_specific() {
662 let json = r#"{"model":"copilot","messages":[{"role":"user","content":"hi"}],"tool_choice":{"type":"function","function":{"name":"get_weather"}}}"#;
663 let req: ChatCompletionRequest = serde_json::from_str(json).expect("deserialize");
664 match req.tool_choice.expect("tool_choice present") {
665 ToolChoice::Specific(s) => assert_eq!(s.function.name, "get_weather"),
666 ToolChoice::Mode(_) => panic!("expected specific"),
667 }
668 }
669
670 #[test]
671 fn serialize_completion_response() {
672 let resp = ChatCompletionResponse {
673 id: "chatcmpl-test".to_owned(),
674 object: "chat.completion",
675 created: 1_700_000_000,
676 model: "copilot:gpt-4o".to_owned(),
677 choices: vec![Choice {
678 index: 0,
679 message: ResponseMessage {
680 role: "assistant",
681 content: Some("Hello!".to_owned()),
682 tool_calls: None,
683 },
684 finish_reason: Some("stop".to_owned()),
685 }],
686 usage: None,
687 warnings: None,
688 };
689 let json = serde_json::to_string(&resp).expect("serialize");
690 assert!(json.contains("chat.completion"));
691 assert!(json.contains("Hello!"));
692 assert!(!json.contains("tool_calls"));
693 }
694
695 #[test]
696 fn serialize_response_with_tool_calls() {
697 let resp = ChatCompletionResponse {
698 id: "chatcmpl-test".to_owned(),
699 object: "chat.completion",
700 created: 1_700_000_000,
701 model: "copilot:gpt-4o".to_owned(),
702 choices: vec![Choice {
703 index: 0,
704 message: ResponseMessage {
705 role: "assistant",
706 content: None,
707 tool_calls: Some(vec![ToolCall {
708 index: 0,
709 id: "call_abc123".to_owned(),
710 tool_type: "function".to_owned(),
711 function: ToolCallFunction {
712 name: "get_weather".to_owned(),
713 arguments: r#"{"city":"Paris"}"#.to_owned(),
714 },
715 }]),
716 },
717 finish_reason: Some("tool_calls".to_owned()),
718 }],
719 usage: None,
720 warnings: None,
721 };
722 let json = serde_json::to_string(&resp).expect("serialize");
723 assert!(json.contains("tool_calls"));
724 assert!(json.contains("call_abc123"));
725 assert!(json.contains("get_weather"));
726 assert!(!json.contains(r#""content""#));
727 }
728
729 #[test]
730 fn serialize_error_response() {
731 let resp = ErrorResponse::new("invalid_request_error", "Unknown model");
732 let json = serde_json::to_string(&resp).expect("serialize");
733 assert!(json.contains("invalid_request_error"));
734 assert!(json.contains("Unknown model"));
735 }
736
737 #[test]
738 fn serialize_chunk_response() {
739 let chunk = ChatCompletionChunk {
740 id: "chatcmpl-test".to_owned(),
741 object: "chat.completion.chunk",
742 created: 1_700_000_000,
743 model: "copilot".to_owned(),
744 choices: vec![ChunkChoice {
745 index: 0,
746 delta: Delta {
747 role: None,
748 content: Some("token".to_owned()),
749 tool_calls: None,
750 },
751 finish_reason: None,
752 }],
753 };
754 let json = serde_json::to_string(&chunk).expect("serialize");
755 assert!(json.contains("chat.completion.chunk"));
756 assert!(json.contains("token"));
757 assert!(!json.contains("tool_calls"));
758 }
759
760 #[test]
761 fn deserialize_tool_choice_none() {
762 let json = r#"{"model":"copilot","messages":[{"role":"user","content":"hi"}],"tool_choice":"none"}"#;
763 let req: ChatCompletionRequest = serde_json::from_str(json).expect("deserialize");
764 match req.tool_choice.expect("tool_choice present") {
765 ToolChoice::Mode(m) => assert_eq!(m, "none"),
766 ToolChoice::Specific(_) => panic!("expected mode"),
767 }
768 }
769
770 #[test]
771 fn deserialize_tool_choice_required() {
772 let json = r#"{"model":"copilot","messages":[{"role":"user","content":"hi"}],"tool_choice":"required"}"#;
773 let req: ChatCompletionRequest = serde_json::from_str(json).expect("deserialize");
774 match req.tool_choice.expect("tool_choice present") {
775 ToolChoice::Mode(m) => assert_eq!(m, "required"),
776 ToolChoice::Specific(_) => panic!("expected mode"),
777 }
778 }
779
780 #[test]
781 fn deserialize_response_format_text() {
782 let json = r#"{"model":"copilot","messages":[{"role":"user","content":"hi"}],"response_format":{"type":"text"}}"#;
783 let req: ChatCompletionRequest = serde_json::from_str(json).expect("deserialize");
784 assert!(matches!(
785 req.response_format,
786 Some(ResponseFormatRequest::Text)
787 ));
788 }
789
790 #[test]
791 fn deserialize_response_format_json_object() {
792 let json = r#"{"model":"copilot","messages":[{"role":"user","content":"hi"}],"response_format":{"type":"json_object"}}"#;
793 let req: ChatCompletionRequest = serde_json::from_str(json).expect("deserialize");
794 assert!(matches!(
795 req.response_format,
796 Some(ResponseFormatRequest::JsonObject)
797 ));
798 }
799
800 #[test]
801 fn deserialize_response_format_json_schema() {
802 let json = r#"{
803 "model": "copilot",
804 "messages": [{"role": "user", "content": "hi"}],
805 "response_format": {
806 "type": "json_schema",
807 "json_schema": {
808 "name": "weather",
809 "schema": {"type": "object", "properties": {"temp": {"type": "number"}}}
810 }
811 }
812 }"#;
813 let req: ChatCompletionRequest = serde_json::from_str(json).expect("deserialize");
814 match req.response_format {
815 Some(ResponseFormatRequest::JsonSchema { json_schema }) => {
816 assert_eq!(json_schema.name, "weather");
817 assert!(json_schema.schema["properties"]["temp"].is_object());
818 }
819 other => panic!("expected JsonSchema, got: {other:?}"),
820 }
821 }
822
823 #[test]
824 fn deserialize_top_p() {
825 let json = r#"{"model":"copilot","messages":[{"role":"user","content":"hi"}],"top_p":0.9}"#;
826 let req: ChatCompletionRequest = serde_json::from_str(json).expect("deserialize");
827 assert_eq!(req.top_p, Some(0.9));
828 }
829
830 #[test]
831 fn deserialize_stop_single() {
832 let json =
833 r#"{"model":"copilot","messages":[{"role":"user","content":"hi"}],"stop":"END"}"#;
834 let req: ChatCompletionRequest = serde_json::from_str(json).expect("deserialize");
835 let stop = req.stop.expect("stop present");
836 assert_eq!(stop.into_vec(), vec!["END"]);
837 }
838
839 #[test]
840 fn deserialize_stop_array() {
841 let json = r#"{"model":"copilot","messages":[{"role":"user","content":"hi"}],"stop":["END","STOP"]}"#;
842 let req: ChatCompletionRequest = serde_json::from_str(json).expect("deserialize");
843 let stop = req.stop.expect("stop present");
844 assert_eq!(stop.into_vec(), vec!["END", "STOP"]);
845 }
846
847 #[test]
848 fn stop_field_len() {
849 let single = StopField::Single("END".to_owned());
850 assert_eq!(single.len(), 1);
851 let multiple = StopField::Multiple(vec!["A".to_owned(), "B".to_owned(), "C".to_owned()]);
852 assert_eq!(multiple.len(), 3);
853 }
854
855 #[test]
856 fn stop_field_into_vec_truncates_at_four() {
857 let oversized = StopField::Multiple((0..10).map(|i| format!("stop_{i}")).collect());
858 let result = oversized.into_vec();
859 assert_eq!(result.len(), 4);
860 assert_eq!(result[0], "stop_0");
861 assert_eq!(result[3], "stop_3");
862 }
863
864 #[test]
865 fn deserialize_all_optional_fields() {
866 let json = r#"{
867 "model": "copilot",
868 "messages": [{"role": "user", "content": "hi"}],
869 "temperature": 0.7,
870 "max_tokens": 100,
871 "top_p": 0.95,
872 "stop": ["END"],
873 "stream": true
874 }"#;
875 let req: ChatCompletionRequest = serde_json::from_str(json).expect("deserialize");
876 assert_eq!(req.temperature, Some(0.7));
877 assert_eq!(req.max_tokens, Some(100));
878 assert_eq!(req.top_p, Some(0.95));
879 assert!(req.stop.is_some());
880 assert!(req.stream);
881 }
882
883 #[test]
884 fn serialize_models_response() {
885 let resp = ModelsResponse {
886 object: "list",
887 data: vec![ModelObject {
888 id: "copilot:gpt-4o".to_owned(),
889 object: "model",
890 owned_by: "copilot".to_owned(),
891 }],
892 };
893 let json = serde_json::to_string(&resp).expect("serialize");
894 assert!(json.contains("copilot:gpt-4o"));
895 }
896}