use crate::Model;
use crate::roles::{ModelRole, RoleRegistry};
pub const DEFAULT_LONG_CONTEXT_THRESHOLD: usize = 60_000;
#[derive(Debug, Clone)]
pub struct RoleSignals<'a> {
pub explicit_override: Option<ModelRole>,
pub current_tool: Option<&'a str>,
pub thinking_enabled: bool,
pub estimated_tokens: usize,
pub long_context_threshold: usize,
pub is_trivial: bool,
}
impl Default for RoleSignals<'_> {
fn default() -> Self {
Self {
explicit_override: None,
current_tool: None,
thinking_enabled: false,
estimated_tokens: 0,
long_context_threshold: DEFAULT_LONG_CONTEXT_THRESHOLD,
is_trivial: false,
}
}
}
#[must_use]
pub fn decide_role(signals: &RoleSignals<'_>) -> ModelRole {
if let Some(role) = signals.explicit_override {
return role;
}
if let Some(tool) = signals.current_tool
&& let Some(role) = role_for_tool(tool)
{
return role;
}
if signals.estimated_tokens > signals.long_context_threshold {
return ModelRole::Slow;
}
if signals.thinking_enabled {
return ModelRole::Slow;
}
if signals.is_trivial {
return ModelRole::Smol;
}
ModelRole::Default
}
#[must_use]
pub fn role_for_tool(tool_name: &str) -> Option<ModelRole> {
match tool_name {
"commit" => Some(ModelRole::Commit),
_ => None,
}
}
#[must_use]
pub fn resolve_role_to_model(role: ModelRole, registry: &RoleRegistry) -> Option<Model> {
let pattern = registry.resolve(role.as_str()).into_iter().next()?;
let (provider, model_id) = pattern.split_once('/')?;
crate::lookup_model(provider, model_id)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn override_wins_over_everything() {
let s = RoleSignals {
explicit_override: Some(ModelRole::Advisor),
current_tool: Some("commit"),
thinking_enabled: true,
estimated_tokens: 100_000,
is_trivial: false,
..RoleSignals::default()
};
assert_eq!(decide_role(&s), ModelRole::Advisor);
}
#[test]
fn tool_signal_selects_commit_role() {
let s = RoleSignals {
current_tool: Some("commit"),
..RoleSignals::default()
};
assert_eq!(decide_role(&s), ModelRole::Commit);
}
#[test]
fn unknown_tool_falls_through() {
let s = RoleSignals {
current_tool: Some("read"),
..RoleSignals::default()
};
assert_eq!(decide_role(&s), ModelRole::Default);
}
#[test]
fn long_context_selects_slow() {
let s = RoleSignals {
estimated_tokens: 80_000,
long_context_threshold: 60_000,
..RoleSignals::default()
};
assert_eq!(decide_role(&s), ModelRole::Slow);
}
#[test]
fn long_context_respects_custom_threshold() {
let s = RoleSignals {
estimated_tokens: 5_000,
long_context_threshold: 4_000,
..RoleSignals::default()
};
assert_eq!(decide_role(&s), ModelRole::Slow);
}
#[test]
fn thinking_selects_slow_even_when_short() {
let s = RoleSignals {
thinking_enabled: true,
estimated_tokens: 100,
..RoleSignals::default()
};
assert_eq!(decide_role(&s), ModelRole::Slow);
}
#[test]
fn trivial_selects_smol() {
let s = RoleSignals {
is_trivial: true,
..RoleSignals::default()
};
assert_eq!(decide_role(&s), ModelRole::Smol);
}
#[test]
fn default_when_no_signal() {
assert_eq!(decide_role(&RoleSignals::default()), ModelRole::Default);
}
#[test]
fn long_context_beats_thinking_order_independence() {
let s = RoleSignals {
thinking_enabled: true,
estimated_tokens: 100_000,
..RoleSignals::default()
};
assert_eq!(decide_role(&s), ModelRole::Slow);
}
#[test]
fn role_for_tool_bindings() {
assert_eq!(role_for_tool("commit"), Some(ModelRole::Commit));
assert_eq!(role_for_tool("generate_image"), None);
assert_eq!(role_for_tool(""), None);
}
#[test]
fn resolve_unconfigured_role_is_none() {
let r = RoleRegistry::new();
assert!(resolve_role_to_model(ModelRole::Commit, &r).is_none());
}
#[test]
fn resolve_pattern_without_slash_is_none() {
let mut r = RoleRegistry::new();
r.set("commit", "just-a-bare-id");
assert!(resolve_role_to_model(ModelRole::Commit, &r).is_none());
}
#[test]
fn resolve_unknown_model_is_none() {
let mut r = RoleRegistry::new();
r.set("commit", "no-such-provider/does-not-exist-xyz");
assert!(resolve_role_to_model(ModelRole::Commit, &r).is_none());
}
#[test]
fn resolve_registered_model_is_some() {
let model = crate::Model::new(
"role-switcher-test-model",
"Role Switcher Test",
crate::Api::AnthropicMessages,
"role-switcher-test",
"",
);
crate::register_model(model);
let mut r = RoleRegistry::new();
r.set("commit", "role-switcher-test/role-switcher-test-model");
let resolved = resolve_role_to_model(ModelRole::Commit, &r);
assert!(
resolved.is_some(),
"registered model must resolve, got {resolved:?}"
);
}
}