diff --git a/desktop/src-tauri/src/managed_agents/discovery.rs b/desktop/src-tauri/src/managed_agents/discovery.rs index 50206567ad..cb409786a4 100644 --- a/desktop/src-tauri/src/managed_agents/discovery.rs +++ b/desktop/src-tauri/src/managed_agents/discovery.rs @@ -111,7 +111,11 @@ const KNOWN_ACP_RUNTIMES: &[KnownAcpRuntime] = &[ adapter_install_hint: "Install the Claude Code ACP adapter via npm.", skill_dir: Some(".claude/skills"), supports_acp_model_switching: false, - model_env_var: None, + // The Claude CLI honors this env var as a session model override, + // and the adapter's child `claude` process inherits the spawn env — + // without this, the model picked in the UI is persisted but never + // applied (#2692). + model_env_var: Some(crate::managed_agents::ANTHROPIC_MODEL_ENV_KEY), provider_env_var: None, provider_locked: true, default_env: &[], diff --git a/desktop/src-tauri/src/managed_agents/discovery/tests.rs b/desktop/src-tauri/src/managed_agents/discovery/tests.rs index 0ed4fe0f6a..e8a5fa732b 100644 --- a/desktop/src-tauri/src/managed_agents/discovery/tests.rs +++ b/desktop/src-tauri/src/managed_agents/discovery/tests.rs @@ -1270,3 +1270,25 @@ fn test_install_shell_from_some_returns_path() { "install_shell_from(Some) must return the path as Ok" ); } + +/// The claude runtime must declare a model env channel — without one, the +/// model picked in the UI is persisted but never applied at spawn (#2692). +/// ANTHROPIC_MODEL is the override the Claude CLI honors, and the same key +/// readiness uses for the anthropic provider. +#[test] +fn claude_runtime_declares_anthropic_model_env_var() { + let claude = super::known_acp_runtime_exact("claude").expect("claude runtime registered"); + assert_eq!(claude.model_env_var, Some("ANTHROPIC_MODEL")); + // Provider stays locked: only the model is injectable for this harness. + assert!(claude.provider_locked); + assert_eq!(claude.provider_env_var, None); +} + +/// A stale persisted ANTHROPIC_MODEL must not shadow the structured model +/// field after a UI edit — same treatment as GOOSE_MODEL / BUZZ_AGENT_MODEL. +#[test] +fn anthropic_model_is_a_derived_env_key() { + use crate::managed_agents::is_derived_provider_model_key; + assert!(is_derived_provider_model_key("ANTHROPIC_MODEL")); + assert!(is_derived_provider_model_key("anthropic_model")); +} diff --git a/desktop/src-tauri/src/managed_agents/env_vars.rs b/desktop/src-tauri/src/managed_agents/env_vars.rs index 6318a5c838..3e41eddc6a 100644 --- a/desktop/src-tauri/src/managed_agents/env_vars.rs +++ b/desktop/src-tauri/src/managed_agents/env_vars.rs @@ -24,11 +24,18 @@ use std::collections::BTreeMap; /// /// Non-structured knobs (`GOOSE_TEMPERATURE`, `GOOSE_CONTEXT_LIMIT`) are NOT /// in this list — they have no structured counterpart and must be preserved. +/// Env var the Claude CLI honors as a session model override. Single source +/// of truth for the key — referenced by the claude runtime entry +/// (`discovery.rs`), the derived-key list below, and the readiness +/// provider-model mapping, so the three cannot drift apart. +pub(crate) const ANTHROPIC_MODEL_ENV_KEY: &str = "ANTHROPIC_MODEL"; + pub(crate) const DERIVED_PROVIDER_MODEL_ENV_KEYS: &[&str] = &[ "GOOSE_MODEL", "GOOSE_PROVIDER", "BUZZ_AGENT_MODEL", "BUZZ_AGENT_PROVIDER", + ANTHROPIC_MODEL_ENV_KEY, ]; /// Returns `true` if `key` is a derived provider/model env key that should be diff --git a/desktop/src-tauri/src/managed_agents/readiness.rs b/desktop/src-tauri/src/managed_agents/readiness.rs index 87ee6241ee..3fe689e422 100644 --- a/desktop/src-tauri/src/managed_agents/readiness.rs +++ b/desktop/src-tauri/src/managed_agents/readiness.rs @@ -327,7 +327,7 @@ fn buzz_agent_requirements(effective: &EffectiveAgentEnv) -> Vec { Some("databricks") | Some("databricks_v2") | Some("databricks-v2") => { Some("DATABRICKS_MODEL") } - Some("anthropic") => Some("ANTHROPIC_MODEL"), + Some("anthropic") => Some(super::ANTHROPIC_MODEL_ENV_KEY), Some("openai") | Some("openai-compat") => Some("OPENAI_COMPAT_MODEL"), _ => None, }; diff --git a/desktop/src-tauri/src/managed_agents/runtime/tests.rs b/desktop/src-tauri/src/managed_agents/runtime/tests.rs index d86b69938f..979ef9f345 100644 --- a/desktop/src-tauri/src/managed_agents/runtime/tests.rs +++ b/desktop/src-tauri/src/managed_agents/runtime/tests.rs @@ -517,13 +517,21 @@ fn runtime_metadata_env_vars_injects_model_and_provider() { #[test] fn runtime_metadata_env_vars_skips_provider_when_locked() { + // The claude runtime shape: model injectable via ANTHROPIC_MODEL (#2692), + // provider locked to Anthropic so no provider env is written. let vars = runtime_metadata_env_vars( - None, // claude has no model_env_var + Some("ANTHROPIC_MODEL"), None, // claude has no provider_env_var true, // provider_locked = true - Some("claude-opus-4-7"), + Some("claude-sonnet-5"), Some("anthropic"), ); + assert_eq!(vars, vec![("ANTHROPIC_MODEL", "claude-sonnet-5")]); +} + +#[test] +fn runtime_metadata_env_vars_empty_without_env_channels() { + let vars = runtime_metadata_env_vars(None, None, true, Some("some-model"), Some("anthropic")); assert!(vars.is_empty()); }