fix(fireworks): skip #transform=inline for base64 data URLs#23729
Conversation
Closes BerriAI#23583 Appending #transform=inline to a data: URL corrupted the base64 payload, causing binascii.Error (Incorrect padding) when Fireworks AI attempted to decode the image. Data URLs are already inlined so the fragment is a no-op anyway — guard both the str and dict image_url branches to skip the suffix when the URL starts with "data:". Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Greptile SummaryThis PR fixes a base64 corruption bug in the Fireworks AI image transformation path: Changes:
Confidence Score: 5/5
|
| Filename | Overview |
|---|---|
| litellm/llms/fireworks_ai/chat/transformation.py | Correct and minimal fix — guards both the str and dict image_url branches so that data: URLs never receive the #transform=inline fragment; includes a case-insensitive .lower() check per RFC 3986. |
| tests/llm_translation/test_fireworks_ai_translation.py | Three new parametrized test cases added: str branch, dict branch, and case-insensitive (Data:) variant — good coverage of the fix. |
| tests/test_litellm/llms/fireworks_ai/chat/test_fireworks_ai_chat_transformation.py | Standalone regression test with correct mock-only calls; minor PEP 8 formatting issue — missing two blank lines before the subsequent @pytest.mark.parametrize decorator. |
Flowchart
%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[_add_transform_inline_image_block called] --> B{vision in model OR disable flag?}
B -- Yes --> C[Return content unchanged]
B -- No --> D{image_url is str?}
D -- Yes --> E{starts with data: ?}
E -- Yes --> F[Return content unchanged - data URL already inlined]
E -- No --> G[Append transform=inline to image_url str]
G --> H[Return content]
D -- No --> I{image_url is dict?}
I -- Yes --> J{url starts with data: ?}
J -- Yes --> K[Return content unchanged - data URL already inlined]
J -- No --> L[Append transform=inline to image_url.url]
L --> H
I -- No --> H
Last reviewed commit: 37e1885
| if isinstance(content["image_url"], str): | ||
| content["image_url"] = f"{content['image_url']}#transform=inline" | ||
| # Skip base64 data URLs — appending #transform=inline corrupts the | ||
| # base64 payload and causes an "Incorrect padding" decode error on | ||
| # the Fireworks side. Data URLs are already inlined by definition. | ||
| if not content["image_url"].startswith("data:"): | ||
| content["image_url"] = f"{content['image_url']}#transform=inline" | ||
| elif isinstance(content["image_url"], dict): | ||
| content["image_url"][ | ||
| "url" | ||
| ] = f"{content['image_url']['url']}#transform=inline" | ||
| url = content["image_url"]["url"] | ||
| if not url.startswith("data:"): | ||
| content["image_url"]["url"] = f"{url}#transform=inline" |
There was a problem hiding this comment.
Missing test cases for the data: URL fix
The fix adds guards for both the str and dict branches, but no test cases were added to validate this behaviour. The existing test_transform_inline parametrized test in tests/llm_translation/test_fireworks_ai_translation.py covers HTTP URLs and vision-model bypass, but has no coverage for data: URLs (the exact scenario described in the linked issue).
The pre-submission checklist also explicitly requires at least one test in tests/test_litellm/. Without it, a future refactor could reintroduce the regression silently. The following cases are missing:
strbranch:{"image_url": "data:image/png;base64,abc123"}→ URL should remain unchangeddictbranch:{"image_url": {"url": "data:image/jpeg;base64,xyz=="}}→ URL should remain unchanged
Example additions to the test_transform_inline parametrize list:
(
{"image_url": "data:image/png;base64,iVBORw0KGgo="},
"gpt-4",
"data:image/png;base64,iVBORw0KGgo=", # must not be modified
),
(
{"image_url": {"url": "data:image/jpeg;base64,/9j/4AAQ=="}},
"gpt-4",
{"url": "data:image/jpeg;base64,/9j/4AAQ=="}, # must not be modified
),Rule Used: What: Ensure that any PR claiming to fix an issue ... (source)
| result = config._add_transform_inline_image_block(str_content) | ||
| assert result["image_url"] == data_url, "data URL must not be modified (str branch)" | ||
|
|
||
| # dict branch | ||
| dict_content = {"type": "image_url", "image_url": {"url": data_url}} | ||
| result = config._add_transform_inline_image_block(dict_content) | ||
| assert result["image_url"]["url"] == data_url, "data URL must not be modified (dict branch)" | ||
|
|
||
| # regular https URL should still get the suffix | ||
| https_content = {"type": "image_url", "image_url": "https://example.com/image.jpg"} | ||
| result = config._add_transform_inline_image_block(https_content) | ||
| assert result["image_url"].endswith("#transform=inline"), "https URL should get #transform=inline" |
There was a problem hiding this comment.
Test calls will raise TypeError — missing required arguments
_add_transform_inline_image_block has two required positional parameters (model: str and disable_add_transform_inline_image_block: Optional[bool]) that have no default values. All three invocations in this test omit both arguments, so every call will immediately raise:
TypeError: _add_transform_inline_image_block() missing 2 required positional arguments: 'model' and 'disable_add_transform_inline_image_block'
The tests need to pass a model name that does not contain "vision" (so the early-return guard is not triggered) and False for the disable flag:
| result = config._add_transform_inline_image_block(str_content) | |
| assert result["image_url"] == data_url, "data URL must not be modified (str branch)" | |
| # dict branch | |
| dict_content = {"type": "image_url", "image_url": {"url": data_url}} | |
| result = config._add_transform_inline_image_block(dict_content) | |
| assert result["image_url"]["url"] == data_url, "data URL must not be modified (dict branch)" | |
| # regular https URL should still get the suffix | |
| https_content = {"type": "image_url", "image_url": "https://example.com/image.jpg"} | |
| result = config._add_transform_inline_image_block(https_content) | |
| assert result["image_url"].endswith("#transform=inline"), "https URL should get #transform=inline" | |
| result = config._add_transform_inline_image_block(str_content, model="fireworks_ai/llama-v3", disable_add_transform_inline_image_block=False) | |
| assert result["image_url"] == data_url, "data URL must not be modified (str branch)" | |
| # dict branch | |
| dict_content = {"type": "image_url", "image_url": {"url": data_url}} | |
| result = config._add_transform_inline_image_block(dict_content, model="fireworks_ai/llama-v3", disable_add_transform_inline_image_block=False) | |
| assert result["image_url"]["url"] == data_url, "data URL must not be modified (dict branch)" | |
| # regular https URL should still get the suffix | |
| https_content = {"type": "image_url", "image_url": "https://example.com/image.jpg"} | |
| result = config._add_transform_inline_image_block(https_content, model="fireworks_ai/llama-v3", disable_add_transform_inline_image_block=False) | |
| assert result["image_url"].endswith("#transform=inline"), "https URL should get #transform=inline" |
| if not content["image_url"].startswith("data:"): | ||
| content["image_url"] = f"{content['image_url']}#transform=inline" | ||
| elif isinstance(content["image_url"], dict): | ||
| content["image_url"][ | ||
| "url" | ||
| ] = f"{content['image_url']['url']}#transform=inline" | ||
| url = content["image_url"]["url"] | ||
| if not url.startswith("data:"): | ||
| content["image_url"]["url"] = f"{url}#transform=inline" |
There was a problem hiding this comment.
Case-insensitive data: scheme check
Per RFC 3986, URI schemes are case-insensitive, so a data: URL could theoretically be written as Data:image/... or DATA:image/... and bypass this guard. While virtually all real-world data URLs use lowercase, a defensive lowercase normalisation would make this airtight:
| if not content["image_url"].startswith("data:"): | |
| content["image_url"] = f"{content['image_url']}#transform=inline" | |
| elif isinstance(content["image_url"], dict): | |
| content["image_url"][ | |
| "url" | |
| ] = f"{content['image_url']['url']}#transform=inline" | |
| url = content["image_url"]["url"] | |
| if not url.startswith("data:"): | |
| content["image_url"]["url"] = f"{url}#transform=inline" | |
| if not content["image_url"].lower().startswith("data:"): | |
| content["image_url"] = f"{content['image_url']}#transform=inline" | |
| elif isinstance(content["image_url"], dict): | |
| url = content["image_url"]["url"] | |
| if not url.lower().startswith("data:"): | |
| content["image_url"]["url"] = f"{url}#transform=inline" |
f33734b to
19cb9d3
Compare
27894d4 to
6a3a2d9
Compare
84b4af4
into
BerriAI:litellm_oss_staging_03_17_2026
| assert result["image_url"].endswith("#transform=inline"), "https URL should get #transform=inline" | ||
| @pytest.mark.parametrize( |
There was a problem hiding this comment.
Missing blank lines between test functions
PEP 8 requires two blank lines between top-level definitions. There is no blank line between the closing assert of test_add_transform_inline_image_block_skips_data_urls and the @pytest.mark.parametrize decorator that begins the next test. While this does not affect test execution, it makes the file harder to read and will typically trigger linting warnings.
| assert result["image_url"].endswith("#transform=inline"), "https URL should get #transform=inline" | |
| @pytest.mark.parametrize( | |
| assert result["image_url"].endswith("#transform=inline"), "https URL should get #transform=inline" | |
| @pytest.mark.parametrize( |
fix(fireworks): skip #transform=inline for base64 data URLs (#23729)
Closes #23583
Appending #transform=inline to a data: URL corrupted the base64 payload, causing binascii.Error (Incorrect padding) when Fireworks AI attempted to decode the image. Data URLs are already inlined so the fragment is a no-op anyway — guard both the str and dict image_url branches to skip the suffix when the URL starts with "data:".
Relevant issues
Pre-Submission checklist
Please complete all items before asking a LiteLLM maintainer to review your PR
tests/test_litellm/directory, Adding at least 1 test is a hard requirement - see detailsmake test-unit@greptileaiand received a Confidence Score of at least 4/5 before requesting a maintainer reviewDelays in PR merge?
If you're seeing a delay in your PR being merged, ping the LiteLLM Team on Slack (#pr-review).
CI (LiteLLM team)
Branch creation CI run
Link:
CI run for the last commit
Link:
Merge / cherry-pick CI run
Links:
Type
🆕 New Feature
🐛 Bug Fix
🧹 Refactoring
📖 Documentation
🚄 Infrastructure
✅ Test
Changes