llm-hubris 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Aimagine-life
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,135 @@
1
+ Metadata-Version: 2.4
2
+ Name: llm-hubris
3
+ Version: 0.1.0
4
+ Summary: LLM plugin for models hosted by Hubris
5
+ Author-email: Hubris <support@hubris.pw>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/Aimagine-life/llm-hubris
8
+ Project-URL: Documentation, https://hubris.pw/docs/integrations/llm
9
+ Project-URL: Changelog, https://github.com/Aimagine-life/llm-hubris/releases
10
+ Project-URL: Issues, https://github.com/Aimagine-life/llm-hubris/issues
11
+ Project-URL: CI, https://github.com/Aimagine-life/llm-hubris/actions
12
+ Requires-Python: >=3.10
13
+ Description-Content-Type: text/markdown
14
+ License-File: LICENSE
15
+ Requires-Dist: llm>=0.27
16
+ Requires-Dist: httpx
17
+ Dynamic: license-file
18
+
19
+ # llm-hubris
20
+
21
+ [![PyPI](https://img.shields.io/pypi/v/llm-hubris.svg)](https://pypi.org/project/llm-hubris/)
22
+ [![Changelog](https://img.shields.io/github/v/release/Aimagine-life/llm-hubris?include_prereleases&label=changelog)](https://github.com/Aimagine-life/llm-hubris/releases)
23
+ [![Tests](https://github.com/Aimagine-life/llm-hubris/actions/workflows/test.yml/badge.svg)](https://github.com/Aimagine-life/llm-hubris/actions/workflows/test.yml)
24
+ [![License](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/Aimagine-life/llm-hubris/blob/main/LICENSE)
25
+
26
+ [LLM](https://llm.datasette.io/) plugin for models hosted by [Hubris](https://hubris.pw) — an OpenAI-compatible LLM gateway billed in Russian rubles, with 400+ models (OpenAI, Anthropic, Google, DeepSeek, Qwen and more) behind a single API key.
27
+
28
+ ## Installation
29
+
30
+ First, [install the LLM command-line utility](https://llm.datasette.io/en/stable/setup.html).
31
+
32
+ Now install this plugin in the same environment as LLM.
33
+
34
+ ```bash
35
+ llm install llm-hubris
36
+ ```
37
+
38
+ ## Configuration
39
+
40
+ You will need an API key from Hubris. Create one at [hubris.pw/keys](https://hubris.pw/keys).
41
+
42
+ You can set that as an environment variable called `HUBRIS_API_KEY`, or add it to the `llm` set of saved keys using:
43
+
44
+ ```bash
45
+ llm keys set hubris
46
+ ```
47
+ ```
48
+ Enter key: <paste key here>
49
+ ```
50
+
51
+ ## Usage
52
+
53
+ The plugin registers every chat model from your Hubris catalog. To list them, run:
54
+
55
+ ```bash
56
+ llm models list
57
+ ```
58
+
59
+ You should see a list that looks something like this:
60
+
61
+ ```
62
+ Hubris: hubris/anthropic/claude-sonnet-5
63
+ Hubris: hubris/openai/gpt-5.6-luna
64
+ Hubris: hubris/google/gemini-3.7-flash
65
+ Hubris: hubris/deepseek/deepseek-v4-flash-0731
66
+ ...
67
+ ```
68
+
69
+ Model IDs always use the full `vendor/model` form shown in the [Hubris catalog](https://hubris.pw/models) — there is no alias resolution on the gateway side.
70
+
71
+ The list is cached for an hour. Force a refresh with:
72
+
73
+ ```bash
74
+ llm hubris refresh
75
+ ```
76
+
77
+ To run a prompt against a model, pass its full model ID to the `-m` option:
78
+
79
+ ```bash
80
+ llm -m hubris/anthropic/claude-sonnet-5 "Five spooky names for a pet tarantula"
81
+ ```
82
+
83
+ You can set a shorter alias for a model using the `llm aliases` command:
84
+
85
+ ```bash
86
+ llm aliases set sonnet hubris/anthropic/claude-sonnet-5
87
+ ```
88
+
89
+ Now you can prompt it with:
90
+
91
+ ```bash
92
+ cat llm_hubris.py | llm -m sonnet -s 'write some pytest tests for this'
93
+ ```
94
+
95
+ Images are supported by vision models:
96
+
97
+ ```bash
98
+ llm -m hubris/anthropic/claude-sonnet-5 'describe this image' -a https://static.simonwillison.net/static/2024/pelicans.jpg
99
+ ```
100
+
101
+ Reasoning models accept the `reasoning_effort` option:
102
+
103
+ ```bash
104
+ llm -m hubris/openai/gpt-5.6-luna -o reasoning_effort high 'Plan a three-day trip to Kazan'
105
+ ```
106
+
107
+ ### Listing models with prices
108
+
109
+ ```bash
110
+ llm hubris models
111
+ ```
112
+
113
+ Outputs every chat model with its context window and RUB prices per million tokens. Add `--json` for the raw catalog entries.
114
+
115
+ ## Development
116
+
117
+ To set up this plugin locally, first checkout the code. Then create a new virtual environment:
118
+
119
+ ```bash
120
+ cd llm-hubris
121
+ python -m venv venv
122
+ source venv/bin/activate
123
+ ```
124
+
125
+ Now install the dependencies and test dependencies:
126
+
127
+ ```bash
128
+ pip install -e '.[test]'
129
+ ```
130
+
131
+ To run the tests:
132
+
133
+ ```bash
134
+ pytest
135
+ ```
@@ -0,0 +1,117 @@
1
+ # llm-hubris
2
+
3
+ [![PyPI](https://img.shields.io/pypi/v/llm-hubris.svg)](https://pypi.org/project/llm-hubris/)
4
+ [![Changelog](https://img.shields.io/github/v/release/Aimagine-life/llm-hubris?include_prereleases&label=changelog)](https://github.com/Aimagine-life/llm-hubris/releases)
5
+ [![Tests](https://github.com/Aimagine-life/llm-hubris/actions/workflows/test.yml/badge.svg)](https://github.com/Aimagine-life/llm-hubris/actions/workflows/test.yml)
6
+ [![License](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/Aimagine-life/llm-hubris/blob/main/LICENSE)
7
+
8
+ [LLM](https://llm.datasette.io/) plugin for models hosted by [Hubris](https://hubris.pw) — an OpenAI-compatible LLM gateway billed in Russian rubles, with 400+ models (OpenAI, Anthropic, Google, DeepSeek, Qwen and more) behind a single API key.
9
+
10
+ ## Installation
11
+
12
+ First, [install the LLM command-line utility](https://llm.datasette.io/en/stable/setup.html).
13
+
14
+ Now install this plugin in the same environment as LLM.
15
+
16
+ ```bash
17
+ llm install llm-hubris
18
+ ```
19
+
20
+ ## Configuration
21
+
22
+ You will need an API key from Hubris. Create one at [hubris.pw/keys](https://hubris.pw/keys).
23
+
24
+ You can set that as an environment variable called `HUBRIS_API_KEY`, or add it to the `llm` set of saved keys using:
25
+
26
+ ```bash
27
+ llm keys set hubris
28
+ ```
29
+ ```
30
+ Enter key: <paste key here>
31
+ ```
32
+
33
+ ## Usage
34
+
35
+ The plugin registers every chat model from your Hubris catalog. To list them, run:
36
+
37
+ ```bash
38
+ llm models list
39
+ ```
40
+
41
+ You should see a list that looks something like this:
42
+
43
+ ```
44
+ Hubris: hubris/anthropic/claude-sonnet-5
45
+ Hubris: hubris/openai/gpt-5.6-luna
46
+ Hubris: hubris/google/gemini-3.7-flash
47
+ Hubris: hubris/deepseek/deepseek-v4-flash-0731
48
+ ...
49
+ ```
50
+
51
+ Model IDs always use the full `vendor/model` form shown in the [Hubris catalog](https://hubris.pw/models) — there is no alias resolution on the gateway side.
52
+
53
+ The list is cached for an hour. Force a refresh with:
54
+
55
+ ```bash
56
+ llm hubris refresh
57
+ ```
58
+
59
+ To run a prompt against a model, pass its full model ID to the `-m` option:
60
+
61
+ ```bash
62
+ llm -m hubris/anthropic/claude-sonnet-5 "Five spooky names for a pet tarantula"
63
+ ```
64
+
65
+ You can set a shorter alias for a model using the `llm aliases` command:
66
+
67
+ ```bash
68
+ llm aliases set sonnet hubris/anthropic/claude-sonnet-5
69
+ ```
70
+
71
+ Now you can prompt it with:
72
+
73
+ ```bash
74
+ cat llm_hubris.py | llm -m sonnet -s 'write some pytest tests for this'
75
+ ```
76
+
77
+ Images are supported by vision models:
78
+
79
+ ```bash
80
+ llm -m hubris/anthropic/claude-sonnet-5 'describe this image' -a https://static.simonwillison.net/static/2024/pelicans.jpg
81
+ ```
82
+
83
+ Reasoning models accept the `reasoning_effort` option:
84
+
85
+ ```bash
86
+ llm -m hubris/openai/gpt-5.6-luna -o reasoning_effort high 'Plan a three-day trip to Kazan'
87
+ ```
88
+
89
+ ### Listing models with prices
90
+
91
+ ```bash
92
+ llm hubris models
93
+ ```
94
+
95
+ Outputs every chat model with its context window and RUB prices per million tokens. Add `--json` for the raw catalog entries.
96
+
97
+ ## Development
98
+
99
+ To set up this plugin locally, first checkout the code. Then create a new virtual environment:
100
+
101
+ ```bash
102
+ cd llm-hubris
103
+ python -m venv venv
104
+ source venv/bin/activate
105
+ ```
106
+
107
+ Now install the dependencies and test dependencies:
108
+
109
+ ```bash
110
+ pip install -e '.[test]'
111
+ ```
112
+
113
+ To run the tests:
114
+
115
+ ```bash
116
+ pytest
117
+ ```
@@ -0,0 +1,135 @@
1
+ Metadata-Version: 2.4
2
+ Name: llm-hubris
3
+ Version: 0.1.0
4
+ Summary: LLM plugin for models hosted by Hubris
5
+ Author-email: Hubris <support@hubris.pw>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/Aimagine-life/llm-hubris
8
+ Project-URL: Documentation, https://hubris.pw/docs/integrations/llm
9
+ Project-URL: Changelog, https://github.com/Aimagine-life/llm-hubris/releases
10
+ Project-URL: Issues, https://github.com/Aimagine-life/llm-hubris/issues
11
+ Project-URL: CI, https://github.com/Aimagine-life/llm-hubris/actions
12
+ Requires-Python: >=3.10
13
+ Description-Content-Type: text/markdown
14
+ License-File: LICENSE
15
+ Requires-Dist: llm>=0.27
16
+ Requires-Dist: httpx
17
+ Dynamic: license-file
18
+
19
+ # llm-hubris
20
+
21
+ [![PyPI](https://img.shields.io/pypi/v/llm-hubris.svg)](https://pypi.org/project/llm-hubris/)
22
+ [![Changelog](https://img.shields.io/github/v/release/Aimagine-life/llm-hubris?include_prereleases&label=changelog)](https://github.com/Aimagine-life/llm-hubris/releases)
23
+ [![Tests](https://github.com/Aimagine-life/llm-hubris/actions/workflows/test.yml/badge.svg)](https://github.com/Aimagine-life/llm-hubris/actions/workflows/test.yml)
24
+ [![License](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/Aimagine-life/llm-hubris/blob/main/LICENSE)
25
+
26
+ [LLM](https://llm.datasette.io/) plugin for models hosted by [Hubris](https://hubris.pw) — an OpenAI-compatible LLM gateway billed in Russian rubles, with 400+ models (OpenAI, Anthropic, Google, DeepSeek, Qwen and more) behind a single API key.
27
+
28
+ ## Installation
29
+
30
+ First, [install the LLM command-line utility](https://llm.datasette.io/en/stable/setup.html).
31
+
32
+ Now install this plugin in the same environment as LLM.
33
+
34
+ ```bash
35
+ llm install llm-hubris
36
+ ```
37
+
38
+ ## Configuration
39
+
40
+ You will need an API key from Hubris. Create one at [hubris.pw/keys](https://hubris.pw/keys).
41
+
42
+ You can set that as an environment variable called `HUBRIS_API_KEY`, or add it to the `llm` set of saved keys using:
43
+
44
+ ```bash
45
+ llm keys set hubris
46
+ ```
47
+ ```
48
+ Enter key: <paste key here>
49
+ ```
50
+
51
+ ## Usage
52
+
53
+ The plugin registers every chat model from your Hubris catalog. To list them, run:
54
+
55
+ ```bash
56
+ llm models list
57
+ ```
58
+
59
+ You should see a list that looks something like this:
60
+
61
+ ```
62
+ Hubris: hubris/anthropic/claude-sonnet-5
63
+ Hubris: hubris/openai/gpt-5.6-luna
64
+ Hubris: hubris/google/gemini-3.7-flash
65
+ Hubris: hubris/deepseek/deepseek-v4-flash-0731
66
+ ...
67
+ ```
68
+
69
+ Model IDs always use the full `vendor/model` form shown in the [Hubris catalog](https://hubris.pw/models) — there is no alias resolution on the gateway side.
70
+
71
+ The list is cached for an hour. Force a refresh with:
72
+
73
+ ```bash
74
+ llm hubris refresh
75
+ ```
76
+
77
+ To run a prompt against a model, pass its full model ID to the `-m` option:
78
+
79
+ ```bash
80
+ llm -m hubris/anthropic/claude-sonnet-5 "Five spooky names for a pet tarantula"
81
+ ```
82
+
83
+ You can set a shorter alias for a model using the `llm aliases` command:
84
+
85
+ ```bash
86
+ llm aliases set sonnet hubris/anthropic/claude-sonnet-5
87
+ ```
88
+
89
+ Now you can prompt it with:
90
+
91
+ ```bash
92
+ cat llm_hubris.py | llm -m sonnet -s 'write some pytest tests for this'
93
+ ```
94
+
95
+ Images are supported by vision models:
96
+
97
+ ```bash
98
+ llm -m hubris/anthropic/claude-sonnet-5 'describe this image' -a https://static.simonwillison.net/static/2024/pelicans.jpg
99
+ ```
100
+
101
+ Reasoning models accept the `reasoning_effort` option:
102
+
103
+ ```bash
104
+ llm -m hubris/openai/gpt-5.6-luna -o reasoning_effort high 'Plan a three-day trip to Kazan'
105
+ ```
106
+
107
+ ### Listing models with prices
108
+
109
+ ```bash
110
+ llm hubris models
111
+ ```
112
+
113
+ Outputs every chat model with its context window and RUB prices per million tokens. Add `--json` for the raw catalog entries.
114
+
115
+ ## Development
116
+
117
+ To set up this plugin locally, first checkout the code. Then create a new virtual environment:
118
+
119
+ ```bash
120
+ cd llm-hubris
121
+ python -m venv venv
122
+ source venv/bin/activate
123
+ ```
124
+
125
+ Now install the dependencies and test dependencies:
126
+
127
+ ```bash
128
+ pip install -e '.[test]'
129
+ ```
130
+
131
+ To run the tests:
132
+
133
+ ```bash
134
+ pytest
135
+ ```
@@ -0,0 +1,11 @@
1
+ LICENSE
2
+ README.md
3
+ llm_hubris.py
4
+ pyproject.toml
5
+ llm_hubris.egg-info/PKG-INFO
6
+ llm_hubris.egg-info/SOURCES.txt
7
+ llm_hubris.egg-info/dependency_links.txt
8
+ llm_hubris.egg-info/entry_points.txt
9
+ llm_hubris.egg-info/requires.txt
10
+ llm_hubris.egg-info/top_level.txt
11
+ tests/test_llm_hubris.py
@@ -0,0 +1,2 @@
1
+ [llm]
2
+ hubris = llm_hubris
@@ -0,0 +1,2 @@
1
+ llm>=0.27
2
+ httpx
@@ -0,0 +1 @@
1
+ llm_hubris
@@ -0,0 +1,176 @@
1
+ """LLM plugin for models hosted by Hubris (https://hubris.pw)."""
2
+
3
+ import json
4
+ import time
5
+ from pathlib import Path
6
+
7
+ import click
8
+ import httpx
9
+ import llm
10
+ from llm.default_plugins.openai_models import AsyncChat, Chat
11
+
12
+ API_BASE = "https://api.hubris.pw/v1"
13
+ KEY_NAME = "hubris"
14
+ KEY_ENV_VAR = "HUBRIS_API_KEY"
15
+ CACHE_FILE = "hubris_models.json"
16
+ CACHE_TIMEOUT = 3600
17
+
18
+
19
+ class HubrisChat(Chat):
20
+ needs_key = KEY_NAME
21
+ key_env_var = KEY_ENV_VAR
22
+
23
+ def __str__(self):
24
+ return "Hubris: {}".format(self.model_id)
25
+
26
+
27
+ class HubrisAsyncChat(AsyncChat):
28
+ needs_key = KEY_NAME
29
+ key_env_var = KEY_ENV_VAR
30
+
31
+ def __str__(self):
32
+ return "Hubris: {}".format(self.model_id)
33
+
34
+
35
+ class DownloadError(Exception):
36
+ pass
37
+
38
+
39
+ def fetch_cached_json(url, path, cache_timeout, headers=None):
40
+ path = Path(path)
41
+ path.parent.mkdir(parents=True, exist_ok=True)
42
+
43
+ if path.is_file():
44
+ mod_time = path.stat().st_mtime
45
+ if time.time() - mod_time < cache_timeout:
46
+ with open(path, "r") as file:
47
+ return json.load(file)
48
+
49
+ try:
50
+ response = httpx.get(url, headers=headers, follow_redirects=True)
51
+ response.raise_for_status()
52
+ data = response.json()
53
+ with open(path, "w") as file:
54
+ json.dump(data, file)
55
+ return data
56
+ except httpx.HTTPError:
57
+ if path.is_file():
58
+ with open(path, "r") as file:
59
+ return json.load(file)
60
+ raise DownloadError(
61
+ f"Failed to download data and no cache is available at {path}"
62
+ )
63
+
64
+
65
+ def get_hubris_models(key, skip_cache=False):
66
+ """Return the raw ``data`` list from ``GET /v1/models``."""
67
+ return fetch_cached_json(
68
+ url=API_BASE + "/models",
69
+ path=llm.user_dir() / CACHE_FILE,
70
+ cache_timeout=0 if skip_cache else CACHE_TIMEOUT,
71
+ headers={"Authorization": f"Bearer {key}"},
72
+ )["data"]
73
+
74
+
75
+ def is_chat_model(model_definition):
76
+ """Hubris also lists image, video, speech and embedding models; keep chat ones."""
77
+ output = model_definition.get("output_modalities") or ["text"]
78
+ pricing = model_definition.get("pricing") or {}
79
+ return "text" in output and pricing.get("unit", "token") == "token"
80
+
81
+
82
+ def supports_images(model_definition):
83
+ return "image" in (model_definition.get("input_modalities") or [])
84
+
85
+
86
+ def model_kwargs(model_definition):
87
+ return dict(
88
+ model_id="hubris/{}".format(model_definition["id"]),
89
+ model_name=model_definition["id"],
90
+ vision=supports_images(model_definition),
91
+ # Hubris follows the OpenAI request shape for all chat models, so the
92
+ # optional reasoning/schema/tools options are exposed everywhere;
93
+ # models that don't support a given feature return an API error.
94
+ reasoning=True,
95
+ supports_schema=True,
96
+ supports_tools=True,
97
+ api_base=API_BASE,
98
+ )
99
+
100
+
101
+ @llm.hookimpl
102
+ def register_models(register):
103
+ # Only register models if a Hubris key is available, since the
104
+ # catalog endpoint itself requires authentication.
105
+ key = llm.get_key("", KEY_NAME, KEY_ENV_VAR)
106
+ if not key:
107
+ return
108
+ try:
109
+ models = get_hubris_models(key)
110
+ except DownloadError:
111
+ return
112
+ for model_definition in models:
113
+ if not is_chat_model(model_definition):
114
+ continue
115
+ kwargs = model_kwargs(model_definition)
116
+ register(HubrisChat(**kwargs), HubrisAsyncChat(**kwargs))
117
+
118
+
119
+ def _format_rub(value):
120
+ if value is None:
121
+ return "-"
122
+ return f"{value:,.2f} RUB/M".replace(",", " ")
123
+
124
+
125
+ @llm.hookimpl
126
+ def register_commands(cli):
127
+ @cli.group()
128
+ def hubris():
129
+ "Commands relating to the llm-hubris plugin"
130
+
131
+ @hubris.command()
132
+ def refresh():
133
+ "Refresh the cached list of models from Hubris"
134
+ key = llm.get_key("", KEY_NAME, KEY_ENV_VAR)
135
+ if not key:
136
+ raise click.ClickException(
137
+ f"No Hubris key found. Run `llm keys set {KEY_NAME}` "
138
+ f"or set {KEY_ENV_VAR}."
139
+ )
140
+ before = set(m["id"] for m in get_hubris_models(key))
141
+ models = get_hubris_models(key, skip_cache=True)
142
+ after = set(m["id"] for m in models)
143
+ added = after - before
144
+ removed = before - after
145
+ click.echo(f"Refreshed {len(models)} models", err=True)
146
+ if added:
147
+ click.echo(f"Added: {', '.join(sorted(added))}", err=True)
148
+ if removed:
149
+ click.echo(f"Removed: {', '.join(sorted(removed))}", err=True)
150
+
151
+ @hubris.command()
152
+ @click.option("json_", "--json", is_flag=True, help="Output as JSON")
153
+ def models(json_):
154
+ "List chat models available on Hubris with their RUB prices"
155
+ key = llm.get_key("", KEY_NAME, KEY_ENV_VAR)
156
+ if not key:
157
+ raise click.ClickException(
158
+ f"No Hubris key found. Run `llm keys set {KEY_NAME}` "
159
+ f"or set {KEY_ENV_VAR}."
160
+ )
161
+ all_models = [m for m in get_hubris_models(key) if is_chat_model(m)]
162
+ if json_:
163
+ click.echo(json.dumps(all_models, indent=2, ensure_ascii=False))
164
+ return
165
+ for model in all_models:
166
+ pricing = model.get("pricing") or {}
167
+ bits = [
168
+ f"- id: {model['id']}",
169
+ f" name: {model.get('display_name', model['id'])}",
170
+ f" context: {model.get('context_window', '-')}",
171
+ f" input: {_format_rub(pricing.get('input_rub_per_million'))}",
172
+ f" output: {_format_rub(pricing.get('output_rub_per_million'))}",
173
+ ]
174
+ if supports_images(model):
175
+ bits.append(" vision: yes")
176
+ click.echo("\n".join(bits))
@@ -0,0 +1,30 @@
1
+ [project]
2
+ name = "llm-hubris"
3
+ version = "0.1.0"
4
+ description = "LLM plugin for models hosted by Hubris"
5
+ readme = "README.md"
6
+ authors = [{name = "Hubris", email = "support@hubris.pw"}]
7
+ license = "MIT"
8
+ requires-python = ">=3.10"
9
+ classifiers = []
10
+ dependencies = [
11
+ "llm>=0.27",
12
+ "httpx",
13
+ ]
14
+
15
+ [project.urls]
16
+ Homepage = "https://github.com/Aimagine-life/llm-hubris"
17
+ Documentation = "https://hubris.pw/docs/integrations/llm"
18
+ Changelog = "https://github.com/Aimagine-life/llm-hubris/releases"
19
+ Issues = "https://github.com/Aimagine-life/llm-hubris/issues"
20
+ CI = "https://github.com/Aimagine-life/llm-hubris/actions"
21
+
22
+ [project.entry-points.llm]
23
+ hubris = "llm_hubris"
24
+
25
+ [build-system]
26
+ requires = ["setuptools"]
27
+ build-backend = "setuptools.build_meta"
28
+
29
+ [dependency-groups]
30
+ dev = ["pytest"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,142 @@
1
+ import json
2
+
3
+ import llm
4
+ import pytest
5
+ from click.testing import CliRunner
6
+ from llm.cli import cli
7
+
8
+ import llm_hubris
9
+
10
+ SAMPLE_MODELS = [
11
+ {
12
+ "id": "anthropic/claude-sonnet-5",
13
+ "object": "model",
14
+ "owned_by": "anthropic",
15
+ "display_name": "Anthropic: Claude Sonnet 5",
16
+ "context_window": 1000000,
17
+ "input_modalities": ["text", "image", "file"],
18
+ "output_modalities": ["text"],
19
+ "pricing": {
20
+ "unit": "token",
21
+ "input_rub_per_million": 252.73,
22
+ "output_rub_per_million": 1263.61,
23
+ "currency": "RUB",
24
+ },
25
+ },
26
+ {
27
+ "id": "deepseek/deepseek-v4-flash-0731",
28
+ "object": "model",
29
+ "owned_by": "deepseek",
30
+ "display_name": "DeepSeek: DeepSeek V4 Flash 0731",
31
+ "context_window": 1310720,
32
+ "input_modalities": ["text"],
33
+ "output_modalities": ["text"],
34
+ "pricing": {
35
+ "unit": "token",
36
+ "input_rub_per_million": 8.21,
37
+ "output_rub_per_million": 22.74,
38
+ "currency": "RUB",
39
+ },
40
+ },
41
+ {
42
+ "id": "google/gemini-2.5-flash-image",
43
+ "object": "model",
44
+ "owned_by": "google",
45
+ "display_name": "Google: Gemini 2.5 Flash Image",
46
+ "input_modalities": ["text", "image"],
47
+ "output_modalities": ["image"],
48
+ "pricing": {"unit": "image", "currency": "RUB"},
49
+ },
50
+ ]
51
+
52
+
53
+ @pytest.fixture
54
+ def user_path(tmpdir):
55
+ path = tmpdir / "llm.datasette.io"
56
+ path.mkdir()
57
+ return path
58
+
59
+
60
+ @pytest.fixture(autouse=True)
61
+ def env_setup(monkeypatch, user_path):
62
+ monkeypatch.setenv("LLM_USER_PATH", str(user_path))
63
+ monkeypatch.setenv("HUBRIS_API_KEY", "sk-gw-test")
64
+ monkeypatch.setattr(
65
+ llm_hubris, "get_hubris_models", lambda key, skip_cache=False: SAMPLE_MODELS
66
+ )
67
+
68
+
69
+ def test_is_chat_model_filters_non_text_models():
70
+ assert llm_hubris.is_chat_model(SAMPLE_MODELS[0])
71
+ assert llm_hubris.is_chat_model(SAMPLE_MODELS[1])
72
+ assert not llm_hubris.is_chat_model(SAMPLE_MODELS[2])
73
+
74
+
75
+ def test_model_kwargs_keeps_vendor_prefix_and_detects_vision():
76
+ kwargs = llm_hubris.model_kwargs(SAMPLE_MODELS[0])
77
+ assert kwargs["model_id"] == "hubris/anthropic/claude-sonnet-5"
78
+ assert kwargs["model_name"] == "anthropic/claude-sonnet-5"
79
+ assert kwargs["api_base"] == "https://api.hubris.pw/v1"
80
+ assert kwargs["vision"] is True
81
+ assert llm_hubris.model_kwargs(SAMPLE_MODELS[1])["vision"] is False
82
+
83
+
84
+ def test_register_models_registers_chat_models_only():
85
+ registered = []
86
+
87
+ def register(*models, **kwargs):
88
+ registered.append(models)
89
+
90
+ llm_hubris.register_models(register)
91
+
92
+ ids = [models[0].model_id for models in registered]
93
+ assert ids == [
94
+ "hubris/anthropic/claude-sonnet-5",
95
+ "hubris/deepseek/deepseek-v4-flash-0731",
96
+ ]
97
+ sync_model, async_model = registered[0]
98
+ assert isinstance(sync_model, llm_hubris.HubrisChat)
99
+ assert isinstance(async_model, llm_hubris.HubrisAsyncChat)
100
+ assert sync_model.needs_key == "hubris"
101
+ assert sync_model.key_env_var == "HUBRIS_API_KEY"
102
+ assert str(sync_model) == "Hubris: hubris/anthropic/claude-sonnet-5"
103
+ assert "image/png" in sync_model.attachment_types
104
+ assert not async_model.attachment_types or "image/png" in async_model.attachment_types
105
+
106
+
107
+ def test_register_models_skips_without_key(monkeypatch):
108
+ monkeypatch.delenv("HUBRIS_API_KEY", raising=False)
109
+ registered = []
110
+ llm_hubris.register_models(lambda *models, **kwargs: registered.append(models))
111
+ assert registered == []
112
+
113
+
114
+ def test_get_model_via_llm():
115
+ model = llm.get_model("hubris/anthropic/claude-sonnet-5")
116
+ assert isinstance(model, llm_hubris.HubrisChat)
117
+ assert model.model_name == "anthropic/claude-sonnet-5"
118
+ assert model.api_base == "https://api.hubris.pw/v1"
119
+
120
+
121
+ def test_hubris_models_command():
122
+ runner = CliRunner()
123
+ result = runner.invoke(cli, ["hubris", "models"])
124
+ assert result.exit_code == 0, result.output
125
+ assert "- id: anthropic/claude-sonnet-5" in result.output
126
+ assert "vision: yes" in result.output
127
+ assert "gemini-2.5-flash-image" not in result.output
128
+
129
+ result = runner.invoke(cli, ["hubris", "models", "--json"])
130
+ assert result.exit_code == 0, result.output
131
+ data = json.loads(result.output)
132
+ assert [m["id"] for m in data] == [
133
+ "anthropic/claude-sonnet-5",
134
+ "deepseek/deepseek-v4-flash-0731",
135
+ ]
136
+
137
+
138
+ def test_hubris_refresh_command():
139
+ runner = CliRunner()
140
+ result = runner.invoke(cli, ["hubris", "refresh"])
141
+ assert result.exit_code == 0, result.output
142
+ assert "Refreshed 3 models" in result.output