1use serde::{Deserialize, Serialize};
10
11#[derive(Debug, Serialize, Deserialize, Clone)]
13pub struct MessageRequest {
14 pub model: String,
15 pub messages: Vec<Message>,
16 pub max_tokens: u32,
17 #[serde(skip_serializing_if = "Option::is_none")]
18 pub system: Option<SystemPrompt>,
19 #[serde(skip_serializing_if = "Option::is_none")]
20 pub tools: Option<Vec<Tool>>,
21 #[serde(skip_serializing_if = "Option::is_none")]
22 pub tool_choice: Option<serde_json::Value>,
23 #[serde(skip_serializing_if = "Option::is_none")]
24 pub metadata: Option<serde_json::Value>,
25 #[serde(skip_serializing_if = "Option::is_none")]
26 pub thinking: Option<serde_json::Value>,
27 #[serde(skip_serializing_if = "Option::is_none")]
30 pub reasoning_effort: Option<String>,
31 #[serde(skip_serializing_if = "Option::is_none")]
32 pub stream: Option<bool>,
33 #[serde(skip_serializing_if = "Option::is_none")]
34 pub temperature: Option<f32>,
35 #[serde(skip_serializing_if = "Option::is_none")]
36 pub top_p: Option<f32>,
37}
38
39#[derive(Debug, Clone)]
46pub struct PrimaryTurnRequest {
47 pub model: String,
48 pub messages: Vec<Message>,
49 pub max_tokens: u32,
50 pub system: Option<SystemPrompt>,
51 pub tools: Option<Vec<Tool>>,
52 pub tool_choice: Option<serde_json::Value>,
53 pub reasoning_effort: Option<String>,
54}
55
56#[must_use]
62pub fn prepare_primary_turn_request(input: PrimaryTurnRequest) -> MessageRequest {
63 MessageRequest {
64 model: input.model,
65 messages: input.messages,
66 max_tokens: input.max_tokens,
67 system: input.system,
68 tools: input.tools,
69 tool_choice: input.tool_choice,
70 metadata: None,
71 thinking: None,
72 reasoning_effort: input.reasoning_effort,
73 stream: Some(true),
74 temperature: None,
75 top_p: None,
76 }
77}
78
79#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
81#[serde(untagged)]
82pub enum SystemPrompt {
83 Text(String),
84 Blocks(Vec<SystemBlock>),
85}
86
87#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
89pub struct SystemBlock {
90 #[serde(rename = "type")]
91 pub block_type: String,
92 pub text: String,
93 #[serde(skip_serializing_if = "Option::is_none")]
94 pub cache_control: Option<CacheControl>,
95}
96
97#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
99pub struct ImageUrlContent {
100 pub url: String,
101}
102
103#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
105pub struct Message {
106 pub role: String,
107 pub content: Vec<ContentBlock>,
108}
109
110pub const INTERRUPTED_ASSISTANT_ROLE: &str = "assistant_interrupted";
112pub const INTERRUPTED_ASSISTANT_CONTEXT_PREFIX: &str = "[The following assistant output was interrupted before completion and may be incomplete or wrong]\n";
114
115#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
119pub struct OpaqueReasoningState {
120 pub provider: String,
121 pub api: String,
122 pub model: String,
123 #[serde(skip_serializing_if = "Option::is_none")]
124 pub id: Option<String>,
125 pub encrypted_content: String,
126}
127
128#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
130#[serde(tag = "type")]
131pub enum ContentBlock {
132 #[serde(rename = "text")]
133 Text {
134 text: String,
135 #[serde(skip_serializing_if = "Option::is_none")]
136 cache_control: Option<CacheControl>,
137 },
138 #[serde(rename = "image_url")]
139 ImageUrl { image_url: ImageUrlContent },
140 #[serde(rename = "thinking")]
141 Thinking {
142 thinking: String,
143 #[serde(skip_serializing_if = "Option::is_none", default)]
148 signature: Option<String>,
149 #[serde(skip_serializing_if = "Option::is_none", default)]
152 state: Option<OpaqueReasoningState>,
153 },
154 #[serde(rename = "tool_use")]
155 ToolUse {
156 id: String,
157 name: String,
158 input: serde_json::Value,
159 #[serde(skip_serializing_if = "Option::is_none")]
160 caller: Option<ToolCaller>,
161 },
162 #[serde(rename = "tool_result")]
163 ToolResult {
164 tool_use_id: String,
165 content: String,
166 #[serde(skip_serializing_if = "Option::is_none")]
167 is_error: Option<bool>,
168 #[serde(skip_serializing_if = "Option::is_none")]
169 content_blocks: Option<Vec<serde_json::Value>>,
170 },
171 #[serde(rename = "server_tool_use")]
172 ServerToolUse {
173 id: String,
174 name: String,
175 input: serde_json::Value,
176 },
177 #[serde(rename = "tool_search_tool_result")]
178 ToolSearchToolResult {
179 tool_use_id: String,
180 content: serde_json::Value,
181 },
182 #[serde(rename = "code_execution_tool_result")]
183 CodeExecutionToolResult {
184 tool_use_id: String,
185 content: serde_json::Value,
186 },
187}
188
189impl ContentBlock {
190 #[must_use]
192 pub fn thinking(thinking: impl Into<String>) -> Self {
193 Self::Thinking {
194 thinking: thinking.into(),
195 signature: None,
196 state: None,
197 }
198 }
199}
200
201#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
203pub struct CacheControl {
204 #[serde(rename = "type")]
205 pub cache_type: String,
206}
207
208#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
210pub struct ToolCaller {
211 #[serde(rename = "type")]
212 pub caller_type: String,
213 #[serde(skip_serializing_if = "Option::is_none")]
214 pub tool_id: Option<String>,
215}
216
217#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
219pub struct Tool {
220 #[serde(rename = "type", skip_serializing_if = "Option::is_none")]
221 pub tool_type: Option<String>,
222 pub name: String,
223 pub description: String,
224 pub input_schema: serde_json::Value,
225 #[serde(skip_serializing_if = "Option::is_none")]
226 pub allowed_callers: Option<Vec<String>>,
227 #[serde(skip_serializing_if = "Option::is_none")]
228 pub defer_loading: Option<bool>,
229 #[serde(skip_serializing_if = "Option::is_none")]
230 pub input_examples: Option<Vec<serde_json::Value>>,
231 #[serde(skip_serializing_if = "Option::is_none")]
232 pub strict: Option<bool>,
233 #[serde(skip_serializing_if = "Option::is_none")]
234 pub cache_control: Option<CacheControl>,
235}
236
237#[cfg(test)]
238mod tests {
239 use super::*;
240 use serde_json::json;
241
242 fn primary_turn() -> PrimaryTurnRequest {
243 PrimaryTurnRequest {
244 model: "deepseek-v4-flash".to_string(),
245 messages: vec![Message {
246 role: "user".to_string(),
247 content: vec![ContentBlock::Text {
248 text: "inspect the request".to_string(),
249 cache_control: None,
250 }],
251 }],
252 max_tokens: 4096,
253 system: Some(SystemPrompt::Text("system".to_string())),
254 tools: Some(vec![Tool {
255 tool_type: None,
256 name: "read_file".to_string(),
257 description: "Read a file".to_string(),
258 input_schema: json!({"zeta": 1, "alpha": 2, "type": "object"}),
259 allowed_callers: None,
260 defer_loading: None,
261 input_examples: None,
262 strict: None,
263 cache_control: None,
264 }]),
265 tool_choice: Some(json!({"type": "auto"})),
266 reasoning_effort: Some("high".to_string()),
267 }
268 }
269
270 #[test]
271 fn primary_turn_preparation_has_stable_serialized_bytes() {
272 let first = prepare_primary_turn_request(primary_turn());
273 let second = prepare_primary_turn_request(primary_turn());
274 let first_bytes = serde_json::to_vec(&first).expect("serialize first request");
275 let second_bytes = serde_json::to_vec(&second).expect("serialize second request");
276
277 assert_eq!(first_bytes, second_bytes);
278 assert_eq!(
279 first_bytes,
280 br#"{"model":"deepseek-v4-flash","messages":[{"role":"user","content":[{"type":"text","text":"inspect the request"}]}],"max_tokens":4096,"system":"system","tools":[{"name":"read_file","description":"Read a file","input_schema":{"zeta":1,"alpha":2,"type":"object"}}],"tool_choice":{"type":"auto"},"reasoning_effort":"high","stream":true}"#
281 );
282 }
283
284 #[test]
285 fn primary_turn_preparation_owns_shared_defaults() {
286 let request = prepare_primary_turn_request(primary_turn());
287 assert_eq!(request.stream, Some(true));
288 assert!(request.metadata.is_none());
289 assert!(request.thinking.is_none());
290 assert!(request.temperature.is_none());
291 assert!(request.top_p.is_none());
292 }
293}