Skip to main content

aither_claude/
provider.rs

1//! Claude provider for listing and instantiating models.
2
3use crate::{
4    Claude,
5    constant::{ANTHROPIC_VERSION, CLAUDE_BASE_URL},
6    error::ClaudeError,
7};
8use aither_core::llm::{
9    LanguageModelProvider, model::Profile as ModelProfile, provider::Profile as ProviderProfile,
10};
11use aither_models::lookup as lookup_model_info;
12use serde::Deserialize;
13use std::{future::Future, sync::Arc};
14use zenwave::{Client, client};
15
16/// Provider capable of listing and instantiating `Claude` models.
17#[derive(Clone, Debug)]
18pub struct ClaudeProvider {
19    inner: Arc<ProviderConfig>,
20}
21
22impl ClaudeProvider {
23    /// Create a new provider with the given API key.
24    pub fn new(api_key: impl Into<String>) -> Self {
25        Self {
26            inner: Arc::new(ProviderConfig {
27                api_key: api_key.into(),
28                base_url: CLAUDE_BASE_URL.to_string(),
29            }),
30        }
31    }
32
33    /// Override the REST base URL.
34    #[must_use]
35    pub fn base_url(mut self, url: impl Into<String>) -> Self {
36        Arc::make_mut(&mut self.inner).base_url = url.into();
37        self
38    }
39}
40
41impl LanguageModelProvider for ClaudeProvider {
42    type Model = Claude;
43    type Error = ClaudeError;
44
45    fn list_models(&self) -> impl Future<Output = Result<Vec<ModelProfile>, Self::Error>> + Send {
46        let cfg = self.inner.clone();
47        async move {
48            let endpoint = format!("{}/models", cfg.base_url.trim_end_matches('/'));
49            let mut backend = client();
50            let response: ModelListResponse = backend
51                .get(endpoint)
52                .map_err(ClaudeError::Http)?
53                .header("x-api-key", cfg.api_key.clone())
54                .map_err(ClaudeError::Http)?
55                .header("anthropic-version", ANTHROPIC_VERSION)
56                .map_err(ClaudeError::Http)?
57                .json()
58                .await
59                .map_err(ClaudeError::Http)?;
60
61            Ok(response
62                .data
63                .into_iter()
64                .filter_map(|model| {
65                    let context_length = model
66                        .input_token_limit
67                        .or(model.context_length)
68                        .or(model.max_tokens)
69                        .or_else(|| lookup_model_info(&model.id).and_then(aither_models::ModelEntry::max_input_tokens));
70                    let Some(context_length) = context_length else {
71                        tracing::warn!(
72                            model = %model.id,
73                            "skip model from Claude list: missing context_length and no aither-models fallback"
74                        );
75                        return None;
76                    };
77                    Some(ModelProfile::new(
78                        model.id.clone(),
79                        "anthropic",
80                        model.id,
81                        model.display_name,
82                        context_length,
83                    ))
84                })
85                .collect())
86        }
87    }
88
89    fn get_model(
90        &self,
91        name: &str,
92    ) -> impl Future<Output = Result<Self::Model, Self::Error>> + Send {
93        let cfg = self.inner.clone();
94        let name = name.to_string();
95        async move {
96            Ok(Claude::new(cfg.api_key.clone())
97                .with_base_url(cfg.base_url.clone())
98                .with_model(name))
99        }
100    }
101
102    fn profile() -> ProviderProfile {
103        ProviderProfile::new("anthropic", "Anthropic Claude API provider")
104    }
105}
106
107#[derive(Debug, Clone)]
108struct ProviderConfig {
109    api_key: String,
110    base_url: String,
111}
112
113#[derive(Debug, Deserialize)]
114struct ModelListResponse {
115    data: Vec<ModelDescriptor>,
116}
117
118#[derive(Debug, Deserialize)]
119struct ModelDescriptor {
120    id: String,
121    display_name: String,
122    #[serde(default, alias = "inputTokenLimit")]
123    input_token_limit: Option<u32>,
124    #[serde(default)]
125    context_length: Option<u32>,
126    #[serde(default)]
127    max_tokens: Option<u32>,
128}