Skip to content

fix(azure): strip litellm format field from file and image content parts - #41275

Merged
mateo-berri merged 4 commits into
mainfrom
litellm_azure_strip_file_format
Sep 18, 2026
Merged

mateo-berri merged 4 commits into
mainfrom
litellm_azure_strip_file_format

Conversation

@devin-ai-integration

@devin-ai-integration devin-ai-integration Bot commented Sep 15, 2026 •

Copy link
Copy Markdown
Contributor

TLDR

Problem this solves:

  • Managed file ids in chat/completions against Azure fail with Unknown parameter: 'messages[0].content[1].file.format'
  • The managed-file remap writes a litellm-only format key that OpenAI strips but Azure did not

How it solves it:

  • convert_to_azure_openai_messages now drops format from file and dict-shaped image_url parts
  • Mirrors what the OpenAI transformation already does

User Flow

Before: a developer attaching an uploaded file to an Azure chat request gets a 400 from Azure

  1. They send POST https://litellm-domain/v1/files with a PDF and target_model_names: "azure-gpt-5.4" and get back a managed file id
  2. They send POST https://litellm-domain/v1/chat/completions to azure-gpt-5.4 with content [{"type": "text", ...}, {"type": "file", "file": {"file_id": "<managed-file-id>"}}]
  3. They get 400 AzureException BadRequestError - Unknown parameter: 'messages[0].content[1].file.format'

After: the same request succeeds

  1. They send POST https://litellm-domain/v1/files with a PDF and target_model_names: "azure-gpt-5.4" and get back a managed file id
  2. They send the same POST https://litellm-domain/v1/chat/completions with the managed file id
  3. They get a 200 with the model's answer about the file

Relevant issues

Customer report via Pylon #8654

Affected release

Linear ticket

Resolves LIT-7809

Pre-Submission checklist

Please complete all items before asking a LiteLLM maintainer to review your PR

  • I have added meaningful tests
  • The handful of test files covering my change pass locally, e.g. uv run pytest tests/test_litellm/<your_test_file>.py -v. Leave the suites (make test-unit-*, make test-unit) to CI: it finishes in ~15 minutes where a laptop takes an hour or more
  • My PR passes all required CI/CD checks (e.g., lint, schema.d.ts sync check, etc.)
  • My PR's scope is as isolated as possible; it only solves 1 specific problem
  • I have received a Greptile Confidence Score of at least 4/5 before requesting a maintainer review (Greptile reviews automatically once the PR is opened; only comment @greptileai to re-request a review after pushing changes)

Delays in PR merge?

If you're seeing a delay in your PR being merged, ping the LiteLLM Team on Slack (#pr-review).

Screenshots / Proof of Fix

Shared setup: two proxies against the real Azure OpenAI deployment, one at the merge base and one at this PR's tip, each in its own worktree with its own venv, a fresh Postgres database, a random port, and --num_workers 2. lit7809_sample.pdf (630 bytes) and lit7809_sample.txt (56 bytes) both contain the sentence "The secret word is pineapple." $B64 is the base64 of a 1x1 red PNG. The upload uses purpose=assistants because Azure OpenAI rejects purpose=user_data on the dated api-versions ("Invalid value for purpose.")

model_list:
  - model_name: azure-gpt-5.4-mini
    litellm_params:
      model: azure/gpt-5.4-mini
      api_base: os.environ/AZURE_FOUNDRY_API_BASE
      api_key: os.environ/AZURE_FOUNDRY_API_KEY
      api_version: "2025-04-01-preview"
general_settings:
  master_key: sk-1234
python litellm/proxy/proxy_cli.py --config lit7809_qa_config.yaml --port $PORT --num_workers 2

Before (2e06d19)

Case 1: PDF managed file

  1. Upload
curl -s http://localhost:21835/v1/files -H "Authorization: Bearer sk-1234" -F purpose=assistants -F target_model_names=azure-gpt-5.4-mini -F file=@lit7809_sample.pdf -w '\nHTTP %{http_code}\n'
{"id":"bGl0ZWxsbV9wcm94eTphcHBsaWNhdGlvbi9wZGY7dW5pZmllZF9pZCw0M2E4MTFhOC05ZTJhLTQ5ZmYtYjhkZS01YTM2NTdhYzI5NTM7dGFyZ2V0X21vZGVsX25hbWVzLGF6dXJlLWdwdC01LjQtbWluaTtsbG1fb3V0cHV0X2ZpbGVfaWQsYXNzaXN0YW50LUtaUHpQaGVXMWpVWTNiSjZXb0Q5czE7bGxtX291dHB1dF9maWxlX21vZGVsX2lkLDAxZWJkY2U1ZDU1YzlkMDQxYWFhNGVmMTA3OTMxM2M3YjEzZjg0Y2RmOTE1NjNlZmFmMjg4OTMwYjgxYWZiOWM","bytes":630,"created_at":1789754558,"filename":"lit7809_sample.pdf","object":"file","purpose":"assistants","status":"uploaded","expires_at":null,"status_details":null}
HTTP 200
  1. Chat
curl -s http://localhost:21835/v1/chat/completions -H "Authorization: Bearer sk-1234" -H "Content-Type: application/json" -d '{"model":"azure-gpt-5.4-mini","messages":[{"role":"user","content":[{"type":"text","text":"What is the secret word in the attached document? Answer with one word."},{"type":"file","file":{"file_id":"<id from step 1>"}}]}]}' -w '\nHTTP %{http_code}\n'
{"error":{"message":"litellm.BadRequestError: AzureException BadRequestError - Unknown parameter: 'messages[0].content[1].file.format'.. Received Model Group=azure-gpt-5.4-mini\nAvailable Model Group Fallbacks=None","type":"invalid_request_error","param":"messages[0].content[1].file.format","code":"400"}}
HTTP 400

Case 2: text managed file

  1. Upload
curl -s http://localhost:21835/v1/files -H "Authorization: Bearer sk-1234" -F purpose=assistants -F target_model_names=azure-gpt-5.4-mini -F file=@lit7809_sample.txt -w '\nHTTP %{http_code}\n'
{"id":"bGl0ZWxsbV9wcm94eTp0ZXh0L3BsYWluO3VuaWZpZWRfaWQsOWQ1MDU2YjUtMDgzYi00YzU5LWExNDgtMTI5NWQyODBiZWRhO3RhcmdldF9tb2RlbF9uYW1lcyxhenVyZS1ncHQtNS40LW1pbmk7bGxtX291dHB1dF9maWxlX2lkLGFzc2lzdGFudC1NRVl5Y0ZkOWhFZEx2emZoa052UUU5O2xsbV9vdXRwdXRfZmlsZV9tb2RlbF9pZCwwMWViZGNlNWQ1NWM5ZDA0MWFhYTRlZjEwNzkzMTNjN2IxM2Y4NGNkZjkxNTYzZWZhZjI4ODkzMGI4MWFmYjlj","bytes":56,"created_at":1789754572,"filename":"lit7809_sample.txt","object":"file","purpose":"assistants","status":"uploaded","expires_at":null,"status_details":null}
HTTP 200
  1. Chat
curl -s http://localhost:21835/v1/chat/completions -H "Authorization: Bearer sk-1234" -H "Content-Type: application/json" -d '{"model":"azure-gpt-5.4-mini","messages":[{"role":"user","content":[{"type":"text","text":"What is the secret word in the attached document? Answer with one word."},{"type":"file","file":{"file_id":"<id from step 1>"}}]}]}' -w '\nHTTP %{http_code}\n'
{"error":{"message":"litellm.BadRequestError: AzureException BadRequestError - Unknown parameter: 'messages[0].content[1].file.format'.. Received Model Group=azure-gpt-5.4-mini\nAvailable Model Group Fallbacks=None","type":"invalid_request_error","param":"messages[0].content[1].file.format","code":"400"}}
HTTP 400

Case 3: image_url with format

$B64 is the base64 of a 1x1 red PNG

  1. Chat
curl -s http://localhost:21835/v1/chat/completions -H "Authorization: Bearer sk-1234" -H "Content-Type: application/json" -d "{\"model\":\"azure-gpt-5.4-mini\",\"messages\":[{\"role\":\"user\",\"content\":[{\"type\":\"text\",\"text\":\"What color is this image? Answer with one word.\"},{\"type\":\"image_url\",\"image_url\":{\"url\":\"data:image/png;base64,$B64\",\"format\":\"image/png\"}}]}]}" -w '\nHTTP %{http_code}\n'
{"id":"chatcmpl-EPXDEOQGCwt9ziORANNZZowEto5Sc","created":1789754576,"model":"azure-gpt-5.4-mini","object":"chat.completion","choices":[{"finish_reason":"stop","index":0,"message":{"content":"Black","role":"assistant","provider_specific_fields":{"refusal":null},"annotations":[]},"provider_specific_fields":{"content_filter_result":{"error":{"code":"content_filter_error","message":"The contents are not filtered"}},"content_filter_results":{}}}],"usage":{"completion_tokens":10,"prompt_tokens":63,"total_tokens":73},"service_tier":"default"}
HTTP 200

After (f219535)

Case 1: PDF managed file

  1. Upload
curl -s http://localhost:24342/v1/files -H "Authorization: Bearer sk-1234" -F purpose=assistants -F target_model_names=azure-gpt-5.4-mini -F file=@lit7809_sample.pdf -w '\nHTTP %{http_code}\n'
{"id":"bGl0ZWxsbV9wcm94eTphcHBsaWNhdGlvbi9wZGY7dW5pZmllZF9pZCwzMTEwMmQ3My1iZGM1LTQ4ZWEtYjhmYi1kYjk5NzlkNWFkNGQ7dGFyZ2V0X21vZGVsX25hbWVzLGF6dXJlLWdwdC01LjQtbWluaTtsbG1fb3V0cHV0X2ZpbGVfaWQsYXNzaXN0YW50LTRmN1VqcmdjYlVLTW5DVzFIRld1ZDc7bGxtX291dHB1dF9maWxlX21vZGVsX2lkLDAxZWJkY2U1ZDU1YzlkMDQxYWFhNGVmMTA3OTMxM2M3YjEzZjg0Y2RmOTE1NjNlZmFmMjg4OTMwYjgxYWZiOWM","bytes":630,"created_at":1789754677,"filename":"lit7809_sample.pdf","object":"file","purpose":"assistants","status":"uploaded","expires_at":null,"status_details":null}
HTTP 200
  1. Chat
curl -s http://localhost:24342/v1/chat/completions -H "Authorization: Bearer sk-1234" -H "Content-Type: application/json" -d '{"model":"azure-gpt-5.4-mini","messages":[{"role":"user","content":[{"type":"text","text":"What is the secret word in the attached document? Answer with one word."},{"type":"file","file":{"file_id":"<id from step 1>"}}]}]}' -w '\nHTTP %{http_code}\n'
{"id":"chatcmpl-EPXEzxH6q88jUce9JfmdSX96EbOZI","created":1789754685,"model":"azure-gpt-5.4-mini","object":"chat.completion","choices":[{"finish_reason":"stop","index":0,"message":{"content":"pineapple","role":"assistant","provider_specific_fields":{"refusal":null},"annotations":[]},"provider_specific_fields":{}}],"usage":{"completion_tokens":5,"prompt_tokens":47,"total_tokens":52},"service_tier":"default"}
HTTP 200

Case 2: text managed file

  1. Upload
curl -s http://localhost:24342/v1/files -H "Authorization: Bearer sk-1234" -F purpose=assistants -F target_model_names=azure-gpt-5.4-mini -F file=@lit7809_sample.txt -w '\nHTTP %{http_code}\n'
{"id":"bGl0ZWxsbV9wcm94eTp0ZXh0L3BsYWluO3VuaWZpZWRfaWQsYWUxYjU3NWYtMWY3My00ZjRlLWE3YzUtMjc5ODhmNjlkZGY4O3RhcmdldF9tb2RlbF9uYW1lcyxhenVyZS1ncHQtNS40LW1pbmk7bGxtX291dHB1dF9maWxlX2lkLGFzc2lzdGFudC1FRVBkV2E4d3pGRWNTUFZMY0E0cXdoO2xsbV9vdXRwdXRfZmlsZV9tb2RlbF9pZCwwMWViZGNlNWQ1NWM5ZDA0MWFhYTRlZjEwNzkzMTNjN2IxM2Y4NGNkZjkxNTYzZWZhZjI4ODkzMGI4MWFmYjlj","bytes":56,"created_at":1789754691,"filename":"lit7809_sample.txt","object":"file","purpose":"assistants","status":"uploaded","expires_at":null,"status_details":null}
HTTP 200
  1. Chat
curl -s http://localhost:24342/v1/chat/completions -H "Authorization: Bearer sk-1234" -H "Content-Type: application/json" -d '{"model":"azure-gpt-5.4-mini","messages":[{"role":"user","content":[{"type":"text","text":"What is the secret word in the attached document? Answer with one word."},{"type":"file","file":{"file_id":"<id from step 1>"}}]}]}' -w '\nHTTP %{http_code}\n'
{"id":"chatcmpl-EPXFBAUGEtOdYu9tTFtN8n8yDR6l3","created":1789754697,"model":"azure-gpt-5.4-mini","object":"chat.completion","choices":[{"finish_reason":"stop","index":0,"message":{"content":"pineapple","role":"assistant","provider_specific_fields":{"refusal":null},"annotations":[]},"provider_specific_fields":{}}],"usage":{"completion_tokens":5,"prompt_tokens":34,"total_tokens":39},"service_tier":"default"}
HTTP 200

Case 3: image_url with format

$B64 is the base64 of a 1x1 red PNG

  1. Chat
curl -s http://localhost:24342/v1/chat/completions -H "Authorization: Bearer sk-1234" -H "Content-Type: application/json" -d "{\"model\":\"azure-gpt-5.4-mini\",\"messages\":[{\"role\":\"user\",\"content\":[{\"type\":\"text\",\"text\":\"What color is this image? Answer with one word.\"},{\"type\":\"image_url\",\"image_url\":{\"url\":\"data:image/png;base64,$B64\",\"format\":\"image/png\"}}]}]}" -w '\nHTTP %{http_code}\n'
{"id":"chatcmpl-EPXFIqD8Od9FWZgmdaavjt4bWVIRG","created":1789754704,"model":"azure-gpt-5.4-mini","object":"chat.completion","choices":[{"finish_reason":"stop","index":0,"message":{"content":"black","role":"assistant","provider_specific_fields":{"refusal":null},"annotations":[]},"provider_specific_fields":{"content_filter_result":{"error":{"code":"content_filter_error","message":"The contents are not filtered"}},"content_filter_results":{}}}],"usage":{"completion_tokens":4,"prompt_tokens":63,"total_tokens":67},"service_tier":"default","prompt_filter_results":[{"prompt_index":0,"content_filter_results":{}}]}
HTTP 200

Observations

  • Same 400 on both managed-file types at the merge base
  • Both managed files answer "pineapple" at the tip
  • image_url.format returned 200 on both sides
  • Model calls the 1x1 red PNG "black" on both sides
  • Every uploaded file deleted from Azure afterwards

Regression sweep (/live-pr-risk)

Same config, two proxies booted with --num_workers 2 on a fresh Postgres each, one at the merge base and one at the tip, then the tip with current main merged in (130f2e789b). Only the two managed-file cases change; everything around them is identical on all three

Scenario 2e06d19 f219535 tip + main
managed PDF, gpt-4o 400 file.format 200 pineapple 200 pineapple
managed PDF, gpt-5.4-mini, stream 400 file.format 200 pineapple 200 pineapple
inline file_data PDF 200 200 200
image_url dict with detail + format 200 200 200
image_url string 200 200 200
file part with no file object 500 KeyError 500 KeyError 500 KeyError
fallback broken key -> gpt-4o 200 200 200
/health/readiness 200 200 200
key generate, use, info, delete 200/200/401 200/200/401 200/200/401

The 500 on the file part without a file object comes from the managed-files hook (get_file_ids_from_messages) before any transform runs, so it is the same on both sides and outside this PR. Spend logs on both sides carry the same rows apart from the two calls that went from failure to success

Type

🐛 Bug Fix

Caveats (if any)

Low

  • image_url.format is also dropped for Azure to match the OpenAI path; the live run shows Azure already returned 200 with it present at the merge base (Case 3), so that part is parity rather than a fix
  • _azure_file_helper writes file: {} when a file part has no file object where the OpenAI path raises BadRequestError; through the proxy that shape never reaches either, the managed-files hook 500s on it first (sweep above), so the two paths only differ on direct SDK use
  • CircleCI at the tip has five red jobs, all fixed on main after the merge base (2026-09-15 11:38 -0700) and none touching this PR's files, so the branch is not rebased: 22 Together AI tests across local_testing_part1, local_testing_part2, and llm_translation_testing fail with "Unable to access non-serverless model openai/gpt-oss-20b" (fixed by test(together_ai): move request-shape checks to the mapped file, drop the live ones #41360); litellm_router_testing test_async_fallbacks asserts on a deployment-selection log line (fixed by test(router): ignore deployment-selection logs in the fallback log assertion #41358); logging_testing test_generic_api_callback_sumologic_uses_ndjson (fixed by test(logging): pick this test's own records out of the shared log batch #41487). The newest main pipeline (89785) is green on all five

Final Attestation

  • The tests check the right things, including the edge cases, and regressions in the respective real-world customer use-cases are not possible after this PR

Link to Devin session: https://app.devin.ai/sessions/378f7876fa414215b1c0d43663ca494c
Open in Devin Desktop: https://app.devin.ai/desktop/session/378f7876fa414215b1c0d43663ca494c?variant=devin

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
@devin-ai-integration
devin-ai-integration Bot requested a review from a team September 15, 2026 18:44
@devin-ai-integration

Copy link
Copy Markdown
Contributor Author

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment, CI, and merge conflict monitoring

@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.

@codspeed

codspeed Bot commented Sep 15, 2026 •

Copy link
Copy Markdown
Contributor

Merging this PR will not alter performance

✅ 31 untouched benchmarks


Comparing litellm_azure_strip_file_format (f219535) with main (2e06d19)1

Open in CodSpeed

Footnotes

  1. No successful run was found on main (d392928) during the generation of this report, so 2e06d19 was used instead as the comparison base. There might be some changes unrelated to this pull request in this report. ↩

@greptile-apps

greptile-apps Bot commented Sep 15, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR removes LiteLLM-only format fields from file and dictionary-shaped image content before Azure requests are sent

  • Adds immutable nested-object filtering for Azure file and image content
  • Adds focused conversion and managed-file regression coverage
  • Fixes the test variable rebinding raised in the previous review

Confidence Score: 5/5

The PR appears safe to merge, with the previous test rebinding issue fixed and no new actionable failures introduced

The latest change binds transformed messages to updated_messages, fully resolving the outstanding rebinding finding. The caller-mutation and unnecessary-comment threads were manually resolved without explanation. The provider-location finding was withdrawn after Greptile accepted the author's cohesion rationale

Important Files Changed

Filename Overview
litellm/litellm_core_utils/prompt_templates/factory.py Filters format from Azure-bound file and image payloads while preserving the remaining fields
tests/test_litellm/litellm_core_utils/prompt_templates/test_litellm_core_utils_prompt_templates_factory.py Verifies direct Azure conversion strips format from both supported content shapes
tests/test_litellm/llms/azure/chat/test_azure_chat_gpt_transformation.py Covers the managed-file request path and now keeps original and transformed messages in separate variables

Reviews (3): Last reviewed commit: "test(azure): avoid rebinding messages in..." | Re-trigger Greptile

Comment thread litellm/litellm_core_utils/prompt_templates/factory.py Outdated
Comment thread litellm/litellm_core_utils/prompt_templates/factory.py Outdated
@codecov

codecov Bot commented Sep 15, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…trings

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
@devin-ai-integration

Copy link
Copy Markdown
Contributor Author

@greptileai

"content": [
{"type": "text", "text": "Summarize this file"},
{"type": "file", "file": {"file_id": managed_file_id}},
],

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 Local Variable Rebinding

Reassigning messages violates the repository's no-rebinding directive. Bind the transformed result to a new Final variable before merging

Context Used: CLAUDE.md (source)

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!

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
@devin-ai-integration

Copy link
Copy Markdown
Contributor Author

@greptileai

@mateo-berri

Copy link
Copy Markdown
Contributor

bugbot run

@cursor cursor Bot left a comment

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.

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit f219535. Configure here.

@mateo-berri mateo-berri left a comment

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.

LGTM

@mateo-berri
mateo-berri merged commit adb5ee0 into main Sep 18, 2026
135 of 140 checks passed
@mateo-berri
mateo-berri deleted the litellm_azure_strip_file_format branch September 18, 2026 18:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants