mirror of
https://github.com/instructkr/claw-code.git
synced 2026-04-08 00:54:49 +08:00
feat: b5-tool-timeout — batch 5 upstream parity
This commit is contained in:
@@ -58,6 +58,7 @@ pub struct RuntimeFeatureConfig {
|
||||
mcp: McpConfigCollection,
|
||||
oauth: Option<OAuthConfig>,
|
||||
model: Option<String>,
|
||||
aliases: BTreeMap<String, String>,
|
||||
permission_mode: Option<ResolvedPermissionMode>,
|
||||
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<String, String> {
|
||||
&self.feature_config.aliases
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn permission_mode(&self) -> Option<ResolvedPermissionMode> {
|
||||
self.feature_config.permission_mode
|
||||
@@ -423,6 +430,11 @@ impl RuntimeFeatureConfig {
|
||||
self.model.as_deref()
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn aliases(&self) -> &BTreeMap<String, String> {
|
||||
&self.aliases
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn permission_mode(&self) -> Option<ResolvedPermissionMode> {
|
||||
self.permission_mode
|
||||
@@ -680,6 +692,13 @@ fn parse_optional_model(root: &JsonValue) -> Option<String> {
|
||||
.map(ToOwned::to_owned)
|
||||
}
|
||||
|
||||
fn parse_optional_aliases(root: &JsonValue) -> Result<BTreeMap<String, String>, 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<RuntimeHookConfig, ConfigError> {
|
||||
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
|
||||
|
||||
@@ -356,11 +356,11 @@ fn parse_args(args: &[String]) -> Result<CliAction, String> {
|
||||
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<CliAction, String> {
|
||||
}
|
||||
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<String> {
|
||||
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<Option<AllowedToolSet>, 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!(
|
||||
|
||||
Reference in New Issue
Block a user