use crate::types::{CacheControl, CacheStrategy};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone)]
pub enum AnthropicAuth {
ApiKey(String),
OAuth {
access_token: String,
},
}
impl AnthropicAuth {
pub fn api_key(key: impl Into<String>) -> Self {
Self::ApiKey(key.into())
}
pub fn oauth(access_token: impl Into<String>) -> Self {
Self::OAuth {
access_token: access_token.into(),
}
}
pub fn is_empty(&self) -> bool {
match self {
Self::ApiKey(key) => key.is_empty(),
Self::OAuth { access_token } => access_token.is_empty(),
}
}
pub fn to_header(&self) -> (&'static str, String) {
match self {
Self::ApiKey(key) => ("x-api-key", key.clone()),
Self::OAuth { access_token } => ("authorization", format!("Bearer {}", access_token)),
}
}
}
#[derive(Debug, Clone)]
pub struct AnthropicConfig {
pub auth: AnthropicAuth,
pub base_url: String,
pub anthropic_version: String,
pub beta_features: Vec<String>,
pub default_cache_strategy: CacheStrategy,
}
pub const OAUTH_BETA_HEADER: &str = "oauth-2025-04-20,claude-code-20250219,interleaved-thinking-2025-05-14,fine-grained-tool-streaming-2025-05-14";
pub const CLAUDE_CODE_SYSTEM_PREFIX: &str =
"You are Claude Code, Anthropic's official CLI for Claude.";
impl AnthropicConfig {
pub fn new(api_key: impl Into<String>) -> Self {
Self {
auth: AnthropicAuth::api_key(api_key),
base_url: "https://api.anthropic.com/v1/".to_string(),
anthropic_version: "2023-06-01".to_string(),
beta_features: vec![],
default_cache_strategy: CacheStrategy::Auto,
}
}
pub fn with_oauth(access_token: impl Into<String>) -> Self {
Self {
auth: AnthropicAuth::oauth(access_token),
base_url: "https://api.anthropic.com/v1/".to_string(),
anthropic_version: "2023-06-01".to_string(),
beta_features: vec![OAUTH_BETA_HEADER.to_string()],
default_cache_strategy: CacheStrategy::Auto,
}
}
pub fn with_auth(auth: AnthropicAuth) -> Self {
let beta_features = match &auth {
AnthropicAuth::OAuth { .. } => vec![OAUTH_BETA_HEADER.to_string()],
AnthropicAuth::ApiKey(_) => vec![],
};
Self {
auth,
base_url: "https://api.anthropic.com/v1/".to_string(),
anthropic_version: "2023-06-01".to_string(),
beta_features,
default_cache_strategy: CacheStrategy::Auto,
}
}
#[deprecated(note = "Use auth field directly instead")]
pub fn api_key(&self) -> &str {
match &self.auth {
AnthropicAuth::ApiKey(key) => key,
AnthropicAuth::OAuth { .. } => "",
}
}
pub fn with_base_url(mut self, base_url: impl Into<String>) -> Self {
let mut url = base_url.into();
if url.ends_with("/messages") {
url = url.trim_end_matches("/messages").to_string();
} else if url.ends_with("/messages/") {
url = url.trim_end_matches("/messages/").to_string();
}
if !url.ends_with('/') {
url.push('/');
}
self.base_url = url;
self
}
pub fn with_version(mut self, version: impl Into<String>) -> Self {
self.anthropic_version = version.into();
self
}
pub fn with_beta_feature(mut self, feature: impl Into<String>) -> Self {
self.beta_features.push(feature.into());
self
}
pub fn with_cache_strategy(mut self, strategy: CacheStrategy) -> Self {
self.default_cache_strategy = strategy;
self
}
}
impl Default for AnthropicConfig {
fn default() -> Self {
Self {
auth: AnthropicAuth::api_key(
std::env::var("ANTHROPIC_API_KEY").unwrap_or_else(|_| String::new()),
),
base_url: "https://api.anthropic.com/v1/".to_string(),
anthropic_version: "2023-06-01".to_string(),
beta_features: vec![],
default_cache_strategy: CacheStrategy::Auto,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AnthropicCacheControl {
#[serde(rename = "type")]
pub type_: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub ttl: Option<String>,
}
impl From<&CacheControl> for AnthropicCacheControl {
fn from(cache: &CacheControl) -> Self {
match cache {
CacheControl::Ephemeral { ttl } => Self {
type_: "ephemeral".to_string(),
ttl: ttl.clone(),
},
}
}
}
impl AnthropicCacheControl {
pub fn ephemeral() -> Self {
Self {
type_: "ephemeral".to_string(),
ttl: None,
}
}
pub fn ephemeral_with_ttl(ttl: impl Into<String>) -> Self {
Self {
type_: "ephemeral".to_string(),
ttl: Some(ttl.into()),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AnthropicSystemBlock {
#[serde(rename = "type")]
pub type_: String,
pub text: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub cache_control: Option<AnthropicCacheControl>,
}
impl AnthropicSystemBlock {
pub fn new(text: impl Into<String>) -> Self {
Self {
type_: "text".to_string(),
text: text.into(),
cache_control: None,
}
}
pub fn with_ephemeral_cache(text: impl Into<String>) -> Self {
Self {
type_: "text".to_string(),
text: text.into(),
cache_control: Some(AnthropicCacheControl::ephemeral()),
}
}
pub fn text(text: impl Into<String>) -> Self {
Self::new(text)
}
pub fn with_cache_control(mut self, cache_control: AnthropicCacheControl) -> Self {
self.cache_control = Some(cache_control);
self
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum AnthropicSystemContent {
String(String),
Blocks(Vec<AnthropicSystemBlock>),
}
#[derive(Debug, Serialize)]
pub struct AnthropicRequest {
pub model: String,
pub messages: Vec<AnthropicMessage>,
pub max_tokens: u32,
#[serde(skip_serializing_if = "Option::is_none")]
pub system: Option<AnthropicSystemContent>,
#[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 metadata: Option<serde_json::Value>,
#[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 thinking: Option<AnthropicThinkingConfig>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tools: Option<Vec<serde_json::Value>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tool_choice: Option<serde_json::Value>,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct AnthropicThinkingConfig {
#[serde(rename = "type")]
pub type_: String,
pub budget_tokens: u32,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct AnthropicMessage {
pub role: String,
pub content: AnthropicMessageContent,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct AnthropicResponse {
pub id: String,
#[serde(rename = "type")]
pub type_: String,
pub role: String,
pub content: Vec<AnthropicContent>,
pub model: String,
pub stop_reason: Option<String>,
pub usage: AnthropicUsage,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum AnthropicContent {
Text {
text: String,
#[serde(skip_serializing_if = "Option::is_none")]
cache_control: Option<AnthropicCacheControl>,
},
Image {
source: AnthropicSource,
#[serde(skip_serializing_if = "Option::is_none")]
cache_control: Option<AnthropicCacheControl>,
},
Thinking {
thinking: String,
signature: String,
},
RedactedThinking {
data: String,
},
ToolUse {
id: String,
name: String,
input: serde_json::Value,
#[serde(skip_serializing_if = "Option::is_none")]
cache_control: Option<AnthropicCacheControl>,
},
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(skip_serializing_if = "Option::is_none")]
cache_control: Option<AnthropicCacheControl>,
},
}
#[derive(Debug, Serialize, Deserialize, Clone)]
#[serde(untagged)]
pub enum AnthropicMessageContent {
String(String),
Blocks(Vec<AnthropicContent>),
}
impl Default for AnthropicMessageContent {
fn default() -> Self {
AnthropicMessageContent::String(String::new())
}
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct AnthropicSource {
#[serde(rename = "type")]
pub type_: String, pub media_type: String,
pub data: String,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct AnthropicUsage {
#[serde(default)]
pub input_tokens: u32,
#[serde(default)]
pub output_tokens: u32,
#[serde(skip_serializing_if = "Option::is_none")]
pub cache_creation_input_tokens: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub cache_read_input_tokens: Option<u32>,
}
#[derive(Debug, Deserialize)]
pub struct AnthropicStreamEvent {
#[serde(rename = "type")]
pub type_: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub message: Option<AnthropicResponse>,
#[serde(skip_serializing_if = "Option::is_none")]
pub index: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub content_block: Option<AnthropicContent>,
#[serde(skip_serializing_if = "Option::is_none")]
pub delta: Option<AnthropicDelta>,
#[serde(skip_serializing_if = "Option::is_none")]
pub usage: Option<AnthropicUsage>,
#[serde(skip_serializing_if = "Option::is_none")]
pub error: Option<AnthropicError>,
}
#[derive(Debug, Deserialize)]
pub struct AnthropicDelta {
#[serde(rename = "type")]
pub type_: Option<String>,
pub text: Option<String>,
pub thinking: Option<String>,
pub _signature: Option<String>,
pub partial_json: Option<String>,
pub _stop_reason: Option<String>,
pub _stop_sequence: Option<String>,
}
#[derive(Debug, Deserialize)]
pub struct AnthropicError {
pub message: String,
}
pub fn infer_max_tokens(model: &str) -> u32 {
if model.contains("opus-4-5") || model.contains("sonnet-4") || model.contains("haiku-4") {
64000
} else if model.contains("opus-4") {
32000
} else if model.contains("3-5") {
8192
} else {
4096
}
}