anthropic.rs

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