Skip to main content

acorn/io/api/
models_dev.rs

1//! Module for downloading model and provider data from models.dev
2//!
3//! See <https://models.dev> for more information.
4use crate::io::api::{ApiResult, DatabasePersistence, RemoteResource, INCLUDED_ENDPOINTS};
5use crate::io::database::schema::{ModelRow, ProviderRow, Table};
6use crate::io::database::{Database, Operations};
7use crate::io::{with_progress, ProgressType};
8use crate::schema::agent::{ModelDetails, ProviderDetails};
9use crate::util::{Label, Searchable};
10use async_trait::async_trait;
11use color_eyre::eyre::{eyre, Report};
12use serde::Deserialize;
13use std::collections::HashMap;
14use std::sync::OnceLock;
15
16/// Process-global cache of the models.dev catalog to avoid redundant downloads
17static CATALOG_CACHE: OnceLock<CatalogResponse> = OnceLock::new();
18/// Top-level catalog response from models.dev
19#[derive(Clone, Debug, Deserialize)]
20pub struct CatalogResponse {
21    /// Flat map of all models keyed by model ID
22    pub models: HashMap<String, ModelDetails>,
23    /// Provider entries keyed by provider ID
24    pub providers: HashMap<String, ProviderDetails>,
25}
26/// Response containing only model data from models.dev
27#[derive(Clone, Debug, Deserialize)]
28pub struct Models {
29    /// Flat map of all models keyed by model ID
30    pub models: HashMap<String, ModelDetails>,
31}
32/// Response containing only provider data from models.dev
33#[derive(Clone, Debug, Deserialize)]
34pub struct Providers {
35    /// Provider entries keyed by provider ID
36    pub providers: HashMap<String, ProviderDetails>,
37}
38impl From<ModelDetails> for ModelRow {
39    fn from(details: ModelDetails) -> Self {
40        let model_id = details.id;
41        let name = details.name;
42        let family = details.family;
43        let variant = details.variant;
44        let version = details.version.as_ref().map(|v| v.to_string());
45        let attachment = details.attachment;
46        let open_weights = details.open_weights;
47        let reasoning = details.reasoning;
48        let structured_output = details.structured_output;
49        let temperature = details.temperature;
50        let tool_call = details.tool_call;
51        let parameters = details.parameters;
52        let release_date = details.release_date;
53        let knowledge = details.knowledge;
54        let last_updated = details.last_updated;
55        let limit = details.limit;
56        let cost = details.cost;
57        let modalities = details.modalities;
58        let benchmarks = details.benchmarks;
59        let weights = details.weights;
60        let limit_context = limit.as_ref().map(|l| l.context as i64);
61        let limit_output = limit.as_ref().and_then(|l| l.output.map(|v| v as i64));
62        let limit_input = limit.as_ref().and_then(|l| l.input.map(|v| v as i64));
63        let cost_input = cost.as_ref().and_then(|c| c.input);
64        let cost_output = cost.as_ref().and_then(|c| c.output);
65        let cost_cache_read = cost.as_ref().and_then(|c| c.cache_read);
66        let cost_cache_write = cost.as_ref().and_then(|c| c.cache_write);
67        let cost_reasoning = cost.as_ref().and_then(|c| c.reasoning);
68        let cost_input_audio = cost.as_ref().and_then(|c| c.input_audio);
69        let cost_output_audio = cost.as_ref().and_then(|c| c.output_audio);
70        let cost_over_200k = cost
71            .as_ref()
72            .and_then(|c| c.context_over_200k.as_ref())
73            .and_then(|c| serde_json::to_string(c).ok());
74        let cost_tiers = cost.as_ref().and_then(|c| c.tiers.as_ref()).and_then(|t| serde_json::to_string(t).ok());
75        let modality_input = modalities
76            .as_ref()
77            .map(|m| &m.input)
78            .filter(|v| !v.is_empty())
79            .map(|v| v.iter().map(ToString::to_string).collect::<Vec<_>>())
80            .map(|v| v.join(","));
81        let modality_output = modalities
82            .as_ref()
83            .map(|m| &m.output)
84            .filter(|v| !v.is_empty())
85            .map(|v| v.iter().map(ToString::to_string).collect::<Vec<_>>())
86            .map(|v| v.join(","));
87        let benchmarks_json = benchmarks.as_ref().and_then(|b| serde_json::to_string(b).ok());
88        let mut row = ModelRow::init()
89            .maybe_model_id(model_id.clone())
90            .maybe_name(name)
91            .maybe_family(family)
92            .maybe_variant(variant)
93            .maybe_version(version)
94            .maybe_attachment(attachment)
95            .maybe_open_weights(open_weights)
96            .maybe_reasoning(reasoning)
97            .maybe_structured_output(structured_output)
98            .maybe_temperature(temperature)
99            .maybe_tool_call(tool_call)
100            .maybe_parameters(parameters)
101            .maybe_release_date(release_date)
102            .maybe_knowledge(knowledge)
103            .maybe_last_updated(last_updated)
104            .maybe_limit_context(limit_context)
105            .maybe_limit_output(limit_output)
106            .maybe_limit_input(limit_input)
107            .maybe_modality_input(modality_input)
108            .maybe_modality_output(modality_output)
109            .maybe_cost_input(cost_input)
110            .maybe_cost_output(cost_output)
111            .maybe_cost_cache_read(cost_cache_read)
112            .maybe_cost_cache_write(cost_cache_write)
113            .maybe_cost_reasoning(cost_reasoning)
114            .maybe_cost_input_audio(cost_input_audio)
115            .maybe_cost_output_audio(cost_output_audio)
116            .maybe_cost_over_200k(cost_over_200k)
117            .maybe_cost_tiers(cost_tiers)
118            .maybe_benchmarks(benchmarks_json)
119            .build();
120        row.weights = weights
121            .unwrap_or_default()
122            .infer_quantization(model_id.as_deref().unwrap_or_default())
123            .as_ref()
124            .and_then(|w| serde_json::to_string(w).ok());
125        row
126    }
127}
128impl From<ProviderDetails> for ProviderRow {
129    fn from(details: ProviderDetails) -> Self {
130        let provider_id = details.id;
131        let name = details.name;
132        let description = details.description;
133        let endpoint = details.endpoint;
134        let documentation = details.documentation;
135        let authentication = details.authentication.as_ref().and_then(|a| serde_json::to_string(a).ok());
136        let env = details.env.filter(|e| !e.is_empty()).map(|e| e.join(","));
137        let npm = details.npm;
138        let url = details.url;
139        let established_date = details.established_date;
140        let last_updated = details.last_updated;
141        let models = details
142            .models
143            .as_ref()
144            .map(|m| m.iter().filter_map(|d| d.id.clone()).collect::<Vec<_>>())
145            .filter(|ids| !ids.is_empty())
146            .map(|ids| ids.join(","));
147        ProviderRow::init()
148            .maybe_provider_id(provider_id)
149            .maybe_name(name)
150            .maybe_description(description)
151            .maybe_endpoint(endpoint)
152            .maybe_documentation(documentation)
153            .maybe_authentication(authentication)
154            .maybe_env(env)
155            .maybe_npm(npm)
156            .maybe_url(url)
157            .maybe_established_date(established_date)
158            .maybe_last_updated(last_updated)
159            .maybe_models(models)
160            .build()
161    }
162}
163#[async_trait]
164impl DatabasePersistence for CatalogResponse {
165    async fn persist(self, database: Database<Table>) -> ApiResult<usize> {
166        match self.models().persist(database.clone()).await {
167            | Ok(model_count) => match self.providers().persist(database).await {
168                | Ok(provider_count) => model_count
169                    .checked_add(provider_count)
170                    .ok_or_else(|| eyre!("Count overflow while persisting models.dev metadata")),
171                | Err(why) => Err(why),
172            },
173            | Err(why) => Err(why),
174        }
175    }
176}
177impl CatalogResponse {
178    pub(crate) fn models(&self) -> Models {
179        Models { models: self.models.clone() }
180    }
181    pub(crate) fn providers(&self) -> Providers {
182        Providers {
183            providers: self.providers.clone(),
184        }
185    }
186}
187#[async_trait]
188impl DatabasePersistence for Models {
189    async fn persist(self, database: Database<Table>) -> ApiResult<usize> {
190        let models: Vec<ModelDetails> = self.models.into_values().collect();
191        let message: fn(&ModelDetails) -> String = |item| format!("Saving \"{}\" model", item.id.as_deref().unwrap_or("unknown"));
192        let operation = |item| async { database.insert(ModelRow::from(item)) };
193        let finish = |count| format!("{}Saved metadata for {count} models", Label::CHECKMARK);
194        with_progress(models, message, operation, finish, None, ProgressType::Bar)
195            .await
196            .map(|counts| counts.into_iter().sum::<usize>())
197            .map_err(Report::msg)
198    }
199}
200#[async_trait]
201impl DatabasePersistence for Providers {
202    async fn persist(self, database: Database<Table>) -> ApiResult<usize> {
203        let providers: Vec<ProviderDetails> = self.providers.into_values().collect();
204        let message: fn(&ProviderDetails) -> String = |item| format!("Saving \"{}\" provider", item.id.as_deref().unwrap_or("unknown"));
205        let operation = |item| async { database.insert(ProviderRow::from(item)) };
206        let finish = |count| format!("{}Saved metadata for {count} providers", Label::CHECKMARK);
207        with_progress(providers, message, operation, finish, None, ProgressType::Bar)
208            .await
209            .map(|counts| counts.into_iter().sum::<usize>())
210            .map_err(Report::msg)
211    }
212}
213/// Download catalog data from models.dev
214///
215/// ## Example
216/// ```ignore
217/// use acorn::io::api::models_dev;
218///
219/// let catalog = models_dev::download().await.unwrap();
220/// println!("Found {} providers and {} models", catalog.providers.len(), catalog.models.len());
221/// ```
222pub async fn download() -> ApiResult<CatalogResponse> {
223    match INCLUDED_ENDPOINTS.find_by_name("models-dev") {
224        | Some(endpoint) => {
225            let response = endpoint.invoke("catalog", None).await;
226            endpoint.handle::<CatalogResponse>(response)
227        }
228        | None => Err(eyre!("No models.dev endpoint found")),
229    }
230}
231/// Download catalog data from models.dev, using a process-wide cache
232pub async fn download_cached() -> ApiResult<&'static CatalogResponse> {
233    match CATALOG_CACHE.get() {
234        | Some(cached) => Ok(cached),
235        | None => match download().await {
236            | Ok(catalog) => {
237                let _ = CATALOG_CACHE.set(catalog);
238                match CATALOG_CACHE.get() {
239                    | Some(cached) => Ok(cached),
240                    | None => Err(eyre!("Cache unset after download — concurrent modification or OOM")),
241                }
242            }
243            | Err(why) => Err(why),
244        },
245    }
246}