use dspk_core::{DspError, DspResult as Result};
use serde::{Deserialize, Serialize};
use std::path::Path;
use super::spec::ModelManifest;
pub const EMBEDDED_MODELS_JSON: &str = include_str!("../../catalog/models.json");
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CatalogDocument {
#[serde(default = "default_schema_version")]
pub schema_version: u32,
pub models: Vec<ModelManifest>,
}
fn default_schema_version() -> u32 {
1
}
#[derive(Debug, Clone)]
pub struct ModelCatalog {
models: Vec<ModelManifest>,
}
impl ModelCatalog {
pub fn load_default() -> Result<Self> {
let mut catalog = Self::from_json_str(EMBEDDED_MODELS_JSON)
.map_err(|e| DspError::InvalidConfig(format!("embedded catalog/models.json: {e}")))?;
if let Ok(custom_path) = std::env::var("DSPK_MODEL_CATALOG") {
let trimmed = custom_path.trim();
if !trimmed.is_empty() {
let p = Path::new(trimmed);
if p.exists() {
catalog.merge_file(p)?;
}
}
}
Ok(catalog)
}
pub fn from_json_str(json: &str) -> Result<Self> {
if let Ok(doc) = serde_json::from_str::<CatalogDocument>(json) {
return Ok(Self { models: doc.models });
}
let models: Vec<ModelManifest> =
serde_json::from_str(json).map_err(|e| DspError::InvalidConfig(format!("invalid model catalog JSON: {e}")))?;
Ok(Self { models })
}
pub fn merge_file(&mut self, path: &Path) -> Result<()> {
let content = std::fs::read_to_string(path)
.map_err(|e| DspError::Io(format!("user catalog override {}: {e}", path.display())))?;
let other = Self::from_json_str(&content)
.map_err(|e| DspError::InvalidConfig(format!("user catalog override {}: {e}", path.display())))?;
self.merge(other);
Ok(())
}
pub fn merge(&mut self, other: ModelCatalog) {
for incoming in other.models {
if let Some(existing) = self.models.iter_mut().find(|m| m.id == incoming.id) {
*existing = incoming;
} else {
self.models.push(incoming);
}
}
}
pub fn models(&self) -> &[ModelManifest] {
&self.models
}
pub fn get(&self, id: &str) -> Option<&ModelManifest> {
self.models
.iter()
.find(|m| m.id == id)
.or_else(|| self.models.iter().find(|m| m.id.eq_ignore_ascii_case(id)))
}
pub fn by_family(&self, family: &str) -> Vec<&ModelManifest> {
self.models
.iter()
.filter(|m| m.family.eq_ignore_ascii_case(family))
.collect()
}
}