use crate::error::{Error, Result};
use crate::features::{Features, TaskKind};
use serde::{Deserialize, Serialize};
use std::time::Duration;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Mode {
Enforce,
Observe,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum RoutingMode {
Observe,
Cost,
#[default]
Balanced,
Quality,
Latency,
Max,
}
impl RoutingMode {
#[must_use]
pub fn as_str(self) -> &'static str {
match self {
Self::Observe => "observe",
Self::Cost => "cost",
Self::Balanced => "balanced",
Self::Quality => "quality",
Self::Latency => "latency",
Self::Max => "max",
}
}
pub const ALL: &'static [Self] = &[
Self::Observe,
Self::Cost,
Self::Balanced,
Self::Quality,
Self::Latency,
Self::Max,
];
#[must_use]
pub fn preset(self) -> ModePreset {
match self {
Self::Observe => ModePreset {
speculation: None,
max_rungs_delta: None,
start_at_top: false,
description: "shadow mode: forward without gating; verdicts are async learning signals only",
tradeoff: "zero added latency, zero quality proof; use to observe before enforcing",
},
Self::Cost => ModePreset {
speculation: Some(0),
max_rungs_delta: None,
start_at_top: false,
description: "no speculative prefetch; bandit start on cheapest rung",
tradeoff: "lowest token spend; can serve lower quality on hard or heavy traffic",
},
Self::Balanced => ModePreset {
speculation: None,
max_rungs_delta: None,
start_at_top: false,
description: "bandit start, configured thresholds and speculation (default — no behavioral change)",
tradeoff: "today's tuned balance; add a mode only when you need a different point",
},
Self::Quality => ModePreset {
speculation: Some(0),
max_rungs_delta: Some(1),
start_at_top: false,
description: "one extra escalation rung; serial (no speculative waste)",
tradeoff: "higher quality ceiling at higher cost; still bounded by gate verification",
},
Self::Latency => ModePreset {
speculation: Some(1),
max_rungs_delta: None,
start_at_top: false,
description: "speculation=1: always prefetch one rung ahead to cut p95 latency",
tradeoff: "pays 1 wasted speculative call when cheap rung passes; speculation_band is overridden",
},
Self::Max => ModePreset {
speculation: Some(0),
max_rungs_delta: None,
start_at_top: true,
description: "start at the top (most expensive) ladder rung; verification as insurance",
tradeoff: "highest quality, highest cost per call; bandit bypassed; savings minimal",
},
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct ModePreset {
pub speculation: Option<u32>,
pub max_rungs_delta: Option<i32>,
pub start_at_top: bool,
pub description: &'static str,
pub tradeoff: &'static str,
}
#[derive(Debug, Clone, Default, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Config {
#[serde(rename = "route", default)]
pub routes: Vec<Route>,
#[serde(default)]
pub budget: Budget,
#[serde(default)]
pub escalation: Escalation,
#[serde(rename = "gate", default)]
pub gate_defs: Vec<GateDef>,
#[serde(rename = "price", default)]
pub price_defs: Vec<PriceDef>,
#[serde(rename = "provider", default)]
pub providers: Vec<ProviderDef>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Dialect {
Anthropic,
Openai,
Gemini,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum AuthScheme {
#[default]
ApiKey,
AwsSigv4,
GcpOauth,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ProviderDef {
pub id: String,
pub dialect: Dialect,
#[serde(default)]
pub base_url: String,
#[serde(default)]
pub api_key_env: Option<String>,
#[serde(default)]
pub auth: AuthScheme,
#[serde(default)]
pub region: Option<String>,
#[serde(default)]
pub project: Option<String>,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct PriceDef {
pub model: String,
pub input_per_mtok: f64,
pub output_per_mtok: f64,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct GateDef {
pub id: String,
#[serde(default)]
pub cmd: Vec<String>,
#[serde(default = "default_gate_timeout_ms")]
pub timeout_ms: u64,
#[serde(default)]
pub judge: Option<JudgeDef>,
#[serde(default)]
pub consistency: Option<ConsistencyDef>,
#[serde(default)]
pub schema: Option<serde_json::Value>,
#[serde(default)]
pub on_abstain: AbstainPolicy,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum AbstainPolicy {
#[default]
FailOpen,
FailClosed,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct JudgeDef {
pub model: String,
#[serde(default = "default_judge_threshold")]
pub threshold: f64,
#[serde(default)]
pub rubric: Option<String>,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ConsistencyDef {
pub model: String,
#[serde(default = "default_consistency_k")]
pub k: u32,
pub threshold: f64,
}
fn default_gate_timeout_ms() -> u64 {
30_000
}
fn default_judge_threshold() -> f64 {
0.7
}
fn default_consistency_k() -> u32 {
3
}
#[derive(Debug, Clone, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Route {
#[serde(rename = "match", default)]
pub match_: Match,
pub mode: Mode,
#[serde(default)]
pub ladder: Vec<String>,
#[serde(default)]
pub gates: Vec<String>,
#[serde(default)]
pub deferred_gates: Vec<String>,
#[serde(default)]
pub routing_mode: Option<RoutingMode>,
}
#[derive(Debug, Clone, Default, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Match {
#[serde(default)]
pub agent: Option<String>,
#[serde(default)]
pub subagent: Option<StringOrVec>,
#[serde(default)]
pub task_kind: Option<TaskKind>,
#[serde(default)]
pub language: Option<StringOrVec>,
}
impl Match {
#[must_use]
pub fn matches(&self, f: &Features) -> bool {
if let Some(agent) = &self.agent
&& f.agent.as_deref() != Some(agent.as_str())
{
return false;
}
if let Some(subs) = &self.subagent {
match &f.subagent {
Some(s) if subs.contains(s) => {}
_ => return false,
}
}
if let Some(tk) = self.task_kind
&& f.task_kind != tk
{
return false;
}
if let Some(langs) = &self.language {
match &f.language {
Some(l) if langs.contains(l) => {}
_ => return false,
}
}
true
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(untagged)]
pub enum StringOrVec {
One(String),
Many(Vec<String>),
}
impl StringOrVec {
#[must_use]
pub fn contains(&self, needle: &str) -> bool {
match self {
StringOrVec::One(s) => s == needle,
StringOrVec::Many(v) => v.iter().any(|s| s == needle),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum OnExhausted {
#[default]
ServeBestAttempt,
Error,
}
#[derive(Debug, Clone, Default, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Budget {
#[serde(default)]
pub per_request_usd: Option<f64>,
#[serde(default)]
pub per_session_usd: Option<f64>,
#[serde(default)]
pub per_day_usd: Option<f64>,
#[serde(default)]
pub on_exhausted: OnExhausted,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Escalation {
#[serde(default = "default_max_rungs")]
pub max_rungs_per_request: u32,
#[serde(default)]
pub session_promotion: Option<SessionPromotion>,
#[serde(default)]
pub speculation: u32,
#[serde(default)]
pub serve_threshold: Option<f64>,
#[serde(default)]
pub adaptive: Option<AdaptiveConfig>,
#[serde(default = "default_enforce_structured")]
pub enforce_structured: bool,
#[serde(default)]
pub bandit: Option<BanditConfig>,
#[serde(default)]
pub speculation_band: Option<[f64; 2]>,
#[serde(default)]
pub exploration: Option<ExplorationConfig>,
#[serde(default)]
pub probe: Option<ProbeConfig>,
#[serde(default)]
pub predictor: Option<PredictorConfig>,
#[serde(default)]
pub elastic: Option<ElasticConfig>,
}
#[derive(Debug, Clone, Copy, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct PredictorConfig {
#[serde(default = "default_predictor_lr")]
pub lr: f64,
#[serde(default = "default_predictor_l2")]
pub l2: f64,
}
fn default_predictor_lr() -> f64 {
0.05
}
fn default_predictor_l2() -> f64 {
1e-4
}
#[derive(Debug, Clone, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct AdaptiveConfig {
pub alpha: f64,
#[serde(default = "default_adaptive_gamma")]
pub gamma: f64,
}
fn default_adaptive_gamma() -> f64 {
0.02
}
#[derive(Debug, Clone, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ExplorationConfig {
pub epsilon: f64,
}
#[derive(Debug, Clone, Copy, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ProbeConfig {
pub k: u32,
pub sample_rate: f64,
}
#[derive(Debug, Clone, Deserialize, PartialEq)]
#[serde(deny_unknown_fields)]
pub struct ElasticConfig {
pub expensive_gates: Vec<String>,
pub lambda: f64,
#[serde(default)]
pub alpha: Option<f64>,
#[serde(default)]
pub delta: Option<f64>,
#[serde(default)]
pub calibration_id: Option<String>,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct BanditConfig {
#[serde(default = "default_bandit_min_observations")]
pub min_observations: usize,
#[serde(default = "default_bandit_exploration")]
pub exploration: f64,
#[serde(default)]
pub algorithm: BanditAlgorithm,
#[serde(default = "default_bandit_discount")]
pub discount: f64,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum BanditAlgorithm {
#[default]
Ucb1,
Thompson,
}
fn default_bandit_discount() -> f64 {
1.0
}
fn default_bandit_min_observations() -> usize {
50
}
fn default_bandit_exploration() -> f64 {
1.0
}
const fn default_max_rungs() -> u32 {
3
}
fn default_enforce_structured() -> bool {
true
}
impl Default for Escalation {
fn default() -> Self {
Self {
max_rungs_per_request: default_max_rungs(),
session_promotion: None,
speculation: 0,
serve_threshold: None,
adaptive: None,
enforce_structured: default_enforce_structured(),
speculation_band: None,
bandit: None,
exploration: None,
probe: None,
predictor: None,
elastic: None,
}
}
}
#[derive(Debug, Clone, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct SessionPromotion {
pub after_failures: u32,
pub window: String,
}
impl SessionPromotion {
pub fn window_duration(&self) -> Result<Duration> {
parse_window(&self.window)
}
}
fn parse_window(s: &str) -> Result<Duration> {
let s = s.trim();
let split = s
.find(|c: char| c.is_ascii_alphabetic())
.ok_or_else(|| Error::BadDuration(s.to_owned()))?;
let (num, unit) = s.split_at(split);
let n: u64 = num
.trim()
.parse()
.map_err(|_| Error::BadDuration(s.to_owned()))?;
let secs = match unit {
"s" => n,
"m" => n.saturating_mul(60),
"h" => n.saturating_mul(3600),
"d" => n.saturating_mul(86_400),
_ => return Err(Error::BadDuration(s.to_owned())),
};
Ok(Duration::from_secs(secs))
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ModelRef {
pub provider: String,
pub model: String,
}
impl ModelRef {
pub fn parse(s: &str) -> Result<Self> {
match s.split_once('/') {
Some((p, m)) if !p.is_empty() && !m.is_empty() && !m.contains('/') => Ok(Self {
provider: p.to_owned(),
model: m.to_owned(),
}),
_ => Err(Error::BadModelRef(s.to_owned())),
}
}
}
impl Config {
pub fn parse(toml_str: &str) -> Result<Self> {
let config: Self = toml::from_str(toml_str)?;
let mut seen = std::collections::HashSet::new();
for def in &config.gate_defs {
if def.id.trim().is_empty() {
return Err(Error::InvalidConfig("gate id must not be empty".to_owned()));
}
let kinds_set = [
!def.cmd.is_empty(),
def.judge.is_some(),
def.consistency.is_some(),
def.schema.is_some(),
]
.iter()
.filter(|&&b| b)
.count();
if kinds_set != 1 {
return Err(Error::InvalidConfig(format!(
"gate {:?} must set exactly one of `cmd`, `judge`, `consistency`, or `schema`",
def.id
)));
}
if let Some(judge) = &def.judge
&& !(0.0..=1.0).contains(&judge.threshold)
{
return Err(Error::InvalidConfig(format!(
"gate {:?} judge threshold {} is outside [0, 1]",
def.id, judge.threshold
)));
}
if let Some(c) = &def.consistency {
if !(2..=8).contains(&c.k) {
return Err(Error::InvalidConfig(format!(
"gate {:?} consistency k {} is outside [2, 8]",
def.id, c.k
)));
}
if !(0.0..=1.0).contains(&c.threshold) {
return Err(Error::InvalidConfig(format!(
"gate {:?} consistency threshold {} is outside [0, 1]",
def.id, c.threshold
)));
}
}
if !seen.insert(def.id.as_str()) {
return Err(Error::InvalidConfig(format!(
"duplicate gate id {:?}",
def.id
)));
}
}
for price in &config.price_defs {
if price.model.trim().is_empty() {
return Err(Error::InvalidConfig(
"price model must not be empty".to_owned(),
));
}
let ok = |v: f64| v.is_finite() && v >= 0.0;
if !ok(price.input_per_mtok) || !ok(price.output_per_mtok) {
return Err(Error::InvalidConfig(format!(
"price for {:?} must be finite and >= 0",
price.model
)));
}
}
if let Some(b) = &config.escalation.bandit {
if !b.exploration.is_finite() || b.exploration < 0.0 {
return Err(Error::InvalidConfig(format!(
"bandit.exploration must be finite and >= 0, got {}",
b.exploration
)));
}
if !b.discount.is_finite() || b.discount <= 0.0 || b.discount > 1.0 {
return Err(Error::InvalidConfig(format!(
"bandit.discount must be in (0, 1], got {}",
b.discount
)));
}
}
if let Some([lo, hi]) = config.escalation.speculation_band {
let ok = |v: f64| v.is_finite() && (0.0..=1.0).contains(&v);
if !ok(lo) || !ok(hi) || lo > hi {
return Err(Error::InvalidConfig(format!(
"escalation.speculation_band must satisfy 0 <= low <= high <= 1, got [{lo}, {hi}]"
)));
}
}
if let Some(exp) = &config.escalation.exploration
&& (!exp.epsilon.is_finite() || exp.epsilon <= 0.0 || exp.epsilon > 0.5)
{
return Err(Error::InvalidConfig(format!(
"escalation.exploration.epsilon must be finite and in (0, 0.5], got {}",
exp.epsilon
)));
}
if let Some(probe) = &config.escalation.probe {
if !(2..=8).contains(&probe.k) {
return Err(Error::InvalidConfig(format!(
"escalation.probe.k must be in [2, 8], got {}",
probe.k
)));
}
if !probe.sample_rate.is_finite() || !(0.0..=1.0).contains(&probe.sample_rate) {
return Err(Error::InvalidConfig(format!(
"escalation.probe.sample_rate must be finite and in [0, 1], got {}",
probe.sample_rate
)));
}
}
if let Some(pred) = &config.escalation.predictor {
if !pred.lr.is_finite() || !(0.0..=1.0).contains(&pred.lr) || pred.lr == 0.0 {
return Err(Error::InvalidConfig(format!(
"escalation.predictor.lr must be finite and in (0, 1], got {}",
pred.lr
)));
}
if !pred.l2.is_finite() || pred.l2 < 0.0 {
return Err(Error::InvalidConfig(format!(
"escalation.predictor.l2 must be finite and >= 0, got {}",
pred.l2
)));
}
}
if let Some(elastic) = &config.escalation.elastic {
if elastic.expensive_gates.is_empty() {
return Err(Error::InvalidConfig(
"escalation.elastic.expensive_gates must name at least one gate to skip".into(),
));
}
if !elastic.lambda.is_finite() || !(0.0..=1.0).contains(&elastic.lambda) {
return Err(Error::InvalidConfig(format!(
"escalation.elastic.lambda must be finite and in [0, 1], got {}",
elastic.lambda
)));
}
}
Ok(config)
}
#[must_use]
pub fn route_for(&self, f: &Features) -> Option<&Route> {
self.routes.iter().find(|r| r.match_.matches(f))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::features::Features;
const SPEC_CONFIG: &str = r#"
[[route]]
match = { agent = "claude-code", subagent = ["test-runner", "explore"] }
mode = "enforce"
ladder = ["anthropic/claude-haiku-4-5", "anthropic/claude-sonnet-5"]
gates = ["schema", "judge-diff"]
[[route]]
match = { task_kind = "code_edit" }
mode = "enforce"
ladder = ["anthropic/claude-sonnet-5", "anthropic/claude-opus-4-8"]
gates = ["patch-applies", "lint-diff", "judge-diff"]
deferred_gates = ["compiles", "tests"]
[[route]]
match = {}
mode = "observe"
ladder = ["anthropic/claude-opus-4-8"]
[budget]
per_request_usd = 0.50
per_session_usd = 10.00
per_day_usd = 250.00
on_exhausted = "serve_best_attempt"
[escalation]
max_rungs_per_request = 3
session_promotion = { after_failures = 3, window = "30m" }
"#;
#[test]
fn parses_the_spec_example() {
let c = Config::parse(SPEC_CONFIG).unwrap();
assert_eq!(c.routes.len(), 3);
assert_eq!(c.routes[0].mode, Mode::Enforce);
assert_eq!(
c.routes[0].ladder,
["anthropic/claude-haiku-4-5", "anthropic/claude-sonnet-5"]
);
assert_eq!(c.routes[1].deferred_gates, ["compiles", "tests"]);
assert_eq!(c.budget.per_request_usd, Some(0.50));
assert_eq!(c.budget.on_exhausted, OnExhausted::ServeBestAttempt);
assert_eq!(c.escalation.max_rungs_per_request, 3);
let sp = c.escalation.session_promotion.as_ref().unwrap();
assert_eq!(sp.after_failures, 3);
assert_eq!(sp.window_duration().unwrap(), Duration::from_secs(1800));
}
#[test]
fn first_matching_route_wins() {
let c = Config::parse(SPEC_CONFIG).unwrap();
let mut f = Features::new(TaskKind::Explore);
f.agent = Some("claude-code".into());
f.subagent = Some("test-runner".into());
let r = c.route_for(&f).unwrap();
assert_eq!(r.ladder[0], "anthropic/claude-haiku-4-5");
let f2 = Features::new(TaskKind::CodeEdit);
let r2 = c.route_for(&f2).unwrap();
assert_eq!(r2.gates, ["patch-applies", "lint-diff", "judge-diff"]);
let f3 = Features::new(TaskKind::Chat);
let r3 = c.route_for(&f3).unwrap();
assert_eq!(r3.mode, Mode::Observe);
}
#[test]
fn shipped_example_config_parses() {
let toml = include_str!("../../../firstpass.example.toml");
let c = Config::parse(toml).expect("firstpass.example.toml must parse");
assert_eq!(c.routes.len(), 3);
assert_eq!(c.routes[0].mode, Mode::Enforce);
}
#[test]
fn parses_gate_definitions() {
let toml = r#"
[[route]]
match = {}
mode = "enforce"
ladder = ["anthropic/claude-haiku-4-5"]
gates = ["my-tests"]
[[gate]]
id = "my-tests"
cmd = ["pytest", "-q"]
[[gate]]
id = "judge"
cmd = ["bash", "-c", "./judge.sh"]
timeout_ms = 60000
"#;
let c = Config::parse(toml).unwrap();
assert_eq!(c.gate_defs.len(), 2);
assert_eq!(c.gate_defs[0].id, "my-tests");
assert_eq!(c.gate_defs[0].cmd, ["pytest", "-q"]);
assert_eq!(c.gate_defs[0].timeout_ms, 30_000, "default timeout applies");
assert_eq!(c.gate_defs[1].timeout_ms, 60_000);
}
#[test]
fn rejects_invalid_gate_definitions() {
let empty_cmd = "[[gate]]\nid = \"g\"\ncmd = []\n";
assert!(matches!(
Config::parse(empty_cmd),
Err(Error::InvalidConfig(_))
));
let dup = "[[gate]]\nid = \"g\"\ncmd = [\"a\"]\n[[gate]]\nid = \"g\"\ncmd = [\"b\"]\n";
assert!(matches!(Config::parse(dup), Err(Error::InvalidConfig(_))));
}
#[test]
fn parses_consistency_gate_definition() {
let toml = r#"
[[gate]]
id = "uncertainty"
consistency = { model = "anthropic/claude-haiku-4-5", k = 5, threshold = 0.6 }
"#;
let c = Config::parse(toml).unwrap();
assert_eq!(c.gate_defs.len(), 1);
let cons = c.gate_defs[0].consistency.as_ref().unwrap();
assert_eq!(cons.model, "anthropic/claude-haiku-4-5");
assert_eq!(cons.k, 5);
assert!((cons.threshold - 0.6).abs() < 1e-9);
}
#[test]
fn consistency_k_defaults_to_3() {
let toml = "[[gate]]\nid = \"u\"\nconsistency = { model = \"anthropic/claude-haiku-4-5\", threshold = 0.7 }\n";
let c = Config::parse(toml).unwrap();
assert_eq!(c.gate_defs[0].consistency.as_ref().unwrap().k, 3);
}
#[test]
fn rejects_exactly_one_of_violations_for_consistency() {
let both = "[[gate]]\nid = \"g\"\ncmd = [\"x\"]\nconsistency = { model = \"a/b\", threshold = 0.5 }\n";
assert!(matches!(Config::parse(both), Err(Error::InvalidConfig(_))));
let bad_k =
"[[gate]]\nid = \"g\"\nconsistency = { model = \"a/b\", k = 1, threshold = 0.5 }\n";
assert!(matches!(Config::parse(bad_k), Err(Error::InvalidConfig(_))));
let bad_k2 =
"[[gate]]\nid = \"g\"\nconsistency = { model = \"a/b\", k = 9, threshold = 0.5 }\n";
assert!(matches!(
Config::parse(bad_k2),
Err(Error::InvalidConfig(_))
));
let bad_thresh =
"[[gate]]\nid = \"g\"\nconsistency = { model = \"a/b\", threshold = 1.1 }\n";
assert!(matches!(
Config::parse(bad_thresh),
Err(Error::InvalidConfig(_))
));
}
#[test]
fn parses_adaptive_conformal_config() {
let c = Config::parse("[escalation.adaptive]\nalpha = 0.1\n").unwrap();
let a = c
.escalation
.adaptive
.expect("[escalation.adaptive] should parse");
assert!((a.alpha - 0.1).abs() < 1e-9);
assert!((a.gamma - 0.02).abs() < 1e-9, "gamma defaults to 0.02");
assert!(Config::parse("").unwrap().escalation.adaptive.is_none());
}
#[test]
fn parses_provider_entries_and_ladders_can_reference_them() {
let c = Config::parse(
r#"
[[provider]]
id = "groq"
dialect = "openai"
base_url = "https://api.groq.com/openai"
api_key_env = "GROQ_API_KEY"
[[provider]]
id = "ollama"
dialect = "openai"
base_url = "http://localhost:11434"
[[route]]
match = {}
mode = "enforce"
ladder = ["groq/llama-3.3-70b-versatile", "anthropic/claude-sonnet-5"]
"#,
)
.unwrap();
assert_eq!(c.providers.len(), 2);
let groq = &c.providers[0];
assert_eq!(groq.id, "groq");
assert_eq!(groq.dialect, Dialect::Openai);
assert_eq!(groq.base_url, "https://api.groq.com/openai");
assert_eq!(groq.api_key_env.as_deref(), Some("GROQ_API_KEY"));
assert_eq!(c.providers[1].id, "ollama");
assert!(c.providers[1].api_key_env.is_none());
assert!(Config::parse("").unwrap().providers.is_empty());
}
#[test]
fn parses_aws_sigv4_provider_and_defaults_auth_to_api_key() {
let c = Config::parse(
r#"
[[provider]]
id = "bedrock"
dialect = "anthropic"
auth = "aws_sigv4"
region = "us-east-1"
"#,
)
.unwrap();
let bedrock = &c.providers[0];
assert_eq!(bedrock.auth, AuthScheme::AwsSigv4);
assert_eq!(bedrock.region.as_deref(), Some("us-east-1"));
assert!(bedrock.project.is_none());
let c2 = Config::parse(
r#"
[[provider]]
id = "groq"
dialect = "openai"
base_url = "https://api.groq.com/openai"
"#,
)
.unwrap();
assert_eq!(c2.providers[0].auth, AuthScheme::ApiKey);
}
#[test]
fn all_documented_provider_shapes_parse() {
let c = Config::parse(
r#"
[[provider]]
id = "groq"
dialect = "openai"
base_url = "https://api.groq.com/openai"
api_key_env = "GROQ_API_KEY"
[[provider]]
id = "ollama"
dialect = "openai"
base_url = "http://localhost:11434"
[[provider]]
id = "gemini"
dialect = "gemini"
base_url = "https://generativelanguage.googleapis.com"
api_key_env = "GEMINI_API_KEY"
[[provider]]
id = "bedrock"
dialect = "anthropic"
auth = "aws_sigv4"
region = "us-east-1"
[[provider]]
id = "vertex"
dialect = "anthropic"
auth = "gcp_oauth"
region = "us-east5"
project = "my-gcp-project"
"#,
)
.expect("every documented provider shape must parse");
assert_eq!(c.providers.len(), 5);
let gemini = c.providers.iter().find(|p| p.id == "gemini").unwrap();
assert_eq!(gemini.dialect, Dialect::Gemini);
assert_eq!(gemini.auth, AuthScheme::ApiKey);
let vertex = c.providers.iter().find(|p| p.id == "vertex").unwrap();
assert_eq!(vertex.auth, AuthScheme::GcpOauth);
assert_eq!(vertex.region.as_deref(), Some("us-east5"));
assert_eq!(vertex.project.as_deref(), Some("my-gcp-project"));
}
#[test]
fn empty_match_is_wildcard() {
let m = Match::default();
assert!(m.matches(&Features::new(TaskKind::Other)));
}
#[test]
fn subagent_list_membership() {
let c = Config::parse(SPEC_CONFIG).unwrap();
let route0 = &c.routes[0];
let mut f = Features::new(TaskKind::Other);
f.agent = Some("claude-code".into());
f.subagent = Some("docs-writer".into()); assert!(!route0.match_.matches(&f));
f.subagent = Some("explore".into());
assert!(route0.match_.matches(&f));
}
#[test]
fn model_ref_parsing() {
let m = ModelRef::parse("anthropic/claude-haiku-4-5").unwrap();
assert_eq!(m.provider, "anthropic");
assert_eq!(m.model, "claude-haiku-4-5");
assert!(ModelRef::parse("no-slash").is_err());
assert!(ModelRef::parse("/model").is_err());
assert!(ModelRef::parse("a/b/c").is_err());
}
#[test]
fn window_parsing_units_and_errors() {
assert_eq!(parse_window("90s").unwrap(), Duration::from_secs(90));
assert_eq!(parse_window("30m").unwrap(), Duration::from_secs(1800));
assert_eq!(parse_window("2h").unwrap(), Duration::from_secs(7200));
assert_eq!(parse_window("1d").unwrap(), Duration::from_secs(86_400));
assert!(parse_window("30x").is_err());
assert!(parse_window("abc").is_err());
}
#[test]
fn empty_config_defaults() {
let c = Config::parse("").unwrap();
assert!(c.routes.is_empty());
assert_eq!(c.escalation.max_rungs_per_request, 3);
assert_eq!(c.budget.on_exhausted, OnExhausted::ServeBestAttempt);
}
#[test]
fn parses_exploration_config() {
let c = Config::parse("[escalation.exploration]\nepsilon = 0.1\n").unwrap();
let exp = c
.escalation
.exploration
.expect("[escalation.exploration] should parse");
assert!((exp.epsilon - 0.1).abs() < 1e-12);
assert!(Config::parse("").unwrap().escalation.exploration.is_none());
}
#[test]
fn exploration_epsilon_boundary_valid() {
let c = Config::parse("[escalation.exploration]\nepsilon = 0.5\n").unwrap();
assert!((c.escalation.exploration.unwrap().epsilon - 0.5).abs() < 1e-12);
}
#[test]
fn exploration_epsilon_above_half_rejected() {
let bad = "[escalation.exploration]\nepsilon = 0.51\n";
assert!(
matches!(Config::parse(bad), Err(Error::InvalidConfig(_))),
"epsilon > 0.5 must be rejected"
);
}
#[test]
fn exploration_epsilon_zero_rejected() {
let bad = "[escalation.exploration]\nepsilon = 0.0\n";
assert!(
matches!(Config::parse(bad), Err(Error::InvalidConfig(_))),
"epsilon = 0 must be rejected (must be strictly positive)"
);
}
#[test]
fn exploration_epsilon_negative_rejected() {
let bad = "[escalation.exploration]\nepsilon = -0.1\n";
assert!(
matches!(Config::parse(bad), Err(Error::InvalidConfig(_))),
"epsilon < 0 must be rejected"
);
}
#[test]
fn gate_def_schema_and_on_abstain_parse() {
let toml = r#"
[[route]]
match = {}
mode = "enforce"
ladder = ["anthropic/claude-haiku-4-5"]
gates = ["extract-shape"]
[[gate]]
id = "extract-shape"
schema = { type = "object", required = ["name"] }
on_abstain = "fail_closed"
"#;
let config = Config::parse(toml).expect("schema gate def must parse");
let def = &config.gate_defs[0];
assert_eq!(def.id, "extract-shape");
let schema = def.schema.as_ref().expect("schema captured");
assert_eq!(schema["type"], "object");
assert_eq!(def.on_abstain, AbstainPolicy::FailClosed);
}
#[test]
fn gate_def_on_abstain_defaults_fail_open() {
let toml = r#"
[[route]]
match = {}
mode = "enforce"
ladder = ["anthropic/claude-haiku-4-5"]
[[gate]]
id = "tests"
cmd = ["true"]
"#;
let config = Config::parse(toml).expect("parse");
assert_eq!(config.gate_defs[0].on_abstain, AbstainPolicy::FailOpen);
}
#[test]
fn gate_def_rejects_schema_plus_cmd() {
let toml = r#"
[[route]]
match = {}
mode = "enforce"
ladder = ["anthropic/claude-haiku-4-5"]
[[gate]]
id = "both"
cmd = ["true"]
schema = { type = "object" }
"#;
let err = Config::parse(toml).unwrap_err();
assert!(
err.to_string().contains("exactly one"),
"two kinds must be rejected: {err}"
);
}
#[test]
fn price_overrides_parse_and_validate() {
let toml = r#"
[[route]]
match = {}
mode = "observe"
ladder = ["anthropic/claude-haiku-4-5"]
[[price]]
model = "anthropic/claude-haiku-4-5"
input_per_mtok = 0.8
output_per_mtok = 4.0
"#;
let config = Config::parse(toml).expect("price override must parse");
assert_eq!(config.price_defs[0].model, "anthropic/claude-haiku-4-5");
assert!((config.price_defs[0].input_per_mtok - 0.8).abs() < 1e-12);
let bad = toml.replace("input_per_mtok = 0.8", "input_per_mtok = -1.0");
assert!(Config::parse(&bad).is_err(), "negative price rejected");
}
#[test]
fn balanced_preset_is_strict_noop() {
let p = RoutingMode::Balanced.preset();
assert!(
p.speculation.is_none(),
"Balanced must not override speculation"
);
assert!(
p.max_rungs_delta.is_none(),
"Balanced must not override max_rungs"
);
assert!(!p.start_at_top, "Balanced must not set start_at_top");
}
#[test]
fn cost_preset_disables_speculation() {
let p = RoutingMode::Cost.preset();
assert_eq!(p.speculation, Some(0));
assert!(p.max_rungs_delta.is_none());
assert!(!p.start_at_top);
}
#[test]
fn quality_preset_bumps_max_rungs_and_disables_speculation() {
let p = RoutingMode::Quality.preset();
assert_eq!(p.max_rungs_delta, Some(1));
assert_eq!(p.speculation, Some(0));
assert!(!p.start_at_top);
}
#[test]
fn latency_preset_enables_speculation() {
let p = RoutingMode::Latency.preset();
assert_eq!(p.speculation, Some(1));
assert!(p.max_rungs_delta.is_none());
assert!(!p.start_at_top);
}
#[test]
fn max_preset_sets_start_at_top_and_disables_speculation() {
let p = RoutingMode::Max.preset();
assert!(p.start_at_top);
assert_eq!(p.speculation, Some(0));
assert!(p.max_rungs_delta.is_none());
}
#[test]
fn routing_mode_as_str_roundtrips() {
for mode in RoutingMode::ALL {
let s = mode.as_str();
let back: RoutingMode = serde_json::from_str(&format!("\"{s}\"")).unwrap();
assert_eq!(*mode, back, "as_str/serde roundtrip for {s}");
}
}
#[test]
fn routing_mode_defaults_to_balanced() {
assert_eq!(RoutingMode::default(), RoutingMode::Balanced);
}
#[test]
fn route_routing_mode_parses_and_defaults_to_none() {
let no_mode = Config::parse(
"[[route]]\nmatch = {}\nmode = \"enforce\"\nladder = [\"anthropic/claude-haiku-4-5\"]\n",
)
.unwrap();
assert_eq!(no_mode.routes[0].routing_mode, None);
let with_mode = Config::parse(
"[[route]]\nmatch = {}\nmode = \"enforce\"\nladder = [\"anthropic/claude-haiku-4-5\"]\nrouting_mode = \"cost\"\n",
)
.unwrap();
assert_eq!(with_mode.routes[0].routing_mode, Some(RoutingMode::Cost));
}
#[test]
fn all_routing_modes_have_non_empty_description_and_tradeoff() {
for mode in RoutingMode::ALL {
let p = mode.preset();
assert!(
!p.description.is_empty(),
"mode {} has empty description",
mode.as_str()
);
assert!(
!p.tradeoff.is_empty(),
"mode {} has empty tradeoff",
mode.as_str()
);
}
}
#[test]
fn bandit_thompson_and_band_parse_and_validate() {
let toml = r#"
[[route]]
match = {}
mode = "enforce"
ladder = ["anthropic/claude-haiku-4-5"]
[escalation]
speculation = 1
speculation_band = [0.3, 0.7]
[escalation.bandit]
algorithm = "thompson"
discount = 0.98
"#;
let config = Config::parse(toml).expect("thompson + band must parse");
let b = config.escalation.bandit.as_ref().unwrap();
assert_eq!(b.algorithm, BanditAlgorithm::Thompson);
assert!((b.discount - 0.98).abs() < 1e-12);
assert_eq!(config.escalation.speculation_band, Some([0.3, 0.7]));
let plain = Config::parse(
"[[route]]\nmatch = {}\nmode = \"enforce\"\nladder = [\"anthropic/claude-haiku-4-5\"]\n[escalation.bandit]\n",
)
.unwrap();
let b = plain.escalation.bandit.as_ref().unwrap();
assert_eq!(b.algorithm, BanditAlgorithm::Ucb1);
assert!((b.discount - 1.0).abs() < 1e-12);
assert!(Config::parse(&toml.replace("discount = 0.98", "discount = 0.0")).is_err());
assert!(
Config::parse(&toml.replace(
"speculation_band = [0.3, 0.7]",
"speculation_band = [0.9, 0.2]"
))
.is_err()
);
}
#[test]
fn probe_absent_defaults_to_none() {
let c = Config::parse("").unwrap();
assert!(c.escalation.probe.is_none());
}
#[test]
fn parses_valid_probe_config() {
let c = Config::parse("[escalation.probe]\nk = 5\nsample_rate = 0.1\n").unwrap();
let p = c.escalation.probe.expect("[escalation.probe] should parse");
assert_eq!(p.k, 5);
assert!((p.sample_rate - 0.1).abs() < 1e-12);
}
#[test]
fn probe_sample_rate_boundaries_accepted() {
let c0 = Config::parse("[escalation.probe]\nk = 2\nsample_rate = 0.0\n").unwrap();
assert!((c0.escalation.probe.unwrap().sample_rate - 0.0).abs() < 1e-12);
let c1 = Config::parse("[escalation.probe]\nk = 8\nsample_rate = 1.0\n").unwrap();
assert!((c1.escalation.probe.unwrap().sample_rate - 1.0).abs() < 1e-12);
}
#[test]
fn probe_rejects_k_below_2() {
let bad = "[escalation.probe]\nk = 1\nsample_rate = 0.5\n";
assert!(
matches!(Config::parse(bad), Err(Error::InvalidConfig(_))),
"k=1 must be rejected"
);
}
#[test]
fn probe_rejects_k_above_8() {
let bad = "[escalation.probe]\nk = 9\nsample_rate = 0.5\n";
assert!(
matches!(Config::parse(bad), Err(Error::InvalidConfig(_))),
"k=9 must be rejected"
);
}
#[test]
fn probe_rejects_sample_rate_above_1() {
let bad = "[escalation.probe]\nk = 5\nsample_rate = 1.5\n";
assert!(
matches!(Config::parse(bad), Err(Error::InvalidConfig(_))),
"sample_rate=1.5 must be rejected"
);
}
#[test]
fn probe_rejects_negative_sample_rate() {
let bad = "[escalation.probe]\nk = 5\nsample_rate = -0.1\n";
assert!(
matches!(Config::parse(bad), Err(Error::InvalidConfig(_))),
"negative sample_rate must be rejected"
);
}
#[test]
fn predictor_config_parses_and_validates() {
let base = "[[route]]\nmatch = {}\nmode = \"enforce\"\nladder = [\"anthropic/claude-haiku-4-5\"]\n[escalation.predictor]\nlr = 0.05\nl2 = 0.001\n";
let cfg = Config::parse(base).expect("valid predictor must parse");
let pred = cfg.escalation.predictor.unwrap();
assert!((pred.lr - 0.05).abs() < 1e-12 && (pred.l2 - 0.001).abs() < 1e-12);
let d = Config::parse("[[route]]\nmatch = {}\nmode = \"enforce\"\nladder = [\"anthropic/claude-haiku-4-5\"]\n[escalation.predictor]\n").unwrap();
assert!(d.escalation.predictor.is_some());
assert!(Config::parse(&base.replace("lr = 0.05", "lr = 0.0")).is_err());
assert!(Config::parse(&base.replace("lr = 0.05", "lr = 1.5")).is_err());
assert!(Config::parse(&base.replace("l2 = 0.001", "l2 = -1.0")).is_err());
}
}