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