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    #[serde(rename = "claude-3-5-sonnet", alias = "claude-3-5-sonnet-latest")]
 41    Claude3_5Sonnet,
 42    #[default]
 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/// An individual rate limit.
325#[derive(Debug)]
326pub struct RateLimit {
327    pub limit: usize,
328    pub remaining: usize,
329    pub reset: DateTime<Utc>,
330}
331
332impl RateLimit {
333    fn from_headers(resource: &str, headers: &HeaderMap<HeaderValue>) -> Result<Self> {
334        let limit =
335            get_header(&format!("anthropic-ratelimit-{resource}-limit"), headers)?.parse()?;
336        let remaining = get_header(
337            &format!("anthropic-ratelimit-{resource}-remaining"),
338            headers,
339        )?
340        .parse()?;
341        let reset = DateTime::parse_from_rfc3339(get_header(
342            &format!("anthropic-ratelimit-{resource}-reset"),
343            headers,
344        )?)?
345        .to_utc();
346
347        Ok(Self {
348            limit,
349            remaining,
350            reset,
351        })
352    }
353}
354
355/// <https://docs.anthropic.com/en/api/rate-limits#response-headers>
356#[derive(Debug)]
357pub struct RateLimitInfo {
358    pub requests: Option<RateLimit>,
359    pub tokens: Option<RateLimit>,
360    pub input_tokens: Option<RateLimit>,
361    pub output_tokens: Option<RateLimit>,
362}
363
364impl RateLimitInfo {
365    fn from_headers(headers: &HeaderMap<HeaderValue>) -> Self {
366        Self {
367            requests: RateLimit::from_headers("requests", headers).log_err(),
368            tokens: RateLimit::from_headers("tokens", headers).log_err(),
369            input_tokens: RateLimit::from_headers("input-tokens", headers).log_err(),
370            output_tokens: RateLimit::from_headers("output-tokens", headers).log_err(),
371        }
372    }
373}
374
375fn get_header<'a>(key: &str, headers: &'a HeaderMap) -> Result<&'a str, anyhow::Error> {
376    Ok(headers
377        .get(key)
378        .ok_or_else(|| anyhow!("missing header `{key}`"))?
379        .to_str()?)
380}
381
382pub async fn stream_completion_with_rate_limit_info(
383    client: &dyn HttpClient,
384    api_url: &str,
385    api_key: &str,
386    request: Request,
387) -> Result<
388    (
389        BoxStream<'static, Result<Event, AnthropicError>>,
390        Option<RateLimitInfo>,
391    ),
392    AnthropicError,
393> {
394    let request = StreamingRequest {
395        base: request,
396        stream: true,
397    };
398    let uri = format!("{api_url}/v1/messages");
399    let beta_headers = Model::from_id(&request.base.model)
400        .map(|model| model.beta_headers())
401        .unwrap_or_else(|_err| Model::DEFAULT_BETA_HEADERS.join(","));
402    let request_builder = HttpRequest::builder()
403        .method(Method::POST)
404        .uri(uri)
405        .header("Anthropic-Version", "2023-06-01")
406        .header("Anthropic-Beta", beta_headers)
407        .header("X-Api-Key", api_key)
408        .header("Content-Type", "application/json");
409    let serialized_request =
410        serde_json::to_string(&request).context("failed to serialize request")?;
411    let request = request_builder
412        .body(AsyncBody::from(serialized_request))
413        .context("failed to construct request body")?;
414
415    let mut response = client
416        .send(request)
417        .await
418        .context("failed to send request to Anthropic")?;
419    if response.status().is_success() {
420        let rate_limits = RateLimitInfo::from_headers(response.headers());
421        let reader = BufReader::new(response.into_body());
422        let stream = reader
423            .lines()
424            .filter_map(|line| async move {
425                match line {
426                    Ok(line) => {
427                        let line = line.strip_prefix("data: ")?;
428                        match serde_json::from_str(line) {
429                            Ok(response) => Some(Ok(response)),
430                            Err(error) => Some(Err(AnthropicError::Other(anyhow!(error)))),
431                        }
432                    }
433                    Err(error) => Some(Err(AnthropicError::Other(anyhow!(error)))),
434                }
435            })
436            .boxed();
437        Ok((stream, Some(rate_limits)))
438    } else {
439        let mut body = Vec::new();
440        response
441            .body_mut()
442            .read_to_end(&mut body)
443            .await
444            .context("failed to read response body")?;
445
446        let body_str =
447            std::str::from_utf8(&body).context("failed to parse response body as UTF-8")?;
448
449        match serde_json::from_str::<Event>(body_str) {
450            Ok(Event::Error { error }) => Err(AnthropicError::ApiError(error)),
451            Ok(_) => Err(AnthropicError::Other(anyhow!(
452                "Unexpected success response while expecting an error: '{body_str}'",
453            ))),
454            Err(_) => Err(AnthropicError::Other(anyhow!(
455                "Failed to connect to API: {} {}",
456                response.status(),
457                body_str,
458            ))),
459        }
460    }
461}
462
463#[derive(Debug, Serialize, Deserialize, Copy, Clone)]
464#[serde(rename_all = "lowercase")]
465pub enum CacheControlType {
466    Ephemeral,
467}
468
469#[derive(Debug, Serialize, Deserialize, Copy, Clone)]
470pub struct CacheControl {
471    #[serde(rename = "type")]
472    pub cache_type: CacheControlType,
473}
474
475#[derive(Debug, Serialize, Deserialize)]
476pub struct Message {
477    pub role: Role,
478    pub content: Vec<RequestContent>,
479}
480
481#[derive(Debug, Serialize, Deserialize, Eq, PartialEq, Hash)]
482#[serde(rename_all = "lowercase")]
483pub enum Role {
484    User,
485    Assistant,
486}
487
488#[derive(Debug, Serialize, Deserialize)]
489#[serde(tag = "type")]
490pub enum RequestContent {
491    #[serde(rename = "text")]
492    Text {
493        text: String,
494        #[serde(skip_serializing_if = "Option::is_none")]
495        cache_control: Option<CacheControl>,
496    },
497    #[serde(rename = "image")]
498    Image {
499        source: ImageSource,
500        #[serde(skip_serializing_if = "Option::is_none")]
501        cache_control: Option<CacheControl>,
502    },
503    #[serde(rename = "tool_use")]
504    ToolUse {
505        id: String,
506        name: String,
507        input: serde_json::Value,
508        #[serde(skip_serializing_if = "Option::is_none")]
509        cache_control: Option<CacheControl>,
510    },
511    #[serde(rename = "tool_result")]
512    ToolResult {
513        tool_use_id: String,
514        is_error: bool,
515        content: String,
516        #[serde(skip_serializing_if = "Option::is_none")]
517        cache_control: Option<CacheControl>,
518    },
519}
520
521#[derive(Debug, Serialize, Deserialize)]
522#[serde(tag = "type")]
523pub enum ResponseContent {
524    #[serde(rename = "text")]
525    Text { text: String },
526    #[serde(rename = "thinking")]
527    Thinking { thinking: String },
528    #[serde(rename = "redacted_thinking")]
529    RedactedThinking { data: String },
530    #[serde(rename = "tool_use")]
531    ToolUse {
532        id: String,
533        name: String,
534        input: serde_json::Value,
535    },
536}
537
538#[derive(Debug, Serialize, Deserialize)]
539pub struct ImageSource {
540    #[serde(rename = "type")]
541    pub source_type: String,
542    pub media_type: String,
543    pub data: String,
544}
545
546#[derive(Debug, Serialize, Deserialize)]
547pub struct Tool {
548    pub name: String,
549    pub description: String,
550    pub input_schema: serde_json::Value,
551}
552
553#[derive(Debug, Serialize, Deserialize)]
554#[serde(tag = "type", rename_all = "lowercase")]
555pub enum ToolChoice {
556    Auto,
557    Any,
558    Tool { name: String },
559}
560
561#[derive(Debug, Serialize, Deserialize)]
562#[serde(tag = "type", rename_all = "lowercase")]
563pub enum Thinking {
564    Enabled { budget_tokens: Option<u32> },
565}
566
567#[derive(Debug, Serialize, Deserialize)]
568#[serde(untagged)]
569pub enum StringOrContents {
570    String(String),
571    Content(Vec<RequestContent>),
572}
573
574#[derive(Debug, Serialize, Deserialize)]
575pub struct Request {
576    pub model: String,
577    pub max_tokens: u32,
578    pub messages: Vec<Message>,
579    #[serde(default, skip_serializing_if = "Vec::is_empty")]
580    pub tools: Vec<Tool>,
581    #[serde(default, skip_serializing_if = "Option::is_none")]
582    pub thinking: Option<Thinking>,
583    #[serde(default, skip_serializing_if = "Option::is_none")]
584    pub tool_choice: Option<ToolChoice>,
585    #[serde(default, skip_serializing_if = "Option::is_none")]
586    pub system: Option<StringOrContents>,
587    #[serde(default, skip_serializing_if = "Option::is_none")]
588    pub metadata: Option<Metadata>,
589    #[serde(default, skip_serializing_if = "Vec::is_empty")]
590    pub stop_sequences: Vec<String>,
591    #[serde(default, skip_serializing_if = "Option::is_none")]
592    pub temperature: Option<f32>,
593    #[serde(default, skip_serializing_if = "Option::is_none")]
594    pub top_k: Option<u32>,
595    #[serde(default, skip_serializing_if = "Option::is_none")]
596    pub top_p: Option<f32>,
597}
598
599#[derive(Debug, Serialize, Deserialize)]
600struct StreamingRequest {
601    #[serde(flatten)]
602    pub base: Request,
603    pub stream: bool,
604}
605
606#[derive(Debug, Serialize, Deserialize)]
607pub struct Metadata {
608    pub user_id: Option<String>,
609}
610
611#[derive(Debug, Serialize, Deserialize, Default)]
612pub struct Usage {
613    #[serde(default, skip_serializing_if = "Option::is_none")]
614    pub input_tokens: Option<u32>,
615    #[serde(default, skip_serializing_if = "Option::is_none")]
616    pub output_tokens: Option<u32>,
617    #[serde(default, skip_serializing_if = "Option::is_none")]
618    pub cache_creation_input_tokens: Option<u32>,
619    #[serde(default, skip_serializing_if = "Option::is_none")]
620    pub cache_read_input_tokens: Option<u32>,
621}
622
623#[derive(Debug, Serialize, Deserialize)]
624pub struct Response {
625    pub id: String,
626    #[serde(rename = "type")]
627    pub response_type: String,
628    pub role: Role,
629    pub content: Vec<ResponseContent>,
630    pub model: String,
631    #[serde(default, skip_serializing_if = "Option::is_none")]
632    pub stop_reason: Option<String>,
633    #[serde(default, skip_serializing_if = "Option::is_none")]
634    pub stop_sequence: Option<String>,
635    pub usage: Usage,
636}
637
638#[derive(Debug, Serialize, Deserialize)]
639#[serde(tag = "type")]
640pub enum Event {
641    #[serde(rename = "message_start")]
642    MessageStart { message: Response },
643    #[serde(rename = "content_block_start")]
644    ContentBlockStart {
645        index: usize,
646        content_block: ResponseContent,
647    },
648    #[serde(rename = "content_block_delta")]
649    ContentBlockDelta { index: usize, delta: ContentDelta },
650    #[serde(rename = "content_block_stop")]
651    ContentBlockStop { index: usize },
652    #[serde(rename = "message_delta")]
653    MessageDelta { delta: MessageDelta, usage: Usage },
654    #[serde(rename = "message_stop")]
655    MessageStop,
656    #[serde(rename = "ping")]
657    Ping,
658    #[serde(rename = "error")]
659    Error { error: ApiError },
660}
661
662#[derive(Debug, Serialize, Deserialize)]
663#[serde(tag = "type")]
664pub enum ContentDelta {
665    #[serde(rename = "text_delta")]
666    TextDelta { text: String },
667    #[serde(rename = "thinking_delta")]
668    ThinkingDelta { thinking: String },
669    #[serde(rename = "signature_delta")]
670    SignatureDelta { signature: String },
671    #[serde(rename = "input_json_delta")]
672    InputJsonDelta { partial_json: String },
673}
674
675#[derive(Debug, Serialize, Deserialize)]
676pub struct MessageDelta {
677    pub stop_reason: Option<String>,
678    pub stop_sequence: Option<String>,
679}
680
681#[derive(Error, Debug)]
682pub enum AnthropicError {
683    #[error("an error occurred while interacting with the Anthropic API: {error_type}: {message}", error_type = .0.error_type, message = .0.message)]
684    ApiError(ApiError),
685    #[error("{0}")]
686    Other(#[from] anyhow::Error),
687}
688
689#[derive(Debug, Serialize, Deserialize)]
690pub struct ApiError {
691    #[serde(rename = "type")]
692    pub error_type: String,
693    pub message: String,
694}
695
696/// An Anthropic API error code.
697/// <https://docs.anthropic.com/en/api/errors#http-errors>
698#[derive(Debug, PartialEq, Eq, Clone, Copy, EnumString)]
699#[strum(serialize_all = "snake_case")]
700pub enum ApiErrorCode {
701    /// 400 - `invalid_request_error`: There was an issue with the format or content of your request.
702    InvalidRequestError,
703    /// 401 - `authentication_error`: There's an issue with your API key.
704    AuthenticationError,
705    /// 403 - `permission_error`: Your API key does not have permission to use the specified resource.
706    PermissionError,
707    /// 404 - `not_found_error`: The requested resource was not found.
708    NotFoundError,
709    /// 413 - `request_too_large`: Request exceeds the maximum allowed number of bytes.
710    RequestTooLarge,
711    /// 429 - `rate_limit_error`: Your account has hit a rate limit.
712    RateLimitError,
713    /// 500 - `api_error`: An unexpected error has occurred internal to Anthropic's systems.
714    ApiError,
715    /// 529 - `overloaded_error`: Anthropic's API is temporarily overloaded.
716    OverloadedError,
717}
718
719impl ApiError {
720    pub fn code(&self) -> Option<ApiErrorCode> {
721        ApiErrorCode::from_str(&self.error_type).ok()
722    }
723
724    pub fn is_rate_limit_error(&self) -> bool {
725        matches!(self.error_type.as_str(), "rate_limit_error")
726    }
727}