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