Skip to content

fix: sanitize unsupported image MIME types (e.g. GIF) in context cleaner#9335

Open
xiaoyuyu6420 wants to merge 2 commits into
AstrBotDevs:masterfrom
xiaoyuyu6420:fix/9295-gif-mime-sanitize
Open

fix: sanitize unsupported image MIME types (e.g. GIF) in context cleaner#9335
xiaoyuyu6420 wants to merge 2 commits into
AstrBotDevs:masterfrom
xiaoyuyu6420:fix/9295-gif-mime-sanitize

Conversation

@xiaoyuyu6420

@xiaoyuyu6420 xiaoyuyu6420 commented Jul 20, 2026

Copy link
Copy Markdown

Issue / 问题

Fixes #9295.

When a quoted animated GIF is added to conversation history, it can be
persisted as data:image/gif;base64,.... Some OpenAI-compatible Gemini
gateways declare image modality support but reject image/gif, so every
later request fails with mime type is not supported /
convert_request_failed until the session history is cleared.

sanitize_contexts_by_modalities() previously only checked coarse modality
flags (image / audio / tool_use) and short-circuited when all three were
enabled, so GIF MIME cleanup never ran for the reporter's configuration.

Modifications / 改动点

  • Keep a narrow unsupported MIME list (image/gif) in
    astrbot/core/provider/modalities.py.

  • Extract image MIME best-effort from OpenAI-style image_url data URLs and
    .gif HTTP URLs.

  • Replace unsupported GIF image blocks with [Image] even when the provider
    claims image support.

  • Preserve existing full image stripping when image modality is absent, and
    preserve JPEG/PNG/WebP / unknown-MIME images when image is supported.

  • Add unit tests for the reporter scenario and related edge cases.

  • This is NOT a breaking change. / 这不是一个破坏性变更。

Screenshots or Test Results / 运行截图或测试结果

.venv/bin/ruff format --check astrbot/core/provider/modalities.py tests/unit/test_modalities_sanitize.py
.venv/bin/ruff check astrbot/core/provider/modalities.py tests/unit/test_modalities_sanitize.py
.venv/bin/pytest tests/unit/test_modalities_sanitize.py -q
.venv/bin/pytest tests/agent/test_context_manager.py tests/unit/test_astr_main_agent.py tests/test_tool_loop_agent_runner.py -q

Results:

  • ruff format: 2 files already formatted
  • ruff check: All checks passed
  • new unit tests: 12 passed
  • related regression suite: 185 passed

Trade-off note

This intentionally uses a narrow hard-coded MIME blacklist (image/gif)
instead of a new provider config switch, to keep the fix minimal and focused
on the reported failure mode. Providers that truly accept GIF will lose GIF
image blocks and receive a text placeholder instead. If maintainers prefer a
configurable MIME whitelist/blacklist, that can be layered on later without
reverting this safety net.

Checklist / 检查清单

  • 😊 If there are new features added in the PR, I have discussed it with the authors through issues/emails, etc.
    / 如果 PR 中有新加入的功能,已经通过 Issue / 邮件等方式和作者讨论过。

  • 👀 My changes have been well-tested, and "Verification Steps" and "Screenshots" have been provided above.
    / 我的更改经过了良好的测试,并已在上方提供了“验证步骤”和“运行截图”

  • 🤓 I have ensured that no new warnings or errors are introduced.
    / 我已确保没有引入新的警告或错误。

Summary by Sourcery

Sanitize conversation contexts to strip unsupported image MIME types (notably GIF) even when providers claim image support, preventing incompatible gateways from rejecting subsequent requests.

Bug Fixes:

  • Ensure animated GIF image parts are replaced with text placeholders instead of being sent to providers that reject image/gif, avoiding persistent request failures.
  • Preserve existing behavior of fully stripping images when image modality is unavailable while keeping supported or unknown image types intact when image is allowed.

Tests:

  • Add unit tests covering GIF data URLs, GIF HTTP URLs, mixed content, non-GIF formats, unknown MIME handling, and empty inputs to verify the updated sanitization logic.

Fixes AstrBotDevs#9295.

When a provider declares image modality support, the context sanitizer
previously kept every image block as-is. Animated GIFs can then be
persisted into session history and later cause continuous failures on
OpenAI-compatible Gemini gateways that reject image/gif.

- Drop image/gif data URLs and .gif http(s) URLs to a [Image] placeholder
  even when the provider supports image modality.
- Keep the existing full image-strip path when image modality is absent.
- Preserve JPEG/PNG/WebP and unknown-MIME images when image is supported.
- Add unit coverage for the reporter scenario and related edge cases.
@dosubot dosubot Bot added size:M This PR changes 30-99 lines, ignoring generated files. area:provider The bug / feature is about AI Provider, Models, LLM Agent, LLM Agent Runner. labels Jul 20, 2026

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

Hey - I've found 2 issues, and left some high level feedback:

  • In _extract_image_mime, consider using urllib.parse.urlsplit (or similar) instead of manual split('?', '#') to make URL path handling more robust for edge cases like encoded ?/# in the path.
  • Given that _UNSUPPORTED_IMAGE_MIMES is meant as a safety net for multiple gateways, you might want to expose a simple extension point (e.g., optional config injection or an overridable helper) so providers that truly support GIF can opt out without code changes.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `_extract_image_mime`, consider using `urllib.parse.urlsplit` (or similar) instead of manual `split('?', '#')` to make URL path handling more robust for edge cases like encoded `?`/`#` in the path.
- Given that `_UNSUPPORTED_IMAGE_MIMES` is meant as a safety net for multiple gateways, you might want to expose a simple extension point (e.g., optional config injection or an overridable helper) so providers that truly support GIF can opt out without code changes.

## Individual Comments

### Comment 1
<location path="astrbot/core/provider/modalities.py" line_range="85-93" />
<code_context>
+                if path.endswith(ext):
+                    return mime
+
+    source = part.get("source")
+    if isinstance(source, dict):
+        media_type = source.get("media_type")
+        if isinstance(media_type, str):
+            return media_type.lower()
</code_context>
<issue_to_address>
**suggestion (bug_risk):** Normalize `media_type` by stripping parameters (e.g. `image/gif; charset=binary`) before comparison.

In the current branch, `media_type` is lowercased but not stripped of parameters. If an API returns values like `image/gif; charset=binary` or `image/jpeg;quality=80`, `_is_unsupported_image_mime` will not match entries such as `image/gif` in `_UNSUPPORTED_IMAGE_MIMES`.

To align with the data-URL handling and make matching robust across providers, normalize by stripping parameters before lowercasing, e.g. `media_type.split(";", 1)[0].strip().lower()`.

```suggestion
    source = part.get("source")
    if isinstance(source, dict):
        media_type = source.get("media_type")
        if isinstance(media_type, str):
            # Normalize media type by stripping parameters before comparison,
            # e.g. "image/gif; charset=binary" -> "image/gif"
            return media_type.split(";", 1)[0].strip().lower()

    mime_type = part.get("mimeType") or part.get("mime_type")
    if isinstance(mime_type, str):
        # Normalize MIME type by stripping parameters before comparison
        return mime_type.split(";", 1)[0].strip().lower()
```
</issue_to_address>

### Comment 2
<location path="astrbot/core/provider/modalities.py" line_range="91-93" />
<code_context>
+        if isinstance(media_type, str):
+            return media_type.lower()
+
+    mime_type = part.get("mimeType") or part.get("mime_type")
+    if isinstance(mime_type, str):
+        return mime_type.lower()
+
</code_context>
<issue_to_address>
**suggestion:** Consider stripping parameters from `mimeType`/`mime_type` as well to align with other MIME parsing.

Because `mimeType`/`mime_type` is returned as `mime_type.lower()`, a value like `image/gif;codec=xyz` won’t match `_UNSUPPORTED_IMAGE_MIMES` even though the base type is `image/gif`. Normalizing it with `split(";", 1)[0].strip().lower()` would align this path with data URLs and ensure unsupported image types are correctly detected.

```suggestion
    mime_type = part.get("mimeType") or part.get("mime_type")
    if isinstance(mime_type, str):
        # Strip any parameters (e.g. "image/gif;codec=xyz" → "image/gif")
        # to align with other MIME parsing and ensure unsupported types are detected.
        return mime_type.split(";", 1)[0].strip().lower()
```
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread astrbot/core/provider/modalities.py Outdated
Comment thread astrbot/core/provider/modalities.py Outdated

@gemini-code-assist gemini-code-assist 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.

Code Review

This pull request introduces a mechanism to filter out unsupported image MIME types (specifically image/gif) during context sanitization, preventing issues with gateways (such as Gemini-compatible proxies) that reject them even when the model claims image support. It includes robust extraction of MIME types from data URLs, path extensions, and various payload structures, accompanied by comprehensive unit tests. The review feedback suggests enhancing robustness by stripping leading and trailing whitespace from URLs, media types, and MIME types before processing.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment on lines +70 to +72
if isinstance(url, str):
# data URLs look like "data:image/gif;base64,...."
if url.lower().startswith("data:"):

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.

medium

To make the MIME extraction more robust against leading or trailing whitespace in URLs (which can occasionally occur in raw or user-submitted contexts), consider stripping the url string before processing.

Suggested change
if isinstance(url, str):
# data URLs look like "data:image/gif;base64,...."
if url.lower().startswith("data:"):
if isinstance(url, str):
url = url.strip()
# data URLs look like "data:image/gif;base64,...."
if url.lower().startswith("data:"):

Comment thread astrbot/core/provider/modalities.py Outdated
Comment on lines +85 to +93
source = part.get("source")
if isinstance(source, dict):
media_type = source.get("media_type")
if isinstance(media_type, str):
return media_type.lower()

mime_type = part.get("mimeType") or part.get("mime_type")
if isinstance(mime_type, str):
return mime_type.lower()

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.

medium

Similar to how the data URL MIME extraction strips whitespace (on line 75), it is safer to strip leading/trailing whitespace from media_type and mime_type to ensure robust matching against _UNSUPPORTED_IMAGE_MIMES.

Suggested change
source = part.get("source")
if isinstance(source, dict):
media_type = source.get("media_type")
if isinstance(media_type, str):
return media_type.lower()
mime_type = part.get("mimeType") or part.get("mime_type")
if isinstance(mime_type, str):
return mime_type.lower()
source = part.get("source")
if isinstance(source, dict):
media_type = source.get("media_type")
if isinstance(media_type, str):
return media_type.strip().lower()
mime_type = part.get("mimeType") or part.get("mime_type")
if isinstance(mime_type, str):
return mime_type.strip().lower()

Address Sourcery/Gemini review feedback on AstrBotDevs#9335 without adding new
abstractions.

- Strip parameters (e.g. "image/gif; charset=binary") and whitespace from
  source.media_type and mimeType/mime_type before comparing against
  _UNSUPPORTED_IMAGE_MIMES, so parameterized GIF values are still detected.
- Strip surrounding whitespace from image URLs defensively.
- Switch the http(s) URL path extraction from a manual split('?', '#') to
  urllib.parse.urlsplit, which keeps percent-encoded delimiters inside the
  path and is more robust for edge cases.
- Inline the normalization at both call sites instead of introducing a
  helper, keeping it consistent with the existing data-URL branch and
  honoring the repo's No-Unnecessary-Helpers rule.

Adds 5 unit tests covering parameterized media_type/mimeType, the encoded
path case, and a non-GIF-with-params regression guard.
@xiaoyuyu6420

Copy link
Copy Markdown
Author

Pushed an iteration addressing the review feedback. Summary of decisions:

Adopted

  • source.media_type and mimeType/mime_type now strip parameters (e.g. image/gif; charset=binaryimage/gif) and surrounding whitespace before comparison, matching how data URLs were already handled. Adds regression tests.
  • Image URLs are .strip()-ed defensively.
  • http(s) URL path extraction switched to urllib.parse.urlsplit, which keeps percent-encoded ?/# inside the path (more robust than the manual split).

Not adopted — extension point / config injection for _UNSUPPORTED_IMAGE_MIMES
Kept this a narrow, code-level blocklist on purpose. Today only image/gif is known to be rejected by some Gemini-flavored OpenAI gateways, so a single-element check is the smallest change that fixes the reported bug. Adding a config knob or overridable helper right now would be speculative generality with no second use case. Happy to promote it to a configurable list once there is a real second MIME that needs different handling per provider.

Note on scope (issue #9295)
This PR focuses on preventing new GIFs from being sent to / persisted in request payloads. The reporter also raised two related asks that are deliberately out of scope here, to keep this change reviewable:

  • sanitizing already-poisoned session history in storage, and
  • auto-retry once on convert_request_failed / mime type is not supported.

If maintainers want either, I can follow up in a separate PR rather than expand this one.

Tests: 16 passed (11 existing + 5 new). ruff format / ruff check clean.

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

Labels

area:provider The bug / feature is about AI Provider, Models, LLM Agent, LLM Agent Runner. size:M This PR changes 30-99 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug] 引用 GIF 会污染会话历史并导致 Gemini 后续请求持续失败

1 participant