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}