Keep provider models visible with Bedrock

This commit is contained in:
2026-08-12 12:39:59 -05:00
parent b806cd76f8
commit c79634e76f
5 changed files with 119 additions and 24 deletions
+16 -11
View File
@@ -45,12 +45,10 @@ pub fn is_using_api_key_for_provider(_provider: &LLMProvider, _app: &AppContext)
false false
} }
pub fn should_show_bedrock_icon_for_model(llm: &LLMInfo, app: &AppContext) -> bool { pub fn should_show_bedrock_icon_for_model(llm: &LLMInfo, _app: &AppContext) -> bool {
UserWorkspaces::as_ref(app).is_bedrock_enabled(app) llm.host_configs
&& llm .get(&LLMModelHost::AwsBedrock)
.host_configs .is_some_and(|config| config.enabled)
.get(&LLMModelHost::AwsBedrock)
.is_some_and(|config| config.enabled)
} }
/// Key for cached LLM metadata in user preferences. /// Key for cached LLM metadata in user preferences.
@@ -826,12 +824,19 @@ impl LLMPreferences {
#[cfg(not(target_family = "wasm"))] #[cfg(not(target_family = "wasm"))]
fn inject_bedrock_models(&mut self, ctx: &AppContext) { fn inject_bedrock_models(&mut self, ctx: &AppContext) {
// Galaxy's runtime inventory is rebuilt exclusively from enabled local // Remove only previously injected Bedrock entries. Other enabled provider
// providers. Never retain Warp-hosted or stale cached model entries. // sections (OpenAI/LiteLLM, ChatGPT subscription, ACP) must remain
self.models_by_feature.agent_mode.choices.clear(); // available in the shared model picker.
self.models_by_feature.coding.choices.clear(); 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 { 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); let settings = AISettings::as_ref(ctx);
+87 -1
View File
@@ -12,7 +12,7 @@ use crate::server::server_api::ServerApiProvider;
use crate::server::sync_queue::SyncQueue; use crate::server::sync_queue::SyncQueue;
use crate::settings::{ use crate::settings::{
AcpAgentSettings, AcpConfigOptionSettings, AcpConfigValueSettings, AcpProviderConfig, AcpAgentSettings, AcpConfigOptionSettings, AcpConfigValueSettings, AcpProviderConfig,
OpenAIModelConfig, BedrockModelConfig, OpenAIModelConfig, OpenAIProviderConfig, OpenAIProviderKind,
}; };
use crate::test_util::settings::initialize_settings_for_tests; use crate::test_util::settings::initialize_settings_for_tests;
use crate::workspaces::team_tester::TeamTesterStatus; 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( fn acp_select_option(
id: &str, id: &str,
category: &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] #[test]
fn provider_discovery_preserves_local_model_overrides() { fn provider_discovery_preserves_local_model_overrides() {
let mut existing = openai_model("codex-gpt-5.6-sol-xhigh"); let mut existing = openai_model("codex-gpt-5.6-sol-xhigh");
+1 -1
View File
@@ -1440,7 +1440,7 @@ impl ProviderSetupView {
} }
children.push( children.push(
Text::new( 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(), appearance.ui_font_family(),
INPUT_FONT_SIZE, INPUT_FONT_SIZE,
) )
+4 -7
View File
@@ -158,9 +158,9 @@ impl AcpAgentPreset {
/// Resolves the best available executable for this preset. /// Resolves the best available executable for this preset.
/// ///
/// OpenCode's native binary is preferred when installed. Codex runs its ACP /// NPM-backed presets run through npx when available. Codex still requires a
/// adapter through npx, while CODEX_PATH points at the user's installed /// locally installed Codex CLI because the ACP adapter delegates to it via
/// Codex CLI rather than downloading a second Codex installation. /// CODEX_PATH rather than downloading a second Codex installation.
pub fn resolve_launch_config(self) -> Result<AcpLaunchConfig, String> { pub fn resolve_launch_config(self) -> Result<AcpLaunchConfig, String> {
self.resolve_launch_config_with(executable_on_path) self.resolve_launch_config_with(executable_on_path)
} }
@@ -197,9 +197,6 @@ impl AcpAgentPreset {
.codex_path(codex)) .codex_path(codex))
} }
Self::OpenCode => { Self::OpenCode => {
if let Some(command) = resolve("opencode") {
return Ok(AcpLaunchConfig::new(command).args(["acp"]));
}
if let Some(command) = resolve("npx") { if let Some(command) = resolve("npx") {
return Ok(AcpLaunchConfig::new(command).args([ return Ok(AcpLaunchConfig::new(command).args([
"--yes".to_owned(), "--yes".to_owned(),
@@ -208,7 +205,7 @@ impl AcpAgentPreset {
])); ]));
} }
Err( 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(), .to_owned(),
) )
} }
+11 -4
View File
@@ -48,7 +48,7 @@ fn opencode_preset_is_version_pinned() {
} }
#[test] #[test]
fn resolved_opencode_prefers_the_native_executable() { fn resolved_opencode_uses_npx_for_the_npm_adapter() {
let launch = AcpAgentPreset::OpenCode let launch = AcpAgentPreset::OpenCode
.resolve_launch_config_with(|command| match command { .resolve_launch_config_with(|command| match command {
"opencode" => Some(PathBuf::from("/opt/bin/opencode")), "opencode" => Some(PathBuf::from("/opt/bin/opencode")),
@@ -57,8 +57,15 @@ fn resolved_opencode_prefers_the_native_executable() {
}) })
.unwrap(); .unwrap();
assert_eq!(launch.command, PathBuf::from("/opt/bin/opencode")); assert_eq!(launch.command, PathBuf::from("/opt/bin/npx"));
assert_eq!(launch.args, vec!["acp"]); assert_eq!(
launch.args,
vec![
"--yes".to_owned(),
format!("opencode-ai@{OPENCODE_NPM_VERSION}"),
"acp".to_owned()
]
);
} }
#[test] #[test]
@@ -129,7 +136,7 @@ fn resolved_presets_explain_missing_launchers() {
(command == "bunx").then(|| PathBuf::from("/opt/bin/bunx")) (command == "bunx").then(|| PathBuf::from("/opt/bin/bunx"))
}) })
.unwrap_err(); .unwrap_err();
assert!(opencode_error.contains("requires the opencode executable or npx")); assert!(opencode_error.contains("requires npx"));
let codex_adapter_error = AcpAgentPreset::Codex let codex_adapter_error = AcpAgentPreset::Codex
.resolve_launch_config_with(|command| { .resolve_launch_config_with(|command| {