acorn/io/api/
models_dev.rs1use 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
16static CATALOG_CACHE: OnceLock<CatalogResponse> = OnceLock::new();
18#[derive(Clone, Debug, Deserialize)]
20pub struct CatalogResponse {
21 pub models: HashMap<String, ModelDetails>,
23 pub providers: HashMap<String, ProviderDetails>,
25}
26#[derive(Clone, Debug, Deserialize)]
28pub struct Models {
29 pub models: HashMap<String, ModelDetails>,
31}
32#[derive(Clone, Debug, Deserialize)]
34pub struct Providers {
35 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}
213pub 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}
231pub 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}