Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
name: CI

on:
pull_request:
branches: [main]
push:
branches: [main]

jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.13"
- name: Install test subjects
run: |
python -m pip install --upgrade pip
python -m pip install -e .
python -m pip install 'pytest>=8.0'
python -m pip install -e packages/mcp2imgl --no-deps
- name: Run tests
run: python -m pytest tests -q
2 changes: 1 addition & 1 deletion imgl/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -331,7 +331,7 @@ def build_parser() -> argparse.ArgumentParser:
interact_parser.add_argument(
"--llm",
action="store_true",
help="Use vision LLM catalog (requires OPENROUTER_API_KEY, pip install litellm)",
help="Use vision LLM catalog (requires OPENROUTER_API_KEY, pip install -e '.[llm]')",
)
interact_parser.add_argument(
"--no-filter",
Expand Down
32 changes: 19 additions & 13 deletions imgl/llm_catalog.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import json
import os
import re
import sys
from io import BytesIO
from pathlib import Path
from typing import Any
Expand Down Expand Up @@ -86,13 +87,21 @@ def llm_available() -> bool:


def llm_dependencies_ok() -> tuple[bool, str | None]:
if sys.version_info < (3, 11):
return False, "SubLLM vision requires Python 3.11+"
try:
import litellm # type: ignore # noqa: F401
_subllm_complete()
except ImportError:
return False, "litellm not installed (pip install -e '.[llm]')"
return False, "subactor-subllm not installed (pip install -e '.[llm]')"
return True, None


def _subllm_complete():
from subllm import complete as subllm_complete

return subllm_complete


def refine_catalog_with_llm(
scene: Scene,
*,
Expand Down Expand Up @@ -219,12 +228,7 @@ def _call_vision_llm(
crop_bbox: BBox | None = None,
window_title: str | None = None,
) -> dict[str, Any]:
os.environ.setdefault("LITELLM_LOG", "ERROR")
import litellm # type: ignore

litellm.set_verbose = False
if hasattr(litellm, "suppress_debug_info"):
litellm.suppress_debug_info = True
subllm_complete = _subllm_complete()
image_b64 = _image_to_base64(image_path, crop_bbox=crop_bbox)
scope_hint = (
f"This image shows one application window{f' ({window_title})' if window_title else ''}. "
Expand All @@ -251,13 +255,15 @@ def _call_vision_llm(
],
},
]
response = litellm.completion(
model=model,
messages=messages,
temperature=0.1,
api_key = os.getenv("OPENROUTER_API_KEY", "").strip()
response = subllm_complete(
"autogrammar-imgl",
"vision",
messages,
response_format={"type": "json_object"},
credentials={"openrouter": api_key} if api_key else None,
)
content = (response.choices[0].message.content or "").strip()
content = (response.content or "").strip()
return _parse_json_payload(content)


Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,8 @@ diagnose = [
"numpy>=1.24",
]
llm = [
"litellm>=1.30",
"python-dotenv>=1.0",
"subactor-subllm @ git+https://github.com/subactor/subllm.git@067732fcd2fab3b36453160bb4497012b13bc298 ; python_version >= '3.11'",
]
web = [
"fastapi>=0.110",
Expand Down
23 changes: 23 additions & 0 deletions tests/test_llm_catalog.py
Original file line number Diff line number Diff line change
Expand Up @@ -124,3 +124,26 @@ def test_merge_heuristic_inputs_appends_missing_fields():
for opt in inputs
for term in ("Chat", "Terminal", "Pole", "Editor", "Input")
)


def test_call_vision_llm_uses_central_subllm(tmp_path: Path, monkeypatch):
from imgl import llm_catalog

captured: dict = {}
from PIL import Image

image = tmp_path / "shot.png"
Image.new("RGB", (8, 8), color=(255, 255, 255)).save(image)

def fake_complete(application, function, messages, **kwargs):
captured.update(application=application, function=function, messages=messages, kwargs=kwargs)
return type("Response", (), {"content": '{"elements":[]}'})()

monkeypatch.setenv("OPENROUTER_API_KEY", "sk-test")
monkeypatch.setattr(llm_catalog, "_subllm_complete", lambda: fake_complete)
payload = llm_catalog._call_vision_llm(str(image), model="ignored", max_elements=5)
assert payload == {"elements": []}
assert captured["application"] == "autogrammar-imgl"
assert captured["function"] == "vision"
assert captured["messages"][1]["content"][0]["type"] == "image_url"
assert captured["kwargs"]["credentials"] == {"openrouter": "sk-test"}
Loading