Skip to content

[Bedrock] Fix Anthropic file_id support - async path + document URL→base64 + beta header filtering - #25047

Merged
ishaan-berri merged 1 commit into
litellm_internal_staging_04_02_2026from
litellm_fix_anthropic_file_id_v2
Apr 3, 2026
Merged

[Bedrock] Fix Anthropic file_id support - async path + document URL→base64 + beta header filtering#25047
ishaan-berri merged 1 commit into
litellm_internal_staging_04_02_2026from
litellm_fix_anthropic_file_id_v2

Conversation

@ishaan-berri

Copy link
Copy Markdown
Contributor

Relevant issues

Pre-Submission checklist

  • I have Added testing in the tests/test_litellm/ directory, Adding at least 1 test is a hard requirement - see details
  • 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

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

Three fixes for Anthropic file_id / document support on Bedrock:

1. Async Claude Messages API routing (invoke_handler.py)
Async calls to Claude models that use the Messages API were falling through to the sync path. Added _async_anthropic_messages_completion and route async calls there when acompletion=True and the model uses the Messages API.

2. Document URL → base64 conversion (bedrock/chat/.../anthropic_claude3_transformation.py)
Bedrock Invoke doesn't accept source.type = "url" for document blocks. Added _convert_document_url_sources_to_base64 (sync) and _async_convert_document_url_sources_to_base64 (async) to convert URL-sourced documents to base64 before sending. Also added async_transform_request so the async path gets the same transformation logic as the sync path.

3. Beta header filtering (bedrock/chat/ and bedrock/messages/)
Both transformation classes now use filter_and_transform_beta_headers(provider="bedrock") to strip beta headers that Bedrock doesn't support (e.g. files-api-2025-04-14) before setting anthropic_beta. Previously these were passed through unfiltered, causing Bedrock to reject requests with file_id content.

4. Test URL fix (base_llm_unit_tests.py)
The file_id test was using a Wikimedia URL that 403s in CI. Switched to a fixture PDF in the repo.

@vercel

vercel Bot commented Apr 3, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
litellm Ready Ready Preview, Comment Apr 3, 2026 4:04am

Request Review

@codspeed-hq

codspeed-hq Bot commented Apr 3, 2026

Copy link
Copy Markdown
Contributor

Merging this PR will not alter performance

✅ 16 untouched benchmarks


Comparing litellm_fix_anthropic_file_id_v2 (7211891) with main (4094801)

Open in CodSpeed

@greptile-apps

greptile-apps Bot commented Apr 3, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes three issues in Bedrock's Anthropic (Claude 3+) integration: (1) async calls via the Invoke API now correctly route to a dedicated _async_anthropic_messages_completion method instead of falling through to a legacy sync path; (2) document blocks with source.type == \"url\" are now converted to base64 before submission, since Bedrock Invoke does not accept URL sources; (3) both the Invoke and Messages transformation classes now filter auto-generated beta headers through filter_and_transform_beta_headers(provider=\"bedrock\"), preventing unsupported entries such as files-api-2025-04-14 from reaching Bedrock.

Key observations:

  • The PR description references a test-URL fix in base_llm_unit_tests.py, but no test file is present in the commit. The pre-submission checklist item for tests is also left unchecked — this is a hard requirement per the project's contributing guide.
  • The document URL → base64 conversion delegates to convert_url_to_base64 / async_convert_url_to_base64, which are image-specific helpers. If a PDF server doesn't return a Content-Type header, the underlying _process_image_response raises \"Unsupported image format\" (an image-specific error), because .pdf is not in its extension fallback map.
  • The beta header filter is applied only to auto-generated betas; headers explicitly passed by the caller bypass it. If a user manually passes files-api-2025-04-14 in extra_headers, Bedrock will still reject the request.
  • The sync and async paths for is_claude_messages_api_model models now use different request transformations (XML-based sync vs. full AnthropicConfig async), leaving a subtle correctness inconsistency for sync callers."

Confidence Score: 4/5

Safe to merge with minor risk — the core logic is sound, but the missing tests and a potential silent failure for edge-case PDF URLs warrant attention before merging

The three main bug fixes (async routing, URL→base64 conversion, beta header filtering) are logically correct and address real Bedrock failures. Score is 4 rather than 5 because: (1) no tests were added despite a hard project requirement and the checklist being unchecked, (2) the document URL conversion reuses image-only download infrastructure that will raise a misleading 'Unsupported image format' error for PDFs served without a Content-Type header, and (3) user-supplied beta headers bypass the Bedrock filter, so the fix is incomplete for explicit header passthrough.

litellm/llms/bedrock/chat/invoke_transformations/anthropic_claude3_transformation.py — document URL conversion and beta filter logic; litellm/llms/bedrock/chat/invoke_handler.py — sync/async format inconsistency for is_claude_messages_api_model

Important Files Changed

Filename Overview
litellm/llms/bedrock/chat/invoke_handler.py Adds _async_anthropic_messages_completion to correctly route async Claude 3+ Bedrock Invoke calls to the Messages API transformation; sync path for the same models still uses a different (older) request format, creating a sync/async inconsistency
litellm/llms/bedrock/chat/invoke_transformations/anthropic_claude3_transformation.py Refactors transform_request into base + beta header helpers, adds async_transform_request, and URL→base64 document conversion; beta filter correctly excludes unsupported auto-betas but leaves user-supplied headers unfiltered; document conversion reuses image-only download infrastructure which can fail with a misleading error for PDFs served without Content-Type headers
litellm/llms/bedrock/messages/invoke_transformations/anthropic_claude3_transformation.py Applies the same auto-only beta header filtering pattern as the invoke path; user-supplied betas still bypass the Bedrock filter, which is the same minor concern

Sequence Diagram

sequenceDiagram
    participant Caller
    participant BedrockLLM
    participant AmazonAnthropicClaudeConfig
    participant BetaFilter as filter_and_transform_beta_headers
    participant URLFetcher as convert_url_to_base64

    Caller->>BedrockLLM: acompletion(model=claude-3+, messages)
    BedrockLLM->>BedrockLLM: Build endpoint_url (/invoke or /invoke-with-response-stream)
    BedrockLLM->>BedrockLLM: is_claude_messages_api_model? → true
    BedrockLLM->>BedrockLLM: _async_anthropic_messages_completion()
    BedrockLLM->>AmazonAnthropicClaudeConfig: async_transform_request()
    AmazonAnthropicClaudeConfig->>AmazonAnthropicClaudeConfig: _build_bedrock_anthropic_request_base()
    AmazonAnthropicClaudeConfig->>URLFetcher: _async_convert_document_url_sources_to_base64()
    URLFetcher-->>AmazonAnthropicClaudeConfig: base64-encoded document
    AmazonAnthropicClaudeConfig->>BetaFilter: _compute_bedrock_invoke_beta_headers() [auto betas only]
    BetaFilter-->>AmazonAnthropicClaudeConfig: filtered beta list (files-api-2025-04-14 removed)
    AmazonAnthropicClaudeConfig-->>BedrockLLM: transformed request dict
    BedrockLLM->>BedrockLLM: sign request headers (AWS SigV4)
    BedrockLLM->>BedrockLLM: async_streaming / async_completion
    BedrockLLM-->>Caller: ModelResponse / CustomStreamWrapper
Loading

Reviews (1): Last reviewed commit: "[Bedrock] Fix Anthropic file_id support ..." | Re-trigger Greptile

Comment on lines +244 to +253
base64_url = convert_url_to_base64(url=source_url)
image_chunk = convert_to_anthropic_image_obj(
openai_image_url=base64_url,
format=inferred_format,
)
block["source"] = {
"type": "base64",
"media_type": image_chunk["media_type"],
"data": image_chunk["data"],
}

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.

P1 Document URL conversion reuses image-only infrastructure

convert_url_to_base64 and async_convert_url_to_base64 are backed by _process_image_response, which has a fallback MIME-type lookup that only covers image extensions (jpg, jpeg, png, gif, webp). When the HTTP server doesn't return a Content-Type header and the URL path doesn't end with one of those extensions (e.g. https://example.com/report.pdf from an S3 bucket without explicit Content-Type), the function raises:

"Error: Unsupported image format. Format=None. Supported types = ['image/jpeg', 'image/png', 'image/gif', 'image/webp']"

This is a confusing, misleading error for a document-handling code path. Any PDF URL served from storage without an explicit Content-Type: application/pdf response header will fail here. The inferred_format = "application/pdf" variable set above is never reached in that failure case — it is only used as an argument to convert_to_anthropic_image_obj after convert_url_to_base64 already returns.

Consider wrapping the call with error translation, or using httpx directly to fetch + encode the document instead of delegating to the image utility. The same issue applies to _async_convert_document_url_sources_to_base64 (line 285).

Comment on lines +210 to +214
auto_beta_list = filter_and_transform_beta_headers(
beta_headers=list(beta_set - user_beta_set),
provider="bedrock",
)
return sorted(user_beta_set.union(set(auto_beta_list)))

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.

P2 User-provided beta headers bypass Bedrock filtering

The filter is applied only to auto-generated betas (beta_set - user_beta_set). Any beta header the caller explicitly supplies (via extra_headers) is passed through to Bedrock unchanged:

auto_beta_list = filter_and_transform_beta_headers(
    beta_headers=list(beta_set - user_beta_set),   # ← user betas never filtered
    provider="bedrock",
)
return sorted(user_beta_set.union(set(auto_beta_list)))

If a user explicitly passes anthropic-beta: files-api-2025-04-14 (or any other header unsupported by Bedrock) in extra_headers, it will still be forwarded and cause Bedrock to reject the request with the same error this PR set out to fix. The same pattern exists in bedrock/messages/invoke_transformations/anthropic_claude3_transformation.py at line ~469.

If the intent is "always respect user-supplied headers", that should be documented. Otherwise both user_beta_set and auto_betas should be filtered.

Comment on lines +858 to +882
if acompletion and provider == "anthropic" and self.is_claude_messages_api_model(
model
):
if isinstance(client, HTTPHandler):
client = None
return self._async_anthropic_messages_completion(
model=model,
messages=messages,
endpoint_url=endpoint_url,
proxy_endpoint_url=proxy_endpoint_url,
credentials=credentials,
aws_region_name=aws_region_name,
model_response=model_response,
print_verbose=print_verbose,
encoding=encoding,
logging_obj=logging_obj,
optional_params=optional_params,
stream=stream,
litellm_params=litellm_params,
logger_fn=logger_fn,
extra_headers=extra_headers,
timeout=timeout,
client=client,
stream_chunk_size=stream_chunk_size,
) # type: ignore[return-value]

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.

P2 Sync and async is_claude_messages_api_model paths use different request formats

The new async branch calls AmazonAnthropicClaudeConfig().async_transform_request, which produces a proper Anthropic Messages API request (JSON format via AnthropicConfig.transform_request).

However, the sync branch at line 916 for the same is_claude_messages_api_model(model) == True case still uses the older XML-based path:

messages = prompt_factory(model=model, messages=messages, custom_llm_provider="anthropic_xml")
data = json.dumps({"messages": messages, **inference_params})

This means an identical call to the same Claude 3+ Bedrock model would produce a different request payload depending on whether it's sync or async. The async path is likely the more correct one since it uses the full AnthropicConfig transformation logic. A follow-up to align the sync path (or at minimum a comment noting the discrepancy) would be valuable to avoid subtle behavior differences.

Comment on lines +216 to +294
def _convert_document_url_sources_to_base64(self, anthropic_request: dict) -> None:
"""
Bedrock Invoke does not accept document URL sources. Convert to base64 payloads.
"""
messages = anthropic_request.get("messages")
if not isinstance(messages, list):
return

for message in messages:
if not isinstance(message, dict):
continue
content = message.get("content")
if not isinstance(content, list):
continue

for block in content:
if not isinstance(block, dict) or block.get("type") != "document":
continue
source = block.get("source")
if not isinstance(source, dict) or source.get("type") != "url":
continue
source_url = source.get("url")
if not isinstance(source_url, str):
continue

inferred_format: Optional[str] = None
if source_url.lower().endswith(".pdf"):
inferred_format = "application/pdf"
base64_url = convert_url_to_base64(url=source_url)
image_chunk = convert_to_anthropic_image_obj(
openai_image_url=base64_url,
format=inferred_format,
)
block["source"] = {
"type": "base64",
"media_type": image_chunk["media_type"],
"data": image_chunk["data"],
}

async def _async_convert_document_url_sources_to_base64(
self, anthropic_request: dict
) -> None:
"""
Async version of document URL conversion for async completion paths.
"""
messages = anthropic_request.get("messages")
if not isinstance(messages, list):
return

for message in messages:
if not isinstance(message, dict):
continue
content = message.get("content")
if not isinstance(content, list):
continue

for block in content:
if not isinstance(block, dict) or block.get("type") != "document":
continue
source = block.get("source")
if not isinstance(source, dict) or source.get("type") != "url":
continue
source_url = source.get("url")
if not isinstance(source_url, str):
continue

inferred_format: Optional[str] = None
if source_url.lower().endswith(".pdf"):
inferred_format = "application/pdf"
base64_url = await async_convert_url_to_base64(url=source_url)
image_chunk = convert_to_anthropic_image_obj(
openai_image_url=base64_url,
format=inferred_format,
)
block["source"] = {
"type": "base64",
"media_type": image_chunk["media_type"],
"data": image_chunk["data"],
}

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.

P2 Duplicated loop body between sync and async URL conversion methods

_convert_document_url_sources_to_base64 and _async_convert_document_url_sources_to_base64 share identical structure — the only difference is await async_convert_url_to_base64(...) vs convert_url_to_base64(...). Any future bug fix or format extension (e.g. supporting .txt or .docx) would need to be applied to both methods.

Consider extracting the inner conversion into a small sync helper, then having the async method call await async_convert_url_to_base64 only at the I/O boundary:

def _build_base64_source(self, source_url: str) -> dict:
    inferred_format = "application/pdf" if source_url.lower().endswith(".pdf") else None
    base64_url = convert_url_to_base64(url=source_url)
    chunk = convert_to_anthropic_image_obj(openai_image_url=base64_url, format=inferred_format)
    return {"type": "base64", "media_type": chunk["media_type"], "data": chunk["data"]}

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

@ishaan-berri
ishaan-berri changed the base branch from main to litellm_internal_staging_04_02_2026 April 3, 2026 04:12
@ishaan-berri
ishaan-berri merged commit 957b654 into litellm_internal_staging_04_02_2026 Apr 3, 2026
108 of 114 checks passed
@ishaan-berri
ishaan-berri deleted the litellm_fix_anthropic_file_id_v2 branch April 3, 2026 04:12
@ishaan-berri ishaan-berri mentioned this pull request Apr 3, 2026
7 tasks
ishaan-berri added a commit that referenced this pull request Apr 3, 2026
…ase64 + beta header filtering (#25047) (#25050)

Co-authored-by: Ishaan Jaffer <ishaanjaffer0324@gmail.com>
fzowl pushed a commit to fzowl/litellm that referenced this pull request Jun 24, 2026
…ase64 + beta header filtering (BerriAI#25047) (BerriAI#25050)

Co-authored-by: Ishaan Jaffer <ishaanjaffer0324@gmail.com>
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.

2 participants