Files
aiturk-hermes-ide/tests/hermes_cli/test_managed_vision_capability.py

186 lines
7.2 KiB
Python

"""Vision capability for managed local models answers from ground truth.
Cloud capability catalogs have never heard of a local GGUF, so without a
managed-runtime answer every local model reads as text-only: pasted images
detour to a cloud auxiliary (a screenshot leaving a local-first machine)
or fail outright. The lookup chain must consult the managed runtime
between the user's config override and the cloud catalog."""
from __future__ import annotations
import pytest
@pytest.fixture
def hermes_home(tmp_path, monkeypatch):
home = tmp_path / ".hermes"
home.mkdir()
monkeypatch.setenv("HERMES_HOME", str(home))
import importlib
import hermes_constants
importlib.reload(hermes_constants)
yield home
importlib.reload(hermes_constants)
def _stage(home_root, name):
# Machine-scoped models dir (the shared root — tmp HERMES_HOME IS the root here).
from hermes_cli.local_runtime.bootstrap import models_dir
mdir = models_dir()
mdir.mkdir(parents=True, exist_ok=True)
(mdir / f"{name}.gguf").write_bytes(b"GGUF" + b"\x00" * 32)
def test_not_ours_returns_none(hermes_home):
from hermes_cli.local_runtime.capabilities import managed_model_supports_vision
assert managed_model_supports_vision("gpt-4o") is None
def test_catalog_vision_model_with_projector_on_disk(hermes_home):
"""Staged catalog model with an mmproj present: True (server down —
the catalog + on-disk projector answer)."""
from hermes_cli.local_runtime.bootstrap import assets_dir
from hermes_cli.local_runtime.capabilities import managed_model_supports_vision
from hermes_cli.local_runtime.catalog import CATALOG
entry = next(e for e in CATALOG if e.mmproj is not None)
variant = entry.variants[-1]
_stage(hermes_home, variant.model_id)
adir = assets_dir()
adir.mkdir(parents=True, exist_ok=True)
(adir / entry.mmproj.local_name).write_bytes(b"GGUF mmproj")
assert managed_model_supports_vision(variant.model_id) is True
def test_catalog_vision_model_missing_projector_is_blind(hermes_home):
"""Same model, projector NOT on disk: False — it genuinely cannot see,
and claiming otherwise sends an image to a model that errors on it."""
from hermes_cli.local_runtime.capabilities import managed_model_supports_vision
from hermes_cli.local_runtime.catalog import CATALOG
entry = next(e for e in CATALOG if e.mmproj is not None)
variant = entry.variants[-1]
_stage(hermes_home, variant.model_id)
assert managed_model_supports_vision(variant.model_id) is False
def test_live_props_beats_catalog(hermes_home, monkeypatch):
"""A running child's modalities report wins over the catalog: the
server that will receive the image is the authority."""
import hermes_cli.local_runtime.capabilities as caps
from hermes_cli.local_runtime.catalog import CATALOG
entry = next(e for e in CATALOG if e.mmproj is not None)
variant = entry.variants[-1]
_stage(hermes_home, variant.model_id)
# Catalog would say False (no projector staged) — live props says True.
monkeypatch.setattr(caps, "_props_modalities", lambda mid: True)
assert caps.managed_model_supports_vision(variant.model_id) is True
def test_lookup_chain_consults_managed_runtime(hermes_home, monkeypatch):
"""_lookup_supports_vision: user override wins, then the managed
answer, and the cloud catalog is never reached for a managed model."""
import agent.image_routing as ir
monkeypatch.setattr(
"hermes_cli.local_runtime.capabilities.managed_model_supports_vision",
lambda mid: True)
def catalog_must_not_run(*a, **k):
raise AssertionError("cloud catalog consulted for a managed model")
monkeypatch.setattr("agent.models_dev.get_model_capabilities",
catalog_must_not_run)
got = ir._lookup_supports_vision("llamacpp", "Some-Local-Model", {})
assert got is True
# Explicit user override still outranks the managed answer.
cfg = {"model": {"provider": "llamacpp", "name": "Some-Local-Model",
"supports_vision": False}}
got = ir._lookup_supports_vision("llamacpp", "Some-Local-Model", cfg)
assert got is False
def test_webp_transcodes_to_png_for_managed_provider(hermes_home, monkeypatch, tmp_path):
"""A .webp attachment bound for the managed server must arrive as PNG:
llama.cpp's stb_image decoder has no WebP support and drops the part
SILENTLY — the model confabulates a description of an image it never
saw. Measured live: the same red square answered 'Red' as PNG and
'Unseen' as WebP."""
pytest.importorskip("PIL")
import io
from PIL import Image
import agent.image_routing as ir
webp_path = tmp_path / "shot.webp"
img = Image.new("RGB", (32, 32), (255, 0, 0))
img.save(webp_path, format="WEBP")
monkeypatch.setattr("agent.auxiliary_client._runtime_main_value",
lambda k: {"provider": "llamacpp",
"base_url": ""}.get(k, ""))
data_url = ir._file_to_data_url(webp_path)
assert data_url is not None
assert data_url.startswith("data:image/png;base64,"), (
"webp must transcode to png for the managed server")
def test_webp_passes_through_for_cloud_providers(hermes_home, monkeypatch, tmp_path):
"""Cloud providers accept WebP natively — no transcode tax for them."""
pytest.importorskip("PIL")
from PIL import Image
import agent.image_routing as ir
webp_path = tmp_path / "shot.webp"
Image.new("RGB", (32, 32), (255, 0, 0)).save(webp_path, format="WEBP")
monkeypatch.setattr("agent.auxiliary_client._runtime_main_value",
lambda k: {"provider": "anthropic",
"base_url": ""}.get(k, ""))
data_url = ir._file_to_data_url(webp_path)
assert data_url is not None
assert data_url.startswith("data:image/webp;base64,")
def test_vision_analyze_normalization_narrows_for_managed(hermes_home, monkeypatch, tmp_path):
"""vision_analyze's native fast path embeds the image into conversation
history via _normalize_to_supported_image — for the managed server a
WebP must convert to PNG THERE too, or the tool path re-introduces the
silent-drop confabulation the attachment path just fixed."""
pytest.importorskip("PIL")
from PIL import Image
import tools.vision_tools as vt
webp_path = tmp_path / "img.webp"
Image.new("RGB", (32, 32), (255, 0, 0)).save(webp_path, format="WEBP")
monkeypatch.setattr("agent.auxiliary_client._runtime_main_value",
lambda k: {"provider": "llamacpp",
"base_url": ""}.get(k, ""))
out_path, mime, err = vt._normalize_to_supported_image(webp_path, "image/webp")
assert err is None
assert mime == "image/png", "managed server: webp must normalize to png"
# Cloud providers keep webp untouched.
monkeypatch.setattr("agent.auxiliary_client._runtime_main_value",
lambda k: {"provider": "anthropic",
"base_url": ""}.get(k, ""))
out_path, mime, err = vt._normalize_to_supported_image(webp_path, "image/webp")
assert err is None
assert mime == "image/webp"