1use std::collections::HashMap;
8
9use embacle::types::TokenUsage;
10use serde::{Deserialize, Serialize};
11
12#[derive(Debug, Deserialize)]
22pub struct ChatCompletionRequest {
23 pub model: ModelField,
25 pub messages: Vec<ChatCompletionMessage>,
27 #[serde(default)]
29 pub stream: bool,
30 #[serde(default)]
32 pub temperature: Option<f32>,
33 #[serde(default)]
35 pub max_tokens: Option<u32>,
36 #[serde(default)]
38 pub strict_capabilities: Option<bool>,
39 #[serde(default)]
41 pub tools: Option<Vec<ToolDefinition>>,
42 #[serde(default)]
44 pub tool_choice: Option<ToolChoice>,
45 #[serde(default)]
51 pub tool_execution: Option<ToolExecutionMode>,
52 #[serde(default)]
54 pub response_format: Option<ResponseFormatRequest>,
55 #[serde(default)]
57 pub top_p: Option<f32>,
58 #[serde(default)]
60 pub stop: Option<StopField>,
61}
62
63#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)]
65#[serde(rename_all = "lowercase")]
66pub enum ToolExecutionMode {
67 Client,
69 Server,
71}
72
73#[derive(Debug, Clone, Deserialize)]
78#[serde(untagged)]
79pub enum StopField {
80 Single(String),
82 Multiple(Vec<String>),
84}
85
86const MAX_STOP_SEQUENCES: usize = 4;
88
89impl StopField {
90 pub const fn len(&self) -> usize {
92 match self {
93 Self::Single(_) => 1,
94 Self::Multiple(v) => v.len(),
95 }
96 }
97
98 pub const fn is_empty(&self) -> bool {
100 matches!(self, Self::Multiple(v) if v.is_empty())
101 }
102
103 pub fn into_vec(self) -> Vec<String> {
106 match self {
107 Self::Single(s) => vec![s],
108 Self::Multiple(v) => v.into_iter().take(MAX_STOP_SEQUENCES).collect(),
109 }
110 }
111
112 pub fn to_bounded_vec(&self) -> Vec<String> {
115 match self {
116 Self::Single(s) => vec![s.clone()],
117 Self::Multiple(v) => v.iter().take(MAX_STOP_SEQUENCES).cloned().collect(),
118 }
119 }
120}
121
122#[derive(Debug, Clone, Deserialize)]
124#[serde(tag = "type")]
125pub enum ResponseFormatRequest {
126 #[serde(rename = "text")]
128 Text,
129 #[serde(rename = "json_object")]
131 JsonObject,
132 #[serde(rename = "json_schema")]
134 JsonSchema {
135 json_schema: JsonSchemaSpec,
137 },
138}
139
140#[derive(Debug, Clone, Deserialize)]
142pub struct JsonSchemaSpec {
143 pub name: String,
145 pub schema: serde_json::Value,
147}
148
149#[derive(Debug, Clone, Deserialize)]
151#[serde(untagged)]
152pub enum ModelField {
153 Single(String),
155 Multiple(Vec<String>),
157}
158
159#[derive(Debug, Clone, Deserialize)]
164#[serde(untagged)]
165pub enum MessageContent {
166 Text(String),
168 Parts(Vec<ContentPart>),
170}
171
172impl MessageContent {
173 pub fn as_text(&self) -> String {
175 match self {
176 Self::Text(s) => s.clone(),
177 Self::Parts(parts) => parts
178 .iter()
179 .filter_map(|p| match p {
180 ContentPart::Text { text } => Some(text.as_str()),
181 ContentPart::ImageUrl { .. } => None,
182 })
183 .collect::<Vec<_>>()
184 .join(""),
185 }
186 }
187}
188
189#[derive(Debug, Clone, Deserialize)]
191#[serde(tag = "type")]
192pub enum ContentPart {
193 #[serde(rename = "text")]
195 Text {
196 text: String,
198 },
199 #[serde(rename = "image_url")]
201 ImageUrl {
202 image_url: ImageUrlDetail,
204 },
205}
206
207#[derive(Debug, Clone, Deserialize)]
209pub struct ImageUrlDetail {
210 pub url: String,
212}
213
214#[derive(Debug, Clone, Deserialize)]
216pub struct ChatCompletionMessage {
217 pub role: String,
219 pub content: Option<MessageContent>,
221 #[serde(default)]
223 pub tool_calls: Option<Vec<ToolCall>>,
224 #[serde(default)]
226 pub tool_call_id: Option<String>,
227 #[serde(default)]
229 pub name: Option<String>,
230}
231
232#[derive(Debug, Clone, Deserialize)]
238pub struct ToolDefinition {
239 #[serde(rename = "type")]
241 pub tool_type: String,
242 pub function: FunctionObject,
244}
245
246#[derive(Debug, Clone, Deserialize)]
248pub struct FunctionObject {
249 pub name: String,
251 #[serde(default)]
253 pub description: Option<String>,
254 #[serde(default)]
256 pub parameters: Option<serde_json::Value>,
257}
258
259#[derive(Debug, Clone, Deserialize)]
261#[serde(untagged)]
262pub enum ToolChoice {
263 Mode(String),
265 Specific(ToolChoiceSpecific),
267}
268
269#[derive(Debug, Clone, Deserialize)]
271pub struct ToolChoiceSpecific {
272 #[serde(rename = "type")]
274 pub tool_type: String,
275 pub function: ToolChoiceFunction,
277}
278
279#[derive(Debug, Clone, Deserialize)]
281pub struct ToolChoiceFunction {
282 pub name: String,
284}
285
286#[derive(Debug, Clone, Serialize, Deserialize)]
288pub struct ToolCall {
289 #[serde(default)]
291 pub index: usize,
292 pub id: String,
294 #[serde(rename = "type")]
296 pub tool_type: String,
297 pub function: ToolCallFunction,
299}
300
301#[derive(Debug, Clone, Serialize, Deserialize)]
303pub struct ToolCallFunction {
304 pub name: String,
306 pub arguments: String,
308}
309
310#[derive(Debug, Serialize)]
316pub struct ChatCompletionResponse {
317 pub id: String,
319 pub object: &'static str,
321 pub created: u64,
323 pub model: String,
325 pub choices: Vec<Choice>,
327 #[serde(skip_serializing_if = "Option::is_none")]
329 pub usage: Option<Usage>,
330 #[serde(skip_serializing_if = "Option::is_none")]
332 pub warnings: Option<Vec<String>>,
333}
334
335#[derive(Debug, Serialize)]
337pub struct Choice {
338 pub index: u32,
340 pub message: ResponseMessage,
342 pub finish_reason: Option<String>,
344}
345
346#[derive(Debug, Serialize)]
348pub struct ResponseMessage {
349 pub role: &'static str,
351 #[serde(skip_serializing_if = "Option::is_none")]
353 pub content: Option<String>,
354 #[serde(skip_serializing_if = "Option::is_none")]
356 pub tool_calls: Option<Vec<ToolCall>>,
357}
358
359#[derive(Debug, Serialize)]
374pub struct Usage {
375 #[serde(rename = "prompt_tokens")]
377 pub prompt: u32,
378 #[serde(rename = "completion_tokens")]
380 pub completion: u32,
381 #[serde(rename = "total_tokens")]
383 pub total: u32,
384 #[serde(skip_serializing_if = "Option::is_none")]
386 pub prompt_tokens_details: Option<PromptTokensDetails>,
387 #[serde(skip_serializing_if = "Option::is_none")]
389 pub completion_tokens_details: Option<CompletionTokensDetails>,
390 #[serde(skip_serializing_if = "Option::is_none")]
394 pub model_requests: Option<u32>,
395}
396
397#[derive(Debug, Serialize)]
399pub struct PromptTokensDetails {
400 #[serde(skip_serializing_if = "Option::is_none")]
402 pub cached_tokens: Option<u32>,
403 #[serde(skip_serializing_if = "Option::is_none")]
407 pub cache_write_tokens: Option<u32>,
408}
409
410#[derive(Debug, Serialize)]
412pub struct CompletionTokensDetails {
413 pub reasoning_tokens: u32,
415}
416
417impl From<&TokenUsage> for Usage {
418 fn from(usage: &TokenUsage) -> Self {
419 let prompt_tokens_details =
420 if usage.cached_read_tokens.is_some() || usage.cached_write_tokens.is_some() {
421 Some(PromptTokensDetails {
422 cached_tokens: usage.cached_read_tokens,
423 cache_write_tokens: usage.cached_write_tokens,
424 })
425 } else {
426 None
427 };
428 let completion = usage
430 .completion_tokens
431 .saturating_add(usage.reasoning_tokens.unwrap_or(0));
432 let total = usage
435 .total_tokens
436 .max(usage.prompt_tokens.saturating_add(completion));
437 Self {
438 prompt: usage.prompt_tokens,
439 completion,
440 total,
441 prompt_tokens_details,
442 completion_tokens_details: usage
443 .reasoning_tokens
444 .map(|reasoning_tokens| CompletionTokensDetails { reasoning_tokens }),
445 model_requests: usage.model_requests,
446 }
447 }
448}
449
450#[derive(Debug, Serialize)]
456pub struct ChatCompletionChunk {
457 pub id: String,
459 pub object: &'static str,
461 pub created: u64,
463 pub model: String,
465 pub choices: Vec<ChunkChoice>,
467}
468
469#[derive(Debug, Serialize)]
471pub struct ChunkChoice {
472 pub index: u32,
474 pub delta: Delta,
476 pub finish_reason: Option<String>,
478}
479
480#[derive(Debug, Serialize)]
482pub struct Delta {
483 #[serde(skip_serializing_if = "Option::is_none")]
485 pub role: Option<&'static str>,
486 #[serde(skip_serializing_if = "Option::is_none")]
488 pub content: Option<String>,
489 #[serde(skip_serializing_if = "Option::is_none")]
491 pub tool_calls: Option<Vec<ToolCall>>,
492}
493
494#[derive(Debug, Serialize)]
500pub struct MultiplexResponse {
501 pub id: String,
503 pub object: &'static str,
505 pub created: u64,
507 pub results: Vec<MultiplexProviderResult>,
509 pub summary: String,
511}
512
513#[derive(Debug, Serialize)]
515pub struct MultiplexProviderResult {
516 pub provider: String,
518 #[serde(skip_serializing_if = "Option::is_none")]
520 pub model: Option<String>,
521 #[serde(skip_serializing_if = "Option::is_none")]
523 pub content: Option<String>,
524 #[serde(skip_serializing_if = "Option::is_none")]
526 pub error: Option<String>,
527 pub duration_ms: u64,
529}
530
531#[derive(Debug, Serialize)]
537pub struct ModelsResponse {
538 pub object: &'static str,
540 pub data: Vec<ModelObject>,
542}
543
544#[derive(Debug, Serialize)]
546pub struct ModelObject {
547 pub id: String,
549 pub object: &'static str,
551 pub owned_by: String,
553}
554
555#[derive(Debug, Serialize)]
561pub struct HealthResponse {
562 pub status: &'static str,
564 pub providers: HashMap<String, String>,
566}
567
568#[derive(Debug, Serialize)]
574pub struct ErrorResponse {
575 pub error: ErrorDetail,
577}
578
579#[derive(Debug, Serialize)]
581pub struct ErrorDetail {
582 pub message: String,
584 #[serde(rename = "type")]
586 pub error_type: String,
587 #[serde(skip_serializing_if = "Option::is_none")]
589 pub param: Option<String>,
590 #[serde(skip_serializing_if = "Option::is_none")]
592 pub code: Option<String>,
593}
594
595impl ErrorResponse {
596 pub fn new(error_type: impl Into<String>, message: impl Into<String>) -> Self {
598 Self {
599 error: ErrorDetail {
600 message: message.into(),
601 error_type: error_type.into(),
602 param: None,
603 code: None,
604 },
605 }
606 }
607}
608
609#[cfg(test)]
610mod tests {
611 use super::*;
612
613 #[test]
614 fn usage_without_breakdown_carries_the_three_counts_only() {
615 let json =
616 serde_json::to_value(Usage::from(&TokenUsage::new(10, 5, 15))).expect("serialize"); assert_eq!(json["prompt_tokens"], 10);
618 assert_eq!(json["completion_tokens"], 5);
619 assert_eq!(json["total_tokens"], 15);
620 assert!(json.get("prompt_tokens_details").is_none());
622 assert!(json.get("completion_tokens_details").is_none());
623 }
624
625 #[test]
626 fn usage_forwards_cache_and_reasoning_counts_in_openai_shape() {
627 let usage = TokenUsage::new(27_862, 4, 27_866)
630 .with_cache(Some(15_320), Some(12_540))
631 .with_reasoning(Some(0));
632 let json = serde_json::to_value(Usage::from(&usage)).expect("serialize"); assert_eq!(json["prompt_tokens"], 27_862);
634 assert_eq!(json["completion_tokens"], 4);
635 assert_eq!(json["total_tokens"], 27_866);
636 assert_eq!(json["prompt_tokens_details"]["cached_tokens"], 15_320);
637 assert_eq!(json["prompt_tokens_details"]["cache_write_tokens"], 12_540);
638 assert_eq!(json["completion_tokens_details"]["reasoning_tokens"], 0);
639 }
640
641 #[test]
642 fn usage_folds_reasoning_into_openai_completion_tokens() {
643 let usage = TokenUsage::new(100, 7, 127).with_reasoning(Some(20));
645 let json = serde_json::to_value(Usage::from(&usage)).expect("serialize"); assert_eq!(json["prompt_tokens"], 100);
647 assert_eq!(json["completion_tokens"], 27);
648 assert_eq!(json["completion_tokens_details"]["reasoning_tokens"], 20);
649 assert_eq!(json["total_tokens"], 127);
650 }
651
652 #[test]
653 fn usage_total_is_raised_when_the_runner_total_omits_reasoning() {
654 let usage = TokenUsage::new(100, 7, 107).with_reasoning(Some(20));
656 let json = serde_json::to_value(Usage::from(&usage)).expect("serialize"); assert_eq!(json["completion_tokens"], 27);
658 assert_eq!(json["total_tokens"], 127);
659 }
660
661 #[test]
662 fn usage_omits_each_unreported_share_on_its_own() {
663 let usage = TokenUsage::new(100, 7, 107).with_cache(Some(64), None);
664 let json = serde_json::to_value(Usage::from(&usage)).expect("serialize"); assert_eq!(json["prompt_tokens_details"]["cached_tokens"], 64);
666 assert!(json["prompt_tokens_details"]
667 .get("cache_write_tokens")
668 .is_none());
669 assert!(json.get("completion_tokens_details").is_none());
670 }
671
672 #[test]
673 fn deserialize_single_model() {
674 let json = r#"{"model":"copilot:gpt-4o","messages":[{"role":"user","content":"hi"}]}"#;
675 let req: ChatCompletionRequest = serde_json::from_str(json).expect("deserialize"); match req.model {
677 ModelField::Single(m) => assert_eq!(m, "copilot:gpt-4o"),
678 ModelField::Multiple(_) => unreachable!("expected single"), }
680 assert!(!req.stream);
681 }
682
683 #[test]
684 fn deserialize_multiple_models() {
685 let json = r#"{"model":["copilot:gpt-4o","claude:opus"],"messages":[{"role":"user","content":"hi"}]}"#;
686 let req: ChatCompletionRequest = serde_json::from_str(json).expect("deserialize"); match req.model {
688 ModelField::Multiple(models) => {
689 assert_eq!(models.len(), 2);
690 assert_eq!(models[0], "copilot:gpt-4o");
691 assert_eq!(models[1], "claude:opus");
692 }
693 ModelField::Single(_) => unreachable!("expected multiple"), }
695 }
696
697 #[test]
698 fn deserialize_with_stream_flag() {
699 let json =
700 r#"{"model":"copilot","messages":[{"role":"user","content":"hi"}],"stream":true}"#;
701 let req: ChatCompletionRequest = serde_json::from_str(json).expect("deserialize"); assert!(req.stream);
703 }
704
705 #[test]
706 fn deserialize_message_with_null_content() {
707 let json = r#"{"model":"copilot","messages":[{"role":"assistant","content":null,"tool_calls":[{"id":"call_1","type":"function","function":{"name":"search","arguments":"{}"}}]}]}"#;
708 let req: ChatCompletionRequest = serde_json::from_str(json).expect("deserialize"); assert!(req.messages[0].content.is_none());
710 assert!(req.messages[0].tool_calls.is_some());
711 }
712
713 #[test]
714 fn deserialize_message_without_content_field() {
715 let json = r#"{"model":"copilot","messages":[{"role":"tool","tool_call_id":"call_1","name":"search","content":"{\"result\":\"found\"}"}]}"#;
716 let req: ChatCompletionRequest = serde_json::from_str(json).expect("deserialize"); assert_eq!(req.messages[0].role, "tool");
718 assert_eq!(req.messages[0].tool_call_id.as_deref(), Some("call_1"));
719 assert_eq!(req.messages[0].name.as_deref(), Some("search"));
720 }
721
722 #[test]
723 fn deserialize_multipart_content() {
724 let json = r#"{
725 "model": "copilot",
726 "messages": [{
727 "role": "user",
728 "content": [
729 {"type": "text", "text": "What is in this image?"},
730 {"type": "image_url", "image_url": {"url": "data:image/png;base64,aGVsbG8="}}
731 ]
732 }]
733 }"#;
734 let req: ChatCompletionRequest = serde_json::from_str(json).expect("deserialize"); let content = req.messages[0].content.as_ref().expect("content present"); match content {
737 MessageContent::Parts(parts) => {
738 assert_eq!(parts.len(), 2);
739 assert!(
740 matches!(&parts[0], ContentPart::Text { text } if text == "What is in this image?")
741 );
742 assert!(
743 matches!(&parts[1], ContentPart::ImageUrl { image_url } if image_url.url.contains("base64"))
744 );
745 }
746 MessageContent::Text(_) => unreachable!("expected Parts variant"), }
748 }
749
750 #[test]
751 fn message_content_as_text_plain_string() {
752 let content = MessageContent::Text("hello".to_owned());
753 assert_eq!(content.as_text(), "hello");
754 }
755
756 #[test]
757 fn message_content_as_text_multipart() {
758 let content = MessageContent::Parts(vec![
759 ContentPart::Text {
760 text: "describe ".to_owned(),
761 },
762 ContentPart::ImageUrl {
763 image_url: ImageUrlDetail {
764 url: "data:image/png;base64,abc".to_owned(),
765 },
766 },
767 ContentPart::Text {
768 text: "this image".to_owned(),
769 },
770 ]);
771 assert_eq!(content.as_text(), "describe this image");
772 }
773
774 #[test]
775 fn deserialize_plain_string_content_backward_compat() {
776 let json = r#"{"model":"copilot","messages":[{"role":"user","content":"hi"}]}"#;
777 let req: ChatCompletionRequest = serde_json::from_str(json).expect("deserialize"); let content = req.messages[0].content.as_ref().expect("content present"); match content {
780 MessageContent::Text(s) => assert_eq!(s, "hi"),
781 MessageContent::Parts(_) => unreachable!("expected Text variant"), }
783 }
784
785 #[test]
786 fn deserialize_tool_definitions() {
787 let json = r#"{
788 "model": "copilot",
789 "messages": [{"role": "user", "content": "hi"}],
790 "tools": [{
791 "type": "function",
792 "function": {
793 "name": "get_weather",
794 "description": "Get weather for a city",
795 "parameters": {"type": "object", "properties": {"city": {"type": "string"}}, "required": ["city"]}
796 }
797 }]
798 }"#;
799 let req: ChatCompletionRequest = serde_json::from_str(json).expect("deserialize"); let tools = req.tools.expect("tools present"); assert_eq!(tools.len(), 1);
802 assert_eq!(tools[0].tool_type, "function");
803 assert_eq!(tools[0].function.name, "get_weather");
804 assert!(tools[0].function.parameters.is_some());
805 }
806
807 #[test]
808 fn deserialize_tool_choice_auto() {
809 let json = r#"{"model":"copilot","messages":[{"role":"user","content":"hi"}],"tool_choice":"auto"}"#;
810 let req: ChatCompletionRequest = serde_json::from_str(json).expect("deserialize"); let tool_choice = req.tool_choice.expect("tool_choice present"); match tool_choice {
813 ToolChoice::Mode(m) => assert_eq!(m, "auto"),
814 ToolChoice::Specific(_) => unreachable!("expected mode"), }
816 }
817
818 #[test]
819 fn deserialize_tool_choice_specific() {
820 let json = r#"{"model":"copilot","messages":[{"role":"user","content":"hi"}],"tool_choice":{"type":"function","function":{"name":"get_weather"}}}"#;
821 let req: ChatCompletionRequest = serde_json::from_str(json).expect("deserialize"); let tool_choice = req.tool_choice.expect("tool_choice present"); match tool_choice {
824 ToolChoice::Specific(s) => assert_eq!(s.function.name, "get_weather"),
825 ToolChoice::Mode(_) => unreachable!("expected specific"), }
827 }
828
829 #[test]
830 fn serialize_completion_response() {
831 let resp = ChatCompletionResponse {
832 id: "chatcmpl-test".to_owned(),
833 object: "chat.completion",
834 created: 1_700_000_000,
835 model: "copilot:gpt-4o".to_owned(),
836 choices: vec![Choice {
837 index: 0,
838 message: ResponseMessage {
839 role: "assistant",
840 content: Some("Hello!".to_owned()),
841 tool_calls: None,
842 },
843 finish_reason: Some("stop".to_owned()),
844 }],
845 usage: None,
846 warnings: None,
847 };
848 let json = serde_json::to_string(&resp).expect("serialize"); assert!(json.contains("chat.completion"));
850 assert!(json.contains("Hello!"));
851 assert!(!json.contains("tool_calls"));
852 }
853
854 #[test]
855 fn serialize_response_with_tool_calls() {
856 let resp = ChatCompletionResponse {
857 id: "chatcmpl-test".to_owned(),
858 object: "chat.completion",
859 created: 1_700_000_000,
860 model: "copilot:gpt-4o".to_owned(),
861 choices: vec![Choice {
862 index: 0,
863 message: ResponseMessage {
864 role: "assistant",
865 content: None,
866 tool_calls: Some(vec![ToolCall {
867 index: 0,
868 id: "call_abc123".to_owned(),
869 tool_type: "function".to_owned(),
870 function: ToolCallFunction {
871 name: "get_weather".to_owned(),
872 arguments: r#"{"city":"Paris"}"#.to_owned(),
873 },
874 }]),
875 },
876 finish_reason: Some("tool_calls".to_owned()),
877 }],
878 usage: None,
879 warnings: None,
880 };
881 let json = serde_json::to_string(&resp).expect("serialize"); assert!(json.contains("tool_calls"));
883 assert!(json.contains("call_abc123"));
884 assert!(json.contains("get_weather"));
885 assert!(!json.contains(r#""content""#));
886 }
887
888 #[test]
889 fn serialize_error_response() {
890 let resp = ErrorResponse::new("invalid_request_error", "Unknown model");
891 let json = serde_json::to_string(&resp).expect("serialize"); assert!(json.contains("invalid_request_error"));
893 assert!(json.contains("Unknown model"));
894 }
895
896 #[test]
897 fn serialize_chunk_response() {
898 let chunk = ChatCompletionChunk {
899 id: "chatcmpl-test".to_owned(),
900 object: "chat.completion.chunk",
901 created: 1_700_000_000,
902 model: "copilot".to_owned(),
903 choices: vec![ChunkChoice {
904 index: 0,
905 delta: Delta {
906 role: None,
907 content: Some("token".to_owned()),
908 tool_calls: None,
909 },
910 finish_reason: None,
911 }],
912 };
913 let json = serde_json::to_string(&chunk).expect("serialize"); assert!(json.contains("chat.completion.chunk"));
915 assert!(json.contains("token"));
916 assert!(!json.contains("tool_calls"));
917 }
918
919 #[test]
920 fn deserialize_tool_execution_server() {
921 let json = r#"{"model":"copilot","messages":[{"role":"user","content":"hi"}],"tool_execution":"server"}"#;
922 let req: ChatCompletionRequest = serde_json::from_str(json).expect("deserialize"); assert_eq!(req.tool_execution, Some(ToolExecutionMode::Server));
924 }
925
926 #[test]
927 fn deserialize_tool_execution_client() {
928 let json = r#"{"model":"copilot","messages":[{"role":"user","content":"hi"}],"tool_execution":"client"}"#;
929 let req: ChatCompletionRequest = serde_json::from_str(json).expect("deserialize"); assert_eq!(req.tool_execution, Some(ToolExecutionMode::Client));
931 }
932
933 #[test]
934 fn tool_execution_defaults_to_none() {
935 let json = r#"{"model":"copilot","messages":[{"role":"user","content":"hi"}]}"#;
936 let req: ChatCompletionRequest = serde_json::from_str(json).expect("deserialize"); assert_eq!(req.tool_execution, None);
938 }
939
940 #[test]
941 fn deserialize_tool_choice_none() {
942 let json = r#"{"model":"copilot","messages":[{"role":"user","content":"hi"}],"tool_choice":"none"}"#;
943 let req: ChatCompletionRequest = serde_json::from_str(json).expect("deserialize"); let tool_choice = req.tool_choice.expect("tool_choice present"); match tool_choice {
946 ToolChoice::Mode(m) => assert_eq!(m, "none"),
947 ToolChoice::Specific(_) => unreachable!("expected mode"), }
949 }
950
951 #[test]
952 fn deserialize_tool_choice_required() {
953 let json = r#"{"model":"copilot","messages":[{"role":"user","content":"hi"}],"tool_choice":"required"}"#;
954 let req: ChatCompletionRequest = serde_json::from_str(json).expect("deserialize"); let tool_choice = req.tool_choice.expect("tool_choice present"); match tool_choice {
957 ToolChoice::Mode(m) => assert_eq!(m, "required"),
958 ToolChoice::Specific(_) => unreachable!("expected mode"), }
960 }
961
962 #[test]
963 fn deserialize_response_format_text() {
964 let json = r#"{"model":"copilot","messages":[{"role":"user","content":"hi"}],"response_format":{"type":"text"}}"#;
965 let req: ChatCompletionRequest = serde_json::from_str(json).expect("deserialize"); assert!(matches!(
967 req.response_format,
968 Some(ResponseFormatRequest::Text)
969 ));
970 }
971
972 #[test]
973 fn deserialize_response_format_json_object() {
974 let json = r#"{"model":"copilot","messages":[{"role":"user","content":"hi"}],"response_format":{"type":"json_object"}}"#;
975 let req: ChatCompletionRequest = serde_json::from_str(json).expect("deserialize"); assert!(matches!(
977 req.response_format,
978 Some(ResponseFormatRequest::JsonObject)
979 ));
980 }
981
982 #[test]
983 fn deserialize_response_format_json_schema() {
984 let json = r#"{
985 "model": "copilot",
986 "messages": [{"role": "user", "content": "hi"}],
987 "response_format": {
988 "type": "json_schema",
989 "json_schema": {
990 "name": "weather",
991 "schema": {"type": "object", "properties": {"temp": {"type": "number"}}}
992 }
993 }
994 }"#;
995 let req: ChatCompletionRequest = serde_json::from_str(json).expect("deserialize"); match req.response_format {
997 Some(ResponseFormatRequest::JsonSchema { json_schema }) => {
998 assert_eq!(json_schema.name, "weather");
999 assert!(json_schema.schema["properties"]["temp"].is_object());
1000 }
1001 other => unreachable!("expected JsonSchema, got: {other:?}"), }
1003 }
1004
1005 #[test]
1006 fn deserialize_top_p() {
1007 let json = r#"{"model":"copilot","messages":[{"role":"user","content":"hi"}],"top_p":0.9}"#;
1008 let req: ChatCompletionRequest = serde_json::from_str(json).expect("deserialize"); assert_eq!(req.top_p, Some(0.9));
1010 }
1011
1012 #[test]
1013 fn deserialize_stop_single() {
1014 let json =
1015 r#"{"model":"copilot","messages":[{"role":"user","content":"hi"}],"stop":"END"}"#;
1016 let req: ChatCompletionRequest = serde_json::from_str(json).expect("deserialize"); let stop = req.stop.expect("stop present"); assert_eq!(stop.into_vec(), vec!["END"]);
1019 }
1020
1021 #[test]
1022 fn deserialize_stop_array() {
1023 let json = r#"{"model":"copilot","messages":[{"role":"user","content":"hi"}],"stop":["END","STOP"]}"#;
1024 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"]);
1027 }
1028
1029 #[test]
1030 fn stop_field_len() {
1031 let single = StopField::Single("END".to_owned());
1032 assert_eq!(single.len(), 1);
1033 let multiple = StopField::Multiple(vec!["A".to_owned(), "B".to_owned(), "C".to_owned()]);
1034 assert_eq!(multiple.len(), 3);
1035 }
1036
1037 #[test]
1038 fn stop_field_into_vec_truncates_at_four() {
1039 let oversized = StopField::Multiple((0..10).map(|i| format!("stop_{i}")).collect());
1040 let result = oversized.into_vec();
1041 assert_eq!(result.len(), 4);
1042 assert_eq!(result[0], "stop_0");
1043 assert_eq!(result[3], "stop_3");
1044 }
1045
1046 #[test]
1047 fn deserialize_all_optional_fields() {
1048 let json = r#"{
1049 "model": "copilot",
1050 "messages": [{"role": "user", "content": "hi"}],
1051 "temperature": 0.7,
1052 "max_tokens": 100,
1053 "top_p": 0.95,
1054 "stop": ["END"],
1055 "stream": true
1056 }"#;
1057 let req: ChatCompletionRequest = serde_json::from_str(json).expect("deserialize"); assert_eq!(req.temperature, Some(0.7));
1059 assert_eq!(req.max_tokens, Some(100));
1060 assert_eq!(req.top_p, Some(0.95));
1061 assert!(req.stop.is_some());
1062 assert!(req.stream);
1063 }
1064
1065 #[test]
1066 fn serialize_models_response() {
1067 let resp = ModelsResponse {
1068 object: "list",
1069 data: vec![ModelObject {
1070 id: "copilot:gpt-4o".to_owned(),
1071 object: "model",
1072 owned_by: "copilot".to_owned(),
1073 }],
1074 };
1075 let json = serde_json::to_string(&resp).expect("serialize"); assert!(json.contains("copilot:gpt-4o"));
1077 }
1078}