From b41edb57c1ff349c0fd537a20a84571b25053b28 Mon Sep 17 00:00:00 2001 From: yucheng Date: Mon, 21 Sep 2026 20:42:03 +0000 Subject: [PATCH] fix(bedrock): keep batch S3 credentials out of chat requests and debug logs Register s3_access_key_id, s3_secret_access_key and s3_encryption_key_id as LiteLLM-owned batch params so they are no longer forwarded to Bedrock as additionalModelRequestFields (which 400s ordinary chat on a batch-configured deployment), keep them on CredentialLiteLLMParams so the batch/file paths still receive them, and redact the S3 credential key names in debug logs. Resolves LIT-8290 Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../litellm_core_utils/secret_redaction.py | 2 +- litellm/types/router.py | 2 + litellm/types/utils.py | 3 ++ .../coverage_registry/llm_conversational.yaml | 1 + tests/e2e/coverage_registry/schema.py | 1 + .../test_bedrock_provider_matrix_e2e.py | 44 +++++++++++++++++++ tests/e2e/models.py | 1 + tests/test_litellm/test_secret_redaction.py | 19 ++++++++ tests/test_litellm/test_utils.py | 27 ++++++++++++ ui/litellm-dashboard/src/lib/http/schema.d.ts | 8 ++++ 10 files changed, 107 insertions(+), 1 deletion(-) diff --git a/litellm/litellm_core_utils/secret_redaction.py b/litellm/litellm_core_utils/secret_redaction.py index 390abf41955..70b2cd08b4c 100644 --- a/litellm/litellm_core_utils/secret_redaction.py +++ b/litellm/litellm_core_utils/secret_redaction.py @@ -65,7 +65,7 @@ def _build_secret_patterns() -> "re.Pattern[str]": # private_key with PEM-aware value capture r"""private_key['\"]?\s*[:=]\s*['\"]?(?:-----BEGIN[A-Z \-]*PRIVATE KEY-----[\s\S]*?-----END[A-Z \-]*PRIVATE KEY-----|[^\s,'\"})\]{}>]+)""", r"(?:master_key|xai_key|database_url|db_url|connection_string|" - r"aws_secret_access_key|aws_session_token|aws_access_key_id|" + r"aws_secret_access_key|aws_session_token|aws_access_key_id|s3_secret_access_key|s3_access_key_id|" r"signing_key|encryption_key|" r"auth_token|access_token|refresh_token|" r"slack_webhook_url|webhook_url|" diff --git a/litellm/types/router.py b/litellm/types/router.py index fd426835d65..8ad01206811 100644 --- a/litellm/types/router.py +++ b/litellm/types/router.py @@ -305,6 +305,8 @@ class CredentialLiteLLMParams(BaseModel): s3_bucket_name: str | None = None s3_endpoint_url: str | None = None s3_region_name: str | None = None + s3_access_key_id: str | None = None + s3_secret_access_key: str | None = None s3_encryption_key_id: str | None = None s3_bucket_owner: str | None = None aws_batch_role_arn: str | None = None diff --git a/litellm/types/utils.py b/litellm/types/utils.py index 82cd0250857..3695be2641a 100644 --- a/litellm/types/utils.py +++ b/litellm/types/utils.py @@ -3840,6 +3840,9 @@ bedrock_batch_litellm_params: Final = ( "s3_endpoint_url", "s3_output_bucket_name", "s3_bucket_owner", + "s3_access_key_id", + "s3_secret_access_key", + "s3_encryption_key_id", "bedrock_tags", ) diff --git a/tests/e2e/coverage_registry/llm_conversational.yaml b/tests/e2e/coverage_registry/llm_conversational.yaml index 36fbd39154d..49d4d92ff0b 100644 --- a/tests/e2e/coverage_registry/llm_conversational.yaml +++ b/tests/e2e/coverage_registry/llm_conversational.yaml @@ -31,6 +31,7 @@ - {id: llm.chat_completions.bedrock_converse.thinking.nonstream.works, module: llm, tier: P1, subject_endpoint: chat_completions, route: bedrock_converse, capability: thinking, streaming: nonstream, assertions: [works], source: "model_prices json", rationale: "Anthropic thinking on Bedrock"} - {id: llm.chat_completions.bedrock_converse.response_headers.nonstream.works, module: llm, tier: P0, subject_endpoint: chat_completions, route: bedrock_converse, capability: response_headers, streaming: nonstream, assertions: [works], source: "llms/bedrock/chat/converse_handler.py:248", rationale: "Bedrock request ids must surface as llm_provider-* response headers on /chat/completions so callers can correlate calls with AWS-side logs (#37003)", fail_before_fix: proven} - {id: llm.chat_completions.bedrock_converse.response_headers.stream.works, module: llm, tier: P0, subject_endpoint: chat_completions, route: bedrock_converse, capability: response_headers, streaming: stream, assertions: [works], source: "llms/bedrock/chat/converse_handler.py:154", rationale: "The llm_provider-* headers must also surface on streaming /chat/completions, where CustomStreamWrapper carries them instead of the nonstream setter"} +- {id: llm.chat_completions.bedrock_converse.batch_deployment.nonstream.works, module: llm, tier: P0, subject_endpoint: chat_completions, route: bedrock_converse, capability: batch_deployment, streaming: nonstream, assertions: [works], source: "types/utils.py bedrock_batch_litellm_params", rationale: "A deployment carrying the documented batch-only S3 keys (s3_access_key_id, s3_secret_access_key, s3_encryption_key_id) must still serve ordinary chat; unregistered keys fall into optional_params and are forwarded as additionalModelRequestFields, which Bedrock 400s and which puts the S3 secret in the request body and debug log (LIT-8290)", fail_before_fix: proven} - {id: llm.chat_completions.bedrock_invoke.basic.nonstream.works, module: llm, tier: P0, subject_endpoint: chat_completions, route: bedrock_invoke, capability: basic, streaming: nonstream, assertions: [works], source: "proxy_server.py:8455", rationale: "Regional inference-profile ids (us.anthropic.*) over the invoke route, the deployment shape behind a customer timeout report on v1.90.0"} - {id: llm.chat_completions.bedrock_invoke.basic.stream.works, module: llm, tier: P0, subject_endpoint: chat_completions, route: bedrock_invoke, capability: basic, streaming: stream, assertions: [works], source: "proxy_server.py:8455", rationale: "Streaming with regional inference-profile ids over the invoke route"} - {id: llm.chat_completions.vertex.basic.nonstream.works, module: llm, tier: P0, subject_endpoint: chat_completions, route: vertex, capability: basic, streaming: nonstream, assertions: [works], source: "proxy_server.py:8455", rationale: "P0 route; Vertex AI"} diff --git a/tests/e2e/coverage_registry/schema.py b/tests/e2e/coverage_registry/schema.py index fa6dad90126..8b0d38a083c 100644 --- a/tests/e2e/coverage_registry/schema.py +++ b/tests/e2e/coverage_registry/schema.py @@ -63,6 +63,7 @@ LlmRoute = Literal[ LlmCapability = Literal[ "assume_role", "basic", + "batch_deployment", "count_tokens", "govcloud_partition", "input_validation", diff --git a/tests/e2e/llm_translation/test_bedrock_provider_matrix_e2e.py b/tests/e2e/llm_translation/test_bedrock_provider_matrix_e2e.py index 3c6aaa75ab3..5f0a931109c 100644 --- a/tests/e2e/llm_translation/test_bedrock_provider_matrix_e2e.py +++ b/tests/e2e/llm_translation/test_bedrock_provider_matrix_e2e.py @@ -131,6 +131,50 @@ class TestBedrockResponseHeaders: _assert_request_id_header(result) +def _register_bedrock_batch_deployment(client: PassthroughClient, resources: ResourceManager) -> str: + model = f"e2e-bedrock-batch-chat-{unique_marker()}" + model_id = client.proxy.create_model( + model, + LiteLLMParamsBody( + model=CONVERSE_REGIONAL_BACKEND, + aws_access_key_id="os.environ/AWS_ACCESS_KEY_ID", + aws_secret_access_key="os.environ/AWS_SECRET_ACCESS_KEY", + aws_region_name="os.environ/AWS_REGION", + s3_bucket_name="os.environ/AWS_BATCH_S3_BUCKET", + s3_access_key_id="os.environ/AWS_ACCESS_KEY_ID", + s3_secret_access_key="os.environ/AWS_SECRET_ACCESS_KEY", + s3_encryption_key_id=f"alias/e2e-unused-{unique_marker()}", + aws_batch_role_arn="os.environ/AWS_BATCH_ROLE_ARN", + ), + ) + resources.defer(lambda: client.proxy.delete_model(model_id)) + return model + + +class TestBedrockBatchDeploymentServesChat: + @pytest.mark.covers( + "llm.chat_completions.bedrock_converse.batch_deployment.nonstream.works", + exercised_on=[], + ) + def test_batch_s3_keys_do_not_break_chat( + self, client: PassthroughClient, resources: ResourceManager + ) -> None: + model = _register_bedrock_batch_deployment(client, resources) + key = resources.key() + + result = client.proxy.transport.send( + "/chat/completions", + headers=client.proxy.transport.bearer(key), + json=ChatBody(model=model, messages=_prompt(), max_tokens=64), + ) + + assert result.ok, ( + f"chat on a batch-configured deployment failed: {result.status_code} {result.body[:300]}; " + "batch-only S3 keys were forwarded to Bedrock as additionalModelRequestFields" + ) + _assert_completion(ChatResponse.model_validate_json(result.body)) + + class TestBedrockInvokeRegionalModelIds: @pytest.mark.covers("llm.chat_completions.bedrock_invoke.basic.nonstream.works", exercised_on=[]) def test_invoke_regional_id_completes( diff --git a/tests/e2e/models.py b/tests/e2e/models.py index 47ef672ebec..441110574eb 100644 --- a/tests/e2e/models.py +++ b/tests/e2e/models.py @@ -991,6 +991,7 @@ class LiteLLMParamsBody(BaseModel): s3_region_name: str | None = None s3_access_key_id: str | None = None s3_secret_access_key: str | None = None + s3_encryption_key_id: str | None = None aws_batch_role_arn: str | None = None aws_role_name: str | None = None aws_session_name: str | None = None diff --git a/tests/test_litellm/test_secret_redaction.py b/tests/test_litellm/test_secret_redaction.py index 85933fbf9e8..f58ade11d1c 100644 --- a/tests/test_litellm/test_secret_redaction.py +++ b/tests/test_litellm/test_secret_redaction.py @@ -629,6 +629,25 @@ def test_aws_credential_redaction_catches_quoted_values(): assert redact_string(safe) == safe +def test_bedrock_batch_s3_credential_redaction_in_deployment_dump(): + """The router logs each deployment's litellm_params at DEBUG. A Bedrock batch + deployment carries s3_secret_access_key there, which the aws_* key-name rule + did not cover, so the S3 secret was printed verbatim (LIT-8290).""" + cases = ( + "{'s3_secret_access_key': 'wJalrXUtnFEMIK7MDENGbPxRfiCYEXAMPLEKEY'}", + "s3_secret_access_key=wJalrXUtnFEMIK7MDENGbPxRfiCYEXAMPLEKEY", + "{'s3_access_key_id': 'not-an-akia-shaped-value'}", + ) + for secret_line in cases: + result = redact_string(secret_line) + assert "REDACTED" in result, f"S3 credential redaction missed: {secret_line!r}" + assert "wJalrXUtnFEMIK7MDENGbPxRfiCYEXAMPLEKEY" not in result + assert "not-an-akia-shaped-value" not in result + + safe = "'s3_bucket_name': 'my-batch-bucket'" + assert redact_string(safe) == safe + + @pytest.mark.parametrize( "extra", ( diff --git a/tests/test_litellm/test_utils.py b/tests/test_litellm/test_utils.py index 8e419b15044..6cc8b7f7565 100644 --- a/tests/test_litellm/test_utils.py +++ b/tests/test_litellm/test_utils.py @@ -4653,6 +4653,33 @@ def test_bedrock_batch_params_never_reach_the_provider(): ) +def test_documented_batch_s3_credentials_never_reach_the_provider(): + """The Bedrock batch docs tell users to put s3_access_key_id, s3_secret_access_key + and s3_encryption_key_id on the deployment. Left unregistered they are swept into + additionalModelRequestFields, Bedrock 400s ordinary chat on that deployment with + `s3_secret_access_key: Extra inputs are not permitted`, and the S3 secret is sent + to the provider and printed in the debug log (LIT-8290). + """ + configured = { + "s3_access_key_id": "configured-access-key-id", + "s3_secret_access_key": "configured-secret-access-key", + "s3_encryption_key_id": "arn:aws:kms:us-east-1:000000000000:key/configured", + } + kwargs = {"a_real_provider_specific_param": 1, **configured} + + non_default = get_non_default_completion_params(dict(kwargs)) + + assert non_default == {"a_real_provider_specific_param": 1}, ( + "documented batch S3 credentials leaked into the provider params: " + f"{sorted(set(non_default) - {'a_real_provider_specific_param'})}" + ) + + batch_params = dict(GenericLiteLLMParams(**kwargs)) + assert {field: batch_params.get(field) for field in configured} == configured, ( + "registering these must not strip them from the batch path" + ) + + def test_client_side_timeout_marker_never_reaches_the_provider(): """The proxy stamps kwargs["client_side_timeout"] = True whenever a request carries a caller-supplied timeout (body timeout / request_timeout / stream_timeout or the diff --git a/ui/litellm-dashboard/src/lib/http/schema.d.ts b/ui/litellm-dashboard/src/lib/http/schema.d.ts index 440958fa412..eb09105817a 100644 --- a/ui/litellm-dashboard/src/lib/http/schema.d.ts +++ b/ui/litellm-dashboard/src/lib/http/schema.d.ts @@ -31211,6 +31211,8 @@ export interface components { regional_processing_uplift_multiplier_us?: number | null; /** Rpm */ rpm?: number | null; + /** S3 Access Key Id */ + s3_access_key_id?: string | null; /** S3 Bucket Name */ s3_bucket_name?: string | null; /** S3 Bucket Owner */ @@ -31223,6 +31225,8 @@ export interface components { s3_output_bucket_name?: string | null; /** S3 Region Name */ s3_region_name?: string | null; + /** S3 Secret Access Key */ + s3_secret_access_key?: string | null; /** Search Context Cost Per Query */ search_context_cost_per_query?: { [key: string]: unknown; @@ -42011,6 +42015,8 @@ export interface components { regional_processing_uplift_multiplier_us?: number | null; /** Rpm */ rpm?: number | null; + /** S3 Access Key Id */ + s3_access_key_id?: string | null; /** S3 Bucket Name */ s3_bucket_name?: string | null; /** S3 Bucket Owner */ @@ -42023,6 +42029,8 @@ export interface components { s3_output_bucket_name?: string | null; /** S3 Region Name */ s3_region_name?: string | null; + /** S3 Secret Access Key */ + s3_secret_access_key?: string | null; /** Search Context Cost Per Query */ search_context_cost_per_query?: { [key: string]: unknown;