Skip to content

fix(bedrock): close anthropic passthrough metadata to provider schema at egress - #31017

Draft
mateo-berri wants to merge 2 commits into
litellm_internal_stagingfrom
litellm_bedrock_passthrough_anthropic_metadata_schema
Draft

fix(bedrock): close anthropic passthrough metadata to provider schema at egress#31017
mateo-berri wants to merge 2 commits into
litellm_internal_stagingfrom
litellm_bedrock_passthrough_anthropic_metadata_schema

Conversation

@mateo-berri

@mateo-berri mateo-berri commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

Relevant issues

Implements the egress hardening from the RCA in #30999 and the category-level guard tracked in #30301, for the leak reported in #30629. Complements #30985 (merged), which stops key/team/header spend tags from landing in metadata at the source; this PR adds the structural guard at the single provider egress so any other internal field that reaches metadata is dropped too.

Linear ticket

N/A

Pre-Submission checklist

  • I have added meaningful tests
  • My PR passes all unit tests on make test-unit
  • My PR's scope is as isolated as possible; it only solves 1 specific problem
  • I have requested a Greptile review by commenting @greptileai and received a Confidence Score of at least 4/5 before requesting a maintainer review

Screenshots / Proof of Fix

No mock; run against a live proxy hitting real AWS Bedrock. Run the proxy with python litellm/proxy/proxy_cli.py --config litellm/proxy/dev_config.yaml --detailed_debug --reload 2>&1 | tee litellm.log and a Bedrock invoke config model bedrock-invoke-sonnet-4-6 pointing at us.anthropic.claude-sonnet-4-6.

  1. Generate a virtual key (any key works; the guard does not depend on tags):
KEY=$(curl -s -X POST 'http://localhost:4000/key/generate' \
  -H "Authorization: Bearer $LITELLM_MASTER_KEY" -H 'Content-Type: application/json' \
  -d '{}' | python -c 'import sys,json;print(json.load(sys.stdin)["key"])')
  1. Send a Bedrock invoke passthrough request whose body carries metadata with user_id plus a non-user_id field. Before this PR AWS returns HTTP 400 metadata.tags: Extra inputs are not permitted; with this PR the field is stripped at the egress and the request returns HTTP 200, with user_id still forwarded:
curl -s -w '\nHTTP %{http_code}\n' -X POST \
  'http://localhost:4000/bedrock/model/bedrock-invoke-sonnet-4-6/invoke' \
  -H "Authorization: Bearer $KEY" -H 'Content-Type: application/json' \
  -d '{"anthropic_version":"bedrock-2023-05-31","max_tokens":16,"messages":[{"role":"user","content":"hi"}],"metadata":{"user_id":"user_abc123","tags":["CC:cost-center-123"]}}'
  1. Confirm attribution is preserved: GET /spend/logs for that request still shows user_abc123, so closing the provider schema does not drop billing data.

Type

🐛 Bug Fix

Changes

Anthropic-on-Bedrock accepts metadata.user_id and rejects every other metadata field with metadata.<field>: Extra inputs are not permitted. The proxy enriches the request metadata with LiteLLM-internal fields (spend tags, spend_logs_metadata, user_api_key_*), and the invoke/converse passthrough route forwards the request body verbatim, so that enrichment reaches AWS and 400s the request.

#30985 fixed the key/team/header tag vector at the source by routing tags into litellm_metadata for Bedrock routes. That is a per-route source patch, so it does not cover the rest of the category: any internal field that still lands in metadata, or a client-supplied non-user_id field, leaks again. This PR closes the category at the one place every invoke/converse passthrough body is serialized to AWS.

restrict_anthropic_bedrock_metadata projects the forwarded body's metadata down to the provider's closed schema (the existing AnthropicMetadata, user_id only) inside BedrockPassthroughConfig.sign_request, right before the body is signed. It is gated on anthropic_version so only Anthropic-native invoke bodies are touched; Titan/Cohere/Llama/Nova bodies and the Converse schema are forwarded unchanged. The projection builds a new body rather than mutating the cached one, so spend tracking still reads the tags it needs for billing while the bytes signed and sent to AWS carry only user_id. This mirrors the closed-schema strip already shipped for /chat/completions (#24661) and /v1/messages (validate_anthropic_api_metadata), now applied at the Bedrock passthrough egress.

Files:

  • litellm/llms/bedrock/passthrough/transformation.py: add restrict_anthropic_bedrock_metadata and call it in BedrockPassthroughConfig.sign_request
  • tests/test_litellm/llms/bedrock/passthrough/test_bedrock_passthrough_transformation.py: regression tests for stripping every non-user_id field, preserving user_id, not mutating the input body, the anthropic_version/no-metadata no-op guards, and a wiring test that fails if sign_request stops feeding the projected body to the signer

… at egress

Anthropic-on-Bedrock accepts metadata.user_id and rejects every other metadata
field with "metadata.<field>: Extra inputs are not permitted". The proxy enriches
the request metadata with internal spend fields (tags, spend_logs_metadata,
user_api_key_*) and the invoke/converse passthrough route forwards that body
verbatim, so the enrichment reaches AWS and 400s the request (#30629)

Project the forwarded metadata down to the provider's closed schema
(AnthropicMetadata, user_id only) in BedrockPassthroughConfig.sign_request, right
before the body is signed. This is a single typed egress choke point that drops
any internal field instead of chasing each enrichment site route by route, which
is the structural hardening #30301 asks for. A new body is built rather than the
cached body mutated, so spend tracking still sees the stripped fields
@codecov

codecov Bot commented Jun 22, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant