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:
teknium1
2026-06-04 06:16:45 -07:00
committed by Teknium
co-authored by kyssta-exe
parent 6bdbe30763
commit dd4ba4c2c4
7 changed files with 203 additions and 11 deletions
+68
View File
@@ -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
# ---------------------------------------------------------------------------