Skip to main content

foundry_local_sdk/
catalog.rs

1//! Model catalog — discovery and lookup for available models.
2//!
3//! The native catalog (owned by the [`FoundryLocalManager`](crate::FoundryLocalManager))
4//! caches the model list and refreshes itself, so this is a thin async wrapper
5//! that preserves the legacy public surface.
6
7use std::sync::Arc;
8
9use crate::detail::api::Api;
10use crate::detail::manager::NativeManager;
11use crate::detail::model::Model;
12use crate::detail::native::NativeCatalog;
13use crate::detail::task::spawn_blocking;
14use crate::error::{FoundryLocalError, Result};
15
16/// The model catalog provides discovery and lookup for all available models.
17pub struct Catalog {
18    native: NativeCatalog,
19    name: String,
20}
21
22impl Catalog {
23    pub(crate) fn new(
24        api: Arc<Api>,
25        ptr: *mut crate::detail::ffi::flCatalog,
26        manager: Arc<NativeManager>,
27    ) -> Result<Self> {
28        let native = NativeCatalog::new(api, ptr, manager);
29        let name = native.name().unwrap_or_else(|_| "default".into());
30        Ok(Self { native, name })
31    }
32
33    /// Catalog name as reported by the native core.
34    pub fn name(&self) -> &str {
35        &self.name
36    }
37
38    /// Refresh the catalog from the native core.
39    ///
40    /// **No-op.** The native catalog manages its own caching and refresh, so
41    /// there is nothing for the SDK to do here. This method is retained only for
42    /// API compatibility with the legacy SDK and always returns `Ok(())`.
43    pub async fn update_models(&self) -> Result<()> {
44        Ok(())
45    }
46
47    /// Return all known models keyed by alias.
48    pub async fn get_models(&self) -> Result<Vec<Arc<Model>>> {
49        let native = self.native.clone();
50        spawn_blocking(move || {
51            native
52                .get_models()?
53                .into_iter()
54                .map(|m| Model::from_group(&native.api, m).map(Arc::new))
55                .collect()
56        })
57        .await
58    }
59
60    /// Look up a model by its alias.
61    pub async fn get_model(&self, alias: &str) -> Result<Arc<Model>> {
62        if alias.trim().is_empty() {
63            return Err(FoundryLocalError::Validation {
64                reason: "Model alias must be a non-empty string".into(),
65            });
66        }
67        let native = self.native.clone();
68        let alias = alias.to_owned();
69        spawn_blocking(move || match native.get_model(&alias)? {
70            Some(m) => Model::from_group(&native.api, m).map(Arc::new),
71            None => {
72                let available: Vec<String> = native
73                    .get_models()
74                    .ok()
75                    .map(|models| {
76                        models
77                            .iter()
78                            .filter_map(|m| {
79                                m.info_ptr().ok().map(|info| unsafe {
80                                    crate::detail::api::cstr_to_string((native
81                                        .api
82                                        .model_api()
83                                        .Info_GetAlias)(
84                                        info
85                                    ))
86                                    .unwrap_or_default()
87                                })
88                            })
89                            .collect()
90                    })
91                    .unwrap_or_default();
92                Err(FoundryLocalError::ModelOperation {
93                    reason: format!("Unknown model alias '{alias}'. Available: {available:?}"),
94                })
95            }
96        })
97        .await
98    }
99
100    /// Look up a specific model variant by its unique id.
101    ///
102    /// NOTE: This will return a `Model` representing a single variant. Use
103    /// [`get_model`](Catalog::get_model) to obtain a `Model` with all
104    /// available variants.
105    pub async fn get_model_variant(&self, id: &str) -> Result<Arc<Model>> {
106        if id.trim().is_empty() {
107            return Err(FoundryLocalError::Validation {
108                reason: "Variant id must be a non-empty string".into(),
109            });
110        }
111        let native = self.native.clone();
112        let id = id.to_owned();
113        spawn_blocking(move || match native.get_model_variant(&id)? {
114            Some(m) => Model::from_variant(&native.api, m).map(Arc::new),
115            None => Err(FoundryLocalError::ModelOperation {
116                reason: format!("Unknown variant id '{id}'."),
117            }),
118        })
119        .await
120    }
121
122    /// Return only the model variants that are currently cached on disk.
123    pub async fn get_cached_models(&self) -> Result<Vec<Arc<Model>>> {
124        let native = self.native.clone();
125        spawn_blocking(move || {
126            native
127                .get_cached_models()?
128                .into_iter()
129                .map(|m| Model::from_variant(&native.api, m).map(Arc::new))
130                .collect()
131        })
132        .await
133    }
134
135    /// Return model variants that are currently loaded into memory.
136    pub async fn get_loaded_models(&self) -> Result<Vec<Arc<Model>>> {
137        let native = self.native.clone();
138        spawn_blocking(move || {
139            native
140                .get_loaded_models()?
141                .into_iter()
142                .map(|m| Model::from_variant(&native.api, m).map(Arc::new))
143                .collect()
144        })
145        .await
146    }
147
148    /// Return catalog versions for an alias, optionally narrowed to one model name.
149    ///
150    /// `max_versions` limits the results per model name; `0` returns all versions.
151    pub async fn get_model_versions(
152        &self,
153        model_alias: &str,
154        model_name: Option<&str>,
155        max_versions: u32,
156    ) -> Result<Vec<Arc<Model>>> {
157        if model_alias.trim().is_empty() {
158            return Err(FoundryLocalError::Validation {
159                reason: "Model alias must be a non-empty string".into(),
160            });
161        }
162        let max_versions =
163            i32::try_from(max_versions).map_err(|_| FoundryLocalError::Validation {
164                reason: "max_versions must not exceed i32::MAX".into(),
165            })?;
166        let native = self.native.clone();
167        let model_alias = model_alias.to_owned();
168        let model_name = model_name.map(str::to_owned);
169        spawn_blocking(move || {
170            native
171                .get_model_versions(&model_alias, model_name.as_deref(), max_versions)?
172                .into_iter()
173                .map(|m| Model::from_variant(&native.api, m).map(Arc::new))
174                .collect()
175        })
176        .await
177    }
178
179    /// Resolve the latest catalog version for the provided model or variant.
180    pub async fn get_latest_version(&self, model_or_model_variant: &Model) -> Result<Arc<Model>> {
181        let native = self.native.clone();
182        let target = model_or_model_variant.selected_native().clone();
183        spawn_blocking(move || {
184            let latest = native.get_latest_version(&target)?;
185            Model::from_variant(&native.api, latest).map(Arc::new)
186        })
187        .await
188    }
189}