diff --git a/crates/ai/src/cli_agents/adapters.rs b/crates/ai/src/cli_agents/adapters.rs index 1f0e5c1e..8875bd3d 100644 --- a/crates/ai/src/cli_agents/adapters.rs +++ b/crates/ai/src/cli_agents/adapters.rs @@ -254,7 +254,8 @@ impl AgentAdapter for ClaudeCode { } if v.get("is_error").and_then(Value::as_bool).unwrap_or(false) { let why = if s("result").is_empty() { s("subtype") } else { s("result") }; - out.push(AgentEvent::Error { message: format!("Claude Code stopped: {why}"), remediation: sign_in_hint(&why), log: None }); + let (message, remediation) = claude_code_failure(&why); + out.push(AgentEvent::Error { message, remediation, log: None }); } } _ => {} diff --git a/crates/ai/src/cli_agents/mod.rs b/crates/ai/src/cli_agents/mod.rs index 456d7026..1f5caec7 100644 --- a/crates/ai/src/cli_agents/mod.rs +++ b/crates/ai/src/cli_agents/mod.rs @@ -155,6 +155,25 @@ pub fn sign_in_hint(message: &str) -> Option { .then(|| "Sign in to Claude Code: open a terminal and run `claude auth login` (or start `claude` and type /login), then try again.".to_string()) } +/// Words in an API's answer when the installed Claude Code is older than the model it was asked +/// to use (#165). +pub const TOO_OLD_FOR_MODEL: &str = "does not support this model"; + +/// Claude Code's failure in plain words, and its fix when the person can make one: a version too +/// old for the model (#165), or a sign-in. +pub fn claude_code_failure(why: &str) -> (String, Option) { + if why.to_ascii_lowercase().contains(TOO_OLD_FOR_MODEL) { + return ( + format!("Claude Code is too old for the model it was asked to use ({}).", why.chars().take(200).collect::()), + Some( + "Update the `claude` command-line tool, which is installed and updated apart from the Claude desktop app: open a terminal and run `claude update`, then try again. Or choose another model for Claude Code in Settings → AI." + .into(), + ), + ); + } + (format!("Claude Code stopped: {why}"), sign_in_hint(why)) +} + /// Variables a Claude Code session sets for its own child processes. When Craft Anything itself /// runs inside such a session, they'd make the person's `claude` act as part of that session /// instead of as their own install, so they're removed. The person's own settings (provider @@ -825,6 +844,14 @@ mod tests { other => panic!("{other:?}"), } assert!(sign_in_hint("rate limited").is_none()); + // #165: an install too old for the model says so, and how to update it. + let raw = + r#"API Error: 400 {"type":"error","error":{"type":"invalid_request_error","message":"Claude Code 2.1.81 does not support this model; version 2"}}"#; + let (message, fix) = claude_code_failure(raw); + assert!(message.starts_with("Claude Code is too old for the model it was asked to use") && message.contains("2.1.81"), "{message}"); + let fix = fix.unwrap_or_default(); + assert!(fix.contains("claude update") && fix.contains("desktop app") && fix.contains("Settings → AI"), "{fix}"); + assert_eq!(claude_code_failure("rate limited"), ("Claude Code stopped: rate limited".to_string(), None)); } #[test] diff --git a/crates/ai/src/rebuild/mod.rs b/crates/ai/src/rebuild/mod.rs index e41a9ada..09154da4 100644 --- a/crates/ai/src/rebuild/mod.rs +++ b/crates/ai/src/rebuild/mod.rs @@ -921,10 +921,12 @@ pub fn builder_backend(cfg: &crate::AiConfig, named: Option<&str>) -> Result bool { let m = message.to_ascii_lowercase(); - !["sign in", "log in", "login", "logged in", "auth", "credit", "billing", "usage limit", "not installed", "install "].iter().any(|k| m.contains(k)) + !["sign in", "log in", "login", "logged in", "auth", "credit", "billing", "usage limit", "not installed", "install ", crate::cli_agents::TOO_OLD_FOR_MODEL] + .iter() + .any(|k| m.contains(k)) } /// The longest wait a service's "retry in …" is followed for. diff --git a/crates/ai/src/rebuild/tests.rs b/crates/ai/src/rebuild/tests.rs index 69698f3a..ec9109d7 100644 --- a/crates/ai/src/rebuild/tests.rs +++ b/crates/ai/src/rebuild/tests.rs @@ -325,9 +325,13 @@ fn a_cut_off_turn_is_resumed_unless_the_person_must_act() { for resumable in ["Claude Code took longer than 86400 s and was stopped.", "API Error: 529 overloaded", "connection reset by peer"] { assert!(worth_retrying(resumable), "{resumable}"); } - for needs_you in - ["Not logged in. Please run /login", "Your credit balance is too low", "Claude Code is not installed", "Invalid API key · Please run /login"] - { + for needs_you in [ + "Not logged in. Please run /login", + "Your credit balance is too low", + "Claude Code is not installed", + "Invalid API key · Please run /login", + "Claude Code 2.1.81 does not support this model", + ] { assert!(!worth_retrying(needs_you), "{needs_you}"); } assert_eq!(RunOptions::default().max_rounds, 6, "room to keep fixing until it works");