use std::collections::{HashMap, HashSet};
use serde::{Deserialize, Serialize};
use super::ConfigError;
use crate::config::{parse_embedding_model_alias, sanitize_key};
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
#[serde(deny_unknown_fields)]
pub struct EngineConfig {
pub name: String,
#[serde(default = "unit_weight")]
pub weight: f64,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub dims: Option<i64>,
}
impl EngineConfig {
pub(crate) fn check_dimensions(&self, actual: usize) -> Result<(), ConfigError> {
if let Some(expected) = self.dims {
if usize::try_from(expected).ok() != Some(actual) {
return Err(ConfigError::EngineDimensionMismatch {
name: self.name.clone(),
expected,
actual,
});
}
}
Ok(())
}
}
fn unit_weight() -> f64 {
1.0
}
pub(crate) fn canonical_engine_name(name: &str) -> String {
parse_embedding_model_alias(name)
.map(|model| model.to_string())
.unwrap_or_else(|| name.to_owned())
}
pub(crate) fn validate_peer_engines(engines: &[EngineConfig]) -> Result<(), ConfigError> {
let mut names = HashSet::new();
let mut canonical_names = HashSet::new();
let mut keys = HashMap::new();
for engine in engines {
if engine.name.trim().is_empty() {
return Err(ConfigError::InvalidEngineName {
name: engine.name.clone(),
});
}
if !names.insert(&engine.name) {
return Err(ConfigError::DuplicateName {
name: engine.name.clone(),
});
}
let canonical = canonical_engine_name(&engine.name);
if !canonical_names.insert(canonical.clone()) {
return Err(ConfigError::AliasCollision {
name: engine.name.clone(),
canonical,
});
}
let key = sanitize_key(&canonical);
if let Some(other) = keys.insert(key.clone(), canonical.clone()) {
return Err(ConfigError::EngineKeyCollision {
name: canonical,
other,
key,
});
}
if !engine.weight.is_finite() || engine.weight <= 0.0 {
return Err(ConfigError::InvalidEngineWeight {
name: engine.name.clone(),
value: engine.weight,
});
}
if let Some(value) = engine.dims {
if value <= 0 || value > i64::from(u32::MAX) {
return Err(ConfigError::InvalidEngineDimensions {
name: engine.name.clone(),
value,
});
}
}
}
if let Some(engine) = engines.iter().find(|engine| engine.weight != 1.0) {
return Err(ConfigError::UnsupportedEngineWeight {
name: engine.name.clone(),
});
}
Ok(())
}
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct LegacyEngine {
name: String,
model: String,
#[serde(default)]
default: bool,
fusion_weight: Option<f64>,
dims: Option<i64>,
}
pub(super) fn normalize_engine_input(
document: &mut toml::Value,
path: &std::path::Path,
) -> Result<bool, ConfigError> {
let Some(value) = document.get_mut("engines") else {
return Ok(false);
};
let Some(entries) = value.as_array_mut() else {
return Ok(true);
};
let legacy = entries.iter().any(|entry| {
["model", "default", "fusion_weight"]
.iter()
.any(|key| entry.get(key).is_some())
});
if !legacy {
return Ok(true);
}
let mut parsed = Vec::with_capacity(entries.len());
let mut labels = HashSet::new();
let mut providers = HashSet::new();
for entry in entries.iter() {
if entry.get("weight").is_some() || entry.get("model").is_none() {
return Err(ConfigError::EngineKeyConflict {
name: entry
.get("name")
.and_then(toml::Value::as_str)
.unwrap_or("<missing>")
.to_owned(),
});
}
let old: LegacyEngine = entry
.clone()
.try_into()
.map_err(|source| ConfigError::Parse {
path: path.to_path_buf(),
source,
})?;
if !labels.insert(old.name.clone()) {
return Err(ConfigError::DuplicateName { name: old.name });
}
let model =
parse_embedding_model_alias(&old.model).ok_or_else(|| ConfigError::UnknownModel {
name: old.name.clone(),
model: old.model.clone(),
})?;
let name = model.to_string();
if !providers.insert(name.clone()) {
return Err(ConfigError::AliasCollision {
name: old.name,
canonical: name,
});
}
parsed.push((
old.default,
EngineConfig {
name,
weight: old.fusion_weight.unwrap_or(1.0),
dims: old.dims,
},
));
}
let found = parsed.iter().filter(|(default, _)| *default).count();
if found != 1 {
return Err(ConfigError::DefaultCount { found });
}
let first = parsed
.iter()
.position(|(default, _)| *default)
.expect("one default");
let primary = parsed.remove(first);
parsed.insert(0, primary);
*entries = parsed
.into_iter()
.map(|(_, engine)| {
let mut table = toml::map::Map::new();
table.insert("name".into(), toml::Value::String(engine.name));
table.insert("weight".into(), toml::Value::Float(engine.weight));
if let Some(dims) = engine.dims {
table.insert("dims".into(), toml::Value::Integer(dims));
}
toml::Value::Table(table)
})
.collect();
tracing::warn!("legacy engine configuration converted to ordered peers; migrate model/default/fusion_weight to name/weight");
Ok(true)
}