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