fix(anthropic): skip non-OpenAI file content blocks in file-id discovery helpers - #26228
Conversation
Greptile SummaryThis PR fixes a Confidence Score: 5/5Safe to merge — targeted two-line fix with comprehensive regression tests and no behaviour change for well-formed OpenAI blocks. Both changed call sites are mechanically equivalent; the No files require special attention.
|
| Filename | Overview |
|---|---|
| litellm/litellm_core_utils/prompt_templates/common_utils.py | Two minimal defensive guards added: .get("file") + isinstance(..., dict) check in both update_messages_with_model_file_ids and get_file_ids_from_messages; non-OpenAI blocks are now skipped instead of raising KeyError. |
| tests/test_litellm/litellm_core_utils/prompt_templates/test_litellm_core_utils_prompt_templates_common_utils.py | Five new regression tests added covering LangChain v1 shape, OpenAI happy path, mixed shapes, non-dict file field, and the remap path; no network calls, no existing tests modified. |
Flowchart
%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[validate_environment] --> B[get_file_ids_from_messages]
A --> C[update_messages_with_model_file_ids]
B --> D{c type == file?}
D -- No --> E[skip block]
D -- Yes --> F[file_object.get 'file']
F --> G{isinstance dict?}
G -- No --> H[skip — non-OpenAI block\ne.g. LangChain v1]
G -- Yes --> I[extract file_id]
C --> J{c type == file?}
J -- No --> K[skip block]
J -- Yes --> L[file_object.get 'file']
L --> M{isinstance dict?}
M -- No --> N[skip — non-OpenAI block]
M -- Yes --> O[remap file_id to provider id]
Reviews (2): Last reviewed commit: "fix(anthropic): tolerate non-OpenAI file..." | Re-trigger Greptile
Merging this PR will not alter performance
Comparing |
Codecov Report❌ Patch coverage is 📢 Thoughts on this report? Let us know! |
|
|
|
| GitGuardian id | GitGuardian status | Secret | Commit | Filename | |
|---|---|---|---|---|---|
| 29203053 | Triggered | Generic Password | f31d4fa | .circleci/config.yml | View secret |
| 29203065 | Triggered | JSON Web Token | e8461b5 | tests/test_litellm/proxy/test_litellm_pre_call_utils.py | View secret |
🛠 Guidelines to remediate hardcoded secrets
- Understand the implications of revoking this secret by investigating where it is used in your code.
- Replace and store your secrets safely. Learn here the best practices.
- Revoke and rotate these secrets.
- 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
- following these best practices for managing and storing secrets including API keys and other credentials
- install secret detection on pre-commit to catch secret before it leaves your machine and ease remediation.
🦉 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.
️✅ There are no secrets present in this pull request anymore.If these secrets were true positive and are still valid, we highly recommend you to revoke them. 🦉 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. |
…scovery
`get_file_ids_from_messages` and `update_messages_with_model_file_ids`
assume every content block with `type: "file"` has a nested `file` dict in
the OpenAI Chat Completions shape. That assumption is too strong: `type:
"file"` is a public content-block discriminator and several real producers
emit blocks that use it without the OpenAI `file` sub-dict. For example,
LangChain v1's `_normalize_messages` rewrites OpenAI file blocks into
`{"type":"file","id":"...","base64":"...","mime_type":"...","extras":{}}`
before they reach LiteLLM.
`AnthropicConfig.validate_environment` calls both helpers unconditionally
on every Anthropic (and Anthropic-via-Vertex) request, so any such block
raises `KeyError: 'file'` which the Vertex partner layer then wraps as a
`500 InternalServerError` before the LLM is even contacted.
This patch switches both helpers from `c["file"]` to a defensive
`c.get("file")` + dict check. When the block does not match the OpenAI
shape there is no file_id to extract or remap, so we skip it and leave
the block untouched for the downstream provider transformer to handle.
Adds 5 regression tests covering the LangChain v1 shape, the OpenAI
happy path, mixed shapes in one message, `file` set to a non-dict value,
and the remap path for non-OpenAI blocks.
Related to BerriAI#24503, which proposed raising `BadRequestError` in the same
spots. For these two discovery functions specifically, the skip semantics
is strictly more permissive: well-formed OpenAI blocks still yield their
file_id, and legitimate non-OpenAI blocks stop crashing the request.
bdc0796 to
2350cca
Compare
0e23aa7
into
BerriAI:litellm_oss_staging_04_22_2026
…scovery (BerriAI#26228) `get_file_ids_from_messages` and `update_messages_with_model_file_ids` assume every content block with `type: "file"` has a nested `file` dict in the OpenAI Chat Completions shape. That assumption is too strong: `type: "file"` is a public content-block discriminator and several real producers emit blocks that use it without the OpenAI `file` sub-dict. For example, LangChain v1's `_normalize_messages` rewrites OpenAI file blocks into `{"type":"file","id":"...","base64":"...","mime_type":"...","extras":{}}` before they reach LiteLLM. `AnthropicConfig.validate_environment` calls both helpers unconditionally on every Anthropic (and Anthropic-via-Vertex) request, so any such block raises `KeyError: 'file'` which the Vertex partner layer then wraps as a `500 InternalServerError` before the LLM is even contacted. This patch switches both helpers from `c["file"]` to a defensive `c.get("file")` + dict check. When the block does not match the OpenAI shape there is no file_id to extract or remap, so we skip it and leave the block untouched for the downstream provider transformer to handle. Adds 5 regression tests covering the LangChain v1 shape, the OpenAI happy path, mixed shapes in one message, `file` set to a non-dict value, and the remap path for non-OpenAI blocks. Related to BerriAI#24503, which proposed raising `BadRequestError` in the same spots. For these two discovery functions specifically, the skip semantics is strictly more permissive: well-formed OpenAI blocks still yield their file_id, and legitimate non-OpenAI blocks stop crashing the request.
Relevant issues
Closes #26227. Related to #24503 (different approach, see below).
Pre-Submission checklist
tests/test_litellm/directory, Adding at least 1 test is a hard requirementmake test-unitfor the affected moduleType
Bug Fix
Problem
AnthropicConfig.validate_environment(run on every Anthropic + Anthropic-via-Vertex request) calls:is_file_id_used->get_file_ids_from_messages(common_utils.py:1049)update_messages_with_model_file_ids(common_utils.py:431)Both helpers short-circuit on
c[\"type\"] == \"file\"and then do a barec[\"file\"]access. Any content block that uses\"file\"as itstypediscriminator but does not match the OpenAI Chat Completions sub-shape (nestedfiledict) raisesKeyError: 'file'. The Vertex partner layer wraps this asVertexAIError(500)->litellm.InternalServerError, before the LLM is contacted.Real-world trigger: LangChain v1's
_normalize_messagesrewrites OpenAI file blocks into{\"type\":\"file\",\"id\":...,\"base64\":...,\"mime_type\":...,\"extras\":{}}on the way into every chat model. Hits everyvertex_ai/claude-*andanthropic/claude-*request that carries an attachment.See #26227 for full RCA, traceback, and minimal repro.
Fix
type: \"file\"is a public content-block discriminator. The two helpers above are discovery passes:get_file_ids_from_messagesreturns a list of file IDs.update_messages_with_model_file_idsrewrites provider-scoped file IDs in-place.A block with no
filesub-dict has no file_id to extract or remap, so the correct behavior is to skip the block, not raise. This patch switches both fromc[\"file\"]toc.get(\"file\")+isinstance(..., dict)check, and continues past non-OpenAI blocks.Why skip and not raise
BadRequestError(as in #24503)PR #24503 raises
BadRequestErrorin these two spots. For the stricter sites it also touches (Gemini/Bedrock/Anthropic transformers,migrate_file_to_image_url), that is the right behavior: a block that has reached the provider transformer is expected to be fully-formed OpenAI shape, and a missingfilesub-dict really is a malformed request.These two discovery helpers are different. They run unconditionally inside
validate_environment, ahead of any provider-specific transformer. RaisingBadRequestErrorhere fails the whole request for any legitimate non-OpenAI block (LangChain v1, provider-native, custom user shapes) even when the downstream transformer would handle it correctly. Skip is strictly more permissive: well-formed OpenAI blocks still yield their file_id, and non-OpenAI blocks stop crashingvalidate_environment.Happy to coordinate with @krisxia0506 if this should land as a delta on top of #24503, or get rolled in there directly.
Changes
litellm/litellm_core_utils/prompt_templates/common_utils.py.get(\"file\")+isinstance(..., dict)check inget_file_ids_from_messagesandupdate_messages_with_model_file_ids; skip the block if the sub-dict is missing or malformed.tests/test_litellm/litellm_core_utils/prompt_templates/test_litellm_core_utils_prompt_templates_common_utils.pyfileset to a non-dict value, remap path for non-OpenAI blocks).Tests
All 25 tests in
test_litellm_core_utils_prompt_templates_common_utils.pypass locally:New tests cover:
test_get_file_ids_from_messages_skips_langchain_v1_file_blocktest_get_file_ids_from_messages_still_extracts_from_openai_shapetest_get_file_ids_from_messages_mixed_shapestest_get_file_ids_from_messages_file_field_not_dicttest_update_messages_with_model_file_ids_skips_non_openai_file_blocks