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().map(|l| l.output 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.as_ref().and_then(|m| serde_json::to_string(&m.input).ok());
76        let modality_output = modalities.as_ref().and_then(|m| serde_json::to_string(&m.output).ok());
77        let benchmarks_json = benchmarks.as_ref().and_then(|b| serde_json::to_string(b).ok());
78        let weights_json = weights.as_ref().and_then(|w| serde_json::to_string(w).ok());
79        ModelRow::init()
80            .maybe_model_id(model_id)
81            .maybe_name(name)
82            .maybe_family(family)
83            .maybe_variant(variant)
84            .maybe_version(version)
85            .maybe_attachment(attachment)
86            .maybe_open_weights(open_weights)
87            .maybe_reasoning(reasoning)
88            .maybe_structured_output(structured_output)
89            .maybe_temperature(temperature)
90            .maybe_tool_call(tool_call)
91            .maybe_parameters(parameters)
92            .maybe_release_date(release_date)
93            .maybe_knowledge(knowledge)
94            .maybe_last_updated(last_updated)
95            .maybe_limit_context(limit_context)
96            .maybe_limit_output(limit_output)
97            .maybe_limit_input(limit_input)
98            .maybe_modality_input(modality_input)
99            .maybe_modality_output(modality_output)
100            .maybe_cost_input(cost_input)
101            .maybe_cost_output(cost_output)
102            .maybe_cost_cache_read(cost_cache_read)
103            .maybe_cost_cache_write(cost_cache_write)
104            .maybe_cost_reasoning(cost_reasoning)
105            .maybe_cost_input_audio(cost_input_audio)
106            .maybe_cost_output_audio(cost_output_audio)
107            .maybe_cost_over_200k(cost_over_200k)
108            .maybe_cost_tiers(cost_tiers)
109            .maybe_benchmarks(benchmarks_json)
110            .maybe_weights(weights_json)
111            .build()
112    }
113}
114impl From<ProviderDetails> for ProviderRow {
115    fn from(details: ProviderDetails) -> Self {
116        let provider_id = details.id;
117        let name = details.name;
118        let description = details.description;
119        let endpoint = details.endpoint;
120        let documentation = details.documentation;
121        let authentication = details.authentication.as_ref().and_then(|a| serde_json::to_string(a).ok());
122        let env = details.env.as_ref().and_then(|e| serde_json::to_string(e).ok());
123        let npm = details.npm;
124        let url = details.url;
125        let established_date = details.established_date;
126        let last_updated = details.last_updated;
127        ProviderRow::init()
128            .maybe_provider_id(provider_id)
129            .maybe_name(name)
130            .maybe_description(description)
131            .maybe_endpoint(endpoint)
132            .maybe_documentation(documentation)
133            .maybe_authentication(authentication)
134            .maybe_env(env)
135            .maybe_npm(npm)
136            .maybe_url(url)
137            .maybe_established_date(established_date)
138            .maybe_last_updated(last_updated)
139            .build()
140    }
141}
142#[async_trait]
143impl DatabasePersistence for CatalogResponse {
144    async fn persist(self, database: Database<Table>) -> ApiResult<usize> {
145        match self.models().persist(database.clone()).await {
146            | Ok(model_count) => match self.providers().persist(database).await {
147                | Ok(provider_count) => model_count
148                    .checked_add(provider_count)
149                    .ok_or_else(|| eyre!("Count overflow while persisting models.dev metadata")),
150                | Err(why) => Err(why),
151            },
152            | Err(why) => Err(why),
153        }
154    }
155}
156impl CatalogResponse {
157    pub(crate) fn models(&self) -> Models {
158        Models { models: self.models.clone() }
159    }
160    pub(crate) fn providers(&self) -> Providers {
161        Providers {
162            providers: self.providers.clone(),
163        }
164    }
165}
166#[async_trait]
167impl DatabasePersistence for Models {
168    async fn persist(self, database: Database<Table>) -> ApiResult<usize> {
169        let models: Vec<ModelDetails> = self.models.into_values().collect();
170        let message: fn(&ModelDetails) -> String = |item| format!("Saving \"{}\" model", item.id.as_deref().unwrap_or("unknown"));
171        let operation = |item| async { database.insert(ModelRow::from(item)) };
172        let finish = |count| format!("{}Saved metadata for {count} models", Label::CHECKMARK);
173        with_progress(models, message, operation, finish, None, ProgressType::Bar)
174            .await
175            .map(|counts| counts.into_iter().sum::<usize>())
176            .map_err(Report::msg)
177    }
178}
179#[async_trait]
180impl DatabasePersistence for Providers {
181    async fn persist(self, database: Database<Table>) -> ApiResult<usize> {
182        let providers: Vec<ProviderDetails> = self.providers.into_values().collect();
183        let message: fn(&ProviderDetails) -> String = |item| format!("Saving \"{}\" provider", item.id.as_deref().unwrap_or("unknown"));
184        let operation = |item| async { database.insert(ProviderRow::from(item)) };
185        let finish = |count| format!("{}Saved metadata for {count} providers", Label::CHECKMARK);
186        with_progress(providers, message, operation, finish, None, ProgressType::Bar)
187            .await
188            .map(|counts| counts.into_iter().sum::<usize>())
189            .map_err(Report::msg)
190    }
191}
192/// Download catalog data from models.dev
193///
194/// ## Example
195/// ```ignore
196/// use acorn::io::api::models_dev;
197///
198/// let catalog = models_dev::download().await.unwrap();
199/// println!("Found {} providers and {} models", catalog.providers.len(), catalog.models.len());
200/// ```
201pub async fn download() -> ApiResult<CatalogResponse> {
202    match INCLUDED_ENDPOINTS.find_by_name("models-dev") {
203        | Some(endpoint) => {
204            let response = endpoint.invoke("catalog", None).await;
205            endpoint.handle::<CatalogResponse>(response)
206        }
207        | None => Err(eyre!("No models.dev endpoint found")),
208    }
209}
210/// Download catalog data from models.dev, using a process-wide cache
211pub async fn download_cached() -> ApiResult<&'static CatalogResponse> {
212    match CATALOG_CACHE.get() {
213        | Some(cached) => Ok(cached),
214        | None => match download().await {
215            | Ok(catalog) => {
216                let _ = CATALOG_CACHE.set(catalog);
217                match CATALOG_CACHE.get() {
218                    | Some(cached) => Ok(cached),
219                    | None => Err(eyre!("Cache unset after download — concurrent modification or OOM")),
220                }
221            }
222            | Err(why) => Err(why),
223        },
224    }
225}