Refactor: Filtering beta header after transformation#23715
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Greptile SummaryThis PR fixes a bug where unsupported Key changes:
Confidence Score: 4/5
|
| Filename | Overview |
|---|---|
| litellm/anthropic_beta_headers_manager.py | Adds update_request_with_filtered_beta — a composite helper that filters both the anthropic-beta HTTP header and the anthropic_beta request-body field. Logic is correct for the list-based case; minor type-safety gap if anthropic_beta body value is a raw string. |
| litellm/llms/anthropic/chat/handler.py | Moves beta-header filtering to after transform_request(), so auto-detected betas (e.g. files-api-2025-04-14) added by the Vertex AI transformer are now properly filtered. Import is also cleaned up from its previous location. |
| tests/test_litellm/test_anthropic_beta_headers_filtering.py | Adds a unit test for the new utility function with correct assertions. Misses an end-to-end regression test that exercises the actual Vertex AI transform path with auto-detected file-ID betas. |
Sequence Diagram
sequenceDiagram
participant Caller
participant handler as AnthropicChatCompletion.completion()
participant transform as VertexAIAnthropicConfig.transform_request()
participant filter as update_request_with_filtered_beta()
participant config as anthropic_beta_headers_config.json
Caller->>handler: completion(model="vertex_ai/...", messages=[...file_id...])
handler->>transform: transform_request(headers, optional_params)
transform->>transform: get_anthropic_beta_list() → adds files-api-2025-04-14, code-execution-2025-05-22
transform->>transform: data["anthropic_beta"] = list(beta_set)
transform->>transform: headers["anthropic-beta"] = ",".join(beta_set)
transform-->>handler: returns data (with unfiltered betas)
handler->>filter: update_request_with_filtered_beta(headers, data, "vertex_ai")
filter->>config: load provider mapping for vertex_ai
config-->>filter: files-api-2025-04-14 → null, code-execution-2025-05-22 → (unknown)
filter->>filter: drop unsupported/unknown headers from both HTTP header and body
filter-->>handler: (filtered headers, filtered data)
handler->>Caller: HTTP POST with only supported betas
Last reviewed commit: 1a8f8c6
| existing_body_betas = request_data.get("anthropic_beta") | ||
| if not existing_body_betas: | ||
| return headers, request_data | ||
|
|
||
| filtered_body_betas = filter_and_transform_beta_headers( | ||
| beta_headers=existing_body_betas, | ||
| provider=provider, | ||
| ) |
There was a problem hiding this comment.
Missing type guard for anthropic_beta string value
filter_and_transform_beta_headers expects a List[str], but request_data["anthropic_beta"] could be a plain comma-separated string if the caller passes it through optional_params directly. If it is a string, for header in beta_headers: will iterate over individual characters ("f", "i", "l", "e", …) — none of which exist in the provider mapping — silently dropping all beta headers.
This is unlikely to occur in the current Vertex AI path (where transform_request always stores it as a list), but the function is a public utility and accepts dict without type constraints, making it brittle for future callers.
existing_body_betas = request_data.get("anthropic_beta")
if not existing_body_betas:
return headers, request_data
# Normalise: handle both list and comma-separated string
if isinstance(existing_body_betas, str):
existing_body_betas = [b.strip() for b in existing_body_betas.split(",") if b.strip()]
filtered_body_betas = filter_and_transform_beta_headers(
beta_headers=existing_body_betas,
provider=provider,
)| def test_update_request_with_filtered_beta_vertex_ai(self): | ||
| """Test combined filtering for both HTTP headers and request body betas.""" | ||
| headers = { | ||
| "anthropic-beta": "files-api-2025-04-14,context-management-2025-06-27,code-execution-2025-05-22" | ||
| } | ||
| request_data = { | ||
| "anthropic_beta": [ | ||
| "files-api-2025-04-14", | ||
| "context-management-2025-06-27", | ||
| "code-execution-2025-05-22", | ||
| ] | ||
| } | ||
|
|
||
| filtered_headers, filtered_request_data = update_request_with_filtered_beta( | ||
| headers=headers, | ||
| request_data=request_data, | ||
| provider="vertex_ai", | ||
| ) | ||
|
|
||
| assert ( | ||
| filtered_headers.get("anthropic-beta") == "context-management-2025-06-27" | ||
| ) | ||
| assert filtered_request_data.get("anthropic_beta") == [ | ||
| "context-management-2025-06-27" | ||
| ] |
There was a problem hiding this comment.
Test covers utility, not the actual bug path
The new test validates update_request_with_filtered_beta in isolation, which is useful. However, the root cause described in the PR is that auto-detected beta headers (injected by get_anthropic_beta_list inside VertexAIAnthropicConfig.transform_request) were never filtered. A more targeted regression test would simulate the real scenario:
- Call
litellm.completion(or the relevant handler directly) with avertex_aimodel and messages that contain afile_idsource block. - Assert that neither
data["anthropic_beta"]norheaders["anthropic-beta"]in the captured HTTP request containfiles-api-2025-04-14orcode-execution-2025-05-22.
Without this, a future refactor that re-introduces the same omission would not be caught by the test suite.
Relevant issues
Unsupported anthropic-beta headers sent to Vertex AI for Anthropic models
Affected versions: At least 1.82.1 (likely earlier)
Symptom:
Requests to Vertex AI Anthropic models (e.g. vertex_ai/claude-sonnet-4-6@default) fail with:
Unexpected value(s)
code-execution-2025-05-22,files-api-2025-04-14for the
anthropic-betaheader.This happens when messages contain file IDs ("source": {"type": "file", "file_id": ...}), because LiteLLM auto-detects the feature and adds the corresponding beta headers — headers that Vertex AI does not support.
Root cause:
VertexAIAnthropicConfig.transform_request() (in litellm/llms/vertex_ai/vertex_ai_partner_models/anthropic/transformation.py) builds a beta header set by calling the shared get_anthropic_beta_list() method, which unconditionally adds files-api-2025-04-14 and code-execution-2025-05-22 when file IDs are detected. This set is written directly to both data["anthropic_beta"] (request body) and headers["anthropic-beta"] (HTTP header) without passing through filter_and_transform_beta_headers().
The filtering infrastructure already exists — anthropic_beta_headers_config.json correctly marks files-api-2025-04-14 as null (unsupported) for vertex_ai, and code-execution-2025-05-22 isn't in the mapping at all (so it would also be dropped). The problem is that this filtering is never invoked on the betas generated inside transform_request():
In handler.py, update_headers_with_filtered_beta() runs before transform_request(), so its filtering gets overwritten.
In llm_http_handler.py, filtering only covers the anthropic messages pass-through path, not the standard completion path.
Pre-Submission checklist
Please complete all items before asking a LiteLLM maintainer to review your PR
tests/test_litellm/directory, Adding at least 1 test is a hard requirement - see detailsmake test-unit@greptileaiand received a Confidence Score of at least 4/5 before requesting a maintainer reviewDelays in PR merge?
If you're seeing a delay in your PR being merged, ping the LiteLLM Team on Slack (#pr-review).
CI (LiteLLM team)
Branch creation CI run
Link:
CI run for the last commit
Link:
Merge / cherry-pick CI run
Links:
Type
🐛 Bug Fix
Changes
Moved beta header filtering to after transform_request() in:
litellm/llms/anthropic/chat/handler.py (lines 362-377) - For the Anthropic-specific handler path (which Vertex AI Claude uses)
Now:
Filter headers["anthropic-beta"] using update_headers_with_filtered_beta()
Filter data["anthropic_beta"] (request body) using filter_and_transform_beta_headers()
This ensures that all beta headers—whether user-provided or auto-detected—are filtered based on the provider's support matrix in anthropic_beta_headers_config.json, where files-api-2025-04-14 is marked as null (unsupported) for vertex_ai, and code-execution-2025-05-22 isn't in the mapping at all (so it gets dropped).
Before

After
