Centralize model defaults in catalog, upgrade OpenAI default to gpt-5.4

Remove hardcoded default_model_for_provider() from arc-workflows and
default_model() from arc-agent, delegating both to the catalog via
arc_llm::catalog::default_model_for_provider(). Add claude-sonnet-4-6
to catalog and move "sonnet"/"claude-sonnet" aliases to it.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Bryan Helmkamp 2026-03-07 00:59:28 -05:00
parent 378c3eb3e3
commit bb48b0d33b
5 changed files with 35 additions and 140 deletions

View file

@ -95,18 +95,6 @@ impl AgentArgs {
}
}
pub fn default_model(provider: Provider) -> &'static str {
match provider {
Provider::OpenAi => "gpt-5.2-codex",
Provider::Gemini => "gemini-3.1-pro-preview",
Provider::Anthropic => "claude-opus-4-6",
Provider::Kimi => "kimi-k2.5",
Provider::Zai => "glm-4.7",
Provider::Minimax => "minimax-m2.5",
Provider::Inception => "mercury",
}
}
fn tool_category(name: &str) -> &'static str {
match name {
"read_file" | "read_many_files" | "grep" | "glob" | "list_dir" => "read",
@ -392,12 +380,13 @@ pub async fn run_with_args(args: AgentArgs) -> anyhow::Result<()> {
}
// Resolve model and build profile
let model = args
.model
.as_deref()
.unwrap_or_else(|| default_model(provider));
let model = args.model.unwrap_or_else(|| {
arc_llm::catalog::default_model_for_provider(provider.as_str())
.map(|m| m.id)
.unwrap_or_else(|| provider.as_str().to_string())
});
eprintln!("{}", styles.dim.apply_to(format!("Using model: {model}")));
let mut profile = build_profile(provider, model, Some(client.clone()));
let mut profile = build_profile(provider, &model, Some(client.clone()));
// Build sandbox
let cwd = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
@ -692,43 +681,6 @@ mod tests {
assert!(is_auto_approved(PermissionLevel::Full, "shell"));
}
// default_model tests
#[test]
fn default_model_anthropic() {
assert_eq!(default_model(Provider::Anthropic), "claude-opus-4-6");
}
#[test]
fn default_model_openai() {
assert_eq!(default_model(Provider::OpenAi), "gpt-5.2-codex");
}
#[test]
fn default_model_gemini() {
assert_eq!(default_model(Provider::Gemini), "gemini-3.1-pro-preview");
}
#[test]
fn default_model_kimi() {
assert_eq!(default_model(Provider::Kimi), "kimi-k2.5");
}
#[test]
fn default_model_zai() {
assert_eq!(default_model(Provider::Zai), "glm-4.7");
}
#[test]
fn default_model_minimax() {
assert_eq!(default_model(Provider::Minimax), "minimax-m2.5");
}
#[test]
fn default_model_inception() {
assert_eq!(default_model(Provider::Inception), "mercury");
}
// build_tool_approval non-interactive tests
#[test]

View file

@ -1,31 +1,15 @@
use arc_agent::cli::default_model;
use arc_agent::{AnthropicProfile, GeminiProfile, OpenAiProfile, ProviderProfile};
use arc_llm::catalog;
use arc_llm::provider::Provider;
#[test]
fn every_default_model_exists_in_catalog() {
for &provider in Provider::ALL {
let model = default_model(provider);
assert!(
catalog::get_model_info(model).is_some(),
"default_model for {:?} is '{}' but it is not in the catalog",
provider,
model
);
}
}
#[test]
fn profile_context_window_matches_catalog_for_default_models() {
for &provider in Provider::ALL {
let model = default_model(provider);
let catalog_info = catalog::get_model_info(model).unwrap_or_else(|| {
panic!(
"default_model '{}' for {:?} not in catalog",
model, provider
)
});
let catalog_info = catalog::default_model_for_provider(provider.as_str())
.unwrap_or_else(|| {
panic!("no default model for {:?} in catalog", provider)
});
let model = &catalog_info.id;
let profile: Box<dyn ProviderProfile> = match provider {
Provider::OpenAi => Box::new(OpenAiProfile::new(model)),

View file

@ -26,6 +26,20 @@
"input_cost_per_million": 3.0,
"output_cost_per_million": 15.0,
"estimated_output_tps": 50,
"aliases": []
},
{
"id": "claude-sonnet-4-6",
"provider": "anthropic",
"display_name": "Claude Sonnet 4.6",
"context_window": 200000,
"max_output": 64000,
"supports_tools": true,
"supports_vision": true,
"supports_reasoning": true,
"input_cost_per_million": 3.0,
"output_cost_per_million": 15.0,
"estimated_output_tps": 50,
"aliases": ["sonnet", "claude-sonnet"]
},
{
@ -54,8 +68,7 @@
"input_cost_per_million": 1.75,
"output_cost_per_million": 14.0,
"estimated_output_tps": 65,
"aliases": ["gpt5"],
"default": true
"aliases": ["gpt5"]
},
{
"id": "gpt-5-mini",
@ -111,7 +124,8 @@
"input_cost_per_million": 2.5,
"output_cost_per_million": 15.0,
"estimated_output_tps": 70,
"aliases": ["gpt54"]
"aliases": ["gpt54"],
"default": true
},
{
"id": "gpt-5.4-pro",

View file

@ -169,7 +169,7 @@ mod tests {
assert!(m.default);
let m = default_model_for_provider("openai").unwrap();
assert_eq!(m.id, "gpt-5.2");
assert_eq!(m.id, "gpt-5.4");
let m = default_model_for_provider("gemini").unwrap();
assert_eq!(m.id, "gemini-3.1-pro-preview");
@ -221,7 +221,7 @@ mod tests {
assert_eq!(info.id, "claude-opus-4-6");
let info = get_model_info("sonnet").unwrap();
assert_eq!(info.id, "claude-sonnet-4-5");
assert_eq!(info.id, "claude-sonnet-4-6");
let info = get_model_info("codex").unwrap();
assert_eq!(info.id, "gpt-5.3-codex");
@ -235,7 +235,7 @@ mod tests {
#[test]
fn list_models_by_provider() {
let anthropic = list_models(Some("anthropic"));
assert_eq!(anthropic.len(), 3);
assert_eq!(anthropic.len(), 4);
assert!(anthropic.iter().all(|m| m.provider == "anthropic"));
let openai = list_models(Some("openai"));
@ -323,7 +323,7 @@ mod tests {
assert!(m.supports_reasoning);
assert_eq!(m.input_cost_per_million, Some(2.5));
assert_eq!(m.output_cost_per_million, Some(15.0));
assert!(!m.default);
assert!(m.default);
}
#[test]

View file

@ -35,19 +35,6 @@ use super::{
RunArgs, SandboxProvider,
};
/// Return the default model string for a given provider.
fn default_model_for_provider(provider: Provider) -> &'static str {
match provider {
Provider::Anthropic => "claude-opus-4-6",
Provider::OpenAi => "gpt-5.2",
Provider::Gemini => "gemini-3.1-pro-preview",
Provider::Kimi => "kimi-k2.5",
Provider::Zai => "glm-4.7",
Provider::Minimax => "minimax-m2.5",
Provider::Inception => "mercury",
}
}
/// Resolve model and provider through the full precedence chain:
/// CLI flag > TOML config > run defaults > DOT graph attrs > provider-specific defaults.
/// Then resolve through the catalog for alias expansion.
@ -87,7 +74,9 @@ fn resolve_model_provider(
.as_deref()
.and_then(|s| s.parse::<Provider>().ok())
.unwrap_or(Provider::Anthropic);
default_model_for_provider(provider_enum).to_string()
arc_llm::catalog::default_model_for_provider(provider_enum.as_str())
.map(|m| m.id)
.unwrap_or_else(|| provider_enum.as_str().to_string())
});
// Resolve model alias through catalog
@ -1502,50 +1491,6 @@ async fn generate_retro(
mod tests {
use super::*;
#[test]
fn default_model_for_anthropic() {
assert_eq!(
default_model_for_provider(Provider::Anthropic),
"claude-opus-4-6"
);
}
#[test]
fn default_model_for_openai() {
assert_eq!(default_model_for_provider(Provider::OpenAi), "gpt-5.2");
}
#[test]
fn default_model_for_gemini() {
assert_eq!(
default_model_for_provider(Provider::Gemini),
"gemini-3.1-pro-preview"
);
}
#[test]
fn default_model_for_kimi() {
assert_eq!(default_model_for_provider(Provider::Kimi), "kimi-k2.5");
}
#[test]
fn default_model_for_zai() {
assert_eq!(default_model_for_provider(Provider::Zai), "glm-4.7");
}
#[test]
fn default_model_for_minimax() {
assert_eq!(
default_model_for_provider(Provider::Minimax),
"minimax-m2.5"
);
}
#[test]
fn default_model_for_inception() {
assert_eq!(default_model_for_provider(Provider::Inception), "mercury");
}
#[test]
fn resolve_model_provider_defaults() {
let graph = crate::graph::types::Graph::new("test");