From 90f2461f75f7a022b1af301f7811c66243636f5a Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Tue, 7 Apr 2026 14:51:12 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20b5-tool-timeout=20=E2=80=94=20batch=205?= =?UTF-8?q?=20upstream=20parity?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- rust/crates/runtime/src/config.rs | 62 ++++++++++++++++++ rust/crates/rusty-claude-cli/src/main.rs | 83 ++++++++++++++++++++---- 2 files changed, 133 insertions(+), 12 deletions(-) diff --git a/rust/crates/runtime/src/config.rs b/rust/crates/runtime/src/config.rs index 1159b54..798afc3 100644 --- a/rust/crates/runtime/src/config.rs +++ b/rust/crates/runtime/src/config.rs @@ -58,6 +58,7 @@ pub struct RuntimeFeatureConfig { mcp: McpConfigCollection, oauth: Option, model: Option, + aliases: BTreeMap, permission_mode: Option, permission_rules: RuntimePermissionRuleConfig, sandbox: SandboxConfig, @@ -290,6 +291,7 @@ impl ConfigLoader { }, oauth: parse_optional_oauth_config(&merged_value, "merged settings.oauth")?, model: parse_optional_model(&merged_value), + aliases: parse_optional_aliases(&merged_value)?, permission_mode: parse_optional_permission_mode(&merged_value)?, permission_rules: parse_optional_permission_rules(&merged_value)?, sandbox: parse_optional_sandbox_config(&merged_value)?, @@ -364,6 +366,11 @@ impl RuntimeConfig { self.feature_config.model.as_deref() } + #[must_use] + pub fn aliases(&self) -> &BTreeMap { + &self.feature_config.aliases + } + #[must_use] pub fn permission_mode(&self) -> Option { self.feature_config.permission_mode @@ -423,6 +430,11 @@ impl RuntimeFeatureConfig { self.model.as_deref() } + #[must_use] + pub fn aliases(&self) -> &BTreeMap { + &self.aliases + } + #[must_use] pub fn permission_mode(&self) -> Option { self.permission_mode @@ -680,6 +692,13 @@ fn parse_optional_model(root: &JsonValue) -> Option { .map(ToOwned::to_owned) } +fn parse_optional_aliases(root: &JsonValue) -> Result, ConfigError> { + let Some(object) = root.as_object() else { + return Ok(BTreeMap::new()); + }; + Ok(optional_string_map(object, "aliases", "merged settings")?.unwrap_or_default()) +} + fn parse_optional_hooks_config(root: &JsonValue) -> Result { let Some(object) = root.as_object() else { return Ok(RuntimeHookConfig::default()); @@ -1613,6 +1632,49 @@ mod tests { fs::remove_dir_all(root).expect("cleanup temp dir"); } + #[test] + fn parses_user_defined_model_aliases_from_settings() { + // given + let root = temp_dir(); + let cwd = root.join("project"); + let home = root.join("home").join(".claw"); + fs::create_dir_all(cwd.join(".claw")).expect("project config dir"); + fs::create_dir_all(&home).expect("home config dir"); + + fs::write( + home.join("settings.json"), + r#"{"aliases":{"fast":"claude-haiku-4-5-20251213","smart":"claude-opus-4-6"}}"#, + ) + .expect("write user settings"); + fs::write( + cwd.join(".claw").join("settings.local.json"), + r#"{"aliases":{"smart":"claude-sonnet-4-6","cheap":"grok-3-mini"}}"#, + ) + .expect("write local settings"); + + // when + let loaded = ConfigLoader::new(&cwd, &home) + .load() + .expect("config should load"); + + // then + let aliases = loaded.aliases(); + assert_eq!( + aliases.get("fast").map(String::as_str), + Some("claude-haiku-4-5-20251213") + ); + assert_eq!( + aliases.get("smart").map(String::as_str), + Some("claude-sonnet-4-6") + ); + assert_eq!( + aliases.get("cheap").map(String::as_str), + Some("grok-3-mini") + ); + + fs::remove_dir_all(root).expect("cleanup temp dir"); + } + #[test] fn empty_settings_file_loads_defaults() { // given diff --git a/rust/crates/rusty-claude-cli/src/main.rs b/rust/crates/rusty-claude-cli/src/main.rs index ae6b763..2593679 100644 --- a/rust/crates/rusty-claude-cli/src/main.rs +++ b/rust/crates/rusty-claude-cli/src/main.rs @@ -356,11 +356,11 @@ fn parse_args(args: &[String]) -> Result { let value = args .get(index + 1) .ok_or_else(|| "missing value for --model".to_string())?; - model = resolve_model_alias(value).to_string(); + model = resolve_model_alias_with_config(value); index += 2; } flag if flag.starts_with("--model=") => { - model = resolve_model_alias(&flag[8..]).to_string(); + model = resolve_model_alias_with_config(&flag[8..]); index += 1; } "--output-format" => { @@ -401,7 +401,7 @@ fn parse_args(args: &[String]) -> Result { } return Ok(CliAction::Prompt { prompt, - model: resolve_model_alias(&model).to_string(), + model: resolve_model_alias_with_config(&model), output_format, allowed_tools: normalize_allowed_tools(&allowed_tool_values)?, permission_mode: permission_mode_override @@ -813,6 +813,27 @@ fn resolve_model_alias(model: &str) -> &str { } } +/// Resolve a model name through user-defined config aliases first, then fall +/// back to the built-in alias table. This is the entry point used wherever a +/// user-supplied model string is about to be dispatched to a provider. +fn resolve_model_alias_with_config(model: &str) -> String { + let trimmed = model.trim(); + if let Some(resolved) = config_alias_for_current_dir(trimmed) { + return resolve_model_alias(&resolved).to_string(); + } + resolve_model_alias(trimmed).to_string() +} + +fn config_alias_for_current_dir(alias: &str) -> Option { + if alias.is_empty() { + return None; + } + let cwd = env::current_dir().ok()?; + let loader = ConfigLoader::default_for(&cwd); + let config = loader.load().ok()?; + config.aliases().get(alias).cloned() +} + fn normalize_allowed_tools(values: &[String]) -> Result, String> { if values.is_empty() { return Ok(None); @@ -903,10 +924,10 @@ fn resolve_repl_model(cli_model: String) -> String { .map(|value| value.trim().to_string()) .filter(|value| !value.is_empty()) { - return resolve_model_alias(&env_model).to_string(); + return resolve_model_alias_with_config(&env_model); } if let Some(config_model) = config_model_for_current_dir() { - return resolve_model_alias(&config_model).to_string(); + return resolve_model_alias_with_config(&config_model); } cli_model } @@ -3667,7 +3688,7 @@ impl LiveCli { return Ok(false); }; - let model = resolve_model_alias(&model).to_string(); + let model = resolve_model_alias_with_config(&model); if model == self.model { println!( @@ -7463,12 +7484,11 @@ mod tests { parse_git_status_metadata_for, parse_git_workspace_summary, permission_policy, print_help_to, push_output_block, render_config_report, render_diff_report, render_diff_report_for, render_memory_report, render_repl_help, render_resume_usage, - render_session_markdown, resolve_model_alias, resolve_repl_model, - resolve_session_reference, response_to_events, resume_supported_slash_commands, - run_resume_command, short_tool_id, - slash_command_completion_candidates_with_sessions, status_context, - summarize_tool_payload_for_markdown, validate_no_args, write_mcp_server_fixture, - CliAction, CliOutputFormat, CliToolExecutor, GitWorkspaceSummary, + resolve_model_alias, resolve_model_alias_with_config, resolve_repl_model, + resolve_session_reference, response_to_events, + resume_supported_slash_commands, run_resume_command, + slash_command_completion_candidates_with_sessions, status_context, validate_no_args, + write_mcp_server_fixture, CliAction, CliOutputFormat, CliToolExecutor, GitWorkspaceSummary, InternalPromptProgressEvent, InternalPromptProgressState, LiveCli, LocalHelpTopic, SlashCommand, StatusUsage, DEFAULT_MODEL, LATEST_SESSION_REFERENCE, }; @@ -8123,6 +8143,45 @@ mod tests { assert_eq!(resolve_model_alias("claude-opus"), "claude-opus"); } + #[test] + fn user_defined_aliases_resolve_before_provider_dispatch() { + // given + let _guard = env_lock(); + let root = temp_dir(); + let cwd = root.join("project"); + let config_home = root.join("config-home"); + std::fs::create_dir_all(cwd.join(".claw")).expect("project config dir should exist"); + std::fs::create_dir_all(&config_home).expect("config home should exist"); + std::fs::write( + cwd.join(".claw").join("settings.json"), + r#"{"aliases":{"fast":"claude-haiku-4-5-20251213","smart":"opus","cheap":"grok-3-mini"}}"#, + ) + .expect("project config should write"); + + let original_config_home = std::env::var("CLAW_CONFIG_HOME").ok(); + std::env::set_var("CLAW_CONFIG_HOME", &config_home); + + // when + let direct = with_current_dir(&cwd, || resolve_model_alias_with_config("fast")); + let chained = with_current_dir(&cwd, || resolve_model_alias_with_config("smart")); + let cross_provider = with_current_dir(&cwd, || resolve_model_alias_with_config("cheap")); + let unknown = with_current_dir(&cwd, || resolve_model_alias_with_config("unknown-model")); + let builtin = with_current_dir(&cwd, || resolve_model_alias_with_config("haiku")); + + match original_config_home { + Some(value) => std::env::set_var("CLAW_CONFIG_HOME", value), + None => std::env::remove_var("CLAW_CONFIG_HOME"), + } + std::fs::remove_dir_all(root).expect("temp config root should clean up"); + + // then + assert_eq!(direct, "claude-haiku-4-5-20251213"); + assert_eq!(chained, "claude-opus-4-6"); + assert_eq!(cross_provider, "grok-3-mini"); + assert_eq!(unknown, "unknown-model"); + assert_eq!(builtin, "claude-haiku-4-5-20251213"); + } + #[test] fn parses_version_flags_without_initializing_prompt_mode() { assert_eq!(