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