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