1use std::io;
2use std::str::FromStr;
3use std::time::Duration;
4
5use anyhow::{Context as _, Result, anyhow};
6use chrono::{DateTime, Utc};
7use futures::{AsyncBufReadExt, AsyncReadExt, StreamExt, io::BufReader, stream::BoxStream};
8use http_client::http::{self, HeaderMap, HeaderValue};
9use http_client::{AsyncBody, HttpClient, Method, Request as HttpRequest, StatusCode};
10use serde::{Deserialize, Serialize};
11use strum::{EnumIter, EnumString};
12use thiserror::Error;
13
14pub const ANTHROPIC_API_URL: &str = "https://api.anthropic.com";
15
16#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
17#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)]
18pub struct AnthropicModelCacheConfiguration {
19 pub min_total_token: u64,
20 pub should_speculate: bool,
21 pub max_cache_anchors: usize,
22}
23
24#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
25#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)]
26pub enum AnthropicModelMode {
27 #[default]
28 Default,
29 Thinking {
30 budget_tokens: Option<u32>,
31 },
32}
33
34#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
35#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, EnumIter)]
36pub enum Model {
37 #[serde(rename = "claude-opus-4", alias = "claude-opus-4-latest")]
38 ClaudeOpus4,
39 #[serde(rename = "claude-opus-4-1", alias = "claude-opus-4-1-latest")]
40 ClaudeOpus4_1,
41 #[serde(
42 rename = "claude-opus-4-thinking",
43 alias = "claude-opus-4-thinking-latest"
44 )]
45 ClaudeOpus4Thinking,
46 #[serde(
47 rename = "claude-opus-4-1-thinking",
48 alias = "claude-opus-4-1-thinking-latest"
49 )]
50 ClaudeOpus4_1Thinking,
51 #[default]
52 #[serde(rename = "claude-sonnet-4", alias = "claude-sonnet-4-latest")]
53 ClaudeSonnet4,
54 #[serde(
55 rename = "claude-sonnet-4-thinking",
56 alias = "claude-sonnet-4-thinking-latest"
57 )]
58 ClaudeSonnet4Thinking,
59 #[serde(rename = "claude-3-7-sonnet", alias = "claude-3-7-sonnet-latest")]
60 Claude3_7Sonnet,
61 #[serde(
62 rename = "claude-3-7-sonnet-thinking",
63 alias = "claude-3-7-sonnet-thinking-latest"
64 )]
65 Claude3_7SonnetThinking,
66 #[serde(rename = "claude-3-5-sonnet", alias = "claude-3-5-sonnet-latest")]
67 Claude3_5Sonnet,
68 #[serde(rename = "claude-3-5-haiku", alias = "claude-3-5-haiku-latest")]
69 Claude3_5Haiku,
70 #[serde(rename = "claude-3-opus", alias = "claude-3-opus-latest")]
71 Claude3Opus,
72 #[serde(rename = "claude-3-sonnet", alias = "claude-3-sonnet-latest")]
73 Claude3Sonnet,
74 #[serde(rename = "claude-3-haiku", alias = "claude-3-haiku-latest")]
75 Claude3Haiku,
76 #[serde(rename = "custom")]
77 Custom {
78 name: String,
79 max_tokens: u64,
80 /// The name displayed in the UI, such as in the assistant panel model dropdown menu.
81 display_name: Option<String>,
82 /// Override this model with a different Anthropic model for tool calls.
83 tool_override: Option<String>,
84 /// Indicates whether this custom model supports caching.
85 cache_configuration: Option<AnthropicModelCacheConfiguration>,
86 max_output_tokens: Option<u64>,
87 default_temperature: Option<f32>,
88 #[serde(default)]
89 extra_beta_headers: Vec<String>,
90 #[serde(default)]
91 mode: AnthropicModelMode,
92 },
93}
94
95impl Model {
96 pub fn default_fast() -> Self {
97 Self::Claude3_5Haiku
98 }
99
100 pub fn from_id(id: &str) -> Result<Self> {
101 if id.starts_with("claude-opus-4-1-thinking") {
102 return Ok(Self::ClaudeOpus4_1Thinking);
103 }
104
105 if id.starts_with("claude-opus-4-thinking") {
106 return Ok(Self::ClaudeOpus4Thinking);
107 }
108
109 if id.starts_with("claude-opus-4-1") {
110 return Ok(Self::ClaudeOpus4_1);
111 }
112
113 if id.starts_with("claude-opus-4") {
114 return Ok(Self::ClaudeOpus4);
115 }
116
117 if id.starts_with("claude-sonnet-4-thinking") {
118 return Ok(Self::ClaudeSonnet4Thinking);
119 }
120
121 if id.starts_with("claude-sonnet-4") {
122 return Ok(Self::ClaudeSonnet4);
123 }
124
125 if id.starts_with("claude-3-7-sonnet-thinking") {
126 return Ok(Self::Claude3_7SonnetThinking);
127 }
128
129 if id.starts_with("claude-3-7-sonnet") {
130 return Ok(Self::Claude3_7Sonnet);
131 }
132
133 if id.starts_with("claude-3-5-sonnet") {
134 return Ok(Self::Claude3_5Sonnet);
135 }
136
137 if id.starts_with("claude-3-5-haiku") {
138 return Ok(Self::Claude3_5Haiku);
139 }
140
141 if id.starts_with("claude-3-opus") {
142 return Ok(Self::Claude3Opus);
143 }
144
145 if id.starts_with("claude-3-sonnet") {
146 return Ok(Self::Claude3Sonnet);
147 }
148
149 if id.starts_with("claude-3-haiku") {
150 return Ok(Self::Claude3Haiku);
151 }
152
153 Err(anyhow!("invalid model ID: {id}"))
154 }
155
156 pub fn id(&self) -> &str {
157 match self {
158 Self::ClaudeOpus4 => "claude-opus-4-latest",
159 Self::ClaudeOpus4_1 => "claude-opus-4-1-latest",
160 Self::ClaudeOpus4Thinking => "claude-opus-4-thinking-latest",
161 Self::ClaudeOpus4_1Thinking => "claude-opus-4-1-thinking-latest",
162 Self::ClaudeSonnet4 => "claude-sonnet-4-latest",
163 Self::ClaudeSonnet4Thinking => "claude-sonnet-4-thinking-latest",
164 Self::Claude3_5Sonnet => "claude-3-5-sonnet-latest",
165 Self::Claude3_7Sonnet => "claude-3-7-sonnet-latest",
166 Self::Claude3_7SonnetThinking => "claude-3-7-sonnet-thinking-latest",
167 Self::Claude3_5Haiku => "claude-3-5-haiku-latest",
168 Self::Claude3Opus => "claude-3-opus-latest",
169 Self::Claude3Sonnet => "claude-3-sonnet-20240229",
170 Self::Claude3Haiku => "claude-3-haiku-20240307",
171 Self::Custom { name, .. } => name,
172 }
173 }
174
175 /// The id of the model that should be used for making API requests
176 pub fn request_id(&self) -> &str {
177 match self {
178 Self::ClaudeOpus4 | Self::ClaudeOpus4Thinking => "claude-opus-4-20250514",
179 Self::ClaudeOpus4_1 | Self::ClaudeOpus4_1Thinking => "claude-opus-4-1-20250805",
180 Self::ClaudeSonnet4 | Self::ClaudeSonnet4Thinking => "claude-sonnet-4-20250514",
181 Self::Claude3_5Sonnet => "claude-3-5-sonnet-latest",
182 Self::Claude3_7Sonnet | Self::Claude3_7SonnetThinking => "claude-3-7-sonnet-latest",
183 Self::Claude3_5Haiku => "claude-3-5-haiku-latest",
184 Self::Claude3Opus => "claude-3-opus-latest",
185 Self::Claude3Sonnet => "claude-3-sonnet-20240229",
186 Self::Claude3Haiku => "claude-3-haiku-20240307",
187 Self::Custom { name, .. } => name,
188 }
189 }
190
191 pub fn display_name(&self) -> &str {
192 match self {
193 Self::ClaudeOpus4 => "Claude Opus 4",
194 Self::ClaudeOpus4_1 => "Claude Opus 4.1",
195 Self::ClaudeOpus4Thinking => "Claude Opus 4 Thinking",
196 Self::ClaudeOpus4_1Thinking => "Claude Opus 4.1 Thinking",
197 Self::ClaudeSonnet4 => "Claude Sonnet 4",
198 Self::ClaudeSonnet4Thinking => "Claude Sonnet 4 Thinking",
199 Self::Claude3_7Sonnet => "Claude 3.7 Sonnet",
200 Self::Claude3_5Sonnet => "Claude 3.5 Sonnet",
201 Self::Claude3_7SonnetThinking => "Claude 3.7 Sonnet Thinking",
202 Self::Claude3_5Haiku => "Claude 3.5 Haiku",
203 Self::Claude3Opus => "Claude 3 Opus",
204 Self::Claude3Sonnet => "Claude 3 Sonnet",
205 Self::Claude3Haiku => "Claude 3 Haiku",
206 Self::Custom {
207 name, display_name, ..
208 } => display_name.as_ref().unwrap_or(name),
209 }
210 }
211
212 pub fn cache_configuration(&self) -> Option<AnthropicModelCacheConfiguration> {
213 match self {
214 Self::ClaudeOpus4
215 | Self::ClaudeOpus4_1
216 | Self::ClaudeOpus4Thinking
217 | Self::ClaudeOpus4_1Thinking
218 | Self::ClaudeSonnet4
219 | Self::ClaudeSonnet4Thinking
220 | Self::Claude3_5Sonnet
221 | Self::Claude3_5Haiku
222 | Self::Claude3_7Sonnet
223 | Self::Claude3_7SonnetThinking
224 | Self::Claude3Haiku => Some(AnthropicModelCacheConfiguration {
225 min_total_token: 2_048,
226 should_speculate: true,
227 max_cache_anchors: 4,
228 }),
229 Self::Custom {
230 cache_configuration,
231 ..
232 } => cache_configuration.clone(),
233 _ => None,
234 }
235 }
236
237 pub fn max_token_count(&self) -> u64 {
238 match self {
239 Self::ClaudeOpus4
240 | Self::ClaudeOpus4_1
241 | Self::ClaudeOpus4Thinking
242 | Self::ClaudeOpus4_1Thinking
243 | Self::ClaudeSonnet4
244 | Self::ClaudeSonnet4Thinking
245 | Self::Claude3_5Sonnet
246 | Self::Claude3_5Haiku
247 | Self::Claude3_7Sonnet
248 | Self::Claude3_7SonnetThinking
249 | Self::Claude3Opus
250 | Self::Claude3Sonnet
251 | Self::Claude3Haiku => 200_000,
252 Self::Custom { max_tokens, .. } => *max_tokens,
253 }
254 }
255
256 pub fn max_output_tokens(&self) -> u64 {
257 match self {
258 Self::ClaudeOpus4
259 | Self::ClaudeOpus4_1
260 | Self::ClaudeOpus4Thinking
261 | Self::ClaudeOpus4_1Thinking
262 | Self::ClaudeSonnet4
263 | Self::ClaudeSonnet4Thinking
264 | Self::Claude3_5Sonnet
265 | Self::Claude3_7Sonnet
266 | Self::Claude3_7SonnetThinking
267 | Self::Claude3_5Haiku => 8_192,
268 Self::Claude3Opus | Self::Claude3Sonnet | Self::Claude3Haiku => 4_096,
269 Self::Custom {
270 max_output_tokens, ..
271 } => max_output_tokens.unwrap_or(4_096),
272 }
273 }
274
275 pub fn default_temperature(&self) -> f32 {
276 match self {
277 Self::ClaudeOpus4
278 | Self::ClaudeOpus4_1
279 | Self::ClaudeOpus4Thinking
280 | Self::ClaudeOpus4_1Thinking
281 | Self::ClaudeSonnet4
282 | Self::ClaudeSonnet4Thinking
283 | Self::Claude3_5Sonnet
284 | Self::Claude3_7Sonnet
285 | Self::Claude3_7SonnetThinking
286 | Self::Claude3_5Haiku
287 | Self::Claude3Opus
288 | Self::Claude3Sonnet
289 | Self::Claude3Haiku => 1.0,
290 Self::Custom {
291 default_temperature,
292 ..
293 } => default_temperature.unwrap_or(1.0),
294 }
295 }
296
297 pub fn mode(&self) -> AnthropicModelMode {
298 match self {
299 Self::ClaudeOpus4
300 | Self::ClaudeOpus4_1
301 | Self::ClaudeSonnet4
302 | Self::Claude3_5Sonnet
303 | Self::Claude3_7Sonnet
304 | Self::Claude3_5Haiku
305 | Self::Claude3Opus
306 | Self::Claude3Sonnet
307 | Self::Claude3Haiku => AnthropicModelMode::Default,
308 Self::ClaudeOpus4Thinking
309 | Self::ClaudeOpus4_1Thinking
310 | Self::ClaudeSonnet4Thinking
311 | Self::Claude3_7SonnetThinking => AnthropicModelMode::Thinking {
312 budget_tokens: Some(4_096),
313 },
314 Self::Custom { mode, .. } => mode.clone(),
315 }
316 }
317
318 pub const DEFAULT_BETA_HEADERS: &[&str] = &["prompt-caching-2024-07-31"];
319
320 pub fn beta_headers(&self) -> String {
321 let mut headers = Self::DEFAULT_BETA_HEADERS
322 .iter()
323 .map(|header| header.to_string())
324 .collect::<Vec<_>>();
325
326 match self {
327 Self::Claude3_7Sonnet | Self::Claude3_7SonnetThinking => {
328 // Try beta token-efficient tool use (supported in Claude 3.7 Sonnet only)
329 // https://docs.anthropic.com/en/docs/build-with-claude/tool-use/token-efficient-tool-use
330 headers.push("token-efficient-tools-2025-02-19".to_string());
331 }
332 Self::Custom {
333 extra_beta_headers, ..
334 } => {
335 headers.extend(
336 extra_beta_headers
337 .iter()
338 .filter(|header| !header.trim().is_empty())
339 .cloned(),
340 );
341 }
342 _ => {}
343 }
344
345 headers.join(",")
346 }
347
348 pub fn tool_model_id(&self) -> &str {
349 if let Self::Custom {
350 tool_override: Some(tool_override),
351 ..
352 } = self
353 {
354 tool_override
355 } else {
356 self.request_id()
357 }
358 }
359}
360
361pub async fn complete(
362 client: &dyn HttpClient,
363 api_url: &str,
364 api_key: &str,
365 request: Request,
366) -> Result<Response, AnthropicError> {
367 let uri = format!("{api_url}/v1/messages");
368 let beta_headers = Model::from_id(&request.model)
369 .map(|model| model.beta_headers())
370 .unwrap_or_else(|_| Model::DEFAULT_BETA_HEADERS.join(","));
371 let request_builder = HttpRequest::builder()
372 .method(Method::POST)
373 .uri(uri)
374 .header("Anthropic-Version", "2023-06-01")
375 .header("Anthropic-Beta", beta_headers)
376 .header("X-Api-Key", api_key.trim())
377 .header("Content-Type", "application/json");
378
379 let serialized_request =
380 serde_json::to_string(&request).map_err(AnthropicError::SerializeRequest)?;
381 let request = request_builder
382 .body(AsyncBody::from(serialized_request))
383 .map_err(AnthropicError::BuildRequestBody)?;
384
385 let mut response = client
386 .send(request)
387 .await
388 .map_err(AnthropicError::HttpSend)?;
389 let status_code = response.status();
390 let mut body = String::new();
391 response
392 .body_mut()
393 .read_to_string(&mut body)
394 .await
395 .map_err(AnthropicError::ReadResponse)?;
396
397 if status_code.is_success() {
398 Ok(serde_json::from_str(&body).map_err(AnthropicError::DeserializeResponse)?)
399 } else {
400 Err(AnthropicError::HttpResponseError {
401 status_code,
402 message: body,
403 })
404 }
405}
406
407pub async fn stream_completion(
408 client: &dyn HttpClient,
409 api_url: &str,
410 api_key: &str,
411 request: Request,
412) -> Result<BoxStream<'static, Result<Event, AnthropicError>>, AnthropicError> {
413 stream_completion_with_rate_limit_info(client, api_url, api_key, request)
414 .await
415 .map(|output| output.0)
416}
417
418/// An individual rate limit.
419#[derive(Debug)]
420pub struct RateLimit {
421 pub limit: usize,
422 pub remaining: usize,
423 pub reset: DateTime<Utc>,
424}
425
426impl RateLimit {
427 fn from_headers(resource: &str, headers: &HeaderMap<HeaderValue>) -> Result<Self> {
428 let limit =
429 get_header(&format!("anthropic-ratelimit-{resource}-limit"), headers)?.parse()?;
430 let remaining = get_header(
431 &format!("anthropic-ratelimit-{resource}-remaining"),
432 headers,
433 )?
434 .parse()?;
435 let reset = DateTime::parse_from_rfc3339(get_header(
436 &format!("anthropic-ratelimit-{resource}-reset"),
437 headers,
438 )?)?
439 .to_utc();
440
441 Ok(Self {
442 limit,
443 remaining,
444 reset,
445 })
446 }
447}
448
449/// <https://docs.anthropic.com/en/api/rate-limits#response-headers>
450#[derive(Debug)]
451pub struct RateLimitInfo {
452 pub retry_after: Option<Duration>,
453 pub requests: Option<RateLimit>,
454 pub tokens: Option<RateLimit>,
455 pub input_tokens: Option<RateLimit>,
456 pub output_tokens: Option<RateLimit>,
457}
458
459impl RateLimitInfo {
460 fn from_headers(headers: &HeaderMap<HeaderValue>) -> Self {
461 // Check if any rate limit headers exist
462 let has_rate_limit_headers = headers
463 .keys()
464 .any(|k| k == "retry-after" || k.as_str().starts_with("anthropic-ratelimit-"));
465
466 if !has_rate_limit_headers {
467 return Self {
468 retry_after: None,
469 requests: None,
470 tokens: None,
471 input_tokens: None,
472 output_tokens: None,
473 };
474 }
475
476 Self {
477 retry_after: parse_retry_after(headers),
478 requests: RateLimit::from_headers("requests", headers).ok(),
479 tokens: RateLimit::from_headers("tokens", headers).ok(),
480 input_tokens: RateLimit::from_headers("input-tokens", headers).ok(),
481 output_tokens: RateLimit::from_headers("output-tokens", headers).ok(),
482 }
483 }
484}
485
486/// Parses the Retry-After header value as an integer number of seconds (anthropic always uses
487/// seconds). Note that other services might specify an HTTP date or some other format for this
488/// header. Returns `None` if the header is not present or cannot be parsed.
489pub fn parse_retry_after(headers: &HeaderMap<HeaderValue>) -> Option<Duration> {
490 headers
491 .get("retry-after")
492 .and_then(|v| v.to_str().ok())
493 .and_then(|v| v.parse::<u64>().ok())
494 .map(Duration::from_secs)
495}
496
497fn get_header<'a>(key: &str, headers: &'a HeaderMap) -> anyhow::Result<&'a str> {
498 Ok(headers
499 .get(key)
500 .with_context(|| format!("missing header `{key}`"))?
501 .to_str()?)
502}
503
504pub async fn stream_completion_with_rate_limit_info(
505 client: &dyn HttpClient,
506 api_url: &str,
507 api_key: &str,
508 request: Request,
509) -> Result<
510 (
511 BoxStream<'static, Result<Event, AnthropicError>>,
512 Option<RateLimitInfo>,
513 ),
514 AnthropicError,
515> {
516 let request = StreamingRequest {
517 base: request,
518 stream: true,
519 };
520 let uri = format!("{api_url}/v1/messages");
521 let beta_headers = Model::from_id(&request.base.model)
522 .map(|model| model.beta_headers())
523 .unwrap_or_else(|_| Model::DEFAULT_BETA_HEADERS.join(","));
524 let request_builder = HttpRequest::builder()
525 .method(Method::POST)
526 .uri(uri)
527 .header("Anthropic-Version", "2023-06-01")
528 .header("Anthropic-Beta", beta_headers)
529 .header("X-Api-Key", api_key.trim())
530 .header("Content-Type", "application/json");
531 let serialized_request =
532 serde_json::to_string(&request).map_err(AnthropicError::SerializeRequest)?;
533 let request = request_builder
534 .body(AsyncBody::from(serialized_request))
535 .map_err(AnthropicError::BuildRequestBody)?;
536
537 let mut response = client
538 .send(request)
539 .await
540 .map_err(AnthropicError::HttpSend)?;
541 let rate_limits = RateLimitInfo::from_headers(response.headers());
542 if response.status().is_success() {
543 let reader = BufReader::new(response.into_body());
544 let stream = reader
545 .lines()
546 .filter_map(|line| async move {
547 match line {
548 Ok(line) => {
549 let line = line.strip_prefix("data: ")?;
550 match serde_json::from_str(line) {
551 Ok(response) => Some(Ok(response)),
552 Err(error) => Some(Err(AnthropicError::DeserializeResponse(error))),
553 }
554 }
555 Err(error) => Some(Err(AnthropicError::ReadResponse(error))),
556 }
557 })
558 .boxed();
559 Ok((stream, Some(rate_limits)))
560 } else if response.status().as_u16() == 529 {
561 Err(AnthropicError::ServerOverloaded {
562 retry_after: rate_limits.retry_after,
563 })
564 } else if let Some(retry_after) = rate_limits.retry_after {
565 Err(AnthropicError::RateLimit { retry_after })
566 } else {
567 let mut body = String::new();
568 response
569 .body_mut()
570 .read_to_string(&mut body)
571 .await
572 .map_err(AnthropicError::ReadResponse)?;
573
574 match serde_json::from_str::<Event>(&body) {
575 Ok(Event::Error { error }) => Err(AnthropicError::ApiError(error)),
576 Ok(_) | Err(_) => Err(AnthropicError::HttpResponseError {
577 status_code: response.status(),
578 message: body,
579 }),
580 }
581 }
582}
583
584#[derive(Debug, Serialize, Deserialize, Copy, Clone)]
585#[serde(rename_all = "lowercase")]
586pub enum CacheControlType {
587 Ephemeral,
588}
589
590#[derive(Debug, Serialize, Deserialize, Copy, Clone)]
591pub struct CacheControl {
592 #[serde(rename = "type")]
593 pub cache_type: CacheControlType,
594}
595
596#[derive(Debug, Serialize, Deserialize)]
597pub struct Message {
598 pub role: Role,
599 pub content: Vec<RequestContent>,
600}
601
602#[derive(Debug, Serialize, Deserialize, Eq, PartialEq, Hash)]
603#[serde(rename_all = "lowercase")]
604pub enum Role {
605 User,
606 Assistant,
607}
608
609#[derive(Debug, Serialize, Deserialize)]
610#[serde(tag = "type")]
611pub enum RequestContent {
612 #[serde(rename = "text")]
613 Text {
614 text: String,
615 #[serde(skip_serializing_if = "Option::is_none")]
616 cache_control: Option<CacheControl>,
617 },
618 #[serde(rename = "thinking")]
619 Thinking {
620 thinking: String,
621 signature: String,
622 #[serde(skip_serializing_if = "Option::is_none")]
623 cache_control: Option<CacheControl>,
624 },
625 #[serde(rename = "redacted_thinking")]
626 RedactedThinking { data: String },
627 #[serde(rename = "image")]
628 Image {
629 source: ImageSource,
630 #[serde(skip_serializing_if = "Option::is_none")]
631 cache_control: Option<CacheControl>,
632 },
633 #[serde(rename = "tool_use")]
634 ToolUse {
635 id: String,
636 name: String,
637 input: serde_json::Value,
638 #[serde(skip_serializing_if = "Option::is_none")]
639 cache_control: Option<CacheControl>,
640 },
641 #[serde(rename = "tool_result")]
642 ToolResult {
643 tool_use_id: String,
644 is_error: bool,
645 content: ToolResultContent,
646 #[serde(skip_serializing_if = "Option::is_none")]
647 cache_control: Option<CacheControl>,
648 },
649}
650
651#[derive(Debug, Serialize, Deserialize)]
652#[serde(untagged)]
653pub enum ToolResultContent {
654 Plain(String),
655 Multipart(Vec<ToolResultPart>),
656}
657
658#[derive(Debug, Serialize, Deserialize)]
659#[serde(tag = "type", rename_all = "lowercase")]
660pub enum ToolResultPart {
661 Text { text: String },
662 Image { source: ImageSource },
663}
664
665#[derive(Debug, Serialize, Deserialize)]
666#[serde(tag = "type")]
667pub enum ResponseContent {
668 #[serde(rename = "text")]
669 Text { text: String },
670 #[serde(rename = "thinking")]
671 Thinking { thinking: String },
672 #[serde(rename = "redacted_thinking")]
673 RedactedThinking { data: String },
674 #[serde(rename = "tool_use")]
675 ToolUse {
676 id: String,
677 name: String,
678 input: serde_json::Value,
679 },
680}
681
682#[derive(Debug, Serialize, Deserialize)]
683pub struct ImageSource {
684 #[serde(rename = "type")]
685 pub source_type: String,
686 pub media_type: String,
687 pub data: String,
688}
689
690#[derive(Debug, Serialize, Deserialize)]
691pub struct Tool {
692 pub name: String,
693 pub description: String,
694 pub input_schema: serde_json::Value,
695}
696
697#[derive(Debug, Serialize, Deserialize)]
698#[serde(tag = "type", rename_all = "lowercase")]
699pub enum ToolChoice {
700 Auto,
701 Any,
702 Tool { name: String },
703 None,
704}
705
706#[derive(Debug, Serialize, Deserialize)]
707#[serde(tag = "type", rename_all = "lowercase")]
708pub enum Thinking {
709 Enabled { budget_tokens: Option<u32> },
710}
711
712#[derive(Debug, Serialize, Deserialize)]
713#[serde(untagged)]
714pub enum StringOrContents {
715 String(String),
716 Content(Vec<RequestContent>),
717}
718
719#[derive(Debug, Serialize, Deserialize)]
720pub struct Request {
721 pub model: String,
722 pub max_tokens: u64,
723 pub messages: Vec<Message>,
724 #[serde(default, skip_serializing_if = "Vec::is_empty")]
725 pub tools: Vec<Tool>,
726 #[serde(default, skip_serializing_if = "Option::is_none")]
727 pub thinking: Option<Thinking>,
728 #[serde(default, skip_serializing_if = "Option::is_none")]
729 pub tool_choice: Option<ToolChoice>,
730 #[serde(default, skip_serializing_if = "Option::is_none")]
731 pub system: Option<StringOrContents>,
732 #[serde(default, skip_serializing_if = "Option::is_none")]
733 pub metadata: Option<Metadata>,
734 #[serde(default, skip_serializing_if = "Vec::is_empty")]
735 pub stop_sequences: Vec<String>,
736 #[serde(default, skip_serializing_if = "Option::is_none")]
737 pub temperature: Option<f32>,
738 #[serde(default, skip_serializing_if = "Option::is_none")]
739 pub top_k: Option<u32>,
740 #[serde(default, skip_serializing_if = "Option::is_none")]
741 pub top_p: Option<f32>,
742}
743
744#[derive(Debug, Serialize, Deserialize)]
745struct StreamingRequest {
746 #[serde(flatten)]
747 pub base: Request,
748 pub stream: bool,
749}
750
751#[derive(Debug, Serialize, Deserialize)]
752pub struct Metadata {
753 pub user_id: Option<String>,
754}
755
756#[derive(Debug, Serialize, Deserialize, Default)]
757pub struct Usage {
758 #[serde(default, skip_serializing_if = "Option::is_none")]
759 pub input_tokens: Option<u64>,
760 #[serde(default, skip_serializing_if = "Option::is_none")]
761 pub output_tokens: Option<u64>,
762 #[serde(default, skip_serializing_if = "Option::is_none")]
763 pub cache_creation_input_tokens: Option<u64>,
764 #[serde(default, skip_serializing_if = "Option::is_none")]
765 pub cache_read_input_tokens: Option<u64>,
766}
767
768#[derive(Debug, Serialize, Deserialize)]
769pub struct Response {
770 pub id: String,
771 #[serde(rename = "type")]
772 pub response_type: String,
773 pub role: Role,
774 pub content: Vec<ResponseContent>,
775 pub model: String,
776 #[serde(default, skip_serializing_if = "Option::is_none")]
777 pub stop_reason: Option<String>,
778 #[serde(default, skip_serializing_if = "Option::is_none")]
779 pub stop_sequence: Option<String>,
780 pub usage: Usage,
781}
782
783#[derive(Debug, Serialize, Deserialize)]
784#[serde(tag = "type")]
785pub enum Event {
786 #[serde(rename = "message_start")]
787 MessageStart { message: Response },
788 #[serde(rename = "content_block_start")]
789 ContentBlockStart {
790 index: usize,
791 content_block: ResponseContent,
792 },
793 #[serde(rename = "content_block_delta")]
794 ContentBlockDelta { index: usize, delta: ContentDelta },
795 #[serde(rename = "content_block_stop")]
796 ContentBlockStop { index: usize },
797 #[serde(rename = "message_delta")]
798 MessageDelta { delta: MessageDelta, usage: Usage },
799 #[serde(rename = "message_stop")]
800 MessageStop,
801 #[serde(rename = "ping")]
802 Ping,
803 #[serde(rename = "error")]
804 Error { error: ApiError },
805}
806
807#[derive(Debug, Serialize, Deserialize)]
808#[serde(tag = "type")]
809pub enum ContentDelta {
810 #[serde(rename = "text_delta")]
811 TextDelta { text: String },
812 #[serde(rename = "thinking_delta")]
813 ThinkingDelta { thinking: String },
814 #[serde(rename = "signature_delta")]
815 SignatureDelta { signature: String },
816 #[serde(rename = "input_json_delta")]
817 InputJsonDelta { partial_json: String },
818}
819
820#[derive(Debug, Serialize, Deserialize)]
821pub struct MessageDelta {
822 pub stop_reason: Option<String>,
823 pub stop_sequence: Option<String>,
824}
825
826#[derive(Debug)]
827pub enum AnthropicError {
828 /// Failed to serialize the HTTP request body to JSON
829 SerializeRequest(serde_json::Error),
830
831 /// Failed to construct the HTTP request body
832 BuildRequestBody(http::Error),
833
834 /// Failed to send the HTTP request
835 HttpSend(anyhow::Error),
836
837 /// Failed to deserialize the response from JSON
838 DeserializeResponse(serde_json::Error),
839
840 /// Failed to read from response stream
841 ReadResponse(io::Error),
842
843 /// HTTP error response from the API
844 HttpResponseError {
845 status_code: StatusCode,
846 message: String,
847 },
848
849 /// Rate limit exceeded
850 RateLimit { retry_after: Duration },
851
852 /// Server overloaded
853 ServerOverloaded { retry_after: Option<Duration> },
854
855 /// API returned an error response
856 ApiError(ApiError),
857}
858
859#[derive(Debug, Serialize, Deserialize, Error)]
860#[error("Anthropic API Error: {error_type}: {message}")]
861pub struct ApiError {
862 #[serde(rename = "type")]
863 pub error_type: String,
864 pub message: String,
865}
866
867/// An Anthropic API error code.
868/// <https://docs.anthropic.com/en/api/errors#http-errors>
869#[derive(Debug, PartialEq, Eq, Clone, Copy, EnumString)]
870#[strum(serialize_all = "snake_case")]
871pub enum ApiErrorCode {
872 /// 400 - `invalid_request_error`: There was an issue with the format or content of your request.
873 InvalidRequestError,
874 /// 401 - `authentication_error`: There's an issue with your API key.
875 AuthenticationError,
876 /// 403 - `permission_error`: Your API key does not have permission to use the specified resource.
877 PermissionError,
878 /// 404 - `not_found_error`: The requested resource was not found.
879 NotFoundError,
880 /// 413 - `request_too_large`: Request exceeds the maximum allowed number of bytes.
881 RequestTooLarge,
882 /// 429 - `rate_limit_error`: Your account has hit a rate limit.
883 RateLimitError,
884 /// 500 - `api_error`: An unexpected error has occurred internal to Anthropic's systems.
885 ApiError,
886 /// 529 - `overloaded_error`: Anthropic's API is temporarily overloaded.
887 OverloadedError,
888}
889
890impl ApiError {
891 pub fn code(&self) -> Option<ApiErrorCode> {
892 ApiErrorCode::from_str(&self.error_type).ok()
893 }
894
895 pub fn is_rate_limit_error(&self) -> bool {
896 matches!(self.error_type.as_str(), "rate_limit_error")
897 }
898
899 pub fn match_window_exceeded(&self) -> Option<u64> {
900 let Some(ApiErrorCode::InvalidRequestError) = self.code() else {
901 return None;
902 };
903
904 parse_prompt_too_long(&self.message)
905 }
906}
907
908pub fn parse_prompt_too_long(message: &str) -> Option<u64> {
909 message
910 .strip_prefix("prompt is too long: ")?
911 .split_once(" tokens")?
912 .0
913 .parse()
914 .ok()
915}
916
917#[test]
918fn test_match_window_exceeded() {
919 let error = ApiError {
920 error_type: "invalid_request_error".to_string(),
921 message: "prompt is too long: 220000 tokens > 200000".to_string(),
922 };
923 assert_eq!(error.match_window_exceeded(), Some(220_000));
924
925 let error = ApiError {
926 error_type: "invalid_request_error".to_string(),
927 message: "prompt is too long: 1234953 tokens".to_string(),
928 };
929 assert_eq!(error.match_window_exceeded(), Some(1234953));
930
931 let error = ApiError {
932 error_type: "invalid_request_error".to_string(),
933 message: "not a prompt length error".to_string(),
934 };
935 assert_eq!(error.match_window_exceeded(), None);
936
937 let error = ApiError {
938 error_type: "rate_limit_error".to_string(),
939 message: "prompt is too long: 12345 tokens".to_string(),
940 };
941 assert_eq!(error.match_window_exceeded(), None);
942
943 let error = ApiError {
944 error_type: "invalid_request_error".to_string(),
945 message: "prompt is too long: invalid tokens".to_string(),
946 };
947 assert_eq!(error.match_window_exceeded(), None);
948}