use crate::omo_config::error::{AgentConfigError, Result};
use crate::omo_config::types::{FallbackModelEntry, FallbackModels, OhMyOpencodeConfig};
use std::collections::HashSet;
pub fn validate_agent_config(config: &OhMyOpencodeConfig) -> Result<()> {
validate_agent_config_with_models(config, None)
}
pub fn validate_agent_config_with_models(
config: &OhMyOpencodeConfig,
available_models: Option<&HashSet<String>>,
) -> Result<()> {
let mut errors = Vec::new();
if let Some(ref order) = config.agent_order {
if order.len() > 64 {
errors.push(format!(
"agent_order exceeds maximum length of 64 (got {})",
order.len()
));
}
for (i, item) in order.iter().enumerate() {
if item.len() > 128 {
errors.push(format!(
"agent_order[{}] exceeds maximum length of 128 (got {})",
i,
item.len()
));
}
}
}
if let Some(ref agents) = config.agents {
if let Some(ref build) = agents.build {
validate_agent_definition(build, "agents.build", &mut errors, available_models);
}
if let Some(ref plan) = agents.plan {
validate_agent_definition(plan, "agents.plan", &mut errors, available_models);
}
if let Some(ref sisyphus) = agents.sisyphus {
validate_agent_definition(sisyphus, "agents.sisyphus", &mut errors, available_models);
}
if let Some(ref hephaestus) = agents.hephaestus {
validate_agent_definition(
hephaestus,
"agents.hephaestus",
&mut errors,
available_models,
);
}
if let Some(ref prometheus) = agents.prometheus {
validate_agent_definition(
prometheus,
"agents.prometheus",
&mut errors,
available_models,
);
}
if let Some(ref oracle) = agents.oracle {
validate_agent_definition(oracle, "agents.oracle", &mut errors, available_models);
}
if let Some(ref librarian) = agents.librarian {
validate_agent_definition(librarian, "agents.librarian", &mut errors, available_models);
}
if let Some(ref explore) = agents.explore {
validate_agent_definition(explore, "agents.explore", &mut errors, available_models);
}
if let Some(ref multimodal_looker) = agents.multimodal_looker {
validate_agent_definition(
multimodal_looker,
"agents.multimodal-looker",
&mut errors,
available_models,
);
}
if let Some(ref metis) = agents.metis {
validate_agent_definition(metis, "agents.metis", &mut errors, available_models);
}
if let Some(ref momus) = agents.momus {
validate_agent_definition(momus, "agents.momus", &mut errors, available_models);
}
if let Some(ref atlas) = agents.atlas {
validate_agent_definition(atlas, "agents.atlas", &mut errors, available_models);
}
for (name, agent) in &agents.custom {
validate_agent_definition(
agent,
&format!("agents.{}", name),
&mut errors,
available_models,
);
}
}
if errors.is_empty() {
Ok(())
} else {
Err(AgentConfigError::ValidationError(errors.join("; ")))
}
}
fn check_model_available(
model_id: &str,
available: &HashSet<String>,
path: &str,
errors: &mut Vec<String>,
) {
let base_model = model_id.split(':').next().unwrap_or(model_id);
if !available.contains(base_model) && !available.contains(model_id) {
errors.push(format!(
"{}: model '{}' not found in available models (run 'opencode models' to see available models)",
path, model_id
));
}
}
fn validate_fallback_models(
fallback: &FallbackModels,
available: &HashSet<String>,
path: &str,
errors: &mut Vec<String>,
) {
match fallback {
FallbackModels::Single(id) => {
check_model_available(id, available, &format!("{}.fallback_models", path), errors);
}
FallbackModels::StringList(ids) => {
for (i, id) in ids.iter().enumerate() {
check_model_available(
id,
available,
&format!("{}.fallback_models[{}]", path, i),
errors,
);
}
}
FallbackModels::DetailedList(specs) => {
for (i, spec) in specs.iter().enumerate() {
check_model_available(
&spec.model,
available,
&format!("{}.fallback_models[{}]", path, i),
errors,
);
}
}
FallbackModels::MixedList(entries) => {
for (i, entry) in entries.iter().enumerate() {
match entry {
FallbackModelEntry::String(id) => {
check_model_available(
id,
available,
&format!("{}.fallback_models[{}]", path, i),
errors,
);
}
FallbackModelEntry::Detailed(spec) => {
check_model_available(
&spec.model,
available,
&format!("{}.fallback_models[{}]", path, i),
errors,
);
}
}
}
}
}
}
fn validate_agent_definition(
agent: &crate::omo_config::types::AgentDefinition,
path: &str,
errors: &mut Vec<String>,
available_models: Option<&HashSet<String>>,
) {
if let Some(temp) = agent.temperature {
if !(0.0..=2.0).contains(&temp) {
errors.push(format!(
"{}: temperature must be between 0 and 2 (got {})",
path, temp
));
}
}
if let Some(top_p) = agent.top_p {
if !(0.0..=1.0).contains(&top_p) {
errors.push(format!(
"{}: top_p must be between 0 and 1 (got {})",
path, top_p
));
}
}
if let Some(ref color) = agent.color {
if !is_valid_hex_color(color) {
errors.push(format!(
"{}: color must be a valid hex color like #RRGGBB (got {})",
path, color
));
}
}
if let Some(max) = agent.max_tokens {
if max == 0 {
errors.push(format!("{}: maxTokens must be greater than 0", path));
}
}
if let Some(ref thinking) = agent.thinking {
if let Some(budget) = thinking.budget_tokens {
if budget == 0 {
errors.push(format!(
"{}: thinking.budgetTokens must be greater than 0",
path
));
}
}
}
if let Some(available) = available_models {
if let Some(ref model_id) = agent.model {
check_model_available(model_id, available, path, errors);
}
if let Some(ref fallback) = agent.fallback_models {
validate_fallback_models(fallback, available, path, errors);
}
}
}
fn is_valid_hex_color(color: &str) -> bool {
if color.len() != 7 {
return false;
}
if !color.starts_with('#') {
return false;
}
color[1..].chars().all(|c| c.is_ascii_hexdigit())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::omo_config::types::{
AgentDefinition, AgentMode, AgentThinking, AgentsConfig, OhMyOpencodeConfig, ThinkingType,
};
#[test]
fn test_valid_config() {
let config = OhMyOpencodeConfig {
new_task_system_enabled: Some(true),
agents: Some(AgentsConfig {
build: Some(AgentDefinition {
model: Some("anthropic/claude-sonnet".to_string()),
temperature: Some(0.7),
mode: Some(AgentMode::Subagent),
color: Some("#FF5733".to_string()),
thinking: Some(AgentThinking {
r#type: ThinkingType::Enabled,
budget_tokens: Some(1024),
}),
..Default::default()
}),
..Default::default()
}),
..Default::default()
};
assert!(validate_agent_config(&config).is_ok());
}
#[test]
fn test_invalid_temperature() {
let config = OhMyOpencodeConfig {
agents: Some(AgentsConfig {
build: Some(AgentDefinition {
temperature: Some(3.0),
..Default::default()
}),
..Default::default()
}),
..Default::default()
};
let err = validate_agent_config(&config).unwrap_err();
assert!(err.to_string().contains("temperature"));
}
#[test]
fn test_invalid_color() {
let config = OhMyOpencodeConfig {
agents: Some(AgentsConfig {
build: Some(AgentDefinition {
color: Some("red".to_string()),
..Default::default()
}),
..Default::default()
}),
..Default::default()
};
let err = validate_agent_config(&config).unwrap_err();
assert!(err.to_string().contains("color"));
}
#[test]
fn test_agent_order_too_long() {
let config = OhMyOpencodeConfig {
agent_order: Some(vec!["agent".to_string(); 65]),
..Default::default()
};
let err = validate_agent_config(&config).unwrap_err();
assert!(err.to_string().contains("agent_order"));
}
}