anthropic.rs

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