fix(model): resolve provider-aware fallback model IDs

This commit is contained in:
argenis de la rosa 2026-02-28 15:53:54 -05:00 committed by Argenis
parent 5d248bf6bf
commit df9ebcb3d2
6 changed files with 179 additions and 32 deletions

View File

@ -218,9 +218,7 @@ impl AgentBuilder {
.memory_loader
.unwrap_or_else(|| Box::new(DefaultMemoryLoader::default())),
config: self.config.unwrap_or_default(),
model_name: self
.model_name
.unwrap_or_else(|| "anthropic/claude-sonnet-4-20250514".into()),
model_name: crate::config::resolve_default_model_id(self.model_name.as_deref(), None),
temperature: self.temperature.unwrap_or(0.7),
workspace_dir: self
.workspace_dir
@ -298,11 +296,10 @@ impl Agent {
let provider_name = config.default_provider.as_deref().unwrap_or("openrouter");
let model_name = config
.default_model
.as_deref()
.unwrap_or("anthropic/claude-sonnet-4-20250514")
.to_string();
let model_name = crate::config::resolve_default_model_id(
config.default_model.as_deref(),
Some(provider_name),
);
let provider: Box<dyn Provider> = providers::create_routed_provider(
provider_name,
@ -714,8 +711,12 @@ pub async fn run(
let model_name = effective_config
.default_model
.as_deref()
.unwrap_or("anthropic/claude-sonnet-4-20250514")
.to_string();
.map(str::trim)
.filter(|m| !m.is_empty())
.map(str::to_string)
.unwrap_or_else(|| {
crate::config::default_model_fallback_for_provider(Some(&provider_name)).to_string()
});
agent.observer.record_event(&ObserverEvent::AgentStart {
provider: provider_name.clone(),

View File

@ -1816,10 +1816,12 @@ pub async fn run(
.or(config.default_provider.as_deref())
.unwrap_or("openrouter");
let model_name = model_override
.as_deref()
.or(config.default_model.as_deref())
.unwrap_or("anthropic/claude-sonnet-4");
let model_name = crate::config::resolve_default_model_id(
model_override
.as_deref()
.or(config.default_model.as_deref()),
Some(provider_name),
);
let provider_runtime_options = providers::ProviderRuntimeOptions {
auth_profile_override: None,
@ -1840,7 +1842,7 @@ pub async fn run(
config.api_url.as_deref(),
&config.reliability,
&config.model_routes,
model_name,
&model_name,
&provider_runtime_options,
)?;
@ -2003,7 +2005,7 @@ pub async fn run(
let native_tools = provider.supports_native_tools();
let mut system_prompt = crate::channels::build_system_prompt_with_mode(
&config.workspace_dir,
model_name,
&model_name,
&tool_descs,
&skills,
Some(&config.identity),
@ -2085,7 +2087,7 @@ pub async fn run(
&tools_registry,
observer.as_ref(),
provider_name,
model_name,
&model_name,
temperature,
false,
approval_manager.as_ref(),
@ -2251,7 +2253,7 @@ pub async fn run(
&tools_registry,
observer.as_ref(),
provider_name,
model_name,
&model_name,
temperature,
false,
approval_manager.as_ref(),
@ -2307,7 +2309,7 @@ pub async fn run(
if let Ok(compacted) = auto_compact_history(
&mut history,
provider.as_ref(),
model_name,
&model_name,
config.agent.max_history_messages,
)
.await
@ -2388,10 +2390,10 @@ pub async fn process_message_with_session(
tools_registry.extend(peripheral_tools);
let provider_name = config.default_provider.as_deref().unwrap_or("openrouter");
let model_name = config
.default_model
.clone()
.unwrap_or_else(|| "anthropic/claude-sonnet-4-20250514".into());
let model_name = crate::config::resolve_default_model_id(
config.default_model.as_deref(),
Some(provider_name),
);
let provider_runtime_options = providers::ProviderRuntimeOptions {
auth_profile_override: None,
provider_api_url: config.api_url.clone(),

View File

@ -938,10 +938,10 @@ fn resolved_default_provider(config: &Config) -> String {
}
fn resolved_default_model(config: &Config) -> String {
config
.default_model
.clone()
.unwrap_or_else(|| "anthropic/claude-sonnet-4.6".to_string())
crate::config::resolve_default_model_id(
config.default_model.as_deref(),
config.default_provider.as_deref(),
)
}
fn runtime_defaults_from_config(config: &Config) -> ChannelRuntimeDefaults {
@ -8790,6 +8790,27 @@ BTC is currently around $65,000 based on latest tool output."#
assert_eq!(policy.outbound_leak_guard.sensitivity, 0.95);
}
#[tokio::test]
async fn load_runtime_defaults_from_config_file_uses_provider_fallback_when_model_missing() {
let temp = tempfile::TempDir::new().expect("temp dir");
let config_path = temp.path().join("config.toml");
let workspace_dir = temp.path().join("workspace");
std::fs::create_dir_all(&workspace_dir).expect("workspace dir");
let mut cfg = Config::default();
cfg.config_path = config_path.clone();
cfg.workspace_dir = workspace_dir;
cfg.default_provider = Some("openai".to_string());
cfg.default_model = None;
cfg.save().await.expect("save config");
let (defaults, _policy) = load_runtime_defaults_from_config_file(&config_path)
.await
.expect("runtime defaults");
assert_eq!(defaults.default_provider, "openai");
assert_eq!(defaults.model, "gpt-5.2");
}
#[tokio::test]
async fn maybe_apply_runtime_config_update_refreshes_autonomy_policy_and_excluded_tools() {
let temp = tempfile::TempDir::new().expect("temp dir");

View File

@ -4,9 +4,10 @@ pub mod traits;
#[allow(unused_imports)]
pub use schema::{
apply_runtime_proxy_to_builder, build_runtime_proxy_client,
build_runtime_proxy_client_with_timeouts, runtime_proxy_config, set_runtime_proxy_config,
AgentConfig, AgentsIpcConfig, AuditConfig, AutonomyConfig, BrowserComputerUseConfig,
BrowserConfig, BuiltinHooksConfig, ChannelsConfig, ClassificationRule, ComposioConfig, Config,
build_runtime_proxy_client_with_timeouts, default_model_fallback_for_provider,
resolve_default_model_id, runtime_proxy_config, set_runtime_proxy_config, AgentConfig,
AgentsIpcConfig, AuditConfig, AutonomyConfig, BrowserComputerUseConfig, BrowserConfig,
BuiltinHooksConfig, ChannelsConfig, ClassificationRule, ComposioConfig, Config,
CoordinationConfig, CostConfig, CronConfig, DelegateAgentConfig, DiscordConfig,
DockerRuntimeConfig, EconomicConfig, EconomicTokenPricing, EmbeddingRouteConfig, EstopConfig,
FeishuConfig, GatewayConfig, GroupReplyConfig, GroupReplyMode, HardwareConfig,
@ -23,6 +24,7 @@ pub use schema::{
StorageProviderSection, StreamMode, SyscallAnomalyConfig, TelegramConfig, TranscriptionConfig,
TunnelConfig, UrlAccessConfig, WasmCapabilityEscalationMode, WasmConfig, WasmModuleHashPolicy,
WasmRuntimeConfig, WasmSecurityConfig, WebFetchConfig, WebSearchConfig, WebhookConfig,
DEFAULT_MODEL_FALLBACK,
};
pub fn name_and_presence<T: traits::ChannelConfig>(channel: Option<&T>) -> (&'static str, bool) {

View File

@ -1,5 +1,7 @@
use crate::config::traits::ChannelConfig;
use crate::providers::{is_glm_alias, is_zai_alias};
use crate::providers::{
canonical_china_provider_name, is_glm_alias, is_qwen_oauth_alias, is_zai_alias,
};
use crate::security::{AutonomyLevel, DomainMatcher};
use anyhow::{Context, Result};
use directories::UserDirs;
@ -14,6 +16,100 @@ use tokio::fs::File;
use tokio::fs::{self, OpenOptions};
use tokio::io::AsyncWriteExt;
/// Default fallback model when none is configured. Uses a format compatible with
/// OpenRouter and other multi-provider gateways. For Anthropic direct API, this
/// model ID will be normalized by the provider layer.
pub const DEFAULT_MODEL_FALLBACK: &str = "anthropic/claude-sonnet-4.6";
fn canonical_provider_for_model_defaults(provider_name: &str) -> String {
if let Some(canonical) = canonical_china_provider_name(provider_name) {
return if canonical == "doubao" {
"volcengine".to_string()
} else {
canonical.to_string()
};
}
match provider_name {
"grok" => "xai".to_string(),
"together" => "together-ai".to_string(),
"google" | "google-gemini" => "gemini".to_string(),
"github-copilot" => "copilot".to_string(),
"openai_codex" | "codex" => "openai-codex".to_string(),
"kimi_coding" | "kimi_for_coding" => "kimi-code".to_string(),
"nvidia-nim" | "build.nvidia.com" => "nvidia".to_string(),
"aws-bedrock" => "bedrock".to_string(),
"llama.cpp" => "llamacpp".to_string(),
_ => provider_name.to_string(),
}
}
/// Returns a provider-aware fallback model ID when `default_model` is missing.
pub fn default_model_fallback_for_provider(provider_name: Option<&str>) -> &'static str {
let normalized_provider = provider_name
.unwrap_or("openrouter")
.trim()
.to_ascii_lowercase()
.replace('_', "-");
if normalized_provider == "qwen-coding-plan" {
return "qwen3-coder-plus";
}
let canonical_provider = if is_qwen_oauth_alias(&normalized_provider) {
"qwen-code".to_string()
} else {
canonical_provider_for_model_defaults(&normalized_provider)
};
match canonical_provider.as_str() {
"anthropic" => "claude-sonnet-4-5-20250929",
"openai" => "gpt-5.2",
"openai-codex" => "gpt-5-codex",
"venice" => "zai-org-glm-5",
"groq" => "llama-3.3-70b-versatile",
"mistral" => "mistral-large-latest",
"deepseek" => "deepseek-chat",
"xai" => "grok-4-1-fast-reasoning",
"perplexity" => "sonar-pro",
"fireworks" => "accounts/fireworks/models/llama-v3p3-70b-instruct",
"novita" => "minimax/minimax-m2.5",
"together-ai" => "meta-llama/Llama-3.3-70B-Instruct-Turbo",
"cohere" => "command-a-03-2025",
"moonshot" => "kimi-k2.5",
"hunyuan" => "hunyuan-t1-latest",
"glm" | "zai" => "glm-5",
"minimax" => "MiniMax-M2.5",
"qwen" => "qwen-plus",
"volcengine" => "doubao-1-5-pro-32k-250115",
"siliconflow" => "Pro/zai-org/GLM-4.7",
"qwen-code" => "qwen3-coder-plus",
"ollama" => "llama3.2",
"llamacpp" => "ggml-org/gpt-oss-20b-GGUF",
"sglang" | "vllm" | "osaurus" | "copilot" => "default",
"gemini" => "gemini-2.5-pro",
"kimi-code" => "kimi-for-coding",
"bedrock" => "anthropic.claude-sonnet-4-5-20250929-v1:0",
"nvidia" => "meta/llama-3.3-70b-instruct",
_ => DEFAULT_MODEL_FALLBACK,
}
}
/// Resolves the model ID used by runtime components.
/// Preference order:
/// 1) Explicit configured model (if non-empty)
/// 2) Provider-aware fallback
pub fn resolve_default_model_id(
default_model: Option<&str>,
provider_name: Option<&str>,
) -> String {
if let Some(model) = default_model.map(str::trim).filter(|m| !m.is_empty()) {
return model.to_string();
}
default_model_fallback_for_provider(provider_name).to_string()
}
const SUPPORTED_PROXY_SERVICE_KEYS: &[&str] = &[
"provider.anthropic",
"provider.compatible",
@ -10953,6 +11049,31 @@ provider_api = "not-a-real-mode"
std::env::remove_var("ZEROCLAW_MODEL");
}
#[test]
async fn resolve_default_model_id_prefers_configured_model() {
let resolved =
resolve_default_model_id(Some(" anthropic/claude-opus-4.6 "), Some("openrouter"));
assert_eq!(resolved, "anthropic/claude-opus-4.6");
}
#[test]
async fn resolve_default_model_id_uses_provider_specific_fallback() {
let openai = resolve_default_model_id(None, Some("openai"));
assert_eq!(openai, "gpt-5.2");
let bedrock = resolve_default_model_id(None, Some("aws-bedrock"));
assert_eq!(bedrock, "anthropic.claude-sonnet-4-5-20250929-v1:0");
}
#[test]
async fn resolve_default_model_id_handles_special_provider_aliases() {
let qwen_coding_plan = resolve_default_model_id(None, Some("qwen-coding-plan"));
assert_eq!(qwen_coding_plan, "qwen3-coder-plus");
let google_alias = resolve_default_model_id(None, Some("google-gemini"));
assert_eq!(google_alias, "gemini-2.5-pro");
}
#[test]
async fn model_provider_profile_maps_to_custom_endpoint() {
let _env_guard = env_override_lock().await;

View File

@ -803,7 +803,7 @@ mod tests {
"coder".to_string(),
DelegateAgentConfig {
provider: "openrouter".to_string(),
model: "anthropic/claude-sonnet-4-20250514".to_string(),
model: crate::config::DEFAULT_MODEL_FALLBACK.to_string(),
system_prompt: None,
api_key: Some("delegate-test-credential".to_string()),
temperature: None,