1use crate::proxy::{LlmMessage, LlmRole, LlmUsage};
10use serde::{Deserialize, Serialize};
11
12pub mod models {
18 pub const OPUS_4_6: &str = "claude-opus-4-6";
20 pub const SONNET_4_6: &str = "claude-sonnet-4-6";
22 pub const HAIKU_4_5: &str = "claude-haiku-4-5";
24
25 pub const OPUS_4_5: &str = "claude-opus-4-5";
27 pub const SONNET_4_5: &str = "claude-sonnet-4-5";
28}
29
30#[derive(Debug, Clone, Serialize)]
36pub struct MessagesRequest {
37 pub model: String,
38 pub messages: Vec<Message>,
39 pub max_tokens: usize,
40 #[serde(skip_serializing_if = "Option::is_none")]
41 pub system: Option<SystemContent>,
42 #[serde(skip_serializing_if = "Option::is_none")]
43 pub temperature: Option<f32>,
44 #[serde(skip_serializing_if = "Option::is_none")]
45 pub top_p: Option<f32>,
46 #[serde(skip_serializing_if = "Option::is_none")]
47 pub top_k: Option<usize>,
48 #[serde(skip_serializing_if = "Option::is_none")]
49 pub stop_sequences: Option<Vec<String>>,
50 #[serde(skip_serializing_if = "Option::is_none")]
51 pub thinking: Option<ThinkingConfig>,
52 #[serde(skip_serializing_if = "Option::is_none")]
53 pub tools: Option<Vec<Tool>>,
54 #[serde(skip_serializing_if = "Option::is_none")]
55 pub tool_choice: Option<ToolChoice>,
56 #[serde(skip_serializing_if = "Option::is_none")]
57 pub output_config: Option<OutputConfig>,
58 #[serde(skip_serializing_if = "Option::is_none")]
59 pub metadata: Option<Metadata>,
60 pub stream: bool,
61}
62
63#[derive(Debug, Clone, Serialize, Deserialize)]
66#[serde(untagged)]
67pub enum SystemContent {
68 Text(String),
69 Blocks(Vec<SystemBlock>),
70}
71
72#[derive(Debug, Clone, Serialize, Deserialize)]
74pub struct SystemBlock {
75 #[serde(rename = "type")]
76 pub block_type: String, pub text: String,
78 #[serde(skip_serializing_if = "Option::is_none")]
79 pub cache_control: Option<CacheControl>,
80}
81
82#[derive(Debug, Clone, Serialize, Deserialize)]
88pub struct Message {
89 pub role: MessageRole,
90 pub content: MessageContent,
91}
92
93impl Message {
94 pub fn user(text: impl Into<String>) -> Self {
96 Self {
97 role: MessageRole::User,
98 content: MessageContent::Text(text.into()),
99 }
100 }
101
102 pub fn assistant(text: impl Into<String>) -> Self {
104 Self {
105 role: MessageRole::Assistant,
106 content: MessageContent::Text(text.into()),
107 }
108 }
109}
110
111#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
112#[serde(rename_all = "lowercase")]
113pub enum MessageRole {
114 User,
115 Assistant,
116}
117
118#[derive(Debug, Clone, Serialize, Deserialize)]
121#[serde(untagged)]
122pub enum MessageContent {
123 Text(String),
125 Blocks(Vec<ContentBlock>),
127}
128
129#[derive(Debug, Clone, Serialize, Deserialize)]
134#[serde(tag = "type", rename_all = "snake_case")]
135pub enum ContentBlock {
136 Text {
138 text: String,
139 #[serde(skip_serializing_if = "Option::is_none")]
140 cache_control: Option<CacheControl>,
141 },
142 Image {
144 source: ImageSource,
145 #[serde(skip_serializing_if = "Option::is_none")]
146 cache_control: Option<CacheControl>,
147 },
148 Thinking {
150 thinking: String,
151 #[serde(skip_serializing_if = "Option::is_none")]
152 signature: Option<String>,
153 },
154 RedactedThinking { data: String },
156 ToolUse {
158 id: String,
159 name: String,
160 input: serde_json::Value,
161 },
162 ToolResult {
164 tool_use_id: String,
165 #[serde(skip_serializing_if = "Option::is_none")]
166 content: Option<ToolResultContent>,
167 #[serde(skip_serializing_if = "Option::is_none")]
168 is_error: Option<bool>,
169 },
170}
171
172impl ContentBlock {
173 pub fn as_text(&self) -> Option<&str> {
175 match self {
176 ContentBlock::Text { text, .. } => Some(text),
177 _ => None,
178 }
179 }
180
181 pub fn as_thinking(&self) -> Option<&str> {
183 match self {
184 ContentBlock::Thinking { thinking, .. } => Some(thinking),
185 _ => None,
186 }
187 }
188}
189
190#[derive(Debug, Clone, Serialize, Deserialize)]
196#[serde(tag = "type", rename_all = "snake_case")]
197pub enum ImageSource {
198 Base64 {
199 media_type: String, data: String,
201 },
202 Url {
203 url: String,
204 },
205}
206
207#[derive(Debug, Clone, Serialize, Deserialize)]
213#[serde(untagged)]
214pub enum ToolResultContent {
215 Text(String),
216 Blocks(Vec<ContentBlock>),
217}
218
219#[derive(Debug, Clone, Serialize, Deserialize)]
228#[serde(tag = "type", rename_all = "snake_case")]
229pub enum ThinkingConfig {
230 Adaptive,
231 Enabled { budget_tokens: usize },
232 Disabled,
233}
234
235#[derive(Debug, Clone, Serialize, Deserialize)]
241pub struct Tool {
242 pub name: String,
243 #[serde(skip_serializing_if = "Option::is_none")]
244 pub description: Option<String>,
245 pub input_schema: serde_json::Value,
246 #[serde(skip_serializing_if = "Option::is_none")]
247 pub cache_control: Option<CacheControl>,
248 #[serde(skip_serializing_if = "Option::is_none")]
250 pub strict: Option<bool>,
251}
252
253#[derive(Debug, Clone, Serialize, Deserialize)]
255#[serde(tag = "type", rename_all = "snake_case")]
256pub enum ToolChoice {
257 Auto {
258 #[serde(skip_serializing_if = "Option::is_none")]
259 disable_parallel_tool_use: Option<bool>,
260 },
261 Any {
262 #[serde(skip_serializing_if = "Option::is_none")]
263 disable_parallel_tool_use: Option<bool>,
264 },
265 Tool {
266 name: String,
267 #[serde(skip_serializing_if = "Option::is_none")]
268 disable_parallel_tool_use: Option<bool>,
269 },
270 None,
271}
272
273#[derive(Debug, Clone, Serialize, Deserialize)]
279pub struct OutputConfig {
280 #[serde(skip_serializing_if = "Option::is_none")]
281 pub effort: Option<Effort>,
282 #[serde(skip_serializing_if = "Option::is_none")]
283 pub format: Option<OutputFormat>,
284}
285
286#[derive(Debug, Clone, Serialize, Deserialize)]
289#[serde(rename_all = "snake_case")]
290pub enum Effort {
291 Low,
292 Medium,
293 High,
294 Max,
295}
296
297#[derive(Debug, Clone, Serialize, Deserialize)]
299#[serde(tag = "type", rename_all = "snake_case")]
300pub enum OutputFormat {
301 JsonSchema { schema: serde_json::Value },
302}
303
304#[derive(Debug, Clone, Serialize, Deserialize)]
310pub struct CacheControl {
311 #[serde(rename = "type")]
312 pub control_type: String, #[serde(skip_serializing_if = "Option::is_none")]
314 pub ttl: Option<String>, }
316
317impl CacheControl {
318 pub fn ephemeral() -> Self {
320 Self {
321 control_type: "ephemeral".to_string(),
322 ttl: None,
323 }
324 }
325
326 pub fn ephemeral_1h() -> Self {
328 Self {
329 control_type: "ephemeral".to_string(),
330 ttl: Some("1h".to_string()),
331 }
332 }
333}
334
335#[derive(Debug, Clone, Serialize, Deserialize)]
337pub struct Metadata {
338 #[serde(skip_serializing_if = "Option::is_none")]
339 pub user_id: Option<String>,
340}
341
342#[derive(Debug, Clone, Deserialize, Serialize)]
348pub struct MessagesResponse {
349 pub id: String,
350 #[serde(rename = "type")]
351 pub response_type: String, pub role: String, pub content: Vec<ContentBlock>,
354 pub model: String,
355 pub stop_reason: Option<StopReason>,
356 pub stop_sequence: Option<String>,
357 pub usage: Usage,
358}
359
360impl MessagesResponse {
361 pub fn text(&self) -> Option<&str> {
363 self.content.iter().find_map(|b| b.as_text())
364 }
365
366 pub fn thinking(&self) -> Option<String> {
368 let parts: Vec<&str> = self
369 .content
370 .iter()
371 .filter_map(|b| b.as_thinking())
372 .collect();
373 if parts.is_empty() {
374 None
375 } else {
376 Some(parts.join("\n"))
377 }
378 }
379
380 pub fn has_tool_use(&self) -> bool {
382 self.stop_reason == Some(StopReason::ToolUse)
383 }
384
385 pub fn tool_calls(&self) -> Vec<(&str, &str, &serde_json::Value)> {
387 self.content
388 .iter()
389 .filter_map(|b| match b {
390 ContentBlock::ToolUse { id, name, input } => {
391 Some((id.as_str(), name.as_str(), input))
392 }
393 _ => None,
394 })
395 .collect()
396 }
397}
398
399#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
401#[serde(rename_all = "snake_case")]
402pub enum StopReason {
403 EndTurn,
404 MaxTokens,
405 StopSequence,
406 ToolUse,
407 PauseTurn,
408 Refusal,
409}
410
411#[derive(Debug, Clone, Deserialize, Serialize)]
413pub struct Usage {
414 pub input_tokens: usize,
415 pub output_tokens: usize,
416 #[serde(skip_serializing_if = "Option::is_none")]
417 pub cache_creation_input_tokens: Option<usize>,
418 #[serde(skip_serializing_if = "Option::is_none")]
419 pub cache_read_input_tokens: Option<usize>,
420}
421
422impl From<LlmMessage> for Message {
427 fn from(msg: LlmMessage) -> Self {
428 Self {
429 role: match msg.role {
430 LlmRole::System => MessageRole::User, LlmRole::User => MessageRole::User,
432 LlmRole::Assistant => MessageRole::Assistant,
433 },
434 content: MessageContent::Text(msg.content),
435 }
436 }
437}
438
439impl From<Usage> for LlmUsage {
440 fn from(u: Usage) -> Self {
441 Self {
442 prompt_tokens: u.input_tokens,
443 completion_tokens: u.output_tokens,
444 total_tokens: u.input_tokens + u.output_tokens,
445 }
446 }
447}