anthropic.rs

  1use std::str::FromStr;
  2use std::time::Duration;
  3
  4use anyhow::{Context as _, Result, anyhow};
  5use chrono::{DateTime, Utc};
  6use futures::{AsyncBufReadExt, AsyncReadExt, StreamExt, io::BufReader, stream::BoxStream};
  7use http_client::http::{HeaderMap, HeaderValue};
  8use http_client::{AsyncBody, HttpClient, Method, Request as HttpRequest};
  9use serde::{Deserialize, Serialize};
 10use strum::{EnumIter, EnumString};
 11use thiserror::Error;
 12
 13pub const ANTHROPIC_API_URL: &str = "https://api.anthropic.com";
 14
 15#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
 16#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)]
 17pub struct AnthropicModelCacheConfiguration {
 18    pub min_total_token: u64,
 19    pub should_speculate: bool,
 20    pub max_cache_anchors: usize,
 21}
 22
 23#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
 24#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)]
 25pub enum AnthropicModelMode {
 26    #[default]
 27    Default,
 28    Thinking {
 29        budget_tokens: Option<u32>,
 30    },
 31}
 32
 33#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
 34#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, EnumIter)]
 35pub enum Model {
 36    #[serde(rename = "claude-opus-4", alias = "claude-opus-4-latest")]
 37    ClaudeOpus4,
 38    #[serde(
 39        rename = "claude-opus-4-thinking",
 40        alias = "claude-opus-4-thinking-latest"
 41    )]
 42    ClaudeOpus4Thinking,
 43    #[default]
 44    #[serde(rename = "claude-sonnet-4", alias = "claude-sonnet-4-latest")]
 45    ClaudeSonnet4,
 46    #[serde(
 47        rename = "claude-sonnet-4-thinking",
 48        alias = "claude-sonnet-4-thinking-latest"
 49    )]
 50    ClaudeSonnet4Thinking,
 51    #[serde(rename = "claude-3-7-sonnet", alias = "claude-3-7-sonnet-latest")]
 52    Claude3_7Sonnet,
 53    #[serde(
 54        rename = "claude-3-7-sonnet-thinking",
 55        alias = "claude-3-7-sonnet-thinking-latest"
 56    )]
 57    Claude3_7SonnetThinking,
 58    #[serde(rename = "claude-3-5-sonnet", alias = "claude-3-5-sonnet-latest")]
 59    Claude3_5Sonnet,
 60    #[serde(rename = "claude-3-5-haiku", alias = "claude-3-5-haiku-latest")]
 61    Claude3_5Haiku,
 62    #[serde(rename = "claude-3-opus", alias = "claude-3-opus-latest")]
 63    Claude3Opus,
 64    #[serde(rename = "claude-3-sonnet", alias = "claude-3-sonnet-latest")]
 65    Claude3Sonnet,
 66    #[serde(rename = "claude-3-haiku", alias = "claude-3-haiku-latest")]
 67    Claude3Haiku,
 68    #[serde(rename = "custom")]
 69    Custom {
 70        name: String,
 71        max_tokens: u64,
 72        /// The name displayed in the UI, such as in the assistant panel model dropdown menu.
 73        display_name: Option<String>,
 74        /// Override this model with a different Anthropic model for tool calls.
 75        tool_override: Option<String>,
 76        /// Indicates whether this custom model supports caching.
 77        cache_configuration: Option<AnthropicModelCacheConfiguration>,
 78        max_output_tokens: Option<u64>,
 79        default_temperature: Option<f32>,
 80        #[serde(default)]
 81        extra_beta_headers: Vec<String>,
 82        #[serde(default)]
 83        mode: AnthropicModelMode,
 84    },
 85}
 86
 87impl Model {
 88    pub fn default_fast() -> Self {
 89        Self::Claude3_5Haiku
 90    }
 91
 92    pub fn from_id(id: &str) -> Result<Self> {
 93        if id.starts_with("claude-opus-4-thinking") {
 94            return Ok(Self::ClaudeOpus4Thinking);
 95        }
 96
 97        if id.starts_with("claude-opus-4") {
 98            return Ok(Self::ClaudeOpus4);
 99        }
100
101        if id.starts_with("claude-sonnet-4-thinking") {
102            return Ok(Self::ClaudeSonnet4Thinking);
103        }
104
105        if id.starts_with("claude-sonnet-4") {
106            return Ok(Self::ClaudeSonnet4);
107        }
108
109        if id.starts_with("claude-3-7-sonnet-thinking") {
110            return Ok(Self::Claude3_7SonnetThinking);
111        }
112
113        if id.starts_with("claude-3-7-sonnet") {
114            return Ok(Self::Claude3_7Sonnet);
115        }
116
117        if id.starts_with("claude-3-5-sonnet") {
118            return Ok(Self::Claude3_5Sonnet);
119        }
120
121        if id.starts_with("claude-3-5-haiku") {
122            return Ok(Self::Claude3_5Haiku);
123        }
124
125        if id.starts_with("claude-3-opus") {
126            return Ok(Self::Claude3Opus);
127        }
128
129        if id.starts_with("claude-3-sonnet") {
130            return Ok(Self::Claude3Sonnet);
131        }
132
133        if id.starts_with("claude-3-haiku") {
134            return Ok(Self::Claude3Haiku);
135        }
136
137        Err(anyhow!("invalid model ID: {id}"))
138    }
139
140    pub fn id(&self) -> &str {
141        match self {
142            Self::ClaudeOpus4 => "claude-opus-4-latest",
143            Self::ClaudeOpus4Thinking => "claude-opus-4-thinking-latest",
144            Self::ClaudeSonnet4 => "claude-sonnet-4-latest",
145            Self::ClaudeSonnet4Thinking => "claude-sonnet-4-thinking-latest",
146            Self::Claude3_5Sonnet => "claude-3-5-sonnet-latest",
147            Self::Claude3_7Sonnet => "claude-3-7-sonnet-latest",
148            Self::Claude3_7SonnetThinking => "claude-3-7-sonnet-thinking-latest",
149            Self::Claude3_5Haiku => "claude-3-5-haiku-latest",
150            Self::Claude3Opus => "claude-3-opus-latest",
151            Self::Claude3Sonnet => "claude-3-sonnet-20240229",
152            Self::Claude3Haiku => "claude-3-haiku-20240307",
153            Self::Custom { name, .. } => name,
154        }
155    }
156
157    /// The id of the model that should be used for making API requests
158    pub fn request_id(&self) -> &str {
159        match self {
160            Self::ClaudeOpus4 | Self::ClaudeOpus4Thinking => "claude-opus-4-20250514",
161            Self::ClaudeSonnet4 | Self::ClaudeSonnet4Thinking => "claude-sonnet-4-20250514",
162            Self::Claude3_5Sonnet => "claude-3-5-sonnet-latest",
163            Self::Claude3_7Sonnet | Self::Claude3_7SonnetThinking => "claude-3-7-sonnet-latest",
164            Self::Claude3_5Haiku => "claude-3-5-haiku-latest",
165            Self::Claude3Opus => "claude-3-opus-latest",
166            Self::Claude3Sonnet => "claude-3-sonnet-20240229",
167            Self::Claude3Haiku => "claude-3-haiku-20240307",
168            Self::Custom { name, .. } => name,
169        }
170    }
171
172    pub fn display_name(&self) -> &str {
173        match self {
174            Self::ClaudeOpus4 => "Claude Opus 4",
175            Self::ClaudeOpus4Thinking => "Claude Opus 4 Thinking",
176            Self::ClaudeSonnet4 => "Claude Sonnet 4",
177            Self::ClaudeSonnet4Thinking => "Claude Sonnet 4 Thinking",
178            Self::Claude3_7Sonnet => "Claude 3.7 Sonnet",
179            Self::Claude3_5Sonnet => "Claude 3.5 Sonnet",
180            Self::Claude3_7SonnetThinking => "Claude 3.7 Sonnet Thinking",
181            Self::Claude3_5Haiku => "Claude 3.5 Haiku",
182            Self::Claude3Opus => "Claude 3 Opus",
183            Self::Claude3Sonnet => "Claude 3 Sonnet",
184            Self::Claude3Haiku => "Claude 3 Haiku",
185            Self::Custom {
186                name, display_name, ..
187            } => display_name.as_ref().unwrap_or(name),
188        }
189    }
190
191    pub fn cache_configuration(&self) -> Option<AnthropicModelCacheConfiguration> {
192        match self {
193            Self::ClaudeOpus4
194            | Self::ClaudeOpus4Thinking
195            | Self::ClaudeSonnet4
196            | Self::ClaudeSonnet4Thinking
197            | Self::Claude3_5Sonnet
198            | Self::Claude3_5Haiku
199            | Self::Claude3_7Sonnet
200            | Self::Claude3_7SonnetThinking
201            | Self::Claude3Haiku => Some(AnthropicModelCacheConfiguration {
202                min_total_token: 2_048,
203                should_speculate: true,
204                max_cache_anchors: 4,
205            }),
206            Self::Custom {
207                cache_configuration,
208                ..
209            } => cache_configuration.clone(),
210            _ => None,
211        }
212    }
213
214    pub fn max_token_count(&self) -> u64 {
215        match self {
216            Self::ClaudeOpus4
217            | Self::ClaudeOpus4Thinking
218            | Self::ClaudeSonnet4
219            | Self::ClaudeSonnet4Thinking
220            | Self::Claude3_5Sonnet
221            | Self::Claude3_5Haiku
222            | Self::Claude3_7Sonnet
223            | Self::Claude3_7SonnetThinking
224            | Self::Claude3Opus
225            | Self::Claude3Sonnet
226            | Self::Claude3Haiku => 200_000,
227            Self::Custom { max_tokens, .. } => *max_tokens,
228        }
229    }
230
231    pub fn max_output_tokens(&self) -> u64 {
232        match self {
233            Self::ClaudeOpus4
234            | Self::ClaudeOpus4Thinking
235            | Self::ClaudeSonnet4
236            | Self::ClaudeSonnet4Thinking
237            | Self::Claude3_5Sonnet
238            | Self::Claude3_7Sonnet
239            | Self::Claude3_7SonnetThinking
240            | Self::Claude3_5Haiku => 8_192,
241            Self::Claude3Opus | Self::Claude3Sonnet | Self::Claude3Haiku => 4_096,
242            Self::Custom {
243                max_output_tokens, ..
244            } => max_output_tokens.unwrap_or(4_096),
245        }
246    }
247
248    pub fn default_temperature(&self) -> f32 {
249        match self {
250            Self::ClaudeOpus4
251            | Self::ClaudeOpus4Thinking
252            | Self::ClaudeSonnet4
253            | Self::ClaudeSonnet4Thinking
254            | Self::Claude3_5Sonnet
255            | Self::Claude3_7Sonnet
256            | Self::Claude3_7SonnetThinking
257            | Self::Claude3_5Haiku
258            | Self::Claude3Opus
259            | Self::Claude3Sonnet
260            | Self::Claude3Haiku => 1.0,
261            Self::Custom {
262                default_temperature,
263                ..
264            } => default_temperature.unwrap_or(1.0),
265        }
266    }
267
268    pub fn mode(&self) -> AnthropicModelMode {
269        match self {
270            Self::ClaudeOpus4
271            | Self::ClaudeSonnet4
272            | Self::Claude3_5Sonnet
273            | Self::Claude3_7Sonnet
274            | Self::Claude3_5Haiku
275            | Self::Claude3Opus
276            | Self::Claude3Sonnet
277            | Self::Claude3Haiku => AnthropicModelMode::Default,
278            Self::ClaudeOpus4Thinking
279            | Self::ClaudeSonnet4Thinking
280            | Self::Claude3_7SonnetThinking => AnthropicModelMode::Thinking {
281                budget_tokens: Some(4_096),
282            },
283            Self::Custom { mode, .. } => mode.clone(),
284        }
285    }
286
287    pub const DEFAULT_BETA_HEADERS: &[&str] = &["prompt-caching-2024-07-31"];
288
289    pub fn beta_headers(&self) -> String {
290        let mut headers = Self::DEFAULT_BETA_HEADERS
291            .iter()
292            .map(|header| header.to_string())
293            .collect::<Vec<_>>();
294
295        match self {
296            Self::Claude3_7Sonnet | Self::Claude3_7SonnetThinking => {
297                // Try beta token-efficient tool use (supported in Claude 3.7 Sonnet only)
298                // https://docs.anthropic.com/en/docs/build-with-claude/tool-use/token-efficient-tool-use
299                headers.push("token-efficient-tools-2025-02-19".to_string());
300            }
301            Self::Custom {
302                extra_beta_headers, ..
303            } => {
304                headers.extend(
305                    extra_beta_headers
306                        .iter()
307                        .filter(|header| !header.trim().is_empty())
308                        .cloned(),
309                );
310            }
311            _ => {}
312        }
313
314        headers.join(",")
315    }
316
317    pub fn tool_model_id(&self) -> &str {
318        if let Self::Custom {
319            tool_override: Some(tool_override),
320            ..
321        } = self
322        {
323            tool_override
324        } else {
325            self.request_id()
326        }
327    }
328}
329
330pub async fn complete(
331    client: &dyn HttpClient,
332    api_url: &str,
333    api_key: &str,
334    request: Request,
335) -> Result<Response, AnthropicError> {
336    let uri = format!("{api_url}/v1/messages");
337    let beta_headers = Model::from_id(&request.model)
338        .map(|model| model.beta_headers())
339        .unwrap_or_else(|_err| Model::DEFAULT_BETA_HEADERS.join(","));
340    let request_builder = HttpRequest::builder()
341        .method(Method::POST)
342        .uri(uri)
343        .header("Anthropic-Version", "2023-06-01")
344        .header("Anthropic-Beta", beta_headers)
345        .header("X-Api-Key", api_key)
346        .header("Content-Type", "application/json");
347
348    let serialized_request =
349        serde_json::to_string(&request).context("failed to serialize request")?;
350    let request = request_builder
351        .body(AsyncBody::from(serialized_request))
352        .context("failed to construct request body")?;
353
354    let mut response = client
355        .send(request)
356        .await
357        .context("failed to send request to Anthropic")?;
358    if response.status().is_success() {
359        let mut body = Vec::new();
360        response
361            .body_mut()
362            .read_to_end(&mut body)
363            .await
364            .context("failed to read response body")?;
365        let response_message: Response =
366            serde_json::from_slice(&body).context("failed to deserialize response body")?;
367        Ok(response_message)
368    } else {
369        let mut body = Vec::new();
370        response
371            .body_mut()
372            .read_to_end(&mut body)
373            .await
374            .context("failed to read response body")?;
375        let body_str =
376            std::str::from_utf8(&body).context("failed to parse response body as UTF-8")?;
377        Err(AnthropicError::Other(anyhow!(
378            "Failed to connect to API: {} {}",
379            response.status(),
380            body_str
381        )))
382    }
383}
384
385pub async fn stream_completion(
386    client: &dyn HttpClient,
387    api_url: &str,
388    api_key: &str,
389    request: Request,
390) -> Result<BoxStream<'static, Result<Event, AnthropicError>>, AnthropicError> {
391    stream_completion_with_rate_limit_info(client, api_url, api_key, request)
392        .await
393        .map(|output| output.0)
394}
395
396/// An individual rate limit.
397#[derive(Debug)]
398pub struct RateLimit {
399    pub limit: usize,
400    pub remaining: usize,
401    pub reset: DateTime<Utc>,
402}
403
404impl RateLimit {
405    fn from_headers(resource: &str, headers: &HeaderMap<HeaderValue>) -> Result<Self> {
406        let limit =
407            get_header(&format!("anthropic-ratelimit-{resource}-limit"), headers)?.parse()?;
408        let remaining = get_header(
409            &format!("anthropic-ratelimit-{resource}-remaining"),
410            headers,
411        )?
412        .parse()?;
413        let reset = DateTime::parse_from_rfc3339(get_header(
414            &format!("anthropic-ratelimit-{resource}-reset"),
415            headers,
416        )?)?
417        .to_utc();
418
419        Ok(Self {
420            limit,
421            remaining,
422            reset,
423        })
424    }
425}
426
427/// <https://docs.anthropic.com/en/api/rate-limits#response-headers>
428#[derive(Debug)]
429pub struct RateLimitInfo {
430    pub retry_after: Option<Duration>,
431    pub requests: Option<RateLimit>,
432    pub tokens: Option<RateLimit>,
433    pub input_tokens: Option<RateLimit>,
434    pub output_tokens: Option<RateLimit>,
435}
436
437impl RateLimitInfo {
438    fn from_headers(headers: &HeaderMap<HeaderValue>) -> Self {
439        // Check if any rate limit headers exist
440        let has_rate_limit_headers = headers
441            .keys()
442            .any(|k| k == "retry-after" || k.as_str().starts_with("anthropic-ratelimit-"));
443
444        if !has_rate_limit_headers {
445            return Self {
446                retry_after: None,
447                requests: None,
448                tokens: None,
449                input_tokens: None,
450                output_tokens: None,
451            };
452        }
453
454        Self {
455            retry_after: headers
456                .get("retry-after")
457                .and_then(|v| v.to_str().ok())
458                .and_then(|v| v.parse::<u64>().ok())
459                .map(Duration::from_secs),
460            requests: RateLimit::from_headers("requests", headers).ok(),
461            tokens: RateLimit::from_headers("tokens", headers).ok(),
462            input_tokens: RateLimit::from_headers("input-tokens", headers).ok(),
463            output_tokens: RateLimit::from_headers("output-tokens", headers).ok(),
464        }
465    }
466}
467
468fn get_header<'a>(key: &str, headers: &'a HeaderMap) -> anyhow::Result<&'a str> {
469    Ok(headers
470        .get(key)
471        .with_context(|| format!("missing header `{key}`"))?
472        .to_str()?)
473}
474
475pub async fn stream_completion_with_rate_limit_info(
476    client: &dyn HttpClient,
477    api_url: &str,
478    api_key: &str,
479    request: Request,
480) -> Result<
481    (
482        BoxStream<'static, Result<Event, AnthropicError>>,
483        Option<RateLimitInfo>,
484    ),
485    AnthropicError,
486> {
487    let request = StreamingRequest {
488        base: request,
489        stream: true,
490    };
491    let uri = format!("{api_url}/v1/messages");
492    let beta_headers = Model::from_id(&request.base.model)
493        .map(|model| model.beta_headers())
494        .unwrap_or_else(|_err| Model::DEFAULT_BETA_HEADERS.join(","));
495    let request_builder = HttpRequest::builder()
496        .method(Method::POST)
497        .uri(uri)
498        .header("Anthropic-Version", "2023-06-01")
499        .header("Anthropic-Beta", beta_headers)
500        .header("X-Api-Key", api_key)
501        .header("Content-Type", "application/json");
502    let serialized_request =
503        serde_json::to_string(&request).context("failed to serialize request")?;
504    let request = request_builder
505        .body(AsyncBody::from(serialized_request))
506        .context("failed to construct request body")?;
507
508    let mut response = client
509        .send(request)
510        .await
511        .context("failed to send request to Anthropic")?;
512    let rate_limits = RateLimitInfo::from_headers(response.headers());
513    if response.status().is_success() {
514        let reader = BufReader::new(response.into_body());
515        let stream = reader
516            .lines()
517            .filter_map(|line| async move {
518                match line {
519                    Ok(line) => {
520                        let line = line.strip_prefix("data: ")?;
521                        match serde_json::from_str(line) {
522                            Ok(response) => Some(Ok(response)),
523                            Err(error) => Some(Err(AnthropicError::Other(anyhow!(error)))),
524                        }
525                    }
526                    Err(error) => Some(Err(AnthropicError::Other(anyhow!(error)))),
527                }
528            })
529            .boxed();
530        Ok((stream, Some(rate_limits)))
531    } else if let Some(retry_after) = rate_limits.retry_after {
532        Err(AnthropicError::RateLimit(retry_after))
533    } else {
534        let mut body = Vec::new();
535        response
536            .body_mut()
537            .read_to_end(&mut body)
538            .await
539            .context("failed to read response body")?;
540
541        let body_str =
542            std::str::from_utf8(&body).context("failed to parse response body as UTF-8")?;
543
544        match serde_json::from_str::<Event>(body_str) {
545            Ok(Event::Error { error }) => Err(AnthropicError::ApiError(error)),
546            Ok(_) => Err(AnthropicError::Other(anyhow!(
547                "Unexpected success response while expecting an error: '{body_str}'",
548            ))),
549            Err(_) => Err(AnthropicError::Other(anyhow!(
550                "Failed to connect to API: {} {}",
551                response.status(),
552                body_str,
553            ))),
554        }
555    }
556}
557
558#[derive(Debug, Serialize, Deserialize, Copy, Clone)]
559#[serde(rename_all = "lowercase")]
560pub enum CacheControlType {
561    Ephemeral,
562}
563
564#[derive(Debug, Serialize, Deserialize, Copy, Clone)]
565pub struct CacheControl {
566    #[serde(rename = "type")]
567    pub cache_type: CacheControlType,
568}
569
570#[derive(Debug, Serialize, Deserialize)]
571pub struct Message {
572    pub role: Role,
573    pub content: Vec<RequestContent>,
574}
575
576#[derive(Debug, Serialize, Deserialize, Eq, PartialEq, Hash)]
577#[serde(rename_all = "lowercase")]
578pub enum Role {
579    User,
580    Assistant,
581}
582
583#[derive(Debug, Serialize, Deserialize)]
584#[serde(tag = "type")]
585pub enum RequestContent {
586    #[serde(rename = "text")]
587    Text {
588        text: String,
589        #[serde(skip_serializing_if = "Option::is_none")]
590        cache_control: Option<CacheControl>,
591    },
592    #[serde(rename = "thinking")]
593    Thinking {
594        thinking: String,
595        signature: String,
596        #[serde(skip_serializing_if = "Option::is_none")]
597        cache_control: Option<CacheControl>,
598    },
599    #[serde(rename = "redacted_thinking")]
600    RedactedThinking { data: String },
601    #[serde(rename = "image")]
602    Image {
603        source: ImageSource,
604        #[serde(skip_serializing_if = "Option::is_none")]
605        cache_control: Option<CacheControl>,
606    },
607    #[serde(rename = "tool_use")]
608    ToolUse {
609        id: String,
610        name: String,
611        input: serde_json::Value,
612        #[serde(skip_serializing_if = "Option::is_none")]
613        cache_control: Option<CacheControl>,
614    },
615    #[serde(rename = "tool_result")]
616    ToolResult {
617        tool_use_id: String,
618        is_error: bool,
619        content: ToolResultContent,
620        #[serde(skip_serializing_if = "Option::is_none")]
621        cache_control: Option<CacheControl>,
622    },
623}
624
625#[derive(Debug, Serialize, Deserialize)]
626#[serde(untagged)]
627pub enum ToolResultContent {
628    Plain(String),
629    Multipart(Vec<ToolResultPart>),
630}
631
632#[derive(Debug, Serialize, Deserialize)]
633#[serde(tag = "type", rename_all = "lowercase")]
634pub enum ToolResultPart {
635    Text { text: String },
636    Image { source: ImageSource },
637}
638
639#[derive(Debug, Serialize, Deserialize)]
640#[serde(tag = "type")]
641pub enum ResponseContent {
642    #[serde(rename = "text")]
643    Text { text: String },
644    #[serde(rename = "thinking")]
645    Thinking { thinking: String },
646    #[serde(rename = "redacted_thinking")]
647    RedactedThinking { data: String },
648    #[serde(rename = "tool_use")]
649    ToolUse {
650        id: String,
651        name: String,
652        input: serde_json::Value,
653    },
654}
655
656#[derive(Debug, Serialize, Deserialize)]
657pub struct ImageSource {
658    #[serde(rename = "type")]
659    pub source_type: String,
660    pub media_type: String,
661    pub data: String,
662}
663
664#[derive(Debug, Serialize, Deserialize)]
665pub struct Tool {
666    pub name: String,
667    pub description: String,
668    pub input_schema: serde_json::Value,
669}
670
671#[derive(Debug, Serialize, Deserialize)]
672#[serde(tag = "type", rename_all = "lowercase")]
673pub enum ToolChoice {
674    Auto,
675    Any,
676    Tool { name: String },
677    None,
678}
679
680#[derive(Debug, Serialize, Deserialize)]
681#[serde(tag = "type", rename_all = "lowercase")]
682pub enum Thinking {
683    Enabled { budget_tokens: Option<u32> },
684}
685
686#[derive(Debug, Serialize, Deserialize)]
687#[serde(untagged)]
688pub enum StringOrContents {
689    String(String),
690    Content(Vec<RequestContent>),
691}
692
693#[derive(Debug, Serialize, Deserialize)]
694pub struct Request {
695    pub model: String,
696    pub max_tokens: u64,
697    pub messages: Vec<Message>,
698    #[serde(default, skip_serializing_if = "Vec::is_empty")]
699    pub tools: Vec<Tool>,
700    #[serde(default, skip_serializing_if = "Option::is_none")]
701    pub thinking: Option<Thinking>,
702    #[serde(default, skip_serializing_if = "Option::is_none")]
703    pub tool_choice: Option<ToolChoice>,
704    #[serde(default, skip_serializing_if = "Option::is_none")]
705    pub system: Option<StringOrContents>,
706    #[serde(default, skip_serializing_if = "Option::is_none")]
707    pub metadata: Option<Metadata>,
708    #[serde(default, skip_serializing_if = "Vec::is_empty")]
709    pub stop_sequences: Vec<String>,
710    #[serde(default, skip_serializing_if = "Option::is_none")]
711    pub temperature: Option<f32>,
712    #[serde(default, skip_serializing_if = "Option::is_none")]
713    pub top_k: Option<u32>,
714    #[serde(default, skip_serializing_if = "Option::is_none")]
715    pub top_p: Option<f32>,
716}
717
718#[derive(Debug, Serialize, Deserialize)]
719struct StreamingRequest {
720    #[serde(flatten)]
721    pub base: Request,
722    pub stream: bool,
723}
724
725#[derive(Debug, Serialize, Deserialize)]
726pub struct Metadata {
727    pub user_id: Option<String>,
728}
729
730#[derive(Debug, Serialize, Deserialize, Default)]
731pub struct Usage {
732    #[serde(default, skip_serializing_if = "Option::is_none")]
733    pub input_tokens: Option<u64>,
734    #[serde(default, skip_serializing_if = "Option::is_none")]
735    pub output_tokens: Option<u64>,
736    #[serde(default, skip_serializing_if = "Option::is_none")]
737    pub cache_creation_input_tokens: Option<u64>,
738    #[serde(default, skip_serializing_if = "Option::is_none")]
739    pub cache_read_input_tokens: Option<u64>,
740}
741
742#[derive(Debug, Serialize, Deserialize)]
743pub struct Response {
744    pub id: String,
745    #[serde(rename = "type")]
746    pub response_type: String,
747    pub role: Role,
748    pub content: Vec<ResponseContent>,
749    pub model: String,
750    #[serde(default, skip_serializing_if = "Option::is_none")]
751    pub stop_reason: Option<String>,
752    #[serde(default, skip_serializing_if = "Option::is_none")]
753    pub stop_sequence: Option<String>,
754    pub usage: Usage,
755}
756
757#[derive(Debug, Serialize, Deserialize)]
758#[serde(tag = "type")]
759pub enum Event {
760    #[serde(rename = "message_start")]
761    MessageStart { message: Response },
762    #[serde(rename = "content_block_start")]
763    ContentBlockStart {
764        index: usize,
765        content_block: ResponseContent,
766    },
767    #[serde(rename = "content_block_delta")]
768    ContentBlockDelta { index: usize, delta: ContentDelta },
769    #[serde(rename = "content_block_stop")]
770    ContentBlockStop { index: usize },
771    #[serde(rename = "message_delta")]
772    MessageDelta { delta: MessageDelta, usage: Usage },
773    #[serde(rename = "message_stop")]
774    MessageStop,
775    #[serde(rename = "ping")]
776    Ping,
777    #[serde(rename = "error")]
778    Error { error: ApiError },
779}
780
781#[derive(Debug, Serialize, Deserialize)]
782#[serde(tag = "type")]
783pub enum ContentDelta {
784    #[serde(rename = "text_delta")]
785    TextDelta { text: String },
786    #[serde(rename = "thinking_delta")]
787    ThinkingDelta { thinking: String },
788    #[serde(rename = "signature_delta")]
789    SignatureDelta { signature: String },
790    #[serde(rename = "input_json_delta")]
791    InputJsonDelta { partial_json: String },
792}
793
794#[derive(Debug, Serialize, Deserialize)]
795pub struct MessageDelta {
796    pub stop_reason: Option<String>,
797    pub stop_sequence: Option<String>,
798}
799
800#[derive(Error, Debug)]
801pub enum AnthropicError {
802    #[error("rate limit exceeded, retry after {0:?}")]
803    RateLimit(Duration),
804    #[error("an error occurred while interacting with the Anthropic API: {error_type}: {message}", error_type = .0.error_type, message = .0.message)]
805    ApiError(ApiError),
806    #[error("{0}")]
807    Other(#[from] anyhow::Error),
808}
809
810#[derive(Debug, Serialize, Deserialize)]
811pub struct ApiError {
812    #[serde(rename = "type")]
813    pub error_type: String,
814    pub message: String,
815}
816
817/// An Anthropic API error code.
818/// <https://docs.anthropic.com/en/api/errors#http-errors>
819#[derive(Debug, PartialEq, Eq, Clone, Copy, EnumString)]
820#[strum(serialize_all = "snake_case")]
821pub enum ApiErrorCode {
822    /// 400 - `invalid_request_error`: There was an issue with the format or content of your request.
823    InvalidRequestError,
824    /// 401 - `authentication_error`: There's an issue with your API key.
825    AuthenticationError,
826    /// 403 - `permission_error`: Your API key does not have permission to use the specified resource.
827    PermissionError,
828    /// 404 - `not_found_error`: The requested resource was not found.
829    NotFoundError,
830    /// 413 - `request_too_large`: Request exceeds the maximum allowed number of bytes.
831    RequestTooLarge,
832    /// 429 - `rate_limit_error`: Your account has hit a rate limit.
833    RateLimitError,
834    /// 500 - `api_error`: An unexpected error has occurred internal to Anthropic's systems.
835    ApiError,
836    /// 529 - `overloaded_error`: Anthropic's API is temporarily overloaded.
837    OverloadedError,
838}
839
840impl ApiError {
841    pub fn code(&self) -> Option<ApiErrorCode> {
842        ApiErrorCode::from_str(&self.error_type).ok()
843    }
844
845    pub fn is_rate_limit_error(&self) -> bool {
846        matches!(self.error_type.as_str(), "rate_limit_error")
847    }
848
849    pub fn match_window_exceeded(&self) -> Option<u64> {
850        let Some(ApiErrorCode::InvalidRequestError) = self.code() else {
851            return None;
852        };
853
854        parse_prompt_too_long(&self.message)
855    }
856}
857
858pub fn parse_prompt_too_long(message: &str) -> Option<u64> {
859    message
860        .strip_prefix("prompt is too long: ")?
861        .split_once(" tokens")?
862        .0
863        .parse()
864        .ok()
865}
866
867#[test]
868fn test_match_window_exceeded() {
869    let error = ApiError {
870        error_type: "invalid_request_error".to_string(),
871        message: "prompt is too long: 220000 tokens > 200000".to_string(),
872    };
873    assert_eq!(error.match_window_exceeded(), Some(220_000));
874
875    let error = ApiError {
876        error_type: "invalid_request_error".to_string(),
877        message: "prompt is too long: 1234953 tokens".to_string(),
878    };
879    assert_eq!(error.match_window_exceeded(), Some(1234953));
880
881    let error = ApiError {
882        error_type: "invalid_request_error".to_string(),
883        message: "not a prompt length error".to_string(),
884    };
885    assert_eq!(error.match_window_exceeded(), None);
886
887    let error = ApiError {
888        error_type: "rate_limit_error".to_string(),
889        message: "prompt is too long: 12345 tokens".to_string(),
890    };
891    assert_eq!(error.match_window_exceeded(), None);
892
893    let error = ApiError {
894        error_type: "invalid_request_error".to_string(),
895        message: "prompt is too long: invalid tokens".to_string(),
896    };
897    assert_eq!(error.match_window_exceeded(), None);
898}