mirror of
https://github.com/block/goose.git
synced 2026-07-09 16:09:22 +00:00
Bedrock model discovery and validation (#9997)
This commit is contained in:
parent
e5a0162513
commit
7026e14585
35 changed files with 720 additions and 135 deletions
26
Cargo.lock
generated
26
Cargo.lock
generated
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -811,10 +811,8 @@ pub async fn configure_provider_dialog() -> anyhow::Result<bool> {
|
|||
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
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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))?;
|
||||
|
|
|
|||
|
|
@ -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<String> {
|
|||
#[derive(Deserialize, ToSchema)]
|
||||
pub struct CheckProviderRequest {
|
||||
pub provider: String,
|
||||
#[serde(default)]
|
||||
pub model: Option<String>,
|
||||
}
|
||||
|
||||
#[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<CheckProviderRequest>,
|
||||
Json(CheckProviderRequest { provider, model }): Json<CheckProviderRequest>,
|
||||
) -> 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<SetProviderRequest>,
|
||||
) -> 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(())
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
|
|
|||
|
|
@ -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!(
|
||||
|
|
|
|||
|
|
@ -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_client_with_credentials(&sdk_config).await?
|
||||
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 {
|
||||
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<Client> {
|
||||
async fn create_runtime_client_with_credentials(
|
||||
sdk_config: &aws_config::SdkConfig,
|
||||
) -> Result<BedrockRuntimeClient> {
|
||||
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<String> {
|
||||
let mut models: Vec<String> = 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<Item = String>,
|
||||
foundation_models: impl IntoIterator<Item = String>,
|
||||
extra_models: impl IntoIterator<Item = String>,
|
||||
) -> Vec<String> {
|
||||
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<Vec<String>, 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<Vec<String>, 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<Vec<String>, 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 <profile-name>' 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 <profile-name>' 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<Vec<String>, 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"
|
||||
));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -70,7 +70,10 @@ async fn init_registry() -> RwLock<ProviderRegistry> {
|
|||
registry.register::<AvianProvider>(false);
|
||||
registry.register::<AzureProvider>(false);
|
||||
#[cfg(feature = "aws-providers")]
|
||||
registry.register::<BedrockProvider>(false);
|
||||
registry.register_with_inventory::<BedrockProvider>(
|
||||
false,
|
||||
Some(registrations::bedrock_inventory()),
|
||||
);
|
||||
#[cfg(feature = "local-inference")]
|
||||
registry.register::<LocalInferenceProvider>(false);
|
||||
registry.register_with_inventory::<ChatGptCodexProvider>(
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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<bool>, Option<String>) {
|
||||
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<bool>,
|
||||
toolshim_model: Option<String>,
|
||||
) -> 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::<Vec<_>>(),
|
||||
),
|
||||
)
|
||||
.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::<Vec<_>>(),
|
||||
),
|
||||
)
|
||||
.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(())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -574,16 +574,13 @@ fn test_steer_session_adds_input_to_active_prompt() {
|
|||
let mut steer_sent = false;
|
||||
let mut steer_message_id: Option<String> = 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"),
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -62,6 +62,17 @@ impl AcpServerSession {
|
|||
.collect()
|
||||
}
|
||||
|
||||
pub async fn wait_for_session_updates(&self) -> Vec<SessionUpdate> {
|
||||
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<ContentBlock>,
|
||||
|
|
|
|||
|
|
@ -3033,6 +3033,10 @@
|
|||
"provider"
|
||||
],
|
||||
"properties": {
|
||||
"model": {
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
},
|
||||
"provider": {
|
||||
"type": "string"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<string, unknown> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
|
|
@ -259,26 +293,30 @@ export async function acpSetSessionProviderModel(
|
|||
modelId?: string | null,
|
||||
thinkingEffort?: ThinkingEffort | null
|
||||
): Promise<AppliedSessionProviderModel> {
|
||||
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');
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -60,6 +60,7 @@ export type ChatRequest = {
|
|||
};
|
||||
|
||||
export type CheckProviderRequest = {
|
||||
model?: string | null;
|
||||
provider: string;
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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<ModelAndProviderProviderProps> =
|
|||
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<ModelAndProviderProviderProps> =
|
|||
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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<void> {
|
||||
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<boolean | null> {
|
||||
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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<Model | null>(null);
|
||||
const [predefinedModels, setPredefinedModels] = useState<Model[]>([]);
|
||||
const [loadingModels, setLoadingModels] = useState<boolean>(false);
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [userClearedModel, setUserClearedModel] = useState(false);
|
||||
const [providerErrors, setProviderErrors] = useState<Record<string, string>>({});
|
||||
const [providerWarnings, setProviderWarnings] = useState<Record<string, string>>({});
|
||||
|
|
@ -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)}
|
||||
</a>
|
||||
<div className="flex gap-2">
|
||||
<Button variant="outline" onClick={handleClose} type="button">
|
||||
<Button variant="outline" onClick={handleClose} type="button" disabled={isSubmitting}>
|
||||
{intl.formatMessage(i18n.cancel)}
|
||||
</Button>
|
||||
<Button onClick={handleSubmit} disabled={!isValid}>
|
||||
{intl.formatMessage(i18n.selectModelButton)}
|
||||
<Button onClick={handleSubmit} disabled={!isValid || isSubmitting}>
|
||||
{isSubmitting
|
||||
? intl.formatMessage(i18n.checkingModel)
|
||||
: intl.formatMessage(i18n.selectModelButton)}
|
||||
</Button>
|
||||
</div>
|
||||
</DialogFooter>
|
||||
|
|
|
|||
|
|
@ -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:"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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:"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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."
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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 :"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -4223,6 +4223,9 @@
|
|||
"switchModelModal.loadingModels": {
|
||||
"defaultMessage": "मॉडल लोड हो रहे हैं…"
|
||||
},
|
||||
"switchModelModal.checkingModel": {
|
||||
"defaultMessage": "मॉडल की जाँच की जा रही है…"
|
||||
},
|
||||
"switchModelModal.localModelsDescription": {
|
||||
"defaultMessage": "स्थानीय अनुमान का उपयोग करने के लिए, आपको पहले अपने कंप्यूटर पर एक मॉडल डाउनलोड करना होगा। स्थानीय मॉडल प्रबंधित करने के लिए Settings → मॉडल पर जाएं।"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -4175,6 +4175,9 @@
|
|||
"switchModelModal.checkProviderConfig": {
|
||||
"defaultMessage": "Periksa konfigurasi penyedia Anda di Pengaturan → Penyedia"
|
||||
},
|
||||
"switchModelModal.checkingModel": {
|
||||
"defaultMessage": "Memeriksa model…"
|
||||
},
|
||||
"switchModelModal.chooseModel": {
|
||||
"defaultMessage": "Pilih model:"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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:"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -4223,6 +4223,9 @@
|
|||
"switchModelModal.loadingModels": {
|
||||
"defaultMessage": "モデルを読み込み中…"
|
||||
},
|
||||
"switchModelModal.checkingModel": {
|
||||
"defaultMessage": "モデルを確認中…"
|
||||
},
|
||||
"switchModelModal.localModelsDescription": {
|
||||
"defaultMessage": "ローカル推論を使用するには、先にモデルをコンピューターにダウンロードする必要があります。設定 → モデルでローカルモデルを管理できます。"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -4223,6 +4223,9 @@
|
|||
"switchModelModal.loadingModels": {
|
||||
"defaultMessage": "모델 로드 중…"
|
||||
},
|
||||
"switchModelModal.checkingModel": {
|
||||
"defaultMessage": "모델 확인 중…"
|
||||
},
|
||||
"switchModelModal.localModelsDescription": {
|
||||
"defaultMessage": "로컬 추론을 사용하려면 먼저 모델을 컴퓨터에 다운로드해야 합니다. 로컬 모델을 관리하려면 설정 → 모델로 이동하세요."
|
||||
},
|
||||
|
|
|
|||
|
|
@ -4175,6 +4175,9 @@
|
|||
"switchModelModal.checkProviderConfig": {
|
||||
"defaultMessage": "Semak konfigurasi penyedia anda dalam Tetapan → Penyedia"
|
||||
},
|
||||
"switchModelModal.checkingModel": {
|
||||
"defaultMessage": "Menyemak model…"
|
||||
},
|
||||
"switchModelModal.chooseModel": {
|
||||
"defaultMessage": "Pilih model:"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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:"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -4223,6 +4223,9 @@
|
|||
"switchModelModal.loadingModels": {
|
||||
"defaultMessage": "Загрузка моделей…"
|
||||
},
|
||||
"switchModelModal.checkingModel": {
|
||||
"defaultMessage": "Проверка модели…"
|
||||
},
|
||||
"switchModelModal.localModelsDescription": {
|
||||
"defaultMessage": "Чтобы использовать локальный инференс, сначала скачайте модель на компьютер. Перейдите в Настройки → Модели для управления локальными моделями."
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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."
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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:"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -4223,6 +4223,9 @@
|
|||
"switchModelModal.loadingModels": {
|
||||
"defaultMessage": "正在加载模型…"
|
||||
},
|
||||
"switchModelModal.checkingModel": {
|
||||
"defaultMessage": "正在检查模型…"
|
||||
},
|
||||
"switchModelModal.localModelsDescription": {
|
||||
"defaultMessage": "要使用本地推理,你需要先下载一个模型到电脑上。前往 设置 → 模型 管理本地模型。"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -4175,6 +4175,9 @@
|
|||
"switchModelModal.checkProviderConfig": {
|
||||
"defaultMessage": "請在「設定」→「提供者」中檢查您的提供者設定"
|
||||
},
|
||||
"switchModelModal.checkingModel": {
|
||||
"defaultMessage": "正在檢查模型…"
|
||||
},
|
||||
"switchModelModal.chooseModel": {
|
||||
"defaultMessage": "選擇模型:"
|
||||
},
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue