Fix provider model inventories and Bedrock verification
This commit is contained in:
@@ -131,7 +131,7 @@ ACP: controller.rs → ResponseStream → acp_output_stream (session-owned lifec
|
|||||||
- `convert.rs` — Bedrock request construction and prompt-caching behavior
|
- `convert.rs` — Bedrock request construction and prompt-caching behavior
|
||||||
- `client.rs` — AWS SDK client construction, runtime creation, and independent background streaming calls
|
- `client.rs` — AWS SDK client construction, runtime creation, and independent background streaming calls
|
||||||
- `models.rs` — Model registry and cross-region inference prefix logic
|
- `models.rs` — Model registry and cross-region inference prefix logic
|
||||||
- `discovery.rs` — AWS profile listing and model discovery (STS identity check + ListFoundationModels)
|
- `discovery.rs` — Checks bundled `galaxy_bedrock_model_catalog` candidates with AWS `GetFoundationModelAvailability`; agreement, authorization, entitlement, and regional availability must all pass. Cached access is never restored as authorization; per-model enablement survives successful refreshes. Sweeps run once at startup when enabled and once per explicit connection Test; settings saves, reloads, and model toggles must never trigger another sweep. Live verification belongs to `LLMPreferences` and is scoped to the exact connection configuration.
|
||||||
- `diagnostic.rs` — Debug logging (enabled via `GALAXY_BEDROCK_DIAGNOSTICS=1`)
|
- `diagnostic.rs` — Debug logging (enabled via `GALAXY_BEDROCK_DIAGNOSTICS=1`)
|
||||||
- `external_config.rs` — Fallback config from Claude Code/OpenCode settings
|
- `external_config.rs` — Fallback config from Claude Code/OpenCode settings
|
||||||
|
|
||||||
|
|||||||
+147
-40
@@ -605,6 +605,11 @@ pub struct LLMPreferences {
|
|||||||
catalog_refresh_generations: HashMap<Option<usize>, u64>,
|
catalog_refresh_generations: HashMap<Option<usize>, u64>,
|
||||||
#[cfg(not(target_family = "wasm"))]
|
#[cfg(not(target_family = "wasm"))]
|
||||||
chatgpt_subscription_models_refresh_in_flight: bool,
|
chatgpt_subscription_models_refresh_in_flight: bool,
|
||||||
|
bedrock_models_refresh_in_flight: bool,
|
||||||
|
#[cfg(not(target_family = "wasm"))]
|
||||||
|
bedrock_verified_config: Option<super::provider::client::BedrockClientConfig>,
|
||||||
|
#[cfg(not(target_family = "wasm"))]
|
||||||
|
bedrock_verified_model_ids: HashSet<String>,
|
||||||
#[cfg(not(target_family = "wasm"))]
|
#[cfg(not(target_family = "wasm"))]
|
||||||
acp_selections: HashMap<LLMId, AcpModelSelection>,
|
acp_selections: HashMap<LLMId, AcpModelSelection>,
|
||||||
}
|
}
|
||||||
@@ -708,8 +713,23 @@ impl LLMPreferences {
|
|||||||
| AISettingsChangedEvent::AcpAgents { .. }
|
| AISettingsChangedEvent::AcpAgents { .. }
|
||||||
| AISettingsChangedEvent::AcpAgentId { .. }
|
| AISettingsChangedEvent::AcpAgentId { .. }
|
||||||
| AISettingsChangedEvent::BedrockModels { .. }
|
| AISettingsChangedEvent::BedrockModels { .. }
|
||||||
|
| AISettingsChangedEvent::BedrockAuthMethod { .. }
|
||||||
|
| AISettingsChangedEvent::BedrockProfile { .. }
|
||||||
|
| AISettingsChangedEvent::BedrockRegion { .. }
|
||||||
|
| AISettingsChangedEvent::BedrockAccessKeyId { .. }
|
||||||
|
| AISettingsChangedEvent::BedrockSecretAccessKey { .. }
|
||||||
|
| AISettingsChangedEvent::BedrockCrossRegionInference { .. }
|
||||||
) {
|
) {
|
||||||
let previous_ids: Vec<_> = me.openai_provider_routing.keys().cloned().collect();
|
let previous_ids: Vec<_> = me
|
||||||
|
.models_by_feature
|
||||||
|
.agent_mode
|
||||||
|
.choices
|
||||||
|
.iter()
|
||||||
|
.filter(|model| {
|
||||||
|
matches!(model.provider, LLMProvider::LiteLLM | LLMProvider::Bedrock)
|
||||||
|
})
|
||||||
|
.map(|model| model.id.as_str().to_owned())
|
||||||
|
.collect();
|
||||||
me.inject_bedrock_models(ctx);
|
me.inject_bedrock_models(ctx);
|
||||||
me.inject_openai_models(ctx);
|
me.inject_openai_models(ctx);
|
||||||
if matches!(
|
if matches!(
|
||||||
@@ -720,11 +740,6 @@ impl LLMPreferences {
|
|||||||
) {
|
) {
|
||||||
me.fetch_openai_models_from_endpoint(ctx);
|
me.fetch_openai_models_from_endpoint(ctx);
|
||||||
}
|
}
|
||||||
if matches!(event, AISettingsChangedEvent::BedrockEnabled { .. })
|
|
||||||
&& *AISettings::as_ref(ctx).bedrock_enabled.value()
|
|
||||||
{
|
|
||||||
me.refresh_bedrock_models(ctx);
|
|
||||||
}
|
|
||||||
me.refresh_changed_litellm_connections(ctx);
|
me.refresh_changed_litellm_connections(ctx);
|
||||||
// Safety: ensure the default model is still present in choices.
|
// Safety: ensure the default model is still present in choices.
|
||||||
// If all provider models were removed, the default_id would dangle.
|
// If all provider models were removed, the default_id would dangle.
|
||||||
@@ -770,6 +785,11 @@ impl LLMPreferences {
|
|||||||
catalog_refresh_generations: HashMap::new(),
|
catalog_refresh_generations: HashMap::new(),
|
||||||
#[cfg(not(target_family = "wasm"))]
|
#[cfg(not(target_family = "wasm"))]
|
||||||
chatgpt_subscription_models_refresh_in_flight: false,
|
chatgpt_subscription_models_refresh_in_flight: false,
|
||||||
|
bedrock_models_refresh_in_flight: false,
|
||||||
|
#[cfg(not(target_family = "wasm"))]
|
||||||
|
bedrock_verified_config: None,
|
||||||
|
#[cfg(not(target_family = "wasm"))]
|
||||||
|
bedrock_verified_model_ids: HashSet::new(),
|
||||||
#[cfg(not(target_family = "wasm"))]
|
#[cfg(not(target_family = "wasm"))]
|
||||||
acp_selections: HashMap::new(),
|
acp_selections: HashMap::new(),
|
||||||
};
|
};
|
||||||
@@ -789,9 +809,8 @@ impl LLMPreferences {
|
|||||||
|
|
||||||
#[cfg(not(target_family = "wasm"))]
|
#[cfg(not(target_family = "wasm"))]
|
||||||
{
|
{
|
||||||
// Make the persisted Bedrock catalog available immediately. Agreement
|
// Saved toggle preferences survive startup, but access must be
|
||||||
// revalidation stays in the background and replaces this cache when it
|
// verified against AWS before a Bedrock model becomes selectable.
|
||||||
// completes, so startup never waits on the per-model AWS checks.
|
|
||||||
me.inject_bedrock_models(ctx);
|
me.inject_bedrock_models(ctx);
|
||||||
me.refresh_bedrock_models(ctx);
|
me.refresh_bedrock_models(ctx);
|
||||||
me.inject_openai_models(ctx);
|
me.inject_openai_models(ctx);
|
||||||
@@ -807,40 +826,87 @@ impl LLMPreferences {
|
|||||||
|
|
||||||
#[cfg(not(target_family = "wasm"))]
|
#[cfg(not(target_family = "wasm"))]
|
||||||
fn refresh_bedrock_models(&mut self, ctx: &mut ModelContext<Self>) {
|
fn refresh_bedrock_models(&mut self, ctx: &mut ModelContext<Self>) {
|
||||||
|
if self.bedrock_models_refresh_in_flight {
|
||||||
|
return;
|
||||||
|
}
|
||||||
let settings = AISettings::as_ref(ctx);
|
let settings = AISettings::as_ref(ctx);
|
||||||
if !*settings.bedrock_enabled.value() {
|
if !*settings.bedrock_enabled.value() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let config = crate::ai::provider::client::BedrockClientConfig {
|
self.bedrock_models_refresh_in_flight = true;
|
||||||
auth_method: *settings.bedrock_auth_method.value(),
|
let config = bedrock_discovery_config(settings);
|
||||||
profile: settings.bedrock_profile.value().clone(),
|
let requested_config = config.clone();
|
||||||
region: settings.bedrock_region.value().clone(),
|
self.bedrock_verified_config = None;
|
||||||
access_key_id: settings.bedrock_access_key_id.value().clone(),
|
self.bedrock_verified_model_ids.clear();
|
||||||
secret_access_key: settings.bedrock_secret_access_key.value().clone(),
|
self.inject_bedrock_models(ctx);
|
||||||
session_token: None,
|
|
||||||
cross_region_inference: *settings.bedrock_cross_region_inference.value(),
|
|
||||||
};
|
|
||||||
|
|
||||||
let _ = ctx.spawn(
|
let _ = ctx.spawn(
|
||||||
async move { crate::ai::provider::discovery::discover_available_models(config).await },
|
async move { crate::ai::provider::discovery::discover_available_models(config).await },
|
||||||
|me, result, ctx| match result {
|
move |me, result, ctx| {
|
||||||
Ok(models) => {
|
let result = result.map(|discovery| {
|
||||||
AISettings::handle(ctx).update(ctx, |settings, ctx| {
|
for record in discovery.records {
|
||||||
if let Err(error) = settings.bedrock_models.set_value(models, ctx) {
|
crate::ai::remote_logging::log_model_event(ctx, record);
|
||||||
log::warn!("[bedrock] Failed to persist discovered models: {error}");
|
}
|
||||||
}
|
discovery.models
|
||||||
});
|
});
|
||||||
me.inject_bedrock_models(ctx);
|
me.bedrock_models_refresh_in_flight = false;
|
||||||
me.ensure_default_model_present();
|
if !*AISettings::as_ref(ctx).bedrock_enabled.value() {
|
||||||
ctx.emit(LLMPreferencesEvent::UpdatedAvailableLLMs);
|
return;
|
||||||
}
|
}
|
||||||
Err(error) => {
|
if bedrock_discovery_config(AISettings::as_ref(ctx)) != requested_config {
|
||||||
log::debug!("[bedrock] Startup model discovery unavailable: {error}");
|
return;
|
||||||
|
}
|
||||||
|
match result {
|
||||||
|
Ok(models) => {
|
||||||
|
// Verification belongs to this live connection, not serialized settings.
|
||||||
|
me.bedrock_verified_model_ids =
|
||||||
|
models.iter().map(|model| model.model_id.clone()).collect();
|
||||||
|
me.bedrock_verified_config = Some(requested_config);
|
||||||
|
AISettings::handle(ctx).update(ctx, |settings, ctx| {
|
||||||
|
let models = super::provider::discovery::preserve_model_toggles(
|
||||||
|
settings.bedrock_models.value(),
|
||||||
|
models,
|
||||||
|
);
|
||||||
|
if let Err(error) = settings.bedrock_models.set_value(models, ctx) {
|
||||||
|
log::warn!(
|
||||||
|
"[bedrock] Failed to persist discovered models: {error}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
me.inject_bedrock_models(ctx);
|
||||||
|
me.ensure_default_model_present();
|
||||||
|
ctx.emit(LLMPreferencesEvent::UpdatedAvailableLLMs);
|
||||||
|
}
|
||||||
|
Err(error) => {
|
||||||
|
log::debug!("[bedrock] Startup model discovery unavailable: {error}");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(not(target_family = "wasm"))]
|
||||||
|
pub(crate) fn record_bedrock_verification(
|
||||||
|
&mut self,
|
||||||
|
config: super::provider::client::BedrockClientConfig,
|
||||||
|
models: &[BedrockModelConfig],
|
||||||
|
ctx: &mut ModelContext<Self>,
|
||||||
|
) {
|
||||||
|
self.bedrock_verified_model_ids =
|
||||||
|
models.iter().map(|model| model.model_id.clone()).collect();
|
||||||
|
self.bedrock_verified_config = Some(config);
|
||||||
|
self.inject_bedrock_models(ctx);
|
||||||
|
self.ensure_default_model_present();
|
||||||
|
ctx.emit(LLMPreferencesEvent::UpdatedAvailableLLMs);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(target_family = "wasm"))]
|
||||||
|
pub(crate) fn bedrock_model_is_verified(&self, model_id: &str, ctx: &AppContext) -> bool {
|
||||||
|
self.bedrock_verified_config.as_ref()
|
||||||
|
== Some(&bedrock_discovery_config(AISettings::as_ref(ctx)))
|
||||||
|
&& self.bedrock_verified_model_ids.contains(model_id)
|
||||||
|
}
|
||||||
|
|
||||||
#[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) {
|
||||||
// Remove only previously injected Bedrock entries. Other enabled provider
|
// Remove only previously injected Bedrock entries. Other enabled provider
|
||||||
@@ -906,6 +972,9 @@ impl LLMPreferences {
|
|||||||
|
|
||||||
let effective = effective;
|
let effective = effective;
|
||||||
for model in effective {
|
for model in effective {
|
||||||
|
if !model.enabled || !self.bedrock_model_is_verified(&model.model_id, ctx) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
let metadata = model_metadata(&model.model_id);
|
let metadata = model_metadata(&model.model_id);
|
||||||
if metadata.is_some_and(|metadata| metadata.supports_agent_runtime() == Some(false)) {
|
if metadata.is_some_and(|metadata| metadata.supports_agent_runtime() == Some(false)) {
|
||||||
log::debug!(
|
log::debug!(
|
||||||
@@ -1078,9 +1147,11 @@ impl LLMPreferences {
|
|||||||
configured_models
|
configured_models
|
||||||
};
|
};
|
||||||
|
|
||||||
// The registry replaces legacy settings. Otherwise stale legacy credentials
|
// Only a registry entry for the same endpoint replaces the legacy
|
||||||
// win duplicate model IDs before the configured provider is considered.
|
// connection. Adding an unrelated subscription must not hide LiteLLM.
|
||||||
if settings.openai_providers.value().is_empty() && !single_provider_models.is_empty() {
|
let include_legacy =
|
||||||
|
settings.legacy_openai_provider_enabled() && !single_provider_models.is_empty();
|
||||||
|
if include_legacy {
|
||||||
let base_url = settings.openai_base_url.value().clone();
|
let base_url = settings.openai_base_url.value().clone();
|
||||||
let api_key = {
|
let api_key = {
|
||||||
let key = settings.openai_api_key.value().clone();
|
let key = settings.openai_api_key.value().clone();
|
||||||
@@ -1132,7 +1203,7 @@ impl LLMPreferences {
|
|||||||
.is_none_or(|project| project.trim().is_empty()),
|
.is_none_or(|project| project.trim().is_empty()),
|
||||||
OpenAIProviderKind::ChatGPTSubscription => false,
|
OpenAIProviderKind::ChatGPTSubscription => false,
|
||||||
};
|
};
|
||||||
if !provider.enabled || missing_credentials || provider.models.is_empty() {
|
if missing_credentials || provider.models.is_empty() {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
Some((
|
Some((
|
||||||
@@ -1147,6 +1218,12 @@ impl LLMPreferences {
|
|||||||
))
|
))
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
// Keep existing raw-model selections routed to registry connections.
|
||||||
|
// The legacy connection remains selectable under a qualified key when
|
||||||
|
// it serves the same model as a registered provider.
|
||||||
|
if include_legacy && provider_entries.len() > 1 {
|
||||||
|
provider_entries.rotate_left(1);
|
||||||
|
}
|
||||||
|
|
||||||
if provider_entries.is_empty() {
|
if provider_entries.is_empty() {
|
||||||
return;
|
return;
|
||||||
@@ -1154,6 +1231,7 @@ impl LLMPreferences {
|
|||||||
|
|
||||||
let mut total_injected = 0;
|
let mut total_injected = 0;
|
||||||
let mut seen_model_ids: HashSet<String> = HashSet::new();
|
let mut seen_model_ids: HashSet<String> = HashSet::new();
|
||||||
|
let mut seen_connections = HashSet::new();
|
||||||
for (
|
for (
|
||||||
provider_name,
|
provider_name,
|
||||||
provider_kind,
|
provider_kind,
|
||||||
@@ -1165,14 +1243,13 @@ impl LLMPreferences {
|
|||||||
models,
|
models,
|
||||||
) in provider_entries
|
) in provider_entries
|
||||||
{
|
{
|
||||||
if !provider_enabled {
|
let connection_identity = format!("{provider_kind:?}|{provider_name}|{base_url}|{provider_project_id:?}|{provider_location:?}");
|
||||||
|
if !seen_connections.insert(connection_identity.clone()) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
let mut connection_model_ids = HashSet::new();
|
||||||
for model in &models {
|
for model in &models {
|
||||||
if !model.enabled {
|
if !connection_model_ids.insert(model.model_id.clone()) {
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if !seen_model_ids.insert(model.model_id.clone()) {
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1189,10 +1266,25 @@ impl LLMPreferences {
|
|||||||
|
|
||||||
for reasoning_effort in reasoning_efforts {
|
for reasoning_effort in reasoning_efforts {
|
||||||
let reasoning_effort = reasoning_effort.cloned();
|
let reasoning_effort = reasoning_effort.cloned();
|
||||||
let model_key = reasoning_effort.as_deref().map_or_else(
|
let mut model_key = reasoning_effort.as_deref().map_or_else(
|
||||||
|| model.model_id.clone(),
|
|| model.model_id.clone(),
|
||||||
|effort| openai_model_variant_id(&model.model_id, effort),
|
|effort| openai_model_variant_id(&model.model_id, effort),
|
||||||
);
|
);
|
||||||
|
if !seen_model_ids.insert(model_key.clone()) {
|
||||||
|
use sha2::{Digest, Sha256};
|
||||||
|
let connection_hash =
|
||||||
|
format!("{:x}", Sha256::digest(connection_identity.as_bytes()));
|
||||||
|
model_key = format!("{model_key}::connection::{connection_hash}");
|
||||||
|
if !seen_model_ids.insert(model_key.clone()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Disabled entries still reserve their selection keys so
|
||||||
|
// toggling a connection cannot silently reroute another
|
||||||
|
// connection's saved model selection.
|
||||||
|
if !provider_enabled || !model.enabled {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
// Register the routing entry. Reasoning variants keep the provider's
|
// Register the routing entry. Reasoning variants keep the provider's
|
||||||
// actual model ID while using their synthetic key only for selection.
|
// actual model ID while using their synthetic key only for selection.
|
||||||
@@ -3355,3 +3447,18 @@ fn get_cached_models(_app: &mut AppContext) -> Option<ModelsByFeature> {
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
#[path = "llms_tests.rs"]
|
#[path = "llms_tests.rs"]
|
||||||
mod tests;
|
mod tests;
|
||||||
|
|
||||||
|
#[cfg(not(target_family = "wasm"))]
|
||||||
|
fn bedrock_discovery_config(
|
||||||
|
settings: &AISettings,
|
||||||
|
) -> crate::ai::provider::client::BedrockClientConfig {
|
||||||
|
crate::ai::provider::client::BedrockClientConfig {
|
||||||
|
auth_method: *settings.bedrock_auth_method.value(),
|
||||||
|
profile: settings.bedrock_profile.value().clone(),
|
||||||
|
region: settings.bedrock_region.value().clone(),
|
||||||
|
access_key_id: settings.bedrock_access_key_id.value().clone(),
|
||||||
|
secret_access_key: settings.bedrock_secret_access_key.value().clone(),
|
||||||
|
session_token: None,
|
||||||
|
cross_region_inference: *settings.bedrock_cross_region_inference.value(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -161,6 +161,8 @@ fn openai_model(model_id: &str) -> OpenAIModelConfig {
|
|||||||
|
|
||||||
fn bedrock_model(model_id: &str) -> BedrockModelConfig {
|
fn bedrock_model(model_id: &str) -> BedrockModelConfig {
|
||||||
BedrockModelConfig {
|
BedrockModelConfig {
|
||||||
|
enabled: true,
|
||||||
|
access_verified: true,
|
||||||
model_id: model_id.to_string(),
|
model_id: model_id.to_string(),
|
||||||
display_name: model_id.to_string(),
|
display_name: model_id.to_string(),
|
||||||
vision_supported: false,
|
vision_supported: false,
|
||||||
@@ -232,6 +234,9 @@ fn empty_preferences() -> LLMPreferences {
|
|||||||
litellm_connections: Vec::new(),
|
litellm_connections: Vec::new(),
|
||||||
catalog_refresh_generations: HashMap::new(),
|
catalog_refresh_generations: HashMap::new(),
|
||||||
chatgpt_subscription_models_refresh_in_flight: false,
|
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(),
|
acp_selections: HashMap::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -304,6 +309,14 @@ fn bedrock_and_openai_provider_models_coexist_in_picker() {
|
|||||||
let mut preferences = empty_preferences();
|
let mut preferences = empty_preferences();
|
||||||
app.read(|ctx| {
|
app.read(|ctx| {
|
||||||
preferences.inject_openai_models(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);
|
preferences.inject_bedrock_models(ctx);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -323,6 +336,14 @@ fn bedrock_and_openai_provider_models_coexist_in_picker() {
|
|||||||
|
|
||||||
let mut preferences = empty_preferences();
|
let mut preferences = empty_preferences();
|
||||||
app.read(|ctx| {
|
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_bedrock_models(ctx);
|
||||||
preferences.inject_openai_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(),
|
litellm_connections: Vec::new(),
|
||||||
catalog_refresh_generations: HashMap::new(),
|
catalog_refresh_generations: HashMap::new(),
|
||||||
chatgpt_subscription_models_refresh_in_flight: false,
|
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(),
|
acp_selections: HashMap::new(),
|
||||||
};
|
};
|
||||||
app.read(|ctx| preferences.inject_acp_models(ctx));
|
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();
|
let mut preferences = empty_preferences();
|
||||||
app.read(|ctx| {
|
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_bedrock_models(ctx);
|
||||||
preferences.inject_acp_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();
|
let mut preferences = empty_preferences();
|
||||||
app.read(|ctx| {
|
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_bedrock_models(ctx);
|
||||||
preferences.inject_acp_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();
|
let mut preferences = empty_preferences();
|
||||||
app.read(|ctx| {
|
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_bedrock_models(ctx);
|
||||||
preferences.inject_acp_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();
|
let mut preferences = empty_preferences();
|
||||||
app.read(|ctx| {
|
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_bedrock_models(ctx);
|
||||||
preferences.inject_openai_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]
|
#[test]
|
||||||
fn provider_registry_overrides_legacy_endpoint_for_duplicate_models() {
|
fn provider_registry_overrides_legacy_endpoint_for_duplicate_models() {
|
||||||
App::test((), |mut app| async move {
|
App::test((), |mut app| async move {
|
||||||
@@ -1071,3 +1215,44 @@ async fn model_refresh_distinguishes_empty_catalog_from_failure() {
|
|||||||
response.remove_async().await;
|
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)));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ pub struct BedrockClient {
|
|||||||
region: String,
|
region: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
pub struct BedrockClientConfig {
|
pub struct BedrockClientConfig {
|
||||||
pub auth_method: BedrockAuthMethod,
|
pub auth_method: BedrockAuthMethod,
|
||||||
pub profile: String,
|
pub profile: String,
|
||||||
@@ -101,7 +101,7 @@ impl BedrockClient {
|
|||||||
BedrockAuthMethod::Profile | BedrockAuthMethod::Sso => {
|
BedrockAuthMethod::Profile | BedrockAuthMethod::Sso => {
|
||||||
let mut loader = aws_config::defaults(BehaviorVersion::latest());
|
let mut loader = aws_config::defaults(BehaviorVersion::latest());
|
||||||
|
|
||||||
if !config.profile.is_empty() && config.profile != "default" {
|
if !config.profile.is_empty() {
|
||||||
loader = loader.profile_name(&config.profile);
|
loader = loader.profile_name(&config.profile);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,34 +8,46 @@ use aws_config::BehaviorVersion;
|
|||||||
use aws_sdk_bedrock::Client;
|
use aws_sdk_bedrock::Client;
|
||||||
use aws_sdk_bedrockruntime::config::Region;
|
use aws_sdk_bedrockruntime::config::Region;
|
||||||
use futures::{stream, StreamExt};
|
use futures::{stream, StreamExt};
|
||||||
use galaxy_bedrock_model_catalog::model_metadata;
|
use galaxy_bedrock_model_catalog::models as catalog_models;
|
||||||
|
use serde_json::json;
|
||||||
|
|
||||||
use super::client::{BedrockClientConfig, BedrockError};
|
use super::client::{BedrockClientConfig, BedrockError};
|
||||||
|
use crate::ai::remote_logging::{RemoteLogLevel, RemoteLogRecord};
|
||||||
use crate::settings::ai::BedrockModelConfig;
|
use crate::settings::ai::BedrockModelConfig;
|
||||||
|
|
||||||
|
pub(crate) struct BedrockDiscovery {
|
||||||
|
pub models: Vec<BedrockModelConfig>,
|
||||||
|
pub records: Vec<RemoteLogRecord>,
|
||||||
|
}
|
||||||
|
|
||||||
const AVAILABILITY_CHECK_CONCURRENCY: usize = 8;
|
const AVAILABILITY_CHECK_CONCURRENCY: usize = 8;
|
||||||
|
|
||||||
pub async fn discover_available_models(
|
pub(crate) async fn discover_available_models(
|
||||||
config: BedrockClientConfig,
|
config: BedrockClientConfig,
|
||||||
) -> Result<Vec<BedrockModelConfig>, String> {
|
) -> Result<BedrockDiscovery, String> {
|
||||||
let aws_config = load_aws_config(&config)
|
let aws_config = load_aws_config(&config)
|
||||||
.await
|
.await
|
||||||
.map_err(|error| error.to_string())?;
|
.map_err(|error| error.to_string())?;
|
||||||
let client = Client::new(&aws_config);
|
let client = Client::new(&aws_config);
|
||||||
let catalog = client
|
// The bundled catalog defines candidates; AWS defines account access.
|
||||||
.list_foundation_models()
|
// Never add arbitrary entries from ListFoundationModels to this catalog.
|
||||||
.send()
|
let candidates = catalog_models().to_vec();
|
||||||
.await
|
let sweep_id = uuid::Uuid::new_v4().to_string();
|
||||||
.map_err(|error| format!("Could not list AWS Bedrock foundation models: {error}"))?;
|
let region = aws_config
|
||||||
|
.region()
|
||||||
// AWS exposes agreement/authorization state only through an individual
|
.map(|region| region.as_ref())
|
||||||
// GetFoundationModelAvailability call. Keep using that control-plane API,
|
.unwrap_or("UNKNOWN")
|
||||||
// but bound the independent checks so a large catalog does not serialize
|
.to_owned();
|
||||||
// startup discovery one model at a time.
|
log::info!(
|
||||||
let checks = catalog.model_summaries().iter().cloned().map(|summary| {
|
"[bedrock] Starting availability sweep {sweep_id}: {} catalog models, region={region}",
|
||||||
|
candidates.len()
|
||||||
|
);
|
||||||
|
let checks = candidates.into_iter().map(|metadata| {
|
||||||
let client = client.clone();
|
let client = client.clone();
|
||||||
|
let sweep_id = sweep_id.clone();
|
||||||
|
let region = region.clone();
|
||||||
async move {
|
async move {
|
||||||
let model_id = summary.model_id();
|
let model_id = metadata.model_id.as_str();
|
||||||
let availability = match client
|
let availability = match client
|
||||||
.get_foundation_model_availability()
|
.get_foundation_model_availability()
|
||||||
.model_id(model_id)
|
.model_id(model_id)
|
||||||
@@ -44,67 +56,59 @@ pub async fn discover_available_models(
|
|||||||
{
|
{
|
||||||
Ok(availability) => availability,
|
Ok(availability) => availability,
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
log::debug!(
|
let error = crate::ai::remote_logging::sanitize_error(format!("{error:?}"));
|
||||||
"[bedrock] Availability check failed for {model_id}; excluding model: {error}"
|
log::warn!("[bedrock] Availability sweep {sweep_id}: model={model_id}, region={region}, decision=excluded, error={error}");
|
||||||
);
|
return (None, RemoteLogRecord {
|
||||||
return None;
|
level: RemoteLogLevel::Warn,
|
||||||
|
message: "Bedrock model availability check failed".to_owned(),
|
||||||
|
context: json!({"event": "bedrock_model_availability", "sweep_id": sweep_id, "model_id": model_id, "aws_region": region, "decision": "excluded", "error": error}),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
let agreement = availability.agreement_availability().map(|value| value.status().as_str());
|
||||||
|
let authorization = availability.authorization_status().as_str();
|
||||||
|
let entitlement = availability.entitlement_availability().as_str();
|
||||||
|
let region_status = availability.region_availability().as_str();
|
||||||
|
let access_allowed = model_availability_is_usable(agreement, authorization, entitlement, region_status);
|
||||||
|
let runtime_supported = metadata.supports_agent_runtime() != Some(false);
|
||||||
|
let allowed = access_allowed && runtime_supported;
|
||||||
|
let decision = if allowed { "allowed" } else { "excluded" };
|
||||||
|
log::info!("[bedrock] Availability sweep {sweep_id}: model={model_id}, agreement={}, authorization={authorization}, entitlement={entitlement}, region_status={region_status}, runtime_supported={runtime_supported}, decision={decision}", agreement.unwrap_or("MISSING"));
|
||||||
|
let record = RemoteLogRecord {
|
||||||
|
level: RemoteLogLevel::Info,
|
||||||
|
message: "Bedrock model availability checked".to_owned(),
|
||||||
|
context: json!({"event": "bedrock_model_availability", "sweep_id": sweep_id, "model_id": model_id, "aws_region": region, "agreement": agreement, "authorization": authorization, "entitlement": entitlement, "region_availability": region_status, "agreement_error": availability.agreement_availability().and_then(|value| value.error_message()).map(crate::ai::remote_logging::sanitize_error), "access_allowed": access_allowed, "runtime_supported": runtime_supported, "decision": decision}),
|
||||||
|
};
|
||||||
|
if !allowed { return (None, record); }
|
||||||
|
|
||||||
if !model_availability_is_usable(
|
(Some(BedrockModelConfig {
|
||||||
availability
|
|
||||||
.agreement_availability()
|
|
||||||
.map(|agreement| agreement.status().as_str()),
|
|
||||||
availability.authorization_status().as_str(),
|
|
||||||
availability.entitlement_availability().as_str(),
|
|
||||||
availability.region_availability().as_str(),
|
|
||||||
) {
|
|
||||||
log::debug!(
|
|
||||||
"[bedrock] Excluding {model_id}: agreement={}, authorization={}, entitlement={}, region={}",
|
|
||||||
availability
|
|
||||||
.agreement_availability()
|
|
||||||
.map(|agreement| agreement.status().as_str())
|
|
||||||
.unwrap_or("MISSING"),
|
|
||||||
availability.authorization_status().as_str(),
|
|
||||||
availability.entitlement_availability().as_str(),
|
|
||||||
availability.region_availability().as_str(),
|
|
||||||
);
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
let metadata = model_metadata(model_id);
|
|
||||||
let display_name = summary.model_name().map(str::to_owned).unwrap_or_else(|| {
|
|
||||||
metadata
|
|
||||||
.map(|metadata| metadata.model_name.clone())
|
|
||||||
.unwrap_or_else(|| prettify_model_id(model_id))
|
|
||||||
});
|
|
||||||
let vision_supported = summary
|
|
||||||
.input_modalities()
|
|
||||||
.iter()
|
|
||||||
.any(|modality| modality.as_str() == "IMAGE")
|
|
||||||
|| metadata.is_some_and(|metadata| metadata.supports_vision_input());
|
|
||||||
|
|
||||||
Some(BedrockModelConfig {
|
|
||||||
model_id: model_id.to_owned(),
|
model_id: model_id.to_owned(),
|
||||||
display_name,
|
display_name: metadata.model_name.clone(),
|
||||||
vision_supported,
|
vision_supported: metadata.supports_vision_input(),
|
||||||
})
|
enabled: true,
|
||||||
|
access_verified: true,
|
||||||
|
}), record)
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
let mut models = stream::iter(checks)
|
let results = stream::iter(checks)
|
||||||
.buffer_unordered(AVAILABILITY_CHECK_CONCURRENCY)
|
.buffer_unordered(AVAILABILITY_CHECK_CONCURRENCY)
|
||||||
.filter_map(futures::future::ready)
|
|
||||||
.collect::<Vec<_>>()
|
.collect::<Vec<_>>()
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
|
let (models, mut records): (Vec<_>, Vec<_>) = results.into_iter().unzip();
|
||||||
|
let mut models = models.into_iter().flatten().collect::<Vec<_>>();
|
||||||
models.sort_by(|left, right| left.display_name.cmp(&right.display_name));
|
models.sort_by(|left, right| left.display_name.cmp(&right.display_name));
|
||||||
if models.is_empty() {
|
log::info!(
|
||||||
return Err(
|
"[bedrock] Availability sweep {sweep_id} complete: checked={}, allowed={}",
|
||||||
"AWS returned no Bedrock models that are authorized and available in this region."
|
records.len(),
|
||||||
.to_string(),
|
models.len()
|
||||||
);
|
);
|
||||||
}
|
records.push(RemoteLogRecord {
|
||||||
Ok(models)
|
level: RemoteLogLevel::Info,
|
||||||
|
message: "Bedrock availability sweep completed".to_owned(),
|
||||||
|
context: json!({"event": "bedrock_availability_sweep_completed", "sweep_id": sweep_id, "aws_region": region, "catalog_count": catalog_models().len(), "checked_count": records.len(), "allowed_count": models.len()}),
|
||||||
|
});
|
||||||
|
Ok(BedrockDiscovery { models, records })
|
||||||
}
|
}
|
||||||
|
|
||||||
fn model_availability_is_usable(
|
fn model_availability_is_usable(
|
||||||
@@ -119,6 +123,21 @@ fn model_availability_is_usable(
|
|||||||
&& region_status == "AVAILABLE"
|
&& region_status == "AVAILABLE"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn preserve_model_toggles(
|
||||||
|
existing: &[BedrockModelConfig],
|
||||||
|
mut discovered: Vec<BedrockModelConfig>,
|
||||||
|
) -> Vec<BedrockModelConfig> {
|
||||||
|
for model in &mut discovered {
|
||||||
|
if let Some(previous) = existing
|
||||||
|
.iter()
|
||||||
|
.find(|previous| previous.model_id == model.model_id)
|
||||||
|
{
|
||||||
|
model.enabled = previous.enabled;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
discovered
|
||||||
|
}
|
||||||
|
|
||||||
async fn load_aws_config(
|
async fn load_aws_config(
|
||||||
config: &BedrockClientConfig,
|
config: &BedrockClientConfig,
|
||||||
) -> Result<aws_config::SdkConfig, BedrockError> {
|
) -> Result<aws_config::SdkConfig, BedrockError> {
|
||||||
@@ -126,7 +145,7 @@ async fn load_aws_config(
|
|||||||
crate::settings::ai::BedrockAuthMethod::Profile
|
crate::settings::ai::BedrockAuthMethod::Profile
|
||||||
| crate::settings::ai::BedrockAuthMethod::Sso => {
|
| crate::settings::ai::BedrockAuthMethod::Sso => {
|
||||||
let mut loader = aws_config::defaults(BehaviorVersion::latest());
|
let mut loader = aws_config::defaults(BehaviorVersion::latest());
|
||||||
if !config.profile.is_empty() && config.profile != "default" {
|
if !config.profile.is_empty() {
|
||||||
loader = loader.profile_name(&config.profile);
|
loader = loader.profile_name(&config.profile);
|
||||||
}
|
}
|
||||||
if !config.region.is_empty() {
|
if !config.region.is_empty() {
|
||||||
@@ -162,14 +181,6 @@ async fn load_aws_config(
|
|||||||
Ok(sdk_config)
|
Ok(sdk_config)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn prettify_model_id(model_id: &str) -> String {
|
|
||||||
model_id
|
|
||||||
.rsplit('.')
|
|
||||||
.next()
|
|
||||||
.unwrap_or(model_id)
|
|
||||||
.replace(['-', ':'], " ")
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
#[path = "discovery_tests.rs"]
|
#[path = "discovery_tests.rs"]
|
||||||
mod tests;
|
mod tests;
|
||||||
|
|||||||
@@ -1,5 +1,26 @@
|
|||||||
use super::model_availability_is_usable;
|
use super::model_availability_is_usable;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cached_access_is_not_authorization_and_refresh_preserves_disabled_models() {
|
||||||
|
let model = crate::settings::BedrockModelConfig {
|
||||||
|
model_id: "test.model".to_owned(),
|
||||||
|
display_name: "Test".to_owned(),
|
||||||
|
vision_supported: false,
|
||||||
|
enabled: false,
|
||||||
|
access_verified: true,
|
||||||
|
};
|
||||||
|
let saved = serde_json::to_value(&model).unwrap();
|
||||||
|
assert!(saved.get("access_verified").is_none());
|
||||||
|
let restored: crate::settings::BedrockModelConfig = serde_json::from_value(saved).unwrap();
|
||||||
|
assert!(!restored.access_verified);
|
||||||
|
assert!(!restored.enabled);
|
||||||
|
let mut discovered = model.clone();
|
||||||
|
discovered.enabled = true;
|
||||||
|
let refreshed = super::preserve_model_toggles(&[model], vec![discovered]);
|
||||||
|
assert!(!refreshed[0].enabled);
|
||||||
|
assert!(refreshed[0].access_verified);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn requires_every_availability_status() {
|
fn requires_every_availability_status() {
|
||||||
assert!(model_availability_is_usable(
|
assert!(model_availability_is_usable(
|
||||||
|
|||||||
@@ -141,6 +141,8 @@ fn parse_claude_code_model_map(
|
|||||||
.map(|(source_id, arn)| {
|
.map(|(source_id, arn)| {
|
||||||
let display_name = derive_display_name(&source_id);
|
let display_name = derive_display_name(&source_id);
|
||||||
BedrockModelConfig {
|
BedrockModelConfig {
|
||||||
|
enabled: true,
|
||||||
|
access_verified: false,
|
||||||
model_id: arn,
|
model_id: arn,
|
||||||
display_name,
|
display_name,
|
||||||
vision_supported: true,
|
vision_supported: true,
|
||||||
@@ -276,8 +278,14 @@ pub fn list_aws_profiles() -> Vec<String> {
|
|||||||
let mut profiles = BTreeSet::new();
|
let mut profiles = BTreeSet::new();
|
||||||
|
|
||||||
if let Some(home) = dirs::home_dir() {
|
if let Some(home) = dirs::home_dir() {
|
||||||
parse_aws_config_file(home.join(".aws").join("config"), &mut profiles);
|
let config_path = std::env::var_os("AWS_CONFIG_FILE")
|
||||||
parse_aws_credentials_file(home.join(".aws").join("credentials"), &mut profiles);
|
.map(PathBuf::from)
|
||||||
|
.unwrap_or_else(|| home.join(".aws").join("config"));
|
||||||
|
let credentials_path = std::env::var_os("AWS_SHARED_CREDENTIALS_FILE")
|
||||||
|
.map(PathBuf::from)
|
||||||
|
.unwrap_or_else(|| home.join(".aws").join("credentials"));
|
||||||
|
parse_aws_config_file(config_path, &mut profiles);
|
||||||
|
parse_aws_credentials_file(credentials_path, &mut profiles);
|
||||||
}
|
}
|
||||||
|
|
||||||
profiles.into_iter().collect()
|
profiles.into_iter().collect()
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ use std::{
|
|||||||
|
|
||||||
use chrono::Utc;
|
use chrono::Utc;
|
||||||
use galaxy_core::channel::ChannelState;
|
use galaxy_core::channel::ChannelState;
|
||||||
use galaxyui::{Entity, ModelContext, SingletonEntity};
|
use galaxyui::{Entity, ModelContext, SingletonEntity, View, ViewContext};
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use serde_json::{json, Value};
|
use serde_json::{json, Value};
|
||||||
use settings::Setting;
|
use settings::Setting;
|
||||||
@@ -123,6 +123,30 @@ where
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Views must log without updating AISettings: its singleton cannot be read
|
||||||
|
/// while it is temporarily borrowed by a model update.
|
||||||
|
pub(crate) fn log_view_event<V: View>(ctx: &mut ViewContext<V>, record: RemoteLogRecord) {
|
||||||
|
let Some(config) = RemoteLogConfig::from_settings(AISettings::as_ref(ctx)) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let payload = RemoteLogPayload {
|
||||||
|
level: record.level.as_str(),
|
||||||
|
message: record.message,
|
||||||
|
service: REMOTE_LOG_SERVICE,
|
||||||
|
context: enrich_context(record.context),
|
||||||
|
};
|
||||||
|
#[cfg(all(feature = "remote_logs_local_file", not(target_family = "wasm")))]
|
||||||
|
write_local_log(&payload);
|
||||||
|
let _ = ctx.spawn(
|
||||||
|
async move { send_remote_log(config, payload).await },
|
||||||
|
|_, result, _| {
|
||||||
|
if let Err(error) = result {
|
||||||
|
log::warn!("[remote-logging] Failed to send remote AI log: {error}");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(all(feature = "remote_logs_local_file", not(target_family = "wasm")))]
|
#[cfg(all(feature = "remote_logs_local_file", not(target_family = "wasm")))]
|
||||||
fn write_local_log(payload: &RemoteLogPayload) {
|
fn write_local_log(payload: &RemoteLogPayload) {
|
||||||
let state = LOCAL_LOG_FILE.get_or_init(|| Mutex::new(None));
|
let state = LOCAL_LOG_FILE.get_or_init(|| Mutex::new(None));
|
||||||
|
|||||||
+30
-13
@@ -819,6 +819,12 @@ impl BedrockAuthMethod {
|
|||||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, schemars::JsonSchema)]
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, schemars::JsonSchema)]
|
||||||
#[schemars(description = "Configuration for a single AWS Bedrock model.")]
|
#[schemars(description = "Configuration for a single AWS Bedrock model.")]
|
||||||
pub struct BedrockModelConfig {
|
pub struct BedrockModelConfig {
|
||||||
|
#[serde(default = "default_enabled")]
|
||||||
|
pub enabled: bool,
|
||||||
|
/// Live AWS verification is never restored from disk.
|
||||||
|
#[serde(skip)]
|
||||||
|
#[schemars(skip)]
|
||||||
|
pub access_verified: bool,
|
||||||
#[schemars(
|
#[schemars(
|
||||||
description = "The Bedrock model ID (e.g. anthropic.claude-sonnet-4-20250514-v1:0)."
|
description = "The Bedrock model ID (e.g. anthropic.claude-sonnet-4-20250514-v1:0)."
|
||||||
)]
|
)]
|
||||||
@@ -2347,27 +2353,33 @@ impl AISettings {
|
|||||||
/// consulted only for the legacy single-endpoint configuration, which has no per-provider
|
/// consulted only for the legacy single-endpoint configuration, which has no per-provider
|
||||||
/// enablement field.
|
/// enablement field.
|
||||||
pub fn is_openai_provider_enabled(&self) -> bool {
|
pub fn is_openai_provider_enabled(&self) -> bool {
|
||||||
if !self.openai_providers.value().is_empty() {
|
self.legacy_openai_provider_enabled()
|
||||||
return self
|
|| self
|
||||||
.openai_providers
|
.openai_providers
|
||||||
.value()
|
.value()
|
||||||
.iter()
|
.iter()
|
||||||
.any(|provider| provider.enabled);
|
.any(|provider| provider.enabled)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn legacy_openai_provider_enabled(&self) -> bool {
|
||||||
*self.openai_enabled.value()
|
*self.openai_enabled.value()
|
||||||
|
&& !self.openai_providers.value().iter().any(|provider| {
|
||||||
|
matches!(
|
||||||
|
provider.kind,
|
||||||
|
OpenAIProviderKind::OpenAI | OpenAIProviderKind::LiteLLM
|
||||||
|
) && provider.base_url.trim_end_matches('/')
|
||||||
|
== self.openai_base_url.value().trim_end_matches('/')
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns whether an OpenAI-compatible provider is enabled and has at least one enabled model.
|
/// Returns whether an OpenAI-compatible provider is enabled and has at least one enabled model.
|
||||||
pub fn has_enabled_openai_provider(&self) -> bool {
|
pub fn has_enabled_openai_provider(&self) -> bool {
|
||||||
if !self.openai_providers.value().is_empty() {
|
self.openai_providers
|
||||||
return self.openai_providers.value().iter().any(|provider| {
|
.value()
|
||||||
provider.enabled && provider.models.iter().any(|model| model.enabled)
|
.iter()
|
||||||
});
|
.any(|provider| provider.enabled && provider.models.iter().any(|model| model.enabled))
|
||||||
}
|
|| (self.legacy_openai_provider_enabled()
|
||||||
|
&& self.openai_models.value().iter().any(|model| model.enabled))
|
||||||
self.is_openai_provider_enabled()
|
|
||||||
&& self.openai_models.value().iter().any(|model| model.enabled)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn configured_acp_providers(&self) -> Vec<AcpProviderConfig> {
|
pub(crate) fn configured_acp_providers(&self) -> Vec<AcpProviderConfig> {
|
||||||
@@ -2434,7 +2446,12 @@ impl AISettings {
|
|||||||
|
|
||||||
/// Returns whether Galaxy has a local model provider or agent runtime enabled.
|
/// Returns whether Galaxy has a local model provider or agent runtime enabled.
|
||||||
pub fn has_enabled_ai_runtime(&self) -> bool {
|
pub fn has_enabled_ai_runtime(&self) -> bool {
|
||||||
(*self.bedrock_enabled.value() && !self.bedrock_models.value().is_empty())
|
(*self.bedrock_enabled.value()
|
||||||
|
&& self
|
||||||
|
.bedrock_models
|
||||||
|
.value()
|
||||||
|
.iter()
|
||||||
|
.any(|model| model.enabled))
|
||||||
|| self.has_enabled_openai_provider()
|
|| self.has_enabled_openai_provider()
|
||||||
|| (cfg!(unix)
|
|| (cfg!(unix)
|
||||||
&& FeatureFlag::AgentClientProtocol.is_enabled()
|
&& FeatureFlag::AgentClientProtocol.is_enabled()
|
||||||
|
|||||||
@@ -441,6 +441,8 @@ fn orchestration_is_enabled_when_ai_is_enabled() {
|
|||||||
.bedrock_models
|
.bedrock_models
|
||||||
.set_value(
|
.set_value(
|
||||||
vec![BedrockModelConfig {
|
vec![BedrockModelConfig {
|
||||||
|
enabled: true,
|
||||||
|
access_verified: true,
|
||||||
model_id: "test-model".to_string(),
|
model_id: "test-model".to_string(),
|
||||||
display_name: "Test model".to_string(),
|
display_name: "Test model".to_string(),
|
||||||
vision_supported: false,
|
vision_supported: false,
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ use galaxyui::elements::{
|
|||||||
Dismiss, Empty, Expanded, Fill, Flex, FormattedTextElement, HighlightedHyperlink, Hoverable,
|
Dismiss, Empty, Expanded, Fill, Flex, FormattedTextElement, HighlightedHyperlink, Hoverable,
|
||||||
HyperlinkLens, HyperlinkUrl, MainAxisAlignment, MainAxisSize, MouseStateHandle,
|
HyperlinkLens, HyperlinkUrl, MainAxisAlignment, MainAxisSize, MouseStateHandle,
|
||||||
OffsetPositioning, Padding, ParentAnchor, ParentElement, ParentOffsetBounds, Radius,
|
OffsetPositioning, Padding, ParentAnchor, ParentElement, ParentOffsetBounds, Radius,
|
||||||
Shrinkable, Stack, Text,
|
Shrinkable, Stack, TableStateHandle, Text,
|
||||||
};
|
};
|
||||||
use galaxyui::fonts::{Properties, Weight};
|
use galaxyui::fonts::{Properties, Weight};
|
||||||
use galaxyui::keymap::{ContextPredicate, FixedBinding, Keystroke};
|
use galaxyui::keymap::{ContextPredicate, FixedBinding, Keystroke};
|
||||||
@@ -42,8 +42,8 @@ use strum::IntoEnumIterator;
|
|||||||
|
|
||||||
use super::execution_profile_view::{ExecutionProfileView, ExecutionProfileViewEvent};
|
use super::execution_profile_view::{ExecutionProfileView, ExecutionProfileViewEvent};
|
||||||
use super::provider_setup_view::{
|
use super::provider_setup_view::{
|
||||||
AcpProviderDraft, BedrockProviderDraft, ProviderSetupProviderType, ProviderSetupView,
|
model_catalog_table, AcpProviderDraft, BedrockProviderDraft, ProviderSetupProviderType,
|
||||||
ProviderSetupViewEvent,
|
ProviderSetupView, ProviderSetupViewEvent,
|
||||||
};
|
};
|
||||||
use super::settings_page::{
|
use super::settings_page::{
|
||||||
build_sub_header, build_toggle_element, render_body_item_label,
|
build_sub_header, build_toggle_element, render_body_item_label,
|
||||||
@@ -2570,12 +2570,17 @@ pub enum AISettingsPageAction {
|
|||||||
RemoveFromMCPDenylist(uuid::Uuid),
|
RemoveFromMCPDenylist(uuid::Uuid),
|
||||||
CreateProfile,
|
CreateProfile,
|
||||||
ToggleBedrockEnabled,
|
ToggleBedrockEnabled,
|
||||||
|
ToggleBedrockModel(String),
|
||||||
ToggleOpenAIEnabled,
|
ToggleOpenAIEnabled,
|
||||||
ToggleAcpEnabled,
|
ToggleAcpEnabled,
|
||||||
FetchOpenAIProviderModels(usize),
|
FetchOpenAIProviderModels(usize),
|
||||||
AddOpenAIProvider(ProviderSetupProviderType),
|
AddOpenAIProvider(ProviderSetupProviderType),
|
||||||
EditOpenAIProvider(usize),
|
EditOpenAIProvider(usize),
|
||||||
ToggleOpenAIProviderEnabled(usize),
|
ToggleOpenAIProviderEnabled(usize),
|
||||||
|
ToggleOpenAIProviderModel {
|
||||||
|
provider_index: usize,
|
||||||
|
model_id: String,
|
||||||
|
},
|
||||||
RemoveOpenAIProvider(usize),
|
RemoveOpenAIProvider(usize),
|
||||||
EditBedrockProvider,
|
EditBedrockProvider,
|
||||||
RemoveBedrockProvider,
|
RemoveBedrockProvider,
|
||||||
@@ -3223,11 +3228,18 @@ impl TypedActionView for AISettingsPageView {
|
|||||||
});
|
});
|
||||||
ctx.notify();
|
ctx.notify();
|
||||||
}
|
}
|
||||||
|
AISettingsPageAction::ToggleBedrockModel(model_id) => {
|
||||||
|
AISettings::handle(ctx).update(ctx, |settings, ctx| {
|
||||||
|
let mut models = settings.bedrock_models.value().clone();
|
||||||
|
if let Some(model) = models.iter_mut().find(|model| model.model_id == *model_id)
|
||||||
|
{
|
||||||
|
model.enabled = !model.enabled;
|
||||||
|
report_if_error!(settings.bedrock_models.set_value(models, ctx));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
AISettingsPageAction::ToggleBedrockEnabled => {
|
AISettingsPageAction::ToggleBedrockEnabled => {
|
||||||
AISettings::handle(ctx).update(ctx, |settings, ctx| {
|
AISettings::handle(ctx).update(ctx, |settings, ctx| {
|
||||||
if !*settings.bedrock_enabled.value() {
|
|
||||||
report_if_error!(settings.bedrock_models.set_value(Vec::new(), ctx));
|
|
||||||
}
|
|
||||||
report_if_error!(settings.bedrock_enabled.toggle_and_save_value(ctx));
|
report_if_error!(settings.bedrock_enabled.toggle_and_save_value(ctx));
|
||||||
});
|
});
|
||||||
ctx.notify();
|
ctx.notify();
|
||||||
@@ -3264,6 +3276,23 @@ impl TypedActionView for AISettingsPageView {
|
|||||||
report_if_error!(settings.openai_providers.set_value(providers, ctx));
|
report_if_error!(settings.openai_providers.set_value(providers, ctx));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
AISettingsPageAction::ToggleOpenAIProviderModel {
|
||||||
|
provider_index,
|
||||||
|
model_id,
|
||||||
|
} => {
|
||||||
|
AISettings::handle(ctx).update(ctx, |settings, ctx| {
|
||||||
|
let mut providers = settings.openai_providers.value().clone();
|
||||||
|
if let Some(model) = providers.get_mut(*provider_index).and_then(|provider| {
|
||||||
|
provider
|
||||||
|
.models
|
||||||
|
.iter_mut()
|
||||||
|
.find(|model| model.model_id == *model_id)
|
||||||
|
}) {
|
||||||
|
model.enabled = !model.enabled;
|
||||||
|
report_if_error!(settings.openai_providers.set_value(providers, ctx));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
AISettingsPageAction::RemoveOpenAIProvider(provider_index) => {
|
AISettingsPageAction::RemoveOpenAIProvider(provider_index) => {
|
||||||
AISettings::handle(ctx).update(ctx, |settings, ctx| {
|
AISettings::handle(ctx).update(ctx, |settings, ctx| {
|
||||||
let mut providers = settings.openai_providers.value().clone();
|
let mut providers = settings.openai_providers.value().clone();
|
||||||
@@ -6428,6 +6457,8 @@ impl ModelProviderSection {
|
|||||||
|
|
||||||
struct OpenAIProviderCardState {
|
struct OpenAIProviderCardState {
|
||||||
enabled_switch: SwitchStateHandle,
|
enabled_switch: SwitchStateHandle,
|
||||||
|
model_switches: RefCell<HashMap<String, SwitchStateHandle>>,
|
||||||
|
model_table_state: TableStateHandle,
|
||||||
edit_button: ViewHandle<ActionButton>,
|
edit_button: ViewHandle<ActionButton>,
|
||||||
remove_button: ViewHandle<ActionButton>,
|
remove_button: ViewHandle<ActionButton>,
|
||||||
}
|
}
|
||||||
@@ -6439,6 +6470,8 @@ struct AcpProviderCardState {
|
|||||||
struct ProviderSettingsWidget {
|
struct ProviderSettingsWidget {
|
||||||
provider_type: ProviderSetupProviderType,
|
provider_type: ProviderSetupProviderType,
|
||||||
bedrock_enabled_toggle: SwitchStateHandle,
|
bedrock_enabled_toggle: SwitchStateHandle,
|
||||||
|
bedrock_model_switches: RefCell<HashMap<String, SwitchStateHandle>>,
|
||||||
|
bedrock_model_table_state: TableStateHandle,
|
||||||
add_openai_provider_button: ViewHandle<ActionButton>,
|
add_openai_provider_button: ViewHandle<ActionButton>,
|
||||||
add_litellm_provider_button: ViewHandle<ActionButton>,
|
add_litellm_provider_button: ViewHandle<ActionButton>,
|
||||||
add_chatgpt_provider_button: ViewHandle<ActionButton>,
|
add_chatgpt_provider_button: ViewHandle<ActionButton>,
|
||||||
@@ -6461,6 +6494,8 @@ impl ProviderSettingsWidget {
|
|||||||
.enumerate()
|
.enumerate()
|
||||||
.map(|(index, _)| OpenAIProviderCardState {
|
.map(|(index, _)| OpenAIProviderCardState {
|
||||||
enabled_switch: SwitchStateHandle::default(),
|
enabled_switch: SwitchStateHandle::default(),
|
||||||
|
model_switches: RefCell::new(HashMap::new()),
|
||||||
|
model_table_state: TableStateHandle::new(0, |_, _| Vec::new()),
|
||||||
edit_button: ctx.add_typed_action_view(move |_| {
|
edit_button: ctx.add_typed_action_view(move |_| {
|
||||||
ActionButton::new("Edit", SecondaryTheme).on_click(move |ctx| {
|
ActionButton::new("Edit", SecondaryTheme).on_click(move |ctx| {
|
||||||
ctx.dispatch_typed_action(AISettingsPageAction::EditOpenAIProvider(index));
|
ctx.dispatch_typed_action(AISettingsPageAction::EditOpenAIProvider(index));
|
||||||
@@ -6550,6 +6585,8 @@ impl ProviderSettingsWidget {
|
|||||||
Self {
|
Self {
|
||||||
provider_type,
|
provider_type,
|
||||||
bedrock_enabled_toggle: SwitchStateHandle::default(),
|
bedrock_enabled_toggle: SwitchStateHandle::default(),
|
||||||
|
bedrock_model_switches: RefCell::new(HashMap::new()),
|
||||||
|
bedrock_model_table_state: TableStateHandle::new(0, |_, _| Vec::new()),
|
||||||
add_openai_provider_button,
|
add_openai_provider_button,
|
||||||
add_litellm_provider_button,
|
add_litellm_provider_button,
|
||||||
add_chatgpt_provider_button,
|
add_chatgpt_provider_button,
|
||||||
@@ -6593,17 +6630,7 @@ impl ProviderSettingsWidget {
|
|||||||
model: &OpenAIModelConfig,
|
model: &OpenAIModelConfig,
|
||||||
provider: &OpenAIProviderConfig,
|
provider: &OpenAIProviderConfig,
|
||||||
) -> String {
|
) -> String {
|
||||||
let context_size = if provider.kind == OpenAIProviderKind::ChatGPTSubscription {
|
let mut details = Vec::new();
|
||||||
// Use the same limit as requests, including for older saved catalogs
|
|
||||||
// that still contain the optional long-context maximum.
|
|
||||||
openai_model_context_size(model, provider.kind)
|
|
||||||
} else {
|
|
||||||
model.context_size
|
|
||||||
};
|
|
||||||
let mut details = vec![
|
|
||||||
if model.enabled { "Enabled" } else { "Disabled" }.to_string(),
|
|
||||||
format!("Context: {context_size}"),
|
|
||||||
];
|
|
||||||
|
|
||||||
if let Some(default_context_size) = model
|
if let Some(default_context_size) = model
|
||||||
.default_context_size
|
.default_context_size
|
||||||
@@ -6686,60 +6713,193 @@ impl ProviderSettingsWidget {
|
|||||||
.finish();
|
.finish();
|
||||||
}
|
}
|
||||||
|
|
||||||
Container::new(Flex::column().with_children(rows).finish())
|
Container::new(
|
||||||
.with_background(appearance.theme().surface_2())
|
Flex::column()
|
||||||
.with_border(Border::all(1.).with_border_fill(appearance.theme().outline()))
|
.with_cross_axis_alignment(CrossAxisAlignment::Stretch)
|
||||||
.with_corner_radius(CornerRadius::with_all(Radius::Pixels(6.)))
|
.with_children(rows)
|
||||||
.finish()
|
.finish(),
|
||||||
|
)
|
||||||
|
.with_background(appearance.theme().surface_2())
|
||||||
|
.with_border(Border::all(1.).with_border_fill(appearance.theme().outline()))
|
||||||
|
.with_corner_radius(CornerRadius::with_all(Radius::Pixels(6.)))
|
||||||
|
.finish()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_openai_model_catalog(
|
fn render_openai_model_catalog(
|
||||||
|
&self,
|
||||||
|
provider_index: usize,
|
||||||
provider: &OpenAIProviderConfig,
|
provider: &OpenAIProviderConfig,
|
||||||
appearance: &Appearance,
|
appearance: &Appearance,
|
||||||
) -> Box<dyn Element> {
|
) -> Box<dyn Element> {
|
||||||
let rows = provider
|
if provider.models.is_empty() {
|
||||||
|
return Self::render_model_catalog(
|
||||||
|
Vec::new(),
|
||||||
|
"No models configured for this connection.",
|
||||||
|
appearance,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
let card = &self.provider_cards[provider_index];
|
||||||
|
let switches = provider
|
||||||
.models
|
.models
|
||||||
.iter()
|
.iter()
|
||||||
.map(|model| {
|
.map(|model| {
|
||||||
Self::render_model_row(
|
card.model_switches
|
||||||
model.display_name.clone(),
|
.borrow_mut()
|
||||||
model.model_id.clone(),
|
.entry(model.model_id.clone())
|
||||||
Self::format_openai_model_details(model, provider),
|
.or_default()
|
||||||
appearance,
|
.clone()
|
||||||
)
|
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
Self::render_model_catalog(
|
let provider = provider.clone();
|
||||||
rows,
|
model_catalog_table(
|
||||||
"No models configured for this connection.",
|
card.model_table_state.clone(),
|
||||||
|
provider.models.len(),
|
||||||
|
move |index, app| {
|
||||||
|
let appearance = Appearance::as_ref(app);
|
||||||
|
let model = &provider.models[index];
|
||||||
|
let model_id = model.model_id.clone();
|
||||||
|
let toggle = appearance
|
||||||
|
.ui_builder()
|
||||||
|
.switch(switches[index].clone())
|
||||||
|
.check(model.enabled)
|
||||||
|
.build()
|
||||||
|
.on_click(move |ctx, _, _| {
|
||||||
|
ctx.dispatch_typed_action(AISettingsPageAction::ToggleOpenAIProviderModel {
|
||||||
|
provider_index,
|
||||||
|
model_id: model_id.clone(),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.finish();
|
||||||
|
let text = |value: String| {
|
||||||
|
Text::new(value, appearance.ui_font_family(), 11.)
|
||||||
|
.with_color(appearance.theme().nonactive_ui_text_color().into())
|
||||||
|
.soft_wrap(true)
|
||||||
|
.finish()
|
||||||
|
};
|
||||||
|
let model_info = Flex::column()
|
||||||
|
.with_spacing(3.)
|
||||||
|
.with_child(
|
||||||
|
Text::new(
|
||||||
|
model.display_name.clone(),
|
||||||
|
appearance.ui_font_family(),
|
||||||
|
CONTENT_FONT_SIZE,
|
||||||
|
)
|
||||||
|
.with_color(appearance.theme().active_ui_text_color().into())
|
||||||
|
.with_style(Properties::default().weight(Weight::Semibold))
|
||||||
|
.finish(),
|
||||||
|
)
|
||||||
|
.with_child(
|
||||||
|
Text::new(
|
||||||
|
model.model_id.clone(),
|
||||||
|
appearance.monospace_font_family(),
|
||||||
|
10.,
|
||||||
|
)
|
||||||
|
.with_color(appearance.theme().nonactive_ui_text_color().into())
|
||||||
|
.finish(),
|
||||||
|
)
|
||||||
|
.finish();
|
||||||
|
vec![
|
||||||
|
Flex::row().with_child(toggle).finish(),
|
||||||
|
model_info,
|
||||||
|
text(openai_model_context_size(model, provider.kind).to_string()),
|
||||||
|
text(Self::format_openai_model_details(model, &provider)),
|
||||||
|
]
|
||||||
|
},
|
||||||
appearance,
|
appearance,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_bedrock_model_catalog(
|
fn render_bedrock_model_catalog(
|
||||||
|
&self,
|
||||||
models: &[BedrockModelConfig],
|
models: &[BedrockModelConfig],
|
||||||
appearance: &Appearance,
|
appearance: &Appearance,
|
||||||
) -> Box<dyn Element> {
|
) -> Box<dyn Element> {
|
||||||
let rows = models
|
if models.is_empty() {
|
||||||
|
return Self::render_model_catalog(
|
||||||
|
Vec::new(),
|
||||||
|
"No Bedrock models passed AWS access checks.",
|
||||||
|
appearance,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
let switches = models
|
||||||
.iter()
|
.iter()
|
||||||
.map(|model| {
|
.map(|model| {
|
||||||
let mut details = Vec::new();
|
self.bedrock_model_switches
|
||||||
if model.vision_supported {
|
.borrow_mut()
|
||||||
details.push("Images".to_string());
|
.entry(model.model_id.clone())
|
||||||
}
|
.or_default()
|
||||||
Self::render_model_row(
|
.clone()
|
||||||
model.display_name.clone(),
|
|
||||||
model.model_id.clone(),
|
|
||||||
if details.is_empty() {
|
|
||||||
"Available".to_string()
|
|
||||||
} else {
|
|
||||||
details.join(" · ")
|
|
||||||
},
|
|
||||||
appearance,
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
Self::render_model_catalog(rows, "No Bedrock models discovered yet.", appearance)
|
let models = models.to_vec();
|
||||||
|
model_catalog_table(
|
||||||
|
self.bedrock_model_table_state.clone(),
|
||||||
|
models.len(),
|
||||||
|
move |index, app| {
|
||||||
|
let appearance = Appearance::as_ref(app);
|
||||||
|
let model = &models[index];
|
||||||
|
let id = model.model_id.clone();
|
||||||
|
let toggle = appearance
|
||||||
|
.ui_builder()
|
||||||
|
.switch(switches[index].clone())
|
||||||
|
.check(model.enabled)
|
||||||
|
.build()
|
||||||
|
.on_click(move |ctx, _, _| {
|
||||||
|
ctx.dispatch_typed_action(AISettingsPageAction::ToggleBedrockModel(
|
||||||
|
id.clone(),
|
||||||
|
))
|
||||||
|
})
|
||||||
|
.finish();
|
||||||
|
let text = |value: String| {
|
||||||
|
Text::new(value, appearance.ui_font_family(), 11.)
|
||||||
|
.with_color(appearance.theme().nonactive_ui_text_color().into())
|
||||||
|
.soft_wrap(true)
|
||||||
|
.finish()
|
||||||
|
};
|
||||||
|
let name = Flex::column()
|
||||||
|
.with_spacing(3.)
|
||||||
|
.with_child(
|
||||||
|
Text::new(
|
||||||
|
model.display_name.clone(),
|
||||||
|
appearance.ui_font_family(),
|
||||||
|
CONTENT_FONT_SIZE,
|
||||||
|
)
|
||||||
|
.with_color(appearance.theme().active_ui_text_color().into())
|
||||||
|
.with_style(Properties::default().weight(Weight::Semibold))
|
||||||
|
.finish(),
|
||||||
|
)
|
||||||
|
.with_child(
|
||||||
|
Text::new(
|
||||||
|
model.model_id.clone(),
|
||||||
|
appearance.monospace_font_family(),
|
||||||
|
10.,
|
||||||
|
)
|
||||||
|
.with_color(appearance.theme().nonactive_ui_text_color().into())
|
||||||
|
.finish(),
|
||||||
|
)
|
||||||
|
.finish();
|
||||||
|
let context = galaxy_bedrock_model_catalog::model_metadata(&model.model_id)
|
||||||
|
.and_then(|metadata| metadata.context_window_tokens)
|
||||||
|
.map(|tokens| tokens.to_string())
|
||||||
|
.unwrap_or_else(|| "Unknown".to_owned());
|
||||||
|
let details = if !LLMPreferences::as_ref(app)
|
||||||
|
.bedrock_model_is_verified(&model.model_id, app)
|
||||||
|
{
|
||||||
|
"Awaiting AWS access verification"
|
||||||
|
} else if model.vision_supported {
|
||||||
|
"Images"
|
||||||
|
} else {
|
||||||
|
"Text"
|
||||||
|
};
|
||||||
|
vec![
|
||||||
|
Flex::row().with_child(toggle).finish(),
|
||||||
|
name,
|
||||||
|
text(context),
|
||||||
|
text(details.to_owned()),
|
||||||
|
]
|
||||||
|
},
|
||||||
|
appearance,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_acp_model_catalog(
|
fn render_acp_model_catalog(
|
||||||
@@ -6876,7 +7036,8 @@ impl ProviderSettingsWidget {
|
|||||||
.with_child(header)
|
.with_child(header)
|
||||||
.with_child(summary)
|
.with_child(summary)
|
||||||
.with_child(enablement)
|
.with_child(enablement)
|
||||||
.with_child(Self::render_openai_model_catalog(provider, appearance))
|
.with_cross_axis_alignment(CrossAxisAlignment::Stretch)
|
||||||
|
.with_child(self.render_openai_model_catalog(provider_index, provider, appearance))
|
||||||
.finish(),
|
.finish(),
|
||||||
)
|
)
|
||||||
.with_padding(Padding::uniform(16.))
|
.with_padding(Padding::uniform(16.))
|
||||||
@@ -6956,7 +7117,7 @@ impl ProviderSettingsWidget {
|
|||||||
)
|
)
|
||||||
.finish(),
|
.finish(),
|
||||||
)
|
)
|
||||||
.with_child(Self::render_bedrock_model_catalog(
|
.with_child(self.render_bedrock_model_catalog(
|
||||||
AISettings::as_ref(app).bedrock_models.value(),
|
AISettings::as_ref(app).bedrock_models.value(),
|
||||||
appearance,
|
appearance,
|
||||||
))
|
))
|
||||||
@@ -7146,6 +7307,7 @@ impl ProviderSettingsWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let mut section = Flex::column()
|
let mut section = Flex::column()
|
||||||
|
.with_cross_axis_alignment(CrossAxisAlignment::Stretch)
|
||||||
.with_spacing(10.)
|
.with_spacing(10.)
|
||||||
.with_child(header.finish())
|
.with_child(header.finish())
|
||||||
.with_child(
|
.with_child(
|
||||||
@@ -7196,7 +7358,9 @@ impl SettingsWidget for ProviderSettingsWidget {
|
|||||||
let settings = AISettings::as_ref(app);
|
let settings = AISettings::as_ref(app);
|
||||||
let providers = settings.openai_providers.value();
|
let providers = settings.openai_providers.value();
|
||||||
|
|
||||||
let mut column = Flex::column().with_spacing(18.);
|
let mut column = Flex::column()
|
||||||
|
.with_cross_axis_alignment(CrossAxisAlignment::Stretch)
|
||||||
|
.with_spacing(18.);
|
||||||
|
|
||||||
let is_setup_visible = view
|
let is_setup_visible = view
|
||||||
.inline_provider_setup
|
.inline_provider_setup
|
||||||
|
|||||||
@@ -3,7 +3,8 @@ use galaxy_core::ui::theme::Fill;
|
|||||||
use galaxyui::elements::{
|
use galaxyui::elements::{
|
||||||
Border, ChildView, ClippedScrollStateHandle, ClippedScrollable, ConstrainedBox, Container,
|
Border, ChildView, ClippedScrollStateHandle, ClippedScrollable, ConstrainedBox, Container,
|
||||||
CornerRadius, CrossAxisAlignment, Flex, FormattedTextElement, Hoverable, MainAxisAlignment,
|
CornerRadius, CrossAxisAlignment, Flex, FormattedTextElement, Hoverable, MainAxisAlignment,
|
||||||
MainAxisSize, MouseStateHandle, Padding, ParentElement, Radius, ScrollbarWidth, Text,
|
MainAxisSize, MouseStateHandle, Padding, ParentElement, Radius, RowBackground, ScrollbarWidth,
|
||||||
|
Table, TableColumnWidth, TableConfig, TableHeader, TableStateHandle, TableVerticalSizing, Text,
|
||||||
};
|
};
|
||||||
use galaxyui::fonts::{Properties, Weight};
|
use galaxyui::fonts::{Properties, Weight};
|
||||||
use galaxyui::platform::Cursor;
|
use galaxyui::platform::Cursor;
|
||||||
@@ -16,6 +17,50 @@ use galaxyui::{
|
|||||||
};
|
};
|
||||||
use pathfinder_color::ColorU;
|
use pathfinder_color::ColorU;
|
||||||
|
|
||||||
|
pub(super) fn model_catalog_table(
|
||||||
|
state: TableStateHandle,
|
||||||
|
row_count: usize,
|
||||||
|
render_row: impl Fn(usize, &AppContext) -> Vec<Box<dyn Element>> + 'static,
|
||||||
|
appearance: &Appearance,
|
||||||
|
) -> Box<dyn Element> {
|
||||||
|
let headers = ["Use", "Model", "Context", "Capabilities"]
|
||||||
|
.into_iter()
|
||||||
|
.enumerate()
|
||||||
|
.map(|(index, label)| {
|
||||||
|
TableHeader::new(
|
||||||
|
Text::new(label.to_owned(), appearance.ui_font_family(), 11.)
|
||||||
|
.with_color(appearance.theme().nonactive_ui_text_color().into())
|
||||||
|
.with_style(Properties::default().weight(Weight::Semibold))
|
||||||
|
.finish(),
|
||||||
|
)
|
||||||
|
.with_width(if index == 3 {
|
||||||
|
TableColumnWidth::Flex(1.)
|
||||||
|
} else {
|
||||||
|
TableColumnWidth::Intrinsic
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
state.invalidate_all_intrinsic_widths();
|
||||||
|
Table::new(state, 0., 0.)
|
||||||
|
.with_headers(headers)
|
||||||
|
.with_row_count(row_count)
|
||||||
|
.with_row_render_fn(render_row)
|
||||||
|
.with_config(TableConfig {
|
||||||
|
border_width: 1.,
|
||||||
|
border_color: appearance.theme().outline().into(),
|
||||||
|
outer_border: true,
|
||||||
|
column_dividers: false,
|
||||||
|
row_dividers: true,
|
||||||
|
cell_padding: 12.,
|
||||||
|
header_background: appearance.theme().surface_2().into(),
|
||||||
|
row_background: RowBackground::uniform(appearance.theme().surface_1().into()),
|
||||||
|
fixed_header: false,
|
||||||
|
vertical_sizing: TableVerticalSizing::ExpandToContent,
|
||||||
|
measure_body_cells_for_intrinsic_widths: true,
|
||||||
|
})
|
||||||
|
.finish()
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(not(target_family = "wasm"))]
|
#[cfg(not(target_family = "wasm"))]
|
||||||
use crate::ai::chatgpt_auth::{ChatGPTAuthModel, ChatGPTAuthModelEvent, ChatGPTAuthState};
|
use crate::ai::chatgpt_auth::{ChatGPTAuthModel, ChatGPTAuthModelEvent, ChatGPTAuthState};
|
||||||
use crate::ai::harness_display;
|
use crate::ai::harness_display;
|
||||||
@@ -32,8 +77,8 @@ use crate::ui_components::icons::Icon;
|
|||||||
use crate::view_components::action_button::{
|
use crate::view_components::action_button::{
|
||||||
ActionButton, ButtonSize, NakedTheme, PrimaryTheme, SecondaryTheme,
|
ActionButton, ButtonSize, NakedTheme, PrimaryTheme, SecondaryTheme,
|
||||||
};
|
};
|
||||||
|
use crate::view_components::{Dropdown, DropdownItem};
|
||||||
|
|
||||||
const SETUP_WIDTH: f32 = 900.;
|
|
||||||
const INPUT_FONT_SIZE: f32 = 12.;
|
const INPUT_FONT_SIZE: f32 = 12.;
|
||||||
const MODEL_LOGO_SIZE: f32 = 20.;
|
const MODEL_LOGO_SIZE: f32 = 20.;
|
||||||
|
|
||||||
@@ -171,6 +216,7 @@ pub enum ProviderSetupViewAction {
|
|||||||
CycleModelCapability(usize, CapabilityKey),
|
CycleModelCapability(usize, CapabilityKey),
|
||||||
ConnectChatGPT,
|
ConnectChatGPT,
|
||||||
SelectBedrockAuth(BedrockAuthMethod),
|
SelectBedrockAuth(BedrockAuthMethod),
|
||||||
|
SelectBedrockProfile(String),
|
||||||
ToggleBedrockCrossRegion,
|
ToggleBedrockCrossRegion,
|
||||||
ToggleBedrockAutoLogin,
|
ToggleBedrockAutoLogin,
|
||||||
SelectAcpAgent(String),
|
SelectAcpAgent(String),
|
||||||
@@ -197,7 +243,8 @@ pub struct ProviderSetupView {
|
|||||||
api_key_editor: ViewHandle<EditorView>,
|
api_key_editor: ViewHandle<EditorView>,
|
||||||
project_id_editor: ViewHandle<EditorView>,
|
project_id_editor: ViewHandle<EditorView>,
|
||||||
location_editor: ViewHandle<EditorView>,
|
location_editor: ViewHandle<EditorView>,
|
||||||
bedrock_profile_editor: ViewHandle<EditorView>,
|
bedrock_profile_dropdown: ViewHandle<Dropdown<ProviderSetupViewAction>>,
|
||||||
|
bedrock_region_request: usize,
|
||||||
bedrock_region_editor: ViewHandle<EditorView>,
|
bedrock_region_editor: ViewHandle<EditorView>,
|
||||||
bedrock_refresh_command_editor: ViewHandle<EditorView>,
|
bedrock_refresh_command_editor: ViewHandle<EditorView>,
|
||||||
bedrock_access_key_editor: ViewHandle<EditorView>,
|
bedrock_access_key_editor: ViewHandle<EditorView>,
|
||||||
@@ -210,11 +257,13 @@ pub struct ProviderSetupView {
|
|||||||
bedrock_cross_region_toggle: SwitchStateHandle,
|
bedrock_cross_region_toggle: SwitchStateHandle,
|
||||||
bedrock_auto_login_toggle: SwitchStateHandle,
|
bedrock_auto_login_toggle: SwitchStateHandle,
|
||||||
model_switches: Vec<SwitchStateHandle>,
|
model_switches: Vec<SwitchStateHandle>,
|
||||||
|
bedrock_model_switches: Vec<SwitchStateHandle>,
|
||||||
model_capability_switches: Vec<[SwitchStateHandle; 2]>,
|
model_capability_switches: Vec<[SwitchStateHandle; 2]>,
|
||||||
model_capability_buttons: Vec<Vec<ViewHandle<ActionButton>>>,
|
model_capability_buttons: Vec<Vec<ViewHandle<ActionButton>>>,
|
||||||
model_context_editors: Vec<ViewHandle<EditorView>>,
|
model_context_editors: Vec<ViewHandle<EditorView>>,
|
||||||
provider_type_scroll_state: ClippedScrollStateHandle,
|
provider_type_scroll_state: ClippedScrollStateHandle,
|
||||||
models_scroll_state: ClippedScrollStateHandle,
|
models_scroll_state: ClippedScrollStateHandle,
|
||||||
|
model_table_state: TableStateHandle,
|
||||||
step_tab_mouse_states: Vec<MouseStateHandle>,
|
step_tab_mouse_states: Vec<MouseStateHandle>,
|
||||||
back_button: ViewHandle<ActionButton>,
|
back_button: ViewHandle<ActionButton>,
|
||||||
cancel_button: ViewHandle<ActionButton>,
|
cancel_button: ViewHandle<ActionButton>,
|
||||||
@@ -245,8 +294,9 @@ impl ProviderSetupView {
|
|||||||
let api_key_editor = Self::create_editor("sk-... (optional)", true, ctx);
|
let api_key_editor = Self::create_editor("sk-... (optional)", true, ctx);
|
||||||
let project_id_editor = Self::create_editor("my-google-cloud-project", false, ctx);
|
let project_id_editor = Self::create_editor("my-google-cloud-project", false, ctx);
|
||||||
let location_editor = Self::create_editor("global", false, ctx);
|
let location_editor = Self::create_editor("global", false, ctx);
|
||||||
let bedrock_profile_editor = Self::create_editor("default", false, ctx);
|
let bedrock_profile_dropdown = ctx.add_typed_action_view(Dropdown::new);
|
||||||
let bedrock_region_editor = Self::create_editor("us-east-1", false, ctx);
|
let bedrock_region_editor =
|
||||||
|
Self::create_editor("Region from AWS profile (or enter an override)", false, ctx);
|
||||||
let bedrock_refresh_command_editor = Self::create_editor("aws sso login", false, ctx);
|
let bedrock_refresh_command_editor = Self::create_editor("aws sso login", false, ctx);
|
||||||
let bedrock_access_key_editor = Self::create_editor("AKIA...", false, ctx);
|
let bedrock_access_key_editor = Self::create_editor("AKIA...", false, ctx);
|
||||||
let bedrock_secret_key_editor = Self::create_editor("Secret access key", true, ctx);
|
let bedrock_secret_key_editor = Self::create_editor("Secret access key", true, ctx);
|
||||||
@@ -332,7 +382,6 @@ impl ProviderSetupView {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
for (editor, update) in [
|
for (editor, update) in [
|
||||||
(bedrock_profile_editor.clone(), 0),
|
|
||||||
(bedrock_region_editor.clone(), 1),
|
(bedrock_region_editor.clone(), 1),
|
||||||
(bedrock_refresh_command_editor.clone(), 2),
|
(bedrock_refresh_command_editor.clone(), 2),
|
||||||
(bedrock_access_key_editor.clone(), 3),
|
(bedrock_access_key_editor.clone(), 3),
|
||||||
@@ -345,7 +394,6 @@ impl ProviderSetupView {
|
|||||||
if matches!(event, EditorEvent::Edited(_)) {
|
if matches!(event, EditorEvent::Edited(_)) {
|
||||||
let value = editor.as_ref(ctx).buffer_text(ctx);
|
let value = editor.as_ref(ctx).buffer_text(ctx);
|
||||||
match update {
|
match update {
|
||||||
0 => me.draft_bedrock.profile = value,
|
|
||||||
1 => me.draft_bedrock.region = value,
|
1 => me.draft_bedrock.region = value,
|
||||||
2 => me.draft_bedrock.auth_refresh_command = value,
|
2 => me.draft_bedrock.auth_refresh_command = value,
|
||||||
3 => me.draft_bedrock.access_key_id = value,
|
3 => me.draft_bedrock.access_key_id = value,
|
||||||
@@ -430,7 +478,8 @@ impl ProviderSetupView {
|
|||||||
api_key_editor,
|
api_key_editor,
|
||||||
project_id_editor,
|
project_id_editor,
|
||||||
location_editor,
|
location_editor,
|
||||||
bedrock_profile_editor,
|
bedrock_profile_dropdown,
|
||||||
|
bedrock_region_request: 0,
|
||||||
bedrock_region_editor,
|
bedrock_region_editor,
|
||||||
bedrock_refresh_command_editor,
|
bedrock_refresh_command_editor,
|
||||||
bedrock_access_key_editor,
|
bedrock_access_key_editor,
|
||||||
@@ -443,11 +492,13 @@ impl ProviderSetupView {
|
|||||||
bedrock_cross_region_toggle: SwitchStateHandle::default(),
|
bedrock_cross_region_toggle: SwitchStateHandle::default(),
|
||||||
bedrock_auto_login_toggle: SwitchStateHandle::default(),
|
bedrock_auto_login_toggle: SwitchStateHandle::default(),
|
||||||
model_switches: Vec::new(),
|
model_switches: Vec::new(),
|
||||||
|
bedrock_model_switches: Vec::new(),
|
||||||
model_capability_switches: Vec::new(),
|
model_capability_switches: Vec::new(),
|
||||||
model_capability_buttons: Vec::new(),
|
model_capability_buttons: Vec::new(),
|
||||||
model_context_editors: Vec::new(),
|
model_context_editors: Vec::new(),
|
||||||
provider_type_scroll_state: ClippedScrollStateHandle::default(),
|
provider_type_scroll_state: ClippedScrollStateHandle::default(),
|
||||||
models_scroll_state: ClippedScrollStateHandle::default(),
|
models_scroll_state: ClippedScrollStateHandle::default(),
|
||||||
|
model_table_state: TableStateHandle::new(0, |_, _| Vec::new()),
|
||||||
step_tab_mouse_states: (0..4).map(|_| MouseStateHandle::default()).collect(),
|
step_tab_mouse_states: (0..4).map(|_| MouseStateHandle::default()).collect(),
|
||||||
back_button,
|
back_button,
|
||||||
cancel_button,
|
cancel_button,
|
||||||
@@ -544,6 +595,9 @@ impl ProviderSetupView {
|
|||||||
};
|
};
|
||||||
self.discovery_state = DiscoveryState::Idle;
|
self.discovery_state = DiscoveryState::Idle;
|
||||||
self.sync_editors(ctx);
|
self.sync_editors(ctx);
|
||||||
|
if self.provider_type == ProviderSetupProviderType::Bedrock {
|
||||||
|
self.load_bedrock_profile_region(ctx);
|
||||||
|
}
|
||||||
self.sync_provider_type_buttons(ctx);
|
self.sync_provider_type_buttons(ctx);
|
||||||
self.sync_acp_agent_buttons(ctx);
|
self.sync_acp_agent_buttons(ctx);
|
||||||
self.sync_bedrock_auth_buttons(ctx);
|
self.sync_bedrock_auth_buttons(ctx);
|
||||||
@@ -601,6 +655,9 @@ impl ProviderSetupView {
|
|||||||
self.draft_models.clear();
|
self.draft_models.clear();
|
||||||
self.discovery_state = DiscoveryState::Idle;
|
self.discovery_state = DiscoveryState::Idle;
|
||||||
self.sync_editors(ctx);
|
self.sync_editors(ctx);
|
||||||
|
if self.draft_bedrock.region.is_empty() {
|
||||||
|
self.load_bedrock_profile_region(ctx);
|
||||||
|
}
|
||||||
self.sync_provider_type_buttons(ctx);
|
self.sync_provider_type_buttons(ctx);
|
||||||
self.sync_bedrock_auth_buttons(ctx);
|
self.sync_bedrock_auth_buttons(ctx);
|
||||||
self.sync_model_switches(ctx);
|
self.sync_model_switches(ctx);
|
||||||
@@ -652,6 +709,43 @@ impl ProviderSetupView {
|
|||||||
ctx.notify();
|
ctx.notify();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn load_bedrock_profile_region(&mut self, ctx: &mut ViewContext<Self>) {
|
||||||
|
self.bedrock_region_request += 1;
|
||||||
|
#[cfg(not(target_family = "wasm"))]
|
||||||
|
{
|
||||||
|
use aws_config::meta::region::ProvideRegion;
|
||||||
|
let request = self.bedrock_region_request;
|
||||||
|
let profile = self.draft_bedrock.profile.clone();
|
||||||
|
let requested_profile = profile.clone();
|
||||||
|
let previous_region = self.draft_bedrock.region.clone();
|
||||||
|
ctx.spawn(
|
||||||
|
async move {
|
||||||
|
let provider = aws_config::profile::ProfileFileRegionProvider::builder()
|
||||||
|
.profile_name(profile)
|
||||||
|
.build();
|
||||||
|
ProvideRegion::region(&provider)
|
||||||
|
.await
|
||||||
|
.map(|region| region.as_ref().to_owned())
|
||||||
|
},
|
||||||
|
move |me, region, ctx| {
|
||||||
|
if request != me.bedrock_region_request
|
||||||
|
|| me.provider_type != ProviderSetupProviderType::Bedrock
|
||||||
|
|| me.draft_bedrock.profile != requested_profile
|
||||||
|
|| me.draft_bedrock.region != previous_region
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
me.draft_bedrock.region = region.unwrap_or_default();
|
||||||
|
me.bedrock_region_editor.update(ctx, |editor, ctx| {
|
||||||
|
editor.system_reset_buffer_text(&me.draft_bedrock.region, ctx);
|
||||||
|
});
|
||||||
|
me.update_next_button(ctx);
|
||||||
|
ctx.notify();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn sync_editors(&self, ctx: &mut ViewContext<Self>) {
|
fn sync_editors(&self, ctx: &mut ViewContext<Self>) {
|
||||||
self.name_editor.update(ctx, |editor, ctx| {
|
self.name_editor.update(ctx, |editor, ctx| {
|
||||||
editor.system_reset_buffer_text(&self.draft_name, ctx);
|
editor.system_reset_buffer_text(&self.draft_name, ctx);
|
||||||
@@ -668,8 +762,30 @@ impl ProviderSetupView {
|
|||||||
self.location_editor.update(ctx, |editor, ctx| {
|
self.location_editor.update(ctx, |editor, ctx| {
|
||||||
editor.system_reset_buffer_text(&self.draft_location, ctx);
|
editor.system_reset_buffer_text(&self.draft_location, ctx);
|
||||||
});
|
});
|
||||||
self.bedrock_profile_editor.update(ctx, |editor, ctx| {
|
let mut profiles = vec!["default".to_owned()];
|
||||||
editor.system_reset_buffer_text(&self.draft_bedrock.profile, ctx);
|
#[cfg(not(target_family = "wasm"))]
|
||||||
|
profiles.extend(crate::ai::provider::external_config::list_aws_profiles());
|
||||||
|
profiles.push(self.draft_bedrock.profile.clone());
|
||||||
|
profiles.retain(|profile| !profile.is_empty());
|
||||||
|
profiles.sort();
|
||||||
|
profiles.dedup();
|
||||||
|
self.bedrock_profile_dropdown.update(ctx, |dropdown, ctx| {
|
||||||
|
dropdown.set_items(
|
||||||
|
profiles
|
||||||
|
.into_iter()
|
||||||
|
.map(|profile| {
|
||||||
|
DropdownItem::new(
|
||||||
|
profile.clone(),
|
||||||
|
ProviderSetupViewAction::SelectBedrockProfile(profile),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
ctx,
|
||||||
|
);
|
||||||
|
dropdown.set_selected_by_action(
|
||||||
|
ProviderSetupViewAction::SelectBedrockProfile(self.draft_bedrock.profile.clone()),
|
||||||
|
ctx,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
self.bedrock_region_editor.update(ctx, |editor, ctx| {
|
self.bedrock_region_editor.update(ctx, |editor, ctx| {
|
||||||
editor.system_reset_buffer_text(&self.draft_bedrock.region, ctx);
|
editor.system_reset_buffer_text(&self.draft_bedrock.region, ctx);
|
||||||
@@ -740,6 +856,8 @@ impl ProviderSetupView {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn sync_model_switches(&mut self, ctx: &mut ViewContext<Self>) {
|
fn sync_model_switches(&mut self, ctx: &mut ViewContext<Self>) {
|
||||||
|
self.bedrock_model_switches
|
||||||
|
.resize_with(self.draft_bedrock.models.len(), SwitchStateHandle::default);
|
||||||
while self.model_switches.len() < self.draft_models.len() {
|
while self.model_switches.len() < self.draft_models.len() {
|
||||||
self.model_switches.push(SwitchStateHandle::default());
|
self.model_switches.push(SwitchStateHandle::default());
|
||||||
}
|
}
|
||||||
@@ -959,13 +1077,30 @@ impl ProviderSetupView {
|
|||||||
session_token: None,
|
session_token: None,
|
||||||
cross_region_inference: self.draft_bedrock.cross_region_inference,
|
cross_region_inference: self.draft_bedrock.cross_region_inference,
|
||||||
};
|
};
|
||||||
|
let requested_config = config.clone();
|
||||||
ctx.spawn(
|
ctx.spawn(
|
||||||
async move {
|
async move {
|
||||||
crate::ai::provider::discovery::discover_available_models(config).await
|
crate::ai::provider::discovery::discover_available_models(config).await
|
||||||
},
|
},
|
||||||
move |me, result, ctx| match result {
|
move |me, result, ctx| match result {
|
||||||
Ok(models) => {
|
Ok(discovery) => {
|
||||||
me.draft_bedrock.models = models;
|
for record in discovery.records {
|
||||||
|
crate::ai::remote_logging::log_view_event(ctx, record);
|
||||||
|
}
|
||||||
|
let models = discovery.models;
|
||||||
|
LLMPreferences::handle(ctx).update(ctx, |preferences, ctx| {
|
||||||
|
preferences.record_bedrock_verification(
|
||||||
|
requested_config,
|
||||||
|
&models,
|
||||||
|
ctx,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
me.draft_bedrock.models =
|
||||||
|
crate::ai::provider::discovery::preserve_model_toggles(
|
||||||
|
&me.draft_bedrock.models,
|
||||||
|
models,
|
||||||
|
);
|
||||||
|
me.sync_model_switches(ctx);
|
||||||
me.discovery_state = DiscoveryState::Idle;
|
me.discovery_state = DiscoveryState::Idle;
|
||||||
me.step = ProviderSetupStep::Models;
|
me.step = ProviderSetupStep::Models;
|
||||||
me.update_next_button(ctx);
|
me.update_next_button(ctx);
|
||||||
@@ -1152,6 +1287,7 @@ impl ProviderSetupView {
|
|||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
let list = Flex::column()
|
let list = Flex::column()
|
||||||
|
.with_cross_axis_alignment(CrossAxisAlignment::Stretch)
|
||||||
.with_spacing(10.)
|
.with_spacing(10.)
|
||||||
.with_children(cards)
|
.with_children(cards)
|
||||||
.finish();
|
.finish();
|
||||||
@@ -1359,11 +1495,8 @@ impl ProviderSetupView {
|
|||||||
);
|
);
|
||||||
match self.draft_bedrock.auth_method {
|
match self.draft_bedrock.auth_method {
|
||||||
BedrockAuthMethod::Profile | BedrockAuthMethod::Sso => {
|
BedrockAuthMethod::Profile | BedrockAuthMethod::Sso => {
|
||||||
children.push(self.render_input(
|
children.push(Self::render_label(appearance, "AWS profile"));
|
||||||
appearance,
|
children.push(ChildView::new(&self.bedrock_profile_dropdown).finish());
|
||||||
"AWS profile",
|
|
||||||
&self.bedrock_profile_editor,
|
|
||||||
));
|
|
||||||
if self.draft_bedrock.auth_method == BedrockAuthMethod::Sso {
|
if self.draft_bedrock.auth_method == BedrockAuthMethod::Sso {
|
||||||
children.push(self.render_input(
|
children.push(self.render_input(
|
||||||
appearance,
|
appearance,
|
||||||
@@ -1518,6 +1651,7 @@ impl ProviderSetupView {
|
|||||||
) -> Box<dyn galaxyui::Element> {
|
) -> Box<dyn galaxyui::Element> {
|
||||||
let list = Flex::column()
|
let list = Flex::column()
|
||||||
.with_spacing(spacing)
|
.with_spacing(spacing)
|
||||||
|
.with_cross_axis_alignment(CrossAxisAlignment::Stretch)
|
||||||
.with_children(rows)
|
.with_children(rows)
|
||||||
.finish();
|
.finish();
|
||||||
let scrollable = ClippedScrollable::vertical(
|
let scrollable = ClippedScrollable::vertical(
|
||||||
@@ -1539,7 +1673,6 @@ impl ProviderSetupView {
|
|||||||
.with_corner_radius(CornerRadius::with_all(Radius::Pixels(6.)))
|
.with_corner_radius(CornerRadius::with_all(Radius::Pixels(6.)))
|
||||||
.finish(),
|
.finish(),
|
||||||
)
|
)
|
||||||
.with_width(SETUP_WIDTH - 56.)
|
|
||||||
.with_max_height(430.)
|
.with_max_height(430.)
|
||||||
.finish()
|
.finish()
|
||||||
}
|
}
|
||||||
@@ -1588,47 +1721,9 @@ impl ProviderSetupView {
|
|||||||
.finish()
|
.finish()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_model_table_header(&self, appearance: &Appearance) -> Box<dyn galaxyui::Element> {
|
|
||||||
let header = |label: &str| {
|
|
||||||
Text::new(label.to_owned(), appearance.ui_font_family(), 11.)
|
|
||||||
.with_color(appearance.theme().nonactive_ui_text_color().into())
|
|
||||||
.with_style(Properties::default().weight(Weight::Semibold))
|
|
||||||
.finish()
|
|
||||||
};
|
|
||||||
Container::new(
|
|
||||||
Flex::row()
|
|
||||||
.with_spacing(12.)
|
|
||||||
.with_cross_axis_alignment(CrossAxisAlignment::Center)
|
|
||||||
.with_child(ConstrainedBox::new(header("Use")).with_width(52.).finish())
|
|
||||||
.with_child(
|
|
||||||
ConstrainedBox::new(header("Model"))
|
|
||||||
.with_width(250.)
|
|
||||||
.finish(),
|
|
||||||
)
|
|
||||||
.with_child(
|
|
||||||
ConstrainedBox::new(header("Context"))
|
|
||||||
.with_width(140.)
|
|
||||||
.finish(),
|
|
||||||
)
|
|
||||||
.with_child(
|
|
||||||
ConstrainedBox::new(header("Capabilities"))
|
|
||||||
.with_width(330.)
|
|
||||||
.finish(),
|
|
||||||
)
|
|
||||||
.finish(),
|
|
||||||
)
|
|
||||||
.with_padding(Padding::uniform(10.).with_vertical(9.))
|
|
||||||
.with_background(appearance.theme().surface_2())
|
|
||||||
.with_border(Border::bottom(1.).with_border_fill(appearance.theme().outline()))
|
|
||||||
.finish()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn render_model_capabilities(
|
fn render_model_capabilities(
|
||||||
&self,
|
buttons: &[ViewHandle<ActionButton>],
|
||||||
appearance: &Appearance,
|
|
||||||
index: usize,
|
|
||||||
) -> Box<dyn galaxyui::Element> {
|
) -> Box<dyn galaxyui::Element> {
|
||||||
let buttons = &self.model_capability_buttons[index];
|
|
||||||
let first_row = buttons[..3]
|
let first_row = buttons[..3]
|
||||||
.iter()
|
.iter()
|
||||||
.map(|button| ChildView::new(button).finish())
|
.map(|button| ChildView::new(button).finish())
|
||||||
@@ -1654,12 +1749,14 @@ impl ProviderSetupView {
|
|||||||
.finish()
|
.finish()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_openai_model_row(
|
fn render_openai_model_cells(
|
||||||
&self,
|
|
||||||
appearance: &Appearance,
|
appearance: &Appearance,
|
||||||
index: usize,
|
index: usize,
|
||||||
model: &OpenAIModelConfig,
|
model: &OpenAIModelConfig,
|
||||||
) -> Box<dyn galaxyui::Element> {
|
switch: SwitchStateHandle,
|
||||||
|
context_editor: ViewHandle<EditorView>,
|
||||||
|
buttons: &[ViewHandle<ActionButton>],
|
||||||
|
) -> Vec<Box<dyn Element>> {
|
||||||
let model_info = Flex::column()
|
let model_info = Flex::column()
|
||||||
.with_spacing(4.)
|
.with_spacing(4.)
|
||||||
.with_child(
|
.with_child(
|
||||||
@@ -1679,16 +1776,9 @@ impl ProviderSetupView {
|
|||||||
.finish(),
|
.finish(),
|
||||||
)
|
)
|
||||||
.finish();
|
.finish();
|
||||||
let model_info = Flex::row()
|
|
||||||
.with_cross_axis_alignment(CrossAxisAlignment::Center)
|
|
||||||
.with_spacing(10.)
|
|
||||||
.with_child(self.render_model_logo())
|
|
||||||
.with_child(model_info)
|
|
||||||
.finish();
|
|
||||||
|
|
||||||
let context_input = appearance
|
let context_input = appearance
|
||||||
.ui_builder()
|
.ui_builder()
|
||||||
.text_input(self.model_context_editors[index].clone())
|
.text_input(context_editor)
|
||||||
.with_style(UiComponentStyles {
|
.with_style(UiComponentStyles {
|
||||||
padding: Some(Coords {
|
padding: Some(Coords {
|
||||||
top: 8.,
|
top: 8.,
|
||||||
@@ -1702,94 +1792,77 @@ impl ProviderSetupView {
|
|||||||
.build()
|
.build()
|
||||||
.finish();
|
.finish();
|
||||||
|
|
||||||
Container::new(
|
let toggle = appearance
|
||||||
Flex::row()
|
.ui_builder()
|
||||||
.with_spacing(12.)
|
.switch(switch)
|
||||||
.with_cross_axis_alignment(CrossAxisAlignment::Center)
|
.check(model.enabled)
|
||||||
.with_child(
|
.build()
|
||||||
ConstrainedBox::new(
|
.on_click(move |ctx, _, _| {
|
||||||
appearance
|
ctx.dispatch_typed_action(ProviderSetupViewAction::ToggleModel(index))
|
||||||
.ui_builder()
|
})
|
||||||
.switch(self.model_switches[index].clone())
|
.finish();
|
||||||
.check(model.enabled)
|
vec![
|
||||||
.build()
|
Flex::row().with_child(toggle).finish(),
|
||||||
.on_click(move |ctx, _, _| {
|
model_info,
|
||||||
ctx.dispatch_typed_action(ProviderSetupViewAction::ToggleModel(
|
ConstrainedBox::new(context_input).with_width(100.).finish(),
|
||||||
index,
|
Self::render_model_capabilities(buttons),
|
||||||
));
|
]
|
||||||
})
|
|
||||||
.finish(),
|
|
||||||
)
|
|
||||||
.with_width(52.)
|
|
||||||
.finish(),
|
|
||||||
)
|
|
||||||
.with_child(ConstrainedBox::new(model_info).with_width(250.).finish())
|
|
||||||
.with_child(ConstrainedBox::new(context_input).with_width(140.).finish())
|
|
||||||
.with_child(
|
|
||||||
ConstrainedBox::new(self.render_model_capabilities(appearance, index))
|
|
||||||
.with_width(330.)
|
|
||||||
.finish(),
|
|
||||||
)
|
|
||||||
.finish(),
|
|
||||||
)
|
|
||||||
.with_padding(Padding::uniform(12.).with_vertical(10.))
|
|
||||||
.with_border(Border::bottom(1.).with_border_fill(appearance.theme().outline()))
|
|
||||||
.finish()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_models(&self, appearance: &Appearance) -> Box<dyn galaxyui::Element> {
|
fn render_models(&self, appearance: &Appearance) -> Box<dyn galaxyui::Element> {
|
||||||
if self.provider_type == ProviderSetupProviderType::Bedrock {
|
if self.provider_type == ProviderSetupProviderType::Bedrock {
|
||||||
let rows = self
|
let models = self.draft_bedrock.models.clone();
|
||||||
.draft_bedrock
|
let switches = self.bedrock_model_switches.clone();
|
||||||
.models
|
let table = model_catalog_table(
|
||||||
.iter()
|
self.model_table_state.clone(),
|
||||||
.map(|model| {
|
models.len(),
|
||||||
Flex::row()
|
move |index, app| {
|
||||||
.with_cross_axis_alignment(CrossAxisAlignment::Center)
|
let appearance = Appearance::as_ref(app);
|
||||||
.with_spacing(10.)
|
let model = &models[index];
|
||||||
.with_child(self.render_model_logo())
|
let text = |value: String| {
|
||||||
.with_child(
|
Text::new(value, appearance.ui_font_family(), 11.)
|
||||||
Flex::column()
|
.with_color(appearance.theme().nonactive_ui_text_color().into())
|
||||||
.with_spacing(2.)
|
.finish()
|
||||||
.with_child(
|
};
|
||||||
Text::new(
|
let toggle = appearance
|
||||||
model.display_name.clone(),
|
.ui_builder()
|
||||||
appearance.ui_font_family(),
|
.switch(switches[index].clone())
|
||||||
INPUT_FONT_SIZE,
|
.check(model.enabled)
|
||||||
)
|
.build()
|
||||||
.with_color(appearance.theme().active_ui_text_color().into())
|
.on_click(move |ctx, _, _| {
|
||||||
.finish(),
|
ctx.dispatch_typed_action(ProviderSetupViewAction::ToggleModel(index))
|
||||||
)
|
})
|
||||||
.with_child(
|
.finish();
|
||||||
Text::new(
|
let name = Flex::column()
|
||||||
model.model_id.clone(),
|
.with_spacing(3.)
|
||||||
appearance.monospace_font_family(),
|
.with_child(text(model.display_name.clone()))
|
||||||
10.,
|
.with_child(text(model.model_id.clone()))
|
||||||
)
|
.finish();
|
||||||
.with_color(appearance.theme().nonactive_ui_text_color().into())
|
let context = galaxy_bedrock_model_catalog::model_metadata(&model.model_id)
|
||||||
.soft_wrap(true)
|
.and_then(|metadata| metadata.context_window_tokens)
|
||||||
.finish(),
|
.map(|tokens| tokens.to_string())
|
||||||
)
|
.unwrap_or_else(|| "Unknown".to_owned());
|
||||||
.finish(),
|
vec![
|
||||||
)
|
Flex::row().with_child(toggle).finish(),
|
||||||
.finish()
|
name,
|
||||||
})
|
text(context),
|
||||||
.collect::<Vec<_>>();
|
text(
|
||||||
return Flex::column()
|
if model.vision_supported {
|
||||||
.with_spacing(12.)
|
"Images"
|
||||||
|
} else {
|
||||||
|
"Text"
|
||||||
|
}
|
||||||
|
.to_owned(),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
},
|
||||||
|
appearance,
|
||||||
|
);
|
||||||
|
return Flex::column().with_cross_axis_alignment(CrossAxisAlignment::Stretch).with_spacing(12.)
|
||||||
.with_child(self.render_input(appearance, "Connection name", &self.name_editor))
|
.with_child(self.render_input(appearance, "Connection name", &self.name_editor))
|
||||||
.with_child(
|
.with_child(Text::new("Only models passing AWS agreement, authorization, entitlement, and region checks are offered. Choose which models Galaxy should enable.", appearance.ui_font_family(), INPUT_FONT_SIZE)
|
||||||
Text::new(
|
.with_color(appearance.theme().nonactive_ui_text_color().into()).soft_wrap(true).finish())
|
||||||
"These models passed AWS Bedrock availability checks. Model selection is managed by discovery and cannot be edited manually.",
|
.with_child(self.render_model_table(appearance, vec![table], 0.)).finish();
|
||||||
appearance.ui_font_family(),
|
|
||||||
INPUT_FONT_SIZE,
|
|
||||||
)
|
|
||||||
.with_color(appearance.theme().nonactive_ui_text_color().into())
|
|
||||||
.soft_wrap(true)
|
|
||||||
.finish(),
|
|
||||||
)
|
|
||||||
.with_child(self.render_model_table(appearance, rows, 10.))
|
|
||||||
.finish();
|
|
||||||
}
|
}
|
||||||
if self.provider_type == ProviderSetupProviderType::Acp {
|
if self.provider_type == ProviderSetupProviderType::Acp {
|
||||||
let option_rows = self
|
let option_rows = self
|
||||||
@@ -1873,17 +1946,29 @@ impl ProviderSetupView {
|
|||||||
.with_child(catalog)
|
.with_child(catalog)
|
||||||
.finish();
|
.finish();
|
||||||
}
|
}
|
||||||
let mut rows = vec![self.render_model_table_header(appearance)];
|
let models = self.draft_models.clone();
|
||||||
rows.extend(
|
let switches = self.model_switches.clone();
|
||||||
self.draft_models
|
let editors = self.model_context_editors.clone();
|
||||||
.iter()
|
let buttons = self.model_capability_buttons.clone();
|
||||||
.enumerate()
|
let table = model_catalog_table(
|
||||||
.map(|(index, model)| self.render_openai_model_row(appearance, index, model)),
|
self.model_table_state.clone(),
|
||||||
|
models.len(),
|
||||||
|
move |index, app| {
|
||||||
|
Self::render_openai_model_cells(
|
||||||
|
Appearance::as_ref(app),
|
||||||
|
index,
|
||||||
|
&models[index],
|
||||||
|
switches[index].clone(),
|
||||||
|
editors[index].clone(),
|
||||||
|
&buttons[index],
|
||||||
|
)
|
||||||
|
},
|
||||||
|
appearance,
|
||||||
);
|
);
|
||||||
|
let table = self.render_model_table(appearance, vec![table], 0.);
|
||||||
let table = self.render_model_table(appearance, rows, 0.);
|
|
||||||
|
|
||||||
Flex::column()
|
Flex::column()
|
||||||
|
.with_cross_axis_alignment(CrossAxisAlignment::Stretch)
|
||||||
.with_spacing(12.)
|
.with_spacing(12.)
|
||||||
.with_child(self.render_input(appearance, "Connection name", &self.name_editor))
|
.with_child(self.render_input(appearance, "Connection name", &self.name_editor))
|
||||||
.with_child(
|
.with_child(
|
||||||
@@ -2101,6 +2186,7 @@ impl View for ProviderSetupView {
|
|||||||
ProviderSetupStep::Models => self.render_models(appearance),
|
ProviderSetupStep::Models => self.render_models(appearance),
|
||||||
};
|
};
|
||||||
Flex::column()
|
Flex::column()
|
||||||
|
.with_cross_axis_alignment(CrossAxisAlignment::Stretch)
|
||||||
.with_spacing(20.)
|
.with_spacing(20.)
|
||||||
.with_child(self.render_step_indicator(appearance))
|
.with_child(self.render_step_indicator(appearance))
|
||||||
.with_child(content)
|
.with_child(content)
|
||||||
@@ -2126,6 +2212,11 @@ impl TypedActionView for ProviderSetupView {
|
|||||||
self.draft_api_key = None;
|
self.draft_api_key = None;
|
||||||
}
|
}
|
||||||
self.sync_editors(ctx);
|
self.sync_editors(ctx);
|
||||||
|
if self.provider_type == ProviderSetupProviderType::Bedrock
|
||||||
|
&& self.draft_bedrock.region.is_empty()
|
||||||
|
{
|
||||||
|
self.load_bedrock_profile_region(ctx);
|
||||||
|
}
|
||||||
self.sync_provider_type_buttons(ctx);
|
self.sync_provider_type_buttons(ctx);
|
||||||
self.sync_bedrock_auth_buttons(ctx);
|
self.sync_bedrock_auth_buttons(ctx);
|
||||||
self.update_next_button(ctx);
|
self.update_next_button(ctx);
|
||||||
@@ -2217,7 +2308,17 @@ impl TypedActionView for ProviderSetupView {
|
|||||||
ctx.notify();
|
ctx.notify();
|
||||||
}
|
}
|
||||||
ProviderSetupViewAction::ToggleModel(index) => {
|
ProviderSetupViewAction::ToggleModel(index) => {
|
||||||
if let Some(model) = self.draft_models.get_mut(*index) {
|
let models = if self.provider_type == ProviderSetupProviderType::Bedrock {
|
||||||
|
&mut self.draft_bedrock.models
|
||||||
|
} else {
|
||||||
|
if let Some(model) = self.draft_models.get_mut(*index) {
|
||||||
|
model.enabled = !model.enabled;
|
||||||
|
self.update_next_button(ctx);
|
||||||
|
ctx.notify();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
if let Some(model) = models.get_mut(*index) {
|
||||||
model.enabled = !model.enabled;
|
model.enabled = !model.enabled;
|
||||||
self.update_next_button(ctx);
|
self.update_next_button(ctx);
|
||||||
ctx.notify();
|
ctx.notify();
|
||||||
@@ -2237,6 +2338,34 @@ impl TypedActionView for ProviderSetupView {
|
|||||||
#[cfg(not(target_family = "wasm"))]
|
#[cfg(not(target_family = "wasm"))]
|
||||||
ChatGPTAuthModel::handle(ctx).update(ctx, |model, ctx| model.connect(ctx));
|
ChatGPTAuthModel::handle(ctx).update(ctx, |model, ctx| model.connect(ctx));
|
||||||
}
|
}
|
||||||
|
ProviderSetupViewAction::SelectBedrockProfile(profile) => {
|
||||||
|
if self.draft_bedrock.profile != *profile {
|
||||||
|
self.draft_bedrock.profile = profile.clone();
|
||||||
|
self.draft_bedrock.region.clear();
|
||||||
|
self.draft_bedrock.models.clear();
|
||||||
|
self.discovery_state = DiscoveryState::Idle;
|
||||||
|
self.bedrock_region_editor.update(ctx, |editor, ctx| {
|
||||||
|
editor.system_reset_buffer_text("", ctx);
|
||||||
|
});
|
||||||
|
// The dropdown dispatches this action while it is borrowed.
|
||||||
|
// Update its selected label only after that handler returns.
|
||||||
|
let selected_profile = profile.clone();
|
||||||
|
ctx.spawn(async {}, move |me, _, ctx| {
|
||||||
|
if me.draft_bedrock.profile != selected_profile {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
me.bedrock_profile_dropdown.update(ctx, |dropdown, ctx| {
|
||||||
|
dropdown.set_selected_by_action(
|
||||||
|
ProviderSetupViewAction::SelectBedrockProfile(selected_profile),
|
||||||
|
ctx,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
self.load_bedrock_profile_region(ctx);
|
||||||
|
self.update_next_button(ctx);
|
||||||
|
ctx.notify();
|
||||||
|
}
|
||||||
|
}
|
||||||
ProviderSetupViewAction::SelectBedrockAuth(method) => {
|
ProviderSetupViewAction::SelectBedrockAuth(method) => {
|
||||||
self.draft_bedrock.auth_method = *method;
|
self.draft_bedrock.auth_method = *method;
|
||||||
self.sync_bedrock_auth_buttons(ctx);
|
self.sync_bedrock_auth_buttons(ctx);
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ const MAX_FILES_IN_CLI_ROTATION: usize = 10;
|
|||||||
const CLI_LOG_SUBDIRECTORY: &str = "oz";
|
const CLI_LOG_SUBDIRECTORY: &str = "oz";
|
||||||
const SESSION_LOG_SUBDIRECTORY: &str = "session-logs";
|
const SESSION_LOG_SUBDIRECTORY: &str = "session-logs";
|
||||||
const TEMP_LOG_FILE_SUFFIX: &str = "old.temp";
|
const TEMP_LOG_FILE_SUFFIX: &str = "old.temp";
|
||||||
|
const BEDROCK_DISCOVERY_LOG_TARGET: &str = "galaxy::ai::provider::discovery";
|
||||||
const INPUT_CLASSIFIER_LOG_TARGET: &str = "input_classifier";
|
const INPUT_CLASSIFIER_LOG_TARGET: &str = "input_classifier";
|
||||||
const FILE_WATCHER_LOG_TARGET: &str = "notify";
|
const FILE_WATCHER_LOG_TARGET: &str = "notify";
|
||||||
const FILE_WATCHER_DEBOUNCER_LOG_TARGET: &str = "notify_debouncer_full";
|
const FILE_WATCHER_DEBOUNCER_LOG_TARGET: &str = "notify_debouncer_full";
|
||||||
@@ -589,6 +590,9 @@ fn init_internal(
|
|||||||
base_logger
|
base_logger
|
||||||
.filter(Some("naga"), LevelFilter::Warn)
|
.filter(Some("naga"), LevelFilter::Warn)
|
||||||
.filter(Some("wgpu_core"), LevelFilter::Warn)
|
.filter(Some("wgpu_core"), LevelFilter::Warn)
|
||||||
|
// Bedrock access checks contain model IDs and approval statuses only.
|
||||||
|
// Retain the complete audit even when full-session logging is disabled.
|
||||||
|
.filter(Some(BEDROCK_DISCOVERY_LOG_TARGET), LevelFilter::Info)
|
||||||
// ANSI rendering emits trace records for every character and terminal control sequence.
|
// ANSI rendering emits trace records for every character and terminal control sequence.
|
||||||
// Preserve meaningful info/warnings/errors without filling full-session logs with typing,
|
// Preserve meaningful info/warnings/errors without filling full-session logs with typing,
|
||||||
// carriage-return, linefeed, cursor-motion, and scrolling noise.
|
// carriage-return, linefeed, cursor-motion, and scrolling noise.
|
||||||
|
|||||||
Reference in New Issue
Block a user