fix(model): resolve provider-aware fallback model IDs
This commit is contained in:
parent
5d248bf6bf
commit
df9ebcb3d2
@ -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(),
|
||||
|
||||
@ -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(),
|
||||
|
||||
@ -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");
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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,
|
||||
|
||||
Loading…
Reference in New Issue
Block a user