Skip to repository content1301 lines · 41.2 KB · rust
tenant.openagents/omega
No repository description is available.
OpenAgents Git authority 2026-07-29T04:12:44.819Z Public web read
NIP-34 coordinate
30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omegaMaintainersHidden in public view
References2 branches · 1 tag
Read-only clone
git clone https://openagents.com/git/tenant.openagents/omega.gitBrowse files
anthropic.rs
1use std::io;
2use std::str::FromStr;
3use std::sync::Arc;
4use std::time::Duration;
5
6use anyhow::{Context as _, Result};
7use chrono::{DateTime, Utc};
8use futures::{AsyncBufReadExt, AsyncReadExt, StreamExt, io::BufReader, stream::BoxStream};
9use http_client::http::{self, HeaderMap, HeaderValue};
10use http_client::{
11 AsyncBody, CustomHeaders, HttpClient, Method, Request as HttpRequest, RequestBuilderExt,
12 StatusCode,
13};
14use serde::{Deserialize, Serialize};
15use strum::EnumString;
16use thiserror::Error;
17
18pub mod batches;
19pub mod completion;
20
21pub const ANTHROPIC_API_URL: &str = "https://api.anthropic.com";
22pub const FAST_MODE_BETA_HEADER: &str = "fast-mode-2026-02-01";
23
24pub fn supports_fast_mode(model_id: &str) -> bool {
25 matches!(model_id, "claude-opus-5" | "claude-opus-4-8")
26}
27
28/// Model IDs where adaptive thinking runs by default when a request omits the
29/// `thinking` field, and where thinking must instead be turned off with an
30/// explicit `thinking: {"type": "disabled"}`.
31///
32/// On earlier Opus models omitting `thinking` means thinking is off; Claude
33/// Opus 5 flipped that default. Claude Fable 5 and Claude Mythos 5 also think
34/// by default, but they reject `{"type": "disabled"}` with a 400 error, so
35/// they are deliberately excluded here (thinking cannot be turned off for
36/// them at all).
37///
38/// <https://platform.claude.com/docs/en/about-claude/models/migration-guide#migrating-to-claude-opus-5>
39pub fn requires_explicit_thinking_opt_out(model_id: &str) -> bool {
40 matches!(model_id, "claude-opus-5")
41}
42
43pub const FABLE_MODEL_ID_PREFIX: &str = "claude-fable-5";
44pub const FABLE_FALLBACK_MODEL_ID: &str = "claude-opus-4-8";
45
46/// <https://platform.claude.com/docs/en/build-with-claude/compaction>
47pub const COMPACTION_BETA_HEADER: &str = "compact-2026-01-12";
48
49#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
50#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)]
51pub enum AnthropicModelMode {
52 #[default]
53 Default,
54 Thinking {
55 budget_tokens: Option<u32>,
56 },
57 AdaptiveThinking,
58}
59
60/// Capabilities reported by the Anthropic models endpoint for a given model.
61#[derive(Clone, Debug, Default, Deserialize)]
62pub struct ModelCapabilities {
63 #[serde(default)]
64 pub thinking: Option<ThinkingCapability>,
65 #[serde(default)]
66 pub image_input: Option<SupportedCapability>,
67 #[serde(default)]
68 pub effort: Option<EffortCapability>,
69}
70
71#[derive(Clone, Debug, Default, Deserialize)]
72pub struct SupportedCapability {
73 #[serde(default)]
74 pub supported: bool,
75}
76
77#[derive(Clone, Debug, Default, Deserialize)]
78pub struct ThinkingCapability {
79 #[serde(default)]
80 pub supported: bool,
81 #[serde(default)]
82 pub types: Option<ThinkingTypes>,
83}
84
85#[derive(Clone, Debug, Default, Deserialize)]
86pub struct ThinkingTypes {
87 #[serde(default)]
88 pub adaptive: SupportedCapability,
89 #[serde(default)]
90 pub enabled: SupportedCapability,
91}
92
93#[derive(Clone, Debug, Default, Deserialize)]
94pub struct EffortCapability {
95 #[serde(default)]
96 pub supported: bool,
97 #[serde(default)]
98 pub low: Option<SupportedCapability>,
99 #[serde(default)]
100 pub medium: Option<SupportedCapability>,
101 #[serde(default)]
102 pub high: Option<SupportedCapability>,
103 #[serde(default)]
104 pub max: Option<SupportedCapability>,
105 #[serde(default)]
106 pub xhigh: Option<SupportedCapability>,
107}
108
109#[derive(Clone, Debug, PartialEq)]
110pub struct Model {
111 pub id: String,
112 pub display_name: String,
113 pub max_input_tokens: u64,
114 pub max_output_tokens: u64,
115 pub default_temperature: f32,
116 pub mode: AnthropicModelMode,
117 pub supports_thinking: bool,
118 pub supports_adaptive_thinking: bool,
119 pub supports_images: bool,
120 pub supports_speed: bool,
121 pub supports_compaction: bool,
122 pub supported_effort_levels: Vec<Effort>,
123 /// A model id to substitute when invoking tools, used for models that
124 /// don't support tool calling natively.
125 pub tool_override: Option<String>,
126 /// Extra `Anthropic-Beta` header values to send with each request.
127 pub extra_beta_headers: Vec<String>,
128}
129
130impl Model {
131 /// Construct a `Model` from an entry returned by the `/v1/models` listing endpoint.
132 pub fn from_listed(entry: ListModelEntry) -> Self {
133 let supports_thinking = entry
134 .capabilities
135 .as_ref()
136 .and_then(|t| t.thinking.as_ref())
137 .map(|t| t.supported)
138 .unwrap_or(false);
139 let supports_adaptive_thinking = entry
140 .capabilities
141 .as_ref()
142 .and_then(|t| t.thinking.as_ref())
143 .and_then(|t| t.types.as_ref())
144 .map(|types| types.adaptive.supported)
145 .unwrap_or(false);
146 let supports_images = entry
147 .capabilities
148 .as_ref()
149 .and_then(|c| c.image_input.as_ref())
150 .map(|c| c.supported)
151 .unwrap_or(false);
152
153 let mut supported_effort_levels = Vec::new();
154 if let Some(effort) = entry.capabilities.as_ref().and_then(|e| e.effort.as_ref()) {
155 for (level, supported) in [
156 (Effort::Low, effort.low.as_ref()),
157 (Effort::Medium, effort.medium.as_ref()),
158 (Effort::High, effort.high.as_ref()),
159 (Effort::XHigh, effort.xhigh.as_ref()),
160 (Effort::Max, effort.max.as_ref()),
161 ] {
162 if supported.map(|c| c.supported).unwrap_or(false) {
163 supported_effort_levels.push(level);
164 }
165 }
166 }
167
168 let mode = if supports_adaptive_thinking {
169 AnthropicModelMode::AdaptiveThinking
170 } else if supports_thinking {
171 AnthropicModelMode::Thinking {
172 budget_tokens: Some(4_096),
173 }
174 } else {
175 AnthropicModelMode::Default
176 };
177
178 let supports_speed = supports_fast_mode(&entry.id);
179
180 // <https://platform.claude.com/docs/en/build-with-claude/compaction#supported-models>
181 let supports_compaction = matches!(
182 entry.id.as_str(),
183 "claude-fable-5"
184 | "claude-mythos-5"
185 | "claude-mythos-preview"
186 | "claude-opus-5"
187 | "claude-opus-4-8"
188 | "claude-opus-4-7"
189 | "claude-opus-4-6"
190 | "claude-sonnet-4-6"
191 );
192
193 let mut extra_beta_headers = Vec::new();
194 if supports_speed {
195 extra_beta_headers.push(FAST_MODE_BETA_HEADER.to_string());
196 }
197 if supports_compaction {
198 extra_beta_headers.push(COMPACTION_BETA_HEADER.to_string());
199 }
200
201 Self {
202 display_name: entry.display_name,
203 id: entry.id,
204 max_input_tokens: entry.max_input_tokens,
205 max_output_tokens: entry.max_tokens,
206 default_temperature: 1.0,
207 mode,
208 supports_thinking,
209 supports_adaptive_thinking,
210 supports_images,
211 supports_speed,
212 supports_compaction,
213 supported_effort_levels,
214 tool_override: None,
215 extra_beta_headers,
216 }
217 }
218
219 pub fn beta_headers(&self) -> Option<String> {
220 let headers: Vec<&str> = self
221 .extra_beta_headers
222 .iter()
223 .map(|h| h.trim())
224 .filter(|h| !h.is_empty())
225 .collect();
226 if headers.is_empty() {
227 None
228 } else {
229 Some(headers.join(","))
230 }
231 }
232
233 pub fn request_id(&self, has_tools: bool) -> &str {
234 if has_tools {
235 self.tool_override.as_deref().unwrap_or(&self.id)
236 } else {
237 &self.id
238 }
239 }
240}
241
242/// Generate completion with streaming.
243pub async fn stream_completion(
244 client: &dyn HttpClient,
245 api_url: &str,
246 api_key: &str,
247 request: Request,
248 beta_headers: Option<String>,
249 extra_headers: &CustomHeaders,
250) -> Result<BoxStream<'static, Result<Event, AnthropicError>>, AnthropicError> {
251 stream_completion_with_rate_limit_info(
252 client,
253 api_url,
254 api_key,
255 request,
256 beta_headers,
257 extra_headers,
258 )
259 .await
260 .map(|output| output.0)
261}
262
263/// A raw model entry returned by the Anthropic models listing endpoint.
264#[derive(Clone, Debug, Deserialize)]
265pub struct ListModelEntry {
266 pub id: String,
267 pub display_name: String,
268 pub max_input_tokens: u64,
269 pub max_tokens: u64,
270 #[serde(default)]
271 pub capabilities: Option<ModelCapabilities>,
272}
273
274#[derive(Debug, Deserialize)]
275struct ListModelsResponse {
276 data: Vec<ListModelEntry>,
277}
278
279/// Fetch the list of models available to the current API key. The returned
280/// models are constructed by feeding each raw entry through
281/// [`Model::from_listed`].
282///
283/// See https://docs.claude.com/en/api/models-list.
284pub async fn list_models(
285 client: &dyn HttpClient,
286 api_url: &str,
287 api_key: &str,
288 extra_headers: &CustomHeaders,
289) -> Result<Vec<Model>> {
290 let uri = format!("{api_url}/v1/models?limit=1000");
291
292 let request = HttpRequest::builder()
293 .method(Method::GET)
294 .uri(uri)
295 .header("Anthropic-Version", "2023-06-01")
296 .header("X-Api-Key", api_key.trim())
297 .header("Accept", "application/json")
298 .extra_headers(extra_headers)
299 .body(AsyncBody::default())
300 .context("failed to build Anthropic models list request")?;
301
302 let mut response = client
303 .send(request)
304 .await
305 .context("failed to send Anthropic models list request")?;
306
307 let mut body = String::new();
308 response
309 .body_mut()
310 .read_to_string(&mut body)
311 .await
312 .context("failed to read Anthropic models list response")?;
313
314 anyhow::ensure!(
315 response.status().is_success(),
316 "failed to list Anthropic models: {} {}",
317 response.status(),
318 body,
319 );
320
321 let parsed: ListModelsResponse =
322 serde_json::from_str(&body).context("failed to parse Anthropic models list response")?;
323
324 let models = parsed
325 .data
326 .into_iter()
327 .map(Model::from_listed)
328 .collect::<Vec<_>>();
329 Ok(models)
330}
331
332/// Generate completion without streaming.
333pub async fn non_streaming_completion(
334 client: &dyn HttpClient,
335 api_url: &str,
336 api_key: &str,
337 request: Request,
338 beta_headers: Option<String>,
339 extra_headers: &CustomHeaders,
340) -> Result<Response, AnthropicError> {
341 let (mut response, rate_limits) = send_request(
342 client,
343 api_url,
344 api_key,
345 &request,
346 beta_headers,
347 extra_headers,
348 )
349 .await?;
350
351 if response.status().is_success() {
352 let mut body = String::new();
353 response
354 .body_mut()
355 .read_to_string(&mut body)
356 .await
357 .map_err(AnthropicError::ReadResponse)?;
358
359 serde_json::from_str(&body).map_err(AnthropicError::DeserializeResponse)
360 } else {
361 Err(handle_error_response(response, rate_limits).await)
362 }
363}
364
365async fn send_request(
366 client: &dyn HttpClient,
367 api_url: &str,
368 api_key: &str,
369 request: impl Serialize,
370 beta_headers: Option<String>,
371 extra_headers: &CustomHeaders,
372) -> Result<(http::Response<AsyncBody>, RateLimitInfo), AnthropicError> {
373 let uri = format!("{api_url}/v1/messages");
374
375 let mut request_builder = HttpRequest::builder()
376 .method(Method::POST)
377 .uri(uri)
378 .header("Anthropic-Version", "2023-06-01")
379 .header("X-Api-Key", api_key.trim())
380 .header("Content-Type", "application/json");
381
382 if let Some(beta_headers) = beta_headers {
383 request_builder = request_builder.header("Anthropic-Beta", beta_headers);
384 }
385
386 let serialized_request =
387 serde_json::to_string(&request).map_err(AnthropicError::SerializeRequest)?;
388 let request = request_builder
389 .extra_headers(extra_headers)
390 .body(AsyncBody::from(serialized_request))
391 .map_err(AnthropicError::BuildRequestBody)?;
392
393 let response = client
394 .send(request)
395 .await
396 .map_err(AnthropicError::HttpSend)?;
397
398 let rate_limits = RateLimitInfo::from_headers(response.headers());
399
400 Ok((response, rate_limits))
401}
402
403async fn handle_error_response(
404 mut response: http::Response<AsyncBody>,
405 rate_limits: RateLimitInfo,
406) -> AnthropicError {
407 if response.status().as_u16() == 529 {
408 return AnthropicError::ServerOverloaded {
409 retry_after: rate_limits.retry_after,
410 };
411 }
412
413 if let Some(retry_after) = rate_limits.retry_after {
414 return AnthropicError::RateLimit { retry_after };
415 }
416
417 let mut body = String::new();
418 let read_result = response
419 .body_mut()
420 .read_to_string(&mut body)
421 .await
422 .map_err(AnthropicError::ReadResponse);
423
424 if let Err(err) = read_result {
425 return err;
426 }
427
428 match serde_json::from_str::<Event>(&body) {
429 Ok(Event::Error { error }) => AnthropicError::ApiError(error),
430 Ok(_) | Err(_) => AnthropicError::HttpResponseError {
431 status_code: response.status(),
432 message: body,
433 },
434 }
435}
436
437/// An individual rate limit.
438#[derive(Debug)]
439pub struct RateLimit {
440 pub limit: usize,
441 pub remaining: usize,
442 pub reset: DateTime<Utc>,
443}
444
445impl RateLimit {
446 fn from_headers(resource: &str, headers: &HeaderMap<HeaderValue>) -> Result<Self> {
447 let limit =
448 get_header(&format!("anthropic-ratelimit-{resource}-limit"), headers)?.parse()?;
449 let remaining = get_header(
450 &format!("anthropic-ratelimit-{resource}-remaining"),
451 headers,
452 )?
453 .parse()?;
454 let reset = DateTime::parse_from_rfc3339(get_header(
455 &format!("anthropic-ratelimit-{resource}-reset"),
456 headers,
457 )?)?
458 .to_utc();
459
460 Ok(Self {
461 limit,
462 remaining,
463 reset,
464 })
465 }
466}
467
468/// <https://docs.anthropic.com/en/api/rate-limits#response-headers>
469#[derive(Debug)]
470pub struct RateLimitInfo {
471 pub retry_after: Option<Duration>,
472 pub requests: Option<RateLimit>,
473 pub tokens: Option<RateLimit>,
474 pub input_tokens: Option<RateLimit>,
475 pub output_tokens: Option<RateLimit>,
476}
477
478impl RateLimitInfo {
479 fn from_headers(headers: &HeaderMap<HeaderValue>) -> Self {
480 // Check if any rate limit headers exist
481 let has_rate_limit_headers = headers
482 .keys()
483 .any(|k| k == "retry-after" || k.as_str().starts_with("anthropic-ratelimit-"));
484
485 if !has_rate_limit_headers {
486 return Self {
487 retry_after: None,
488 requests: None,
489 tokens: None,
490 input_tokens: None,
491 output_tokens: None,
492 };
493 }
494
495 Self {
496 retry_after: parse_retry_after(headers),
497 requests: RateLimit::from_headers("requests", headers).ok(),
498 tokens: RateLimit::from_headers("tokens", headers).ok(),
499 input_tokens: RateLimit::from_headers("input-tokens", headers).ok(),
500 output_tokens: RateLimit::from_headers("output-tokens", headers).ok(),
501 }
502 }
503}
504
505/// Parses the Retry-After header value as an integer number of seconds (anthropic always uses
506/// seconds). Note that other services might specify an HTTP date or some other format for this
507/// header. Returns `None` if the header is not present or cannot be parsed.
508pub fn parse_retry_after(headers: &HeaderMap<HeaderValue>) -> Option<Duration> {
509 headers
510 .get("retry-after")
511 .and_then(|v| v.to_str().ok())
512 .and_then(|v| v.parse::<u64>().ok())
513 .map(Duration::from_secs)
514}
515
516fn get_header<'a>(key: &str, headers: &'a HeaderMap) -> anyhow::Result<&'a str> {
517 Ok(headers
518 .get(key)
519 .with_context(|| format!("missing header `{key}`"))?
520 .to_str()?)
521}
522
523pub async fn stream_completion_with_rate_limit_info(
524 client: &dyn HttpClient,
525 api_url: &str,
526 api_key: &str,
527 request: Request,
528 beta_headers: Option<String>,
529 extra_headers: &CustomHeaders,
530) -> Result<
531 (
532 BoxStream<'static, Result<Event, AnthropicError>>,
533 Option<RateLimitInfo>,
534 ),
535 AnthropicError,
536> {
537 let request = StreamingRequest {
538 base: request,
539 stream: true,
540 };
541
542 let (response, rate_limits) = send_request(
543 client,
544 api_url,
545 api_key,
546 &request,
547 beta_headers,
548 extra_headers,
549 )
550 .await?;
551
552 if response.status().is_success() {
553 let reader = BufReader::new(response.into_body());
554 let stream = reader
555 .lines()
556 .filter_map(|line| async move {
557 match line {
558 Ok(line) => {
559 let line = line
560 .strip_prefix("data: ")
561 .or_else(|| line.strip_prefix("data:"))?;
562
563 match serde_json::from_str(line) {
564 Ok(response) => Some(Ok(response)),
565 Err(error) => Some(Err(AnthropicError::DeserializeResponse(error))),
566 }
567 }
568 Err(error) => Some(Err(AnthropicError::ReadResponse(error))),
569 }
570 })
571 .boxed();
572 Ok((stream, Some(rate_limits)))
573 } else {
574 Err(handle_error_response(response, rate_limits).await)
575 }
576}
577
578#[derive(Debug, Serialize, Deserialize, Copy, Clone)]
579#[serde(rename_all = "lowercase")]
580pub enum CacheControlType {
581 Ephemeral,
582}
583
584#[derive(Debug, Serialize, Deserialize, Copy, Clone)]
585pub enum CacheTtl {
586 /// Anthropic's default ephemeral TTL (currently 5 minutes). Refreshes for
587 /// free on every cache hit.
588 #[serde(rename = "5m")]
589 FiveMinutes,
590 /// Anthropic's extended ephemeral TTL (currently 1 hour). Costs 2x base
591 /// input tokens to write, but persists across longer idle gaps.
592 #[serde(rename = "1h")]
593 OneHour,
594}
595
596#[derive(Debug, Serialize, Deserialize, Copy, Clone)]
597pub struct CacheControl {
598 #[serde(rename = "type")]
599 pub cache_type: CacheControlType,
600 /// Omitted (None) means the API's default 5-minute TTL. Anthropic requires
601 /// that cache entries with longer TTLs appear before shorter ones in the
602 /// prefix order (tools → system → messages).
603 #[serde(default, skip_serializing_if = "Option::is_none")]
604 pub ttl: Option<CacheTtl>,
605}
606
607#[derive(Debug, Serialize, Deserialize)]
608pub struct Message {
609 pub role: Role,
610 pub content: Vec<RequestContent>,
611}
612
613#[derive(Debug, Serialize, Deserialize, Eq, PartialEq, Hash)]
614#[serde(rename_all = "lowercase")]
615pub enum Role {
616 User,
617 Assistant,
618}
619
620#[derive(Debug, Serialize, Deserialize)]
621#[serde(tag = "type")]
622pub enum RequestContent {
623 #[serde(rename = "text")]
624 Text {
625 text: String,
626 #[serde(skip_serializing_if = "Option::is_none")]
627 cache_control: Option<CacheControl>,
628 },
629 #[serde(rename = "thinking")]
630 Thinking {
631 thinking: String,
632 signature: String,
633 #[serde(skip_serializing_if = "Option::is_none")]
634 cache_control: Option<CacheControl>,
635 },
636 #[serde(rename = "redacted_thinking")]
637 RedactedThinking { data: String },
638 #[serde(rename = "image")]
639 Image {
640 source: ImageSource,
641 #[serde(skip_serializing_if = "Option::is_none")]
642 cache_control: Option<CacheControl>,
643 },
644 #[serde(rename = "tool_use")]
645 ToolUse {
646 id: String,
647 name: String,
648 input: serde_json::Value,
649 #[serde(skip_serializing_if = "Option::is_none")]
650 cache_control: Option<CacheControl>,
651 },
652 #[serde(rename = "tool_result")]
653 ToolResult {
654 tool_use_id: String,
655 is_error: bool,
656 content: ToolResultContent,
657 #[serde(skip_serializing_if = "Option::is_none")]
658 cache_control: Option<CacheControl>,
659 },
660 #[serde(rename = "compaction")]
661 Compaction {
662 content: Option<Arc<str>>,
663 /// Opaque metadata from a prior compaction that must be round-tripped
664 /// verbatim for Anthropic to recognize the block.
665 #[serde(default, skip_serializing_if = "Option::is_none")]
666 encrypted_content: Option<Arc<str>>,
667 #[serde(skip_serializing_if = "Option::is_none")]
668 cache_control: Option<CacheControl>,
669 },
670}
671
672#[derive(Debug, Serialize, Deserialize)]
673#[serde(untagged)]
674pub enum ToolResultContent {
675 Plain(String),
676 Multipart(Vec<ToolResultPart>),
677}
678
679#[derive(Debug, Serialize, Deserialize)]
680#[serde(tag = "type", rename_all = "lowercase")]
681pub enum ToolResultPart {
682 Text { text: String },
683 Image { source: ImageSource },
684}
685
686#[derive(Debug, Serialize, Deserialize)]
687#[serde(tag = "type")]
688pub enum ResponseContent {
689 #[serde(rename = "text")]
690 Text { text: String },
691 #[serde(rename = "thinking")]
692 Thinking { thinking: String },
693 #[serde(rename = "redacted_thinking")]
694 RedactedThinking { data: String },
695 #[serde(rename = "tool_use")]
696 ToolUse {
697 id: String,
698 name: String,
699 input: serde_json::Value,
700 },
701 #[serde(rename = "compaction")]
702 Compaction {
703 content: Option<Arc<str>>,
704 #[serde(default)]
705 encrypted_content: Option<Arc<str>>,
706 },
707}
708
709#[derive(Debug, Serialize, Deserialize)]
710pub struct ImageSource {
711 #[serde(rename = "type")]
712 pub source_type: String,
713 pub media_type: String,
714 pub data: String,
715}
716
717fn is_false(value: &bool) -> bool {
718 !value
719}
720
721#[derive(Debug, Serialize, Deserialize)]
722pub struct Tool {
723 pub name: String,
724 pub description: String,
725 pub input_schema: serde_json::Value,
726 #[serde(default, skip_serializing_if = "is_false")]
727 pub eager_input_streaming: bool,
728 #[serde(default, skip_serializing_if = "Option::is_none")]
729 pub cache_control: Option<CacheControl>,
730}
731
732#[derive(Debug, Serialize, Deserialize)]
733#[serde(tag = "type", rename_all = "lowercase")]
734pub enum ToolChoice {
735 Auto,
736 Any,
737 Tool { name: String },
738 None,
739}
740
741#[derive(Debug, Serialize, Deserialize)]
742#[serde(tag = "type", rename_all = "lowercase")]
743pub enum Thinking {
744 Enabled {
745 budget_tokens: Option<u32>,
746 },
747 Adaptive {
748 #[serde(default, skip_serializing_if = "Option::is_none")]
749 display: Option<AdaptiveThinkingDisplay>,
750 },
751 /// Explicitly turns thinking off. Required by models where thinking runs
752 /// by default (see [`requires_explicit_thinking_opt_out`]); only accepted
753 /// at effort `high` or below.
754 Disabled,
755}
756
757#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
758#[serde(rename_all = "lowercase")]
759pub enum AdaptiveThinkingDisplay {
760 Omitted,
761 Summarized,
762}
763
764#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, EnumString)]
765#[serde(rename_all = "snake_case")]
766#[strum(serialize_all = "snake_case")]
767pub enum Effort {
768 Low,
769 Medium,
770 High,
771 #[serde(rename = "xhigh")]
772 #[strum(serialize = "xhigh")]
773 XHigh,
774 Max,
775}
776
777#[derive(Debug, Clone, Serialize, Deserialize)]
778pub struct OutputConfig {
779 pub effort: Option<Effort>,
780}
781
782#[derive(Debug, Serialize, Deserialize)]
783#[serde(untagged)]
784pub enum StringOrContents {
785 String(String),
786 Content(Vec<RequestContent>),
787}
788
789/// Server-side context management configuration.
790///
791/// <https://platform.claude.com/docs/en/build-with-claude/compaction>
792#[derive(Debug, Clone, Serialize, Deserialize)]
793pub struct ContextManagement {
794 pub edits: Vec<ContextManagementEdit>,
795}
796
797/// A context management edit strategy.
798#[derive(Debug, Clone, Serialize, Deserialize)]
799#[serde(tag = "type")]
800pub enum ContextManagementEdit {
801 #[serde(rename = "compact_20260112")]
802 Compact {
803 #[serde(default, skip_serializing_if = "Option::is_none")]
804 trigger: Option<CompactionTrigger>,
805 },
806}
807
808/// When to trigger server-side compaction.
809#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
810#[serde(tag = "type", rename_all = "snake_case")]
811pub enum CompactionTrigger {
812 InputTokens { value: u64 },
813}
814
815#[derive(Debug, Serialize, Deserialize)]
816pub struct Request {
817 pub model: String,
818 pub max_tokens: u64,
819 pub messages: Vec<Message>,
820 #[serde(default, skip_serializing_if = "Vec::is_empty")]
821 pub tools: Vec<Tool>,
822 #[serde(default, skip_serializing_if = "Option::is_none")]
823 pub thinking: Option<Thinking>,
824 #[serde(default, skip_serializing_if = "Option::is_none")]
825 pub tool_choice: Option<ToolChoice>,
826 #[serde(default, skip_serializing_if = "Option::is_none")]
827 pub system: Option<StringOrContents>,
828 /// Top-level cache_control opts into Anthropic's automatic prompt caching.
829 /// When set, Anthropic places the cache breakpoint on the last cacheable block
830 /// in the request (covering tools + system + the full conversation prefix), so
831 /// we don't have to micromanage per-block breakpoints ourselves.
832 #[serde(default, skip_serializing_if = "Option::is_none")]
833 pub cache_control: Option<CacheControl>,
834 #[serde(default, skip_serializing_if = "Option::is_none")]
835 pub context_management: Option<ContextManagement>,
836 #[serde(default, skip_serializing_if = "Option::is_none")]
837 pub metadata: Option<Metadata>,
838 #[serde(default, skip_serializing_if = "Option::is_none")]
839 pub output_config: Option<OutputConfig>,
840 #[serde(default, skip_serializing_if = "Vec::is_empty")]
841 pub stop_sequences: Vec<String>,
842 #[serde(default, skip_serializing_if = "Option::is_none")]
843 pub speed: Option<Speed>,
844 #[serde(default, skip_serializing_if = "Option::is_none")]
845 pub temperature: Option<f32>,
846 #[serde(default, skip_serializing_if = "Option::is_none")]
847 pub top_k: Option<u32>,
848 #[serde(default, skip_serializing_if = "Option::is_none")]
849 pub top_p: Option<f32>,
850}
851
852#[derive(Debug, Default, Serialize, Deserialize)]
853#[serde(rename_all = "snake_case")]
854pub enum Speed {
855 #[default]
856 Standard,
857 Fast,
858}
859
860#[derive(Debug, Serialize, Deserialize)]
861pub struct StreamingRequest {
862 #[serde(flatten)]
863 pub base: Request,
864 pub stream: bool,
865}
866
867#[derive(Debug, Serialize, Deserialize)]
868pub struct Metadata {
869 pub user_id: Option<String>,
870}
871
872#[derive(Debug, Serialize, Deserialize, Default)]
873pub struct Usage {
874 #[serde(default, skip_serializing_if = "Option::is_none")]
875 pub input_tokens: Option<u64>,
876 #[serde(default, skip_serializing_if = "Option::is_none")]
877 pub output_tokens: Option<u64>,
878 #[serde(default, skip_serializing_if = "Option::is_none")]
879 pub cache_creation_input_tokens: Option<u64>,
880 #[serde(default, skip_serializing_if = "Option::is_none")]
881 pub cache_read_input_tokens: Option<u64>,
882 /// Only populated when a new compaction is triggered during the request.
883 /// The top-level token fields exclude compaction iterations, so total
884 /// billable usage is the sum across all iterations.
885 #[serde(default, skip_serializing_if = "Option::is_none")]
886 pub iterations: Option<Vec<UsageIteration>>,
887}
888
889#[derive(Debug, Serialize, Deserialize)]
890pub struct UsageIteration {
891 #[serde(rename = "type")]
892 pub iteration_type: UsageIterationType,
893 #[serde(default, skip_serializing_if = "Option::is_none")]
894 pub input_tokens: Option<u64>,
895 #[serde(default, skip_serializing_if = "Option::is_none")]
896 pub output_tokens: Option<u64>,
897 #[serde(default, skip_serializing_if = "Option::is_none")]
898 pub cache_creation_input_tokens: Option<u64>,
899 #[serde(default, skip_serializing_if = "Option::is_none")]
900 pub cache_read_input_tokens: Option<u64>,
901}
902
903#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
904#[serde(rename_all = "snake_case")]
905pub enum UsageIterationType {
906 Compaction,
907 Message,
908 #[serde(other)]
909 Unknown,
910}
911
912#[derive(Debug, Serialize, Deserialize)]
913pub struct Response {
914 pub id: String,
915 #[serde(rename = "type")]
916 pub response_type: String,
917 pub role: Role,
918 pub content: Vec<ResponseContent>,
919 pub model: String,
920 #[serde(default, skip_serializing_if = "Option::is_none")]
921 pub stop_reason: Option<String>,
922 #[serde(default, skip_serializing_if = "Option::is_none")]
923 pub stop_sequence: Option<String>,
924 pub usage: Usage,
925}
926
927#[derive(Debug, Serialize, Deserialize)]
928#[serde(tag = "type")]
929pub enum Event {
930 #[serde(rename = "message_start")]
931 MessageStart { message: Response },
932 #[serde(rename = "content_block_start")]
933 ContentBlockStart {
934 index: usize,
935 content_block: ResponseContent,
936 },
937 #[serde(rename = "content_block_delta")]
938 ContentBlockDelta { index: usize, delta: ContentDelta },
939 #[serde(rename = "content_block_stop")]
940 ContentBlockStop { index: usize },
941 #[serde(rename = "message_delta")]
942 MessageDelta { delta: MessageDelta, usage: Usage },
943 #[serde(rename = "message_stop")]
944 MessageStop,
945 #[serde(rename = "ping")]
946 Ping,
947 #[serde(rename = "error")]
948 Error { error: ApiError },
949}
950
951#[derive(Debug, Serialize, Deserialize)]
952#[serde(tag = "type")]
953pub enum ContentDelta {
954 #[serde(rename = "text_delta")]
955 TextDelta { text: String },
956 #[serde(rename = "thinking_delta")]
957 ThinkingDelta { thinking: String },
958 #[serde(rename = "signature_delta")]
959 SignatureDelta { signature: String },
960 #[serde(rename = "input_json_delta")]
961 InputJsonDelta { partial_json: String },
962 #[serde(rename = "compaction_delta")]
963 CompactionDelta {
964 content: Option<Arc<str>>,
965 #[serde(default)]
966 encrypted_content: Option<Arc<str>>,
967 },
968}
969
970#[derive(Debug, Serialize, Deserialize)]
971pub struct MessageDelta {
972 pub stop_reason: Option<String>,
973 pub stop_sequence: Option<String>,
974}
975
976#[derive(Debug)]
977pub enum AnthropicError {
978 /// Failed to serialize the HTTP request body to JSON
979 SerializeRequest(serde_json::Error),
980
981 /// Failed to construct the HTTP request body
982 BuildRequestBody(http::Error),
983
984 /// Failed to send the HTTP request
985 HttpSend(anyhow::Error),
986
987 /// Failed to deserialize the response from JSON
988 DeserializeResponse(serde_json::Error),
989
990 /// Failed to read from response stream
991 ReadResponse(io::Error),
992
993 /// HTTP error response from the API
994 HttpResponseError {
995 status_code: StatusCode,
996 message: String,
997 },
998
999 /// Rate limit exceeded
1000 RateLimit { retry_after: Duration },
1001
1002 /// Server overloaded
1003 ServerOverloaded { retry_after: Option<Duration> },
1004
1005 /// API returned an error response
1006 ApiError(ApiError),
1007}
1008
1009#[derive(Debug, Serialize, Deserialize, Error)]
1010#[error("Anthropic API Error: {error_type}: {message}")]
1011pub struct ApiError {
1012 #[serde(rename = "type")]
1013 pub error_type: String,
1014 pub message: String,
1015}
1016
1017/// An Anthropic API error code.
1018/// <https://docs.anthropic.com/en/api/errors#http-errors>
1019#[derive(Debug, PartialEq, Eq, Clone, Copy, EnumString)]
1020#[strum(serialize_all = "snake_case")]
1021pub enum ApiErrorCode {
1022 /// 400 - `invalid_request_error`: There was an issue with the format or content of your request.
1023 InvalidRequestError,
1024 /// 401 - `authentication_error`: There's an issue with your API key.
1025 AuthenticationError,
1026 /// 403 - `permission_error`: Your API key does not have permission to use the specified resource.
1027 PermissionError,
1028 /// 404 - `not_found_error`: The requested resource was not found.
1029 NotFoundError,
1030 /// 413 - `request_too_large`: Request exceeds the maximum allowed number of bytes.
1031 RequestTooLarge,
1032 /// 429 - `rate_limit_error`: Your account has hit a rate limit.
1033 RateLimitError,
1034 /// 500 - `api_error`: An unexpected error has occurred internal to Anthropic's systems.
1035 ApiError,
1036 /// 529 - `overloaded_error`: Anthropic's API is temporarily overloaded.
1037 OverloadedError,
1038}
1039
1040impl ApiError {
1041 pub fn code(&self) -> Option<ApiErrorCode> {
1042 ApiErrorCode::from_str(&self.error_type).ok()
1043 }
1044
1045 pub fn is_rate_limit_error(&self) -> bool {
1046 matches!(self.error_type.as_str(), "rate_limit_error")
1047 }
1048
1049 pub fn match_window_exceeded(&self) -> Option<u64> {
1050 let Some(ApiErrorCode::InvalidRequestError) = self.code() else {
1051 return None;
1052 };
1053
1054 parse_prompt_too_long(&self.message)
1055 }
1056}
1057
1058pub fn parse_prompt_too_long(message: &str) -> Option<u64> {
1059 message
1060 .strip_prefix("prompt is too long: ")?
1061 .split_once(" tokens")?
1062 .0
1063 .parse()
1064 .ok()
1065}
1066
1067// -- Conversions from/to `language_model_core` types --
1068
1069impl From<language_model_core::Speed> for Speed {
1070 fn from(speed: language_model_core::Speed) -> Self {
1071 match speed {
1072 language_model_core::Speed::Standard => Speed::Standard,
1073 language_model_core::Speed::Fast => Speed::Fast,
1074 }
1075 }
1076}
1077
1078impl From<AnthropicError> for language_model_core::LanguageModelCompletionError {
1079 fn from(error: AnthropicError) -> Self {
1080 completion_error_from_anthropic(error, language_model_core::ANTHROPIC_PROVIDER_NAME)
1081 }
1082}
1083
1084impl From<ApiError> for language_model_core::LanguageModelCompletionError {
1085 fn from(error: ApiError) -> Self {
1086 completion_error_from_anthropic_api(error, language_model_core::ANTHROPIC_PROVIDER_NAME)
1087 }
1088}
1089
1090pub fn completion_error_from_anthropic(
1091 error: AnthropicError,
1092 provider: language_model_core::LanguageModelProviderName,
1093) -> language_model_core::LanguageModelCompletionError {
1094 use language_model_core::LanguageModelCompletionError as Error;
1095 match error {
1096 AnthropicError::SerializeRequest(error) => Error::SerializeRequest { provider, error },
1097 AnthropicError::BuildRequestBody(error) => Error::BuildRequestBody { provider, error },
1098 AnthropicError::HttpSend(error) => Error::HttpSend { provider, error },
1099 AnthropicError::DeserializeResponse(error) => {
1100 Error::DeserializeResponse { provider, error }
1101 }
1102 AnthropicError::ReadResponse(error) => Error::ApiReadResponseError { provider, error },
1103 AnthropicError::HttpResponseError {
1104 status_code,
1105 message,
1106 } => Error::HttpResponseError {
1107 provider,
1108 status_code,
1109 message,
1110 },
1111 AnthropicError::RateLimit { retry_after } => Error::RateLimitExceeded {
1112 provider,
1113 retry_after: Some(retry_after),
1114 },
1115 AnthropicError::ServerOverloaded { retry_after } => Error::ServerOverloaded {
1116 provider,
1117 retry_after,
1118 },
1119 AnthropicError::ApiError(api_error) => {
1120 completion_error_from_anthropic_api(api_error, provider)
1121 }
1122 }
1123}
1124
1125pub fn completion_error_from_anthropic_api(
1126 error: ApiError,
1127 provider: language_model_core::LanguageModelProviderName,
1128) -> language_model_core::LanguageModelCompletionError {
1129 use ApiErrorCode::*;
1130 use language_model_core::LanguageModelCompletionError as Error;
1131 match error.code() {
1132 Some(code) => match code {
1133 InvalidRequestError => Error::BadRequestFormat {
1134 provider,
1135 message: error.message,
1136 },
1137 AuthenticationError => Error::AuthenticationError {
1138 provider,
1139 message: error.message,
1140 },
1141 PermissionError => Error::PermissionError {
1142 provider,
1143 message: error.message,
1144 },
1145 NotFoundError => Error::ApiEndpointNotFound { provider },
1146 RequestTooLarge => Error::PromptTooLarge {
1147 tokens: language_model_core::parse_prompt_too_long(&error.message),
1148 },
1149 RateLimitError => Error::RateLimitExceeded {
1150 provider,
1151 retry_after: None,
1152 },
1153 ApiError => Error::ApiInternalServerError {
1154 provider,
1155 message: error.message,
1156 },
1157 OverloadedError => Error::ServerOverloaded {
1158 provider,
1159 retry_after: None,
1160 },
1161 },
1162 None => Error::Other(error.into()),
1163 }
1164}
1165
1166#[cfg(test)]
1167mod tests {
1168 use super::*;
1169
1170 fn listed_entry(id: &str, capabilities: ModelCapabilities) -> ListModelEntry {
1171 ListModelEntry {
1172 id: id.to_string(),
1173 display_name: id.to_string(),
1174 max_input_tokens: 200_000,
1175 max_tokens: 64_000,
1176 capabilities: Some(capabilities),
1177 }
1178 }
1179
1180 #[test]
1181 fn from_listed_picks_adaptive_thinking_mode() {
1182 let entry = listed_entry(
1183 "claude-test-adaptive",
1184 ModelCapabilities {
1185 thinking: Some(ThinkingCapability {
1186 supported: true,
1187 types: Some(ThinkingTypes {
1188 adaptive: SupportedCapability { supported: true },
1189 enabled: SupportedCapability { supported: true },
1190 }),
1191 }),
1192 ..Default::default()
1193 },
1194 );
1195 let model = Model::from_listed(entry);
1196 assert!(model.supports_thinking);
1197 assert!(model.supports_adaptive_thinking);
1198 assert_eq!(model.mode, AnthropicModelMode::AdaptiveThinking);
1199 }
1200
1201 #[test]
1202 fn from_listed_picks_thinking_mode_when_only_enabled_supported() {
1203 let entry = listed_entry(
1204 "claude-test-thinking",
1205 ModelCapabilities {
1206 thinking: Some(ThinkingCapability {
1207 supported: true,
1208 types: Some(ThinkingTypes {
1209 adaptive: SupportedCapability { supported: false },
1210 enabled: SupportedCapability { supported: true },
1211 }),
1212 }),
1213 ..Default::default()
1214 },
1215 );
1216 let model = Model::from_listed(entry);
1217 assert!(model.supports_thinking);
1218 assert!(!model.supports_adaptive_thinking);
1219 assert!(matches!(model.mode, AnthropicModelMode::Thinking { .. }));
1220 }
1221
1222 #[test]
1223 fn from_listed_default_mode_when_no_thinking() {
1224 let entry = listed_entry("claude-test-default", ModelCapabilities::default());
1225 let model = Model::from_listed(entry);
1226 assert!(!model.supports_thinking);
1227 assert!(!model.supports_adaptive_thinking);
1228 assert_eq!(model.mode, AnthropicModelMode::Default);
1229 }
1230
1231 #[test]
1232 fn from_listed_enables_fast_mode_and_compaction_for_supported_opus_models() {
1233 for model_id in ["claude-opus-5", "claude-opus-4-8"] {
1234 let model = Model::from_listed(listed_entry(model_id, ModelCapabilities::default()));
1235
1236 assert!(model.supports_speed);
1237 let beta_headers = model
1238 .beta_headers()
1239 .expect("model should have beta headers");
1240 assert!(beta_headers.contains(FAST_MODE_BETA_HEADER));
1241 assert!(beta_headers.contains(COMPACTION_BETA_HEADER));
1242 }
1243 }
1244
1245 #[test]
1246 fn from_listed_collects_supported_effort_levels() {
1247 let entry = listed_entry(
1248 "claude-test-effort",
1249 ModelCapabilities {
1250 effort: Some(EffortCapability {
1251 supported: true,
1252 low: Some(SupportedCapability { supported: true }),
1253 medium: Some(SupportedCapability { supported: false }),
1254 high: Some(SupportedCapability { supported: true }),
1255 max: Some(SupportedCapability { supported: true }),
1256 xhigh: Some(SupportedCapability { supported: true }),
1257 }),
1258 ..Default::default()
1259 },
1260 );
1261 let model = Model::from_listed(entry);
1262 assert_eq!(
1263 &model.supported_effort_levels,
1264 &[Effort::Low, Effort::High, Effort::XHigh, Effort::Max]
1265 );
1266 }
1267}
1268
1269#[test]
1270fn test_match_window_exceeded() {
1271 let error = ApiError {
1272 error_type: "invalid_request_error".to_string(),
1273 message: "prompt is too long: 220000 tokens > 200000".to_string(),
1274 };
1275 assert_eq!(error.match_window_exceeded(), Some(220_000));
1276
1277 let error = ApiError {
1278 error_type: "invalid_request_error".to_string(),
1279 message: "prompt is too long: 1234953 tokens".to_string(),
1280 };
1281 assert_eq!(error.match_window_exceeded(), Some(1234953));
1282
1283 let error = ApiError {
1284 error_type: "invalid_request_error".to_string(),
1285 message: "not a prompt length error".to_string(),
1286 };
1287 assert_eq!(error.match_window_exceeded(), None);
1288
1289 let error = ApiError {
1290 error_type: "rate_limit_error".to_string(),
1291 message: "prompt is too long: 12345 tokens".to_string(),
1292 };
1293 assert_eq!(error.match_window_exceeded(), None);
1294
1295 let error = ApiError {
1296 error_type: "invalid_request_error".to_string(),
1297 message: "prompt is too long: invalid tokens".to_string(),
1298 };
1299 assert_eq!(error.match_window_exceeded(), None);
1300}
1301