anthropic.rs

  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}