Fix provider model inventories and Bedrock verification

This commit is contained in:
2026-10-02 16:41:16 -05:00
parent 2796fdd114
commit 188489c890
13 changed files with 1016 additions and 344 deletions
+185
View File
@@ -161,6 +161,8 @@ fn openai_model(model_id: &str) -> OpenAIModelConfig {
fn bedrock_model(model_id: &str) -> BedrockModelConfig {
BedrockModelConfig {
enabled: true,
access_verified: true,
model_id: model_id.to_string(),
display_name: model_id.to_string(),
vision_supported: false,
@@ -232,6 +234,9 @@ fn empty_preferences() -> LLMPreferences {
litellm_connections: Vec::new(),
catalog_refresh_generations: HashMap::new(),
chatgpt_subscription_models_refresh_in_flight: false,
bedrock_models_refresh_in_flight: false,
bedrock_verified_config: None,
bedrock_verified_model_ids: HashSet::new(),
acp_selections: HashMap::new(),
}
}
@@ -304,6 +309,14 @@ fn bedrock_and_openai_provider_models_coexist_in_picker() {
let mut preferences = empty_preferences();
app.read(|ctx| {
preferences.inject_openai_models(ctx);
preferences.bedrock_verified_config =
Some(super::bedrock_discovery_config(AISettings::as_ref(ctx)));
preferences.bedrock_verified_model_ids = AISettings::as_ref(ctx)
.bedrock_models
.value()
.iter()
.map(|model| model.model_id.clone())
.collect();
preferences.inject_bedrock_models(ctx);
});
@@ -323,6 +336,14 @@ fn bedrock_and_openai_provider_models_coexist_in_picker() {
let mut preferences = empty_preferences();
app.read(|ctx| {
preferences.bedrock_verified_config =
Some(super::bedrock_discovery_config(AISettings::as_ref(ctx)));
preferences.bedrock_verified_model_ids = AISettings::as_ref(ctx)
.bedrock_models
.value()
.iter()
.map(|model| model.model_id.clone())
.collect();
preferences.inject_bedrock_models(ctx);
preferences.inject_openai_models(ctx);
});
@@ -429,6 +450,9 @@ fn acp_models_are_injected_only_while_acp_is_enabled() {
litellm_connections: Vec::new(),
catalog_refresh_generations: HashMap::new(),
chatgpt_subscription_models_refresh_in_flight: false,
bedrock_models_refresh_in_flight: false,
bedrock_verified_config: None,
bedrock_verified_model_ids: HashSet::new(),
acp_selections: HashMap::new(),
};
app.read(|ctx| preferences.inject_acp_models(ctx));
@@ -489,6 +513,14 @@ fn acp_is_selectable_before_protocol_model_discovery_completes() {
let mut preferences = empty_preferences();
app.read(|ctx| {
preferences.bedrock_verified_config =
Some(super::bedrock_discovery_config(AISettings::as_ref(ctx)));
preferences.bedrock_verified_model_ids = AISettings::as_ref(ctx)
.bedrock_models
.value()
.iter()
.map(|model| model.model_id.clone())
.collect();
preferences.inject_bedrock_models(ctx);
preferences.inject_acp_models(ctx);
});
@@ -571,6 +603,14 @@ fn acp_models_expand_reasoning_levels_for_only_the_configured_agent() {
let mut preferences = empty_preferences();
app.read(|ctx| {
preferences.bedrock_verified_config =
Some(super::bedrock_discovery_config(AISettings::as_ref(ctx)));
preferences.bedrock_verified_model_ids = AISettings::as_ref(ctx)
.bedrock_models
.value()
.iter()
.map(|model| model.model_id.clone())
.collect();
preferences.inject_bedrock_models(ctx);
preferences.inject_acp_models(ctx);
});
@@ -710,6 +750,14 @@ fn acp_bedrock_models_are_hidden_while_bedrock_is_disabled() {
let mut preferences = empty_preferences();
app.read(|ctx| {
preferences.bedrock_verified_config =
Some(super::bedrock_discovery_config(AISettings::as_ref(ctx)));
preferences.bedrock_verified_model_ids = AISettings::as_ref(ctx)
.bedrock_models
.value()
.iter()
.map(|model| model.model_id.clone())
.collect();
preferences.inject_bedrock_models(ctx);
preferences.inject_acp_models(ctx);
});
@@ -751,6 +799,14 @@ fn disabled_providers_do_not_leave_models_in_the_runtime_inventory() {
let mut preferences = empty_preferences();
app.read(|ctx| {
preferences.bedrock_verified_config =
Some(super::bedrock_discovery_config(AISettings::as_ref(ctx)));
preferences.bedrock_verified_model_ids = AISettings::as_ref(ctx)
.bedrock_models
.value()
.iter()
.map(|model| model.model_id.clone())
.collect();
preferences.inject_bedrock_models(ctx);
preferences.inject_openai_models(ctx);
});
@@ -940,6 +996,94 @@ fn legacy_chatgpt_default_limits_routing() {
);
}
#[test]
fn subscription_and_litellm_connections_keep_overlapping_models_and_independent_toggles() {
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).unwrap();
settings
.openai_base_url
.set_value("https://legacy.test/v1".to_owned(), ctx)
.unwrap();
settings
.openai_models
.set_value(vec![openai_model("legacy-only")], ctx)
.unwrap();
settings
.openai_providers
.set_value(
vec![
OpenAIProviderConfig {
kind: OpenAIProviderKind::ChatGPTSubscription,
enabled: true,
name: "Subscription".to_owned(),
base_url: String::new(),
api_key: None,
project_id: None,
location: None,
models: vec![openai_model("shared-model")],
},
OpenAIProviderConfig {
kind: OpenAIProviderKind::LiteLLM,
enabled: true,
name: "LiteLLM".to_owned(),
base_url: "https://proxy.test/v1".to_owned(),
api_key: None,
project_id: None,
location: None,
models: vec![openai_model("shared-model")],
},
],
ctx,
)
.unwrap();
});
let mut preferences = empty_preferences();
app.read(|ctx| preferences.inject_openai_models(ctx));
let models = &preferences.models_by_feature.agent_mode.choices;
assert_eq!(models.len(), 3);
for (label, kind) in [
("Subscription", OpenAIProviderKind::ChatGPTSubscription),
("LiteLLM", OpenAIProviderKind::LiteLLM),
] {
let model = models
.iter()
.find(|model| model.description.as_deref() == Some(label))
.unwrap();
let route = preferences
.openai_client_config_for_model(model.id.as_str())
.unwrap();
assert_eq!(route.kind, kind);
assert_eq!(route.model.as_deref(), Some("shared-model"));
}
assert!(models
.iter()
.any(|model| model.id.as_str() == "legacy-only"));
let litellm_id = models
.iter()
.find(|model| model.description.as_deref() == Some("LiteLLM"))
.unwrap()
.id
.clone();
AISettings::handle(&app).update(&mut app, |settings, ctx| {
let mut providers = settings.openai_providers.value().clone();
providers[0].models[0].enabled = false;
settings.openai_providers.set_value(providers, ctx).unwrap();
});
app.read(|ctx| preferences.inject_openai_models(ctx));
let models = &preferences.models_by_feature.agent_mode.choices;
assert_eq!(models.len(), 2);
assert!(models
.iter()
.all(|model| model.description.as_deref() != Some("Subscription")));
assert!(models
.iter()
.any(|model| model.description.as_deref() == Some("LiteLLM")));
assert!(models.iter().any(|model| model.id == litellm_id));
});
}
#[test]
fn provider_registry_overrides_legacy_endpoint_for_duplicate_models() {
App::test((), |mut app| async move {
@@ -1071,3 +1215,44 @@ async fn model_refresh_distinguishes_empty_catalog_from_failure() {
response.remove_async().await;
}
}
#[test]
fn bedrock_verification_survives_settings_reload_but_not_connection_changes() {
App::test((), |mut app| async move {
initialize_settings_for_tests(&mut app);
let model = bedrock_model("anthropic.claude-test");
let reloaded: BedrockModelConfig =
serde_json::from_value(serde_json::to_value(&model).unwrap()).unwrap();
assert!(!reloaded.access_verified);
let mut preferences = empty_preferences();
app.read(|ctx| {
preferences.bedrock_verified_config =
Some(super::bedrock_discovery_config(AISettings::as_ref(ctx)));
preferences
.bedrock_verified_model_ids
.insert(model.model_id.clone());
});
app.update(|ctx| {
AISettings::handle(ctx).update(ctx, |settings, ctx| {
settings
.bedrock_models
.set_value(vec![reloaded], ctx)
.unwrap();
})
});
app.read(|ctx| assert!(preferences.bedrock_model_is_verified(&model.model_id, ctx)));
// Another process/startup cannot inherit an in-memory approval.
app.read(|ctx| {
assert!(!empty_preferences().bedrock_model_is_verified(&model.model_id, ctx))
});
app.update(|ctx| {
AISettings::handle(ctx).update(ctx, |settings, ctx| {
settings
.bedrock_region
.set_value("different-region".to_owned(), ctx)
.unwrap();
})
});
app.read(|ctx| assert!(!preferences.bedrock_model_is_verified(&model.model_id, ctx)));
});
}