Skip to content

Commit

Permalink
Add Image Support - thanks, @montasaurus
Browse files Browse the repository at this point in the history
* add image support
* bump python and llm versions
* reset user directory on each test
* add recordings to readme
* Ensure compatible OpenAI library

---------

Co-authored-by: Simon Willison <swillison@gmail.com>
  • Loading branch information
montasaurus and simonw authored Dec 8, 2024
1 parent b01ab71 commit 8fbcfec
Show file tree
Hide file tree
Showing 10 changed files with 1,780 additions and 46 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"]
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"]
steps:
- uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
Expand All @@ -38,7 +38,7 @@ jobs:
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.12"
python-version: "3.13"
cache: pip
cache-dependency-path: pyproject.toml
- name: Install dependencies
Expand Down
3 changes: 1 addition & 2 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"]
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"]
steps:
- uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
Expand All @@ -25,4 +25,3 @@ jobs:
- name: Run tests
run: |
pytest
13 changes: 12 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,13 @@ Now you can prompt Claude using:
```bash
cat llm_openrouter.py | llm -m claude -s 'write some pytest tests for this'
```

Images are supported too, for some models:
```bash
llm -m openrouter/anthropic/claude-3.5-sonnet 'describe this image' -a https://static.simonwillison.net/static/2024/pelicans.jpg
llm -m openrouter/anthropic/claude-3-haiku 'extract text' -a page.png
```

## Development

To set up this plugin locally, first checkout the code. Then create a new virtual environment:
Expand All @@ -64,9 +71,13 @@ source venv/bin/activate
```
Now install the dependencies and test dependencies:
```bash
pip install -e '.[test]'
llm install -e '.[test]'
```
To run the tests:
```bash
pytest
```
To add new recordings and snapshots, run:
```bash
pytest --record-mode=once --inline-snapshot=create
```
13 changes: 13 additions & 0 deletions llm_openrouter.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,12 @@ def register_models(register):
if not key:
return
for model_definition in get_openrouter_models():
supports_images = get_supports_images(model_definition)
register(
OpenRouterChat(
model_id="openrouter/{}".format(model_definition["id"]),
model_name=model_definition["id"],
vision=supports_images,
api_base="https://openrouter.ai/api/v1",
headers={"HTTP-Referer": "https://llm.datasette.io/", "X-Title": "LLM"},
)
Expand Down Expand Up @@ -78,3 +80,14 @@ def fetch_cached_json(url, path, cache_timeout):
raise DownloadError(
f"Failed to download data and no cache is available at {path}"
)


def get_supports_images(model_definition):
try:
# e.g. `text->text` or `text+image->text`
modality = model_definition["architecture"]["modality"]

input_modalities = modality.split("->")[0].split("+")
return "image" in input_modalities
except Exception:
return False
7 changes: 4 additions & 3 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,9 @@ classifiers = [
"License :: OSI Approved :: Apache Software License"
]
dependencies = [
"llm>=0.8",
"httpx"
"llm>=0.17",
"httpx",
"openai>=1.57.0",
]

[project.urls]
Expand All @@ -23,4 +24,4 @@ CI = "https://github.com/simonw/llm-openrouter/actions"
openrouter = "llm_openrouter"

[project.optional-dependencies]
test = ["pytest", "pytest-httpx"]
test = ["pytest", "pytest-recording", "inline-snapshot"]
608 changes: 608 additions & 0 deletions tests/cassettes/test_llm_openrouter/test_image_prompt.yaml

Large diffs are not rendered by default.

483 changes: 483 additions & 0 deletions tests/cassettes/test_llm_openrouter/test_llm_models.yaml

Large diffs are not rendered by default.

591 changes: 591 additions & 0 deletions tests/cassettes/test_llm_openrouter/test_prompt.yaml

Large diffs are not rendered by default.

35 changes: 7 additions & 28 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -1,30 +1,5 @@
import json
import llm
import pytest

DUMMY_MODELS = {
"data": [
{
"id": "openai/gpt-3.5-turbo",
"pricing": {"prompt": "0.0000015", "completion": "0.000002"},
"context_length": 4095,
"per_request_limits": {
"prompt_tokens": "2871318",
"completion_tokens": "2153488",
},
},
{
"id": "anthropic/claude-2",
"pricing": {"prompt": "0.00001102", "completion": "0.00003268"},
"context_length": 100000,
"per_request_limits": {
"prompt_tokens": "390832",
"completion_tokens": "131792",
},
},
]
}


@pytest.fixture
def user_path(tmpdir):
Expand All @@ -36,7 +11,11 @@ def user_path(tmpdir):
@pytest.fixture(autouse=True)
def env_setup(monkeypatch, user_path):
monkeypatch.setenv("LLM_USER_PATH", str(user_path))
# Write out the models.json file
(llm.user_dir() / "openrouter_models.json").write_text(
json.dumps(DUMMY_MODELS), "utf-8"
monkeypatch.setenv(
"LLM_OPENROUTER_KEY",
"sk-...",
)
monkeypatch.setenv(
"OPENROUTER_KEY",
"sk-...",
)
69 changes: 59 additions & 10 deletions tests/test_llm_openrouter.py
Original file line number Diff line number Diff line change
@@ -1,22 +1,71 @@
import llm
import pytest
from click.testing import CliRunner
from inline_snapshot import snapshot
from llm.cli import cli
import json
import pytest

TINY_PNG = (
b"\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\xa6\x00\x00\x01\x1a"
b"\x02\x03\x00\x00\x00\xe6\x99\xc4^\x00\x00\x00\tPLTE\xff\xff\xff"
b"\x00\xff\x00\xfe\x01\x00\x12t\x01J\x00\x00\x00GIDATx\xda\xed\xd81\x11"
b"\x000\x08\xc0\xc0.]\xea\xaf&Q\x89\x04V\xe0>\xf3+\xc8\x91Z\xf4\xa2\x08EQ\x14E"
b"Q\x14EQ\x14EQ\xd4B\x91$I3\xbb\xbf\x08EQ\x14EQ\x14EQ\x14E\xd1\xa5"
b"\xd4\x17\x91\xc6\x95\x05\x15\x0f\x9f\xc5\t\x9f\xa4\x00\x00\x00\x00IEND\xaeB`"
b"\x82"
)


@pytest.mark.vcr
def test_prompt():
model = llm.get_model("openrouter/openai/gpt-4o")
response = model.prompt("Two names for a pet pelican, be brief")
assert str(response) == snapshot("Gully or Skipper")
response_dict = dict(response.response_json)
response_dict.pop("id") # differs between requests
assert response_dict == snapshot(
{
"content": "Gully or Skipper",
"role": "assistant",
"finish_reason": "stop",
"usage": {"completion_tokens": 5, "prompt_tokens": 17, "total_tokens": 22},
"object": "chat.completion.chunk",
"model": "openai/gpt-4o",
"created": 1731200404,
}
)


@pytest.mark.parametrize("set_key", (False, True))
def test_llm_models(set_key, user_path):
@pytest.mark.vcr
def test_llm_models():
runner = CliRunner()
if set_key:
(user_path / "keys.json").write_text(json.dumps({"openrouter": "x"}), "utf-8")
result = runner.invoke(cli, ["models", "list"])
assert result.exit_code == 0, result.output
fragments = (
"OpenRouter: openrouter/openai/gpt-3.5-turbo",
"OpenRouter: openrouter/anthropic/claude-2",
)
for fragment in fragments:
if set_key:
assert fragment in result.output
else:
assert fragment not in result.output
assert fragment in result.output


@pytest.mark.vcr
def test_image_prompt():
model = llm.get_model("openrouter/anthropic/claude-3.5-sonnet")
response = model.prompt(
"Describe image in three words",
attachments=[llm.Attachment(content=TINY_PNG)],
)
assert str(response) == snapshot("Red and green")
response_dict = response.response_json
response_dict.pop("id") # differs between requests
assert response_dict == snapshot(
{
"content": "Red and green",
"role": "assistant",
"finish_reason": "end_turn",
"usage": {"completion_tokens": 7, "prompt_tokens": 82, "total_tokens": 89},
"object": "chat.completion.chunk",
"model": "anthropic/claude-3.5-sonnet",
"created": 1731200406,
}
)

0 comments on commit 8fbcfec

Please sign in to comment.