use crate::config::effort::Effort;
use crate::config::error::LoadError;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
use std::path::Path;
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq, Default)]
pub struct PerRepoProviders {
#[serde(default)]
pub roles: BTreeMap<String, RoleAssignment>,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
pub struct RoleAssignment {
pub provider: String,
pub model: String,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub tools: Vec<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub effort: Option<Effort>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub priority: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub max_output_tokens: Option<u32>,
}
const LEGACY_KEYS: &[&str] = &["providers", "models"];
impl PerRepoProviders {
pub fn parse(raw: &str, path: &Path) -> Result<Self, LoadError> {
let doc: serde_yaml_ng::Value =
serde_yaml_ng::from_str(raw).map_err(|source| LoadError::Yaml {
path: path.to_path_buf(),
source,
})?;
if let Some(map) = doc.as_mapping() {
for legacy in LEGACY_KEYS {
if map.contains_key(*legacy) {
return Err(LoadError::Invalid {
path: path.to_path_buf(),
key: (*legacy).to_string(),
message: format!(
"{legacy:?} block is retired: provider rows are \
brazen's config and models are named directly on \
roles; the per-repo file must only carry the \
'roles:' section (ARCH §4.1, §4.3)",
),
});
}
}
}
let roles_value = doc
.as_mapping()
.and_then(|m| m.get("roles"))
.cloned()
.unwrap_or(serde_yaml_ng::Value::Null);
let roles: BTreeMap<String, RoleAssignment> = if roles_value.is_null() {
BTreeMap::new()
} else {
serde_yaml_ng::from_value(roles_value).map_err(|source| LoadError::Yaml {
path: path.to_path_buf(),
source,
})?
};
Ok(Self { roles })
}
}
#[cfg(test)]
mod tests;