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}
46
47#[derive(Debug, Clone, Deserialize)]
49#[serde(tag = "type")]
50pub enum ResponseFormatRequest {
51 #[serde(rename = "text")]
53 Text,
54 #[serde(rename = "json_object")]
56 JsonObject,
57 #[serde(rename = "json_schema")]
59 JsonSchema {
60 json_schema: JsonSchemaSpec,
62 },
63}
64
65#[derive(Debug, Clone, Deserialize)]
67pub struct JsonSchemaSpec {
68 pub name: String,
70 pub schema: serde_json::Value,
72}
73
74#[derive(Debug, Clone, Deserialize)]
76#[serde(untagged)]
77pub enum ModelField {
78 Single(String),
80 Multiple(Vec<String>),
82}
83
84#[derive(Debug, Clone, Deserialize)]
86pub struct ChatCompletionMessage {
87 pub role: String,
89 pub content: Option<String>,
91 #[serde(default)]
93 pub tool_calls: Option<Vec<ToolCall>>,
94 #[serde(default)]
96 pub tool_call_id: Option<String>,
97 #[serde(default)]
99 pub name: Option<String>,
100}
101
102#[derive(Debug, Clone, Deserialize)]
108pub struct ToolDefinition {
109 #[serde(rename = "type")]
111 pub tool_type: String,
112 pub function: FunctionObject,
114}
115
116#[derive(Debug, Clone, Deserialize)]
118pub struct FunctionObject {
119 pub name: String,
121 #[serde(default)]
123 pub description: Option<String>,
124 #[serde(default)]
126 pub parameters: Option<serde_json::Value>,
127}
128
129#[derive(Debug, Clone, Deserialize)]
131#[serde(untagged)]
132pub enum ToolChoice {
133 Mode(String),
135 Specific(ToolChoiceSpecific),
137}
138
139#[derive(Debug, Clone, Deserialize)]
141pub struct ToolChoiceSpecific {
142 #[serde(rename = "type")]
144 pub tool_type: String,
145 pub function: ToolChoiceFunction,
147}
148
149#[derive(Debug, Clone, Deserialize)]
151pub struct ToolChoiceFunction {
152 pub name: String,
154}
155
156#[derive(Debug, Clone, Serialize, Deserialize)]
158pub struct ToolCall {
159 #[serde(default)]
161 pub index: usize,
162 pub id: String,
164 #[serde(rename = "type")]
166 pub tool_type: String,
167 pub function: ToolCallFunction,
169}
170
171#[derive(Debug, Clone, Serialize, Deserialize)]
173pub struct ToolCallFunction {
174 pub name: String,
176 pub arguments: String,
178}
179
180#[derive(Debug, Serialize)]
186pub struct ChatCompletionResponse {
187 pub id: String,
189 pub object: &'static str,
191 pub created: u64,
193 pub model: String,
195 pub choices: Vec<Choice>,
197 #[serde(skip_serializing_if = "Option::is_none")]
199 pub usage: Option<Usage>,
200 #[serde(skip_serializing_if = "Option::is_none")]
202 pub warnings: Option<Vec<String>>,
203}
204
205#[derive(Debug, Serialize)]
207pub struct Choice {
208 pub index: u32,
210 pub message: ResponseMessage,
212 pub finish_reason: Option<String>,
214}
215
216#[derive(Debug, Serialize)]
218pub struct ResponseMessage {
219 pub role: &'static str,
221 #[serde(skip_serializing_if = "Option::is_none")]
223 pub content: Option<String>,
224 #[serde(skip_serializing_if = "Option::is_none")]
226 pub tool_calls: Option<Vec<ToolCall>>,
227}
228
229#[derive(Debug, Serialize)]
231pub struct Usage {
232 #[serde(rename = "prompt_tokens")]
234 pub prompt: u32,
235 #[serde(rename = "completion_tokens")]
237 pub completion: u32,
238 #[serde(rename = "total_tokens")]
240 pub total: u32,
241}
242
243#[derive(Debug, Serialize)]
249pub struct ChatCompletionChunk {
250 pub id: String,
252 pub object: &'static str,
254 pub created: u64,
256 pub model: String,
258 pub choices: Vec<ChunkChoice>,
260}
261
262#[derive(Debug, Serialize)]
264pub struct ChunkChoice {
265 pub index: u32,
267 pub delta: Delta,
269 pub finish_reason: Option<String>,
271}
272
273#[derive(Debug, Serialize)]
275pub struct Delta {
276 #[serde(skip_serializing_if = "Option::is_none")]
278 pub role: Option<&'static str>,
279 #[serde(skip_serializing_if = "Option::is_none")]
281 pub content: Option<String>,
282 #[serde(skip_serializing_if = "Option::is_none")]
284 pub tool_calls: Option<Vec<ToolCall>>,
285}
286
287#[derive(Debug, Serialize)]
293pub struct MultiplexResponse {
294 pub id: String,
296 pub object: &'static str,
298 pub created: u64,
300 pub results: Vec<MultiplexProviderResult>,
302 pub summary: String,
304}
305
306#[derive(Debug, Serialize)]
308pub struct MultiplexProviderResult {
309 pub provider: String,
311 #[serde(skip_serializing_if = "Option::is_none")]
313 pub model: Option<String>,
314 #[serde(skip_serializing_if = "Option::is_none")]
316 pub content: Option<String>,
317 #[serde(skip_serializing_if = "Option::is_none")]
319 pub error: Option<String>,
320 pub duration_ms: u64,
322}
323
324#[derive(Debug, Serialize)]
330pub struct ModelsResponse {
331 pub object: &'static str,
333 pub data: Vec<ModelObject>,
335}
336
337#[derive(Debug, Serialize)]
339pub struct ModelObject {
340 pub id: String,
342 pub object: &'static str,
344 pub owned_by: String,
346}
347
348#[derive(Debug, Serialize)]
354pub struct HealthResponse {
355 pub status: &'static str,
357 pub providers: std::collections::HashMap<String, String>,
359}
360
361#[derive(Debug, Serialize)]
367pub struct ErrorResponse {
368 pub error: ErrorDetail,
370}
371
372#[derive(Debug, Serialize)]
374pub struct ErrorDetail {
375 pub message: String,
377 #[serde(rename = "type")]
379 pub error_type: String,
380 #[serde(skip_serializing_if = "Option::is_none")]
382 pub param: Option<String>,
383 #[serde(skip_serializing_if = "Option::is_none")]
385 pub code: Option<String>,
386}
387
388impl ErrorResponse {
389 pub fn new(error_type: impl Into<String>, message: impl Into<String>) -> Self {
391 Self {
392 error: ErrorDetail {
393 message: message.into(),
394 error_type: error_type.into(),
395 param: None,
396 code: None,
397 },
398 }
399 }
400}
401
402#[cfg(test)]
403mod tests {
404 use super::*;
405
406 #[test]
407 fn deserialize_single_model() {
408 let json = r#"{"model":"copilot:gpt-4o","messages":[{"role":"user","content":"hi"}]}"#;
409 let req: ChatCompletionRequest = serde_json::from_str(json).expect("deserialize");
410 match req.model {
411 ModelField::Single(m) => assert_eq!(m, "copilot:gpt-4o"),
412 ModelField::Multiple(_) => panic!("expected single"),
413 }
414 assert!(!req.stream);
415 }
416
417 #[test]
418 fn deserialize_multiple_models() {
419 let json = r#"{"model":["copilot:gpt-4o","claude:opus"],"messages":[{"role":"user","content":"hi"}]}"#;
420 let req: ChatCompletionRequest = serde_json::from_str(json).expect("deserialize");
421 match req.model {
422 ModelField::Multiple(models) => {
423 assert_eq!(models.len(), 2);
424 assert_eq!(models[0], "copilot:gpt-4o");
425 assert_eq!(models[1], "claude:opus");
426 }
427 ModelField::Single(_) => panic!("expected multiple"),
428 }
429 }
430
431 #[test]
432 fn deserialize_with_stream_flag() {
433 let json =
434 r#"{"model":"copilot","messages":[{"role":"user","content":"hi"}],"stream":true}"#;
435 let req: ChatCompletionRequest = serde_json::from_str(json).expect("deserialize");
436 assert!(req.stream);
437 }
438
439 #[test]
440 fn deserialize_message_with_null_content() {
441 let json = r#"{"model":"copilot","messages":[{"role":"assistant","content":null,"tool_calls":[{"id":"call_1","type":"function","function":{"name":"search","arguments":"{}"}}]}]}"#;
442 let req: ChatCompletionRequest = serde_json::from_str(json).expect("deserialize");
443 assert!(req.messages[0].content.is_none());
444 assert!(req.messages[0].tool_calls.is_some());
445 }
446
447 #[test]
448 fn deserialize_message_without_content_field() {
449 let json = r#"{"model":"copilot","messages":[{"role":"tool","tool_call_id":"call_1","name":"search","content":"{\"result\":\"found\"}"}]}"#;
450 let req: ChatCompletionRequest = serde_json::from_str(json).expect("deserialize");
451 assert_eq!(req.messages[0].role, "tool");
452 assert_eq!(req.messages[0].tool_call_id.as_deref(), Some("call_1"));
453 assert_eq!(req.messages[0].name.as_deref(), Some("search"));
454 }
455
456 #[test]
457 fn deserialize_tool_definitions() {
458 let json = r#"{
459 "model": "copilot",
460 "messages": [{"role": "user", "content": "hi"}],
461 "tools": [{
462 "type": "function",
463 "function": {
464 "name": "get_weather",
465 "description": "Get weather for a city",
466 "parameters": {"type": "object", "properties": {"city": {"type": "string"}}, "required": ["city"]}
467 }
468 }]
469 }"#;
470 let req: ChatCompletionRequest = serde_json::from_str(json).expect("deserialize");
471 let tools = req.tools.expect("tools present");
472 assert_eq!(tools.len(), 1);
473 assert_eq!(tools[0].tool_type, "function");
474 assert_eq!(tools[0].function.name, "get_weather");
475 assert!(tools[0].function.parameters.is_some());
476 }
477
478 #[test]
479 fn deserialize_tool_choice_auto() {
480 let json = r#"{"model":"copilot","messages":[{"role":"user","content":"hi"}],"tool_choice":"auto"}"#;
481 let req: ChatCompletionRequest = serde_json::from_str(json).expect("deserialize");
482 match req.tool_choice.expect("tool_choice present") {
483 ToolChoice::Mode(m) => assert_eq!(m, "auto"),
484 ToolChoice::Specific(_) => panic!("expected mode"),
485 }
486 }
487
488 #[test]
489 fn deserialize_tool_choice_specific() {
490 let json = r#"{"model":"copilot","messages":[{"role":"user","content":"hi"}],"tool_choice":{"type":"function","function":{"name":"get_weather"}}}"#;
491 let req: ChatCompletionRequest = serde_json::from_str(json).expect("deserialize");
492 match req.tool_choice.expect("tool_choice present") {
493 ToolChoice::Specific(s) => assert_eq!(s.function.name, "get_weather"),
494 ToolChoice::Mode(_) => panic!("expected specific"),
495 }
496 }
497
498 #[test]
499 fn serialize_completion_response() {
500 let resp = ChatCompletionResponse {
501 id: "chatcmpl-test".to_owned(),
502 object: "chat.completion",
503 created: 1_700_000_000,
504 model: "copilot:gpt-4o".to_owned(),
505 choices: vec![Choice {
506 index: 0,
507 message: ResponseMessage {
508 role: "assistant",
509 content: Some("Hello!".to_owned()),
510 tool_calls: None,
511 },
512 finish_reason: Some("stop".to_owned()),
513 }],
514 usage: None,
515 warnings: None,
516 };
517 let json = serde_json::to_string(&resp).expect("serialize");
518 assert!(json.contains("chat.completion"));
519 assert!(json.contains("Hello!"));
520 assert!(!json.contains("tool_calls"));
521 }
522
523 #[test]
524 fn serialize_response_with_tool_calls() {
525 let resp = ChatCompletionResponse {
526 id: "chatcmpl-test".to_owned(),
527 object: "chat.completion",
528 created: 1_700_000_000,
529 model: "copilot:gpt-4o".to_owned(),
530 choices: vec![Choice {
531 index: 0,
532 message: ResponseMessage {
533 role: "assistant",
534 content: None,
535 tool_calls: Some(vec![ToolCall {
536 index: 0,
537 id: "call_abc123".to_owned(),
538 tool_type: "function".to_owned(),
539 function: ToolCallFunction {
540 name: "get_weather".to_owned(),
541 arguments: r#"{"city":"Paris"}"#.to_owned(),
542 },
543 }]),
544 },
545 finish_reason: Some("tool_calls".to_owned()),
546 }],
547 usage: None,
548 warnings: None,
549 };
550 let json = serde_json::to_string(&resp).expect("serialize");
551 assert!(json.contains("tool_calls"));
552 assert!(json.contains("call_abc123"));
553 assert!(json.contains("get_weather"));
554 assert!(!json.contains(r#""content""#));
555 }
556
557 #[test]
558 fn serialize_error_response() {
559 let resp = ErrorResponse::new("invalid_request_error", "Unknown model");
560 let json = serde_json::to_string(&resp).expect("serialize");
561 assert!(json.contains("invalid_request_error"));
562 assert!(json.contains("Unknown model"));
563 }
564
565 #[test]
566 fn serialize_chunk_response() {
567 let chunk = ChatCompletionChunk {
568 id: "chatcmpl-test".to_owned(),
569 object: "chat.completion.chunk",
570 created: 1_700_000_000,
571 model: "copilot".to_owned(),
572 choices: vec![ChunkChoice {
573 index: 0,
574 delta: Delta {
575 role: None,
576 content: Some("token".to_owned()),
577 tool_calls: None,
578 },
579 finish_reason: None,
580 }],
581 };
582 let json = serde_json::to_string(&chunk).expect("serialize");
583 assert!(json.contains("chat.completion.chunk"));
584 assert!(json.contains("token"));
585 assert!(!json.contains("tool_calls"));
586 }
587
588 #[test]
589 fn deserialize_tool_choice_none() {
590 let json = r#"{"model":"copilot","messages":[{"role":"user","content":"hi"}],"tool_choice":"none"}"#;
591 let req: ChatCompletionRequest = serde_json::from_str(json).expect("deserialize");
592 match req.tool_choice.expect("tool_choice present") {
593 ToolChoice::Mode(m) => assert_eq!(m, "none"),
594 ToolChoice::Specific(_) => panic!("expected mode"),
595 }
596 }
597
598 #[test]
599 fn deserialize_tool_choice_required() {
600 let json = r#"{"model":"copilot","messages":[{"role":"user","content":"hi"}],"tool_choice":"required"}"#;
601 let req: ChatCompletionRequest = serde_json::from_str(json).expect("deserialize");
602 match req.tool_choice.expect("tool_choice present") {
603 ToolChoice::Mode(m) => assert_eq!(m, "required"),
604 ToolChoice::Specific(_) => panic!("expected mode"),
605 }
606 }
607
608 #[test]
609 fn deserialize_response_format_text() {
610 let json = r#"{"model":"copilot","messages":[{"role":"user","content":"hi"}],"response_format":{"type":"text"}}"#;
611 let req: ChatCompletionRequest = serde_json::from_str(json).expect("deserialize");
612 assert!(matches!(
613 req.response_format,
614 Some(ResponseFormatRequest::Text)
615 ));
616 }
617
618 #[test]
619 fn deserialize_response_format_json_object() {
620 let json = r#"{"model":"copilot","messages":[{"role":"user","content":"hi"}],"response_format":{"type":"json_object"}}"#;
621 let req: ChatCompletionRequest = serde_json::from_str(json).expect("deserialize");
622 assert!(matches!(
623 req.response_format,
624 Some(ResponseFormatRequest::JsonObject)
625 ));
626 }
627
628 #[test]
629 fn deserialize_response_format_json_schema() {
630 let json = r#"{
631 "model": "copilot",
632 "messages": [{"role": "user", "content": "hi"}],
633 "response_format": {
634 "type": "json_schema",
635 "json_schema": {
636 "name": "weather",
637 "schema": {"type": "object", "properties": {"temp": {"type": "number"}}}
638 }
639 }
640 }"#;
641 let req: ChatCompletionRequest = serde_json::from_str(json).expect("deserialize");
642 match req.response_format {
643 Some(ResponseFormatRequest::JsonSchema { json_schema }) => {
644 assert_eq!(json_schema.name, "weather");
645 assert!(json_schema.schema["properties"]["temp"].is_object());
646 }
647 other => panic!("expected JsonSchema, got: {other:?}"),
648 }
649 }
650
651 #[test]
652 fn serialize_models_response() {
653 let resp = ModelsResponse {
654 object: "list",
655 data: vec![ModelObject {
656 id: "copilot:gpt-4o".to_owned(),
657 object: "model",
658 owned_by: "copilot".to_owned(),
659 }],
660 };
661 let json = serde_json::to_string(&resp).expect("serialize");
662 assert!(json.contains("copilot:gpt-4o"));
663 }
664}