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 #[serde(skip_serializing_if = "Option::is_none", default)]
166 thought_signature: Option<String>,
167 },
168 #[serde(rename = "tool_result")]
169 ToolResult {
170 tool_use_id: String,
171 content: String,
172 #[serde(skip_serializing_if = "Option::is_none")]
173 is_error: Option<bool>,
174 #[serde(skip_serializing_if = "Option::is_none")]
175 content_blocks: Option<Vec<serde_json::Value>>,
176 },
177 #[serde(rename = "server_tool_use")]
178 ServerToolUse {
179 id: String,
180 name: String,
181 input: serde_json::Value,
182 },
183 #[serde(rename = "tool_search_tool_result")]
184 ToolSearchToolResult {
185 tool_use_id: String,
186 content: serde_json::Value,
187 },
188 #[serde(rename = "code_execution_tool_result")]
189 CodeExecutionToolResult {
190 tool_use_id: String,
191 content: serde_json::Value,
192 },
193}
194
195impl ContentBlock {
196 #[must_use]
198 pub fn thinking(thinking: impl Into<String>) -> Self {
199 Self::Thinking {
200 thinking: thinking.into(),
201 signature: None,
202 state: None,
203 }
204 }
205}
206
207#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
209pub struct CacheControl {
210 #[serde(rename = "type")]
211 pub cache_type: String,
212}
213
214#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
216pub struct ToolCaller {
217 #[serde(rename = "type")]
218 pub caller_type: String,
219 #[serde(skip_serializing_if = "Option::is_none")]
220 pub tool_id: Option<String>,
221}
222
223#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
225pub struct Tool {
226 #[serde(rename = "type", skip_serializing_if = "Option::is_none")]
227 pub tool_type: Option<String>,
228 pub name: String,
229 pub description: String,
230 pub input_schema: serde_json::Value,
231 #[serde(skip_serializing_if = "Option::is_none")]
232 pub allowed_callers: Option<Vec<String>>,
233 #[serde(skip_serializing_if = "Option::is_none")]
234 pub defer_loading: Option<bool>,
235 #[serde(skip_serializing_if = "Option::is_none")]
236 pub input_examples: Option<Vec<serde_json::Value>>,
237 #[serde(skip_serializing_if = "Option::is_none")]
238 pub strict: Option<bool>,
239 #[serde(skip_serializing_if = "Option::is_none")]
240 pub cache_control: Option<CacheControl>,
241}
242
243#[cfg(test)]
244mod tests {
245 use super::*;
246 use serde_json::json;
247
248 fn primary_turn() -> PrimaryTurnRequest {
249 PrimaryTurnRequest {
250 model: "deepseek-v4-flash".to_string(),
251 messages: vec![Message {
252 role: "user".to_string(),
253 content: vec![ContentBlock::Text {
254 text: "inspect the request".to_string(),
255 cache_control: None,
256 }],
257 }],
258 max_tokens: 4096,
259 system: Some(SystemPrompt::Text("system".to_string())),
260 tools: Some(vec![Tool {
261 tool_type: None,
262 name: "read_file".to_string(),
263 description: "Read a file".to_string(),
264 input_schema: json!({"zeta": 1, "alpha": 2, "type": "object"}),
265 allowed_callers: None,
266 defer_loading: None,
267 input_examples: None,
268 strict: None,
269 cache_control: None,
270 }]),
271 tool_choice: Some(json!({"type": "auto"})),
272 reasoning_effort: Some("high".to_string()),
273 }
274 }
275
276 #[test]
277 fn primary_turn_preparation_has_stable_serialized_bytes() {
278 let first = prepare_primary_turn_request(primary_turn());
279 let second = prepare_primary_turn_request(primary_turn());
280 let first_bytes = serde_json::to_vec(&first).expect("serialize first request");
281 let second_bytes = serde_json::to_vec(&second).expect("serialize second request");
282
283 assert_eq!(first_bytes, second_bytes);
284 assert_eq!(
285 first_bytes,
286 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}"#
287 );
288 }
289
290 #[test]
291 fn primary_turn_preparation_owns_shared_defaults() {
292 let request = prepare_primary_turn_request(primary_turn());
293 assert_eq!(request.stream, Some(true));
294 assert!(request.metadata.is_none());
295 assert!(request.thinking.is_none());
296 assert!(request.temperature.is_none());
297 assert!(request.top_p.is_none());
298 }
299}