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().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}
192pub 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}
210pub 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}