Skip to content

fix(bedrock): allowlist Bedrock Invoke body fields and filter all anthropic-beta values - #26148

Merged
mateo-berri merged 3 commits into
litellm_internal_stagingfrom
litellm_fix-bedrock-invoke-allowlist
Apr 21, 2026
Merged

fix(bedrock): allowlist Bedrock Invoke body fields and filter all anthropic-beta values#26148
mateo-berri merged 3 commits into
litellm_internal_stagingfrom
litellm_fix-bedrock-invoke-allowlist

Conversation

@mateo-berri

@mateo-berri mateo-berri commented Apr 21, 2026

Copy link
Copy Markdown
Contributor

Relevant issues

Fixes a 400 from Bedrock Invoke when Claude Code (or any client on a newer Anthropic extension) calls bedrock/us.anthropic.claude-opus-4-7 through the /v1/messages pass-through:

{"type":"error","error":{"type":"invalid_request_error","message":"context_management: Extra inputs are not permitted"}}

The narrow trigger is Claude Code sending context_management on every request. But the underlying pattern — Claude Code sends Anthropic-direct features, LiteLLM forwards them to Bedrock, Bedrock 400s on anything it doesn't recognize — will repeat whenever Anthropic ships a new body field or beta header. This PR closes both leak points.

What changed

Two fail-safes in the messages/invoke transformation (litellm/llms/bedrock/messages/invoke_transformations/anthropic_claude3_transformation.py):

1. Top-level body fields → typed allowlist

New BedrockInvokeAnthropicMessagesRequest TypedDict in litellm/types/llms/bedrock.py captures the Bedrock Invoke Anthropic Messages body schema. The runtime allowlist is derived from __annotations__ so the type and the filter share one definition — editing the type automatically updates the filter. Anchored to the AWS reference page in the TypedDict docstring and the transform comment.

Fields (13 total): anthropic_version, anthropic_beta, messages, system, max_tokens, stop_sequences, temperature, top_p, top_k, tools, tool_choice, thinking, metadata. The first 11 come straight from the AWS docs; thinking and metadata are part of the Anthropic Messages surface Bedrock implements but aren't spelled out on that AWS page (thinking is actively constructed for Opus 4.5 / Sonnet 4 extended thinking).

Drops context_management, output_config, speed, mcp_servers, container, inference_geo, cache_control top-level, internal litellm_metadata, and any future Anthropic addition Claude Code starts sending. output_format stays as an active inline-schema conversion rather than a plain strip. Stripped keys logged at DEBUG.

An exact-set assertion test pins the resolved allowlist so any future edit forces a conscious review — you can't silently broaden or narrow the surface.

2. anthropic-beta values → filtered + transformed for the full union (not just auto-injected)

Previously the code only ran filter_and_transform_beta_headers over auto-injected betas (beta_set - user_beta_set) and union'd user-provided values back in untouched. That left a real hole: a client (Claude Code, any SDK) on a new Anthropic-direct beta — e.g. advisor-tool-2026-03-01, or the context-management-2025-06-27 header that pairs with the body-field fix above — gets forwarded as-is and 400s. It also skipped the Bedrock-side rename (e.g. advanced-tool-use-2025-11-20tool-search-tool-2025-10-19), so even supported betas could fail because of Anthropic-spelling.

Mental model flip: in a proxy deployment the client doesn't know the backend, so its beta-header declarations are advisory; the provider mapping is authoritative. The code now filters + transforms the full beta_set, and warns at WARNING when a user-provided beta gets dropped so intentional overrides leave a breadcrumb.

Drive-by test fix

tests/test_litellm/llms/bedrock/test_anthropic_beta_support.py::test_messages_transformation_anthropic_beta asserted on output-128k-2025-02-19 passing through — a beta that's null in the Bedrock mapping and would 400 at runtime. The test was encoding the old buggy-pass-through as expected behavior. Rewrote it against context-1m-2025-08-07 (which IS in the Bedrock mapping) to preserve the actual intent.

Scope

Messages pass-through only. The same user-beta bypass exists in chat/invoke_transformations/ but that path is the OpenAI-format → Bedrock route where manual beta-header use is more common and filter behavior may need different trade-offs — intentionally left as follow-up.

Pre-Submission checklist

  • Added tests in tests/test_litellm/llms/bedrock/messages/invoke_transformations/test_anthropic_claude3_transformation.py:
    • test_bedrock_invoke_allowlist_exact_contents — exact-set pin on the resolved allowlist
    • test_bedrock_messages_strips_context_management — reproduces the reported trace
    • test_bedrock_messages_allowlist_filters_anthropic_only_fields — allowlist vs 8 Anthropic-only/internal fields
    • test_bedrock_messages_filters_user_provided_unsupported_beta_header — unsupported user beta dropped, supported one passes
    • test_bedrock_messages_renames_user_provided_aliased_beta_header — user-provided alias rewritten to Bedrock-side spelling
  • make test-unit — ran full tests/test_litellm/llms/bedrock/ suite, 485 tests pass
  • Scope isolated to Bedrock Invoke's Anthropic Messages path
  • Greptile review requested

Screenshots / Proof of Fix

Before: Claude Code → LiteLLM proxy → bedrock/us.anthropic.claude-opus-4-7 returns 400 "context_management: Extra inputs are not permitted" (trace 8fa3e31a-8027-46e7-a77a-276eef3ae500).

After: the context_management body field and any unsupported beta-header values are stripped before signing. Per-case regression tests green on this branch and red on main.

Type

🐛 Bug Fix

Changes

  • litellm/types/llms/bedrock.py — new BedrockInvokeAnthropicMessagesRequest TypedDict
  • litellm/llms/bedrock/messages/invoke_transformations/anthropic_claude3_transformation.py — allowlist derived from TypedDict, beta-header full-union filter
  • tests/test_litellm/llms/bedrock/messages/invoke_transformations/test_anthropic_claude3_transformation.py — 5 new tests
  • tests/test_litellm/llms/bedrock/test_anthropic_beta_support.py — drive-by: updated test to use a bedrock-supported beta

@CLAassistant

Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.
You have signed the CLA already but the status is still pending? Let us recheck it.

@greptile-apps

greptile-apps Bot commented Apr 21, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes a 400 \"Extra inputs are not permitted\" error on Bedrock Invoke by (1) adding a TypedDict-driven allowlist that strips any top-level body field Bedrock doesn't recognise (catching context_management, output_config, speed, mcp_servers, and future additions in one pass) and (2) applying the Bedrock beta-header mapping to the full union of auto-injected and user-provided anthropic-beta values instead of only the auto-injected subset.

The TypedDict design (BedrockInvokeAnthropicMessagesRequest) is a clean single-source-of-truth; four of the five promised regression tests are solid. The one missing test (test_bedrock_invoke_allowlist_exact_contents — the exact-set pin the PR description specifically called out as a forced-review guard) and the lack of a feature flag for the user-beta behavioral change are the only noteworthy gaps.

Confidence Score: 5/5

Safe to merge; all remaining findings are P2 style/coverage gaps that don't affect correctness on the happy path.

The core fix (allowlist + full-union beta filtering) is correct and well-tested for the reported failure mode. All open findings are P2: a missing test that was promised but not added, a backwards-compatibility note (the affected scenario — user-provided betas that previously 400'd at Bedrock — was already broken), and a minor redundant function call. No P0/P1 issues identified.

anthropic_claude3_transformation.py (backwards-compat note on user beta filtering) and the test file (missing exact-set allowlist pin test).

Important Files Changed

Filename Overview
litellm/llms/bedrock/messages/invoke_transformations/anthropic_claude3_transformation.py Core transformation logic: adds TypedDict-driven allowlist filtering (step 7) and moves full-union beta filtering to cover user-provided headers too; removes dedicated output_config pop (now handled by allowlist). Logic is sound but includes a redundant per-beta filter call for the warning log and a backwards-incompatible behavioral change for user-supplied betas with no feature flag.
litellm/types/llms/bedrock.py Adds BedrockInvokeAnthropicMessagesRequest TypedDict with 13 allowlisted fields anchored to the AWS docs; clean single-source-of-truth design with a helpful docstring. No issues.
tests/test_litellm/llms/bedrock/messages/invoke_transformations/test_anthropic_claude3_transformation.py Four of the five promised new tests are present and well-scoped. The exact-set allowlist pin test (test_bedrock_invoke_allowlist_exact_contents) mentioned as a critical guard in the PR description is absent.
tests/test_litellm/llms/bedrock/test_anthropic_beta_support.py Drive-by fix: replaces output-128k-2025-02-19 (null/unsupported in Bedrock mapping, old test encoded buggy pass-through) with context-1m-2025-08-07 (properly supported). Change is correct and improves test validity.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[Client Request
/v1/messages → Bedrock] --> B[transform_anthropic_messages_request]
    B --> C[Steps 1-5: build request,
pop output_format, remove custom tools]
    C --> D[Step 6: Build beta_set
user_beta_set + auto_betas]
    D --> E[filter_and_transform_beta_headers
full beta_set through Bedrock mapping]
    E --> F{filtered_betas
non-empty?}
    F -- Yes --> G[Set anthropic_beta
in request]
    F -- No --> H[Skip anthropic_beta]
    G --> I[Step 7: Allowlist filter
BedrockInvokeAnthropicMessagesRequest
.__annotations__.keys]
    H --> I
    I --> J{Field in
allowlist?}
    J -- Yes --> K[Keep field]
    J -- No --> L[Strip field
+ DEBUG log]
    K --> M[Signed Bedrock
InvokeModel request]
    L --> M

    style E fill:#d4edda
    style I fill:#d4edda
    style L fill:#fff3cd
Loading

Reviews (5): Last reviewed commit: "chore: make `uv` newer than 0.10 allowab..." | Re-trigger Greptile

# litellm_metadata, and any future Anthropic additions Claude Code may send)
# is filtered out before the request is forwarded, preventing
# "Extra inputs are not permitted" 400s.
BEDROCK_INVOKE_ALLOWED_TOP_LEVEL_FIELDS = frozenset(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this not just a type, Bedrock Invoke Request body ?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great call out -- it's a type now

@Rainson12

Copy link
Copy Markdown

is there any workaround available until this is merged? Seems like claude code is not working for me with litellm as its not just happening with opus 4.7 but also 4.6 and sonnet 4.6 when using aws bedrock?

@mateo-berri
mateo-berri force-pushed the litellm_fix-bedrock-invoke-allowlist branch from bd15e3f to 77465ef Compare April 21, 2026 18:00
@mateo-berri
mateo-berri temporarily deployed to integration-postgres April 21, 2026 18:00 — with GitHub Actions Inactive
@mateo-berri
mateo-berri temporarily deployed to integration-postgres April 21, 2026 18:00 — with GitHub Actions Inactive
@mateo-berri
mateo-berri temporarily deployed to integration-postgres April 21, 2026 18:00 — with GitHub Actions Inactive
@mateo-berri mateo-berri changed the title fix(bedrock): allowlist top-level fields in Bedrock Invoke Anthropic Messages body fix(bedrock): allowlist Bedrock Invoke body fields and filter all anthropic-beta values Apr 21, 2026
@mateo-berri
mateo-berri force-pushed the litellm_fix-bedrock-invoke-allowlist branch from 77465ef to 47c77f4 Compare April 21, 2026 18:16
@mateo-berri
mateo-berri temporarily deployed to integration-postgres April 21, 2026 18:16 — with GitHub Actions Inactive
@mateo-berri
mateo-berri temporarily deployed to integration-postgres April 21, 2026 18:17 — with GitHub Actions Inactive
@mateo-berri
mateo-berri force-pushed the litellm_fix-bedrock-invoke-allowlist branch from 47c77f4 to 363d9ce Compare April 21, 2026 18:22
@mateo-berri
mateo-berri force-pushed the litellm_fix-bedrock-invoke-allowlist branch from 363d9ce to 021a702 Compare April 21, 2026 18:24
…hropic-beta values

Two fail-safes for the /v1/messages → Bedrock Invoke pass-through so new
Anthropic-only extensions Claude Code starts sending can't reach Bedrock
and trigger a 400 "Extra inputs are not permitted":

1. Top-level body fields are filtered to a typed allowlist. New
   `BedrockInvokeAnthropicMessagesRequest` TypedDict (in
   `litellm/types/llms/bedrock.py`) captures the Bedrock Invoke Anthropic
   Messages body schema; the runtime allowlist is derived from its
   `__annotations__` so the type and the filter can't drift. Anchored to
   the AWS reference page in docstrings + transform comment. An
   exact-set test pins the resolved allowlist so any future edit forces
   conscious review.

   Drops context_management, output_config, speed, mcp_servers,
   container, inference_geo, internal litellm_metadata, and any future
   Anthropic addition. output_format stays as an active inline-schema
   conversion (not just a strip).

2. The anthropic-beta header list is filtered + transformed against the
   bedrock mapping for ALL betas, not just auto-injected ones. The
   previous code union'd user-provided betas back in unfiltered, so a
   client on a new Anthropic-direct beta (e.g. advisor-tool-…,
   context-management-…) could still pin the request to fail. In a proxy
   context the client can't know the backend is Bedrock; the provider
   mapping is authoritative. User-provided drops are logged at WARNING
   so intentional overrides leave a breadcrumb.

Updates one existing test that happened to assert on the old buggy
pass-through (it used output-128k-2025-02-19, which is null in the
bedrock mapping and would 400 at runtime); rewrote it against a
bedrock-supported beta.

Scope: messages/invoke only. The same user-beta bypass exists in
chat/invoke but that's a different code path with different
user-expectation trade-offs — follow-up.
@mateo-berri
mateo-berri force-pushed the litellm_fix-bedrock-invoke-allowlist branch from 021a702 to 583bdd3 Compare April 21, 2026 18:26
@mateo-berri
mateo-berri temporarily deployed to integration-postgres April 21, 2026 18:27 — with GitHub Actions Inactive
@mateo-berri
mateo-berri temporarily deployed to integration-postgres April 21, 2026 18:27 — with GitHub Actions Inactive
@gitguardian

gitguardian Bot commented Apr 21, 2026

Copy link
Copy Markdown

⚠️ GitGuardian has uncovered 1 secret following the scan of your pull request.

Please consider investigating the findings and remediating the incidents. Failure to do so may lead to compromising the associated services or software components.

🔎 Detected hardcoded secret in your pull request
GitGuardian id GitGuardian status Secret Commit Filename
29203053 Triggered Generic Password 6da9ee9 .circleci/config.yml View secret
🛠 Guidelines to remediate hardcoded secrets
  1. Understand the implications of revoking this secret by investigating where it is used in your code.
  2. Replace and store your secret safely. Learn here the best practices.
  3. Revoke and rotate this secret.
  4. If possible, rewrite git history. Rewriting git history is not a trivial act. You might completely break other contributing developers' workflow and you risk accidentally deleting legitimate data.

To avoid such incidents in the future consider


🦉 GitGuardian detects secrets in your source code to help developers and security teams secure the modern development process. You are seeing this because you or someone else with access to this repository has authorized GitGuardian to scan your pull request.

@mateo-berri
mateo-berri temporarily deployed to integration-postgres April 21, 2026 18:40 — with GitHub Actions Inactive
@mateo-berri
mateo-berri temporarily deployed to integration-postgres April 21, 2026 18:40 — with GitHub Actions Inactive
@mateo-berri
mateo-berri temporarily deployed to integration-postgres April 21, 2026 18:40 — with GitHub Actions Inactive
@mateo-berri
mateo-berri temporarily deployed to integration-postgres April 21, 2026 18:40 — with GitHub Actions Inactive
@mateo-berri

Copy link
Copy Markdown
Contributor Author

⚠️ GitGuardian has uncovered 1 secret following the scan of your pull request.

Please consider investigating the findings and remediating the incidents. Failure to do so may lead to compromising the associated services or software components.

🔎 Detected hardcoded secret in your pull request

GitGuardian id GitGuardian status Secret Commit Filename
29203053 Triggered Generic Password 6da9ee9 .circleci/config.yml View secret
🛠 Guidelines to remediate hardcoded secrets

  1. Understand the implications of revoking this secret by investigating where it is used in your code.
  2. Replace and store your secret safely. Learn here the best practices.
  3. Revoke and rotate this secret.
  4. If possible, rewrite git history. Rewriting git history is not a trivial act. You might completely break other contributing developers' workflow and you risk accidentally deleting legitimate data.

To avoid such incidents in the future consider

🦉 GitGuardian detects secrets in your source code to help developers and security teams secure the modern development process. You are seeing this because you or someone else with access to this repository has authorized GitGuardian to scan your pull request.

Seems like this is a result of merging origin/litellm_internal_staging

@mateo-berri

Copy link
Copy Markdown
Contributor Author

is there any workaround available until this is merged? Seems like claude code is not working for me with litellm as its not just happening with opus 4.7 but also 4.6 and sonnet 4.6 when using aws bedrock?

@Rainson12 For now, add this header to your settings file: CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS": "1"

@codecov

codecov Bot commented Apr 21, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@mateo-berri

Copy link
Copy Markdown
Contributor Author

Tested working on:

Claude Code v2.1.116
bedrock/us.anthropic.claude-opus-4-6-v1, bedrock/us.anthropic.claude-opus-4-7, and bedrock/us.anthropic.claude-sonnet-4-6 with max effort

@mateo-berri
mateo-berri merged commit df9d6c7 into litellm_internal_staging Apr 21, 2026
98 of 99 checks passed
@mateo-berri
mateo-berri deleted the litellm_fix-bedrock-invoke-allowlist branch April 21, 2026 20:22
songkuan-zheng added a commit to GhishaDev/litellm that referenced this pull request Jun 4, 2026
… 6e) (#60)

* fix(passthrough): record real TTFT and start_time for streaming requests

Pass-through streaming requests (/v1/messages, /vertex_ai/*, /gemini/*,
/cohere/*, /assemblyai/*, /openai/*, /cursor/*) all share
PassThroughStreamingHandler.chunk_processor, which had two timing bugs
that interacted to collapse spend_logs.completionStartTime onto
spend_logs.endTime (off by ~1ms of clock resolution, not literally
identical) — making the streaming phase (endTime - completionStartTime)
round to roughly zero and TTFT effectively soak up the entire request
duration for every pass-through streaming row.

Root cause

1. `start_time` arg too late. The caller's start_time originates in
   BaseAnthropicMessagesStreamingIterator.__init__, which runs AFTER
   the upstream HTTP response has already been received. SpendLogs.
   startTime therefore reflects "moment we started reading the
   stream", not "moment the client request entered the proxy" — the
   real TTFT window is silently subtracted from Duration.

2. First-chunk arrival never recorded. The chunk loop yielded bytes
   to the client and collected them for logging, but never noted when
   the first byte arrived. With litellm_logging_obj.completion_start_time
   left as None, the fallback at litellm_logging.py:1834-1837 sets it
   to end_time — completionStartTime lands within ~1ms of endTime and
   streaming_phase rounds to 0.

Both bugs hide each other. Fixing only #2 gives TTFT close to 0 with
Duration deflated by ~TTFT. Fixing only #1 leaves completionStartTime
still pinned to endTime. Both must be fixed for the math to be correct.

Fix

In chunk_processor, at the top of the try block:
  - Override start_time with litellm_logging_obj.start_time when the
    latter is an earlier datetime — that's the true request-entry
    timestamp set in common_request_processing.base_process_llm_request.
  - On the first chunk yielded by response.aiter_bytes(), call
    litellm_logging_obj._update_completion_start_time(datetime.now())
    to populate the field that downstream payload builders look for.

Verified end-to-end (Anthropic claude-sonnet-4-6, 200-word stream):

  Before fix:  Duration=6528ms TTFT=6527ms streaming_phase=1ms
  After fix:   Duration=8496ms TTFT=2373ms streaming_phase=6123ms
  Control:     /v1/chat/completions (same model+prompt)
               Duration=8143ms TTFT=2071ms streaming_phase=6072ms

Test plan
  - New e2e case 13 (`13_passthrough_streaming_ttft.md` + data/13_*.sh)
    sends a real ~200-word streamed completion through /v1/messages,
    polls spend_logs for up to 30s, asserts:
      * streaming_phase_ms > 1000   (catches bug #2 regression)
      * ttft_ms > 300              (catches bug #1 regression)
      * ttft_ms < duration_ms / 2  (catches either regression)
    Plus a soft parity check vs /v1/chat/completions for the same
    upstream model.
  - Cost ~$0.005 per case run.
  - GREEN against the fix; was RED before (streaming_phase=1ms,
    ttft=6527ms) — assertions 1 and 3 fired.

Blast radius: all pass-through endpoints — they all flow through this
single chunk_processor. User noticed the bug via Anthropic; the same
fix improves TTFT observability for Vertex AI, Gemini, Cohere, and
the other pass-through providers in the same release.

* hotfix(bedrock-anthropic): clean stray conflict markers from Wave 6d merge

PR #59 (Wave 6d) accidentally committed unresolved cherry-pick conflict
markers in the Bedrock anthropic_claude3_transformation Invoke filter.
The file is broken in ship/v1.87.0 — the Python module fails to import.

Fix: apply the intended Wave 6d resolution — keep upstream's single
`filtered_betas = sorted(...)` shape from PR BerriAI#26148, layer our
`overrides=_overrides` argument on top so the
`anthropic_beta_overrides` per-deployment config still threads through.

This commit only touches the unresolved hunk; no behavioral change vs
the intent of Wave 6d.
fzowl pushed a commit to fzowl/litellm that referenced this pull request Jun 24, 2026
…oke-allowlist

fix(bedrock): allowlist Bedrock Invoke body fields and filter all anthropic-beta values
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.

5 participants