use std::collections::HashMap;
use serde::{Deserialize, Serialize};
#[derive(Debug, Deserialize)]
pub struct ChatCompletionRequest {
pub model: ModelField,
pub messages: Vec<ChatCompletionMessage>,
#[serde(default)]
pub stream: bool,
#[serde(default)]
pub temperature: Option<f32>,
#[serde(default)]
pub max_tokens: Option<u32>,
#[serde(default)]
pub strict_capabilities: Option<bool>,
#[serde(default)]
pub tools: Option<Vec<ToolDefinition>>,
#[serde(default)]
pub tool_choice: Option<ToolChoice>,
#[serde(default)]
pub tool_execution: Option<ToolExecutionMode>,
#[serde(default)]
pub response_format: Option<ResponseFormatRequest>,
#[serde(default)]
pub top_p: Option<f32>,
#[serde(default)]
pub stop: Option<StopField>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum ToolExecutionMode {
Client,
Server,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(untagged)]
pub enum StopField {
Single(String),
Multiple(Vec<String>),
}
const MAX_STOP_SEQUENCES: usize = 4;
impl StopField {
pub const fn len(&self) -> usize {
match self {
Self::Single(_) => 1,
Self::Multiple(v) => v.len(),
}
}
pub const fn is_empty(&self) -> bool {
matches!(self, Self::Multiple(v) if v.is_empty())
}
pub fn into_vec(self) -> Vec<String> {
match self {
Self::Single(s) => vec![s],
Self::Multiple(v) => v.into_iter().take(MAX_STOP_SEQUENCES).collect(),
}
}
pub fn to_bounded_vec(&self) -> Vec<String> {
match self {
Self::Single(s) => vec![s.clone()],
Self::Multiple(v) => v.iter().take(MAX_STOP_SEQUENCES).cloned().collect(),
}
}
}
#[derive(Debug, Clone, Deserialize)]
#[serde(tag = "type")]
pub enum ResponseFormatRequest {
#[serde(rename = "text")]
Text,
#[serde(rename = "json_object")]
JsonObject,
#[serde(rename = "json_schema")]
JsonSchema {
json_schema: JsonSchemaSpec,
},
}
#[derive(Debug, Clone, Deserialize)]
pub struct JsonSchemaSpec {
pub name: String,
pub schema: serde_json::Value,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(untagged)]
pub enum ModelField {
Single(String),
Multiple(Vec<String>),
}
#[derive(Debug, Clone, Deserialize)]
#[serde(untagged)]
pub enum MessageContent {
Text(String),
Parts(Vec<ContentPart>),
}
impl MessageContent {
pub fn as_text(&self) -> String {
match self {
Self::Text(s) => s.clone(),
Self::Parts(parts) => parts
.iter()
.filter_map(|p| match p {
ContentPart::Text { text } => Some(text.as_str()),
ContentPart::ImageUrl { .. } => None,
})
.collect::<Vec<_>>()
.join(""),
}
}
}
#[derive(Debug, Clone, Deserialize)]
#[serde(tag = "type")]
pub enum ContentPart {
#[serde(rename = "text")]
Text {
text: String,
},
#[serde(rename = "image_url")]
ImageUrl {
image_url: ImageUrlDetail,
},
}
#[derive(Debug, Clone, Deserialize)]
pub struct ImageUrlDetail {
pub url: String,
}
#[derive(Debug, Clone, Deserialize)]
pub struct ChatCompletionMessage {
pub role: String,
pub content: Option<MessageContent>,
#[serde(default)]
pub tool_calls: Option<Vec<ToolCall>>,
#[serde(default)]
pub tool_call_id: Option<String>,
#[serde(default)]
pub name: Option<String>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct ToolDefinition {
#[serde(rename = "type")]
pub tool_type: String,
pub function: FunctionObject,
}
#[derive(Debug, Clone, Deserialize)]
pub struct FunctionObject {
pub name: String,
#[serde(default)]
pub description: Option<String>,
#[serde(default)]
pub parameters: Option<serde_json::Value>,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(untagged)]
pub enum ToolChoice {
Mode(String),
Specific(ToolChoiceSpecific),
}
#[derive(Debug, Clone, Deserialize)]
pub struct ToolChoiceSpecific {
#[serde(rename = "type")]
pub tool_type: String,
pub function: ToolChoiceFunction,
}
#[derive(Debug, Clone, Deserialize)]
pub struct ToolChoiceFunction {
pub name: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToolCall {
#[serde(default)]
pub index: usize,
pub id: String,
#[serde(rename = "type")]
pub tool_type: String,
pub function: ToolCallFunction,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToolCallFunction {
pub name: String,
pub arguments: String,
}
#[derive(Debug, Serialize)]
pub struct ChatCompletionResponse {
pub id: String,
pub object: &'static str,
pub created: u64,
pub model: String,
pub choices: Vec<Choice>,
#[serde(skip_serializing_if = "Option::is_none")]
pub usage: Option<Usage>,
#[serde(skip_serializing_if = "Option::is_none")]
pub warnings: Option<Vec<String>>,
}
#[derive(Debug, Serialize)]
pub struct Choice {
pub index: u32,
pub message: ResponseMessage,
pub finish_reason: Option<String>,
}
#[derive(Debug, Serialize)]
pub struct ResponseMessage {
pub role: &'static str,
#[serde(skip_serializing_if = "Option::is_none")]
pub content: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tool_calls: Option<Vec<ToolCall>>,
}
#[derive(Debug, Serialize)]
pub struct Usage {
#[serde(rename = "prompt_tokens")]
pub prompt: u32,
#[serde(rename = "completion_tokens")]
pub completion: u32,
#[serde(rename = "total_tokens")]
pub total: u32,
}
#[derive(Debug, Serialize)]
pub struct ChatCompletionChunk {
pub id: String,
pub object: &'static str,
pub created: u64,
pub model: String,
pub choices: Vec<ChunkChoice>,
}
#[derive(Debug, Serialize)]
pub struct ChunkChoice {
pub index: u32,
pub delta: Delta,
pub finish_reason: Option<String>,
}
#[derive(Debug, Serialize)]
pub struct Delta {
#[serde(skip_serializing_if = "Option::is_none")]
pub role: Option<&'static str>,
#[serde(skip_serializing_if = "Option::is_none")]
pub content: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tool_calls: Option<Vec<ToolCall>>,
}
#[derive(Debug, Serialize)]
pub struct MultiplexResponse {
pub id: String,
pub object: &'static str,
pub created: u64,
pub results: Vec<MultiplexProviderResult>,
pub summary: String,
}
#[derive(Debug, Serialize)]
pub struct MultiplexProviderResult {
pub provider: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub model: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub content: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub error: Option<String>,
pub duration_ms: u64,
}
#[derive(Debug, Serialize)]
pub struct ModelsResponse {
pub object: &'static str,
pub data: Vec<ModelObject>,
}
#[derive(Debug, Serialize)]
pub struct ModelObject {
pub id: String,
pub object: &'static str,
pub owned_by: String,
}
#[derive(Debug, Serialize)]
pub struct HealthResponse {
pub status: &'static str,
pub providers: HashMap<String, String>,
}
#[derive(Debug, Serialize)]
pub struct ErrorResponse {
pub error: ErrorDetail,
}
#[derive(Debug, Serialize)]
pub struct ErrorDetail {
pub message: String,
#[serde(rename = "type")]
pub error_type: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub param: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub code: Option<String>,
}
impl ErrorResponse {
pub fn new(error_type: impl Into<String>, message: impl Into<String>) -> Self {
Self {
error: ErrorDetail {
message: message.into(),
error_type: error_type.into(),
param: None,
code: None,
},
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn deserialize_single_model() {
let json = r#"{"model":"copilot:gpt-4o","messages":[{"role":"user","content":"hi"}]}"#;
let req: ChatCompletionRequest = serde_json::from_str(json).expect("deserialize"); match req.model {
ModelField::Single(m) => assert_eq!(m, "copilot:gpt-4o"),
ModelField::Multiple(_) => unreachable!("expected single"), }
assert!(!req.stream);
}
#[test]
fn deserialize_multiple_models() {
let json = r#"{"model":["copilot:gpt-4o","claude:opus"],"messages":[{"role":"user","content":"hi"}]}"#;
let req: ChatCompletionRequest = serde_json::from_str(json).expect("deserialize"); match req.model {
ModelField::Multiple(models) => {
assert_eq!(models.len(), 2);
assert_eq!(models[0], "copilot:gpt-4o");
assert_eq!(models[1], "claude:opus");
}
ModelField::Single(_) => unreachable!("expected multiple"), }
}
#[test]
fn deserialize_with_stream_flag() {
let json =
r#"{"model":"copilot","messages":[{"role":"user","content":"hi"}],"stream":true}"#;
let req: ChatCompletionRequest = serde_json::from_str(json).expect("deserialize"); assert!(req.stream);
}
#[test]
fn deserialize_message_with_null_content() {
let json = r#"{"model":"copilot","messages":[{"role":"assistant","content":null,"tool_calls":[{"id":"call_1","type":"function","function":{"name":"search","arguments":"{}"}}]}]}"#;
let req: ChatCompletionRequest = serde_json::from_str(json).expect("deserialize"); assert!(req.messages[0].content.is_none());
assert!(req.messages[0].tool_calls.is_some());
}
#[test]
fn deserialize_message_without_content_field() {
let json = r#"{"model":"copilot","messages":[{"role":"tool","tool_call_id":"call_1","name":"search","content":"{\"result\":\"found\"}"}]}"#;
let req: ChatCompletionRequest = serde_json::from_str(json).expect("deserialize"); assert_eq!(req.messages[0].role, "tool");
assert_eq!(req.messages[0].tool_call_id.as_deref(), Some("call_1"));
assert_eq!(req.messages[0].name.as_deref(), Some("search"));
}
#[test]
fn deserialize_multipart_content() {
let json = r#"{
"model": "copilot",
"messages": [{
"role": "user",
"content": [
{"type": "text", "text": "What is in this image?"},
{"type": "image_url", "image_url": {"url": "data:image/png;base64,aGVsbG8="}}
]
}]
}"#;
let req: ChatCompletionRequest = serde_json::from_str(json).expect("deserialize"); let content = req.messages[0].content.as_ref().expect("content present"); match content {
MessageContent::Parts(parts) => {
assert_eq!(parts.len(), 2);
assert!(
matches!(&parts[0], ContentPart::Text { text } if text == "What is in this image?")
);
assert!(
matches!(&parts[1], ContentPart::ImageUrl { image_url } if image_url.url.contains("base64"))
);
}
MessageContent::Text(_) => unreachable!("expected Parts variant"), }
}
#[test]
fn message_content_as_text_plain_string() {
let content = MessageContent::Text("hello".to_owned());
assert_eq!(content.as_text(), "hello");
}
#[test]
fn message_content_as_text_multipart() {
let content = MessageContent::Parts(vec![
ContentPart::Text {
text: "describe ".to_owned(),
},
ContentPart::ImageUrl {
image_url: ImageUrlDetail {
url: "data:image/png;base64,abc".to_owned(),
},
},
ContentPart::Text {
text: "this image".to_owned(),
},
]);
assert_eq!(content.as_text(), "describe this image");
}
#[test]
fn deserialize_plain_string_content_backward_compat() {
let json = r#"{"model":"copilot","messages":[{"role":"user","content":"hi"}]}"#;
let req: ChatCompletionRequest = serde_json::from_str(json).expect("deserialize"); let content = req.messages[0].content.as_ref().expect("content present"); match content {
MessageContent::Text(s) => assert_eq!(s, "hi"),
MessageContent::Parts(_) => unreachable!("expected Text variant"), }
}
#[test]
fn deserialize_tool_definitions() {
let json = r#"{
"model": "copilot",
"messages": [{"role": "user", "content": "hi"}],
"tools": [{
"type": "function",
"function": {
"name": "get_weather",
"description": "Get weather for a city",
"parameters": {"type": "object", "properties": {"city": {"type": "string"}}, "required": ["city"]}
}
}]
}"#;
let req: ChatCompletionRequest = serde_json::from_str(json).expect("deserialize"); let tools = req.tools.expect("tools present"); assert_eq!(tools.len(), 1);
assert_eq!(tools[0].tool_type, "function");
assert_eq!(tools[0].function.name, "get_weather");
assert!(tools[0].function.parameters.is_some());
}
#[test]
fn deserialize_tool_choice_auto() {
let json = r#"{"model":"copilot","messages":[{"role":"user","content":"hi"}],"tool_choice":"auto"}"#;
let req: ChatCompletionRequest = serde_json::from_str(json).expect("deserialize"); let tool_choice = req.tool_choice.expect("tool_choice present"); match tool_choice {
ToolChoice::Mode(m) => assert_eq!(m, "auto"),
ToolChoice::Specific(_) => unreachable!("expected mode"), }
}
#[test]
fn deserialize_tool_choice_specific() {
let json = r#"{"model":"copilot","messages":[{"role":"user","content":"hi"}],"tool_choice":{"type":"function","function":{"name":"get_weather"}}}"#;
let req: ChatCompletionRequest = serde_json::from_str(json).expect("deserialize"); let tool_choice = req.tool_choice.expect("tool_choice present"); match tool_choice {
ToolChoice::Specific(s) => assert_eq!(s.function.name, "get_weather"),
ToolChoice::Mode(_) => unreachable!("expected specific"), }
}
#[test]
fn serialize_completion_response() {
let resp = ChatCompletionResponse {
id: "chatcmpl-test".to_owned(),
object: "chat.completion",
created: 1_700_000_000,
model: "copilot:gpt-4o".to_owned(),
choices: vec![Choice {
index: 0,
message: ResponseMessage {
role: "assistant",
content: Some("Hello!".to_owned()),
tool_calls: None,
},
finish_reason: Some("stop".to_owned()),
}],
usage: None,
warnings: None,
};
let json = serde_json::to_string(&resp).expect("serialize"); assert!(json.contains("chat.completion"));
assert!(json.contains("Hello!"));
assert!(!json.contains("tool_calls"));
}
#[test]
fn serialize_response_with_tool_calls() {
let resp = ChatCompletionResponse {
id: "chatcmpl-test".to_owned(),
object: "chat.completion",
created: 1_700_000_000,
model: "copilot:gpt-4o".to_owned(),
choices: vec![Choice {
index: 0,
message: ResponseMessage {
role: "assistant",
content: None,
tool_calls: Some(vec![ToolCall {
index: 0,
id: "call_abc123".to_owned(),
tool_type: "function".to_owned(),
function: ToolCallFunction {
name: "get_weather".to_owned(),
arguments: r#"{"city":"Paris"}"#.to_owned(),
},
}]),
},
finish_reason: Some("tool_calls".to_owned()),
}],
usage: None,
warnings: None,
};
let json = serde_json::to_string(&resp).expect("serialize"); assert!(json.contains("tool_calls"));
assert!(json.contains("call_abc123"));
assert!(json.contains("get_weather"));
assert!(!json.contains(r#""content""#));
}
#[test]
fn serialize_error_response() {
let resp = ErrorResponse::new("invalid_request_error", "Unknown model");
let json = serde_json::to_string(&resp).expect("serialize"); assert!(json.contains("invalid_request_error"));
assert!(json.contains("Unknown model"));
}
#[test]
fn serialize_chunk_response() {
let chunk = ChatCompletionChunk {
id: "chatcmpl-test".to_owned(),
object: "chat.completion.chunk",
created: 1_700_000_000,
model: "copilot".to_owned(),
choices: vec![ChunkChoice {
index: 0,
delta: Delta {
role: None,
content: Some("token".to_owned()),
tool_calls: None,
},
finish_reason: None,
}],
};
let json = serde_json::to_string(&chunk).expect("serialize"); assert!(json.contains("chat.completion.chunk"));
assert!(json.contains("token"));
assert!(!json.contains("tool_calls"));
}
#[test]
fn deserialize_tool_execution_server() {
let json = r#"{"model":"copilot","messages":[{"role":"user","content":"hi"}],"tool_execution":"server"}"#;
let req: ChatCompletionRequest = serde_json::from_str(json).expect("deserialize"); assert_eq!(req.tool_execution, Some(ToolExecutionMode::Server));
}
#[test]
fn deserialize_tool_execution_client() {
let json = r#"{"model":"copilot","messages":[{"role":"user","content":"hi"}],"tool_execution":"client"}"#;
let req: ChatCompletionRequest = serde_json::from_str(json).expect("deserialize"); assert_eq!(req.tool_execution, Some(ToolExecutionMode::Client));
}
#[test]
fn tool_execution_defaults_to_none() {
let json = r#"{"model":"copilot","messages":[{"role":"user","content":"hi"}]}"#;
let req: ChatCompletionRequest = serde_json::from_str(json).expect("deserialize"); assert_eq!(req.tool_execution, None);
}
#[test]
fn deserialize_tool_choice_none() {
let json = r#"{"model":"copilot","messages":[{"role":"user","content":"hi"}],"tool_choice":"none"}"#;
let req: ChatCompletionRequest = serde_json::from_str(json).expect("deserialize"); let tool_choice = req.tool_choice.expect("tool_choice present"); match tool_choice {
ToolChoice::Mode(m) => assert_eq!(m, "none"),
ToolChoice::Specific(_) => unreachable!("expected mode"), }
}
#[test]
fn deserialize_tool_choice_required() {
let json = r#"{"model":"copilot","messages":[{"role":"user","content":"hi"}],"tool_choice":"required"}"#;
let req: ChatCompletionRequest = serde_json::from_str(json).expect("deserialize"); let tool_choice = req.tool_choice.expect("tool_choice present"); match tool_choice {
ToolChoice::Mode(m) => assert_eq!(m, "required"),
ToolChoice::Specific(_) => unreachable!("expected mode"), }
}
#[test]
fn deserialize_response_format_text() {
let json = r#"{"model":"copilot","messages":[{"role":"user","content":"hi"}],"response_format":{"type":"text"}}"#;
let req: ChatCompletionRequest = serde_json::from_str(json).expect("deserialize"); assert!(matches!(
req.response_format,
Some(ResponseFormatRequest::Text)
));
}
#[test]
fn deserialize_response_format_json_object() {
let json = r#"{"model":"copilot","messages":[{"role":"user","content":"hi"}],"response_format":{"type":"json_object"}}"#;
let req: ChatCompletionRequest = serde_json::from_str(json).expect("deserialize"); assert!(matches!(
req.response_format,
Some(ResponseFormatRequest::JsonObject)
));
}
#[test]
fn deserialize_response_format_json_schema() {
let json = r#"{
"model": "copilot",
"messages": [{"role": "user", "content": "hi"}],
"response_format": {
"type": "json_schema",
"json_schema": {
"name": "weather",
"schema": {"type": "object", "properties": {"temp": {"type": "number"}}}
}
}
}"#;
let req: ChatCompletionRequest = serde_json::from_str(json).expect("deserialize"); match req.response_format {
Some(ResponseFormatRequest::JsonSchema { json_schema }) => {
assert_eq!(json_schema.name, "weather");
assert!(json_schema.schema["properties"]["temp"].is_object());
}
other => unreachable!("expected JsonSchema, got: {other:?}"), }
}
#[test]
fn deserialize_top_p() {
let json = r#"{"model":"copilot","messages":[{"role":"user","content":"hi"}],"top_p":0.9}"#;
let req: ChatCompletionRequest = serde_json::from_str(json).expect("deserialize"); assert_eq!(req.top_p, Some(0.9));
}
#[test]
fn deserialize_stop_single() {
let json =
r#"{"model":"copilot","messages":[{"role":"user","content":"hi"}],"stop":"END"}"#;
let req: ChatCompletionRequest = serde_json::from_str(json).expect("deserialize"); let stop = req.stop.expect("stop present"); assert_eq!(stop.into_vec(), vec!["END"]);
}
#[test]
fn deserialize_stop_array() {
let json = r#"{"model":"copilot","messages":[{"role":"user","content":"hi"}],"stop":["END","STOP"]}"#;
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"]);
}
#[test]
fn stop_field_len() {
let single = StopField::Single("END".to_owned());
assert_eq!(single.len(), 1);
let multiple = StopField::Multiple(vec!["A".to_owned(), "B".to_owned(), "C".to_owned()]);
assert_eq!(multiple.len(), 3);
}
#[test]
fn stop_field_into_vec_truncates_at_four() {
let oversized = StopField::Multiple((0..10).map(|i| format!("stop_{i}")).collect());
let result = oversized.into_vec();
assert_eq!(result.len(), 4);
assert_eq!(result[0], "stop_0");
assert_eq!(result[3], "stop_3");
}
#[test]
fn deserialize_all_optional_fields() {
let json = r#"{
"model": "copilot",
"messages": [{"role": "user", "content": "hi"}],
"temperature": 0.7,
"max_tokens": 100,
"top_p": 0.95,
"stop": ["END"],
"stream": true
}"#;
let req: ChatCompletionRequest = serde_json::from_str(json).expect("deserialize"); assert_eq!(req.temperature, Some(0.7));
assert_eq!(req.max_tokens, Some(100));
assert_eq!(req.top_p, Some(0.95));
assert!(req.stop.is_some());
assert!(req.stream);
}
#[test]
fn serialize_models_response() {
let resp = ModelsResponse {
object: "list",
data: vec![ModelObject {
id: "copilot:gpt-4o".to_owned(),
object: "model",
owned_by: "copilot".to_owned(),
}],
};
let json = serde_json::to_string(&resp).expect("serialize"); assert!(json.contains("copilot:gpt-4o"));
}
}