use serde::{Deserialize, Serialize};
use serde_json::Value;
#[derive(Debug, Serialize)]
pub struct MessagesRequest {
pub model: String,
pub max_tokens: u32,
#[serde(skip_serializing_if = "Option::is_none")]
pub system: Option<SystemPrompt>,
pub messages: Vec<MessageParam>,
#[serde(skip_serializing_if = "Option::is_none")]
pub temperature: Option<f32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub top_p: Option<f32>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub tools: Vec<Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tool_choice: Option<Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub stream: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub thinking: Option<ThinkingConfig>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub stop_sequences: Vec<String>,
}
#[derive(Debug, Serialize)]
pub struct ThinkingConfig {
#[serde(rename = "type")]
pub kind: &'static str,
pub budget_tokens: u32,
}
impl ThinkingConfig {
pub fn enabled(budget_tokens: u32) -> Self {
Self {
kind: "enabled",
budget_tokens,
}
}
}
#[derive(Debug, Serialize)]
#[serde(untagged)]
pub enum SystemPrompt {
Text(String),
Blocks(Vec<SystemBlock>),
}
#[derive(Debug, Serialize)]
pub struct SystemBlock {
#[serde(rename = "type")]
pub kind: &'static str,
pub text: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub cache_control: Option<CacheControl>,
}
#[derive(Debug, Serialize)]
pub struct CacheControl {
#[serde(rename = "type")]
pub kind: &'static str,
}
impl CacheControl {
pub fn ephemeral() -> Self {
Self { kind: "ephemeral" }
}
}
#[derive(Debug, Serialize)]
pub struct MessageParam {
pub role: String,
pub content: Vec<ContentBlock>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum ContentBlock {
Text {
text: String,
},
Image {
source: MediaSource,
},
Document {
source: MediaSource,
},
ToolUse {
id: String,
name: String,
input: Value,
},
ToolResult {
tool_use_id: String,
content: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
is_error: Option<bool>,
},
#[serde(other)]
Other,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum MediaSource {
Url { url: String },
Base64 { media_type: String, data: String },
}
#[derive(Debug, Serialize)]
pub struct MessagesTool {
pub name: String,
pub description: String,
pub input_schema: Value,
}
#[derive(Debug, Deserialize)]
pub struct MessagesResponse {
pub id: String,
pub model: String,
pub content: Vec<ContentBlock>,
pub stop_reason: Option<String>,
pub usage: MessagesUsage,
}
#[derive(Debug, Deserialize)]
pub struct MessagesUsage {
#[serde(default)]
pub input_tokens: u32,
#[serde(default)]
pub output_tokens: u32,
#[serde(default)]
pub cache_creation_input_tokens: Option<u32>,
#[serde(default)]
pub cache_read_input_tokens: Option<u32>,
}
#[derive(Debug, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum StreamEventPayload {
MessageStart {
message: StreamMessageStart,
},
ContentBlockStart {
index: usize,
content_block: StreamContentBlockStart,
},
ContentBlockDelta {
index: usize,
delta: StreamDelta,
},
ContentBlockStop {
index: usize,
},
MessageDelta {
delta: StreamMessageDelta,
#[serde(default)]
usage: Option<StreamMessageDeltaUsage>,
},
MessageStop,
Ping,
Error {
error: StreamErrorDetail,
},
#[serde(other)]
Unknown,
}
#[derive(Debug, Deserialize)]
pub struct StreamMessageStart {
pub id: String,
pub model: String,
pub usage: MessagesUsage,
}
#[derive(Debug, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum StreamContentBlockStart {
Text {
#[serde(default)]
text: String,
},
ToolUse {
id: String,
name: String,
},
#[serde(other)]
Other,
}
#[derive(Debug, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum StreamDelta {
TextDelta {
text: String,
},
InputJsonDelta {
partial_json: String,
},
#[serde(other)]
Other,
}
#[derive(Debug, Deserialize)]
pub struct StreamMessageDelta {
pub stop_reason: Option<String>,
}
#[derive(Debug, Deserialize)]
pub struct StreamMessageDeltaUsage {
#[serde(default)]
pub output_tokens: u32,
}
#[derive(Debug, Deserialize)]
pub struct StreamErrorDetail {
#[serde(rename = "type")]
pub kind: Option<String>,
pub message: String,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_response_containing_an_unrecognized_content_block_type_still_decodes() {
let json = r#"{
"id": "msg_1",
"model": "claude-3-5-haiku-20241022",
"content": [
{"type": "thinking", "thinking": "Let me think...", "signature": "abc"},
{"type": "text", "text": "The answer is 4."}
],
"stop_reason": "end_turn",
"usage": {"input_tokens": 10, "output_tokens": 5}
}"#;
let response: MessagesResponse =
serde_json::from_str(json).expect("should decode despite the unrecognized block");
assert!(matches!(response.content[0], ContentBlock::Other));
assert!(matches!(response.content[1], ContentBlock::Text { .. }));
}
}