Skip to content
37 changes: 26 additions & 11 deletions crates/biorouter/src/privacy/config_keys.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,13 +54,18 @@ pub const NOT_CAPABILITY_CONFIG_KEYS: &[(&str, &str)] = &[
("LLAMACPP_TIMEOUT", "transport timeout"),
("LLAMACPP_STARTUP_TIMEOUT", "sidecar readiness deadline"),
("LLAMACPP_CONTEXT_SIZE", "token budget"),
// ⚠ The four endpoint keys below MOVE where a Private-badged provider sends
// traffic, but they cannot RAISE a tier: Task 5 name-keys versa_azure and
// versa_bedrock Private regardless of endpoint, and azure.rs ships the
// UCSF gateway as a PUBLIC provider's default for the same reason.
// Pointing a private-badged provider off-site is a real and different
// problem — it belongs to Task 5's tier definition and to Open question 5,
// not to DR-16 — and it is recorded here rather than left unstated.
// ⚠ The two endpoint keys below MOVE where a Private-badged provider sends
// traffic, and since `e2e4eb9d` that moves its tier as well: `tier()`
// follows the endpoint an instance resolved (`ucsf_gateway_tier`), so an
// off-site value demotes it to Public, and deleting that value restores
// Private. These rows used to say the keys "cannot RAISE a tier" because
// Task 5 name-keyed versa_* Private regardless of endpoint, and that
// stopped being true. The classification rests on this instead: the only
// value that reads Private is the UCSF gateway's own host, so no write can
// make an off-site endpoint look Private, and a raise through one of these
// keys is always a return to the institution's gateway. Whether even that
// raise should be a user act, as it is for `OLLAMA_HOST`, is an open DR-16
// question, recorded here rather than left unstated.
//
// Versa Azure's three overrides, in its own namespace. It used to share the
// public `azure_openai` card's `AZURE_OPENAI_*` keys, which went wrong both
Expand All @@ -74,15 +79,25 @@ pub const NOT_CAPABILITY_CONFIG_KEYS: &[(&str, &str)] = &[
// tier-input file, because `azure_openai` is Public wherever it points.
(
"VERSA_AZURE_ENDPOINT",
"moves a Private provider's endpoint; does not raise a tier (see Task 5)",
"moves a Private provider's endpoint; only the UCSF gateway reads Private (see above)",
),
("VERSA_AZURE_DEPLOYMENT_NAME", "deployment selection"),
("VERSA_AZURE_API_VERSION", "wire version"),
// Versa Bedrock's two overrides, in its own namespace since 2026-09-11. It
// used to declare and read the public Amazon Bedrock card's `AWS_REGION` and
// an `AWS_ENDPOINT_URL_BEDROCK` key, then fall back to the process
// environment, so the public side's values steered Versa and a Versa setup
// configured the public card. No tier-input file reads an `AWS_*` key now,
// so none has a row; `bedrock.rs` still reads them and is not a tier-input
// file, because `aws_bedrock` is Public wherever it points.
(
"AWS_ENDPOINT_URL_BEDROCK",
"moves a Private provider's endpoint; does not raise a tier (see Task 5)",
"VERSA_BEDROCK_ENDPOINT",
"moves a Private provider's endpoint; only the UCSF gateway reads Private (see above)",
),
(
"VERSA_BEDROCK_REGION",
"SigV4 signing region; the endpoint, not the region, decides where a request goes",
),
("AWS_REGION", "region selection"),
("BEDROCK_MAX_RETRIES", "retry policy"),
("BEDROCK_INITIAL_RETRY_INTERVAL_MS", "retry policy"),
("BEDROCK_BACKOFF_MULTIPLIER", "retry policy"),
Expand Down
39 changes: 30 additions & 9 deletions crates/biorouter/src/providers/bedrock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -83,15 +83,23 @@ impl BedrockProvider {
set_aws_env_vars(config.all_values());
set_aws_env_vars(config.all_secrets());

// Normalize AWS_ENDPOINT_URL_BEDROCK → AWS_ENDPOINT_URL_BEDROCK_RUNTIME.
// The AWS SDK for Rust reads the service-specific key AWS_ENDPOINT_URL_BEDROCK_RUNTIME,
// but users (and older configs) often set the shorter AWS_ENDPOINT_URL_BEDROCK.
// Accept either: if only the short form is set, promote it to the correct key.
if std::env::var("AWS_ENDPOINT_URL_BEDROCK_RUNTIME").is_err() {
if let Ok(url) = std::env::var("AWS_ENDPOINT_URL_BEDROCK") {
std::env::set_var("AWS_ENDPOINT_URL_BEDROCK_RUNTIME", url);
}
}
// ⚠ `AWS_ENDPOINT_URL_BEDROCK` is not an endpoint for this provider. The
// AWS SDK aims Bedrock Runtime at `AWS_ENDPOINT_URL_BEDROCK_RUNTIME`,
// from the environment or from `config.yaml` through the export above,
// or at an AWS profile's `services` section; that is how a VPC endpoint
// or a proxy is meant to be set. `AWS_ENDPOINT_URL_BEDROCK` is the name
// the SDK derives for a different service, the Bedrock control plane.
//
// This used to promote it to `AWS_ENDPOINT_URL_BEDROCK_RUNTIME`, added
// on 2026-04-12 for configs and setup scripts that used the short name,
// a month before Versa Bedrock existed. But every Biorouter surface that
// has written that key wrote it for Versa Bedrock: its onboarding card on
// every connect, its Settings form on every save, `biorouter configure`
// when asked to. So once Versa was set up, the promotion made UCSF's
// gateway THIS provider's endpoint, and the user's own AWS-signed
// requests went there and were refused. Versa reads its own
// `VERSA_BEDROCK_ENDPOINT` now, but installs keep the old key, so it has
// to be ignored here, not merely left unwritten (2026-09-11).

// Use load_defaults() which supports AWS SSO, profiles, and environment variables
let mut loader = aws_config::defaults(aws_config::BehaviorVersion::latest());
Expand Down Expand Up @@ -137,6 +145,19 @@ impl BedrockProvider {
})
}

/// The same client with only its HTTP transport replaced. Everything
/// `from_env` resolved — endpoint, region, credentials — is kept, so a
/// request captured through it is the request production would have sent.
#[cfg(test)]
pub(crate) fn with_http_client(
mut self,
http_client: impl aws_sdk_bedrockruntime::config::HttpClient + 'static,
) -> Self {
let config = self.client.config().to_builder().http_client(http_client);
self.client = Client::from_conf(config.build());
self
}

fn load_retry_config(config: &crate::config::Config) -> RetryConfig {
let max_retries = config
.get_param::<usize>("BEDROCK_MAX_RETRIES")
Expand Down
Loading
Loading