Skip to repository content

tenant.openagents/omega

No repository description is available.

OpenAgents Git authority 2026-07-29T04:12:44.819Z Public web read
NIP-34 coordinate30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omega
MaintainersHidden in public view
References2 branches · 1 tag
Read-only clonegit clone https://openagents.com/git/tenant.openagents/omega.git
Browse files

anthropic.rs

1301 lines · 41.2 KB · rust
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
Served at tenant.openagents/omega Member data and write actions are omitted.