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}
391
392#[derive(Debug, Serialize)]
394pub struct PromptTokensDetails {
395 #[serde(skip_serializing_if = "Option::is_none")]
397 pub cached_tokens: Option<u32>,
398 #[serde(skip_serializing_if = "Option::is_none")]
402 pub cache_write_tokens: Option<u32>,
403}
404
405#[derive(Debug, Serialize)]
407pub struct CompletionTokensDetails {
408 pub reasoning_tokens: u32,
410}
411
412impl From<&TokenUsage> for Usage {
413 fn from(usage: &TokenUsage) -> Self {
414 let prompt_tokens_details =
415 if usage.cached_read_tokens.is_some() || usage.cached_write_tokens.is_some() {
416 Some(PromptTokensDetails {
417 cached_tokens: usage.cached_read_tokens,
418 cache_write_tokens: usage.cached_write_tokens,
419 })
420 } else {
421 None
422 };
423 let completion = usage
425 .completion_tokens
426 .saturating_add(usage.reasoning_tokens.unwrap_or(0));
427 let total = usage
430 .total_tokens
431 .max(usage.prompt_tokens.saturating_add(completion));
432 Self {
433 prompt: usage.prompt_tokens,
434 completion,
435 total,
436 prompt_tokens_details,
437 completion_tokens_details: usage
438 .reasoning_tokens
439 .map(|reasoning_tokens| CompletionTokensDetails { reasoning_tokens }),
440 }
441 }
442}
443
444#[derive(Debug, Serialize)]
450pub struct ChatCompletionChunk {
451 pub id: String,
453 pub object: &'static str,
455 pub created: u64,
457 pub model: String,
459 pub choices: Vec<ChunkChoice>,
461}
462
463#[derive(Debug, Serialize)]
465pub struct ChunkChoice {
466 pub index: u32,
468 pub delta: Delta,
470 pub finish_reason: Option<String>,
472}
473
474#[derive(Debug, Serialize)]
476pub struct Delta {
477 #[serde(skip_serializing_if = "Option::is_none")]
479 pub role: Option<&'static str>,
480 #[serde(skip_serializing_if = "Option::is_none")]
482 pub content: Option<String>,
483 #[serde(skip_serializing_if = "Option::is_none")]
485 pub tool_calls: Option<Vec<ToolCall>>,
486}
487
488#[derive(Debug, Serialize)]
494pub struct MultiplexResponse {
495 pub id: String,
497 pub object: &'static str,
499 pub created: u64,
501 pub results: Vec<MultiplexProviderResult>,
503 pub summary: String,
505}
506
507#[derive(Debug, Serialize)]
509pub struct MultiplexProviderResult {
510 pub provider: String,
512 #[serde(skip_serializing_if = "Option::is_none")]
514 pub model: Option<String>,
515 #[serde(skip_serializing_if = "Option::is_none")]
517 pub content: Option<String>,
518 #[serde(skip_serializing_if = "Option::is_none")]
520 pub error: Option<String>,
521 pub duration_ms: u64,
523}
524
525#[derive(Debug, Serialize)]
531pub struct ModelsResponse {
532 pub object: &'static str,
534 pub data: Vec<ModelObject>,
536}
537
538#[derive(Debug, Serialize)]
540pub struct ModelObject {
541 pub id: String,
543 pub object: &'static str,
545 pub owned_by: String,
547}
548
549#[derive(Debug, Serialize)]
555pub struct HealthResponse {
556 pub status: &'static str,
558 pub providers: HashMap<String, String>,
560}
561
562#[derive(Debug, Serialize)]
568pub struct ErrorResponse {
569 pub error: ErrorDetail,
571}
572
573#[derive(Debug, Serialize)]
575pub struct ErrorDetail {
576 pub message: String,
578 #[serde(rename = "type")]
580 pub error_type: String,
581 #[serde(skip_serializing_if = "Option::is_none")]
583 pub param: Option<String>,
584 #[serde(skip_serializing_if = "Option::is_none")]
586 pub code: Option<String>,
587}
588
589impl ErrorResponse {
590 pub fn new(error_type: impl Into<String>, message: impl Into<String>) -> Self {
592 Self {
593 error: ErrorDetail {
594 message: message.into(),
595 error_type: error_type.into(),
596 param: None,
597 code: None,
598 },
599 }
600 }
601}
602
603#[cfg(test)]
604mod tests {
605 use super::*;
606
607 #[test]
608 fn usage_without_breakdown_carries_the_three_counts_only() {
609 let json =
610 serde_json::to_value(Usage::from(&TokenUsage::new(10, 5, 15))).expect("serialize"); assert_eq!(json["prompt_tokens"], 10);
612 assert_eq!(json["completion_tokens"], 5);
613 assert_eq!(json["total_tokens"], 15);
614 assert!(json.get("prompt_tokens_details").is_none());
616 assert!(json.get("completion_tokens_details").is_none());
617 }
618
619 #[test]
620 fn usage_forwards_cache_and_reasoning_counts_in_openai_shape() {
621 let usage = TokenUsage::new(27_862, 4, 27_866)
624 .with_cache(Some(15_320), Some(12_540))
625 .with_reasoning(Some(0));
626 let json = serde_json::to_value(Usage::from(&usage)).expect("serialize"); assert_eq!(json["prompt_tokens"], 27_862);
628 assert_eq!(json["completion_tokens"], 4);
629 assert_eq!(json["total_tokens"], 27_866);
630 assert_eq!(json["prompt_tokens_details"]["cached_tokens"], 15_320);
631 assert_eq!(json["prompt_tokens_details"]["cache_write_tokens"], 12_540);
632 assert_eq!(json["completion_tokens_details"]["reasoning_tokens"], 0);
633 }
634
635 #[test]
636 fn usage_folds_reasoning_into_openai_completion_tokens() {
637 let usage = TokenUsage::new(100, 7, 127).with_reasoning(Some(20));
639 let json = serde_json::to_value(Usage::from(&usage)).expect("serialize"); assert_eq!(json["prompt_tokens"], 100);
641 assert_eq!(json["completion_tokens"], 27);
642 assert_eq!(json["completion_tokens_details"]["reasoning_tokens"], 20);
643 assert_eq!(json["total_tokens"], 127);
644 }
645
646 #[test]
647 fn usage_total_is_raised_when_the_runner_total_omits_reasoning() {
648 let usage = TokenUsage::new(100, 7, 107).with_reasoning(Some(20));
650 let json = serde_json::to_value(Usage::from(&usage)).expect("serialize"); assert_eq!(json["completion_tokens"], 27);
652 assert_eq!(json["total_tokens"], 127);
653 }
654
655 #[test]
656 fn usage_omits_each_unreported_share_on_its_own() {
657 let usage = TokenUsage::new(100, 7, 107).with_cache(Some(64), None);
658 let json = serde_json::to_value(Usage::from(&usage)).expect("serialize"); assert_eq!(json["prompt_tokens_details"]["cached_tokens"], 64);
660 assert!(json["prompt_tokens_details"]
661 .get("cache_write_tokens")
662 .is_none());
663 assert!(json.get("completion_tokens_details").is_none());
664 }
665
666 #[test]
667 fn deserialize_single_model() {
668 let json = r#"{"model":"copilot:gpt-4o","messages":[{"role":"user","content":"hi"}]}"#;
669 let req: ChatCompletionRequest = serde_json::from_str(json).expect("deserialize"); match req.model {
671 ModelField::Single(m) => assert_eq!(m, "copilot:gpt-4o"),
672 ModelField::Multiple(_) => unreachable!("expected single"), }
674 assert!(!req.stream);
675 }
676
677 #[test]
678 fn deserialize_multiple_models() {
679 let json = r#"{"model":["copilot:gpt-4o","claude:opus"],"messages":[{"role":"user","content":"hi"}]}"#;
680 let req: ChatCompletionRequest = serde_json::from_str(json).expect("deserialize"); match req.model {
682 ModelField::Multiple(models) => {
683 assert_eq!(models.len(), 2);
684 assert_eq!(models[0], "copilot:gpt-4o");
685 assert_eq!(models[1], "claude:opus");
686 }
687 ModelField::Single(_) => unreachable!("expected multiple"), }
689 }
690
691 #[test]
692 fn deserialize_with_stream_flag() {
693 let json =
694 r#"{"model":"copilot","messages":[{"role":"user","content":"hi"}],"stream":true}"#;
695 let req: ChatCompletionRequest = serde_json::from_str(json).expect("deserialize"); assert!(req.stream);
697 }
698
699 #[test]
700 fn deserialize_message_with_null_content() {
701 let json = r#"{"model":"copilot","messages":[{"role":"assistant","content":null,"tool_calls":[{"id":"call_1","type":"function","function":{"name":"search","arguments":"{}"}}]}]}"#;
702 let req: ChatCompletionRequest = serde_json::from_str(json).expect("deserialize"); assert!(req.messages[0].content.is_none());
704 assert!(req.messages[0].tool_calls.is_some());
705 }
706
707 #[test]
708 fn deserialize_message_without_content_field() {
709 let json = r#"{"model":"copilot","messages":[{"role":"tool","tool_call_id":"call_1","name":"search","content":"{\"result\":\"found\"}"}]}"#;
710 let req: ChatCompletionRequest = serde_json::from_str(json).expect("deserialize"); assert_eq!(req.messages[0].role, "tool");
712 assert_eq!(req.messages[0].tool_call_id.as_deref(), Some("call_1"));
713 assert_eq!(req.messages[0].name.as_deref(), Some("search"));
714 }
715
716 #[test]
717 fn deserialize_multipart_content() {
718 let json = r#"{
719 "model": "copilot",
720 "messages": [{
721 "role": "user",
722 "content": [
723 {"type": "text", "text": "What is in this image?"},
724 {"type": "image_url", "image_url": {"url": "data:image/png;base64,aGVsbG8="}}
725 ]
726 }]
727 }"#;
728 let req: ChatCompletionRequest = serde_json::from_str(json).expect("deserialize"); let content = req.messages[0].content.as_ref().expect("content present"); match content {
731 MessageContent::Parts(parts) => {
732 assert_eq!(parts.len(), 2);
733 assert!(
734 matches!(&parts[0], ContentPart::Text { text } if text == "What is in this image?")
735 );
736 assert!(
737 matches!(&parts[1], ContentPart::ImageUrl { image_url } if image_url.url.contains("base64"))
738 );
739 }
740 MessageContent::Text(_) => unreachable!("expected Parts variant"), }
742 }
743
744 #[test]
745 fn message_content_as_text_plain_string() {
746 let content = MessageContent::Text("hello".to_owned());
747 assert_eq!(content.as_text(), "hello");
748 }
749
750 #[test]
751 fn message_content_as_text_multipart() {
752 let content = MessageContent::Parts(vec![
753 ContentPart::Text {
754 text: "describe ".to_owned(),
755 },
756 ContentPart::ImageUrl {
757 image_url: ImageUrlDetail {
758 url: "data:image/png;base64,abc".to_owned(),
759 },
760 },
761 ContentPart::Text {
762 text: "this image".to_owned(),
763 },
764 ]);
765 assert_eq!(content.as_text(), "describe this image");
766 }
767
768 #[test]
769 fn deserialize_plain_string_content_backward_compat() {
770 let json = r#"{"model":"copilot","messages":[{"role":"user","content":"hi"}]}"#;
771 let req: ChatCompletionRequest = serde_json::from_str(json).expect("deserialize"); let content = req.messages[0].content.as_ref().expect("content present"); match content {
774 MessageContent::Text(s) => assert_eq!(s, "hi"),
775 MessageContent::Parts(_) => unreachable!("expected Text variant"), }
777 }
778
779 #[test]
780 fn deserialize_tool_definitions() {
781 let json = r#"{
782 "model": "copilot",
783 "messages": [{"role": "user", "content": "hi"}],
784 "tools": [{
785 "type": "function",
786 "function": {
787 "name": "get_weather",
788 "description": "Get weather for a city",
789 "parameters": {"type": "object", "properties": {"city": {"type": "string"}}, "required": ["city"]}
790 }
791 }]
792 }"#;
793 let req: ChatCompletionRequest = serde_json::from_str(json).expect("deserialize"); let tools = req.tools.expect("tools present"); assert_eq!(tools.len(), 1);
796 assert_eq!(tools[0].tool_type, "function");
797 assert_eq!(tools[0].function.name, "get_weather");
798 assert!(tools[0].function.parameters.is_some());
799 }
800
801 #[test]
802 fn deserialize_tool_choice_auto() {
803 let json = r#"{"model":"copilot","messages":[{"role":"user","content":"hi"}],"tool_choice":"auto"}"#;
804 let req: ChatCompletionRequest = serde_json::from_str(json).expect("deserialize"); let tool_choice = req.tool_choice.expect("tool_choice present"); match tool_choice {
807 ToolChoice::Mode(m) => assert_eq!(m, "auto"),
808 ToolChoice::Specific(_) => unreachable!("expected mode"), }
810 }
811
812 #[test]
813 fn deserialize_tool_choice_specific() {
814 let json = r#"{"model":"copilot","messages":[{"role":"user","content":"hi"}],"tool_choice":{"type":"function","function":{"name":"get_weather"}}}"#;
815 let req: ChatCompletionRequest = serde_json::from_str(json).expect("deserialize"); let tool_choice = req.tool_choice.expect("tool_choice present"); match tool_choice {
818 ToolChoice::Specific(s) => assert_eq!(s.function.name, "get_weather"),
819 ToolChoice::Mode(_) => unreachable!("expected specific"), }
821 }
822
823 #[test]
824 fn serialize_completion_response() {
825 let resp = ChatCompletionResponse {
826 id: "chatcmpl-test".to_owned(),
827 object: "chat.completion",
828 created: 1_700_000_000,
829 model: "copilot:gpt-4o".to_owned(),
830 choices: vec![Choice {
831 index: 0,
832 message: ResponseMessage {
833 role: "assistant",
834 content: Some("Hello!".to_owned()),
835 tool_calls: None,
836 },
837 finish_reason: Some("stop".to_owned()),
838 }],
839 usage: None,
840 warnings: None,
841 };
842 let json = serde_json::to_string(&resp).expect("serialize"); assert!(json.contains("chat.completion"));
844 assert!(json.contains("Hello!"));
845 assert!(!json.contains("tool_calls"));
846 }
847
848 #[test]
849 fn serialize_response_with_tool_calls() {
850 let resp = ChatCompletionResponse {
851 id: "chatcmpl-test".to_owned(),
852 object: "chat.completion",
853 created: 1_700_000_000,
854 model: "copilot:gpt-4o".to_owned(),
855 choices: vec![Choice {
856 index: 0,
857 message: ResponseMessage {
858 role: "assistant",
859 content: None,
860 tool_calls: Some(vec![ToolCall {
861 index: 0,
862 id: "call_abc123".to_owned(),
863 tool_type: "function".to_owned(),
864 function: ToolCallFunction {
865 name: "get_weather".to_owned(),
866 arguments: r#"{"city":"Paris"}"#.to_owned(),
867 },
868 }]),
869 },
870 finish_reason: Some("tool_calls".to_owned()),
871 }],
872 usage: None,
873 warnings: None,
874 };
875 let json = serde_json::to_string(&resp).expect("serialize"); assert!(json.contains("tool_calls"));
877 assert!(json.contains("call_abc123"));
878 assert!(json.contains("get_weather"));
879 assert!(!json.contains(r#""content""#));
880 }
881
882 #[test]
883 fn serialize_error_response() {
884 let resp = ErrorResponse::new("invalid_request_error", "Unknown model");
885 let json = serde_json::to_string(&resp).expect("serialize"); assert!(json.contains("invalid_request_error"));
887 assert!(json.contains("Unknown model"));
888 }
889
890 #[test]
891 fn serialize_chunk_response() {
892 let chunk = ChatCompletionChunk {
893 id: "chatcmpl-test".to_owned(),
894 object: "chat.completion.chunk",
895 created: 1_700_000_000,
896 model: "copilot".to_owned(),
897 choices: vec![ChunkChoice {
898 index: 0,
899 delta: Delta {
900 role: None,
901 content: Some("token".to_owned()),
902 tool_calls: None,
903 },
904 finish_reason: None,
905 }],
906 };
907 let json = serde_json::to_string(&chunk).expect("serialize"); assert!(json.contains("chat.completion.chunk"));
909 assert!(json.contains("token"));
910 assert!(!json.contains("tool_calls"));
911 }
912
913 #[test]
914 fn deserialize_tool_execution_server() {
915 let json = r#"{"model":"copilot","messages":[{"role":"user","content":"hi"}],"tool_execution":"server"}"#;
916 let req: ChatCompletionRequest = serde_json::from_str(json).expect("deserialize"); assert_eq!(req.tool_execution, Some(ToolExecutionMode::Server));
918 }
919
920 #[test]
921 fn deserialize_tool_execution_client() {
922 let json = r#"{"model":"copilot","messages":[{"role":"user","content":"hi"}],"tool_execution":"client"}"#;
923 let req: ChatCompletionRequest = serde_json::from_str(json).expect("deserialize"); assert_eq!(req.tool_execution, Some(ToolExecutionMode::Client));
925 }
926
927 #[test]
928 fn tool_execution_defaults_to_none() {
929 let json = r#"{"model":"copilot","messages":[{"role":"user","content":"hi"}]}"#;
930 let req: ChatCompletionRequest = serde_json::from_str(json).expect("deserialize"); assert_eq!(req.tool_execution, None);
932 }
933
934 #[test]
935 fn deserialize_tool_choice_none() {
936 let json = r#"{"model":"copilot","messages":[{"role":"user","content":"hi"}],"tool_choice":"none"}"#;
937 let req: ChatCompletionRequest = serde_json::from_str(json).expect("deserialize"); let tool_choice = req.tool_choice.expect("tool_choice present"); match tool_choice {
940 ToolChoice::Mode(m) => assert_eq!(m, "none"),
941 ToolChoice::Specific(_) => unreachable!("expected mode"), }
943 }
944
945 #[test]
946 fn deserialize_tool_choice_required() {
947 let json = r#"{"model":"copilot","messages":[{"role":"user","content":"hi"}],"tool_choice":"required"}"#;
948 let req: ChatCompletionRequest = serde_json::from_str(json).expect("deserialize"); let tool_choice = req.tool_choice.expect("tool_choice present"); match tool_choice {
951 ToolChoice::Mode(m) => assert_eq!(m, "required"),
952 ToolChoice::Specific(_) => unreachable!("expected mode"), }
954 }
955
956 #[test]
957 fn deserialize_response_format_text() {
958 let json = r#"{"model":"copilot","messages":[{"role":"user","content":"hi"}],"response_format":{"type":"text"}}"#;
959 let req: ChatCompletionRequest = serde_json::from_str(json).expect("deserialize"); assert!(matches!(
961 req.response_format,
962 Some(ResponseFormatRequest::Text)
963 ));
964 }
965
966 #[test]
967 fn deserialize_response_format_json_object() {
968 let json = r#"{"model":"copilot","messages":[{"role":"user","content":"hi"}],"response_format":{"type":"json_object"}}"#;
969 let req: ChatCompletionRequest = serde_json::from_str(json).expect("deserialize"); assert!(matches!(
971 req.response_format,
972 Some(ResponseFormatRequest::JsonObject)
973 ));
974 }
975
976 #[test]
977 fn deserialize_response_format_json_schema() {
978 let json = r#"{
979 "model": "copilot",
980 "messages": [{"role": "user", "content": "hi"}],
981 "response_format": {
982 "type": "json_schema",
983 "json_schema": {
984 "name": "weather",
985 "schema": {"type": "object", "properties": {"temp": {"type": "number"}}}
986 }
987 }
988 }"#;
989 let req: ChatCompletionRequest = serde_json::from_str(json).expect("deserialize"); match req.response_format {
991 Some(ResponseFormatRequest::JsonSchema { json_schema }) => {
992 assert_eq!(json_schema.name, "weather");
993 assert!(json_schema.schema["properties"]["temp"].is_object());
994 }
995 other => unreachable!("expected JsonSchema, got: {other:?}"), }
997 }
998
999 #[test]
1000 fn deserialize_top_p() {
1001 let json = r#"{"model":"copilot","messages":[{"role":"user","content":"hi"}],"top_p":0.9}"#;
1002 let req: ChatCompletionRequest = serde_json::from_str(json).expect("deserialize"); assert_eq!(req.top_p, Some(0.9));
1004 }
1005
1006 #[test]
1007 fn deserialize_stop_single() {
1008 let json =
1009 r#"{"model":"copilot","messages":[{"role":"user","content":"hi"}],"stop":"END"}"#;
1010 let req: ChatCompletionRequest = serde_json::from_str(json).expect("deserialize"); let stop = req.stop.expect("stop present"); assert_eq!(stop.into_vec(), vec!["END"]);
1013 }
1014
1015 #[test]
1016 fn deserialize_stop_array() {
1017 let json = r#"{"model":"copilot","messages":[{"role":"user","content":"hi"}],"stop":["END","STOP"]}"#;
1018 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"]);
1021 }
1022
1023 #[test]
1024 fn stop_field_len() {
1025 let single = StopField::Single("END".to_owned());
1026 assert_eq!(single.len(), 1);
1027 let multiple = StopField::Multiple(vec!["A".to_owned(), "B".to_owned(), "C".to_owned()]);
1028 assert_eq!(multiple.len(), 3);
1029 }
1030
1031 #[test]
1032 fn stop_field_into_vec_truncates_at_four() {
1033 let oversized = StopField::Multiple((0..10).map(|i| format!("stop_{i}")).collect());
1034 let result = oversized.into_vec();
1035 assert_eq!(result.len(), 4);
1036 assert_eq!(result[0], "stop_0");
1037 assert_eq!(result[3], "stop_3");
1038 }
1039
1040 #[test]
1041 fn deserialize_all_optional_fields() {
1042 let json = r#"{
1043 "model": "copilot",
1044 "messages": [{"role": "user", "content": "hi"}],
1045 "temperature": 0.7,
1046 "max_tokens": 100,
1047 "top_p": 0.95,
1048 "stop": ["END"],
1049 "stream": true
1050 }"#;
1051 let req: ChatCompletionRequest = serde_json::from_str(json).expect("deserialize"); assert_eq!(req.temperature, Some(0.7));
1053 assert_eq!(req.max_tokens, Some(100));
1054 assert_eq!(req.top_p, Some(0.95));
1055 assert!(req.stop.is_some());
1056 assert!(req.stream);
1057 }
1058
1059 #[test]
1060 fn serialize_models_response() {
1061 let resp = ModelsResponse {
1062 object: "list",
1063 data: vec![ModelObject {
1064 id: "copilot:gpt-4o".to_owned(),
1065 object: "model",
1066 owned_by: "copilot".to_owned(),
1067 }],
1068 };
1069 let json = serde_json::to_string(&resp).expect("serialize"); assert!(json.contains("copilot:gpt-4o"));
1071 }
1072}