use anyhow::{Result, anyhow};
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
use std::str::FromStr;
pub mod context_transcript_io;
pub mod context_usage;
const CLAUDE_CODE_OPUS_ALIAS: &str = "opus";
#[derive(
Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize,
)]
#[serde(rename_all = "lowercase")]
pub enum Tier {
#[default]
Auto,
Low,
Med,
High,
}
impl std::fmt::Display for Tier {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Auto => write!(f, "auto"),
Self::Low => write!(f, "low"),
Self::Med => write!(f, "med"),
Self::High => write!(f, "high"),
}
}
}
impl FromStr for Tier {
type Err = anyhow::Error;
fn from_str(s: &str) -> Result<Self> {
match s.trim().to_ascii_lowercase().as_str() {
"auto" => Ok(Self::Auto),
"low" => Ok(Self::Low),
"med" | "medium" => Ok(Self::Med),
"high" => Ok(Self::High),
other => Err(anyhow!(
"invalid tier `{}`: expected one of auto|low|med|high",
other
)),
}
}
}
#[derive(Debug, Default, Clone, Serialize, Deserialize)]
pub struct TierMap {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub low: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub med: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub high: Option<String>,
}
impl TierMap {
pub fn get(&self, tier: Tier) -> Option<&str> {
match tier {
Tier::Auto => None,
Tier::Low => self.low.as_deref(),
Tier::Med => self.med.as_deref(),
Tier::High => self.high.as_deref(),
}
}
pub fn tier_of(&self, model_name: &str) -> Option<Tier> {
if self.low.as_deref() == Some(model_name) {
Some(Tier::Low)
} else if self.med.as_deref() == Some(model_name) {
Some(Tier::Med)
} else if self.high.as_deref() == Some(model_name) {
Some(Tier::High)
} else {
None
}
}
}
#[derive(Debug, Default, Clone, Serialize, Deserialize)]
pub struct ModelConfig {
#[serde(default = "default_auto")]
pub auto: bool,
#[serde(default)]
pub tiers: BTreeMap<String, TierMap>,
}
fn default_auto() -> bool {
true
}
fn builtin_claude_code() -> TierMap {
TierMap {
low: Some("haiku".to_string()),
med: Some("sonnet".to_string()),
high: Some(CLAUDE_CODE_OPUS_ALIAS.to_string()),
}
}
fn builtin_codex() -> TierMap {
TierMap {
low: Some("gpt-4o-mini".to_string()),
med: Some("gpt-4o".to_string()),
high: Some("o3".to_string()),
}
}
fn builtin_default() -> TierMap {
TierMap {
low: Some("haiku".to_string()),
med: Some("sonnet".to_string()),
high: Some("opus".to_string()),
}
}
fn builtin_for(harness: &str) -> TierMap {
match harness {
"claude-code" => builtin_claude_code(),
"codex" => builtin_codex(),
_ => builtin_default(),
}
}
pub fn detect_harness() -> String {
if ["CLAUDE_CODE_SESSION", "CLAUDE_CODE", "CLAUDECODE"]
.iter()
.any(|key| std::env::var_os(key).is_some())
{
"claude-code".to_string()
} else if ["CODEX_SESSION", "CODEX_THREAD_ID", "CODEX_CLI", "CODEX"]
.iter()
.any(|key| std::env::var_os(key).is_some())
{
"codex".to_string()
} else if ["OPENCODE_CLIENT", "OPENCODE"]
.iter()
.any(|key| std::env::var_os(key).is_some())
{
"opencode".to_string()
} else {
"default".to_string()
}
}
pub fn harness_key_for_agent_name(agent_name: &str) -> String {
let normalized = agent_name
.trim()
.to_ascii_lowercase()
.replace(['_', ' '], "-");
match normalized.as_str() {
"claude" | "claude-code" | "claudecode" | "claude-code-cli" => "claude-code".to_string(),
"codex" | "codex-cli" | "openai-codex" => "codex".to_string(),
"opencode" | "open-code" | "opencode-ai" => "opencode".to_string(),
"" => "default".to_string(),
other => other.to_string(),
}
}
pub fn canonical_harness_name(value: &str) -> Option<String> {
let normalized = value.trim().to_ascii_lowercase().replace(['_', ' '], "-");
match normalized.as_str() {
"" | "default" | "generic" => None,
"claude" | "claude-code" | "claudecode" | "claude-code-cli" => {
Some("claude-code".to_string())
}
"codex" | "codex-cli" | "openai-codex" => Some("codex".to_string()),
"opencode" | "open-code" | "opencode-ai" => Some("opencode".to_string()),
other => Some(other.to_string()),
}
}
pub const HARNESS_MISMATCH_WARNING_CODE: &str = "harness_mismatch";
pub const CODEX_NETWORK_ACCESS_NON_CODEX_HARNESS_WARNING_CODE: &str =
"codex_network_access_non_codex_harness";
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct HarnessMismatchWarning {
pub code: &'static str,
pub message: String,
pub document_agent: String,
pub active_harness: String,
}
pub fn harness_mismatch_warning(
document_agent: Option<&str>,
active_harness: &str,
) -> Option<HarnessMismatchWarning> {
let declared_raw = document_agent?.trim();
if declared_raw.is_empty() {
return None;
}
let declared = canonical_harness_name(declared_raw)?;
let active = canonical_harness_name(active_harness)?;
if declared == active {
return None;
}
Some(HarnessMismatchWarning {
code: HARNESS_MISMATCH_WARNING_CODE,
message: format!(
"Document declares agent: {} but active harness is {}; responses will use the active harness attribution and closeout path.",
declared_raw, active
),
document_agent: declared_raw.to_string(),
active_harness: active,
})
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CodexNetworkAccessNonCodexHarnessWarning {
pub code: &'static str,
pub message: String,
pub document_agent: Option<String>,
pub active_harness: String,
}
pub fn codex_network_access_non_codex_harness_warning(
file_display: &str,
document_agent: Option<&str>,
active_harness: &str,
codex_network_access_configured: bool,
) -> Option<CodexNetworkAccessNonCodexHarnessWarning> {
if !codex_network_access_configured
|| canonical_harness_name(active_harness).as_deref() == Some("codex")
{
return None;
}
Some(CodexNetworkAccessNonCodexHarnessWarning {
code: CODEX_NETWORK_ACCESS_NON_CODEX_HARNESS_WARNING_CODE,
message: format!(
"{file_display}: `codex_network_access` is Codex-specific and has no effect when the active harness is {active_harness}. \
Either remove it from the document frontmatter or switch the agent to codex."
),
document_agent: document_agent.map(str::to_string),
active_harness: active_harness.to_string(),
})
}
pub fn resolve_tier_to_model(
tier: Tier,
harness: &str,
model_config: &ModelConfig,
) -> Option<String> {
if matches!(tier, Tier::Auto) {
return None;
}
if let Some(map) = model_config.tiers.get(harness)
&& let Some(name) = map.get(tier)
{
return Some(name.to_string());
}
builtin_for(harness).get(tier).map(|s| s.to_string())
}
fn is_claude_code_opus(model_name: &str) -> bool {
let m = model_name.trim();
m == CLAUDE_CODE_OPUS_ALIAS || m.starts_with("claude-opus") || m.starts_with("opus-")
}
pub fn is_bare_model_name(model_name: &str) -> bool {
let trimmed = model_name.trim();
!trimmed.contains('/') || trimmed.starts_with('/')
}
pub fn canonical_model_name(
model_name: &str,
harness: &str,
_model_config: &ModelConfig,
) -> String {
if harness == "opencode" && is_bare_model_name(model_name) {
eprintln!(
"[model_tier] WARNING: opencode model name {:?} lacks a provider prefix \
(expected \"provider/model\", e.g. \"zai-coding-plan/glm-5.1\"). \
Dispatch may fail or use a wrong provider.",
model_name.trim()
);
}
model_name.to_string()
}
pub fn short_model_name(model_id: &str) -> &str {
if let Some(suffix) = model_id.strip_prefix("claude-") {
return suffix;
}
model_id
}
pub fn resolve_agent_model(
frontmatter_model: Option<&str>,
harness: &str,
model_config: &ModelConfig,
) -> Option<String> {
let model = frontmatter_model?;
let canonical = canonical_model_name(model, harness, model_config);
if harness == "claude-code" && canonical.trim() == CLAUDE_CODE_OPUS_ALIAS {
return None;
}
Some(short_model_name(&canonical).to_string())
}
pub fn tier_from_model_name(
model_name: &str,
harness: &str,
model_config: &ModelConfig,
) -> Option<Tier> {
if let Some(map) = model_config.tiers.get(harness)
&& let Some(t) = map.tier_of(model_name)
{
return Some(t);
}
builtin_for(harness).tier_of(model_name).or_else(|| {
if harness == "claude-code" && is_claude_code_opus(model_name) {
Some(Tier::High)
} else {
None
}
})
}
pub fn extract_model_component(content: &str) -> Option<String> {
let comp = agent_doc_markdown_ast::overlay::components(content)
.into_iter()
.find(|c| c.name == "model")?;
let open_end = content[comp.start_byte..].find('\n')? + comp.start_byte + 1;
let before_end = content[..comp.end_byte].trim_end_matches('\n');
let close_start = before_end.rfind('\n').map_or(comp.start_byte, |i| i + 1);
if close_start < open_end {
return None;
}
let inner = &content[open_end..close_start];
let trimmed = inner.trim();
if trimmed.is_empty() {
None
} else {
Some(trimmed.to_string())
}
}
pub fn component_value_to_tier(
value: &str,
harness: &str,
model_config: &ModelConfig,
) -> Option<Tier> {
if let Ok(tier) = Tier::from_str(value) {
return Some(tier);
}
tier_from_model_name(value, harness, model_config)
}
pub fn suggested_tier(
diff_type: Option<&str>,
lines_added: usize,
doc_path: &std::path::Path,
) -> Tier {
let base = match diff_type {
Some("simple_question")
| Some("approval")
| Some("boundary_artifact")
| Some("annotation") => Tier::Low,
Some("content_addition") => {
if lines_added < 10 {
Tier::Low
} else {
Tier::Med
}
}
Some("multi_topic") | Some("structural_change") => Tier::Med,
_ => Tier::Med,
};
let path_str = doc_path.to_string_lossy();
let boost = path_str.contains("tasks/software/")
|| path_str.contains("/specs/")
|| path_str.contains("agent-doc-bugs")
|| path_str.contains("plan-")
|| path_str.contains("/plan.md");
if boost {
match base {
Tier::Auto | Tier::Low => Tier::Med,
Tier::Med => Tier::High,
Tier::High => Tier::High,
}
} else {
base
}
}
#[derive(Debug, Clone)]
pub struct ModelSwitchScan {
pub model_switch: Option<String>,
pub model_switch_tier: Option<Tier>,
pub stripped_diff: String,
}
pub fn scan_model_switch(diff: &str, harness: &str, model_config: &ModelConfig) -> ModelSwitchScan {
let mut model_switch: Option<String> = None;
let mut model_switch_tier: Option<Tier> = None;
let mut kept_lines: Vec<&str> = Vec::with_capacity(diff.lines().count());
let mut in_fence = false;
let mut fence_char = '`';
let mut fence_len = 0usize;
for line in diff.lines() {
if line.starts_with("---") || line.starts_with("+++") || line.starts_with("@@") {
kept_lines.push(line);
continue;
}
let content = if line.starts_with('+') || line.starts_with('-') || line.starts_with(' ') {
&line[1..]
} else {
line
};
let trimmed = content.trim_start();
if !in_fence {
let fc = trimmed.chars().next().unwrap_or('\0');
if (fc == '`' || fc == '~')
&& let fl = trimmed.chars().take_while(|&c| c == fc).count()
&& fl >= 3
{
in_fence = true;
fence_char = fc;
fence_len = fl;
kept_lines.push(line);
continue;
}
} else {
let fc = trimmed.chars().next().unwrap_or('\0');
if fc == fence_char {
let fl = trimmed.chars().take_while(|&c| c == fc).count();
if fl >= fence_len && trimmed[fl..].trim().is_empty() {
in_fence = false;
kept_lines.push(line);
continue;
}
}
}
let is_added = line.starts_with('+') && !line.starts_with("+++");
if !is_added {
kept_lines.push(line);
continue;
}
if in_fence {
kept_lines.push(line);
continue;
}
if content.starts_with('>') {
kept_lines.push(line);
continue;
}
let stripped = content.trim_end();
if let Some(rest) = stripped.strip_prefix("/model")
&& let Some(arg) = rest.split_whitespace().next()
&& !arg.is_empty()
{
if let Some((tier, name)) = parse_model_arg(arg, harness, model_config) {
if model_switch.is_none() {
model_switch = Some(name);
model_switch_tier = Some(tier);
}
continue;
}
continue;
}
kept_lines.push(line);
}
ModelSwitchScan {
model_switch,
model_switch_tier,
stripped_diff: kept_lines.join("\n"),
}
}
pub fn compose_effective_tier(
model_switch_tier: Option<Tier>,
component_tier: Option<Tier>,
frontmatter_tier: Option<Tier>,
suggested: Tier,
) -> Tier {
for candidate in [model_switch_tier, component_tier, frontmatter_tier] {
if let Some(t) = candidate
&& !matches!(t, Tier::Auto)
{
return t;
}
}
suggested
}
pub fn parse_model_arg(
arg: &str,
harness: &str,
model_config: &ModelConfig,
) -> Option<(Tier, String)> {
let trimmed = arg.trim();
if let Ok(tier) = Tier::from_str(trimmed) {
if matches!(tier, Tier::Auto) {
return None;
}
let name = resolve_tier_to_model(tier, harness, model_config)
.unwrap_or_else(|| trimmed.to_string());
return Some((tier, name));
}
if let Some(tier) = tier_from_model_name(trimmed, harness, model_config) {
return Some((tier, canonical_model_name(trimmed, harness, model_config)));
}
if harness == "opencode" && !is_bare_model_name(trimmed) {
return Some((
Tier::Auto,
canonical_model_name(trimmed, harness, model_config),
));
}
None
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::Mutex;
static ENV_LOCK: Mutex<()> = Mutex::new(());
struct EnvRestore {
values: Vec<(&'static str, Option<std::ffi::OsString>)>,
}
impl EnvRestore {
fn clear(keys: &[&'static str]) -> Self {
let values = keys
.iter()
.map(|key| (*key, std::env::var_os(key)))
.collect::<Vec<_>>();
for key in keys {
unsafe { std::env::remove_var(key) };
}
Self { values }
}
}
impl Drop for EnvRestore {
fn drop(&mut self) {
for (key, value) in &self.values {
match value {
Some(value) => {
unsafe { std::env::set_var(key, value) };
}
None => {
unsafe { std::env::remove_var(key) };
}
}
}
}
}
const HARNESS_ENV_KEYS: &[&str] = &[
"CLAUDE_CODE_SESSION",
"CLAUDE_CODE",
"CLAUDECODE",
"CODEX_SESSION",
"CODEX_THREAD_ID",
"CODEX_CLI",
"CODEX",
"OPENCODE_CLIENT",
"OPENCODE",
];
#[test]
fn tier_ordering() {
assert!(Tier::Auto < Tier::Low);
assert!(Tier::Low < Tier::Med);
assert!(Tier::Med < Tier::High);
assert!(Tier::High > Tier::Low);
assert!(Tier::Med >= Tier::Med);
}
#[test]
fn tier_from_str_case_insensitive() {
assert_eq!("LOW".parse::<Tier>().unwrap(), Tier::Low);
assert_eq!("low".parse::<Tier>().unwrap(), Tier::Low);
assert_eq!("Low".parse::<Tier>().unwrap(), Tier::Low);
assert_eq!("AUTO".parse::<Tier>().unwrap(), Tier::Auto);
assert_eq!("med".parse::<Tier>().unwrap(), Tier::Med);
assert_eq!("medium".parse::<Tier>().unwrap(), Tier::Med);
assert_eq!("HIGH".parse::<Tier>().unwrap(), Tier::High);
}
#[test]
fn tier_from_str_invalid() {
assert!("ultra".parse::<Tier>().is_err());
assert!("".parse::<Tier>().is_err());
assert!("opus".parse::<Tier>().is_err());
}
#[test]
fn tier_display() {
assert_eq!(Tier::Low.to_string(), "low");
assert_eq!(Tier::Med.to_string(), "med");
assert_eq!(Tier::High.to_string(), "high");
assert_eq!(Tier::Auto.to_string(), "auto");
}
#[test]
fn harness_detection_returns_known_value() {
let h = detect_harness();
assert!(
matches!(h.as_str(), "claude-code" | "codex" | "opencode" | "default"),
"unexpected harness: {h}"
);
}
#[test]
fn harness_key_for_agent_name_maps_cli_aliases() {
assert_eq!(harness_key_for_agent_name("claude"), "claude-code");
assert_eq!(harness_key_for_agent_name("claude_code"), "claude-code");
assert_eq!(harness_key_for_agent_name("codex"), "codex");
assert_eq!(harness_key_for_agent_name("opencode"), "opencode");
}
#[test]
fn canonical_harness_name_skips_default_and_normalizes_aliases() {
assert_eq!(canonical_harness_name("default"), None);
assert_eq!(canonical_harness_name("generic"), None);
assert_eq!(
canonical_harness_name("claude_code").as_deref(),
Some("claude-code")
);
assert_eq!(
canonical_harness_name("openai-codex").as_deref(),
Some("codex")
);
assert_eq!(
canonical_harness_name("open-code").as_deref(),
Some("opencode")
);
}
#[test]
fn harness_mismatch_warning_normalizes_aliases() {
assert!(
harness_mismatch_warning(Some("claude"), "claude-code").is_none(),
"claude and claude-code are the same canonical harness"
);
let warning = harness_mismatch_warning(Some("codex"), "claude-code").unwrap();
assert_eq!(warning.code, HARNESS_MISMATCH_WARNING_CODE);
assert_eq!(warning.document_agent, "codex");
assert_eq!(warning.active_harness, "claude-code");
assert!(warning.message.contains("Document declares agent: codex"));
}
#[test]
fn harness_mismatch_warning_skips_unknown_active_harness() {
assert!(harness_mismatch_warning(Some("codex"), "default").is_none());
assert!(harness_mismatch_warning(None, "claude-code").is_none());
}
#[test]
fn codex_network_access_warning_fires_for_non_codex_harness() {
let warning = codex_network_access_non_codex_harness_warning(
"tasks/session.md",
Some("opencode"),
"opencode",
true,
)
.unwrap();
assert_eq!(
warning.code,
CODEX_NETWORK_ACCESS_NON_CODEX_HARNESS_WARNING_CODE
);
assert_eq!(warning.document_agent.as_deref(), Some("opencode"));
assert_eq!(warning.active_harness, "opencode");
assert!(warning.message.contains("tasks/session.md"));
assert!(
warning
.message
.contains("`codex_network_access` is Codex-specific")
);
}
#[test]
fn codex_network_access_warning_skips_codex_and_unconfigured() {
assert!(
codex_network_access_non_codex_harness_warning(
"tasks/session.md",
Some("codex"),
"codex-cli",
true,
)
.is_none()
);
assert!(
codex_network_access_non_codex_harness_warning(
"tasks/session.md",
Some("opencode"),
"opencode",
false,
)
.is_none()
);
}
#[test]
fn harness_detection_recognizes_cli_environment_aliases() {
let _env_lock = ENV_LOCK
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner());
let _restore = EnvRestore::clear(HARNESS_ENV_KEYS);
unsafe { std::env::set_var("CLAUDE_CODE", "1") };
assert_eq!(detect_harness(), "claude-code");
unsafe { std::env::remove_var("CLAUDE_CODE") };
unsafe { std::env::set_var("CODEX_THREAD_ID", "thread-123") };
assert_eq!(detect_harness(), "codex");
unsafe { std::env::remove_var("CODEX_THREAD_ID") };
unsafe { std::env::set_var("OPENCODE", "1") };
assert_eq!(detect_harness(), "opencode");
}
#[test]
fn resolve_builtin_claude_code() {
let cfg = ModelConfig::default();
assert_eq!(
resolve_tier_to_model(Tier::High, "claude-code", &cfg).as_deref(),
Some("opus")
);
assert_eq!(
resolve_tier_to_model(Tier::Med, "claude-code", &cfg).as_deref(),
Some("sonnet")
);
assert_eq!(
resolve_tier_to_model(Tier::Low, "claude-code", &cfg).as_deref(),
Some("haiku")
);
assert_eq!(resolve_tier_to_model(Tier::Auto, "claude-code", &cfg), None);
}
#[test]
fn resolve_builtin_codex() {
let cfg = ModelConfig::default();
assert_eq!(
resolve_tier_to_model(Tier::High, "codex", &cfg).as_deref(),
Some("o3")
);
assert_eq!(
resolve_tier_to_model(Tier::Low, "codex", &cfg).as_deref(),
Some("gpt-4o-mini")
);
}
#[test]
fn resolve_unknown_harness_uses_default() {
let cfg = ModelConfig::default();
assert_eq!(
resolve_tier_to_model(Tier::High, "junie", &cfg).as_deref(),
Some("opus")
);
}
#[test]
fn user_config_overrides_builtin() {
let mut cfg = ModelConfig::default();
let mut tiers = BTreeMap::new();
tiers.insert(
"claude-code".to_string(),
TierMap {
low: Some("haiku-3".to_string()),
med: Some("sonnet-4".to_string()),
high: Some("opus-4-1".to_string()),
},
);
cfg.tiers = tiers;
assert_eq!(
resolve_tier_to_model(Tier::High, "claude-code", &cfg).as_deref(),
Some("opus-4-1")
);
}
#[test]
fn tier_from_model_name_builtin() {
let cfg = ModelConfig::default();
assert_eq!(
tier_from_model_name("opus", "claude-code", &cfg),
Some(Tier::High)
);
assert_eq!(
tier_from_model_name("claude-opus-4-8", "claude-code", &cfg),
Some(Tier::High)
);
assert_eq!(
tier_from_model_name("sonnet", "claude-code", &cfg),
Some(Tier::Med)
);
assert_eq!(
tier_from_model_name("haiku", "claude-code", &cfg),
Some(Tier::Low)
);
assert_eq!(tier_from_model_name("unknown", "claude-code", &cfg), None);
}
#[test]
fn parse_model_arg_tier_name() {
let cfg = ModelConfig::default();
let (tier, name) = parse_model_arg("high", "claude-code", &cfg).unwrap();
assert_eq!(tier, Tier::High);
assert_eq!(name, "opus");
}
#[test]
fn parse_model_arg_concrete_name() {
let cfg = ModelConfig::default();
let (tier, name) = parse_model_arg("opus", "claude-code", &cfg).unwrap();
assert_eq!(tier, Tier::High);
assert_eq!(name, "opus");
}
#[test]
fn canonical_model_name_defers_claude_code_opus_alias() {
let cfg = ModelConfig::default();
assert_eq!(canonical_model_name("opus", "claude-code", &cfg), "opus");
assert_eq!(canonical_model_name("opus", "codex", &cfg), "opus");
assert_eq!(
canonical_model_name("claude-opus-4-8", "claude-code", &cfg),
"claude-opus-4-8"
);
}
#[test]
fn tier_from_model_name_classifies_concrete_and_deferred_opus_as_high() {
let cfg = ModelConfig::default();
assert_eq!(
tier_from_model_name("opus", "claude-code", &cfg),
Some(Tier::High)
);
assert_eq!(
tier_from_model_name("claude-opus-4-8", "claude-code", &cfg),
Some(Tier::High)
);
assert_eq!(
tier_from_model_name("claude-opus-4-9", "claude-code", &cfg),
Some(Tier::High)
);
}
#[test]
fn is_bare_model_name_detects_missing_provider_prefix() {
assert!(is_bare_model_name("glm-5.1"));
assert!(is_bare_model_name("opus"));
assert!(is_bare_model_name("haiku"));
assert!(!is_bare_model_name("zai-coding-plan/glm-5.1"));
assert!(!is_bare_model_name("anthropic/claude-opus-4-7"));
assert!(is_bare_model_name("/leading-slash-only"));
}
#[test]
fn canonical_model_name_warns_on_bare_opencode_name() {
let cfg = ModelConfig::default();
assert_eq!(canonical_model_name("glm-5.1", "opencode", &cfg), "glm-5.1");
assert_eq!(
canonical_model_name("zai-coding-plan/glm-5.1", "opencode", &cfg),
"zai-coding-plan/glm-5.1"
);
}
#[test]
fn short_model_name_strips_claude_prefix() {
assert_eq!(short_model_name("claude-sonnet-4-6"), "sonnet-4-6");
assert_eq!(short_model_name("claude-opus-4"), "opus-4");
assert_eq!(short_model_name("claude-haiku-4-5"), "haiku-4-5");
}
#[test]
fn short_model_name_returns_as_is_without_prefix() {
assert_eq!(short_model_name("sonnet-4-6"), "sonnet-4-6");
assert_eq!(short_model_name("gpt-4o"), "gpt-4o");
assert_eq!(short_model_name("gpt-5"), "gpt-5");
assert_eq!(short_model_name("gpt-5.4"), "gpt-5.4");
assert_eq!(short_model_name("opus-4-6"), "opus-4-6");
assert_eq!(short_model_name(""), "");
}
#[test]
fn resolve_agent_model_uses_frontmatter_only() {
let cfg = ModelConfig::default();
let result = resolve_agent_model(Some("claude-opus-4"), "claude-code", &cfg);
assert_eq!(result, Some("opus-4".to_string()));
}
#[test]
fn resolve_agent_model_strips_claude_prefix_from_frontmatter() {
let cfg = ModelConfig::default();
let result = resolve_agent_model(Some("claude-haiku-4-5"), "claude-code", &cfg);
assert_eq!(result, Some("haiku-4-5".to_string()));
}
#[test]
fn resolve_agent_model_defers_claude_code_opus_alias() {
let cfg = ModelConfig::default();
let result = resolve_agent_model(Some("opus"), "claude-code", &cfg);
assert_eq!(result, None);
}
#[test]
fn resolve_agent_model_stamps_pinned_concrete_opus() {
let cfg = ModelConfig::default();
let result = resolve_agent_model(Some("claude-opus-4-8"), "claude-code", &cfg);
assert_eq!(result, Some("opus-4-8".to_string()));
}
#[test]
fn resolve_agent_model_preserves_short_openai_style_name() {
let cfg = ModelConfig::default();
let result = resolve_agent_model(Some("gpt-5"), "codex", &cfg);
assert_eq!(result, Some("gpt-5".to_string()));
}
#[test]
fn resolve_agent_model_none_when_no_frontmatter() {
let cfg = ModelConfig::default();
let result = resolve_agent_model(None, "claude-code", &cfg);
assert_eq!(result, None);
}
#[test]
fn parse_model_arg_opencode_rejects_bare_name() {
let cfg = ModelConfig::default();
assert!(parse_model_arg("glm-5.1", "opencode", &cfg).is_none());
}
#[test]
fn parse_model_arg_opencode_accepts_provider_prefixed_name() {
let cfg = ModelConfig::default();
let (tier, name) = parse_model_arg("zai-coding-plan/glm-5.1", "opencode", &cfg).unwrap();
assert_eq!(tier, Tier::Auto);
assert_eq!(name, "zai-coding-plan/glm-5.1");
}
#[test]
fn parse_model_arg_unknown() {
let cfg = ModelConfig::default();
assert!(parse_model_arg("xyz-3000", "claude-code", &cfg).is_none());
}
#[test]
fn parse_model_arg_auto_rejected() {
let cfg = ModelConfig::default();
assert!(parse_model_arg("auto", "claude-code", &cfg).is_none());
}
#[test]
fn extract_model_component_present() {
let doc = "# Title\n\n<!-- agent:model -->\nhigh\n<!-- /agent:model -->\n\nbody\n";
assert_eq!(extract_model_component(doc).as_deref(), Some("high"));
}
#[test]
fn extract_model_component_absent() {
let doc = "# Title\n\nbody only\n";
assert_eq!(extract_model_component(doc), None);
}
#[test]
fn extract_model_component_empty_inner() {
let doc = "<!-- agent:model -->\n<!-- /agent:model -->\n";
assert_eq!(extract_model_component(doc), None);
}
#[test]
fn extract_model_component_concrete_name() {
let doc = "<!-- agent:model -->\nopus\n<!-- /agent:model -->\n";
assert_eq!(extract_model_component(doc).as_deref(), Some("opus"));
}
#[test]
fn component_value_to_tier_tier_name() {
let cfg = ModelConfig::default();
assert_eq!(
component_value_to_tier("high", "claude-code", &cfg),
Some(Tier::High)
);
}
#[test]
fn component_value_to_tier_concrete_name() {
let cfg = ModelConfig::default();
assert_eq!(
component_value_to_tier("opus", "claude-code", &cfg),
Some(Tier::High)
);
}
#[test]
fn component_value_to_tier_unknown() {
let cfg = ModelConfig::default();
assert_eq!(component_value_to_tier("xyz", "claude-code", &cfg), None);
}
#[test]
fn suggested_tier_simple_question() {
let path = std::path::Path::new("tasks/research/x.md");
assert_eq!(suggested_tier(Some("simple_question"), 1, path), Tier::Low);
}
#[test]
fn suggested_tier_small_addition() {
let path = std::path::Path::new("tasks/research/x.md");
assert_eq!(suggested_tier(Some("content_addition"), 5, path), Tier::Low);
}
#[test]
fn suggested_tier_large_addition() {
let path = std::path::Path::new("tasks/research/x.md");
assert_eq!(
suggested_tier(Some("content_addition"), 50, path),
Tier::Med
);
}
#[test]
fn suggested_tier_default_for_unknown() {
let path = std::path::Path::new("tasks/research/x.md");
assert_eq!(suggested_tier(None, 0, path), Tier::Med);
}
#[test]
fn suggested_tier_path_boost_software() {
let path = std::path::Path::new("tasks/software/foo.md");
assert_eq!(suggested_tier(Some("simple_question"), 1, path), Tier::Med);
assert_eq!(
suggested_tier(Some("content_addition"), 50, path),
Tier::High
);
}
#[test]
fn suggested_tier_path_boost_caps_at_high() {
let path = std::path::Path::new("tasks/software/foo.md");
let t = suggested_tier(Some("content_addition"), 50, path);
assert_eq!(t, Tier::High);
}
#[test]
fn compose_effective_tier_model_switch_wins() {
let t = compose_effective_tier(
Some(Tier::High),
Some(Tier::Low),
Some(Tier::Med),
Tier::Low,
);
assert_eq!(t, Tier::High);
}
#[test]
fn compose_effective_tier_component_beats_frontmatter() {
let t = compose_effective_tier(None, Some(Tier::High), Some(Tier::Low), Tier::Med);
assert_eq!(t, Tier::High);
}
#[test]
fn compose_effective_tier_frontmatter_beats_heuristic() {
let t = compose_effective_tier(None, None, Some(Tier::High), Tier::Low);
assert_eq!(t, Tier::High);
}
#[test]
fn compose_effective_tier_falls_through_to_heuristic() {
let t = compose_effective_tier(None, None, None, Tier::Med);
assert_eq!(t, Tier::Med);
}
#[test]
fn scan_model_switch_concrete_name() {
let cfg = ModelConfig::default();
let diff = "@@ -1,3 +1,4 @@\n context\n+/model opus\n+real edit\n";
let result = scan_model_switch(diff, "claude-code", &cfg);
assert_eq!(result.model_switch.as_deref(), Some("opus"));
assert_eq!(result.model_switch_tier, Some(Tier::High));
assert!(!result.stripped_diff.contains("/model opus"));
assert!(result.stripped_diff.contains("real edit"));
}
#[test]
fn scan_model_switch_tier_name() {
let cfg = ModelConfig::default();
let diff = "+/model high\n+other line\n";
let result = scan_model_switch(diff, "claude-code", &cfg);
assert_eq!(result.model_switch_tier, Some(Tier::High));
assert_eq!(result.model_switch.as_deref(), Some("opus"));
assert!(!result.stripped_diff.contains("/model high"));
}
#[test]
fn scan_model_switch_haiku() {
let cfg = ModelConfig::default();
let diff = "+/model haiku\n";
let result = scan_model_switch(diff, "claude-code", &cfg);
assert_eq!(result.model_switch_tier, Some(Tier::Low));
}
#[test]
fn scan_model_switch_inside_fenced_code_ignored() {
let cfg = ModelConfig::default();
let diff = "+```\n+/model opus\n+```\n+real line\n";
let result = scan_model_switch(diff, "claude-code", &cfg);
assert_eq!(result.model_switch, None);
assert!(result.stripped_diff.contains("/model opus"));
}
#[test]
fn scan_model_switch_inside_blockquote_ignored() {
let cfg = ModelConfig::default();
let diff = "+> /model opus\n+real line\n";
let result = scan_model_switch(diff, "claude-code", &cfg);
assert_eq!(result.model_switch, None);
assert!(result.stripped_diff.contains("/model opus"));
}
#[test]
fn scan_model_switch_only_added_lines() {
let cfg = ModelConfig::default();
let diff = " /model opus\n+real line\n";
let result = scan_model_switch(diff, "claude-code", &cfg);
assert_eq!(result.model_switch, None);
}
#[test]
fn scan_model_switch_no_match() {
let cfg = ModelConfig::default();
let diff = "+just a normal line\n+another\n";
let result = scan_model_switch(diff, "claude-code", &cfg);
assert_eq!(result.model_switch, None);
assert!(result.stripped_diff.contains("just a normal line"));
assert!(result.stripped_diff.contains("another"));
}
#[test]
fn scan_model_switch_unknown_arg_still_stripped() {
let cfg = ModelConfig::default();
let diff = "+/model xyz-3000\n+real line\n";
let result = scan_model_switch(diff, "claude-code", &cfg);
assert_eq!(result.model_switch, None);
assert!(!result.stripped_diff.contains("/model xyz-3000"));
assert!(result.stripped_diff.contains("real line"));
}
#[test]
fn scan_model_switch_first_match_wins() {
let cfg = ModelConfig::default();
let diff = "+/model opus\n+/model haiku\n";
let result = scan_model_switch(diff, "claude-code", &cfg);
assert_eq!(result.model_switch.as_deref(), Some("opus"));
assert!(!result.stripped_diff.contains("/model"));
}
#[test]
fn compose_effective_tier_auto_falls_through() {
let t = compose_effective_tier(
Some(Tier::Auto),
Some(Tier::Auto),
Some(Tier::High),
Tier::Low,
);
assert_eq!(t, Tier::High);
}
}