use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq)]
pub struct CacheControl {
#[serde(rename = "type")]
pub control_type: CacheControlType,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub ttl: Option<String>,
}
#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq)]
#[serde(rename_all = "lowercase")]
pub enum CacheControlType {
#[default]
Ephemeral,
#[serde(other)]
Unknown,
}
const MIN_TTL_SECONDS: u64 = 300;
const MAX_TTL_SECONDS: u64 = 3600;
impl CacheControl {
pub fn ttl_seconds(&self) -> u64 {
let raw = match self.ttl.as_deref() {
None => return MIN_TTL_SECONDS,
Some("5m") => 300,
Some("1h") => 3600,
Some(other) => match other.parse::<u64>() {
Ok(secs) => secs,
Err(_) => {
tracing::warn!("Unrecognized TTL '{}', defaulting to 300s", other);
return MIN_TTL_SECONDS;
}
},
};
raw.clamp(MIN_TTL_SECONDS, MAX_TTL_SECONDS)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SystemContent {
pub text: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub cache_control: Option<CacheControl>,
}
fn deserialize_system_prompt<'de, D>(deserializer: D) -> Result<Option<SystemContent>, D::Error>
where
D: serde::Deserializer<'de>,
{
#[derive(Deserialize)]
#[serde(untagged)]
enum SystemPrompt {
Text(String),
Blocks(Vec<SystemBlock>),
}
#[derive(Deserialize)]
struct SystemBlock {
text: String,
#[serde(default)]
cache_control: Option<CacheControl>,
}
let maybe: Option<SystemPrompt> = Option::deserialize(deserializer)?;
Ok(maybe.map(|sp| match sp {
SystemPrompt::Text(s) => SystemContent {
text: s,
cache_control: None,
},
SystemPrompt::Blocks(blocks) => {
let cache_control = blocks.iter().rev().find_map(|b| b.cache_control.clone());
let text = blocks
.into_iter()
.map(|b| b.text)
.collect::<Vec<_>>()
.join("\n");
SystemContent {
text,
cache_control,
}
}
}))
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AnthropicCreateMessageRequest {
pub model: String,
pub max_tokens: u32,
pub messages: Vec<AnthropicMessage>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub nvext: Option<serde_json::Value>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize_system_prompt"
)]
pub system: Option<SystemContent>,
#[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 = "Option::is_none")]
pub top_k: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub stop_sequences: Option<Vec<String>>,
#[serde(default)]
pub stream: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub metadata: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tools: Option<Vec<AnthropicTool>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tool_choice: Option<AnthropicToolChoice>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub cache_control: Option<CacheControl>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub thinking: Option<ThinkingConfig>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub service_tier: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub container: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub output_config: Option<serde_json::Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ThinkingConfig {
#[serde(rename = "type")]
pub thinking_type: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub budget_tokens: Option<u32>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AnthropicMessage {
pub role: AnthropicRole,
#[serde(flatten)]
pub content: AnthropicMessageContent,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum AnthropicRole {
User,
Assistant,
System,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum AnthropicMessageContent {
Text { content: String },
Blocks { content: Vec<AnthropicContentBlock> },
}
#[derive(Debug, Clone, Serialize)]
#[serde(tag = "type")]
pub enum AnthropicContentBlock {
#[serde(rename = "text")]
Text {
text: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
citations: Option<Vec<serde_json::Value>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
cache_control: Option<CacheControl>,
},
#[serde(rename = "image")]
Image { source: AnthropicImageSource },
#[serde(rename = "tool_use")]
ToolUse {
id: String,
name: String,
input: serde_json::Value,
#[serde(default, skip_serializing_if = "Option::is_none")]
cache_control: Option<CacheControl>,
},
#[serde(rename = "tool_result")]
ToolResult {
tool_use_id: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
content: Option<ToolResultContent>,
#[serde(skip_serializing_if = "Option::is_none")]
is_error: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
cache_control: Option<CacheControl>,
},
#[serde(rename = "thinking")]
Thinking {
thinking: String,
signature: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
cache_control: Option<CacheControl>,
},
#[serde(rename = "redacted_thinking")]
RedactedThinking { data: String },
#[serde(rename = "server_tool_use")]
ServerToolUse {
id: String,
name: String,
#[serde(default)]
input: serde_json::Value,
},
#[serde(rename = "web_search_tool_result")]
WebSearchToolResult {
tool_use_id: String,
#[serde(default)]
content: serde_json::Value,
},
#[serde(untagged)]
Other(serde_json::Value),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum ToolResultContent {
Text(String),
Blocks(Vec<ToolResultContentBlock>),
}
impl ToolResultContent {
pub fn into_text(self) -> String {
match self {
ToolResultContent::Text(s) => s,
ToolResultContent::Blocks(blocks) => blocks
.into_iter()
.filter_map(|b| match b {
ToolResultContentBlock::Text { text } => Some(text),
ToolResultContentBlock::Other(_) => None,
})
.collect::<Vec<_>>()
.join(""),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum ToolResultContentBlock {
Text {
text: String,
},
Other(serde_json::Value),
}
impl<'de> Deserialize<'de> for AnthropicContentBlock {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let value = serde_json::Value::deserialize(deserializer)?;
let block_type = value
.get("type")
.and_then(|t| t.as_str())
.unwrap_or("")
.to_string();
match block_type.as_str() {
"text" => {
let text = value
.get("text")
.and_then(|t| t.as_str())
.ok_or_else(|| serde::de::Error::missing_field("text"))?
.to_string();
let citations: Option<Vec<serde_json::Value>> = value
.get("citations")
.cloned()
.and_then(|v| serde_json::from_value(v).ok());
let cache_control: Option<CacheControl> = value
.get("cache_control")
.cloned()
.and_then(|v| serde_json::from_value(v).ok());
Ok(AnthropicContentBlock::Text {
text,
citations,
cache_control,
})
}
"image" => {
let source: AnthropicImageSource =
serde_json::from_value(value.get("source").cloned().unwrap_or_default())
.map_err(serde::de::Error::custom)?;
Ok(AnthropicContentBlock::Image { source })
}
"tool_use" => {
let id = value
.get("id")
.and_then(|v| v.as_str())
.ok_or_else(|| serde::de::Error::missing_field("id"))?
.to_string();
let name = value
.get("name")
.and_then(|v| v.as_str())
.ok_or_else(|| serde::de::Error::missing_field("name"))?
.to_string();
let input = value.get("input").cloned().unwrap_or(serde_json::json!({}));
let cache_control: Option<CacheControl> = value
.get("cache_control")
.cloned()
.and_then(|v| serde_json::from_value(v).ok());
Ok(AnthropicContentBlock::ToolUse {
id,
name,
input,
cache_control,
})
}
"tool_result" => {
let tool_use_id = value
.get("tool_use_id")
.and_then(|v| v.as_str())
.ok_or_else(|| serde::de::Error::missing_field("tool_use_id"))?
.to_string();
let content: Option<ToolResultContent> = value
.get("content")
.cloned()
.and_then(|v| serde_json::from_value(v).ok());
let is_error = value.get("is_error").and_then(|v| v.as_bool());
let cache_control: Option<CacheControl> = value
.get("cache_control")
.cloned()
.and_then(|v| serde_json::from_value(v).ok());
Ok(AnthropicContentBlock::ToolResult {
tool_use_id,
content,
is_error,
cache_control,
})
}
"thinking" => {
let thinking = value
.get("thinking")
.and_then(|v| v.as_str())
.ok_or_else(|| serde::de::Error::missing_field("thinking"))?
.to_string();
let signature = value
.get("signature")
.and_then(|v| v.as_str())
.ok_or_else(|| serde::de::Error::missing_field("signature"))?
.to_string();
let cache_control: Option<CacheControl> = value
.get("cache_control")
.cloned()
.and_then(|v| serde_json::from_value(v).ok());
Ok(AnthropicContentBlock::Thinking {
thinking,
signature,
cache_control,
})
}
"redacted_thinking" => {
let data = value
.get("data")
.and_then(|v| v.as_str())
.ok_or_else(|| serde::de::Error::missing_field("data"))?
.to_string();
Ok(AnthropicContentBlock::RedactedThinking { data })
}
"server_tool_use" => {
let id = value
.get("id")
.and_then(|v| v.as_str())
.ok_or_else(|| serde::de::Error::missing_field("id"))?
.to_string();
let name = value
.get("name")
.and_then(|v| v.as_str())
.ok_or_else(|| serde::de::Error::missing_field("name"))?
.to_string();
let input = value.get("input").cloned().unwrap_or(serde_json::json!({}));
Ok(AnthropicContentBlock::ServerToolUse { id, name, input })
}
"web_search_tool_result" => {
let tool_use_id = value
.get("tool_use_id")
.and_then(|v| v.as_str())
.ok_or_else(|| serde::de::Error::missing_field("tool_use_id"))?
.to_string();
let content = value
.get("content")
.cloned()
.unwrap_or(serde_json::json!([]));
Ok(AnthropicContentBlock::WebSearchToolResult {
tool_use_id,
content,
})
}
other => {
tracing::debug!(
"Unrecognized Anthropic content block type '{}', preserving as Other",
other
);
Ok(AnthropicContentBlock::Other(value))
}
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AnthropicImageSource {
#[serde(rename = "type")]
pub source_type: String,
pub media_type: String,
pub data: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AnthropicTool {
pub name: String,
#[serde(default, rename = "type", skip_serializing_if = "Option::is_none")]
pub tool_type: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub input_schema: Option<serde_json::Value>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub cache_control: Option<CacheControl>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum AnthropicToolChoice {
Named(AnthropicToolChoiceNamed),
Simple(AnthropicToolChoiceSimple),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AnthropicToolChoiceSimple {
#[serde(rename = "type")]
pub choice_type: AnthropicToolChoiceMode,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub disable_parallel_tool_use: Option<bool>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum AnthropicToolChoiceMode {
Auto,
Any,
None,
Tool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AnthropicToolChoiceNamed {
#[serde(rename = "type")]
pub choice_type: AnthropicToolChoiceMode,
pub name: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub disable_parallel_tool_use: Option<bool>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AnthropicMessageResponse {
pub id: String,
#[serde(rename = "type")]
pub object_type: String,
pub role: String,
pub content: Vec<AnthropicResponseContentBlock>,
pub model: String,
pub stop_reason: Option<AnthropicStopReason>,
pub stop_sequence: Option<String>,
pub usage: AnthropicUsage,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type")]
pub enum AnthropicResponseContentBlock {
#[serde(rename = "thinking")]
Thinking { thinking: String, signature: String },
#[serde(rename = "text")]
Text {
text: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
citations: Option<Vec<serde_json::Value>>,
},
#[serde(rename = "tool_use")]
ToolUse {
id: String,
name: String,
input: serde_json::Value,
},
#[serde(rename = "redacted_thinking")]
RedactedThinking { data: String },
#[serde(rename = "server_tool_use")]
ServerToolUse {
id: String,
name: String,
#[serde(default)]
input: serde_json::Value,
},
#[serde(rename = "web_search_tool_result")]
WebSearchToolResult {
tool_use_id: String,
#[serde(default)]
content: serde_json::Value,
},
#[serde(untagged)]
Other(serde_json::Value),
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct AnthropicUsage {
pub input_tokens: u32,
pub output_tokens: u32,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub cache_creation_input_tokens: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub cache_read_input_tokens: Option<u32>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum AnthropicStopReason {
EndTurn,
MaxTokens,
StopSequence,
ToolUse,
PauseTurn,
Refusal,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type")]
pub enum AnthropicStreamEvent {
#[serde(rename = "message_start")]
MessageStart { message: AnthropicMessageResponse },
#[serde(rename = "content_block_start")]
ContentBlockStart {
index: u32,
content_block: AnthropicResponseContentBlock,
},
#[serde(rename = "content_block_delta")]
ContentBlockDelta { index: u32, delta: AnthropicDelta },
#[serde(rename = "content_block_stop")]
ContentBlockStop { index: u32 },
#[serde(rename = "message_delta")]
MessageDelta {
delta: AnthropicMessageDeltaBody,
usage: AnthropicUsage,
},
#[serde(rename = "message_stop")]
MessageStop {},
#[serde(rename = "ping")]
Ping {},
#[serde(rename = "error")]
Error { error: AnthropicErrorBody },
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type")]
pub enum AnthropicDelta {
#[serde(rename = "thinking_delta")]
ThinkingDelta { thinking: String },
#[serde(rename = "text_delta")]
TextDelta { text: String },
#[serde(rename = "input_json_delta")]
InputJsonDelta { partial_json: String },
#[serde(rename = "signature_delta")]
SignatureDelta { signature: String },
#[serde(rename = "citations_delta")]
CitationsDelta { citation: serde_json::Value },
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AnthropicMessageDeltaBody {
pub stop_reason: Option<AnthropicStopReason>,
#[serde(skip_serializing_if = "Option::is_none")]
pub stop_sequence: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AnthropicErrorResponse {
#[serde(rename = "type")]
pub object_type: String,
pub error: AnthropicErrorBody,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AnthropicErrorBody {
#[serde(rename = "type")]
pub error_type: String,
pub message: String,
}
impl AnthropicErrorResponse {
pub fn invalid_request(message: impl Into<String>) -> Self {
Self {
object_type: "error".to_string(),
error: AnthropicErrorBody {
error_type: "invalid_request_error".to_string(),
message: message.into(),
},
}
}
pub fn api_error(message: impl Into<String>) -> Self {
Self {
object_type: "error".to_string(),
error: AnthropicErrorBody {
error_type: "api_error".to_string(),
message: message.into(),
},
}
}
pub fn not_found(message: impl Into<String>) -> Self {
Self {
object_type: "error".to_string(),
error: AnthropicErrorBody {
error_type: "not_found_error".to_string(),
message: message.into(),
},
}
}
}
#[derive(Debug, Clone, Deserialize)]
pub struct AnthropicCountTokensRequest {
pub model: String,
pub messages: Vec<AnthropicMessage>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize_system_prompt"
)]
pub system: Option<SystemContent>,
#[serde(default)]
pub tools: Option<Vec<AnthropicTool>>,
}
#[derive(Debug, Clone, Serialize)]
pub struct AnthropicCountTokensResponse {
pub input_tokens: u32,
}
impl AnthropicCountTokensRequest {
pub fn estimate_tokens(&self) -> u32 {
let mut total_len: usize = 0;
if let Some(system) = &self.system {
total_len += system.text.len();
}
for msg in &self.messages {
total_len += match msg.role {
AnthropicRole::User => 4,
AnthropicRole::Assistant => 9,
AnthropicRole::System => 6,
};
match &msg.content {
AnthropicMessageContent::Text { content } => total_len += content.len(),
AnthropicMessageContent::Blocks { content } => {
for block in content {
total_len += estimate_block_len(block);
}
}
}
}
if let Some(tools) = &self.tools {
for tool in tools {
total_len += tool.name.len();
if let Some(desc) = &tool.description {
total_len += desc.len();
}
if let Some(schema) = &tool.input_schema {
total_len += schema.to_string().len();
}
}
}
let tokens = total_len / 3;
if tokens == 0 && total_len > 0 {
1
} else {
tokens as u32
}
}
}
fn estimate_block_len(block: &AnthropicContentBlock) -> usize {
match block {
AnthropicContentBlock::Text { text, .. } => text.len(),
AnthropicContentBlock::ToolUse { name, input, .. } => name.len() + input.to_string().len(),
AnthropicContentBlock::ToolResult { content, .. } => content
.as_ref()
.map(|c| match c {
ToolResultContent::Text(s) => s.len(),
ToolResultContent::Blocks(blocks) => blocks
.iter()
.map(|b| match b {
ToolResultContentBlock::Text { text } => text.len(),
ToolResultContentBlock::Other(v) => v.to_string().len(),
})
.sum(),
})
.unwrap_or(0),
AnthropicContentBlock::Thinking { thinking, .. } => thinking.len(),
AnthropicContentBlock::RedactedThinking { data, .. } => data.len(),
AnthropicContentBlock::ServerToolUse { name, input, .. } => {
name.len() + input.to_string().len()
}
AnthropicContentBlock::WebSearchToolResult { content, .. } => content.to_string().len(),
AnthropicContentBlock::Image { .. } => 256, AnthropicContentBlock::Other(v) => v.to_string().len(),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn messages_request_keeps_nvext_opaque() {
let request: AnthropicCreateMessageRequest = serde_json::from_value(serde_json::json!({
"model": "test-model",
"max_tokens": 16,
"messages": [{"role": "user", "content": "hi"}],
"nvext": {
"unknown_future_extension": {"nested": true},
"agent_context": {"trajectory_id": 7}
}
}))
.unwrap();
let nvext = request.nvext.expect("opaque nvext value");
assert_eq!(nvext["unknown_future_extension"]["nested"], true);
assert_eq!(nvext["agent_context"]["trajectory_id"], 7);
}
}