Skip to content

fix(gemini): support images in tool_results for /v1/messages routing#23724

Merged
4 commits merged intoBerriAI:litellm_oss_staging_03_17_2026from
awais786:fix/tool-result-images-gemini
Mar 17, 2026
Merged

fix(gemini): support images in tool_results for /v1/messages routing#23724
4 commits merged intoBerriAI:litellm_oss_staging_03_17_2026from
awais786:fix/tool-result-images-gemini

Conversation

@awais786
Copy link
Copy Markdown
Contributor

convert_to_gemini_tool_call_result() dropped images in two cases:

  • data-URL strings (data:image/...;base64,...) treated as plain text
  • Anthropic image blocks in list content skipped

Add detection and convert both to Gemini inline_data BlobType so image bytes are preserved.

Fixes #23712.

Relevant issues

Pre-Submission checklist

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

  • I have Added testing in the tests/test_litellm/ directory, Adding at least 1 test is a hard requirement - see details
  • My PR passes all unit tests on make test-unit
  • My PR's scope is as isolated as possible, it only solves 1 specific problem
  • I have requested a Greptile review by commenting @greptileai and received a Confidence Score of at least 4/5 before requesting a maintainer review

Delays 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)

CI status guideline:

  • 50-55 passing tests: main is stable with minor issues.
  • 45-49 passing tests: acceptable but needs attention
  • <= 40 passing tests: unstable; be careful with your merges and assess the risk.
  • 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

convert_to_gemini_tool_call_result() dropped images in two cases:
- data-URL strings (data:image/...;base64,...) treated as plain text
- Anthropic image blocks in list content skipped

Add detection and convert both to Gemini inline_data BlobType so image
bytes are preserved.

Fixes BerriAI#23712.
@vercel
Copy link
Copy Markdown

vercel bot commented Mar 16, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
litellm Error Error Mar 16, 2026 11:40am

Request Review

convert_to_gemini_tool_call_result() dropped images in two cases:
- data-URL strings (data:image/...;base64,...) treated as plain text
- Anthropic image blocks in list content skipped

Add detection and convert both to Gemini inline_data BlobType so image
bytes are preserved.

Fixes BerriAI#23712.
@codspeed-hq
Copy link
Copy Markdown
Contributor

codspeed-hq bot commented Mar 16, 2026

Merging this PR will not alter performance

✅ 16 untouched benchmarks


Comparing awais786:fix/tool-result-images-gemini (41f3282) with main (58e74a6)

Open in CodSpeed

convert_to_gemini_tool_call_result() dropped images in two cases:
- data-URL strings (data:image/...;base64,...) treated as plain text
- Anthropic image blocks in list content skipped

Add detection and convert both to Gemini inline_data BlobType so image
bytes are preserved.

Fixes BerriAI#23712.
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps bot commented Mar 16, 2026

Greptile Summary

This PR fixes silent image data loss in convert_to_gemini_tool_call_result when routing Anthropic /v1/messages requests through Gemini. Two previously unhandled cases are now correctly converted to Gemini inline_data parts: (1) tool result content that is a bare data-URL string, and (2) Anthropic-native {"type": "image", "source": {...}} blocks in list content. The inline_data single-variable was also correctly refactored to an inline_data_list so multiple images in one tool result are all preserved.

Key changes:

  • Data-URL strings (e.g. data:image/png;base64,...) in str content are detected, parsed, and promoted to inline_data entries; extra MIME parameters like ;charset=UTF-8 are stripped cleanly
  • Anthropic "image" blocks with "source": {"type": "base64", ...} in list content are now converted rather than silently skipped
  • Pre-existing input_image/image_url handling is also migrated from the old single-variable pattern to the list, preserving multiple images correctly
  • Four focused unit tests are added covering single image, multiple images, plain data-URL, and data-URL with extra MIME params — all mock-only with no real network calls

Issues found:

  • elif content_type == "image": only handles source.type == "base64"; blocks with source.type == "url" are silently discarded without logging a warning, which can cause confusing data loss

Confidence Score: 4/5

  • PR is safe to merge with one minor gap to address: URL-sourced Anthropic image blocks are silently dropped.
  • The core logic is correct and well-tested. The inline_data_list refactor is clean and backward-compatible — callers already handle both VertexPartType and List[VertexPartType] returns. The only concern is that the new "image" branch silently discards URL-typed source blocks without a warning, which could cause confusing behaviour for users passing Anthropic URL images through tool results. This is a minor gap that doesn't affect the primary bug fix.
  • litellm/litellm_core_utils/prompt_templates/factory.py — the new elif content_type == "image": branch (lines 1530–1544) should log a warning for unhandled source types to avoid silent data loss.

Important Files Changed

Filename Overview
litellm/litellm_core_utils/prompt_templates/factory.py Extends convert_to_gemini_tool_call_result to detect and convert data-URL strings and Anthropic-native image blocks into Gemini inline_data parts. Correctly refactors single inline_data variable into a list to support multiple images. One gap: URL-sourced Anthropic image blocks (source.type == "url") are silently dropped without a warning.
tests/test_litellm/litellm_core_utils/prompt_templates/test_litellm_core_utils_prompt_templates_factory.py Adds four new unit tests covering single/multiple Anthropic image blocks, plain data-URL strings, and data URLs with extra MIME parameters. All tests use local in-memory data — no real network calls — and correctly validate both MIME type and data integrity.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[convert_to_gemini_tool_call_result] --> B{message content type?}
    B -->|str| C{starts with data: and has ;base64,?}
    C -->|Yes| D[Parse MIME type, strip extra params\nadd to inline_data_list\nclear content_str]
    C -->|No| E[Use as plain content_str]
    B -->|list| F[Iterate content blocks]
    F --> G{block type?}
    G -->|text| H[Append to content_str]
    G -->|image NEW| I{source.type == base64?}
    I -->|Yes| J[BlobType from source.data + source.media_type\nadd to inline_data_list]
    I -->|No URL/other| K[⚠️ Silently skipped — no warning logged]
    G -->|input_image / image_url| L[convert_to_anthropic_image_obj\nadd BlobType to inline_data_list]
    G -->|file / input_file| M[convert_to_anthropic_image_obj\nadd BlobType to inline_data_list]
    D --> N[Build function_response from content_str]
    E --> N
    H --> N
    J --> N
    L --> N
    M --> N
    N --> O{inline_data_list empty?}
    O -->|No| P[Return list: function_response part + one inline_data part per entry]
    O -->|Yes| Q[Return single function_response VertexPartType]
Loading

Last reviewed commit: 41f3282

Comment on lines +1512 to +1516
mime_rest = content_str[5:].split(";base64,", 1)
if len(mime_rest) == 2 and mime_rest[0].startswith("image/"):
inline_data = BlobType(
data=mime_rest[1], mime_type=mime_rest[0]
)
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.

MIME type extraction may include extra parameters

The current split keeps the full segment before ;base64, as the mime_type. For standard image data URLs (data:image/png;base64,...) this is fine, but if extra parameters appear between the media-type and the base64 marker (e.g. data:image/png;charset=UTF-8;base64,...), the resulting mime_type would be "image/png;charset=UTF-8" instead of just "image/png", which could be rejected by the Gemini API.

Consider extracting only the part up to the first ; or space to guard against this:

Suggested change
mime_rest = content_str[5:].split(";base64,", 1)
if len(mime_rest) == 2 and mime_rest[0].startswith("image/"):
inline_data = BlobType(
data=mime_rest[1], mime_type=mime_rest[0]
)
mime_rest = content_str[5:].split(";base64,", 1)
if len(mime_rest) == 2 and mime_rest[0].startswith("image/"):
raw_mime = mime_rest[0].split(";")[0].strip()
inline_data = BlobType(
data=mime_rest[1], mime_type=raw_mime

Comment on lines +1528 to +1540
elif content_type == "image":
# Anthropic-native image block: {"type": "image", "source": {"type": "base64", ...}}
source = content.get("source", {})
if isinstance(source, dict) and source.get("type") == "base64":
try:
inline_data = BlobType(
data=source.get("data", ""),
mime_type=source.get("media_type", "image/jpeg"),
)
except Exception as e:
verbose_logger.warning(
f"Failed to process Anthropic image block in tool response: {e}"
)
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.

Only the last image is preserved when multiple image blocks are in one list

inline_data is a single variable; each image block encountered in the loop overwrites the previous value. If a tool result contains more than one image (or an image alongside an input_image/image_url block), all but the last image are silently discarded.

This same limitation exists for the pre-existing input_image/image_url branches, but the new image branch makes the problem more visible now that Anthropic native image blocks are recognised.

If multiple images must be supported, inline_data would need to become a list and the return path adjusted to emit one inline_data part per image:

inline_data_list: List[BlobType] = []
...
# inside the loop:
inline_data_list.append(BlobType(...))
...
# at the bottom:
if inline_data_list:
    return [_part] + [{"inline_data": d} for d in inline_data_list]

Even if multi-image tool results are rare today, a warning log when more than one image is detected would prevent silent data loss.

Comment on lines +1530 to +1544
elif content_type == "image":
# Anthropic-native image block: {"type": "image", "source": {"type": "base64", ...}}
source = content.get("source", {})
if isinstance(source, dict) and source.get("type") == "base64":
try:
inline_data_list.append(
BlobType(
data=source.get("data", ""),
mime_type=source.get("media_type", "image/jpeg"),
)
)
except Exception as e:
verbose_logger.warning(
f"Failed to process Anthropic image block in tool response: {e}"
)
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.

URL-sourced Anthropic image blocks silently dropped

The new elif content_type == "image": branch only converts blocks where source.get("type") == "base64". If an Anthropic image block arrives with source.type == "url" (a valid Anthropic source type), the block is silently skipped — no warning is logged and the image is lost with no indication to the caller.

This is a silent data-loss gap: the input_image/image_url branch above does handle URL-based images via convert_to_anthropic_image_obj, so URL-sourced images would work through that path but not through the new native image block path.

Consider logging a warning for the unhandled source.type cases, similar to the error handling already used in this function:

elif content_type == "image":
    source = content.get("source", {})
    if isinstance(source, dict) and source.get("type") == "base64":
        try:
            inline_data_list.append(
                BlobType(
                    data=source.get("data", ""),
                    mime_type=source.get("media_type", "image/jpeg"),
                )
            )
        except Exception as e:
            verbose_logger.warning(
                f"Failed to process Anthropic image block in tool response: {e}"
            )
    else:
        source_type = source.get("type") if isinstance(source, dict) else type(source)
        verbose_logger.warning(
            f"Unsupported Anthropic image source type '{source_type}' in tool response; image will be dropped."
        )

@ghost ghost changed the base branch from main to litellm_oss_staging_03_17_2026 March 17, 2026 05:37
@ghost ghost merged commit 186c2ad into BerriAI:litellm_oss_staging_03_17_2026 Mar 17, 2026
36 of 39 checks passed
This pull request was closed.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: Images in tool_results silently dropped when routing /v1/messages to Gemini models

1 participant