use crate::{
client::OpenRouterClient,
error::{OpenRouterError, Result},
};
use serde::{Deserialize, Serialize};
use serde_json::Value;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AnthropicMessageRequest {
pub model: String,
pub max_tokens: u32,
pub messages: Vec<AnthropicMessageParam>,
#[serde(skip_serializing_if = "Option::is_none")]
pub system: Option<AnthropicSystemContent>,
#[serde(skip_serializing_if = "Option::is_none")]
pub metadata: Option<AnthropicMetadata>,
#[serde(skip_serializing_if = "Option::is_none")]
pub stop_sequences: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub stream: Option<bool>,
#[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 tools: Option<Vec<AnthropicTool>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tool_choice: Option<AnthropicToolChoice>,
#[serde(skip_serializing_if = "Option::is_none")]
pub thinking: Option<AnthropicThinking>,
#[serde(skip_serializing_if = "Option::is_none")]
pub service_tier: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub provider: Option<Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub plugins: Option<Vec<Value>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub user: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub session_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub models: Option<Vec<String>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum AnthropicSystemContent {
String(String),
Array(Vec<AnthropicTextContent>),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AnthropicMessageParam {
pub role: AnthropicRole,
#[serde(skip_serializing_if = "Option::is_none")]
pub content: Option<AnthropicMessageContent>,
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum AnthropicRole {
User,
Assistant,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum AnthropicMessageContent {
String(String),
Array(Vec<AnthropicContentItem>),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type")]
pub enum AnthropicContentItem {
#[serde(rename = "text")]
Text { text: String },
#[serde(rename = "image")]
Image { source: AnthropicImageSource },
#[serde(rename = "document")]
Document { source: AnthropicDocumentSource },
#[serde(rename = "tool_use")]
ToolUse {
id: String,
name: String,
#[serde(skip_serializing_if = "Option::is_none")]
input: Option<Value>,
},
#[serde(rename = "tool_result")]
ToolResult {
tool_use_id: String,
#[serde(skip_serializing_if = "Option::is_none")]
content: Option<AnthropicMessageContent>,
#[serde(skip_serializing_if = "Option::is_none")]
is_error: Option<bool>,
},
#[serde(rename = "thinking")]
Thinking { thinking: String, signature: String },
#[serde(rename = "redacted_thinking")]
RedactedThinking { data: String },
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type")]
pub enum AnthropicImageSource {
#[serde(rename = "base64")]
Base64 {
media_type: String,
data: String,
},
#[serde(rename = "url")]
Url { url: String },
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type")]
pub enum AnthropicDocumentSource {
#[serde(rename = "base64")]
Base64 { media_type: String, data: String },
#[serde(rename = "text")]
Text { media_type: String, data: String },
#[serde(rename = "content")]
Content { content: Vec<AnthropicContentItem> },
#[serde(rename = "url")]
Url { url: String },
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AnthropicMetadata {
#[serde(skip_serializing_if = "Option::is_none")]
pub user_id: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AnthropicTool {
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
pub input_schema: Value,
#[serde(skip_serializing_if = "Option::is_none")]
pub tool_type: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub cache_control: Option<AnthropicCacheControl>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type")]
pub enum AnthropicToolChoice {
#[serde(rename = "auto")]
Auto { #[serde(skip_serializing_if = "Option::is_none")] disable_parallel_tool_use: Option<bool> },
#[serde(rename = "any")]
Any { #[serde(skip_serializing_if = "Option::is_none")] disable_parallel_tool_use: Option<bool> },
#[serde(rename = "none")]
None,
#[serde(rename = "tool")]
Tool { name: String, #[serde(skip_serializing_if = "Option::is_none")] disable_parallel_tool_use: Option<bool> },
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type")]
pub enum AnthropicThinking {
#[serde(rename = "enabled")]
Enabled { budget_tokens: u32 },
#[serde(rename = "disabled")]
Disabled,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AnthropicCacheControl {
#[serde(rename = "type")]
pub cache_type: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub ttl: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AnthropicTextContent {
#[serde(rename = "type")]
pub content_type: String,
pub text: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AnthropicMessageResponse {
pub id: String,
#[serde(rename = "type")]
pub response_type: String,
pub role: String,
pub content: Vec<AnthropicResponseContent>,
pub model: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub stop_reason: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub stop_sequence: Option<String>,
pub usage: AnthropicUsage,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type")]
pub enum AnthropicResponseContent {
#[serde(rename = "text")]
Text { text: String },
#[serde(rename = "tool_use")]
ToolUse { id: String, name: String, #[serde(skip_serializing_if = "Option::is_none")] input: Option<Value> },
#[serde(rename = "thinking")]
Thinking { thinking: String, signature: String },
#[serde(rename = "redacted_thinking")]
RedactedThinking { data: String },
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AnthropicUsage {
pub input_tokens: f64,
pub output_tokens: f64,
#[serde(skip_serializing_if = "Option::is_none")]
pub cache_creation_input_tokens: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub cache_read_input_tokens: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub cache_creation: Option<AnthropicCacheCreation>,
#[serde(skip_serializing_if = "Option::is_none")]
pub server_tool_use: Option<AnthropicServerToolUse>,
#[serde(skip_serializing_if = "Option::is_none")]
pub service_tier: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AnthropicCacheCreation {
pub ephemeral_5m_input_tokens: f64,
pub ephemeral_1h_input_tokens: f64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AnthropicServerToolUse {
pub web_search_requests: f64,
}
pub struct AnthropicMessageBuilder {
request: AnthropicMessageRequest,
}
impl AnthropicMessageBuilder {
pub fn new(model: impl Into<String>, max_tokens: u32) -> Self {
Self {
request: AnthropicMessageRequest {
model: model.into(),
max_tokens,
messages: Vec::new(),
system: None,
metadata: None,
stop_sequences: None,
stream: None,
temperature: None,
top_p: None,
top_k: None,
tools: None,
tool_choice: None,
thinking: None,
service_tier: None,
provider: None,
plugins: None,
user: None,
session_id: None,
models: None,
},
}
}
pub fn message(mut self, role: AnthropicRole, content: impl Into<String>) -> Self {
self.request.messages.push(AnthropicMessageParam {
role,
content: Some(AnthropicMessageContent::String(content.into())),
name: None,
});
self
}
pub fn user_message(self, content: impl Into<String>) -> Self {
self.message(AnthropicRole::User, content)
}
pub fn assistant_message(self, content: impl Into<String>) -> Self {
self.message(AnthropicRole::Assistant, content)
}
pub fn system(mut self, system: impl Into<String>) -> Self {
self.request.system = Some(AnthropicSystemContent::String(system.into()));
self
}
pub fn temperature(mut self, temp: f32) -> Self {
self.request.temperature = Some(temp);
self
}
pub fn thinking(mut self, budget_tokens: u32) -> Self {
self.request.thinking = Some(AnthropicThinking::Enabled { budget_tokens });
self
}
pub fn tools(mut self, tools: Vec<AnthropicTool>) -> Self {
self.request.tools = Some(tools);
self
}
pub fn stream(mut self, stream: bool) -> Self {
self.request.stream = Some(stream);
self
}
pub fn build(self) -> AnthropicMessageRequest {
self.request
}
}
impl OpenRouterClient {
pub async fn create_anthropic_message(
&self,
request: AnthropicMessageRequest,
) -> Result<AnthropicMessageResponse> {
let url = format!("{}/messages", self.base_url);
let headers = self.build_headers()?;
let response = self
.client
.post(&url)
.headers(headers)
.json(&request)
.send()
.await
.map_err(OpenRouterError::HttpError)?;
let status = response.status();
if !status.is_success() {
let error_text = response.text().await.unwrap_or_default();
return Err(OpenRouterError::ApiError {
code: status.as_u16(),
message: error_text,
});
}
let result = response
.json::<AnthropicMessageResponse>()
.await
.map_err(OpenRouterError::HttpError)?;
Ok(result)
}
}