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