fix(vision): cap pixel dimensions proactively at embed time + declare Pillow
Follow-up to the salvaged #37727. That PR fixed the reactive recovery path (classifier + post-failure shrinker) but left the PROACTIVE embed-time guard in vision_tools byte-only — a tall small-byte screenshot (e.g. 1200x12000 at 0.06 MB) still baked into immutable history un-resized, relying on a failed round-trip to trigger reactive shrink. - vision_tools: add _image_exceeds_dimension() + _EMBED_MAX_DIMENSION (7900px); the embed-time cap now fires on bytes OR pixels and passes max_dimension to the resizer, so tall small-byte images are shrunk before they're embedded. - vision_tools: best-effort lazy-install of Pillow (tool.vision) in the resize ImportError fallback so the soft dep self-heals (respects allow_lazy_installs). - error_classifier: add two more Anthropic dimension-cap wording variants. - pyproject + lazy_deps: declare Pillow as the [vision] extra / tool.vision lazy dep (it was undeclared everywhere; without it ALL resize recovery no-ops). - tests: cover _image_exceeds_dimension (tall/small/edge/no-Pillow/corrupt). Co-authored-by: kyssta-exe <kyssta-exe@users.noreply.github.com>
This commit is contained in:
committed by
Teknium
co-authored by
kyssta-exe
parent
6bdbe30763
commit
dd4ba4c2c4
@@ -15,6 +15,8 @@ from tools.vision_tools import (
|
||||
_determine_mime_type,
|
||||
_image_to_base64_data_url,
|
||||
_resize_image_for_vision,
|
||||
_image_exceeds_dimension,
|
||||
_EMBED_MAX_DIMENSION,
|
||||
_is_image_size_error,
|
||||
_MAX_BASE64_BYTES,
|
||||
_RESIZE_TARGET_BYTES,
|
||||
@@ -889,6 +891,72 @@ class TestResizeImageForVision:
|
||||
assert len(result) > 100
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# _image_exceeds_dimension — proactive embed-time pixel-cap detector
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestImageExceedsDimension:
|
||||
"""The proactive embed path checks pixel dimensions, not just bytes.
|
||||
|
||||
A tall full-page screenshot can be well under the byte budget yet far
|
||||
over Anthropic's 8000px per-side cap (e.g. 1200x12000 at 0.06 MB). The
|
||||
byte-only embed guard let it slip into immutable history un-resized,
|
||||
bricking the session on a non-retryable 400. This helper flags it so the
|
||||
embed-time resize fires on dimensions too.
|
||||
"""
|
||||
|
||||
def test_tall_small_byte_image_flagged(self, tmp_path):
|
||||
try:
|
||||
from PIL import Image
|
||||
except ImportError:
|
||||
pytest.skip("Pillow not installed")
|
||||
# 1200x12000 solid color: trips the pixel cap, tiny in bytes.
|
||||
img = Image.new("RGB", (1200, 12000), (40, 40, 40))
|
||||
path = tmp_path / "tall.png"
|
||||
img.save(path, "PNG")
|
||||
assert _image_exceeds_dimension(path, _EMBED_MAX_DIMENSION) is True
|
||||
|
||||
def test_small_image_not_flagged(self, tmp_path):
|
||||
try:
|
||||
from PIL import Image
|
||||
except ImportError:
|
||||
pytest.skip("Pillow not installed")
|
||||
img = Image.new("RGB", (800, 600), (10, 200, 10))
|
||||
path = tmp_path / "small.png"
|
||||
img.save(path, "PNG")
|
||||
assert _image_exceeds_dimension(path, _EMBED_MAX_DIMENSION) is False
|
||||
|
||||
def test_exactly_at_cap_not_flagged(self, tmp_path):
|
||||
try:
|
||||
from PIL import Image
|
||||
except ImportError:
|
||||
pytest.skip("Pillow not installed")
|
||||
img = Image.new("RGB", (_EMBED_MAX_DIMENSION, 100), (1, 2, 3))
|
||||
path = tmp_path / "edge.png"
|
||||
img.save(path, "PNG")
|
||||
# max == cap is fine; only strictly greater forces a resize.
|
||||
assert _image_exceeds_dimension(path, _EMBED_MAX_DIMENSION) is False
|
||||
|
||||
def test_missing_pillow_returns_false(self, tmp_path):
|
||||
# Without Pillow we can't inspect dimensions — return False so the
|
||||
# byte-based checks still apply and a missing soft dep never breaks
|
||||
# the embed path.
|
||||
path = tmp_path / "x.png"
|
||||
path.write_bytes(b"\x89PNG\r\n\x1a\n" + b"\x00" * 100)
|
||||
with patch.dict("sys.modules", {"PIL": None, "PIL.Image": None}):
|
||||
assert _image_exceeds_dimension(path, _EMBED_MAX_DIMENSION) is False
|
||||
|
||||
def test_corrupt_file_returns_false(self, tmp_path):
|
||||
try:
|
||||
import PIL # noqa: F401
|
||||
except ImportError:
|
||||
pytest.skip("Pillow not installed")
|
||||
path = tmp_path / "corrupt.png"
|
||||
path.write_bytes(b"not an image at all")
|
||||
assert _image_exceeds_dimension(path, _EMBED_MAX_DIMENSION) is False
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# _is_image_size_error — detect size-related API errors
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user