synaps_cli/runtime/openai/catalog/
anthropic.rs1use 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}