use regex::Regex;
use crate::router::{
error::{Error, Result},
strategies::RoutingStrategy,
types::{ModelInfo, RoutingDecision},
};
#[derive(Debug, Clone)]
pub struct RegexRule {
pub pattern: Regex,
pub model: String,
pub provider: String,
pub reasoning: Option<String>,
}
impl RegexRule {
pub fn new(
pattern: &str,
model: impl Into<String>,
provider: impl Into<String>,
) -> std::result::Result<Self, Error> {
let compiled = Regex::new(pattern)
.map_err(|e| Error::InvalidPattern(format!("{}: {}", pattern, e)))?;
Ok(Self {
pattern: compiled,
model: model.into(),
provider: provider.into(),
reasoning: None,
})
}
pub fn with_reasoning(mut self, r: impl Into<String>) -> Self {
self.reasoning = Some(r.into());
self
}
}
pub struct RegexStrategy {
rules: Vec<RegexRule>,
default_model: String,
default_provider: String,
}
impl RegexStrategy {
pub fn new(
rules: Vec<RegexRule>,
default_model: impl Into<String>,
default_provider: impl Into<String>,
) -> Self {
Self {
rules,
default_model: default_model.into(),
default_provider: default_provider.into(),
}
}
}
impl RoutingStrategy for RegexStrategy {
fn name(&self) -> &'static str {
"regex"
}
fn route(
&self,
content: &str,
_embedding: Option<&[f32]>,
_models: &[ModelInfo],
) -> Result<RoutingDecision> {
if let Some(rule) = self.rules.iter().find(|r| r.pattern.is_match(content)) {
return Ok(RoutingDecision::new(&rule.model, &rule.provider)
.with_reasoning(
rule.reasoning.clone().unwrap_or_else(|| "Matched regex rule".to_string()),
));
}
Ok(RoutingDecision::new(&self.default_model, &self.default_provider)
.with_reasoning("Default rule (no regex matched)"))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::router::types::ModelInfo;
fn models() -> Vec<ModelInfo> {
vec![
ModelInfo::new("gpt-4o", "openai"),
ModelInfo::new("gpt-4o-mini", "openai"),
]
}
fn strategy() -> RegexStrategy {
RegexStrategy::new(
vec![
RegexRule::new(r"(?i)code|function|algorithm", "gpt-4o", "openai").unwrap(),
RegexRule::new(r"(?i)summarize|tldr", "gpt-4o-mini", "openai").unwrap(),
],
"gpt-4o-mini",
"openai",
)
}
#[test]
fn matches_first_rule() {
let d = strategy().route("Write a sorting algorithm", None, &models()).unwrap();
assert_eq!(d.model, "gpt-4o");
}
#[test]
fn matches_second_rule() {
let d = strategy().route("Please summarize this article", None, &models()).unwrap();
assert_eq!(d.model, "gpt-4o-mini");
}
#[test]
fn falls_back_to_default() {
let d = strategy().route("What is the weather?", None, &models()).unwrap();
assert_eq!(d.model, "gpt-4o-mini");
assert!(d.reasoning.as_deref().unwrap().contains("Default"));
}
#[test]
fn case_insensitive_match() {
let d = strategy().route("Write a CODE review", None, &models()).unwrap();
assert_eq!(d.model, "gpt-4o");
}
#[test]
fn first_matching_rule_wins() {
let d = strategy().route("summarize this code algorithm", None, &models()).unwrap();
assert_eq!(d.model, "gpt-4o"); }
#[test]
fn invalid_pattern_returns_error() {
let result = RegexRule::new("[invalid(", "m", "p");
assert!(result.is_err());
}
#[test]
fn reasoning_carried_through() {
let rule = RegexRule::new(r"test", "m", "p")
.unwrap()
.with_reasoning("Custom reason");
let strat = RegexStrategy::new(vec![rule], "m", "p");
let d = strat.route("this is a test", None, &[ModelInfo::new("m", "p")]).unwrap();
assert_eq!(d.reasoning.as_deref(), Some("Custom reason"));
}
#[test]
fn name_is_regex() {
assert_eq!(strategy().name(), "regex");
}
}