Skip to main content

synaps_cli/runtime/openai/catalog/
anthropic.rs

1use super::*;
2use serde::Deserialize;
3
4pub(super) const ANTHROPIC_MODELS_URL: &str = "https://api.anthropic.com/v1/models";
5pub(super) const ANTHROPIC_MODELS_PAGE_LIMIT: usize = 100;
6
7#[derive(Debug, Clone, PartialEq, Eq)]
8pub struct AnthropicCatalogPage {
9    pub models: Vec<CatalogModel>,
10    pub has_more: bool,
11    pub last_id: Option<String>,
12}
13
14#[derive(Debug, Deserialize)]
15struct AnthropicModelsPage {
16    data: Vec<AnthropicModelItem>,
17    #[serde(default)]
18    has_more: bool,
19    #[serde(default)]
20    last_id: Option<String>,
21}
22
23#[derive(Debug, Deserialize)]
24struct AnthropicModelItem {
25    id: String,
26    #[serde(default)]
27    display_name: Option<String>,
28    #[serde(default)]
29    max_input_tokens: Option<u64>,
30    #[serde(default)]
31    max_tokens: Option<u64>,
32    #[serde(default)]
33    capabilities: Option<AnthropicCapabilities>,
34}
35
36#[derive(Debug, Deserialize)]
37struct AnthropicCapabilities {
38    #[serde(default)]
39    thinking: Option<CapabilitySupported>,
40    #[serde(default)]
41    effort: Option<AnthropicEffortCapability>,
42}
43
44#[derive(Debug, Deserialize)]
45struct CapabilitySupported {
46    #[serde(default)]
47    supported: bool,
48}
49
50#[derive(Debug, Deserialize)]
51struct AnthropicEffortCapability {
52    #[serde(default)]
53    supported: bool,
54}
55
56pub fn parse_anthropic_catalog_page(body: &str) -> Result<AnthropicCatalogPage, serde_json::Error> {
57    let page: AnthropicModelsPage = serde_json::from_str(body)?;
58    let models = page
59        .data
60        .into_iter()
61        .filter_map(|item| {
62            let mut m = CatalogModel::new("anthropic", "Anthropic", item.id)?;
63            m.provider_kind = CatalogProviderKind::Anthropic;
64            m.label = item.display_name.filter(|name| !name.trim().is_empty());
65            m.context_tokens = item.max_input_tokens;
66            m.max_output_tokens = item.max_tokens;
67            m.reasoning = match item.capabilities {
68                Some(caps) if caps.thinking.as_ref().is_some_and(|c| c.supported) => {
69                    ReasoningSupport::AnthropicAdaptive {
70                        adaptive: caps.effort.as_ref().is_some_and(|c| c.supported),
71                    }
72                }
73                _ => ReasoningSupport::Unknown,
74            };
75            m.source = CatalogSource::Live;
76            Some(m)
77        })
78        .collect();
79
80    Ok(AnthropicCatalogPage {
81        models,
82        has_more: page.has_more,
83        last_id: page.last_id.filter(|id| !id.trim().is_empty()),
84    })
85}
86
87pub fn parse_anthropic_catalog_models(body: &str) -> Result<Vec<CatalogModel>, serde_json::Error> {
88    parse_anthropic_catalog_page(body).map(|page| page.models)
89}
90
91pub fn anthropic_models_url(after_id: Option<&str>) -> String {
92    let mut url = format!("{ANTHROPIC_MODELS_URL}?limit={ANTHROPIC_MODELS_PAGE_LIMIT}");
93    if let Some(after_id) = after_id.filter(|id| !id.trim().is_empty()) {
94        url.push_str("&after_id=");
95        url.push_str(after_id);
96    }
97    url
98}
99
100pub fn merge_catalog_pages(pages: Vec<Vec<CatalogModel>>) -> Vec<CatalogModel> {
101    let mut seen = std::collections::BTreeSet::new();
102    let mut merged = Vec::new();
103    for page in pages {
104        for model in page {
105            if seen.insert(model.id.clone()) {
106                merged.push(model);
107            }
108        }
109    }
110    merged
111}