# NousResearch/hermes-agent · Vision image size limits and browser screenshot controls

## Open now

- 5 issues, 7 PRs open on GitHub.
- #19620: browser_vision unconditionally passes --full, producing unreadable screenshots on long pages
- #25837: \[Bug\]: vision_analyze / browser_vision can brick session by inlining oversized image
- #47467: browser_vision skips proactive embed cap — oversized full-page screenshots brick sessions
- #54371: browser_vision always captures full-page screenshots; consider viewport-default for context cost
- #61994: image-shrink recovery re-encodes oversized images on every API call — repaired images never persisted to session history
- PR #19647: fix(browser): add full_page parameter to browser_vision for viewport-only screenshots
- PR #21833: feat(browser): add \`full\` parameter to \`browser_vision\` tool and default to false
- PR #25838: fix(vision): clamp image dimensions before inline base64 encode
- PR #39226: fix(browser): apply vision embed-time pixel/byte caps in browser_vision
- PR #47468: fix(vision): apply proactive embed cap to browser_vision screenshots
- PR #62005: fix(agent): persist image-shrink recovery
- PR #62051: fix(agent): validate and downscale oversized images at ingestion to prevent infinite shrink-recovery loops (#61994)

These items address oversized images (exceeding ~8000px or byte limits) crashing conversation sessions, and the related issue of browser_vision defaulting to full-page screenshots. Work spans pixel-dimension guards at embed time, shrink-recovery persistence, and making full-page capture opt-in.

## TL;DR

- 20 member(s): 5 open issue(s), 1 closed issue(s), 7 open PR(s), 1 merged PR(s), 6 PR(s) closed without merging.
- A fix was merged to main in PR #39033. Verify it addresses your case.
- **Recommended:** read PR #39033 (and any competing PRs below) before filing. Confirm the merged change covers your symptom.

## Maintainer resolution verdict

- Internal-tracker verdict over this cluster's issue members: `partially_resolved` · deciding PR(s): `pr-39033`. This is distinct from the fix status above. Weigh it yourself.

## How this cluster was grouped

- Link evidence (`STRUCTURAL_PARTIAL`): some members are connected by structural evidence (shared changed files, or a closing/superseded reference); the rest are joined by an automated relevance judgement over their text.
- Members outside that structural core may belong to a different underlying problem.
- Pairwise link density `0.11`: the share of member pairs that carry a link, counting a structural link in full and a relevance-only link at 0.36. `1.00` means every pair is linked.

## Open related issues

- #19620: browser_vision unconditionally passes --full, producing unreadable screenshots on long pages
- #25837: \[Bug\]: vision_analyze / browser_vision can brick session by inlining oversized image
- #47467: browser_vision skips proactive embed cap — oversized full-page screenshots brick sessions
- #54371: browser_vision always captures full-page screenshots; consider viewport-default for context cost
- #61994: image-shrink recovery re-encodes oversized images on every API call — repaired images never persisted to session history

## Closed / duplicate issues

- #37677: Oversized image (>8000px) permanently bricks a conversation thread — image guards check bytes, never pixel dimensions

## Prior & competing PRs

- **#37701: fix(vision): cap image pixel dimensions at 7900px on embed + reactive recover**. closed without merge · fix tier `suggested`
  - maintainer direction: category `implemented_elsewhere`; implication `superseded`; refs `#40490`, `#40490`, `#40490`
  - _suggested-tier fix detail withheld; re-request with `?tier=all` to include it._
- **#39033: fix(vision): guard image pixel dimensions (8000px cap), not just bytes**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`; implication `superseded`
  - files: `agent/conversation_compression.py`, `agent/error_classifier.py`, `pyproject.toml`, `scripts/release.py`, `tests/run_agent/test_image_shrink_recovery.py`, `tests/tools/test_vision_tools.py`, `tools/lazy_deps.py`, `tools/vision_tools.py`
  - fix hunks:
  - `agent/conversation_compression.py`

    ```diff
    @@ -646,6 +646,11 @@ def try_shrink_image_parts_in_messages(api_messages: list) -> bool:
         # much larger; shrinking to 4 MB here loses quality but only fires
         # after a confirmed provider rejection, so the alternative is failure.
         target_bytes = 4 * 1024 * 1024
    +    # Anthropic enforces an 8000px per-side dimension cap independently of
    +    # the 5 MB byte cap.  A tall screenshot can be well under 5 MB yet far
    +    # over 8000px (e.g. 1200×12000 at 0.06 MB).  We check pixel dimensions
    +    # even when the byte budget is fine.
    +    max_dimension = 8000
         changed_count = 0
         # Track parts that are over the target but could NOT be shrunk under it.
         # If any survive, retrying is pointless — the same oversized payload will
    ```

    ```diff
    @@ -658,9 +663,30 @@ def _shrink_data_url(url: str) -> Optional[str]:
             """Return a smaller data URL, or None if shrink can't help."""
             if not isinstance(url, str) or not url.startswith("data:"):
                 return None
    -        if len(url) <= target_bytes:
    -            # This specific image wasn't the oversized one.
    -            return None
    +
    +        # Check both byte size AND pixel dimensions.
    +        needs_shrink = len(url) > target_bytes  # over byte budget
    +        if not needs_shrink:
    +            # Even if bytes are fine, check pixel dimensions against
    +            # Anthropic's 8000px cap.  A tall image can be tiny in bytes
    +            # yet huge in pixels.
    +            try:
    +                import base64 as _b64_dim
    +                header_d, _, data_d = url.partition(",")
    +                if not data_d:
    +                    return None
    +                raw_d = _b64_dim.b64decode(data_d)
    +                from PIL import Image as _PILImage
    +                import io as _io_dim
    +                with _PILImage.open(_io_dim.BytesIO(raw_d)) as _img:
    +                    if max(_img.size) <= max_dimension:
    +                        return None  # both bytes and pixels are fine
    +                needs_shrink = True  # pixels exceed limit, force shrink
    +            except Exception:
    +                # If we can't check dimensions (Pillow unavailable, corrupt
    +                # image, etc.), fall back to byte-only check.
    +                return None
    +
             try:
                 header, _, data = url.partition(",")
                 mime = "image/jpeg"
    ```

    ```diff
    @@ -684,6 +710,7 @@ def _shrink_data_url(url: str) -> Optional[str]:
                         Path(tmp.name),
                         mime_type=mime,
                         max_base64_bytes=target_bytes,
    +                    max_dimension=max_dimension,
                     )
                 finally:
                     try:
    ```
  - `agent/error_classifier.py`

    ```diff
    @@ -171,6 +171,9 @@ def is_auth(self) -> bool:
         "image too large",      # generic
         "image_too_large",      # error_code variant
         "image size exceeds",   # variant
    +    "image dimensions exceed",  # Anthropic: "image dimensions exceed max allowed size: 8000 pixels"
    +    "dimensions exceed max allowed size",  # Anthropic dimension-cap (wording variant)
    +    "max allowed size: 8000",  # Anthropic dimension-cap (explicit pixel ceiling)
         # "request_too_large" on a request known to contain an image → image is
         # the likely culprit; we still try the shrink path before giving up.
     ]
    ```
  - `pyproject.toml`

    ```diff
    @@ -127,6 +127,14 @@ pty = [
       # without pulling in extra packages.
     ]
     honcho = ["honcho-ai==2.0.1"]
    +# Image resize recovery for the vision tools.  Pillow is a soft dependency:
    +# vision_tools / conversation_compression degrade gracefully without it (they
    +# log and skip the resize), but without it the byte AND pixel-dimension shrink
    +# paths silently no-op, so an oversized image (>5 MB or >8000px) bakes into
    +# immutable history and bricks the session on Anthropic's non-retryable 400.
    +# Declared here so packagers (Nix, Homebrew) ship it with [all] and so
    +# `pip install hermes-agent[vision]` / the lazy-install path can resolve it.
    +vision = ["Pillow==12.2.0"]
     # CVE-2026-48710 (BadHost): Starlette is pulled transitively by mcp's
     # sse-starlette / HTTP-SSE stack (and by fastapi in the `web` extra). Before
     # 1.0.1, a malformed Host header makes `request.url.path` desync from the path
    ```
  - `scripts/release.py`

    ```diff
    @@ -46,6 +46,7 @@
     # Auto-extracted from noreply emails + manual overrides
     AUTHOR_MAP = {
         "[redacted:email]": "zhaoleibd",
    +    "[redacted:email]": "kyssta-exe",
         "[redacted:email]": "stremtec",
         "[redacted:email]": "solaitken",
         "[redacted:email]": "vladkvlchk",
    ```
  - `tests/run_agent/test_image_shrink_recovery.py`

    ```diff
    @@ -143,7 +143,7 @@ def test_oversized_image_url_dict_shape_rewritten(self, monkeypatch):
             oversized_url = _big_png_data_url(5000)  # ~5 MB raw → ~6.7 MB b64
             shrunk = "data:image/jpeg;base64," + "A" * 1000  # small
     
    -        def _fake_resize(path, mime_type=None, max_base64_bytes=None):
    +        def _fake_resize(path, mime_type=None, max_base64_bytes=None, max_dimension=None):
                 return shrunk
     
             monkeypatch.setattr(
    ```
  - `tests/tools/test_vision_tools.py`

    ```diff
    @@ -15,6 +15,8 @@
         _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,
    ```

    ```diff
    @@ -889,6 +891,72 @@ def test_no_pillow_returns_original(self, tmp_path):
                     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
     # ---------------------------------------------------------------------------
    ```
  - `tools/lazy_deps.py`

    ```diff
    @@ -175,6 +175,12 @@
             "uvicorn[standard]==0.41.0",
             "starlette==1.0.1",  # CVE-2026-48710 (BadHost) — keep lazy-install in sync with pyproject [web]
         ),
    +    # Vision image-resize recovery (Pillow). Soft dependency: vision_tools and
    +    # conversation_compression degrade gracefully without it, but the byte AND
    +    # pixel-dimension shrink paths no-op when it's absent, so an oversized
    +    # image can brick a session on Anthropic's non-retryable 400. Keep in sync
    +    # with pyproject [vision].
    +    "tool.vision": ("Pillow==12.2.0",),
     }
     
     
    ```
  - `tools/vision_tools.py`

    ```diff
    @@ -326,6 +326,15 @@ def _image_to_base64_data_url(image_path: Path, mime_type: Optional[str] = None)
     # whether we resize proactively or reactively.
     _EMBED_TARGET_BYTES = 4 * 1024 * 1024
     
    +# Proactive embed dimension cap (px, longest side).  Anthropic enforces an
    +# 8000px per-side ceiling INDEPENDENTLY of the 5 MB byte cap — a tall full-page
    +# screenshot can be well under 5 MB yet far over 8000px (e.g. 1200×12000 at
    +# 0.06 MB), so the byte-only embed check above lets it slip into immutable
    +# history un-resized and the session bricks on a non-retryable 400.  We cap at
    +# 7900 (headroom under 8000) so the proactive resize shrinks tall small-byte
    +# images before they are embedded.
    +_EMBED_MAX_DIMENSION = 7900
    +
     # Target size when auto-resizing on API failure (5 MB).  After a provider
     # rejects an image, we downscale to this target and retry once.
     _RESIZE_TARGET_BYTES = 5 * 1024 * 1024
    ```

    ```diff
    @@ -341,21 +350,59 @@ def _is_image_size_error(error: Exception) -> bool:
         ))
     
     
    +def _image_exceeds_dimension(image_path: Path, max_dimension: int) -> bool:
    +    """True if the image's longest side exceeds ``max_dimension`` px.
    +
    +    Anthropic enforces an 8000px per-side cap independently of the 5 MB byte
    +    cap, so a tall small-byte screenshot can pass every byte check yet trip a
    +    non-retryable 400.  Returns False (don't force a resize) when Pillow is
    +    unavailable or the file can't be read as an image — the byte-based checks
    +    still apply, and we never want a missing soft dependency to break the
    +    embed path.
    +    """
    +    try:
    +        from PIL import Image as _PILImage
    +        with _PILImage.open(image_path) as _img:
    +            return max(_img.size) > max_dimension
    +    except Exception:
    +        return False
    +
    +
     def _resize_image_for_vision(image_path: Path, mime_type: Optional[str] = None,
    -                              max_base64_bytes: int = _RESIZE_TARGET_BYTES) -> str:
    +                              max_base64_bytes: int = _RESIZE_TARGET_BYTES,
    +                              max_dimension: Optional[int] = None) -> str:
         """Convert an image to a base64 data URL, auto-resizing if too large.
     
         Tries Pillow first to progressively downscale oversized images.  If Pillow
         is not installed or resizing still exceeds the limit, falls back to the raw
         bytes and lets the caller handle the size check.
     
    +    Args:
    +        max_dimension: If set, images whose longest side exceeds this pixel
    +            count are forcibly downscaled even if they're under the byte
    +            budget.  Anthropic enforces an 8000 px per-side cap independently
    +            of the 5 MB byte cap.
    +
         Returns the base64 data URL string.
         """
         # Quick file-size estimate: base64 expands by ~4/3, plus data URL header.
         # Skip the expensive full-read + encode if Pillow can resize directly.
         file_size = image_path.stat().st_size
         estimated_b64 = (file_size * 4) // 3 + 100  # ~header overhead
    -    if estimated_b64 <= max_base64_bytes:
    +    needs_resize_for_bytes = estimated_b64 > max_base64_bytes
    +
    +    # Check pixel dimensions even if bytes are fine.
    +    needs_resize_for_dims = False
    +    if max_dimension is not None:
    +        try:
    +            from PIL import Image as _PILQuick
    +            with _PILQuick.open(image_path) as _quick_img:
    +                if max(_quick_img.size) > max_dimension:
    +                    needs_resize_for_dims = True
    +        except Exception:
    +            pass  # can't check; Pillow path below will handle or skip
    +
    +    if not needs_resize_for_bytes and not needs_resize_for_dims:
             # Small enough — just encode directly.
             data_url = _image_to_base64_data_url(image_path, mime_type=mime_type)
             if len(data_url) <= max_base64_bytes:
    ```

    ```diff
    @@ -368,14 +415,24 @@ def _resize_image_for_vision(image_path: Path, mime_type: Optional[str] = None,
             from PIL import Image
             import io as _io
         except ImportError:
    -        logger.info("Pillow not installed — cannot auto-resize oversized image")
    -        if data_url is None:
    -            data_url = _image_to_base64_data_url(image_path, mime_type=mime_type)
    -        return data_url  # caller will raise the size error
    +        # Pillow is a lazy-installable soft dependency. Try a best-effort
    +        # install (respects security.allow_lazy_installs; no-op if disabled or
    +        # offline), then re-import. If it still isn't importable, fall back to
    +        # the raw bytes and let the caller raise the size error.
    +        try:
    +            from tools.lazy_deps import ensure as _ensure_dep
    +            _ensure_dep("tool.vision")
    +            from PIL import Image
    +            import io as _io
    +        except Exception:
    +            logger.info("Pillow not installed — cannot auto-resize oversized image")
    +            if data_url is None:
    +                data_url = _image_to_base64_data_url(image_path, mime_type=mime_type)
    +            return data_url  # caller will raise the size error
     
    -    logger.info("Image file is %.1f MB (estimated base64 %.1f MB, limit %.1f MB), auto-resizing...",
    +    logger.info("Image file is %.1f MB (estimated base64 %.1f MB, limit %.1f MB, max_dimension=%s), auto-resizing...",
                     file_size / (1024 * 1024), estimated_b64 / (1024 * 1024),
    -                max_base64_bytes / (1024 * 1024))
    +                max_base64_bytes / (1024 * 1024), max_dimension)
     
         mime = mime_type or _determine_mime_type(image_path)
         # Choose output format: JPEG for photos (smaller), PNG for transparency
    ```

    ```diff
    @@ -393,13 +450,20 @@ def _resize_image_for_vision(image_path: Path, mime_type: Optional[str] = None,
         if pil_format == "JPEG" and img.mode in {"RGBA", "P"}:
             img = img.convert("RGB")
     
    -    # Strategy: halve dimensions until base64 fits, up to 4 rounds.
    +    # Strategy: halve dimensions until both base64 fits AND pixel dimensions
    +    # are within limits, up to 4 rounds.
         # For JPEG, also try reducing quality at each size step.
         # For PNG, quality is irrelevant — only dimension reduction helps.
         quality_steps = (85, 70, 50) if pil_format == "JPEG" else (None,)
         prev_dims = (img.width, img.height)
         candidate = None  # will be set on first loop iteration
     
    +    def _dims_ok(w: int, h: int) -> bool:
    +        """True if both pixel dimensions are within the limit."""
    +        if max_dimension is None:
    +            return True
    +        return max(w, h) <= max_dimension
    +
         for attempt in range(5):
             if attempt > 0:
                 # Proportional scaling: halve the longer side and scale the
    ```

    ```diff
    @@ -430,7 +494,7 @@ def _resize_image_for_vision(image_path: Path, mime_type: Optional[str] = None,
                 img.save(buf, **save_kwargs)
                 encoded = base64.b64encode(buf.getvalue()).decode("ascii")
                 candidate = f"data:{out_mime};base64,{encoded}"
    -            if len(candidate) <= max_base64_bytes:
    +            if len(candidate) <= max_base64_bytes and _dims_ok(img.width, img.height):
                     logger.info("Auto-resized image fits: %.1f MB (quality=%s, %dx%d)",
                                 len(candidate) / (1024 * 1024), q,
                                 img.width, img.height)
    ```

    ```diff
    @@ -669,15 +733,19 @@ async def _vision_analyze_native(
     
             # Proactive embed cap: this image gets baked into conversation
             # history and re-sent on every subsequent turn.  Anthropic rejects
    -        # any single base64 image over 5 MB with a 400, and because history
    -        # is immutable, an oversized embed permanently wedges the session —
    -        # retries can't clear bytes that are already in the request.  Resize
    -        # DOWN to the embed target (4 MB, headroom under 5 MB) whenever the
    -        # payload exceeds it, not just at the 20 MB hard ceiling.
    -        if len(image_data_url) > _EMBED_TARGET_BYTES:
    +        # any single base64 image over 5 MB OR over 8000px per side with a
    +        # 400, and because history is immutable, an oversized embed
    +        # permanently wedges the session — retries can't clear bytes (or
    +        # pixels) that are already in the request.  Resize DOWN to the embed
    +        # target (4 MB / 7900px, headroom under both ceilings) whenever the
    +        # payload exceeds either limit, not just at the 20 MB hard ceiling.
    +        _over_bytes = len(image_data_url) > _EMBED_TARGET_BYTES
    +        _over_dims = _image_exceeds_dimension(temp_image_path, _EMBED_MAX_DIMENSION)
    +        if _over_bytes or _over_dims:
                 image_data_url = _resize_image_for_vision(
                     temp_image_path, mime_type=detected_mime_type,
                     max_base64_bytes=_EMBED_TARGET_BYTES,
    +                max_dimension=_EMBED_MAX_DIMENSION,
                 )
                 # If even resizing can't get under the absolute hard ceiling,
                 # there's nothing more we can do — reject rather than embed a
    ```
  - `uv.lock`

    ```diff
    @@ -1570,6 +1570,9 @@ termux-all = [
     tts-premium = [
         { name = "elevenlabs" },
     ]
    +vision = [
    +    { name = "pillow" },
    +]
     voice = [
         { name = "faster-whisper" },
         { name = "numpy" },
    ```

    ```diff
    @@ -1659,6 +1662,7 @@ requires-dist = [
         { name = "openai", specifier = "==2.24.0" },
         { name = "parallel-web", marker = "extra == 'parallel-web'", specifier = "==0.4.2" },
         { name = "pathspec", specifier = "==1.1.1" },
    +    { name = "pillow", marker = "extra == 'vision'", specifier = "==12.2.0" },
         { name = "prompt-toolkit", specifier = "==3.0.52" },
         { name = "psutil", specifier = "==7.2.2" },
         { name = "ptyprocess", marker = "sys_platform != 'win32'", specifier = ">=0.7.0,<1" },
    ```

    ```diff
    @@ -1697,7 +1701,7 @@ requires-dist = [
         { name = "uvicorn", extras = ["standard"], marker = "extra == 'web'", specifier = "==0.41.0" },
         { name = "youtube-transcript-api", marker = "extra == 'youtube'", specifier = "==1.2.4" },
     ]
    -provides-extras = ["anthropic", "exa", "firecrawl", "parallel-web", "fal", "edge-tts", "modal", "daytona", "hindsight", "dev", "messaging", "cron", "slack", "matrix", "wecom", "cli", "tts-premium", "voice", "pty", "honcho", "mcp", "nemo-relay", "homeassistant", "sms", "computer-use", "acp", "mistral", "bedrock", "azure-identity", "termux", "termux-all", "dingtalk", "feishu", "google", "youtube", "web", "all"]
    +provides-extras = ["anthropic", "exa", "firecrawl", "parallel-web", "fal", "edge-tts", "modal", "daytona", "hindsight", "dev", "messaging", "cron", "slack", "matrix", "wecom", "cli", "tts-premium", "voice", "pty", "honcho", "vision", "mcp", "nemo-relay", "homeassistant", "sms", "computer-use", "acp", "mistral", "bedrock", "azure-identity", "termux", "termux-all", "dingtalk", "feishu", "google", "youtube", "web", "all"]
     
     [[package]]
     name = "hf-xet"
    ```

    ```diff
    @@ -2716,6 +2720,68 @@ wheels = [
         { url = "https://files.pythonhosted.org/packages/f1/d9/7fb5aa316bc299258e68c73ba3bddbc499654a07f151cba08f6153988714/pathspec-1.1.1-py3-none-any.whl", hash = "sha256:a00ce642f577bf7f473932318056212bc4f8bfdf53128c78bbd5af0b9b20b189", size = 57328, upload-time = "2026-04-27T01:46:07.06Z" },
     ]
     
    +[[package]]
    +name = "pillow"
    +version = "12.2.0"
    +source = { registry = "https://pypi.org/simple" }
    +sdist = { url = "https://files.pythonhosted.org/packages/8c/21/c2bcdd5906101a30244eaffc1b6e6ce71a31bd0742a01eb89e660ebfac2d/pillow-12.2.0.tar.gz", hash = "sha256:a830b1a40919539d07806aa58e1b114df53ddd43213d9c8b75847eee6c0182b5", size = 46987819, upload-time = "2026-04-01T14:46:17.687Z" }
    +wheels = [
    +    { url = "https://files.pythonhosted.org/packages/68/e1/748f5663efe6edcfc4e74b2b93edfb9b8b99b67f21a854c3ae416500a2d9/pillow-12.2.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:8be29e59487a79f173507c30ddf57e733a357f67881430449bb32614075a40ab", size = 5354347, upload-time = "2026-04-01T14:42:44.255Z" },
    +    { url = "https://files.pythonhosted.org/packages/47/a1/d5ff69e747374c33a3b53b9f98cca7889fce1fd03d79cdc4e1bccc6c5a87/pillow-12.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:71cde9a1e1551df7d34a25462fc60325e8a11a82cc2e2f54578e5e9a1e153d65", size = 4695873, upload-time = "2026-04-01T14:42:46.452Z" },
    +    { url = "https://files.pythonhosted.org/packages/df/21/e3fbdf54408a973c7f7f89a23b2cb97a7ef30c61ab4142af31eee6aebc88/pillow-12.2.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f490f9368b6fc026f021db16d7ec2fbf7d89e2edb42e8ec09d2c60505f5729c7", size = 6280168, upload-time = "2026-04-01T14:42:49.228Z" },
    +    { url = "https://files.pythonhosted.org/packages/d3/f1/00b7278c7dd52b17ad4329153748f87b6756ec195ff786c2bdf12518337d/pillow-12.2.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8bd7903a5f2a4545f6fd5935c90058b89d30045568985a71c79f5fd6edf9b91e", size = 8088188, upload-time = "2026-04-01T14:42:51.735Z" },
    +    { url = "https://files.pythonhosted.org/packages/ad/cf/220a5994ef1b10e70e85748b75649d77d506499352be135a4989c957b701/pillow-12.2.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3997232e10d2920a68d25191392e3a4487d8183039e1c74c2297f00ed1c50705", size = 6394401, upload-time = "2026-04-01T14:42:54.343Z" },
    +    { url = "https://files.pythonhosted.org/packages/e9/bd/e51a61b1054f09437acfbc2ff9106c30d1eb76bc1453d428399946781253/pillow-12.2.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e74473c875d78b8e9d5da2a70f7099549f9eb37ded4e2f6a463e60125bccd176", size = 7079655, upload-time = "2026-04-01T14:42:56.954Z" },
    +    { url = "https://files.pythonhosted.org/packages/6b/3d/45132c57d5fb4b5744567c3817026480ac7fc3ce5d4c47902bc0e7f6f853/pillow-12.2.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:56a3f9c60a13133a98ecff6197af34d7824de9b7b38c3654861a725c970c197b", size = 6503105, upload-time = "2026-04-01T14:42:59.847Z" },
    +    { url = "https://files.pythonhosted.org/packages/7d/2e/9df2fc1e82097b1df3dce58dc43286aa01068e918c07574711fcc53e6fb4/pillow-12.2.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:90e6f81de50ad6b534cab6e5aef77ff6e37722b2f5d908686f4a5c9eba17a909", size = 7203402, upload-time = "2026-04-01T14:43:02.664Z" },
    +    { url = "https://files.pythonhosted.org/packages/bd/2e/2941e42858ebb67e50ae741473de81c2984e6eff7b397017623c676e2e8d/pillow-12.2.0-cp311-cp311-win32.whl", hash = "sha256:8c984051042858021a54926eb597d6ee3012393ce9c181814115df4c60b9a808", size = 6378149, upload-time = "2026-04-01T14:43:05.274Z" },
    +    { url = "https://files.pythonhosted.org/packages/69/42/836b6f3cd7f3e5fa10a1f1a5420447c17966044c8fbf589cc0452d5502db/pillow-12.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:6e6b2a0c538fc200b38ff9eb6628228b77908c319a005815f2dde585a0664b60", size = 7082626, upload-time = "2026-04-01T14:43:08.557Z" },
    +    { url = "https://files.pythonhosted.org/packages/c2/88/549194b5d6f1f494b485e493edc6693c0a16f4ada488e5bd974ed1f42fad/pillow-12.2.0-cp311-cp311-win_arm64.whl", hash = "sha256:9a8a34cc89c67a65ea7437ce257cea81a9dad65b29805f3ecee8c8fe8ff25ffe", size = 2463531, upload-time = "2026-04-01T14:43:10.743Z" },
    +    { url = "https://files.pythonhosted.org/packages/58/be/7482c8a5ebebbc6470b3eb791812fff7d5e0216c2be3827b30b8bb6603ed/pillow-12.2.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2d192a155bbcec180f8564f693e6fd9bccff5a7af9b32e2e4bf8c9c69dbad6b5", size = 5308279, upload-time = "2026-04-01T14:43:13.246Z" },
    +    { url = "https://files.pythonhosted.org/packages/d8/95/0a351b9289c2b5cbde0bacd4a83ebc44023e835490a727b2a3bd60ddc0f4/pillow-12.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f3f40b3c5a968281fd507d519e444c35f0ff171237f4fdde090dd60699458421", size = 4695490, upload-time = "2026-04-01T14:43:15.584Z" },
    +    { url = "https://files.pythonhosted.org/packages/de/af/4e8e6869cbed569d43c416fad3dc4ecb944cb5d9492defaed89ddd6fe871/pillow-12.2.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:03e7e372d5240cc23e9f07deca4d775c0817bffc641b01e9c3af208dbd300987", size = 6284462, upload-time = "2026-04-01T14:43:18.268Z" },
    +    { url = "https://files.pythonhosted.org/packages/e9/9e/c05e19657fd57841e476be1ab46c4d501bffbadbafdc31a6d665f8b737b6/pillow-12.2.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b86024e52a1b269467a802258c25521e6d742349d760728092e1bc2d135b4d76", size = 8094744, upload-time = "2026-04-01T14:43:20.716Z" },
    +    { url = "https://files.pythonhosted.org/packages/2b/54/1789c455ed10176066b6e7e6da1b01e50e36f94ba584dc68d9eebfe9156d/pillow-12.2.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7371b48c4fa448d20d2714c9a1f775a81155050d383333e0a6c15b1123dda005", size = 6398371, upload-time = "2026-04-01T14:43:23.443Z" },
    +    { url = "https://files.pythonhosted.org/packages/43/e3/fdc657359e919462369869f1c9f0e973f353f9a9ee295a39b1fea8ee1a77/pillow-12.2.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:62f5409336adb0663b7caa0da5c7d9e7bdbaae9ce761d34669420c2a801b2780", size = 7087215, upload-time = "2026-04-01T14:43:26.758Z" },
    +    { url = "https://files.pythonhosted.org/packages/8b/f8/2f6825e441d5b1959d2ca5adec984210f1ec086435b0ed5f52c19b3b8a6e/pillow-12.2.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:01afa7cf67f74f09523699b4e88c73fb55c13346d212a59a2db1f86b0a63e8c5", size = 6509783, upload-time = "2026-04-01T14:43:29.56Z" },
    +    { url = "https://files.pythonhosted.org/packages/67/f9/029a27095ad20f854f9dba026b3ea6428548316e057e6fc3545409e86651/pillow-12.2.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fc3d34d4a8fbec3e88a79b92e5465e0f9b842b628675850d860b8bd300b159f5", size = 7212112, upload-time = "2026-04-01T14:43:32.091Z" },
    +    { url = "https://files.pythonhosted.org/packages/be/42/025cfe05d1be22dbfdb4f264fe9de1ccda83f66e4fc3aac94748e784af04/pillow-12.2.0-cp312-cp312-win32.whl", hash = "sha256:58f62cc0f00fd29e64b29f4fd923ffdb3859c9f9e6105bfc37ba1d08994e8940", size = 6378489, upload-time = "2026-04-01T14:43:34.601Z" },
    +    { url = "https://files.pythonhosted.org/packages/5d/7b/25a221d2c761c6a8ae21bfa3874988ff2583e19cf8a27bf2fee358df7942/pillow-12.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:7f84204dee22a783350679a0333981df803dac21a0190d706a50475e361c93f5", size = 7084129, upload-time = "2026-04-01T14:43:37.213Z" },
    +    { url = "https://files.pythonhosted.org/packages/10/e1/542a474affab20fd4a0f1836cb234e8493519da6b76899e30bcc5d990b8b/pillow-12.2.0-cp312-cp312-win_arm64.whl", hash = "sha256:af73337013e0b3b46f175e79492d96845b16126ddf79c438d7ea7ff27783a414", size = 2463612, upload-time = "2026-04-01T14:43:39.421Z" },
    +    { url = "https://files.pythonhosted.org/packages/4a/01/53d10cf0dbad820a8db274d259a37ba50b88b24768ddccec07355382d5ad/pillow-12.2.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:8297651f5b5679c19968abefd6bb84d95fe30ef712eb1b2d9b2d31ca61267f4c", size = 4100837, upload-time = "2026-04-01T14:43:41.506Z" },
    +    { url = "https://files.pythonhosted.org/packages/0f/98/f3a6657ecb698c937f6c76ee564882945f29b79bad496abcba0e84659ec5/pillow-12.2.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:50d8520da2a6ce0af445fa6d648c4273c3eeefbc32d7ce049f22e8b5c3daecc2", size = 4176528, upload-time = "2026-04-01T14:43:43.773Z" },
    +    { url = "https://files.pythonhosted.org/packages/69/bc/8986948f05e3ea490b8442ea1c1d4d990b24a7e43d8a51b2c7d8b1dced36/pillow-12.2.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:766cef22385fa1091258ad7e6216792b156dc16d8d3fa607e7545b2b72061f1c", size = 3640401, upload-time = "2026-04-01T14:43:45.87Z" },
    +    { url = "https://files.pythonhosted.org/packages/34/46/6c717baadcd62bc8ed51d238d521ab651eaa74838291bda1f86fe1f864c9/pillow-12.2.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5d2fd0fa6b5d9d1de415060363433f28da8b1526c1c129020435e186794b3795", size = 5308094, upload-time = "2026-04-01T14:43:48.438Z" },
    +    { url = "https://files.pythonhosted.org/packages/71/43/905a14a8b17fdb1ccb58d282454490662d2cb89a6bfec26af6d3520da5ec/pillow-12.2.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:56b25336f502b6ed02e889f4ece894a72612fe885889a6e8c4c80239ff6e5f5f", size = 4695402, upload-time = "2026-04-01T14:43:51.292Z" },
    +    { url = "https://files.pythonhosted.org/packages/73/dd/42107efcb777b16fa0393317eac58f5b5cf30e8392e266e76e51cff28c3d/pillow-12.2.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f1c943e96e85df3d3478f7b691f229887e143f81fedab9b20205349ab04d73ed", size = 6280005, upload-time = "2026-04-01T14:43:54.242Z" },
    +    { url = "https://files.pythonhosted.org/packages/a8/68/b93e09e5e8549019e61acf49f65b1a8530765a7f812c77a7461bca7e4494/pillow-12.2.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:03f6fab9219220f041c74aeaa2939ff0062bd5c364ba9ce037197f4c6d498cd9", size = 8090669, upload-time = "2026-04-01T14:43:57.335Z" },
    +    { url = "https://files.pythonhosted.org/packages/4b/6e/3ccb54ce8ec4ddd1accd2d89004308b7b0b21c4ac3d20fa70af4760a4330/pillow-12.2.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5cdfebd752ec52bf5bb4e35d9c64b40826bc5b40a13df7c3cda20a2c03a0f5ed", size = 6395194, upload-time = "2026-04-01T14:43:59.864Z" },
    +    { url = "https://files.pythonhosted.org/packages/67/ee/21d4e8536afd1a328f01b359b4d3997b291ffd35a237c877b331c1c3b71c/pillow-12.2.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eedf4b74eda2b5a4b2b2fb4c006d6295df3bf29e459e198c90ea48e130dc75c3", size = 7082423, upload-time = "2026-04-01T14:44:02.74Z" },
    +    { url = "https://files.pythonhosted.org/packages/78/5f/e9f86ab0146464e8c133fe85df987ed9e77e08b29d8d35f9f9f4d6f917ba/pillow-12.2.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:00a2865911330191c0b818c59103b58a5e697cae67042366970a6b6f1b20b7f9", size = 6505667, upload-time = "2026-04-01T14:44:05.381Z" },
    +    { url = "https://files.pythonhosted.org/packages/ed/1e/409007f56a2fdce61584fd3acbc2bbc259857d555196cedcadc68c015c82/pillow-12.2.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1e1757442ed87f4912397c6d35a0db6a7b52592156014706f17658ff58bbf795", size = 7208580, upload-time = "2026-04-01T14:44:08.39Z" },
    +    { url = "https://files.pythonhosted.org/packages/23/c4/7349421080b12fb35414607b8871e9534546c128a11965fd4a7002ccfbee/pillow-12.2.0-cp313-cp313-win32.whl", hash = "sha256:144748b3af2d1b358d41286056d0003f47cb339b8c43a9ea42f5fea4d8c66b6e", size = 6375896, upload-time = "2026-04-01T14:44:11.197Z" },
    +    { url = "https://files.pythonhosted.org/packages/3f/82/8a3739a5e470b3c6cbb1d21d315800d8e16bff503d1f16b03a4ec3212786/pillow-12.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:390ede346628ccc626e5730107cde16c42d3836b89662a115a921f28440e6a3b", size = 7081266, upload-time = "2026-04-01T14:44:13.947Z" },
    +    { url = "https://files.pythonhosted.org/packages/c3/25/f968f618a062574294592f668218f8af564830ccebdd1fa6200f598e65c5/pillow-12.2.0-cp313-cp313-win_arm64.whl", hash = "sha256:8023abc91fba39036dbce14a7d6535632f99c0b857807cbbbf21ecc9f4717f06", size = 2463508, upload-time = "2026-04-01T14:44:16.312Z" },
    +    { url = "https://files.pythonhosted.org/packages/4d/a4/b342930964e3cb4dce5038ae34b0eab4653334995336cd486c5a8c25a00c/pillow-12.2.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:042db20a421b9bafecc4b84a8b6e444686bd9d836c7fd24542db3e7df7baad9b", size = 5309927, upload-time = "2026-04-01T14:44:18.89Z" },
    +    { url = "https://files.pythonhosted.org/packages/9f/de/23198e0a65a9cf06123f5435a5d95cea62a635697f8f03d134d3f3a96151/pillow-12.2.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:dd025009355c926a84a612fecf58bb315a3f6814b17ead51a8e48d3823d9087f", size = 4698624, upload-time = "2026-04-01T14:44:21.115Z" },
    +    { url = "https://files.pythonhosted.org/packages/01/a6/1265e977f17d93ea37aa28aa81bad4fa597933879fac2520d24e021c8da3/pillow-12.2.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:88ddbc66737e277852913bd1e07c150cc7bb124539f94c4e2df5344494e0a612", size = 6321252, upload-time = "2026-04-01T14:44:23.663Z" },
    +    { url = "https://files.pythonhosted.org/packages/3c/83/5982eb4a285967baa70340320be9f88e57665a387e3a53a7f0db8231a0cd/pillow-12.2.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d362d1878f00c142b7e1a16e6e5e780f02be8195123f164edf7eddd911eefe7c", size = 8126550, upload-time = "2026-04-01T14:44:26.772Z" },
    +    { url = "https://files.pythonhosted.org/packages/4e/48/6ffc514adce69f6050d0753b1a18fd920fce8cac87620d5a31231b04bfc5/pillow-12.2.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2c727a6d53cb0018aadd8018c2b938376af27914a68a492f59dfcaca650d5eea", size = 6433114, upload-time = "2026-04-01T14:44:29.615Z" },
    +    { url = "https://files.pythonhosted.org/packages/36/a3/f9a77144231fb8d40ee27107b4463e205fa4677e2ca2548e14da5cf18dce/pillow-12.2.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:efd8c21c98c5cc60653bcb311bef2ce0401642b7ce9d09e03a7da87c878289d4", size = 7115667, upload-time = "2026-04-01T14:44:32.773Z" },
    +    { url = "https://files.pythonhosted.org/packages/c1/fc/ac4ee3041e7d5a565e1c4fd72a113f03b6394cc72ab7089d27608f8aaccb/pillow-12.2.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9f08483a632889536b8139663db60f6724bfcb443c96f1b18855860d7d5c0fd4", size = 6538966, upload-time = "2026-04-01T14:44:35.252Z" },
    +    { url = "https://files.pythonhosted.org/packages/c0/a8/27fb307055087f3668f6d0a8ccb636e7431d56ed0750e07a60547b1e083e/pillow-12.2.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dac8d77255a37e81a2efcbd1fc05f1c15ee82200e6c240d7e127e25e365c39ea", size = 7238241, upload-time = "2026-04-01T14:44:37.875Z" },
    +    { url = "https://files.pythonhosted.org/packages/ad/4b/926ab182c07fccae9fcb120043464e1ff1564775ec8864f21a0ebce6ac25/pillow-12.2.0-cp313-cp313t-win32.whl", hash = "sha256:ee3120ae9dff32f121610bb08e4313be87e03efeadfc6c0d18f89127e24d0c24", size = 6379592, upload-time = "2026-04-01T14:44:40.336Z" },
    +    { url = "https://files.pythonhosted.org/packages/c2/c4/f9e476451a098181b30050cc4c9a3556b64c02cf6497ea421ac047e89e4b/pillow-12.2.0-cp313-cp313t-win_amd64.whl", hash = "sha256:325ca0528c6788d2a6c3d40e3568639398137346c3d6e66bb61db96b96511c98", size = 7085542, upload-time = "2026-04-01T14:44:43.251Z" },
    +    { url = "https://files.pythonhosted.org/packages/00/a4/285f12aeacbe2d6dc36c407dfbbe9e96d4a80b0fb710a337f6d2ad978c75/pillow-12.2.0-cp313-cp313t-win_arm64.whl", hash = "sha256:2e5a76d03a6c6dcef67edabda7a52494afa4035021a79c8558e14af25313d453", size = 2465765, upload-time = "2026-04-01T14:44:45.996Z" },
    +    { url = "https://files.pythonhosted.org/packages/4e/b7/2437044fb910f499610356d1352e3423753c98e34f915252aafecc64889f/pillow-12.2.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0538bd5e05efec03ae613fd89c4ce0368ecd2ba239cc25b9f9be7ed426b0af1f", size = 5273969, upload-time = "2026-04-01T14:45:55.538Z" },
    +    { url = "https://files.pythonhosted.org/packages/f6/f4/8316e31de11b780f4ac08ef3654a75555e624a98db1056ecb2122d008d5a/pillow-12.2.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:394167b21da716608eac917c60aa9b969421b5dcbbe02ae7f013e7b85811c69d", size = 4659674, upload-time = "2026-04-01T14:45:58.093Z" },
    +    { url = "https://files.pythonhosted.org/packages/d4/37/664fca7201f8bb2aa1d20e2c3d5564a62e6ae5111741966c8319ca802361/pillow-12.2.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5d04bfa02cc2d23b497d1e90a0f927070043f6cbf303e738300532379a4b4e0f", size = 5288479, upload-time = "2026-04-01T14:46:01.141Z" },
    +    { url = "https://files.pythonhosted.org/packages/49/62/5b0ed78fce87346be7a5cfcfaaad91f6a1f98c26f86bdbafa2066c647ef6/pillow-12.2.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0c838a5125cee37e68edec915651521191cef1e6aa336b855f495766e77a366e", size = 7032230, upload-time = "2026-04-01T14:46:03.874Z" },
    +    { url = "https://files.pythonhosted.org/packages/c3/28/ec0fc38107fc32536908034e990c47914c57cd7c5a3ece4d8d8f7ffd7e27/pillow-12.2.0-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4a6c9fa44005fa37a91ebfc95d081e8079757d2e904b27103f4f5fa6f0bf78c0", size = 5355404, upload-time = "2026-04-01T14:46:06.33Z" },
    +    { url = "https://files.pythonhosted.org/packages/5e/8b/51b0eddcfa2180d60e41f06bd6d0a62202b20b59c68f5a132e615b75aecf/pillow-12.2.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:25373b66e0dd5905ed63fa3cae13c82fbddf3079f2c8bf15c6fb6a35586324c1", size = 6002215, upload-time = "2026-04-01T14:46:08.83Z" },
    +    { url = "https://files.pythonhosted.org/packages/bc/60/5382c03e1970de634027cee8e1b7d39776b778b81812aaf45b694dfe9e28/pillow-12.2.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:bfa9c230d2fe991bed5318a5f119bd6780cda2915cca595393649fc118ab895e", size = 7080946, upload-time = "2026-04-01T14:46:11.734Z" },
    +]
    +
     [[package]]
     name = "pluggy"
     version = "1.6.0"
    ```
- **#19647: fix(browser): add full_page parameter to browser_vision for viewport-only screenshots**. open
  - maintainer direction: category `uncategorized`; implication `needs_llm`; refs `#19620`, `#9775`
  - files: `gateway/platforms/whatsapp.py`, `tests/tools/test_browser_console.py`, `tests/tools/test_file_tools.py`, `tools/browser_tool.py`, `tools/file_tools.py`
- **#20118: fix(tools): make browser_vision full-page capture opt-in via flag**. closed without merge
  - maintainer direction: category `volume_quota`; implication `active`
- **#21833: feat(browser): add \`full\` parameter to \`browser_vision\` tool and default to false**. open
  - maintainer direction: category `superseded_by_pr`; implication `superseded`; refs `#19647`, `#19765`, `#19620`, `#19647`, `#20118`
  - files: `tools/browser_tool.py`
- **#25838: fix(vision): clamp image dimensions before inline base64 encode**. open
  - maintainer direction: category `uncategorized`; implication `needs_llm`
  - files: `agent/conversation_compression.py`, `tests/run_agent/test_image_shrink_recovery.py`, `tests/tools/test_vision_tools.py`, `tools/browser_tool.py`, `tools/vision_tools.py`
- **#26001: fix: cap vision image pixel dimensions at 8000px to prevent Anthropic 400 errors**. closed without merge
  - maintainer direction: category `superseded_by_pr`; implication `superseded`; refs `#25838`, `#25838`
- **#37727: fix(vision): guard image pixel dimensions, not just bytes #37677**. closed without merge
  - maintainer direction: category `merged_via`; implication `superseded`; refs `6bdbe3076`, `#39033`, `#37677`
- **#38739: fix(agent): enforce pixel-dimension cap for images exceeding Anthropic 8000px limit (#37677)**. closed without merge
  - maintainer direction: category `uncategorized`; implication `needs_llm`; refs `6bdbe3076`
- **#39226: fix(browser): apply vision embed-time pixel/byte caps in browser_vision**. open
  - maintainer direction: category `uncategorized`; implication `needs_llm`
  - files: `tests/tools/test_browser_vision_embed_cap.py`, `tools/browser_tool.py`
- **#47468: fix(vision): apply proactive embed cap to browser_vision screenshots**. open
  - maintainer direction: category `duplicate_of`; implication `duplicate`; refs `#39226`, `#39226`, `#44922`, `#25838`, `#47467`
  - files: `tests/tools/test_browser_console.py`, `tools/browser_tool.py`
- **#47551: fix(browser): proactively resize oversized screenshots before vision API (#47467)**. closed without merge
  - maintainer direction: category `duplicate_of`; implication `duplicate`; refs `#47468`, `#47467`, `#47468`, `#47468`
- **#62005: fix(agent): persist image-shrink recovery**. open
  - maintainer direction: category `implemented_elsewhere`; implication `superseded`; refs `63e5b4d56`
  - files: `agent/conversation_compression.py`, `agent/conversation_loop.py`, `run_agent.py`, `tests/run_agent/test_image_shrink_recovery.py`
- **#62051: fix(agent): validate and downscale oversized images at ingestion to prevent infinite shrink-recovery loops (#61994)**. open
  - maintainer direction: category `wontfix_bydesign`; implication `wontfix`; refs `#61994`, `#62005`, `#61994`, `#62005`, `#48035`, `#49140`
  - files: `agent/image_routing.py`

## Fix status

- Merged to main in PR #39033 (cited above). No release data is tracked. Verify downstream.
- Fix last verified (merged) `2026-06-04`.
- Subsystem: `agent`
