Skip to main content

embacle_server/
openai_types.rs

1// ABOUTME: OpenAI-compatible request/response envelope types for the REST API
2// ABOUTME: Maps between OpenAI chat completion format and embacle ChatRequest/ChatResponse
3//
4// SPDX-License-Identifier: Apache-2.0
5// Copyright (c) 2026 dravr.ai
6
7use serde::{Deserialize, Serialize};
8
9// ============================================================================
10// Request Types
11// ============================================================================
12
13/// OpenAI-compatible chat completion request
14///
15/// Accepts either a single model string or an array of model strings
16/// for multiplex mode. Each model string may contain a provider prefix
17/// (e.g., "copilot:gpt-4o") parsed by the provider resolver.
18#[derive(Debug, Deserialize)]
19pub struct ChatCompletionRequest {
20    /// Model identifier(s) — single string or array for multiplex
21    pub model: ModelField,
22    /// Conversation messages
23    pub messages: Vec<ChatCompletionMessage>,
24    /// Whether to stream the response
25    #[serde(default)]
26    pub stream: bool,
27    /// Temperature for response randomness (0.0 - 2.0)
28    #[serde(default)]
29    pub temperature: Option<f32>,
30    /// Maximum tokens to generate
31    #[serde(default)]
32    pub max_tokens: Option<u32>,
33    /// Enable strict capability checking (reject unsupported parameters)
34    #[serde(default)]
35    pub strict_capabilities: Option<bool>,
36    /// Tool definitions for function calling
37    #[serde(default)]
38    pub tools: Option<Vec<ToolDefinition>>,
39    /// Controls which tools the model may call
40    #[serde(default)]
41    pub tool_choice: Option<ToolChoice>,
42    /// Controls the response format (text, `json_object`, or `json_schema`)
43    #[serde(default)]
44    pub response_format: Option<ResponseFormatRequest>,
45    /// Nucleus sampling parameter (0.0 - 1.0)
46    #[serde(default)]
47    pub top_p: Option<f32>,
48    /// Stop sequence(s) that halt generation — single string or array
49    #[serde(default)]
50    pub stop: Option<StopField>,
51}
52
53/// Stop field that accepts either a single string or an array of strings
54///
55/// Per `OpenAI` spec, the `stop` parameter can be a string or an array of
56/// up to 4 strings.
57#[derive(Debug, Clone, Deserialize)]
58#[serde(untagged)]
59pub enum StopField {
60    /// Single stop sequence
61    Single(String),
62    /// Multiple stop sequences
63    Multiple(Vec<String>),
64}
65
66impl StopField {
67    /// Convert to a Vec of strings regardless of the variant
68    pub fn into_vec(self) -> Vec<String> {
69        match self {
70            Self::Single(s) => vec![s],
71            Self::Multiple(v) => v,
72        }
73    }
74}
75
76/// OpenAI-compatible response format request
77#[derive(Debug, Clone, Deserialize)]
78#[serde(tag = "type")]
79pub enum ResponseFormatRequest {
80    /// Default text response
81    #[serde(rename = "text")]
82    Text,
83    /// Force JSON object output
84    #[serde(rename = "json_object")]
85    JsonObject,
86    /// Force JSON output conforming to a specific schema
87    #[serde(rename = "json_schema")]
88    JsonSchema {
89        /// The JSON schema specification
90        json_schema: JsonSchemaSpec,
91    },
92}
93
94/// JSON schema specification within a response format request
95#[derive(Debug, Clone, Deserialize)]
96pub struct JsonSchemaSpec {
97    /// Schema name for identification
98    pub name: String,
99    /// The JSON Schema definition
100    pub schema: serde_json::Value,
101}
102
103/// A model field that can be either a single string or an array of strings
104#[derive(Debug, Clone, Deserialize)]
105#[serde(untagged)]
106pub enum ModelField {
107    /// Single model string (standard `OpenAI`)
108    Single(String),
109    /// Array of model strings (multiplex extension)
110    Multiple(Vec<String>),
111}
112
113/// OpenAI-compatible message in a chat completion request
114#[derive(Debug, Clone, Deserialize)]
115pub struct ChatCompletionMessage {
116    /// Role: "system", "user", "assistant", or "tool"
117    pub role: String,
118    /// Message content (None for tool-call-only assistant messages)
119    pub content: Option<String>,
120    /// Tool calls requested by the assistant
121    #[serde(default)]
122    pub tool_calls: Option<Vec<ToolCall>>,
123    /// ID of the tool call this message responds to (role="tool")
124    #[serde(default)]
125    pub tool_call_id: Option<String>,
126    /// Function name for tool result messages
127    #[serde(default)]
128    pub name: Option<String>,
129}
130
131// ============================================================================
132// Tool Calling Types
133// ============================================================================
134
135/// A tool definition in the `OpenAI` format
136#[derive(Debug, Clone, Deserialize)]
137pub struct ToolDefinition {
138    /// Tool type (always "function" currently)
139    #[serde(rename = "type")]
140    pub tool_type: String,
141    /// Function definition
142    pub function: FunctionObject,
143}
144
145/// A function definition within a tool
146#[derive(Debug, Clone, Deserialize)]
147pub struct FunctionObject {
148    /// Name of the function
149    pub name: String,
150    /// Description of what the function does
151    #[serde(default)]
152    pub description: Option<String>,
153    /// JSON Schema for the function parameters
154    #[serde(default)]
155    pub parameters: Option<serde_json::Value>,
156}
157
158/// Controls which tools the model may call
159#[derive(Debug, Clone, Deserialize)]
160#[serde(untagged)]
161pub enum ToolChoice {
162    /// String variant: "none", "auto", or "required"
163    Mode(String),
164    /// Specific function variant: {"type": "function", "function": {"name": "..."}}
165    Specific(ToolChoiceSpecific),
166}
167
168/// A specific tool choice forcing a particular function
169#[derive(Debug, Clone, Deserialize)]
170pub struct ToolChoiceSpecific {
171    /// Tool type (always "function")
172    #[serde(rename = "type")]
173    pub tool_type: String,
174    /// Function to force
175    pub function: ToolChoiceFunction,
176}
177
178/// Function name within a specific tool choice
179#[derive(Debug, Clone, Deserialize)]
180pub struct ToolChoiceFunction {
181    /// Name of the function to call
182    pub name: String,
183}
184
185/// A tool call issued by the assistant
186#[derive(Debug, Clone, Serialize, Deserialize)]
187pub struct ToolCall {
188    /// Position index of this tool call in the array (required by `OpenAI` spec)
189    #[serde(default)]
190    pub index: usize,
191    /// Unique identifier for this tool call
192    pub id: String,
193    /// Tool type (always "function")
194    #[serde(rename = "type")]
195    pub tool_type: String,
196    /// Function call details
197    pub function: ToolCallFunction,
198}
199
200/// Function call details within a tool call
201#[derive(Debug, Clone, Serialize, Deserialize)]
202pub struct ToolCallFunction {
203    /// Name of the function to call
204    pub name: String,
205    /// JSON-encoded arguments
206    pub arguments: String,
207}
208
209// ============================================================================
210// Response Types (non-streaming)
211// ============================================================================
212
213/// OpenAI-compatible chat completion response
214#[derive(Debug, Serialize)]
215pub struct ChatCompletionResponse {
216    /// Unique response identifier
217    pub id: String,
218    /// Object type (always "chat.completion")
219    pub object: &'static str,
220    /// Unix timestamp of creation
221    pub created: u64,
222    /// Model used for generation
223    pub model: String,
224    /// Response choices (always one for embacle)
225    pub choices: Vec<Choice>,
226    /// Token usage statistics
227    #[serde(skip_serializing_if = "Option::is_none")]
228    pub usage: Option<Usage>,
229    /// Warnings about unsupported request parameters
230    #[serde(skip_serializing_if = "Option::is_none")]
231    pub warnings: Option<Vec<String>>,
232}
233
234/// A single choice in a chat completion response
235#[derive(Debug, Serialize)]
236pub struct Choice {
237    /// Choice index (always 0)
238    pub index: u32,
239    /// Generated message
240    pub message: ResponseMessage,
241    /// Reason the generation stopped
242    pub finish_reason: Option<String>,
243}
244
245/// Message in a chat completion response
246#[derive(Debug, Serialize)]
247pub struct ResponseMessage {
248    /// Role (always "assistant")
249    pub role: &'static str,
250    /// Generated content (None when `tool_calls` are present)
251    #[serde(skip_serializing_if = "Option::is_none")]
252    pub content: Option<String>,
253    /// Tool calls requested by the assistant
254    #[serde(skip_serializing_if = "Option::is_none")]
255    pub tool_calls: Option<Vec<ToolCall>>,
256}
257
258/// Token usage statistics
259#[derive(Debug, Serialize)]
260pub struct Usage {
261    /// Tokens in the prompt
262    #[serde(rename = "prompt_tokens")]
263    pub prompt: u32,
264    /// Tokens in the completion
265    #[serde(rename = "completion_tokens")]
266    pub completion: u32,
267    /// Total tokens
268    #[serde(rename = "total_tokens")]
269    pub total: u32,
270}
271
272// ============================================================================
273// Streaming Response Types
274// ============================================================================
275
276/// OpenAI-compatible streaming chunk
277#[derive(Debug, Serialize)]
278pub struct ChatCompletionChunk {
279    /// Unique response identifier (same across all chunks)
280    pub id: String,
281    /// Object type (always "chat.completion.chunk")
282    pub object: &'static str,
283    /// Unix timestamp of creation
284    pub created: u64,
285    /// Model used for generation
286    pub model: String,
287    /// Streaming choices
288    pub choices: Vec<ChunkChoice>,
289}
290
291/// A single choice in a streaming chunk
292#[derive(Debug, Serialize)]
293pub struct ChunkChoice {
294    /// Choice index (always 0)
295    pub index: u32,
296    /// Content delta
297    pub delta: Delta,
298    /// Reason the generation stopped (only on final chunk)
299    pub finish_reason: Option<String>,
300}
301
302/// Delta content in a streaming chunk
303#[derive(Debug, Serialize)]
304pub struct Delta {
305    /// Role (only present on first chunk)
306    #[serde(skip_serializing_if = "Option::is_none")]
307    pub role: Option<&'static str>,
308    /// Content token (empty string on role-only or final chunk)
309    #[serde(skip_serializing_if = "Option::is_none")]
310    pub content: Option<String>,
311    /// Tool calls (reserved for future streaming tool call support)
312    #[serde(skip_serializing_if = "Option::is_none")]
313    pub tool_calls: Option<Vec<ToolCall>>,
314}
315
316// ============================================================================
317// Multiplex Response (non-standard extension)
318// ============================================================================
319
320/// Response for multiplex requests (multiple providers)
321#[derive(Debug, Serialize)]
322pub struct MultiplexResponse {
323    /// Unique response identifier
324    pub id: String,
325    /// Object type (always "chat.completion.multiplex")
326    pub object: &'static str,
327    /// Unix timestamp of creation
328    pub created: u64,
329    /// Per-provider results
330    pub results: Vec<MultiplexProviderResult>,
331    /// Human-readable summary
332    pub summary: String,
333}
334
335/// Result from a single provider in a multiplex request
336#[derive(Debug, Serialize)]
337pub struct MultiplexProviderResult {
338    /// Provider identifier
339    pub provider: String,
340    /// Model used
341    #[serde(skip_serializing_if = "Option::is_none")]
342    pub model: Option<String>,
343    /// Response content (None on failure)
344    #[serde(skip_serializing_if = "Option::is_none")]
345    pub content: Option<String>,
346    /// Error message (None on success)
347    #[serde(skip_serializing_if = "Option::is_none")]
348    pub error: Option<String>,
349    /// Wall-clock time in milliseconds
350    pub duration_ms: u64,
351}
352
353// ============================================================================
354// Models Endpoint
355// ============================================================================
356
357/// Response for GET /v1/models
358#[derive(Debug, Serialize)]
359pub struct ModelsResponse {
360    /// Object type (always "list")
361    pub object: &'static str,
362    /// Available models
363    pub data: Vec<ModelObject>,
364}
365
366/// A single model entry in the models list
367#[derive(Debug, Serialize)]
368pub struct ModelObject {
369    /// Model identifier (e.g., "copilot:gpt-4o")
370    pub id: String,
371    /// Object type (always "model")
372    pub object: &'static str,
373    /// Owner/provider name
374    pub owned_by: String,
375}
376
377// ============================================================================
378// Health Endpoint
379// ============================================================================
380
381/// Response for GET /health
382#[derive(Debug, Serialize)]
383pub struct HealthResponse {
384    /// Overall status
385    pub status: &'static str,
386    /// Per-provider readiness
387    pub providers: std::collections::HashMap<String, String>,
388}
389
390// ============================================================================
391// Error Response
392// ============================================================================
393
394/// OpenAI-compatible error response
395#[derive(Debug, Serialize)]
396pub struct ErrorResponse {
397    /// Error details
398    pub error: ErrorDetail,
399}
400
401/// Error detail within an `OpenAI` error response
402#[derive(Debug, Serialize)]
403pub struct ErrorDetail {
404    /// Error message
405    pub message: String,
406    /// Error type
407    #[serde(rename = "type")]
408    pub error_type: String,
409    /// Parameter that caused the error (if applicable)
410    #[serde(skip_serializing_if = "Option::is_none")]
411    pub param: Option<String>,
412    /// Error code
413    #[serde(skip_serializing_if = "Option::is_none")]
414    pub code: Option<String>,
415}
416
417impl ErrorResponse {
418    /// Build an error response with the given type and message
419    pub fn new(error_type: impl Into<String>, message: impl Into<String>) -> Self {
420        Self {
421            error: ErrorDetail {
422                message: message.into(),
423                error_type: error_type.into(),
424                param: None,
425                code: None,
426            },
427        }
428    }
429}
430
431#[cfg(test)]
432mod tests {
433    use super::*;
434
435    #[test]
436    fn deserialize_single_model() {
437        let json = r#"{"model":"copilot:gpt-4o","messages":[{"role":"user","content":"hi"}]}"#;
438        let req: ChatCompletionRequest = serde_json::from_str(json).expect("deserialize");
439        match req.model {
440            ModelField::Single(m) => assert_eq!(m, "copilot:gpt-4o"),
441            ModelField::Multiple(_) => panic!("expected single"),
442        }
443        assert!(!req.stream);
444    }
445
446    #[test]
447    fn deserialize_multiple_models() {
448        let json = r#"{"model":["copilot:gpt-4o","claude:opus"],"messages":[{"role":"user","content":"hi"}]}"#;
449        let req: ChatCompletionRequest = serde_json::from_str(json).expect("deserialize");
450        match req.model {
451            ModelField::Multiple(models) => {
452                assert_eq!(models.len(), 2);
453                assert_eq!(models[0], "copilot:gpt-4o");
454                assert_eq!(models[1], "claude:opus");
455            }
456            ModelField::Single(_) => panic!("expected multiple"),
457        }
458    }
459
460    #[test]
461    fn deserialize_with_stream_flag() {
462        let json =
463            r#"{"model":"copilot","messages":[{"role":"user","content":"hi"}],"stream":true}"#;
464        let req: ChatCompletionRequest = serde_json::from_str(json).expect("deserialize");
465        assert!(req.stream);
466    }
467
468    #[test]
469    fn deserialize_message_with_null_content() {
470        let json = r#"{"model":"copilot","messages":[{"role":"assistant","content":null,"tool_calls":[{"id":"call_1","type":"function","function":{"name":"search","arguments":"{}"}}]}]}"#;
471        let req: ChatCompletionRequest = serde_json::from_str(json).expect("deserialize");
472        assert!(req.messages[0].content.is_none());
473        assert!(req.messages[0].tool_calls.is_some());
474    }
475
476    #[test]
477    fn deserialize_message_without_content_field() {
478        let json = r#"{"model":"copilot","messages":[{"role":"tool","tool_call_id":"call_1","name":"search","content":"{\"result\":\"found\"}"}]}"#;
479        let req: ChatCompletionRequest = serde_json::from_str(json).expect("deserialize");
480        assert_eq!(req.messages[0].role, "tool");
481        assert_eq!(req.messages[0].tool_call_id.as_deref(), Some("call_1"));
482        assert_eq!(req.messages[0].name.as_deref(), Some("search"));
483    }
484
485    #[test]
486    fn deserialize_tool_definitions() {
487        let json = r#"{
488            "model": "copilot",
489            "messages": [{"role": "user", "content": "hi"}],
490            "tools": [{
491                "type": "function",
492                "function": {
493                    "name": "get_weather",
494                    "description": "Get weather for a city",
495                    "parameters": {"type": "object", "properties": {"city": {"type": "string"}}, "required": ["city"]}
496                }
497            }]
498        }"#;
499        let req: ChatCompletionRequest = serde_json::from_str(json).expect("deserialize");
500        let tools = req.tools.expect("tools present");
501        assert_eq!(tools.len(), 1);
502        assert_eq!(tools[0].tool_type, "function");
503        assert_eq!(tools[0].function.name, "get_weather");
504        assert!(tools[0].function.parameters.is_some());
505    }
506
507    #[test]
508    fn deserialize_tool_choice_auto() {
509        let json = r#"{"model":"copilot","messages":[{"role":"user","content":"hi"}],"tool_choice":"auto"}"#;
510        let req: ChatCompletionRequest = serde_json::from_str(json).expect("deserialize");
511        match req.tool_choice.expect("tool_choice present") {
512            ToolChoice::Mode(m) => assert_eq!(m, "auto"),
513            ToolChoice::Specific(_) => panic!("expected mode"),
514        }
515    }
516
517    #[test]
518    fn deserialize_tool_choice_specific() {
519        let json = r#"{"model":"copilot","messages":[{"role":"user","content":"hi"}],"tool_choice":{"type":"function","function":{"name":"get_weather"}}}"#;
520        let req: ChatCompletionRequest = serde_json::from_str(json).expect("deserialize");
521        match req.tool_choice.expect("tool_choice present") {
522            ToolChoice::Specific(s) => assert_eq!(s.function.name, "get_weather"),
523            ToolChoice::Mode(_) => panic!("expected specific"),
524        }
525    }
526
527    #[test]
528    fn serialize_completion_response() {
529        let resp = ChatCompletionResponse {
530            id: "chatcmpl-test".to_owned(),
531            object: "chat.completion",
532            created: 1_700_000_000,
533            model: "copilot:gpt-4o".to_owned(),
534            choices: vec![Choice {
535                index: 0,
536                message: ResponseMessage {
537                    role: "assistant",
538                    content: Some("Hello!".to_owned()),
539                    tool_calls: None,
540                },
541                finish_reason: Some("stop".to_owned()),
542            }],
543            usage: None,
544            warnings: None,
545        };
546        let json = serde_json::to_string(&resp).expect("serialize");
547        assert!(json.contains("chat.completion"));
548        assert!(json.contains("Hello!"));
549        assert!(!json.contains("tool_calls"));
550    }
551
552    #[test]
553    fn serialize_response_with_tool_calls() {
554        let resp = ChatCompletionResponse {
555            id: "chatcmpl-test".to_owned(),
556            object: "chat.completion",
557            created: 1_700_000_000,
558            model: "copilot:gpt-4o".to_owned(),
559            choices: vec![Choice {
560                index: 0,
561                message: ResponseMessage {
562                    role: "assistant",
563                    content: None,
564                    tool_calls: Some(vec![ToolCall {
565                        index: 0,
566                        id: "call_abc123".to_owned(),
567                        tool_type: "function".to_owned(),
568                        function: ToolCallFunction {
569                            name: "get_weather".to_owned(),
570                            arguments: r#"{"city":"Paris"}"#.to_owned(),
571                        },
572                    }]),
573                },
574                finish_reason: Some("tool_calls".to_owned()),
575            }],
576            usage: None,
577            warnings: None,
578        };
579        let json = serde_json::to_string(&resp).expect("serialize");
580        assert!(json.contains("tool_calls"));
581        assert!(json.contains("call_abc123"));
582        assert!(json.contains("get_weather"));
583        assert!(!json.contains(r#""content""#));
584    }
585
586    #[test]
587    fn serialize_error_response() {
588        let resp = ErrorResponse::new("invalid_request_error", "Unknown model");
589        let json = serde_json::to_string(&resp).expect("serialize");
590        assert!(json.contains("invalid_request_error"));
591        assert!(json.contains("Unknown model"));
592    }
593
594    #[test]
595    fn serialize_chunk_response() {
596        let chunk = ChatCompletionChunk {
597            id: "chatcmpl-test".to_owned(),
598            object: "chat.completion.chunk",
599            created: 1_700_000_000,
600            model: "copilot".to_owned(),
601            choices: vec![ChunkChoice {
602                index: 0,
603                delta: Delta {
604                    role: None,
605                    content: Some("token".to_owned()),
606                    tool_calls: None,
607                },
608                finish_reason: None,
609            }],
610        };
611        let json = serde_json::to_string(&chunk).expect("serialize");
612        assert!(json.contains("chat.completion.chunk"));
613        assert!(json.contains("token"));
614        assert!(!json.contains("tool_calls"));
615    }
616
617    #[test]
618    fn deserialize_tool_choice_none() {
619        let json = r#"{"model":"copilot","messages":[{"role":"user","content":"hi"}],"tool_choice":"none"}"#;
620        let req: ChatCompletionRequest = serde_json::from_str(json).expect("deserialize");
621        match req.tool_choice.expect("tool_choice present") {
622            ToolChoice::Mode(m) => assert_eq!(m, "none"),
623            ToolChoice::Specific(_) => panic!("expected mode"),
624        }
625    }
626
627    #[test]
628    fn deserialize_tool_choice_required() {
629        let json = r#"{"model":"copilot","messages":[{"role":"user","content":"hi"}],"tool_choice":"required"}"#;
630        let req: ChatCompletionRequest = serde_json::from_str(json).expect("deserialize");
631        match req.tool_choice.expect("tool_choice present") {
632            ToolChoice::Mode(m) => assert_eq!(m, "required"),
633            ToolChoice::Specific(_) => panic!("expected mode"),
634        }
635    }
636
637    #[test]
638    fn deserialize_response_format_text() {
639        let json = r#"{"model":"copilot","messages":[{"role":"user","content":"hi"}],"response_format":{"type":"text"}}"#;
640        let req: ChatCompletionRequest = serde_json::from_str(json).expect("deserialize");
641        assert!(matches!(
642            req.response_format,
643            Some(ResponseFormatRequest::Text)
644        ));
645    }
646
647    #[test]
648    fn deserialize_response_format_json_object() {
649        let json = r#"{"model":"copilot","messages":[{"role":"user","content":"hi"}],"response_format":{"type":"json_object"}}"#;
650        let req: ChatCompletionRequest = serde_json::from_str(json).expect("deserialize");
651        assert!(matches!(
652            req.response_format,
653            Some(ResponseFormatRequest::JsonObject)
654        ));
655    }
656
657    #[test]
658    fn deserialize_response_format_json_schema() {
659        let json = r#"{
660            "model": "copilot",
661            "messages": [{"role": "user", "content": "hi"}],
662            "response_format": {
663                "type": "json_schema",
664                "json_schema": {
665                    "name": "weather",
666                    "schema": {"type": "object", "properties": {"temp": {"type": "number"}}}
667                }
668            }
669        }"#;
670        let req: ChatCompletionRequest = serde_json::from_str(json).expect("deserialize");
671        match req.response_format {
672            Some(ResponseFormatRequest::JsonSchema { json_schema }) => {
673                assert_eq!(json_schema.name, "weather");
674                assert!(json_schema.schema["properties"]["temp"].is_object());
675            }
676            other => panic!("expected JsonSchema, got: {other:?}"),
677        }
678    }
679
680    #[test]
681    fn deserialize_top_p() {
682        let json = r#"{"model":"copilot","messages":[{"role":"user","content":"hi"}],"top_p":0.9}"#;
683        let req: ChatCompletionRequest = serde_json::from_str(json).expect("deserialize");
684        assert_eq!(req.top_p, Some(0.9));
685    }
686
687    #[test]
688    fn deserialize_stop_single() {
689        let json =
690            r#"{"model":"copilot","messages":[{"role":"user","content":"hi"}],"stop":"END"}"#;
691        let req: ChatCompletionRequest = serde_json::from_str(json).expect("deserialize");
692        let stop = req.stop.expect("stop present");
693        assert_eq!(stop.into_vec(), vec!["END"]);
694    }
695
696    #[test]
697    fn deserialize_stop_array() {
698        let json = r#"{"model":"copilot","messages":[{"role":"user","content":"hi"}],"stop":["END","STOP"]}"#;
699        let req: ChatCompletionRequest = serde_json::from_str(json).expect("deserialize");
700        let stop = req.stop.expect("stop present");
701        assert_eq!(stop.into_vec(), vec!["END", "STOP"]);
702    }
703
704    #[test]
705    fn deserialize_all_optional_fields() {
706        let json = r#"{
707            "model": "copilot",
708            "messages": [{"role": "user", "content": "hi"}],
709            "temperature": 0.7,
710            "max_tokens": 100,
711            "top_p": 0.95,
712            "stop": ["END"],
713            "stream": true
714        }"#;
715        let req: ChatCompletionRequest = serde_json::from_str(json).expect("deserialize");
716        assert_eq!(req.temperature, Some(0.7));
717        assert_eq!(req.max_tokens, Some(100));
718        assert_eq!(req.top_p, Some(0.95));
719        assert!(req.stop.is_some());
720        assert!(req.stream);
721    }
722
723    #[test]
724    fn serialize_models_response() {
725        let resp = ModelsResponse {
726            object: "list",
727            data: vec![ModelObject {
728                id: "copilot:gpt-4o".to_owned(),
729                object: "model",
730                owned_by: "copilot".to_owned(),
731            }],
732        };
733        let json = serde_json::to_string(&resp).expect("serialize");
734        assert!(json.contains("copilot:gpt-4o"));
735    }
736}