diff --git a/Cargo.lock b/Cargo.lock index bce322a502d9..05b3fe976a83 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -678,6 +678,31 @@ dependencies = [ "uuid", ] +[[package]] +name = "aws-sdk-bedrock" +version = "1.146.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd3d870793928e6c18de8273dcdceed08af8cc9083be9d7de3e52efc7ac01e9e" +dependencies = [ + "arc-swap", + "aws-credential-types", + "aws-runtime", + "aws-smithy-async", + "aws-smithy-http", + "aws-smithy-json", + "aws-smithy-observability", + "aws-smithy-runtime", + "aws-smithy-runtime-api", + "aws-smithy-types", + "aws-types", + "bytes", + "fastrand", + "http 0.2.12", + "http 1.4.2", + "regex-lite", + "tracing", +] + [[package]] name = "aws-sdk-bedrockruntime" version = "1.133.0" @@ -4876,6 +4901,7 @@ dependencies = [ "async-trait", "aws-config", "aws-lc-rs", + "aws-sdk-bedrock", "aws-sdk-bedrockruntime", "aws-sdk-sagemakerruntime", "aws-smithy-types", diff --git a/crates/goose-cli/src/commands/configure.rs b/crates/goose-cli/src/commands/configure.rs index bb51673d1af3..d6a0cd6ba71e 100644 --- a/crates/goose-cli/src/commands/configure.rs +++ b/crates/goose-cli/src/commands/configure.rs @@ -811,10 +811,8 @@ pub async fn configure_provider_dialog() -> anyhow::Result { let spin = spinner(); spin.start("Checking your configuration..."); - let toolshim_enabled = std::env::var("GOOSE_TOOLSHIM") - .map(|val| val == "1" || val.to_lowercase() == "true") - .unwrap_or(false); - let toolshim_model = std::env::var("GOOSE_TOOLSHIM_OLLAMA_MODEL").ok(); + let (toolshim_enabled, toolshim_model) = + goose::providers::provider_test::toolshim_settings_from_env(); match test_provider_configuration(provider_name, &model, toolshim_enabled, toolshim_model).await { diff --git a/crates/goose-server/src/routes/agent.rs b/crates/goose-server/src/routes/agent.rs index 20c91d0db61b..a7ea390f890e 100644 --- a/crates/goose-server/src/routes/agent.rs +++ b/crates/goose-server/src/routes/agent.rs @@ -17,6 +17,7 @@ use goose::agents::ExtensionConfig; use goose::config::resolve_extensions_for_new_session; use goose::config::{Config, GooseMode}; use goose::providers::create; +use goose::providers::provider_test::test_provider_model; use goose::recipe::Recipe; use goose::recipe_deeplink; use goose::session::session_manager::SessionType; @@ -591,6 +592,11 @@ async fn update_agent_provider( if let Some(request_params) = payload.request_params { model_config = model_config.with_merged_request_params(request_params); } + + test_provider_model(&payload.provider, &model) + .await + .map_err(|e| (StatusCode::BAD_REQUEST, e.to_string()))?; + let model_info = resolve_provider_model_info(&payload.provider, &model) .await .map_err(|e| (e.status, e.message))?; diff --git a/crates/goose-server/src/routes/config_management.rs b/crates/goose-server/src/routes/config_management.rs index 0f65ef7aa873..bb7a354533dd 100644 --- a/crates/goose-server/src/routes/config_management.rs +++ b/crates/goose-server/src/routes/config_management.rs @@ -19,6 +19,7 @@ use goose::providers::catalog::{ ProviderTemplate, }; use goose::providers::create_with_default_model; +use goose::providers::provider_test::test_provider_model; use goose::providers::providers as get_providers; use goose::{ agents::execute_commands, agents::ExtensionConfig, slash_commands::recipe_slash_command, @@ -108,6 +109,8 @@ fn normalize_custom_provider_api_key(api_key: String) -> Option { #[derive(Deserialize, ToSchema)] pub struct CheckProviderRequest { pub provider: String, + #[serde(default)] + pub model: Option, } #[derive(Deserialize, ToSchema)] @@ -827,8 +830,20 @@ pub async fn update_custom_provider( request_body = CheckProviderRequest, )] pub async fn check_provider( - Json(CheckProviderRequest { provider }): Json, + Json(CheckProviderRequest { provider, model }): Json, ) -> Result<(), ErrorResponse> { + if let Some(model) = model.filter(|model| !model.trim().is_empty()) { + test_provider_model(&provider, &model) + .await + .map_err(|err| { + ErrorResponse::bad_request(format!( + "Provider '{}' with model '{}' check failed: {}", + provider, model, err + )) + })?; + return Ok(()); + } + create_with_default_model(&provider, Vec::new()) .await .map_err(|err| { @@ -845,19 +860,22 @@ pub async fn check_provider( pub async fn set_config_provider( Json(SetProviderRequest { provider, model }): Json, ) -> Result<(), ErrorResponse> { - create_with_default_model(&provider, Vec::new()) + test_provider_model(&provider, &model) .await - .and_then(|_| { - let config = Config::global(); - goose::config::set_active_provider(config, &provider, &model) - .map_err(|e| anyhow::anyhow!(e)) - }) .map_err(|err| { ErrorResponse::bad_request(format!( "Failed to set provider to '{}' with model '{}': {}", provider, model, err )) })?; + + let config = Config::global(); + goose::config::set_active_provider(config, &provider, &model).map_err(|err| { + ErrorResponse::bad_request(format!( + "Failed to set provider to '{}' with model '{}': {}", + provider, model, err + )) + })?; Ok(()) } diff --git a/crates/goose/Cargo.toml b/crates/goose/Cargo.toml index 0f0fdd25fca8..aaccc2efb5de 100644 --- a/crates/goose/Cargo.toml +++ b/crates/goose/Cargo.toml @@ -35,6 +35,7 @@ local-inference = [ aws-providers = [ "dep:aws-config", "dep:aws-smithy-types", + "dep:aws-sdk-bedrock", "dep:aws-sdk-bedrockruntime", "dep:aws-sdk-sagemakerruntime", "dep:smithy-transport-reqwest", @@ -234,6 +235,7 @@ llama-cpp-sys-2 = { workspace = true, optional = true } image = { version = "0.24.9", default-features = false, features = ["png", "jpeg", "gif", "webp"] } subtle = { version = "2.5", default-features = false, features = ["std"] } gethostname = "1.1.0" +aws-sdk-bedrock = { version = "1.132", default-features = false, features = ["rt-tokio"], optional = true } [target.'cfg(target_os = "windows")'.dependencies] winapi = { workspace = true } diff --git a/crates/goose/src/acp/server/config.rs b/crates/goose/src/acp/server/config.rs index e70ddf98dfb1..69c81d245547 100644 --- a/crates/goose/src/acp/server/config.rs +++ b/crates/goose/src/acp/server/config.rs @@ -215,7 +215,8 @@ impl GooseAcpAgent { } if let Some(model_id) = model_id.as_deref() { - let model_exists = entry.default_model == model_id + let model_exists = entry.supports_refresh + || entry.default_model == model_id || entry.models.iter().any(|model| model.id == model_id); if !model_exists { return Err(agent_client_protocol::Error::invalid_params().data(format!( diff --git a/crates/goose/src/providers/bedrock.rs b/crates/goose/src/providers/bedrock.rs index a9ffd0b9300c..dfc68e649439 100644 --- a/crates/goose/src/providers/bedrock.rs +++ b/crates/goose/src/providers/bedrock.rs @@ -1,4 +1,4 @@ -use std::collections::HashMap; +use std::collections::{BTreeSet, HashMap}; use super::base::{ConfigKey, MessageStream, Provider, ProviderDef, ProviderMetadata}; use super::openai_compatible::{handle_status, stream_responses_compat}; @@ -8,11 +8,16 @@ use crate::session_context::SESSION_ID_HEADER; use anyhow::Result; use async_stream::try_stream; use async_trait::async_trait; +use aws_sdk_bedrock::types::{ + FoundationModelLifecycleStatus, FoundationModelSummary, InferenceProfileStatus, + InferenceProfileType, InferenceType, ModelModality, +}; +use aws_sdk_bedrock::Client as BedrockControlClient; use aws_sdk_bedrockruntime::config::ProvideCredentials; use aws_sdk_bedrockruntime::operation::converse::ConverseError; use aws_sdk_bedrockruntime::operation::converse_stream::ConverseStreamError; use aws_sdk_bedrockruntime::types::error::ConverseStreamOutputError; -use aws_sdk_bedrockruntime::{types as bedrock, Client}; +use aws_sdk_bedrockruntime::{types as bedrock, Client as BedrockRuntimeClient}; use base64::Engine; use futures::future::BoxFuture; use goose_providers::conversation::token_usage::{ProviderUsage, Usage}; @@ -36,15 +41,8 @@ pub const BEDROCK_DOC_LINK: &str = "https://docs.aws.amazon.com/bedrock/latest/userguide/models-supported.html"; pub const BEDROCK_DEFAULT_MODEL: &str = "us.anthropic.claude-sonnet-4-5-20250929-v1:0"; -pub const BEDROCK_KNOWN_MODELS: &[&str] = &[ - "us.anthropic.claude-sonnet-4-5-20250929-v1:0", - "us.anthropic.claude-sonnet-4-20250514-v1:0", - "us.anthropic.claude-3-7-sonnet-20250219-v1:0", - "us.anthropic.claude-opus-4-20250514-v1:0", - "us.anthropic.claude-opus-4-1-20250805-v1:0", - "openai.gpt-5.5", - "openai.gpt-5.4", -]; +pub const BEDROCK_MANTLE_MODELS: &[&str] = &["openai.gpt-5.5", "openai.gpt-5.4"]; +pub const BEDROCK_BOOTSTRAP_MODELS: &[&str] = &[BEDROCK_DEFAULT_MODEL]; pub const BEDROCK_DEFAULT_MAX_RETRIES: usize = 6; pub const BEDROCK_DEFAULT_INITIAL_RETRY_INTERVAL_MS: u64 = 2000; @@ -54,7 +52,9 @@ pub const BEDROCK_DEFAULT_MAX_RETRY_INTERVAL_MS: u64 = 120_000; #[derive(Debug, serde::Serialize)] pub struct BedrockProvider { #[serde(skip)] - client: Client, + client: BedrockRuntimeClient, + #[serde(skip)] + control_plane_client: BedrockControlClient, #[serde(skip)] retry_config: RetryConfig, #[serde(skip)] @@ -163,15 +163,26 @@ impl BedrockProvider { )) .build(); - Client::from_conf(bedrock_config) + BedrockRuntimeClient::from_conf(bedrock_config) + } else { + Self::create_runtime_client_with_credentials(&sdk_config).await? + }; + + let control_plane_client = if let Some(ref token) = bearer_token { + let bedrock_config = aws_sdk_bedrock::Config::new(&sdk_config) + .to_builder() + .bearer_token(aws_sdk_bedrock::config::Token::new(token.clone(), None)) + .build(); + BedrockControlClient::from_conf(bedrock_config) } else { - Self::create_client_with_credentials(&sdk_config).await? + BedrockControlClient::new(&sdk_config) }; let retry_config = Self::load_retry_config(config); Ok(Self { client, + control_plane_client, retry_config, name: BEDROCK_PROVIDER_NAME.to_string(), region: resolved_region, @@ -181,7 +192,9 @@ impl BedrockProvider { }) } - async fn create_client_with_credentials(sdk_config: &aws_config::SdkConfig) -> Result { + async fn create_runtime_client_with_credentials( + sdk_config: &aws_config::SdkConfig, + ) -> Result { sdk_config .credentials_provider() .ok_or_else(|| anyhow::anyhow!("No AWS credentials provider configured"))? @@ -194,7 +207,152 @@ impl BedrockProvider { ) })?; - Ok(Client::new(sdk_config)) + Ok(BedrockRuntimeClient::new(sdk_config)) + } + + fn bootstrap_models(include_mantle: bool) -> Vec { + let mut models: Vec = BEDROCK_BOOTSTRAP_MODELS + .iter() + .map(|model| model.to_string()) + .collect(); + if include_mantle { + models.extend(BEDROCK_MANTLE_MODELS.iter().map(|model| model.to_string())); + } + models + } + + fn merge_discovered_model_ids( + inference_profiles: impl IntoIterator, + foundation_models: impl IntoIterator, + extra_models: impl IntoIterator, + ) -> Vec { + let mut models = BTreeSet::new(); + models.extend(inference_profiles); + models.extend(foundation_models); + models.extend(extra_models); + models.into_iter().collect() + } + + fn is_excluded_non_chat_model_id(model_id: &str) -> bool { + let id = model_id.to_lowercase(); + id.contains(".embed") + || id.contains("-embed-") + || id.ends_with("-embed") + || id.contains(".rerank") + || id.contains("-rerank-") + || id.ends_with("-rerank") + } + + fn is_chat_capable_model_id(model_id: &str) -> bool { + !Self::is_excluded_non_chat_model_id(model_id) + } + + fn is_chat_capable_foundation_model(summary: &FoundationModelSummary) -> bool { + if !Self::is_chat_capable_model_id(summary.model_id()) { + return false; + } + + let has_text_input = summary.input_modalities().contains(&ModelModality::Text); + let has_text_output = summary.output_modalities().contains(&ModelModality::Text); + let has_embedding_output = summary + .output_modalities() + .contains(&ModelModality::Embedding); + let streaming = summary.response_streaming_supported().unwrap_or(false); + let not_legacy = summary + .model_lifecycle() + .map(|lifecycle| lifecycle.status() != &FoundationModelLifecycleStatus::Legacy) + .unwrap_or(true); + + has_text_input && has_text_output && !has_embedding_output && streaming && not_legacy + } + + fn is_mantle_model_id(model_id: &str) -> bool { + BEDROCK_MANTLE_MODELS.contains(&model_id) + } + + async fn fetch_inference_profile_ids(&self) -> Result, ProviderError> { + let mut ids = BTreeSet::new(); + + for profile_type in [ + InferenceProfileType::SystemDefined, + InferenceProfileType::Application, + ] { + let mut next_token = None; + loop { + let mut request = self + .control_plane_client + .list_inference_profiles() + .type_equals(profile_type.clone()); + if let Some(token) = &next_token { + request = request.next_token(token); + } + + let response = request.send().await.map_err(|err| { + ProviderError::ExecutionError(format!( + "Failed to list Bedrock inference profiles: {}", + err + )) + })?; + + for summary in response.inference_profile_summaries() { + if summary.status() == &InferenceProfileStatus::Active { + let id = summary.inference_profile_id(); + if Self::is_chat_capable_model_id(id) { + ids.insert(id.to_string()); + } + } + } + + next_token = response.next_token().map(|token| token.to_string()); + if next_token.is_none() { + break; + } + } + } + + Ok(ids.into_iter().collect()) + } + + async fn fetch_foundation_model_ids(&self) -> Result, ProviderError> { + let response = self + .control_plane_client + .list_foundation_models() + .by_inference_type(InferenceType::OnDemand) + .by_output_modality(ModelModality::Text) + .send() + .await + .map_err(|err| { + ProviderError::ExecutionError(format!( + "Failed to list Bedrock foundation models: {}", + err + )) + })?; + + Ok(response + .model_summaries() + .iter() + .filter(|summary| Self::is_chat_capable_foundation_model(summary)) + .map(|summary| summary.model_id().to_string()) + .collect()) + } + + async fn fetch_models_from_aws(&self) -> Result, ProviderError> { + let inference_profiles = self.fetch_inference_profile_ids().await?; + let foundation_models = self.fetch_foundation_model_ids().await?; + let extra_models = if self.bearer_token.is_some() { + BEDROCK_MANTLE_MODELS + .iter() + .map(|model| model.to_string()) + .collect() + } else { + Vec::new() + }; + + Ok(Self::merge_discovered_model_ids( + inference_profiles, + foundation_models, + extra_models, + )) } fn load_retry_config(config: &crate::config::Config) -> RetryConfig { @@ -402,6 +560,12 @@ impl BedrockProvider { "Bedrock validation error: {}", err.message().unwrap_or("unknown validation error") )), + ConverseError::ResourceNotFoundException(err) => { + ProviderError::ExecutionError(format!( + "Bedrock model not found or not accessible: {}", + err.message().unwrap_or("unknown resource error") + )) + } ConverseError::ModelErrorException(err) => { ProviderError::ExecutionError(format!("Failed to call Bedrock: {:?}", err)) } @@ -494,6 +658,18 @@ impl BedrockProvider { err )) } + ConverseStreamError::ValidationException(err) => { + ProviderError::ExecutionError(format!( + "Bedrock validation error: {}", + err.message().unwrap_or("unknown validation error") + )) + } + ConverseStreamError::ResourceNotFoundException(err) => { + ProviderError::ExecutionError(format!( + "Bedrock model not found or not accessible: {}", + err.message().unwrap_or("unknown resource error") + )) + } ConverseStreamError::ModelErrorException(err) => { ProviderError::ExecutionError(format!("Failed to call Bedrock: {:?}", err)) } @@ -689,9 +865,9 @@ impl goose_providers::base::ProviderDescriptor for BedrockProvider { ProviderMetadata::new( BEDROCK_PROVIDER_NAME, "Amazon Bedrock", - "Run models through Amazon Bedrock. Supports AWS SSO profiles - run 'aws sso login --profile ' before using. Configure with AWS_PROFILE and AWS_REGION, use environment variables/credentials, or use AWS_BEARER_TOKEN_BEDROCK for bearer token authentication. Region is required for bearer token auth (can be set via AWS_REGION, AWS_DEFAULT_REGION, or AWS profile). Prompt caching can be enabled for Anthropic Claude models by setting BEDROCK_ENABLE_CACHING=true. Responses stream via the ConverseStream API; set BEDROCK_DISABLE_STREAMING=true to fall back to blocking Converse calls.", + "Run models through Amazon Bedrock. Supports AWS SSO profiles - run 'aws sso login --profile ' before using. Configure with AWS_PROFILE and AWS_REGION, use environment variables/credentials, or use AWS_BEARER_TOKEN_BEDROCK for bearer token authentication. Region is required for bearer token auth (can be set via AWS_REGION, AWS_DEFAULT_REGION, or AWS profile). Model discovery requires bedrock:ListFoundationModels and bedrock:ListInferenceProfiles permissions. Prompt caching can be enabled for Anthropic Claude models by setting BEDROCK_ENABLE_CACHING=true. Responses stream via the ConverseStream API; set BEDROCK_DISABLE_STREAMING=true to fall back to blocking Converse calls.", BEDROCK_DEFAULT_MODEL, - BEDROCK_KNOWN_MODELS.to_vec(), + BEDROCK_BOOTSTRAP_MODELS.to_vec(), BEDROCK_DOC_LINK, vec![ ConfigKey::new("AWS_PROFILE", false, false, Some("default"), true), @@ -731,8 +907,25 @@ impl Provider for BedrockProvider { self.retry_config.clone() } + fn skip_canonical_filtering(&self) -> bool { + true + } + async fn fetch_supported_models(&self) -> Result, ProviderError> { - Ok(BEDROCK_KNOWN_MODELS.iter().map(|s| s.to_string()).collect()) + match self.fetch_models_from_aws().await { + Ok(models) if !models.is_empty() => Ok(models), + Ok(_) => { + tracing::debug!("Bedrock model discovery returned no models, using bootstrap list"); + Ok(Self::bootstrap_models(self.bearer_token.is_some())) + } + Err(err) => { + tracing::warn!( + "Bedrock model discovery failed ({}), using bootstrap list", + err + ); + Ok(Self::bootstrap_models(self.bearer_token.is_some())) + } + } } async fn stream( @@ -756,7 +949,7 @@ impl Provider for BedrockProvider { let (base_name, effort) = extract_reasoning_effort(without_prefix); let bedrock_model_id = format!("openai.{}", base_name); - let is_mantle_model = BEDROCK_KNOWN_MODELS.contains(&bedrock_model_id.as_str()); + let is_mantle_model = Self::is_mantle_model_id(&bedrock_model_id); if is_mantle_model { let mut normalized_config = ModelConfig { @@ -919,11 +1112,23 @@ mod tests { .behavior_version(aws_config::BehaviorVersion::latest()) .region(aws_config::Region::new("us-east-1")) .build(); - let client = Client::new(&sdk_config); + let client = BedrockRuntimeClient::new(&sdk_config); + let control_plane_client = BedrockControlClient::new(&sdk_config); + let model = ModelConfig { + model_name: model_name.to_string(), + context_limit: None, + temperature: None, + max_tokens: None, + toolshim: false, + toolshim_model: None, + request_params: None, + reasoning: None, + }; ( BedrockProvider { client, + control_plane_client, retry_config: RetryConfig::default(), name: "aws_bedrock".to_string(), region: None, @@ -931,16 +1136,7 @@ mod tests { http_client: reqwest::Client::new(), mantle_base_url: None, }, - ModelConfig { - model_name: model_name.to_string(), - context_limit: None, - temperature: None, - max_tokens: None, - toolshim: false, - toolshim_model: None, - request_params: None, - reasoning: None, - }, + model, ) } @@ -1006,6 +1202,21 @@ mod tests { ); } + #[test] + #[serial] + fn test_caching_disabled_by_default() { + std::env::set_var("BEDROCK_ENABLE_CACHING", "false"); + + let (provider, model) = + create_mock_provider_and_model("us.anthropic.claude-sonnet-4-5-20250929-v1:0"); + assert!( + !provider.should_enable_caching(&model), + "Caching should be disabled by default" + ); + + std::env::remove_var("BEDROCK_ENABLE_CACHING"); + } + #[test] fn test_caching_disabled_for_non_claude_models() { let (provider, model) = create_mock_provider_and_model("amazon.titan-text-express-v1"); @@ -1093,9 +1304,19 @@ mod tests { .region(aws_config::Region::new("us-east-1")) .build(); - let model = ModelConfig::new("openai.gpt-5.5"); + let model = ModelConfig { + model_name: "openai.gpt-5.5".to_string(), + context_limit: None, + temperature: None, + max_tokens: None, + toolshim: false, + toolshim_model: None, + request_params: None, + reasoning: None, + }; let provider = BedrockProvider { - client: Client::new(&sdk_config), + client: BedrockRuntimeClient::new(&sdk_config), + control_plane_client: BedrockControlClient::new(&sdk_config), retry_config: RetryConfig::default(), name: "aws_bedrock".to_string(), region: Some("us-east-1".to_string()), @@ -1497,4 +1718,116 @@ mod tests { other => panic!("expected RedactedThinking, got {:?}", other), } } + + #[test] + fn test_merge_discovered_model_ids_dedupes_and_sorts() { + let models = BedrockProvider::merge_discovered_model_ids( + [ + "eu.anthropic.claude-sonnet-4-5-20250929-v1:0".to_string(), + "us.anthropic.claude-sonnet-4-5-20250929-v1:0".to_string(), + ], + [ + "anthropic.claude-3-5-sonnet-20240620-v1:0".to_string(), + "eu.anthropic.claude-sonnet-4-5-20250929-v1:0".to_string(), + ], + ["openai.gpt-5.5".to_string()], + ); + + assert_eq!( + models, + vec![ + "anthropic.claude-3-5-sonnet-20240620-v1:0".to_string(), + "eu.anthropic.claude-sonnet-4-5-20250929-v1:0".to_string(), + "openai.gpt-5.5".to_string(), + "us.anthropic.claude-sonnet-4-5-20250929-v1:0".to_string(), + ] + ); + } + + #[test] + fn test_bootstrap_models_includes_mantle_with_bearer_token() { + let without_mantle = BedrockProvider::bootstrap_models(false); + assert_eq!(without_mantle, vec![BEDROCK_DEFAULT_MODEL.to_string()]); + + let with_mantle = BedrockProvider::bootstrap_models(true); + assert!(with_mantle.contains(&"openai.gpt-5.5".to_string())); + assert!(with_mantle.contains(&BEDROCK_DEFAULT_MODEL.to_string())); + } + + #[test] + fn test_is_mantle_model_id() { + assert!(BedrockProvider::is_mantle_model_id("openai.gpt-5.5")); + assert!(BedrockProvider::is_mantle_model_id("openai.gpt-5.4")); + assert!(!BedrockProvider::is_mantle_model_id( + "openai.gpt-oss-120b-1:0" + )); + assert!(!BedrockProvider::is_mantle_model_id( + "us.anthropic.claude-sonnet-4-5-20250929-v1:0" + )); + } + + #[test] + fn test_skip_canonical_filtering_enabled() { + let (provider, _) = create_mock_provider_and_model("test"); + assert!(provider.skip_canonical_filtering()); + } + + #[test] + fn test_is_chat_capable_foundation_model_filters_embeddings() { + let chat_model = FoundationModelSummary::builder() + .model_arn("arn:aws:bedrock:us-east-1::foundation-model/anthropic.claude-3-5-sonnet-20240620-v1:0") + .model_id("anthropic.claude-3-5-sonnet-20240620-v1:0") + .input_modalities(ModelModality::Text) + .output_modalities(ModelModality::Text) + .response_streaming_supported(true) + .build() + .unwrap(); + assert!(BedrockProvider::is_chat_capable_foundation_model( + &chat_model + )); + + let embedding_model = FoundationModelSummary::builder() + .model_arn("arn:aws:bedrock:us-east-1::foundation-model/amazon.titan-embed-text-v2:0") + .model_id("amazon.titan-embed-text-v2:0") + .input_modalities(ModelModality::Text) + .output_modalities(ModelModality::Embedding) + .response_streaming_supported(false) + .build() + .unwrap(); + assert!(!BedrockProvider::is_chat_capable_foundation_model( + &embedding_model + )); + + let cohere_embed_v4 = FoundationModelSummary::builder() + .model_arn("arn:aws:bedrock:us-east-1::foundation-model/cohere.embed-v4:0") + .model_id("cohere.embed-v4:0") + .input_modalities(ModelModality::Text) + .output_modalities(ModelModality::Text) + .output_modalities(ModelModality::Embedding) + .response_streaming_supported(true) + .build() + .unwrap(); + assert!(!BedrockProvider::is_chat_capable_foundation_model( + &cohere_embed_v4 + )); + } + + #[test] + fn test_is_chat_capable_model_id_excludes_embed_and_rerank_profiles() { + assert!(!BedrockProvider::is_chat_capable_model_id( + "cohere.embed-v4:0" + )); + assert!(!BedrockProvider::is_chat_capable_model_id( + "us.cohere.embed-v4:0" + )); + assert!(!BedrockProvider::is_chat_capable_model_id( + "amazon.titan-embed-text-v2:0" + )); + assert!(!BedrockProvider::is_chat_capable_model_id( + "cohere.rerank-v3-5:0" + )); + assert!(BedrockProvider::is_chat_capable_model_id( + "us.anthropic.claude-sonnet-4-5-20250929-v1:0" + )); + } } diff --git a/crates/goose/src/providers/init.rs b/crates/goose/src/providers/init.rs index 3752715fbfd8..c9bfe3e98392 100644 --- a/crates/goose/src/providers/init.rs +++ b/crates/goose/src/providers/init.rs @@ -70,7 +70,10 @@ async fn init_registry() -> RwLock { registry.register::(false); registry.register::(false); #[cfg(feature = "aws-providers")] - registry.register::(false); + registry.register_with_inventory::( + false, + Some(registrations::bedrock_inventory()), + ); #[cfg(feature = "local-inference")] registry.register::(false); registry.register_with_inventory::( diff --git a/crates/goose/src/providers/inventory/registrations.rs b/crates/goose/src/providers/inventory/registrations.rs index b3852cf5062b..dda04b62afe8 100644 --- a/crates/goose/src/providers/inventory/registrations.rs +++ b/crates/goose/src/providers/inventory/registrations.rs @@ -6,6 +6,8 @@ use crate::config::{self, Config}; use crate::providers::acp_tooling::{acp_adapter_installed, resolved_acp_command}; use crate::providers::amp_acp::{AMP_ACP_BINARY, AMP_ACP_PROVIDER_NAME}; use crate::providers::base::ProviderDescriptor; +#[cfg(feature = "aws-providers")] +use crate::providers::bedrock::{BedrockProvider, BEDROCK_PROVIDER_NAME}; use crate::providers::chatgpt_codex::TokenCache as ChatGptCodexTokenCache; use crate::providers::claude_acp::{CLAUDE_ACP_BINARY, CLAUDE_ACP_PROVIDER_NAME}; use crate::providers::codex_acp::CODEX_ACP_PROVIDER_NAME; @@ -125,6 +127,20 @@ pub fn ollama_inventory() -> InventoryRegistration { }) } +#[cfg(feature = "aws-providers")] +pub fn bedrock_inventory() -> InventoryRegistration { + InventoryRegistration::new(true, || { + let config = Config::global(); + let metadata = BedrockProvider::metadata(); + Ok(default_inventory_identity( + BEDROCK_PROVIDER_NAME, + BEDROCK_PROVIDER_NAME, + &metadata.config_keys, + config, + )) + }) +} + pub fn huggingface_inventory() -> InventoryRegistration { InventoryRegistration::new(false, || { let metadata = HuggingFaceProvider::metadata(); diff --git a/crates/goose/src/providers/provider_test.rs b/crates/goose/src/providers/provider_test.rs index b7ee3cad8a2c..e0b66c19de0d 100644 --- a/crates/goose/src/providers/provider_test.rs +++ b/crates/goose/src/providers/provider_test.rs @@ -3,45 +3,88 @@ use anyhow::Result; use futures::StreamExt; use rmcp::model::ToolAnnotations; use rmcp::{model::Tool, object}; +use std::time::Duration; +use tokio::time::timeout; + +const PROVIDER_TEST_TIMEOUT: Duration = Duration::from_secs(60); + +pub fn provider_model_validation_enabled() -> bool { + !matches!( + std::env::var("GOOSE_SKIP_PROVIDER_MODEL_VALIDATION"), + Ok(value) if value == "1" || value.eq_ignore_ascii_case("true") + ) +} + +pub fn toolshim_settings_from_env() -> (Option, Option) { + let toolshim_enabled = std::env::var("GOOSE_TOOLSHIM") + .map(|val| val == "1" || val.to_lowercase() == "true") + .ok(); + let toolshim_model = std::env::var("GOOSE_TOOLSHIM_OLLAMA_MODEL").ok(); + (toolshim_enabled, toolshim_model) +} + +pub async fn test_provider_model(provider_name: &str, model: &str) -> Result<()> { + if !provider_model_validation_enabled() { + return Ok(()); + } + test_provider_configuration(provider_name, model, None, None).await +} pub async fn test_provider_configuration( provider_name: &str, model: &str, - toolshim_enabled: bool, + toolshim_enabled: Option, toolshim_model: Option, ) -> Result<()> { - let model_config = crate::model_config::model_config_from_user_config(provider_name, model)? - .with_max_tokens(Some(50)) - .with_toolshim(toolshim_enabled) - .with_toolshim_model(toolshim_model); + let mut model_config = + crate::model_config::model_config_from_user_config(provider_name, model)? + .with_max_tokens(Some(50)); + + if let Some(toolshim_enabled) = toolshim_enabled { + model_config = model_config.with_toolshim(toolshim_enabled); + } + if toolshim_model.is_some() { + model_config = model_config.with_toolshim_model(toolshim_model); + } let provider = create(provider_name, Vec::new()).await?; let messages = vec![Message::user().with_text("What is the weather like in San Francisco today?")]; - let tools = if !toolshim_enabled { + let tools = if !model_config.toolshim { vec![create_sample_weather_tool()] } else { vec![] }; - let mut stream = crate::session_context::with_session_id( - Some("test-session-id".to_string()), - provider.stream( - &model_config, - "You are an AI agent called goose. You use tools of connected extensions to solve problems.", - &messages, - &tools.into_iter().collect::>(), - ), - ) - .await?; + timeout(PROVIDER_TEST_TIMEOUT, async { + let mut stream = crate::session_context::with_session_id( + Some("test-session-id".to_string()), + provider.stream( + &model_config, + "You are an AI agent called goose. You use tools of connected extensions to solve problems.", + &messages, + &tools.into_iter().collect::>(), + ), + ) + .await?; + + let first_chunk = stream + .next() + .await + .ok_or_else(|| anyhow::anyhow!("Provider test stream returned no events"))?; + first_chunk?; - let first_chunk = stream - .next() - .await - .ok_or_else(|| anyhow::anyhow!("Provider test stream returned no events"))?; - first_chunk?; + Ok::<(), anyhow::Error>(()) + }) + .await + .map_err(|_| { + anyhow::anyhow!( + "Provider configuration test timed out after {}s", + PROVIDER_TEST_TIMEOUT.as_secs() + ) + })??; Ok(()) } diff --git a/crates/goose/tests/acp_custom_requests_test.rs b/crates/goose/tests/acp_custom_requests_test.rs index 36c13e8aa767..11e4ab2e6c92 100644 --- a/crates/goose/tests/acp_custom_requests_test.rs +++ b/crates/goose/tests/acp_custom_requests_test.rs @@ -574,16 +574,13 @@ fn test_steer_session_adds_input_to_active_prompt() { let mut steer_sent = false; let mut steer_message_id: Option = None; let mut final_response = None; + let mut observed_updates = Vec::new(); let deadline = tokio::time::Instant::now() + Duration::from_secs(3); while tokio::time::Instant::now() < deadline { tokio::select! { - response = &mut prompt => { - final_response = Some(response.unwrap()); - break; - } - _ = tokio::time::sleep(Duration::from_millis(10)), if !steer_sent => { - let updates = session.session_updates(); + biased; + updates = session.wait_for_session_updates(), if !steer_sent => { if let Some(run_id) = updates.iter().find_map(active_run_id_from_update) { let response = send_custom( conn.cx(), @@ -607,6 +604,11 @@ fn test_steer_session_adds_input_to_active_prompt() { steer_message_id = mid.map(ToString::to_string); steer_sent = true; } + observed_updates.extend(updates); + } + response = &mut prompt => { + final_response = Some(response.unwrap()); + break; } } } @@ -615,7 +617,8 @@ fn test_steer_session_adds_input_to_active_prompt() { assert_eq!(response.stop_reason, StopReason::EndTurn); assert!(steer_sent, "test never observed an active run id"); - let updates = session.session_updates(); + let mut updates = observed_updates; + updates.extend(session.session_updates()); let agent_text = collect_agent_text(&updates); assert!( agent_text.contains("saw steer"), diff --git a/crates/goose/tests/acp_fixtures/mod.rs b/crates/goose/tests/acp_fixtures/mod.rs index 5290bbc8829e..c1b287cdada4 100644 --- a/crates/goose/tests/acp_fixtures/mod.rs +++ b/crates/goose/tests/acp_fixtures/mod.rs @@ -770,6 +770,7 @@ where if std::env::var_os("GOOSE_PATH_ROOT").is_none() { std::env::set_var("GOOSE_PATH_ROOT", ACP_CONFIG_ROOT.path()); } + std::env::set_var("GOOSE_SKIP_PROVIDER_MODEL_VALIDATION", "1"); register_builtin_extensions(goose_mcp::BUILTIN_EXTENSIONS.clone()); let handle = std::thread::Builder::new() diff --git a/crates/goose/tests/acp_fixtures/server.rs b/crates/goose/tests/acp_fixtures/server.rs index dc86a8ac5e6c..b2a7b7735781 100644 --- a/crates/goose/tests/acp_fixtures/server.rs +++ b/crates/goose/tests/acp_fixtures/server.rs @@ -62,6 +62,17 @@ impl AcpServerSession { .collect() } + pub async fn wait_for_session_updates(&self) -> Vec { + loop { + let notified = self.notify.notified(); + let updates = self.session_updates(); + if !updates.is_empty() { + return updates; + } + notified.await; + } + } + async fn send_prompt( &mut self, content: Vec, diff --git a/ui/desktop/openapi.json b/ui/desktop/openapi.json index e8ce9725a3fc..fc40f91349e7 100644 --- a/ui/desktop/openapi.json +++ b/ui/desktop/openapi.json @@ -3033,6 +3033,10 @@ "provider" ], "properties": { + "model": { + "type": "string", + "nullable": true + }, "provider": { "type": "string" } diff --git a/ui/desktop/src/acp/providers.ts b/ui/desktop/src/acp/providers.ts index e9e967f40564..d3e63e396427 100644 --- a/ui/desktop/src/acp/providers.ts +++ b/ui/desktop/src/acp/providers.ts @@ -11,6 +11,36 @@ import { getAcpClient } from './acpConnection'; export type { CanonicalModelInfoDto, ProviderSecretDto }; +function acpErrorMessage(error: unknown): string | null { + if (typeof error !== 'object' || error === null) { + return null; + } + + const candidate = 'error' in error && isRecord(error.error) ? error.error : error; + if (!isRecord(candidate)) { + return null; + } + if (typeof candidate.data === 'string') { + return candidate.data; + } + return typeof candidate.message === 'string' ? candidate.message : null; +} + +function isRecord(value: unknown): value is Record { + return typeof value === 'object' && value !== null; +} + +function normalizeAcpError(error: unknown, fallback: string): Error { + const message = acpErrorMessage(error); + if (message) { + return new Error(message); + } + if (error instanceof Error) { + return error; + } + return new Error(fallback); +} + function updateRequestToCreate( request: UpdateCustomProviderRequest ): CustomProviderCreateRequest_unstable { @@ -175,8 +205,12 @@ export async function acpReadDefaults(): Promise<{ } export async function acpSaveDefaults(providerId: string, modelId?: string | null): Promise { - const client = await getAcpClient(); - await client.goose.defaultsSave_unstable({ providerId, modelId: modelId ?? null }); + try { + const client = await getAcpClient(); + await client.goose.defaultsSave_unstable({ providerId, modelId: modelId ?? null }); + } catch (error) { + throw normalizeAcpError(error, 'Failed to save default provider/model'); + } } export async function acpClearDefaults(): Promise { @@ -259,26 +293,30 @@ export async function acpSetSessionProviderModel( modelId?: string | null, thinkingEffort?: ThinkingEffort | null ): Promise { - const client = await getAcpClient(); - let response = await client.setSessionConfigOption({ - sessionId, - configId: 'provider', - value: providerId, - }); - if (modelId) { - response = await client.setSessionConfigOption({ + try { + const client = await getAcpClient(); + let response = await client.setSessionConfigOption({ sessionId, - configId: 'model', - value: modelId, + configId: 'provider', + value: providerId, }); - } - if (thinkingEffort != null) { - response = await client.setSessionConfigOption({ - sessionId, - configId: 'thinking_effort', - value: thinkingEffort, - }); - } + if (modelId) { + response = await client.setSessionConfigOption({ + sessionId, + configId: 'model', + value: modelId, + }); + } + if (thinkingEffort != null) { + response = await client.setSessionConfigOption({ + sessionId, + configId: 'thinking_effort', + value: thinkingEffort, + }); + } - return extractAppliedSessionProviderModel(response.configOptions); + return extractAppliedSessionProviderModel(response.configOptions); + } catch (error) { + throw normalizeAcpError(error, 'Failed to update session provider/model'); + } } diff --git a/ui/desktop/src/api/types.gen.ts b/ui/desktop/src/api/types.gen.ts index 83c68e99e55d..d2dff53b4c9a 100644 --- a/ui/desktop/src/api/types.gen.ts +++ b/ui/desktop/src/api/types.gen.ts @@ -60,6 +60,7 @@ export type ChatRequest = { }; export type CheckProviderRequest = { + model?: string | null; provider: string; }; diff --git a/ui/desktop/src/components/ModelAndProviderContext.tsx b/ui/desktop/src/components/ModelAndProviderContext.tsx index 5bd3f9226ae4..dc9bc40e9e06 100644 --- a/ui/desktop/src/components/ModelAndProviderContext.tsx +++ b/ui/desktop/src/components/ModelAndProviderContext.tsx @@ -1,6 +1,6 @@ import React, { createContext, useContext, useState, useEffect, useMemo, useCallback } from 'react'; import { toastError, toastSuccess } from '../toasts'; -import Model, { getProviderMetadata } from './settings/models/modelInterface'; +import Model, { getProviderMetadata, validateProviderModel } from './settings/models/modelInterface'; import type { ProviderMetadata } from '../types/providers'; import { acpChatSessionActions, acpChatSessionStore } from '../acp/chatSessionStore'; import { @@ -93,10 +93,13 @@ export const ModelAndProviderProvider: React.FC = async (sessionId: string | null, model: Model) => { const modelName = model.name; const providerName = model.provider; - let phase = 'agent'; + let phase = 'validation'; try { + await validateProviderModel(providerName, modelName); + if (sessionId) { + phase = 'agent'; const applied = await acpSetSessionProviderModel( sessionId, providerName, @@ -128,13 +131,14 @@ export const ModelAndProviderProvider: React.FC = return true; } catch (error) { console.error(`Failed to change model at ${phase} step -- ${modelName} ${providerName}`); + const message = errorMessage(error); toastError({ title: intl.formatMessage(i18n.modelChangeFailed, { provider: providerName, model: modelName, }), - msg: `${error}`, - traceback: errorMessage(error), + msg: message, + traceback: message, }); return false; } diff --git a/ui/desktop/src/components/settings/models/modelInterface.ts b/ui/desktop/src/components/settings/models/modelInterface.ts index b16f82ea8560..c06f50c482fa 100644 --- a/ui/desktop/src/components/settings/models/modelInterface.ts +++ b/ui/desktop/src/components/settings/models/modelInterface.ts @@ -1,5 +1,10 @@ +import { + checkProvider, + getProviderModelInfo, + getProviderModels, +} from '../../../api'; import { listLocalModels } from '../../../acp/local-inference'; -import { acpListProviderDetails, acpListProviderModels } from '../../../acp/providers'; +import { acpListProviderDetails } from '../../../acp/providers'; import type { ProviderDetails, ThinkingEffort } from '../../../types/providers'; import { errorMessage as getErrorMessage } from '../../../utils/conversionUtils'; @@ -43,6 +48,13 @@ export async function getProviderMetadata(providerName: string) { return matches.metadata; } +export async function validateProviderModel(provider: string, model: string): Promise { + await checkProvider({ + body: { provider, model }, + throwOnError: true, + }); +} + export interface ProviderModelsResult { provider: ProviderDetails; models: Model[] | null; @@ -64,13 +76,16 @@ export async function fetchModelsForProviders( return { provider: p, models: downloadedModels, error: null, warning: null }; } - const providerModels = await acpListProviderModels(p.name); - const models = providerModels.map( + const response = await getProviderModels({ + path: { name: p.name }, + throwOnError: true, + }); + const models = (response.data || []).map( (m) => ({ - name: m.id, + name: m.name, provider: p.name, - context_limit: m.contextLimit ?? undefined, + context_limit: m.context_limit, reasoning: m.reasoning ?? undefined, }) as Model ); @@ -118,9 +133,11 @@ export async function fetchModelReasoning( fallback?: boolean ): Promise { try { - const models = await acpListProviderModels(provider); - const match = models.find((m) => m.id === model); - return match?.reasoning ?? fallback ?? null; + const response = await getProviderModelInfo({ + path: { name: provider }, + body: { model }, + }); + return response.data?.reasoning ?? fallback ?? null; } catch { return fallback ?? null; } diff --git a/ui/desktop/src/components/settings/models/subcomponents/SwitchModelModal.tsx b/ui/desktop/src/components/settings/models/subcomponents/SwitchModelModal.tsx index 326ca8e21bfb..df21822bea61 100644 --- a/ui/desktop/src/components/settings/models/subcomponents/SwitchModelModal.tsx +++ b/ui/desktop/src/components/settings/models/subcomponents/SwitchModelModal.tsx @@ -132,6 +132,10 @@ const i18n = defineMessages({ id: 'switchModelModal.loadingModels', defaultMessage: 'Loading models…', }, + checkingModel: { + id: 'switchModelModal.checkingModel', + defaultMessage: 'Checking model…', + }, selectModelPlaceholder: { id: 'switchModelModal.selectModelPlaceholder', defaultMessage: 'Select a model, type to search', @@ -302,6 +306,7 @@ export const SwitchModelModal = ({ const [selectedPredefinedModel, setSelectedPredefinedModel] = useState(null); const [predefinedModels, setPredefinedModels] = useState([]); const [loadingModels, setLoadingModels] = useState(false); + const [isSubmitting, setIsSubmitting] = useState(false); const [userClearedModel, setUserClearedModel] = useState(false); const [providerErrors, setProviderErrors] = useState>({}); const [providerWarnings, setProviderWarnings] = useState>({}); @@ -395,41 +400,46 @@ export const SwitchModelModal = ({ setAttemptedSubmit(true); const isFormValid = validateForm(); - if (isFormValid) { - let modelObj: Model; + if (!isFormValid) { + return; + } - if (usePredefinedModels && selectedPredefinedModel) { - modelObj = selectedPredefinedModel; - } else { - const providerMetaData = await getProviderMetadata(provider || ''); - const providerDisplayName = providerMetaData.display_name; - modelObj = { - name: model, - provider: provider, - subtext: providerDisplayName, - } as Model; - } + let modelObj: Model; + + if (usePredefinedModels && selectedPredefinedModel) { + modelObj = selectedPredefinedModel; + } else { + const providerMetaData = await getProviderMetadata(provider || ''); + modelObj = { + name: model, + provider: provider, + subtext: providerMetaData.display_name, + } as Model; + } + modelObj = { + ...modelObj, + reasoning: selectedModelReasoning ?? modelObj.reasoning, + }; + + if (showThinkingControl) { + const effort = thinkingEffort ?? modelObj.request_params?.thinking_effort ?? 'off'; modelObj = { ...modelObj, - reasoning: selectedModelReasoning ?? modelObj.reasoning, + request_params: { ...modelObj.request_params, thinking_effort: effort }, }; + acpSaveThinkingEffort(effort).catch(console.warn); + } - if (showThinkingControl) { - const effort = thinkingEffort ?? modelObj.request_params?.thinking_effort ?? 'off'; - modelObj = { - ...modelObj, - request_params: { ...modelObj.request_params, thinking_effort: effort }, - }; - acpSaveThinkingEffort(effort).catch(console.warn); - } - + setIsSubmitting(true); + try { const success = await changeModel(sessionId, modelObj); if (success) { onModelSelected?.(modelObj.name, modelObj.provider || ''); trackModelChanged(modelObj.provider || '', modelObj.name); + onClose(); } - - onClose(); + } finally { + setIsSubmitting(false); } }; @@ -978,11 +988,13 @@ export const SwitchModelModal = ({ {intl.formatMessage(i18n.quickStartGuide)}
- -
diff --git a/ui/desktop/src/i18n/messages/de.json b/ui/desktop/src/i18n/messages/de.json index 19dc36913bf3..f75b3aeb16ef 100644 --- a/ui/desktop/src/i18n/messages/de.json +++ b/ui/desktop/src/i18n/messages/de.json @@ -4175,6 +4175,9 @@ "switchModelModal.checkProviderConfig": { "defaultMessage": "Überprüfen Sie Ihre Anbieterkonfiguration unter Einstellungen → Anbieter" }, + "switchModelModal.checkingModel": { + "defaultMessage": "Modell wird überprüft…" + }, "switchModelModal.chooseModel": { "defaultMessage": "Wählen Sie ein Modell:" }, diff --git a/ui/desktop/src/i18n/messages/en.json b/ui/desktop/src/i18n/messages/en.json index 85085508b237..efb6a746ed7a 100644 --- a/ui/desktop/src/i18n/messages/en.json +++ b/ui/desktop/src/i18n/messages/en.json @@ -4175,6 +4175,9 @@ "switchModelModal.checkProviderConfig": { "defaultMessage": "Check your provider configuration in Settings → Providers" }, + "switchModelModal.checkingModel": { + "defaultMessage": "Checking model…" + }, "switchModelModal.chooseModel": { "defaultMessage": "Choose a model:" }, diff --git a/ui/desktop/src/i18n/messages/es.json b/ui/desktop/src/i18n/messages/es.json index 8aba39ef76b8..3dfa347ff5ab 100644 --- a/ui/desktop/src/i18n/messages/es.json +++ b/ui/desktop/src/i18n/messages/es.json @@ -4223,6 +4223,9 @@ "switchModelModal.loadingModels": { "defaultMessage": "Cargando modelos…" }, + "switchModelModal.checkingModel": { + "defaultMessage": "Comprobando modelo…" + }, "switchModelModal.localModelsDescription": { "defaultMessage": "Para usar inferencia local, primero debes descargar un modelo a tu computadora. Ve a Ajustes → Modelos para gestionar los modelos locales." }, diff --git a/ui/desktop/src/i18n/messages/fr.json b/ui/desktop/src/i18n/messages/fr.json index 41fc303d1588..163736ccd2c3 100644 --- a/ui/desktop/src/i18n/messages/fr.json +++ b/ui/desktop/src/i18n/messages/fr.json @@ -4175,6 +4175,9 @@ "switchModelModal.checkProviderConfig": { "defaultMessage": "Vérifiez la configuration de votre fournisseur dans Paramètres → Fournisseurs" }, + "switchModelModal.checkingModel": { + "defaultMessage": "Vérification du modèle…" + }, "switchModelModal.chooseModel": { "defaultMessage": "Choisissez un modèle :" }, diff --git a/ui/desktop/src/i18n/messages/hi.json b/ui/desktop/src/i18n/messages/hi.json index 906fcb129b9b..4a5bc9b716ca 100644 --- a/ui/desktop/src/i18n/messages/hi.json +++ b/ui/desktop/src/i18n/messages/hi.json @@ -4223,6 +4223,9 @@ "switchModelModal.loadingModels": { "defaultMessage": "मॉडल लोड हो रहे हैं…" }, + "switchModelModal.checkingModel": { + "defaultMessage": "मॉडल की जाँच की जा रही है…" + }, "switchModelModal.localModelsDescription": { "defaultMessage": "स्थानीय अनुमान का उपयोग करने के लिए, आपको पहले अपने कंप्यूटर पर एक मॉडल डाउनलोड करना होगा। स्थानीय मॉडल प्रबंधित करने के लिए Settings → मॉडल पर जाएं।" }, diff --git a/ui/desktop/src/i18n/messages/id.json b/ui/desktop/src/i18n/messages/id.json index bdd7de71287c..256146d366e2 100644 --- a/ui/desktop/src/i18n/messages/id.json +++ b/ui/desktop/src/i18n/messages/id.json @@ -4175,6 +4175,9 @@ "switchModelModal.checkProviderConfig": { "defaultMessage": "Periksa konfigurasi penyedia Anda di Pengaturan → Penyedia" }, + "switchModelModal.checkingModel": { + "defaultMessage": "Memeriksa model…" + }, "switchModelModal.chooseModel": { "defaultMessage": "Pilih model:" }, diff --git a/ui/desktop/src/i18n/messages/it.json b/ui/desktop/src/i18n/messages/it.json index c19fa48e167f..22ee6c22a7b9 100644 --- a/ui/desktop/src/i18n/messages/it.json +++ b/ui/desktop/src/i18n/messages/it.json @@ -4175,6 +4175,9 @@ "switchModelModal.checkProviderConfig": { "defaultMessage": "Controlla la configurazione del provider in Impostazioni → Provider" }, + "switchModelModal.checkingModel": { + "defaultMessage": "Verifica del modello…" + }, "switchModelModal.chooseModel": { "defaultMessage": "Scegli un modello:" }, diff --git a/ui/desktop/src/i18n/messages/ja.json b/ui/desktop/src/i18n/messages/ja.json index 75863c6d3b13..8793cab3bcc7 100644 --- a/ui/desktop/src/i18n/messages/ja.json +++ b/ui/desktop/src/i18n/messages/ja.json @@ -4223,6 +4223,9 @@ "switchModelModal.loadingModels": { "defaultMessage": "モデルを読み込み中…" }, + "switchModelModal.checkingModel": { + "defaultMessage": "モデルを確認中…" + }, "switchModelModal.localModelsDescription": { "defaultMessage": "ローカル推論を使用するには、先にモデルをコンピューターにダウンロードする必要があります。設定 → モデルでローカルモデルを管理できます。" }, diff --git a/ui/desktop/src/i18n/messages/ko.json b/ui/desktop/src/i18n/messages/ko.json index 09aa51362141..82d8af68d653 100644 --- a/ui/desktop/src/i18n/messages/ko.json +++ b/ui/desktop/src/i18n/messages/ko.json @@ -4223,6 +4223,9 @@ "switchModelModal.loadingModels": { "defaultMessage": "모델 로드 중…" }, + "switchModelModal.checkingModel": { + "defaultMessage": "모델 확인 중…" + }, "switchModelModal.localModelsDescription": { "defaultMessage": "로컬 추론을 사용하려면 먼저 모델을 컴퓨터에 다운로드해야 합니다. 로컬 모델을 관리하려면 설정 → 모델로 이동하세요." }, diff --git a/ui/desktop/src/i18n/messages/ms.json b/ui/desktop/src/i18n/messages/ms.json index 68fedd6c2495..4c8586927a96 100644 --- a/ui/desktop/src/i18n/messages/ms.json +++ b/ui/desktop/src/i18n/messages/ms.json @@ -4175,6 +4175,9 @@ "switchModelModal.checkProviderConfig": { "defaultMessage": "Semak konfigurasi penyedia anda dalam Tetapan → Penyedia" }, + "switchModelModal.checkingModel": { + "defaultMessage": "Menyemak model…" + }, "switchModelModal.chooseModel": { "defaultMessage": "Pilih model:" }, diff --git a/ui/desktop/src/i18n/messages/pt.json b/ui/desktop/src/i18n/messages/pt.json index b715b37b6501..609a5afdf090 100644 --- a/ui/desktop/src/i18n/messages/pt.json +++ b/ui/desktop/src/i18n/messages/pt.json @@ -4175,6 +4175,9 @@ "switchModelModal.checkProviderConfig": { "defaultMessage": "Verifique a configuração do seu fornecedor em Definições → Fornecedores" }, + "switchModelModal.checkingModel": { + "defaultMessage": "A verificar modelo…" + }, "switchModelModal.chooseModel": { "defaultMessage": "Escolha um modelo:" }, diff --git a/ui/desktop/src/i18n/messages/ru.json b/ui/desktop/src/i18n/messages/ru.json index fb890d315396..421ff67a8e4b 100644 --- a/ui/desktop/src/i18n/messages/ru.json +++ b/ui/desktop/src/i18n/messages/ru.json @@ -4223,6 +4223,9 @@ "switchModelModal.loadingModels": { "defaultMessage": "Загрузка моделей…" }, + "switchModelModal.checkingModel": { + "defaultMessage": "Проверка модели…" + }, "switchModelModal.localModelsDescription": { "defaultMessage": "Чтобы использовать локальный инференс, сначала скачайте модель на компьютер. Перейдите в Настройки → Модели для управления локальными моделями." }, diff --git a/ui/desktop/src/i18n/messages/tr.json b/ui/desktop/src/i18n/messages/tr.json index c69394cd8d4c..95629166b969 100644 --- a/ui/desktop/src/i18n/messages/tr.json +++ b/ui/desktop/src/i18n/messages/tr.json @@ -4223,6 +4223,9 @@ "switchModelModal.loadingModels": { "defaultMessage": "Modeller yükleniyor…" }, + "switchModelModal.checkingModel": { + "defaultMessage": "Model kontrol ediliyor…" + }, "switchModelModal.localModelsDescription": { "defaultMessage": "Yerel çıkarımı kullanmak için öncelikle bilgisayarınıza bir model indirmeniz gerekir. Yerel modelleri yönetmek için Ayarlar → Modeller'e gidin." }, diff --git a/ui/desktop/src/i18n/messages/vi.json b/ui/desktop/src/i18n/messages/vi.json index 8f948474413c..7e3eea6f8caa 100644 --- a/ui/desktop/src/i18n/messages/vi.json +++ b/ui/desktop/src/i18n/messages/vi.json @@ -4175,6 +4175,9 @@ "switchModelModal.checkProviderConfig": { "defaultMessage": "Kiểm tra cấu hình nhà cung cấp của bạn trong Cài đặt → Nhà cung cấp" }, + "switchModelModal.checkingModel": { + "defaultMessage": "Đang kiểm tra mô hình…" + }, "switchModelModal.chooseModel": { "defaultMessage": "Chọn một mô hình:" }, diff --git a/ui/desktop/src/i18n/messages/zh-CN.json b/ui/desktop/src/i18n/messages/zh-CN.json index ca094060afb2..4f3f88b020e1 100644 --- a/ui/desktop/src/i18n/messages/zh-CN.json +++ b/ui/desktop/src/i18n/messages/zh-CN.json @@ -4223,6 +4223,9 @@ "switchModelModal.loadingModels": { "defaultMessage": "正在加载模型…" }, + "switchModelModal.checkingModel": { + "defaultMessage": "正在检查模型…" + }, "switchModelModal.localModelsDescription": { "defaultMessage": "要使用本地推理,你需要先下载一个模型到电脑上。前往 设置 → 模型 管理本地模型。" }, diff --git a/ui/desktop/src/i18n/messages/zh-TW.json b/ui/desktop/src/i18n/messages/zh-TW.json index 002bae4570a1..e96cd7c79278 100644 --- a/ui/desktop/src/i18n/messages/zh-TW.json +++ b/ui/desktop/src/i18n/messages/zh-TW.json @@ -4175,6 +4175,9 @@ "switchModelModal.checkProviderConfig": { "defaultMessage": "請在「設定」→「提供者」中檢查您的提供者設定" }, + "switchModelModal.checkingModel": { + "defaultMessage": "正在檢查模型…" + }, "switchModelModal.chooseModel": { "defaultMessage": "選擇模型:" },