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.
- llm_hubris-0.1.0/LICENSE +21 -0
- llm_hubris-0.1.0/PKG-INFO +135 -0
- llm_hubris-0.1.0/README.md +117 -0
- llm_hubris-0.1.0/llm_hubris.egg-info/PKG-INFO +135 -0
- llm_hubris-0.1.0/llm_hubris.egg-info/SOURCES.txt +11 -0
- llm_hubris-0.1.0/llm_hubris.egg-info/dependency_links.txt +1 -0
- llm_hubris-0.1.0/llm_hubris.egg-info/entry_points.txt +2 -0
- llm_hubris-0.1.0/llm_hubris.egg-info/requires.txt +2 -0
- llm_hubris-0.1.0/llm_hubris.egg-info/top_level.txt +1 -0
- llm_hubris-0.1.0/llm_hubris.py +176 -0
- llm_hubris-0.1.0/pyproject.toml +30 -0
- llm_hubris-0.1.0/setup.cfg +4 -0
- llm_hubris-0.1.0/tests/test_llm_hubris.py +142 -0
llm_hubris-0.1.0/LICENSE
ADDED
|
@@ -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
|
+
[](https://pypi.org/project/llm-hubris/)
|
|
22
|
+
[](https://github.com/Aimagine-life/llm-hubris/releases)
|
|
23
|
+
[](https://github.com/Aimagine-life/llm-hubris/actions/workflows/test.yml)
|
|
24
|
+
[](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
|
+
[](https://pypi.org/project/llm-hubris/)
|
|
4
|
+
[](https://github.com/Aimagine-life/llm-hubris/releases)
|
|
5
|
+
[](https://github.com/Aimagine-life/llm-hubris/actions/workflows/test.yml)
|
|
6
|
+
[](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
|
+
[](https://pypi.org/project/llm-hubris/)
|
|
22
|
+
[](https://github.com/Aimagine-life/llm-hubris/releases)
|
|
23
|
+
[](https://github.com/Aimagine-life/llm-hubris/actions/workflows/test.yml)
|
|
24
|
+
[](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 @@
|
|
|
1
|
+
|
|
@@ -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,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
|