aither_claude/
provider.rs1use 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#[derive(Clone, Debug)]
18pub struct ClaudeProvider {
19 inner: Arc<ProviderConfig>,
20}
21
22impl ClaudeProvider {
23 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 #[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}