diff --git a/app/src/ai/llms.rs b/app/src/ai/llms.rs index a6915490..6b61a05f 100644 --- a/app/src/ai/llms.rs +++ b/app/src/ai/llms.rs @@ -45,12 +45,10 @@ pub fn is_using_api_key_for_provider(_provider: &LLMProvider, _app: &AppContext) false } -pub fn should_show_bedrock_icon_for_model(llm: &LLMInfo, app: &AppContext) -> bool { - UserWorkspaces::as_ref(app).is_bedrock_enabled(app) - && llm - .host_configs - .get(&LLMModelHost::AwsBedrock) - .is_some_and(|config| config.enabled) +pub fn should_show_bedrock_icon_for_model(llm: &LLMInfo, _app: &AppContext) -> bool { + llm.host_configs + .get(&LLMModelHost::AwsBedrock) + .is_some_and(|config| config.enabled) } /// Key for cached LLM metadata in user preferences. @@ -826,12 +824,19 @@ impl LLMPreferences { #[cfg(not(target_family = "wasm"))] fn inject_bedrock_models(&mut self, ctx: &AppContext) { - // Galaxy's runtime inventory is rebuilt exclusively from enabled local - // providers. Never retain Warp-hosted or stale cached model entries. - self.models_by_feature.agent_mode.choices.clear(); - self.models_by_feature.coding.choices.clear(); + // Remove only previously injected Bedrock entries. Other enabled provider + // sections (OpenAI/LiteLLM, ChatGPT subscription, ACP) must remain + // available in the shared model picker. + self.models_by_feature + .agent_mode + .choices + .retain(|m| m.provider != LLMProvider::Bedrock); + self.models_by_feature + .coding + .choices + .retain(|m| m.provider != LLMProvider::Bedrock); if let Some(ref mut cli) = self.models_by_feature.cli_agent { - cli.choices.clear(); + cli.choices.retain(|m| m.provider != LLMProvider::Bedrock); } let settings = AISettings::as_ref(ctx); diff --git a/app/src/ai/llms_tests.rs b/app/src/ai/llms_tests.rs index 9afbf049..5189fe33 100644 --- a/app/src/ai/llms_tests.rs +++ b/app/src/ai/llms_tests.rs @@ -12,7 +12,7 @@ use crate::server::server_api::ServerApiProvider; use crate::server::sync_queue::SyncQueue; use crate::settings::{ AcpAgentSettings, AcpConfigOptionSettings, AcpConfigValueSettings, AcpProviderConfig, - OpenAIModelConfig, + BedrockModelConfig, OpenAIModelConfig, OpenAIProviderConfig, OpenAIProviderKind, }; use crate::test_util::settings::initialize_settings_for_tests; use crate::workspaces::team_tester::TeamTesterStatus; @@ -159,6 +159,15 @@ fn openai_model(model_id: &str) -> OpenAIModelConfig { } } +fn bedrock_model(model_id: &str) -> BedrockModelConfig { + BedrockModelConfig { + model_id: model_id.to_string(), + display_name: model_id.to_string(), + vision_supported: false, + use_rig: true, + } +} + fn acp_select_option( id: &str, category: &str, @@ -216,6 +225,83 @@ fn empty_preferences() -> LLMPreferences { } } +#[test] +fn bedrock_and_openai_provider_models_coexist_in_picker() { + App::test((), |mut app| async move { + initialize_settings_for_tests(&mut app); + AISettings::handle(&app).update(&mut app, |settings, ctx| { + settings + .openai_enabled + .set_value(true, ctx) + .expect("OpenAI setting should update"); + settings + .openai_providers + .set_value( + vec![OpenAIProviderConfig { + kind: OpenAIProviderKind::OpenAI, + enabled: true, + name: "OpenAI Test".to_owned(), + base_url: "https://api.openai.test/v1".to_owned(), + api_key: Some("test-key".to_owned()), + project_id: None, + location: None, + models: vec![openai_model("gpt-test")], + }], + ctx, + ) + .expect("OpenAI providers should update"); + settings + .bedrock_enabled + .set_value(true, ctx) + .expect("Bedrock setting should update"); + settings + .bedrock_models + .set_value(vec![bedrock_model("anthropic.claude-test")], ctx) + .expect("Bedrock models should update"); + }); + + let mut preferences = empty_preferences(); + app.read(|ctx| { + preferences.inject_openai_models(ctx); + preferences.inject_bedrock_models(ctx); + }); + + let agent_models = &preferences.models_by_feature.agent_mode.choices; + assert!( + agent_models + .iter() + .any(|model| model.id.as_str() == "gpt-test" + && model.provider == LLMProvider::LiteLLM) + ); + assert!(agent_models.iter().any(|model| { + model.id.as_str() == "anthropic.claude-test" && model.provider == LLMProvider::Bedrock + })); + assert!(preferences + .openai_client_config_for_model("gpt-test") + .is_some()); + + let mut preferences = empty_preferences(); + app.read(|ctx| { + preferences.inject_bedrock_models(ctx); + preferences.inject_openai_models(ctx); + }); + + let agent_models = &preferences.models_by_feature.agent_mode.choices; + assert!( + agent_models + .iter() + .any(|model| model.id.as_str() == "gpt-test" + && model.provider == LLMProvider::LiteLLM) + ); + assert!(agent_models.iter().any(|model| { + model.id.as_str() == "anthropic.claude-test" && model.provider == LLMProvider::Bedrock + })); + assert!(preferences + .openai_client_config_for_model("gpt-test") + .is_some()); + }); +} + #[test] fn provider_discovery_preserves_local_model_overrides() { let mut existing = openai_model("codex-gpt-5.6-sol-xhigh"); diff --git a/app/src/settings_view/provider_setup_view.rs b/app/src/settings_view/provider_setup_view.rs index 8cfa7e1c..e8730fbc 100644 --- a/app/src/settings_view/provider_setup_view.rs +++ b/app/src/settings_view/provider_setup_view.rs @@ -1440,7 +1440,7 @@ impl ProviderSetupView { } children.push( Text::new( - "Known clients use their local executable. If the client is not installed, Galaxy will show a launch error. Choose Custom for another ACP-compatible command.", + "Known NPM-backed clients use npx with a pinned package version. If an ACP client needs another toolchain, choose Custom and configure the executable after installing it yourself.", appearance.ui_font_family(), INPUT_FONT_SIZE, ) diff --git a/crates/acp/src/config.rs b/crates/acp/src/config.rs index d225cd2b..15b43a94 100644 --- a/crates/acp/src/config.rs +++ b/crates/acp/src/config.rs @@ -158,9 +158,9 @@ impl AcpAgentPreset { /// Resolves the best available executable for this preset. /// - /// OpenCode's native binary is preferred when installed. Codex runs its ACP - /// adapter through npx, while CODEX_PATH points at the user's installed - /// Codex CLI rather than downloading a second Codex installation. + /// NPM-backed presets run through npx when available. Codex still requires a + /// locally installed Codex CLI because the ACP adapter delegates to it via + /// CODEX_PATH rather than downloading a second Codex installation. pub fn resolve_launch_config(self) -> Result { self.resolve_launch_config_with(executable_on_path) } @@ -197,9 +197,6 @@ impl AcpAgentPreset { .codex_path(codex)) } Self::OpenCode => { - if let Some(command) = resolve("opencode") { - return Ok(AcpLaunchConfig::new(command).args(["acp"])); - } if let Some(command) = resolve("npx") { return Ok(AcpLaunchConfig::new(command).args([ "--yes".to_owned(), @@ -208,7 +205,7 @@ impl AcpAgentPreset { ])); } Err( - "OpenCode ACP requires the opencode executable or npx; install OpenCode or Node.js/npm, or configure a custom ACP executable" + "OpenCode ACP requires npx because its ACP adapter is distributed as an NPM package; install Node.js/npm or configure a custom ACP executable" .to_owned(), ) } diff --git a/crates/acp/src/config_tests.rs b/crates/acp/src/config_tests.rs index 1dbe295c..efec74dc 100644 --- a/crates/acp/src/config_tests.rs +++ b/crates/acp/src/config_tests.rs @@ -48,7 +48,7 @@ fn opencode_preset_is_version_pinned() { } #[test] -fn resolved_opencode_prefers_the_native_executable() { +fn resolved_opencode_uses_npx_for_the_npm_adapter() { let launch = AcpAgentPreset::OpenCode .resolve_launch_config_with(|command| match command { "opencode" => Some(PathBuf::from("/opt/bin/opencode")), @@ -57,8 +57,15 @@ fn resolved_opencode_prefers_the_native_executable() { }) .unwrap(); - assert_eq!(launch.command, PathBuf::from("/opt/bin/opencode")); - assert_eq!(launch.args, vec!["acp"]); + assert_eq!(launch.command, PathBuf::from("/opt/bin/npx")); + assert_eq!( + launch.args, + vec![ + "--yes".to_owned(), + format!("opencode-ai@{OPENCODE_NPM_VERSION}"), + "acp".to_owned() + ] + ); } #[test] @@ -129,7 +136,7 @@ fn resolved_presets_explain_missing_launchers() { (command == "bunx").then(|| PathBuf::from("/opt/bin/bunx")) }) .unwrap_err(); - assert!(opencode_error.contains("requires the opencode executable or npx")); + assert!(opencode_error.contains("requires npx")); let codex_adapter_error = AcpAgentPreset::Codex .resolve_launch_config_with(|command| {