Skip to content

fix(images): stop forwarding the raw image[] and mask[] form keys - #39512

Merged
mateo-berri merged 4 commits into
mainfrom
litellm_fix_image_edits_bracketed_alias
Sep 17, 2026
Merged

mateo-berri merged 4 commits into
mainfrom
litellm_fix_image_edits_bracketed_alias

Conversation

@mateo-berri

@mateo-berri mateo-berri commented Sep 3, 2026 •

Copy link
Copy Markdown
Contributor

TLDR

Problem this solves:

  • /v1/images/edits forwarded the raw image[] key to providers
  • OpenAI 400s the documented alias: "Invalid type for 'image[0]'"
  • mask[] 400s the same way: "This parameter already has a different value"

How it solves it:

  • Drop image[] and mask[] from what gets forwarded
  • The handler already folded them into image and mask
  • Provider-agnostic: no OpenAI special-casing

User Flow

Before: a developer following LiteLLM's own documented image[] form gets a 400 and no edited image

  1. They send POST https://litellm-domain/v1/images/edits with model=gpt-image-1, image[]=@tree.png, and prompt=add a small red hat on top, exactly the form the endpoint's own docs show
  2. They get back HTTP 400: Invalid type for 'image[0]': expected a file, but got a string instead.
  3. They drop the brackets and send the same request with image=@tree.png, which returns HTTP 200 with an edited image
  4. They add a mask the documented way, sending image=@tree.png plus mask[]=@mask.png, and get back HTTP 400: Invalid parameter: 'mask'. This parameter already has a different value in your parameters.
  5. They drop those brackets too and send mask=@mask.png, which returns HTTP 200, so the documented bracketed form is unusable through the gateway on either field

After: the same documented image[] and mask[] requests come back with an edited image

  1. They send POST https://litellm-domain/v1/images/edits with model=gpt-image-1, image[]=@tree.png, and prompt=add a small red hat on top, exactly the form the endpoint's own docs show
  2. They get back HTTP 200 with a b64_json image and a usage block
  3. They send the same request with image=@tree.png, which returns HTTP 200 with an edited image
  4. They add a mask the documented way, sending image=@tree.png plus mask[]=@mask.png, and get back HTTP 200 with an edited image
  5. They send mask=@mask.png instead and get HTTP 200 as well, so both spellings work on both fields
  6. Sending image and image[] together still returns HTTP 422 Cannot specify both 'image' and 'image[]', so the existing guard against ambiguous requests is unchanged

Relevant issues

Fixes #41421

Affected release

regression in v1.100.0 (PR #38104), still present in v1.101.0 and v1.102.0-rc.2

Linear ticket

Resolves LIT-6816

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

Both legs run the same proxy config against the real OpenAI API with gpt-image-2.5-sunburst, the model the reporting customer hit this on, two uvicorn workers (--num_workers 2) on each proxy, and the same 256x256 tree.png / tree2.png / tree3.png / mask.png set. The customer's own call shape is the OpenAI Python SDK's client.images.edit(image=[...]), which the SDK sends as repeated image[] parts, so the SDK cases come first and the curl cases cover the documented bracketed forms directly. Real spend on every 200. Before is the merge base bc9f4fec5b (current main), After is this PR's tip 255190ca35, same nine cases in the same order on both

config.yaml:

model_list:
  - model_name: gpt-image-2.5-sunburst
    litellm_params:
      model: openai/gpt-image-2.5-sunburst
      api_key: os.environ/OPENAI_API_KEY
  - model_name: gpt-image-1
    litellm_params:
      model: openai/gpt-image-1
      api_key: os.environ/OPENAI_API_KEY

general_settings:
  master_key: sk-lit6816

sdk_edit.py:

import sys, json
from openai import OpenAI, APIStatusError
base_url, model, mode = sys.argv[1], sys.argv[2], sys.argv[3]
client = OpenAI(base_url=base_url, api_key="sk-lit6816")
files = {"single": open("tree.png", "rb"), "list1": [open("tree.png", "rb")], "list3": [open("tree.png", "rb"), open("tree2.png", "rb"), open("tree3.png", "rb")]}[mode]
try:
    r = client.images.edit(model=model, image=files, prompt="add a small red hat on top of the tree", size="1024x1024", quality="low")
    print(json.dumps({"created": r.created, "usage_total_tokens": r.usage.total_tokens if r.usage else None, "b64_json_length": len(r.data[0].b64_json or "")}))
except APIStatusError as e:
    print(f"HTTP {e.status_code}", json.dumps(e.body)[:400])

Each case is the command followed by its output. A 200 is summarized to created, usage.total_tokens, the b64_json length, and the number of images returned; an error shows the proxy's response body

Before (bc9f4fe)

SDK: three images in a list (the customer flow)

$ python sdk_edit.py http://127.0.0.1:34271 gpt-image-2.5-sunburst list3   # OpenAI SDK: client.images.edit(model=..., image=<list3>, prompt=..., size="1024x1024", quality="low")
HTTP 400 {"message": "litellm.BadRequestError: OpenAIException - {\n  \"error\": {\n    \"message\": \"Invalid type for 'image[0]': expected a file, but got a string instead.\",\n    \"type\": \"invalid_request_error\",\n    \"param\": \"image[0]\",\n    \"code\": \"invalid_type\"\n  }\n}. Received Model Group=gpt-image-2.5-sunburst\nAvailable Model Group Fallbacks=None", "type": "invalid_request_error", "

SDK: a list of one image

$ python sdk_edit.py http://127.0.0.1:34271 gpt-image-2.5-sunburst list1   # OpenAI SDK: client.images.edit(model=..., image=<list1>, prompt=..., size="1024x1024", quality="low")
HTTP 400 {"message": "litellm.BadRequestError: OpenAIException - {\n  \"error\": {\n    \"message\": \"Invalid type for 'image[0]': expected a file, but got a string instead.\",\n    \"type\": \"invalid_request_error\",\n    \"param\": \"image[0]\",\n    \"code\": \"invalid_type\"\n  }\n}. Received Model Group=gpt-image-2.5-sunburst\nAvailable Model Group Fallbacks=None", "type": "invalid_request_error", "

SDK: a single image, not a list

$ python sdk_edit.py http://127.0.0.1:34271 gpt-image-2.5-sunburst single   # OpenAI SDK: client.images.edit(model=..., image=<single>, prompt=..., size="1024x1024", quality="low")
{"created": 1789682988, "usage_total_tokens": 478, "b64_json_length": 1143356}

curl: two image[] parts

$ curl -s http://127.0.0.1:34271/v1/images/edits -H "Authorization: Bearer sk-lit6816" -F "model=gpt-image-2.5-sunburst" -F "prompt=add a small red hat on top of the tree" -F "image[]=@tree.png" -F "image[]=@tree2.png" -F "size=1024x1024" -F "quality=low"
HTTP 400 {"error": {"message": "litellm.BadRequestError: OpenAIException - {\n  \"error\": {\n    \"message\": \"Invalid type for 'image[0]': expected a file, but got a string instead.\",\n    \"type\": \"invalid_request_error\",\n    \"param\": \"image[0]\",\n    \"code\": \"invalid_type\"\n  }\n}. Received Model Group=gpt-image-2.5-sunburst\nAvailable Model Group Fallbacks=None", "type": "invalid_request

curl: one image[] part

$ curl -s http://127.0.0.1:34271/v1/images/edits -H "Authorization: Bearer sk-lit6816" -F "model=gpt-image-2.5-sunburst" -F "prompt=add a small red hat on top of the tree" -F "image[]=@tree.png" -F "size=1024x1024" -F "quality=low"
HTTP 400 {"error": {"message": "litellm.BadRequestError: OpenAIException - {\n  \"error\": {\n    \"message\": \"Invalid type for 'image[0]': expected a file, but got a string instead.\",\n    \"type\": \"invalid_request_error\",\n    \"param\": \"image[0]\",\n    \"code\": \"invalid_type\"\n  }\n}. Received Model Group=gpt-image-2.5-sunburst\nAvailable Model Group Fallbacks=None", "type": "invalid_request

curl: canonical image part

$ curl -s http://127.0.0.1:34271/v1/images/edits -H "Authorization: Bearer sk-lit6816" -F "model=gpt-image-2.5-sunburst" -F "prompt=add a small red hat on top of the tree" -F "image=@tree.png" -F "size=1024x1024" -F "quality=low"
HTTP 200 {"created": 1789683015, "usage_total_tokens": 478, "b64_json_length": 1139820, "images": 1}

curl: image plus mask[]

$ curl -s http://127.0.0.1:34271/v1/images/edits -H "Authorization: Bearer sk-lit6816" -F "model=gpt-image-2.5-sunburst" -F "prompt=add a small red hat on top of the tree" -F "image=@tree.png" -F "mask[]=@mask.png" -F "size=1024x1024" -F "quality=low"
HTTP 400 {"error": {"message": "litellm.BadRequestError: OpenAIException - {\n  \"error\": {\n    \"message\": \"Invalid parameter: 'mask'. This parameter already has a different value in your parameters. Please check your parameters.\",\n    \"type\": \"invalid_request_error\",\n    \"param\": \"mask\",\n    \"code\": \"invalid_parameter\"\n  }\n}. Received Model Group=gpt-image-2.5-sunburst\nAvailable Mo

curl: image plus canonical mask

$ curl -s http://127.0.0.1:34271/v1/images/edits -H "Authorization: Bearer sk-lit6816" -F "model=gpt-image-2.5-sunburst" -F "prompt=add a small red hat on top of the tree" -F "image=@tree.png" -F "mask=@mask.png" -F "size=1024x1024" -F "quality=low"
HTTP 200 {"created": 1789683035, "usage_total_tokens": 478, "b64_json_length": 1111556, "images": 1}

curl: image and image[] in one request (rejected on both sides)

$ curl -s http://127.0.0.1:34271/v1/images/edits -H "Authorization: Bearer sk-lit6816" -F "model=gpt-image-2.5-sunburst" -F "prompt=add a small red hat on top of the tree" -F "image=@tree.png" -F "image[]=@tree2.png" -F "size=1024x1024" -F "quality=low"
HTTP 422 {"detail": "Cannot specify both 'image' and 'image[]'"}

After (255190c)

SDK: three images in a list (the customer flow)

$ python sdk_edit.py http://127.0.0.1:51671 gpt-image-2.5-sunburst list3   # OpenAI SDK: client.images.edit(model=..., image=<list3>, prompt=..., size="1024x1024", quality="low")
{"created": 1789683089, "usage_total_tokens": 1010, "b64_json_length": 1143568}

SDK: a list of one image

$ python sdk_edit.py http://127.0.0.1:51671 gpt-image-2.5-sunburst list1   # OpenAI SDK: client.images.edit(model=..., image=<list1>, prompt=..., size="1024x1024", quality="low")
{"created": 1789683105, "usage_total_tokens": 478, "b64_json_length": 1139396}

SDK: a single image, not a list

$ python sdk_edit.py http://127.0.0.1:51671 gpt-image-2.5-sunburst single   # OpenAI SDK: client.images.edit(model=..., image=<single>, prompt=..., size="1024x1024", quality="low")
{"created": 1789683122, "usage_total_tokens": 478, "b64_json_length": 1142032}

curl: two image[] parts

$ curl -s http://127.0.0.1:51671/v1/images/edits -H "Authorization: Bearer sk-lit6816" -F "model=gpt-image-2.5-sunburst" -F "prompt=add a small red hat on top of the tree" -F "image[]=@tree.png" -F "image[]=@tree2.png" -F "size=1024x1024" -F "quality=low"
HTTP 200 {"created": 1789683133, "usage_total_tokens": 744, "b64_json_length": 1147548, "images": 1}

curl: one image[] part

$ curl -s http://127.0.0.1:51671/v1/images/edits -H "Authorization: Bearer sk-lit6816" -F "model=gpt-image-2.5-sunburst" -F "prompt=add a small red hat on top of the tree" -F "image[]=@tree.png" -F "size=1024x1024" -F "quality=low"
HTTP 200 {"created": 1789683144, "usage_total_tokens": 478, "b64_json_length": 1139776, "images": 1}

curl: canonical image part

$ curl -s http://127.0.0.1:51671/v1/images/edits -H "Authorization: Bearer sk-lit6816" -F "model=gpt-image-2.5-sunburst" -F "prompt=add a small red hat on top of the tree" -F "image=@tree.png" -F "size=1024x1024" -F "quality=low"
HTTP 200 {"created": 1789683155, "usage_total_tokens": 478, "b64_json_length": 1144872, "images": 1}

curl: image plus mask[]

$ curl -s http://127.0.0.1:51671/v1/images/edits -H "Authorization: Bearer sk-lit6816" -F "model=gpt-image-2.5-sunburst" -F "prompt=add a small red hat on top of the tree" -F "image=@tree.png" -F "mask[]=@mask.png" -F "size=1024x1024" -F "quality=low"
HTTP 200 {"created": 1789683167, "usage_total_tokens": 478, "b64_json_length": 1105448, "images": 1}

curl: image plus canonical mask

$ curl -s http://127.0.0.1:51671/v1/images/edits -H "Authorization: Bearer sk-lit6816" -F "model=gpt-image-2.5-sunburst" -F "prompt=add a small red hat on top of the tree" -F "image=@tree.png" -F "mask=@mask.png" -F "size=1024x1024" -F "quality=low"
HTTP 200 {"created": 1789683176, "usage_total_tokens": 478, "b64_json_length": 1110120, "images": 1}

curl: image and image[] in one request (rejected on both sides)

$ curl -s http://127.0.0.1:51671/v1/images/edits -H "Authorization: Bearer sk-lit6816" -F "model=gpt-image-2.5-sunburst" -F "prompt=add a small red hat on top of the tree" -F "image=@tree.png" -F "image[]=@tree2.png" -F "size=1024x1024" -F "quality=low"
HTTP 422 {"detail": "Cannot specify both 'image' and 'image[]'"}

Observations from the run:

  • mask[] 400s too on Before, different OpenAI error; fixed here
  • Single-image SDK calls never hit the bug; unchanged
  • image plus image[] still 422s on both sides; unchanged
  • Two image[] parts yield one edited image, 744 tokens; provider behavior
  • Gemini answers the same on both sides; masks unsupported there

Type

🐛 Bug Fix

Caveats (if any)

Low

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

Note

Low Risk
Scoped bug fix to proxy image-edit request shaping with new regression tests; no auth or billing changes.

Overview
Fixes a regression where /v1/images/edits still forwarded raw multipart keys image[] and mask[] to upstream providers after the handler had already normalized those uploads into image / mask, which caused OpenAI (and SDK clients using bracketed parts) to return 400s.

The image edit handler now defines shared constants for those aliases and strips image[] and mask[] from the coerced form data before routing, while continuing to populate image and mask from the file parameters. Tests assert bracketed aliases are omitted from the forwarded payload, canonical image/mask uploads are unchanged, and existing multipart numeric coercion behavior is preserved.

Reviewed by Cursor Bugbot for commit 255190c. Bugbot is set up for automated code reviews on this repo. Configure here.

The /v1/images/edits handler binds the documented image[] and mask[] aliases
into their canonical parameters, then re-reads the multipart body, so the raw
bracketed keys rode along to the provider next to the values already built from
them. OpenAI rejected both: image[] as "Invalid type for 'image[0]'" and mask[]
as "Invalid parameter: 'mask'". Drop both aliases from what gets forwarded.
@codspeed

codspeed Bot commented Sep 3, 2026 •

Copy link
Copy Markdown
Contributor

Merging this PR will not alter performance

✅ 31 untouched benchmarks


Comparing litellm_fix_image_edits_bracketed_alias (255190c) with main (356b8d4)

Open in CodSpeed

@greptile-apps

greptile-apps Bot commented Sep 3, 2026 •

Copy link
Copy Markdown
Contributor

RetriggerConfidence Score: 5/5

The PR appears safe to merge.

Summary

The PR normalizes bracketed multipart image-edit aliases without forwarding their raw form keys downstream.

  • Adds shared constants for image[] and mask[].
  • Removes bracketed aliases from parsed request data after FastAPI binds them to canonical file parameters.
  • Adds regression coverage for bracketed and canonical image and mask uploads.

Reviews (4) · Last reviewed commit: "Merge remote-tracking branch 'origin/mai..."

@codecov

codecov Bot commented Sep 3, 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

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.

Stale Bugbot comment from a previous run.

@yuneng-berri
yuneng-berri deleted the branch main September 13, 2026 04:48
auto-merge was automatically disabled September 13, 2026 04:48

Pull request was closed

@mateo-berri mateo-berri reopened this Sep 13, 2026
@mateo-berri
mateo-berri changed the base branch from litellm_internal_staging to main September 13, 2026 05:14
galactic-batter Bot added a commit to codgician/litellm that referenced this pull request Sep 13, 2026
…keys

Forkit-Contribution: BerriAI#39512
Forkit-Source-Base: 066d5f0
Forkit-Source-Head: 06fb9dc
Forkit-Applied-To: v1.100.1
Forkit-Input: sha256:2da47ca821e6ef88631df9078e810e9d298587ed05ac8d3574026aa7ebf389e8
tin-berri
tin-berri previously approved these changes Sep 16, 2026

@tin-berri tin-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

@tin-berri
tin-berri dismissed their stale review September 16, 2026 01:26

Approval revoked at user request

@tin-berri tin-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

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 16, 2026 13:33
@devin-ai-integration

Copy link
Copy Markdown
Contributor

Merged main to clear the #39510 conflict; both behaviours composed. A customer report (Pylon #8687) confirms the same failure, including multi-image SDK edits

@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 all sign our Contributor License Agreement before we can accept your contribution.
1 out of 2 committers have signed the CLA.

✅ mateo-berri
❌ devin-ai-integration[bot]
You have signed the CLA already but the status is still pending? Let us recheck it.

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

Copy link
Copy Markdown

Can confirm this fixes the issue. Also works with Open WebUI's image edit feature

@mateo-berri mateo-berri added the backport-stable P0 regression fix only (Urgent ticket): cherry-pick onto the baking rc line before the stable tag label Sep 17, 2026
@mateo-berri

Copy link
Copy Markdown
Contributor Author

@greptileai

@mateo-berri mateo-berri added run-ci and removed run-ci labels Sep 17, 2026
@mateo-berri mateo-berri added run-ci and removed run-ci labels Sep 17, 2026
@mateo-berri

Copy link
Copy Markdown
Contributor Author

@greptileai

@mateo-berri

Copy link
Copy Markdown
Contributor Author

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 255190c. Configure here.

@mateo-berri
mateo-berri merged commit 289c52b into main Sep 17, 2026
142 of 147 checks passed
@mateo-berri
mateo-berri deleted the litellm_fix_image_edits_bracketed_alias branch September 17, 2026 22:48
mateo-berri added a commit that referenced this pull request Sep 17, 2026
fix(images): backport the image[] and mask[] form key drop to rc/1.102.0 (#39512)
@martinmose

Copy link
Copy Markdown

Do we know which version will include this fix? 🙏

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

backport-stable P0 regression fix only (Urgent ticket): cherry-pick onto the baking rc line before the stable tag run-ci

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: LiteLLM Proxy 1.100.0 serializes image-edit uploads as strings for OpenAI GPT Image models

6 participants