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