diff --git a/litellm-rust/crates/gateway-inference/src/chat_completions.rs b/litellm-rust/crates/gateway-inference/src/chat_completions.rs index 41a3ac0fbc9..87380ead495 100644 --- a/litellm-rust/crates/gateway-inference/src/chat_completions.rs +++ b/litellm-rust/crates/gateway-inference/src/chat_completions.rs @@ -187,23 +187,30 @@ fn split(mut body: Map) -> (Value, Map) { /// A deployment's connection for a provider chat translates. Python's router reads each /// `os.environ/` setting from the environment when it loads the deployment, and `completion` -/// hands the Vertex AI settings to the call, where a request body cannot name them. +/// hands the Vertex AI and AWS settings to the call, where a request body cannot name them. async fn connection( gateway: &Gateway, deployment: &Deployment, optional_params: &mut Map, ) -> Result<(Option, Option), Error> { - let vertex = match serde_json::to_value(&deployment.litellm_params.vertex) { - Ok(Value::Object(fields)) => fields, - _ => Map::new(), - }; + // The Vertex AI and AWS settings both live in their own groups of `litellm_params`, not in + // `extra`, so each is copied into the call's params, where the provider config reads it. + let mut settings = Map::new(); + for group in [ + serde_json::to_value(&deployment.litellm_params.vertex), + serde_json::to_value(&deployment.litellm_params.aws), + ] { + if let Ok(Value::Object(fields)) = group { + settings.extend(fields); + } + } let references: Vec<&str> = [ deployment.api_key.as_deref(), deployment.api_base.as_deref(), ] .into_iter() .flatten() - .chain(vertex.values().filter_map(Value::as_str)) + .chain(settings.values().filter_map(Value::as_str)) .filter_map(|value| value.strip_prefix(ENV_REFERENCE)) .collect(); let secrets = match references.is_empty() { @@ -221,7 +228,7 @@ async fn connection( Some(name) => secrets.as_ref().and_then(|secrets| secrets.get(name)), None => Some(value.to_owned()), }; - for (name, value) in vertex { + for (name, value) in settings { let value = match value { Value::String(text) => resolve(&text).map(Value::String), Value::Null => None, diff --git a/litellm-rust/crates/gateway/tests/chat_translation.rs b/litellm-rust/crates/gateway/tests/chat_translation.rs index aa0c1da8f6f..3c7f7acf305 100644 --- a/litellm-rust/crates/gateway/tests/chat_translation.rs +++ b/litellm-rust/crates/gateway/tests/chat_translation.rs @@ -276,6 +276,13 @@ async fn harness() -> Harness { model: bedrock/{BEDROCK_MODEL} api_base: {upstream} api_key: bedrock-token + - model_name: bedrock-keys + litellm_params: + model: bedrock/{BEDROCK_MODEL} + aws_access_key_id: AKIACONFIGKEY + aws_secret_access_key: config-secret + aws_region_name: us-west-2 + aws_bedrock_runtime_endpoint: {upstream} - model_name: claude-dropping litellm_params: model: anthropic/{ANTHROPIC_MODEL} @@ -449,6 +456,24 @@ async fn anthropic_tool_round_trip() { ); } +#[tokio::test] +async fn bedrock_signs_with_the_keys_region_and_endpoint_in_the_config() { + let harness = harness().await; + harness + .json(json!({"model": "bedrock-keys", "messages": answered("tooluse_1"), "max_tokens": 16})) + .await; + + let sent = harness.sent(); + assert_eq!(sent.len(), 1, "the configured endpoint gets the call"); + assert!(sent[0].path.ends_with("/converse"), "{}", sent[0].path); + let authorization = sent[0].header("authorization").unwrap(); + assert!( + authorization.starts_with("AWS4-HMAC-SHA256 Credential=AKIACONFIGKEY/") + && authorization.contains("/us-west-2/bedrock/aws4_request"), + "{authorization}" + ); +} + #[tokio::test] async fn bedrock_tool_round_trip() { let harness = harness().await;