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