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