Skip to main content

llm_trait/
request.rs

1//! Chat request types.
2
3use serde_json::Value;
4
5use super::message::ChatMessage;
6use super::reasoning::ReasoningConfig;
7
8/// Response format configuration.
9#[derive(Clone, Debug)]
10pub enum ResponseFormat {
11    JsonObject,
12    JsonSchema { name: String, schema: Value },
13}
14
15impl ResponseFormat {
16    pub fn to_api_value(&self) -> Value {
17        match self {
18            ResponseFormat::JsonObject => {
19                serde_json::json!({ "type": "json_object" })
20            }
21            ResponseFormat::JsonSchema { name, schema } => {
22                serde_json::json!({
23                    "type": "json_schema",
24                    "json_schema": {
25                        "name": name,
26                        "schema": schema,
27                    }
28                })
29            }
30        }
31    }
32}
33
34/// Unified chat request format.
35#[derive(Debug, Clone)]
36pub struct ChatRequest {
37    /// Message list (supports multimodal content)
38    pub messages: Vec<ChatMessage>,
39
40    /// Available tools
41    pub tools: Vec<Value>,
42
43    /// Reasoning/thinking configuration
44    pub reasoning: Option<ReasoningConfig>,
45
46    /// Response format configuration
47    pub response_format: Option<ResponseFormat>,
48
49    /// Optional model override.
50    ///
51    /// - `None`: Use the provider's default model
52    /// - `Some(name)`: Use the specified model for this request
53    ///
54    /// This enables multi-model routing: the application can set different
55    /// models for different scenes (e.g., "lite" for sub-agents, "advanced"
56    /// for architecture decisions) without creating multiple providers.
57    pub model: Option<String>,
58}
59
60impl ChatRequest {
61    /// Create a simple text request.
62    pub fn new(messages: Vec<ChatMessage>) -> Self {
63        Self {
64            messages,
65            tools: Vec::new(),
66            reasoning: None,
67            response_format: None,
68            model: None,
69        }
70    }
71
72    /// Set model override.
73    pub fn with_model(mut self, model: impl Into<String>) -> Self {
74        self.model = Some(model.into());
75        self
76    }
77
78    /// Add tools.
79    pub fn with_tools(mut self, tools: Vec<Value>) -> Self {
80        self.tools = tools;
81        self
82    }
83
84    /// Add reasoning configuration.
85    pub fn with_reasoning(mut self, reasoning: ReasoningConfig) -> Self {
86        self.reasoning = Some(reasoning);
87        self
88    }
89
90    /// Set response format.
91    pub fn with_response_format(mut self, format: ResponseFormat) -> Self {
92        self.response_format = Some(format);
93        self
94    }
95}
96
97#[cfg(test)]
98mod response_format_tests {
99    use super::*;
100
101    #[test]
102    fn json_object_to_api_value() {
103        assert_eq!(
104            ResponseFormat::JsonObject.to_api_value(),
105            serde_json::json!({ "type": "json_object" })
106        );
107    }
108
109    #[test]
110    fn json_schema_to_api_value() {
111        let schema = serde_json::json!({"type": "object"});
112        let value = ResponseFormat::JsonSchema {
113            name: "answer".to_string(),
114            schema: schema.clone(),
115        }
116        .to_api_value();
117
118        assert_eq!(value["type"], "json_schema");
119        assert_eq!(value["json_schema"]["name"], "answer");
120        assert_eq!(value["json_schema"]["schema"], schema);
121    }
122
123    #[test]
124    fn with_response_format_stores_the_format() {
125        let req = ChatRequest::new(vec![ChatMessage::user("hi")])
126            .with_response_format(ResponseFormat::JsonObject);
127        assert!(matches!(
128            req.response_format,
129            Some(ResponseFormat::JsonObject)
130        ));
131    }
132}
133
134#[cfg(test)]
135mod tests {
136    use super::*;
137
138    #[test]
139    fn chat_request_new() {
140        let req = ChatRequest::new(vec![ChatMessage::user("hello")]);
141        assert_eq!(req.messages.len(), 1);
142        assert!(req.tools.is_empty());
143        assert!(req.reasoning.is_none());
144        assert!(req.response_format.is_none());
145        assert!(req.model.is_none());
146    }
147
148    #[test]
149    fn chat_request_with_model() {
150        let req = ChatRequest::new(vec![ChatMessage::user("hello")]).with_model("mimo-v2.5-pro");
151        assert_eq!(req.model.as_deref(), Some("mimo-v2.5-pro"));
152    }
153
154    #[test]
155    fn chat_request_with_tools() {
156        let tools = vec![serde_json::json!({"type": "function", "function": {"name": "test"}})];
157        let req = ChatRequest::new(vec![ChatMessage::user("hello")]).with_tools(tools.clone());
158        assert_eq!(req.tools.len(), 1);
159        assert_eq!(req.tools[0], tools[0]);
160    }
161
162    #[test]
163    fn chat_request_with_reasoning() {
164        let reasoning = ReasoningConfig {
165            enabled: Some(true),
166            budget_tokens: Some(4096),
167            effort: None,
168        };
169        let req = ChatRequest::new(vec![ChatMessage::user("hello")]).with_reasoning(reasoning);
170        assert!(req.reasoning.is_some());
171        assert_eq!(req.reasoning.as_ref().unwrap().budget_tokens, Some(4096));
172    }
173
174    #[test]
175    fn chat_request_builder_chain() {
176        let tools = vec![serde_json::json!({"name": "tool1"})];
177        let reasoning = ReasoningConfig {
178            enabled: Some(true),
179            ..Default::default()
180        };
181        let req = ChatRequest::new(vec![ChatMessage::user("hello")])
182            .with_tools(tools)
183            .with_reasoning(reasoning);
184        assert_eq!(req.tools.len(), 1);
185        assert!(req.reasoning.is_some());
186    }
187}