use serde::Serialize;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Default)]
#[serde(rename_all = "lowercase")]
pub enum AgentModel {
Opus,
#[default]
Sonnet,
Haiku,
}
impl AgentModel {
pub fn all() -> &'static [AgentModel] {
&[AgentModel::Opus, AgentModel::Sonnet, AgentModel::Haiku]
}
pub fn id(self) -> &'static str {
match self {
AgentModel::Opus => "opus",
AgentModel::Sonnet => "sonnet",
AgentModel::Haiku => "haiku",
}
}
pub fn label(self) -> &'static str {
match self {
AgentModel::Opus => "Opus 4.8",
AgentModel::Sonnet => "Sonnet 4.6",
AgentModel::Haiku => "Haiku 4.5",
}
}
pub fn parse(s: &str) -> Option<AgentModel> {
match s.trim().to_ascii_lowercase().as_str() {
"opus" => Some(AgentModel::Opus),
"sonnet" => Some(AgentModel::Sonnet),
"haiku" => Some(AgentModel::Haiku),
_ => None,
}
}
pub fn next(self) -> AgentModel {
match self {
AgentModel::Opus => AgentModel::Sonnet,
AgentModel::Sonnet => AgentModel::Haiku,
AgentModel::Haiku => AgentModel::Opus,
}
}
pub fn prev(self) -> AgentModel {
match self {
AgentModel::Opus => AgentModel::Haiku,
AgentModel::Sonnet => AgentModel::Opus,
AgentModel::Haiku => AgentModel::Sonnet,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Default)]
#[serde(rename_all = "lowercase")]
pub enum Effort {
Low,
#[default]
Medium,
High,
}
impl Effort {
pub fn all() -> &'static [Effort] {
&[Effort::Low, Effort::Medium, Effort::High]
}
pub fn id(self) -> &'static str {
match self {
Effort::Low => "low",
Effort::Medium => "medium",
Effort::High => "high",
}
}
pub fn label(self) -> &'static str {
self.id()
}
pub fn parse(s: &str) -> Option<Effort> {
match s.trim().to_ascii_lowercase().as_str() {
"low" => Some(Effort::Low),
"medium" | "med" => Some(Effort::Medium),
"high" => Some(Effort::High),
_ => None,
}
}
pub fn next(self) -> Effort {
match self {
Effort::Low => Effort::Medium,
Effort::Medium => Effort::High,
Effort::High => Effort::Low,
}
}
pub fn prev(self) -> Effort {
match self {
Effort::Low => Effort::High,
Effort::Medium => Effort::Low,
Effort::High => Effort::Medium,
}
}
pub fn directive(self) -> Option<&'static str> {
match self {
Effort::Low => Some("Work quickly and keep your reasoning brief."),
Effort::Medium => None,
Effort::High => Some("Think carefully and review the diff thoroughly before writing."),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct AgentOptions {
pub model: AgentModel,
pub effort: Effort,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn model_parse_roundtrips_and_rejects_unknown() {
for &m in AgentModel::all() {
assert_eq!(AgentModel::parse(m.id()), Some(m));
}
assert_eq!(AgentModel::parse("OPUS"), Some(AgentModel::Opus));
assert_eq!(AgentModel::parse(" sonnet "), Some(AgentModel::Sonnet));
assert_eq!(AgentModel::parse("gpt"), None);
}
#[test]
fn model_cycle_visits_every_variant() {
let mut seen = vec![AgentModel::Opus];
let mut cur = AgentModel::Opus;
for _ in 0..AgentModel::all().len() - 1 {
cur = cur.next();
seen.push(cur);
}
assert_eq!(cur.next(), AgentModel::Opus); assert_eq!(seen.len(), AgentModel::all().len());
}
#[test]
fn model_serializes_lowercase() {
assert_eq!(
serde_json::to_string(&AgentModel::Sonnet).unwrap(),
"\"sonnet\""
);
}
#[test]
fn effort_parse_accepts_aliases() {
assert_eq!(Effort::parse("low"), Some(Effort::Low));
assert_eq!(Effort::parse("MED"), Some(Effort::Medium));
assert_eq!(Effort::parse("medium"), Some(Effort::Medium));
assert_eq!(Effort::parse("High"), Some(Effort::High));
assert_eq!(Effort::parse("max"), None);
}
#[test]
fn effort_directive_only_for_non_baseline() {
assert!(Effort::Low.directive().is_some());
assert!(Effort::Medium.directive().is_none());
assert!(Effort::High.directive().is_some());
}
#[test]
fn effort_cycle_wraps() {
assert_eq!(Effort::Low.next(), Effort::Medium);
assert_eq!(Effort::Medium.next(), Effort::High);
assert_eq!(Effort::High.next(), Effort::Low);
}
#[test]
fn prev_is_the_inverse_of_next() {
for &m in AgentModel::all() {
assert_eq!(m.next().prev(), m);
assert_eq!(m.prev().next(), m);
}
for &e in Effort::all() {
assert_eq!(e.next().prev(), e);
assert_eq!(e.prev().next(), e);
}
}
#[test]
fn defaults_are_sonnet_and_medium() {
let opts = AgentOptions::default();
assert_eq!(opts.model, AgentModel::Sonnet);
assert_eq!(opts.effort, Effort::Medium);
}
}