From b3be0935f72176d664febc2a8f1e821541ade104 Mon Sep 17 00:00:00 2001 From: Michael Neale Date: Sun, 31 May 2026 14:46:00 +1000 Subject: [PATCH] fix(model): recognize version-first Claude 4+ ids in max_output_tokens floor The Claude 4+ fallback floor in max_output_tokens() only matched family-first ids (claude-opus-4-8). Version-first ids used by Snowflake/Databricks (claude-4-sonnet, claude-4-opus, claude-4-5-sonnet, goose-claude-4-sonnet-bedrock) did not match, so a version-first Claude 4 model not yet in the canonical table would still fall back to 4096 and truncate tool calls. Extend the regex to match the major version in either ordering. Older 3.x ids in either ordering keep the conservative 4096 default. Added version-first cases to both the positive and negative tests. Signed-off-by: Michael Neale --- crates/goose/src/model.rs | 29 ++++++++++++++++++++++------- 1 file changed, 22 insertions(+), 7 deletions(-) diff --git a/crates/goose/src/model.rs b/crates/goose/src/model.rs index 0b74da835d..1b61c86d21 100644 --- a/crates/goose/src/model.rs +++ b/crates/goose/src/model.rs @@ -451,17 +451,23 @@ impl ModelConfig { if !lower.contains("claude") { return false; } - // Match `claude--` where major >= 4. Covers ids like - // `claude-opus-4-8`, `claude-sonnet-4-5`, `anthropic/claude-opus-4.6`, - // `databricks-claude-opus-4.5`, etc. Older Claude 3.x / 2.x ids keep - // the conservative 4096 default since their real output caps are - // genuinely small (3.x sonnet = 8192, 3.x opus = 4096). + // Match a major version >= 4 in either Claude id ordering: + // family-first — `claude-opus-4-8`, `claude-sonnet-4-5`, + // `anthropic/claude-opus-4.6`, `databricks-claude-opus-4.5` + // version-first — `claude-4-sonnet`, `claude-4-opus`, + // `claude-4-5-sonnet` (Snowflake/Databricks naming) + // Older Claude 3.x / 2.x ids keep the conservative 4096 default since + // their real output caps are genuinely small (3.x sonnet = 8192, + // 3.x opus = 4096). static RE: std::sync::OnceLock = std::sync::OnceLock::new(); let re = RE.get_or_init(|| { - regex::Regex::new(r"claude[-/](?:opus|sonnet|haiku)[-.]?(\d+)").unwrap() + regex::Regex::new( + r"claude[-/](?:(?:opus|sonnet|haiku)[-.]?(\d+)|(\d+)(?:[-.]\d+)*[-.](?:opus|sonnet|haiku))", + ) + .unwrap() }); re.captures(&lower) - .and_then(|c| c.get(1)) + .and_then(|c| c.get(1).or_else(|| c.get(2))) .and_then(|m| m.as_str().parse::().ok()) .map(|major| major >= 4) .unwrap_or(false) @@ -618,6 +624,12 @@ mod tests { "claude-haiku-4-2", "anthropic/claude-opus-4.8", "databricks-claude-opus-4.6", + // version-first ordering (Snowflake/Databricks) + "claude-4-sonnet", + "claude-4-opus", + "claude-4-5-sonnet", + "databricks-claude-4-sonnet", + "goose-claude-4-sonnet-bedrock", ] { let cfg = ModelConfig { model_name: model.to_string(), @@ -645,6 +657,9 @@ mod tests { for model in [ "claude-3-opus-20240229", "claude-3-5-sonnet-20241022", + // version-first 3.x must stay on the conservative default + "claude-3-sonnet", + "claude-3-5-sonnet", "gpt-4o", "some-unknown-model", ] {