diff --git a/src/agent/loop_.rs b/src/agent/loop_.rs index 950095c9c..bccd689c2 100644 --- a/src/agent/loop_.rs +++ b/src/agent/loop_.rs @@ -627,11 +627,18 @@ fn stop_reason_name(reason: &NormalizedStopReason) -> &'static str { } } +fn is_legacy_cron_model_fallback(model: &str) -> bool { + let normalized = model.trim().to_ascii_lowercase(); + matches!(normalized.as_str(), "gpt-4o-mini" | "openai/gpt-4o-mini") +} + fn maybe_inject_cron_add_delivery( tool_name: &str, tool_args: &mut serde_json::Value, channel_name: &str, reply_target: Option<&str>, + provider_name: &str, + active_model: &str, ) { if tool_name != "cron_add" || !AUTO_CRON_DELIVERY_CHANNELS @@ -700,6 +707,44 @@ fn maybe_inject_cron_add_delivery( serde_json::Value::String(reply_target.to_string()), ); } + + let active_model = active_model.trim(); + if active_model.is_empty() { + return; + } + + let model_missing = args_obj + .get("model") + .and_then(serde_json::Value::as_str) + .is_none_or(|value| value.trim().is_empty()); + if model_missing { + args_obj.insert( + "model".to_string(), + serde_json::Value::String(active_model.to_string()), + ); + return; + } + + let is_custom_provider = provider_name + .trim() + .to_ascii_lowercase() + .starts_with("custom:"); + if !is_custom_provider { + return; + } + + let should_replace_model = args_obj + .get("model") + .and_then(serde_json::Value::as_str) + .is_some_and(|value| { + is_legacy_cron_model_fallback(value) && !value.trim().eq_ignore_ascii_case(active_model) + }); + if should_replace_model { + args_obj.insert( + "model".to_string(), + serde_json::Value::String(active_model.to_string()), + ); + } } async fn await_non_cli_approval_decision( @@ -1980,6 +2025,8 @@ pub async fn run_tool_call_loop( &mut tool_args, channel_name, channel_reply_target.as_deref(), + provider_name, + active_model.as_str(), ); if excluded_tools.iter().any(|ex| ex == &tool_name) { @@ -3438,11 +3485,19 @@ mod tests { "prompt": "remind me later" }); - maybe_inject_cron_add_delivery("cron_add", &mut args, "telegram", Some("-10012345")); + maybe_inject_cron_add_delivery( + "cron_add", + &mut args, + "telegram", + Some("-10012345"), + "custom:https://llm.example.com/v1", + "gpt-oss:20b", + ); assert_eq!(args["delivery"]["mode"], "announce"); assert_eq!(args["delivery"]["channel"], "telegram"); assert_eq!(args["delivery"]["to"], "-10012345"); + assert_eq!(args["model"], "gpt-oss:20b"); } #[test] @@ -3457,7 +3512,14 @@ mod tests { } }); - maybe_inject_cron_add_delivery("cron_add", &mut args, "telegram", Some("-10012345")); + maybe_inject_cron_add_delivery( + "cron_add", + &mut args, + "telegram", + Some("-10012345"), + "openrouter", + "anthropic/claude-sonnet-4.6", + ); assert_eq!(args["delivery"]["channel"], "discord"); assert_eq!(args["delivery"]["to"], "C123"); @@ -3470,7 +3532,14 @@ mod tests { "command": "echo hello" }); - maybe_inject_cron_add_delivery("cron_add", &mut args, "telegram", Some("-10012345")); + maybe_inject_cron_add_delivery( + "cron_add", + &mut args, + "telegram", + Some("-10012345"), + "openrouter", + "anthropic/claude-sonnet-4.6", + ); assert!(args.get("delivery").is_none()); } @@ -3481,7 +3550,14 @@ mod tests { "job_type": "agent", "prompt": "daily summary" }); - maybe_inject_cron_add_delivery("cron_add", &mut lark_args, "lark", Some("oc_xxx")); + maybe_inject_cron_add_delivery( + "cron_add", + &mut lark_args, + "lark", + Some("oc_xxx"), + "openrouter", + "anthropic/claude-sonnet-4.6", + ); assert_eq!(lark_args["delivery"]["channel"], "lark"); assert_eq!(lark_args["delivery"]["to"], "oc_xxx"); @@ -3489,11 +3565,58 @@ mod tests { "job_type": "agent", "prompt": "daily summary" }); - maybe_inject_cron_add_delivery("cron_add", &mut feishu_args, "feishu", Some("oc_yyy")); + maybe_inject_cron_add_delivery( + "cron_add", + &mut feishu_args, + "feishu", + Some("oc_yyy"), + "openrouter", + "anthropic/claude-sonnet-4.6", + ); assert_eq!(feishu_args["delivery"]["channel"], "feishu"); assert_eq!(feishu_args["delivery"]["to"], "oc_yyy"); } + #[test] + fn maybe_inject_cron_add_delivery_replaces_legacy_model_on_custom_provider() { + let mut args = serde_json::json!({ + "job_type": "agent", + "prompt": "remind me later", + "model": "gpt-4o-mini" + }); + + maybe_inject_cron_add_delivery( + "cron_add", + &mut args, + "discord", + Some("C123"), + "custom:https://somecoolai.endpoint.lan/api/v1", + "gpt-oss:20b", + ); + + assert_eq!(args["model"], "gpt-oss:20b"); + } + + #[test] + fn maybe_inject_cron_add_delivery_keeps_explicit_model_for_non_custom_provider() { + let mut args = serde_json::json!({ + "job_type": "agent", + "prompt": "remind me later", + "model": "gpt-4o-mini" + }); + + maybe_inject_cron_add_delivery( + "cron_add", + &mut args, + "discord", + Some("C123"), + "openrouter", + "anthropic/claude-sonnet-4.6", + ); + + assert_eq!(args["model"], "gpt-4o-mini"); + } + #[test] fn safety_heartbeat_interval_zero_disables_injection() { for counter in [0, 1, 2, 10, 100] { diff --git a/src/tools/cron_add.rs b/src/tools/cron_add.rs index 091661f8c..f77fc02a1 100644 --- a/src/tools/cron_add.rs +++ b/src/tools/cron_add.rs @@ -78,7 +78,10 @@ impl Tool for CronAddTool { "command": { "type": "string" }, "prompt": { "type": "string" }, "session_target": { "type": "string", "enum": ["isolated", "main"] }, - "model": { "type": "string" }, + "model": { + "type": "string", + "description": "Optional model override for this job. Omit unless the user explicitly requests a different model; defaults to the active model/context." + }, "recurring_confirmed": { "type": "boolean", "description": "Required for agent recurring schedules (schedule.kind='cron' or 'every'). Set true only when recurring behavior is intentional.",