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 beta_headers: String,
367) -> Result<Response, AnthropicError> {
368 let uri = format!("{api_url}/v1/messages");
369 let request_builder = HttpRequest::builder()
370 .method(Method::POST)
371 .uri(uri)
372 .header("Anthropic-Version", "2023-06-01")
373 .header("Anthropic-Beta", beta_headers)
374 .header("X-Api-Key", api_key.trim())
375 .header("Content-Type", "application/json");
376
377 let serialized_request =
378 serde_json::to_string(&request).map_err(AnthropicError::SerializeRequest)?;
379 let request = request_builder
380 .body(AsyncBody::from(serialized_request))
381 .map_err(AnthropicError::BuildRequestBody)?;
382
383 let mut response = client
384 .send(request)
385 .await
386 .map_err(AnthropicError::HttpSend)?;
387 let status_code = response.status();
388 let mut body = String::new();
389 response
390 .body_mut()
391 .read_to_string(&mut body)
392 .await
393 .map_err(AnthropicError::ReadResponse)?;
394
395 if status_code.is_success() {
396 Ok(serde_json::from_str(&body).map_err(AnthropicError::DeserializeResponse)?)
397 } else {
398 Err(AnthropicError::HttpResponseError {
399 status_code,
400 message: body,
401 })
402 }
403}
404
405pub async fn stream_completion(
406 client: &dyn HttpClient,
407 api_url: &str,
408 api_key: &str,
409 request: Request,
410 beta_headers: String,
411) -> Result<BoxStream<'static, Result<Event, AnthropicError>>, AnthropicError> {
412 stream_completion_with_rate_limit_info(client, api_url, api_key, request, beta_headers)
413 .await
414 .map(|output| output.0)
415}
416
417/// An individual rate limit.
418#[derive(Debug)]
419pub struct RateLimit {
420 pub limit: usize,
421 pub remaining: usize,
422 pub reset: DateTime<Utc>,
423}
424
425impl RateLimit {
426 fn from_headers(resource: &str, headers: &HeaderMap<HeaderValue>) -> Result<Self> {
427 let limit =
428 get_header(&format!("anthropic-ratelimit-{resource}-limit"), headers)?.parse()?;
429 let remaining = get_header(
430 &format!("anthropic-ratelimit-{resource}-remaining"),
431 headers,
432 )?
433 .parse()?;
434 let reset = DateTime::parse_from_rfc3339(get_header(
435 &format!("anthropic-ratelimit-{resource}-reset"),
436 headers,
437 )?)?
438 .to_utc();
439
440 Ok(Self {
441 limit,
442 remaining,
443 reset,
444 })
445 }
446}
447
448/// <https://docs.anthropic.com/en/api/rate-limits#response-headers>
449#[derive(Debug)]
450pub struct RateLimitInfo {
451 pub retry_after: Option<Duration>,
452 pub requests: Option<RateLimit>,
453 pub tokens: Option<RateLimit>,
454 pub input_tokens: Option<RateLimit>,
455 pub output_tokens: Option<RateLimit>,
456}
457
458impl RateLimitInfo {
459 fn from_headers(headers: &HeaderMap<HeaderValue>) -> Self {
460 // Check if any rate limit headers exist
461 let has_rate_limit_headers = headers
462 .keys()
463 .any(|k| k == "retry-after" || k.as_str().starts_with("anthropic-ratelimit-"));
464
465 if !has_rate_limit_headers {
466 return Self {
467 retry_after: None,
468 requests: None,
469 tokens: None,
470 input_tokens: None,
471 output_tokens: None,
472 };
473 }
474
475 Self {
476 retry_after: parse_retry_after(headers),
477 requests: RateLimit::from_headers("requests", headers).ok(),
478 tokens: RateLimit::from_headers("tokens", headers).ok(),
479 input_tokens: RateLimit::from_headers("input-tokens", headers).ok(),
480 output_tokens: RateLimit::from_headers("output-tokens", headers).ok(),
481 }
482 }
483}
484
485/// Parses the Retry-After header value as an integer number of seconds (anthropic always uses
486/// seconds). Note that other services might specify an HTTP date or some other format for this
487/// header. Returns `None` if the header is not present or cannot be parsed.
488pub fn parse_retry_after(headers: &HeaderMap<HeaderValue>) -> Option<Duration> {
489 headers
490 .get("retry-after")
491 .and_then(|v| v.to_str().ok())
492 .and_then(|v| v.parse::<u64>().ok())
493 .map(Duration::from_secs)
494}
495
496fn get_header<'a>(key: &str, headers: &'a HeaderMap) -> anyhow::Result<&'a str> {
497 Ok(headers
498 .get(key)
499 .with_context(|| format!("missing header `{key}`"))?
500 .to_str()?)
501}
502
503pub async fn stream_completion_with_rate_limit_info(
504 client: &dyn HttpClient,
505 api_url: &str,
506 api_key: &str,
507 request: Request,
508 beta_headers: String,
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
522 let request_builder = HttpRequest::builder()
523 .method(Method::POST)
524 .uri(uri)
525 .header("Anthropic-Version", "2023-06-01")
526 .header("Anthropic-Beta", beta_headers)
527 .header("X-Api-Key", api_key.trim())
528 .header("Content-Type", "application/json");
529 let serialized_request =
530 serde_json::to_string(&request).map_err(AnthropicError::SerializeRequest)?;
531 let request = request_builder
532 .body(AsyncBody::from(serialized_request))
533 .map_err(AnthropicError::BuildRequestBody)?;
534
535 let mut response = client
536 .send(request)
537 .await
538 .map_err(AnthropicError::HttpSend)?;
539 let rate_limits = RateLimitInfo::from_headers(response.headers());
540 if response.status().is_success() {
541 let reader = BufReader::new(response.into_body());
542 let stream = reader
543 .lines()
544 .filter_map(|line| async move {
545 match line {
546 Ok(line) => {
547 let line = line.strip_prefix("data: ")?;
548 match serde_json::from_str(line) {
549 Ok(response) => Some(Ok(response)),
550 Err(error) => Some(Err(AnthropicError::DeserializeResponse(error))),
551 }
552 }
553 Err(error) => Some(Err(AnthropicError::ReadResponse(error))),
554 }
555 })
556 .boxed();
557 Ok((stream, Some(rate_limits)))
558 } else if response.status().as_u16() == 529 {
559 Err(AnthropicError::ServerOverloaded {
560 retry_after: rate_limits.retry_after,
561 })
562 } else if let Some(retry_after) = rate_limits.retry_after {
563 Err(AnthropicError::RateLimit { retry_after })
564 } else {
565 let mut body = String::new();
566 response
567 .body_mut()
568 .read_to_string(&mut body)
569 .await
570 .map_err(AnthropicError::ReadResponse)?;
571
572 match serde_json::from_str::<Event>(&body) {
573 Ok(Event::Error { error }) => Err(AnthropicError::ApiError(error)),
574 Ok(_) | Err(_) => Err(AnthropicError::HttpResponseError {
575 status_code: response.status(),
576 message: body,
577 }),
578 }
579 }
580}
581
582#[derive(Debug, Serialize, Deserialize, Copy, Clone)]
583#[serde(rename_all = "lowercase")]
584pub enum CacheControlType {
585 Ephemeral,
586}
587
588#[derive(Debug, Serialize, Deserialize, Copy, Clone)]
589pub struct CacheControl {
590 #[serde(rename = "type")]
591 pub cache_type: CacheControlType,
592}
593
594#[derive(Debug, Serialize, Deserialize)]
595pub struct Message {
596 pub role: Role,
597 pub content: Vec<RequestContent>,
598}
599
600#[derive(Debug, Serialize, Deserialize, Eq, PartialEq, Hash)]
601#[serde(rename_all = "lowercase")]
602pub enum Role {
603 User,
604 Assistant,
605}
606
607#[derive(Debug, Serialize, Deserialize)]
608#[serde(tag = "type")]
609pub enum RequestContent {
610 #[serde(rename = "text")]
611 Text {
612 text: String,
613 #[serde(skip_serializing_if = "Option::is_none")]
614 cache_control: Option<CacheControl>,
615 },
616 #[serde(rename = "thinking")]
617 Thinking {
618 thinking: String,
619 signature: String,
620 #[serde(skip_serializing_if = "Option::is_none")]
621 cache_control: Option<CacheControl>,
622 },
623 #[serde(rename = "redacted_thinking")]
624 RedactedThinking { data: String },
625 #[serde(rename = "image")]
626 Image {
627 source: ImageSource,
628 #[serde(skip_serializing_if = "Option::is_none")]
629 cache_control: Option<CacheControl>,
630 },
631 #[serde(rename = "tool_use")]
632 ToolUse {
633 id: String,
634 name: String,
635 input: serde_json::Value,
636 #[serde(skip_serializing_if = "Option::is_none")]
637 cache_control: Option<CacheControl>,
638 },
639 #[serde(rename = "tool_result")]
640 ToolResult {
641 tool_use_id: String,
642 is_error: bool,
643 content: ToolResultContent,
644 #[serde(skip_serializing_if = "Option::is_none")]
645 cache_control: Option<CacheControl>,
646 },
647}
648
649#[derive(Debug, Serialize, Deserialize)]
650#[serde(untagged)]
651pub enum ToolResultContent {
652 Plain(String),
653 Multipart(Vec<ToolResultPart>),
654}
655
656#[derive(Debug, Serialize, Deserialize)]
657#[serde(tag = "type", rename_all = "lowercase")]
658pub enum ToolResultPart {
659 Text { text: String },
660 Image { source: ImageSource },
661}
662
663#[derive(Debug, Serialize, Deserialize)]
664#[serde(tag = "type")]
665pub enum ResponseContent {
666 #[serde(rename = "text")]
667 Text { text: String },
668 #[serde(rename = "thinking")]
669 Thinking { thinking: String },
670 #[serde(rename = "redacted_thinking")]
671 RedactedThinking { data: String },
672 #[serde(rename = "tool_use")]
673 ToolUse {
674 id: String,
675 name: String,
676 input: serde_json::Value,
677 },
678}
679
680#[derive(Debug, Serialize, Deserialize)]
681pub struct ImageSource {
682 #[serde(rename = "type")]
683 pub source_type: String,
684 pub media_type: String,
685 pub data: String,
686}
687
688#[derive(Debug, Serialize, Deserialize)]
689pub struct Tool {
690 pub name: String,
691 pub description: String,
692 pub input_schema: serde_json::Value,
693}
694
695#[derive(Debug, Serialize, Deserialize)]
696#[serde(tag = "type", rename_all = "lowercase")]
697pub enum ToolChoice {
698 Auto,
699 Any,
700 Tool { name: String },
701 None,
702}
703
704#[derive(Debug, Serialize, Deserialize)]
705#[serde(tag = "type", rename_all = "lowercase")]
706pub enum Thinking {
707 Enabled { budget_tokens: Option<u32> },
708}
709
710#[derive(Debug, Serialize, Deserialize)]
711#[serde(untagged)]
712pub enum StringOrContents {
713 String(String),
714 Content(Vec<RequestContent>),
715}
716
717#[derive(Debug, Serialize, Deserialize)]
718pub struct Request {
719 pub model: String,
720 pub max_tokens: u64,
721 pub messages: Vec<Message>,
722 #[serde(default, skip_serializing_if = "Vec::is_empty")]
723 pub tools: Vec<Tool>,
724 #[serde(default, skip_serializing_if = "Option::is_none")]
725 pub thinking: Option<Thinking>,
726 #[serde(default, skip_serializing_if = "Option::is_none")]
727 pub tool_choice: Option<ToolChoice>,
728 #[serde(default, skip_serializing_if = "Option::is_none")]
729 pub system: Option<StringOrContents>,
730 #[serde(default, skip_serializing_if = "Option::is_none")]
731 pub metadata: Option<Metadata>,
732 #[serde(default, skip_serializing_if = "Vec::is_empty")]
733 pub stop_sequences: Vec<String>,
734 #[serde(default, skip_serializing_if = "Option::is_none")]
735 pub temperature: Option<f32>,
736 #[serde(default, skip_serializing_if = "Option::is_none")]
737 pub top_k: Option<u32>,
738 #[serde(default, skip_serializing_if = "Option::is_none")]
739 pub top_p: Option<f32>,
740}
741
742#[derive(Debug, Serialize, Deserialize)]
743struct StreamingRequest {
744 #[serde(flatten)]
745 pub base: Request,
746 pub stream: bool,
747}
748
749#[derive(Debug, Serialize, Deserialize)]
750pub struct Metadata {
751 pub user_id: Option<String>,
752}
753
754#[derive(Debug, Serialize, Deserialize, Default)]
755pub struct Usage {
756 #[serde(default, skip_serializing_if = "Option::is_none")]
757 pub input_tokens: Option<u64>,
758 #[serde(default, skip_serializing_if = "Option::is_none")]
759 pub output_tokens: Option<u64>,
760 #[serde(default, skip_serializing_if = "Option::is_none")]
761 pub cache_creation_input_tokens: Option<u64>,
762 #[serde(default, skip_serializing_if = "Option::is_none")]
763 pub cache_read_input_tokens: Option<u64>,
764}
765
766#[derive(Debug, Serialize, Deserialize)]
767pub struct Response {
768 pub id: String,
769 #[serde(rename = "type")]
770 pub response_type: String,
771 pub role: Role,
772 pub content: Vec<ResponseContent>,
773 pub model: String,
774 #[serde(default, skip_serializing_if = "Option::is_none")]
775 pub stop_reason: Option<String>,
776 #[serde(default, skip_serializing_if = "Option::is_none")]
777 pub stop_sequence: Option<String>,
778 pub usage: Usage,
779}
780
781#[derive(Debug, Serialize, Deserialize)]
782#[serde(tag = "type")]
783pub enum Event {
784 #[serde(rename = "message_start")]
785 MessageStart { message: Response },
786 #[serde(rename = "content_block_start")]
787 ContentBlockStart {
788 index: usize,
789 content_block: ResponseContent,
790 },
791 #[serde(rename = "content_block_delta")]
792 ContentBlockDelta { index: usize, delta: ContentDelta },
793 #[serde(rename = "content_block_stop")]
794 ContentBlockStop { index: usize },
795 #[serde(rename = "message_delta")]
796 MessageDelta { delta: MessageDelta, usage: Usage },
797 #[serde(rename = "message_stop")]
798 MessageStop,
799 #[serde(rename = "ping")]
800 Ping,
801 #[serde(rename = "error")]
802 Error { error: ApiError },
803}
804
805#[derive(Debug, Serialize, Deserialize)]
806#[serde(tag = "type")]
807pub enum ContentDelta {
808 #[serde(rename = "text_delta")]
809 TextDelta { text: String },
810 #[serde(rename = "thinking_delta")]
811 ThinkingDelta { thinking: String },
812 #[serde(rename = "signature_delta")]
813 SignatureDelta { signature: String },
814 #[serde(rename = "input_json_delta")]
815 InputJsonDelta { partial_json: String },
816}
817
818#[derive(Debug, Serialize, Deserialize)]
819pub struct MessageDelta {
820 pub stop_reason: Option<String>,
821 pub stop_sequence: Option<String>,
822}
823
824#[derive(Debug)]
825pub enum AnthropicError {
826 /// Failed to serialize the HTTP request body to JSON
827 SerializeRequest(serde_json::Error),
828
829 /// Failed to construct the HTTP request body
830 BuildRequestBody(http::Error),
831
832 /// Failed to send the HTTP request
833 HttpSend(anyhow::Error),
834
835 /// Failed to deserialize the response from JSON
836 DeserializeResponse(serde_json::Error),
837
838 /// Failed to read from response stream
839 ReadResponse(io::Error),
840
841 /// HTTP error response from the API
842 HttpResponseError {
843 status_code: StatusCode,
844 message: String,
845 },
846
847 /// Rate limit exceeded
848 RateLimit { retry_after: Duration },
849
850 /// Server overloaded
851 ServerOverloaded { retry_after: Option<Duration> },
852
853 /// API returned an error response
854 ApiError(ApiError),
855}
856
857#[derive(Debug, Serialize, Deserialize, Error)]
858#[error("Anthropic API Error: {error_type}: {message}")]
859pub struct ApiError {
860 #[serde(rename = "type")]
861 pub error_type: String,
862 pub message: String,
863}
864
865/// An Anthropic API error code.
866/// <https://docs.anthropic.com/en/api/errors#http-errors>
867#[derive(Debug, PartialEq, Eq, Clone, Copy, EnumString)]
868#[strum(serialize_all = "snake_case")]
869pub enum ApiErrorCode {
870 /// 400 - `invalid_request_error`: There was an issue with the format or content of your request.
871 InvalidRequestError,
872 /// 401 - `authentication_error`: There's an issue with your API key.
873 AuthenticationError,
874 /// 403 - `permission_error`: Your API key does not have permission to use the specified resource.
875 PermissionError,
876 /// 404 - `not_found_error`: The requested resource was not found.
877 NotFoundError,
878 /// 413 - `request_too_large`: Request exceeds the maximum allowed number of bytes.
879 RequestTooLarge,
880 /// 429 - `rate_limit_error`: Your account has hit a rate limit.
881 RateLimitError,
882 /// 500 - `api_error`: An unexpected error has occurred internal to Anthropic's systems.
883 ApiError,
884 /// 529 - `overloaded_error`: Anthropic's API is temporarily overloaded.
885 OverloadedError,
886}
887
888impl ApiError {
889 pub fn code(&self) -> Option<ApiErrorCode> {
890 ApiErrorCode::from_str(&self.error_type).ok()
891 }
892
893 pub fn is_rate_limit_error(&self) -> bool {
894 matches!(self.error_type.as_str(), "rate_limit_error")
895 }
896
897 pub fn match_window_exceeded(&self) -> Option<u64> {
898 let Some(ApiErrorCode::InvalidRequestError) = self.code() else {
899 return None;
900 };
901
902 parse_prompt_too_long(&self.message)
903 }
904}
905
906pub fn parse_prompt_too_long(message: &str) -> Option<u64> {
907 message
908 .strip_prefix("prompt is too long: ")?
909 .split_once(" tokens")?
910 .0
911 .parse()
912 .ok()
913}
914
915#[test]
916fn test_match_window_exceeded() {
917 let error = ApiError {
918 error_type: "invalid_request_error".to_string(),
919 message: "prompt is too long: 220000 tokens > 200000".to_string(),
920 };
921 assert_eq!(error.match_window_exceeded(), Some(220_000));
922
923 let error = ApiError {
924 error_type: "invalid_request_error".to_string(),
925 message: "prompt is too long: 1234953 tokens".to_string(),
926 };
927 assert_eq!(error.match_window_exceeded(), Some(1234953));
928
929 let error = ApiError {
930 error_type: "invalid_request_error".to_string(),
931 message: "not a prompt length error".to_string(),
932 };
933 assert_eq!(error.match_window_exceeded(), None);
934
935 let error = ApiError {
936 error_type: "rate_limit_error".to_string(),
937 message: "prompt is too long: 12345 tokens".to_string(),
938 };
939 assert_eq!(error.match_window_exceeded(), None);
940
941 let error = ApiError {
942 error_type: "invalid_request_error".to_string(),
943 message: "prompt is too long: invalid tokens".to_string(),
944 };
945 assert_eq!(error.match_window_exceeded(), None);
946}