Repository navigation
test(integration): cover images endpoint contracts - #44653
Open
devin-ai-integration[bot] wants to merge 3 commits into
Open
devin-ai-integration[bot] wants to merge 3 commits into
devin-ai-integration[bot] wants to merge 3 commits into
Conversation
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Contributor
Author
|
I'll fix CI failures and address comments from users with write access. I'll skip comments containing "(aside)".
|
Contributor
|
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Contributor
Contributor
|
bugbot run |
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
…iations route Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Contributor
|
bugbot run |
Contributor
Contributor
There was a problem hiding this comment.
✅ 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 ef4ca2e. Configure here.
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
TLDR
Problem this solves:
/v1/images/editsalias spellings and the/images/*and Azure path aliases had no proxy-level wire testHow it solves it:
User Flow
Before: a team that edits images through the proxy with the OpenAI SDK could ship a proxy build that broke them, because no test sent the request the way the SDK does
client.images.edit(image=[a, b], mask=m, prompt=...), which sends POST https://litellm-domain/v1/images/edits withimage[]andmask[]partsAfter: the same change fails the integration suite before it can merge
image[]andmask[]partsmaskfile part, with no text part carrying an uploadRelevant issues
Follow-up to #39512, which fixed the
image[]/mask[]regression introduced by #38104Linear ticket
Related to LIT-6816, LIT-7940 and LIT-6807
What is now covered
Every inbound spelling of
/v1/images/editsnow goes through the real proxy against a scripted OpenAI upstream, and the upstream handler parses the multipart body into an ordered list of parts instead of the old name-to-value dicts, so a duplicate or stringified part can no longer hide. The covered spellings areimage,image[], repeatedimage[], repeatedimage,maskwithimage,mask[]withimage[], and the real openai-pythonimages.edit(image=[a, b], mask=m, n=2)call. Each test asserts the exact text parts (sorted, duplicates kept), every file part with its name, filename, content type and bytes equal to the upload, the client-parsed response, and that exactly one upstream request happened. Sending bothimageandimage[], or bothmaskandmask[], now has a test asserting the 422 body and zero upstream requests. A multipartn=2is asserted to reach fal as the integernum_images: 2in the full JSON body, and to reach OpenAI as one text partn=2. The OpenAI SDK'simages.generatecall with every gpt-image field is asserted against the full upstream JSON body, withnas an integer, and the SDK-parsed data and usageThe bracketed
image[]test is parametrized over/v1/images/edits,/images/editsand/openai/deployments/{model}/images/edits, and the full gpt-image generation test drives the SDK at/v1/images/generations,/images/generationsand, throughAzureOpenAI,/openai/deployments/{model}/images/generations, so deleting any one route decorator fails a test. Twomask[]parts are asserted to reach OpenAI as exactly onemaskfile part carrying the first upload's bytes, which is the contractOpenAIImageEditConfigimplements today/v1/images/variationsis a product bug on main:litellm.image_variationexists but the proxy registers no route, soPOST /v1/images/variationsreturns 404{"detail":"Not Found"}.tests/integration/providers/test_openai_image_variation_wire.py::test_image_variation_multipart_image_and_string_n_reach_openai_as_file_partholds the full contract (oneimagefile part with the upload bytes, text partsmodelandn=2, the parsed response, one upstream request) behindpytest.skip("BUG: POST /v1/images/variations is not routed by the proxy"). With the skip removed it fails onresponse.status_code == 200with 404Gap table
Every test was proven by mutation. The one-line change below was applied under
litellm/, the proxy restarted, the test run and seen failing on the named assertion, then the change reverted and the test seen passingtests/integration/providers/test_openai_image_edit_wire.py::test_image_edit_bracketed_image_field_reaches_openai_as_one_file_part[v1,unversioned,azure_deployment]endpoints.pyimage_edit_api:if key not in BRACKETED_FILE_FIELDStoif True_text_parts(parts) == (("model", ...), ("prompt", ...), ("seed", "42"))with an extra("image[]", "UploadFile(filename='red_circle.png', ...)")...test_image_edit_bracketed_image_field_reaches_openai_as_one_file_part[unversioned]endpoints.py: delete the@router.post("/images/edits", ...)decoratorresponse.status_code == 200got 404{"detail":"Not Found"}...test_image_edit_bracketed_image_field_reaches_openai_as_one_file_part[azure_deployment]endpoints.py: delete the@router.post("/openai/deployments/{model:path}/images/edits", ...)decoratorresponse.status_code == 200got 404...test_openai_image_edit_wire.py::test_image_edit_repeated_bracketed_mask_parts_reach_openai_as_first_mask_onlylitellm/llms/openai/image_edit/transformation.py:_mask = _mask[0] if _mask else Noneto_mask = _mask[-1] if _mask else None_file_parts(parts) == (image, first_mask)got the second mask's bytes...test_openai_image_edit_wire.py::test_image_edit_repeated_bracketed_image_parts_reach_openai_in_upload_orderendpoints.pybatch_to_bytesio:for u in uploadstofor u in uploads[:1](also fails under the images-1 mutation on_text_parts)_file_parts(parts) == (first, second)got one file part...test_openai_image_edit_wire.py::test_image_edit_bracketed_mask_reaches_openai_as_mask_file_partendpoints.py:BRACKETED_FILE_FIELDS = frozenset({IMAGE_ARRAY_FIELD, MASK_ARRAY_FIELD})tofrozenset({IMAGE_ARRAY_FIELD})(also fails under images-1)_text_parts(parts) == (("model", ...), ("prompt", ...))got an extra("mask[]", "UploadFile(filename=...)")...test_openai_image_edit_wire.py::test_image_edit_plain_mask_reaches_openai_as_mask_file_partendpoints.py:mask_files = await batch_to_bytesio(mask)tomask_files = Noneparsed.is_multipart()in_multipart_partsfailed, the upstream got no multipart body...test_openai_image_edit_wire.py::test_image_edit_repeated_plain_image_parts_reach_openai_as_two_file_partsendpoints.py:batch_to_bytesio(image)tobatch_to_bytesio(image[-1:] if image else image)_file_parts(parts) == (first, second)got only the second upload...test_openai_image_edit_wire.py::test_image_edit_rejects_plain_and_bracketed_spelling_together_without_calling_upstream[image]endpoints.py: delete theimage/image[]conflict guard (two lines)response.status_code == 422got 200...test_openai_image_edit_wire.py::test_image_edit_rejects_plain_and_bracketed_spelling_together_without_calling_upstream[mask]endpoints.py: delete themask/mask[]conflict guard (two lines)response.status_code == 422got 200tests/integration/providers/test_fal_ai_image_wire.py::test_fal_gpt_image_25_edit_sends_multipart_n_as_integer_num_imagesendpoints.py:numeric_fields=IMAGE_EDIT_NUMERIC_FORM_FIELDStonumeric_fields={}== {..., "num_images": 2}got"num_images": "2"tests/integration/providers/test_image_gen_drop_params_wire.py::test_gpt_image_generation_forwards_every_sdk_field_once_with_integer_n[v1,unversioned,azure_deployment]litellm/llms/openai/openai.pyimage_generation:**optional_paramsto**{k: v for k, v in optional_params.items() if k != "n"}== {..., "n": 2, ...}with non...test_gpt_image_generation_forwards_every_sdk_field_once_with_integer_n[unversioned]endpoints.py: delete the@router.post("/images/generations", ...)decoratorclient.images.generate(...)raisedopenai.NotFoundError404...test_gpt_image_generation_forwards_every_sdk_field_once_with_integer_n[azure_deployment]endpoints.py: delete the@router.post("/openai/deployments/{model:path}/images/generations", ...)decoratorclient.images.generate(...)raisedopenai.NotFoundError404tests/integration/providers/test_openai_image_variation_wire.py::test_image_variation_multipart_image_and_string_n_reach_openai_as_file_partresponse.status_code == 200got 404{"detail":"Not Found"}...test_openai_image_edit_wire.py::test_openai_sdk_image_edit_with_image_list_and_mask_reaches_openai_as_file_partsendpoints.pyimage_edit_api:if key not in BRACKETED_FILE_FIELDStoif True_text_parts(parts) == (("model", ...), ("n", "2"), ("prompt", ...))got an extra("image[]", "UploadFile(filename='second.png', ...)")The OpenAI half of images-7 (text part
n=2exactly once) is asserted inside the images-2 test. It stays green under the images-7 mutation because OpenAI receives the string form either way, so the integer contract is proven only on the fal side. The requested second mutation, making the multipart flattenerstr()an upload, is already the live behavior on main (_form_field_valueinlitellm/litellm_core_utils/llm_request_utils.pyhas no file branch), so it is not a separate edit. The raw-alias filter is the only guard, and reverting it reproduces the full RCA symptom on top of the livestr()fallback: text parts'image[]'='UploadFile(...)'and'mask[]'='UploadFile(...)'reach the upstream, which the images-1 and images-3 tests reject part by partScreenshots / Proof of Fix
Tests-only change, so the proof is the integration selection run against the CircleCI-shaped rig
Rig
Postgres on 127.0.0.1:5432 (
circle_test), Redis on 127.0.0.1:6379, the owned upstream on 127.0.0.1:8190, and the proxy started the way.circleci/scripts/run_integration.shdoes itAfter (ef4ca2e)
uv run --no-sync python tests/integration/run.py providers tests/integration/providers/test_openai_image_edit_wire.py tests/integration/providers/test_fal_ai_image_wire.py tests/integration/providers/test_image_gen_drop_params_wire.py tests/integration/providers/test_openai_image_variation_wire.pygave24 passed, 1 skipped in 33.97s, the skip being the variations BUG test24 passed, 1 skipped in 29.85swith the identical 25 selected node idsmake checkgavecheck: PASSType
Test
Pre-Submission checklist
Caveats (if any)
Low
/v1/images/variationshas no proxy route on main, so its test is skipped as a product bug until the route landsimage.png/mask.png, which the provider transformation setsThe diff changes only test functions and helpers, and no product symbol, so the /live-pr-risk dependency graph is empty and there is no base vs head A/B to run
Link to Devin session: https://app.devin.ai/sessions/bd64a051f8c54192af4b2e29eebbf7ee
Open in Devin Desktop: https://app.devin.ai/desktop/session/bd64a051f8c54192af4b2e29eebbf7ee?variant=devin
Requested by: @mateo-berri
Note
Low Risk
Test-only changes with no production code; CI runtime may increase slightly from new integration cases.
Overview
Adds integration wire tests so the proxy’s image APIs are exercised the way clients (OpenAI SDK, multipart uploads, path aliases) actually call them, with assertions on the exact upstream request bodies and multipart parts.
Image edits coverage is expanded heavily:
image/image[], repeated uploads,mask/mask[](including first-mask-only for duplicatemask[]), OpenAI SDKimages.editwith image list + mask, route aliases (/v1,/images, Azure deployment path), and 422 whenimageandimage[](ormaskandmask[]) are mixed—with no upstream call. Multipart helpers now assert ordered file parts (name, filename, type, bytes) instead of loose dicts.GPT image generation gets a parametrized test that drives the official SDK across the same path aliases and checks the full JSON upstream payload (including
nas integer and usage parsing). Fal edit gets a test that string formn=2becomesnum_images: 2in JSON.A new variations wire test documents the desired contract but is
pytest.skip’d becausePOST /v1/images/variationsis not routed on main (404).Reviewed by Cursor Bugbot for commit ef4ca2e. Bugbot is set up for automated code reviews on this repo. Configure here.