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}