feat(tools): support GIF images in view_image (#4438)

Add GIF to the view_image allowlist: map the .gif extension to
image/gif and detect the GIF87a/GIF89a magic bytes so the existing
extension/content cross-check accepts GIFs instead of rejecting them
as an unsupported format. Covered by a new success test.
This commit is contained in:
Ryker_Feng 2026-07-24 13:12:43 +08:00 committed by GitHub
parent 80c06414f8
commit cd9432bcc1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 24 additions and 1 deletions

View File

@ -22,6 +22,7 @@ _EXTENSION_TO_MIME = {
".jpeg": "image/jpeg",
".png": "image/png",
".webp": "image/webp",
".gif": "image/gif",
}
@ -36,6 +37,8 @@ def _detect_image_mime(image_data: bytes) -> str | None:
return "image/png"
if len(image_data) >= 12 and image_data.startswith(b"RIFF") and image_data[8:12] == b"WEBP":
return "image/webp"
if image_data.startswith((b"GIF87a", b"GIF89a")):
return "image/gif"
return None
@ -63,7 +66,7 @@ def view_image_tool(
- For multiple files at once (use present_files instead)
Args:
image_path: Absolute /mnt/user-data virtual path to the image file. Common formats supported: jpg, jpeg, png, webp.
image_path: Absolute /mnt/user-data virtual path to the image file. Common formats supported: jpg, jpeg, png, webp, gif.
"""
from deerflow.sandbox.exceptions import SandboxRuntimeError
from deerflow.sandbox.tools import (

View File

@ -12,6 +12,9 @@ view_image_module = importlib.import_module("deerflow.tools.builtins.view_image_
PNG_BYTES = base64.b64decode("iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==")
# Minimal 1x1 transparent GIF (starts with the "GIF89a" magic bytes).
GIF_BYTES = base64.b64decode("R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7")
def _make_thread_data(tmp_path: Path) -> dict[str, str]:
user_data = tmp_path / "threads" / "thread-1" / "user-data"
@ -73,6 +76,23 @@ def test_view_image_reads_virtual_uploads_path(tmp_path: Path) -> None:
assert viewed_image["actual_path"] == str(image_path)
def test_view_image_reads_gif(tmp_path: Path) -> None:
thread_data = _make_thread_data(tmp_path)
image_path = Path(thread_data["uploads_path"]) / "animation.gif"
image_path.write_bytes(GIF_BYTES)
result = view_image_tool.func(
runtime=_make_runtime(thread_data),
image_path="/mnt/user-data/uploads/animation.gif",
tool_call_id="tc-gif",
)
assert _message_content(result) == "Successfully read image"
viewed_image = result.update["viewed_images"]["/mnt/user-data/uploads/animation.gif"]
assert viewed_image["mime_type"] == "image/gif"
assert viewed_image["size"] == len(GIF_BYTES)
def test_view_image_rejects_spoofed_extension(tmp_path: Path) -> None:
thread_data = _make_thread_data(tmp_path)
image_path = Path(thread_data["uploads_path"]) / "not-really.png"