blablador 0.2.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.
- blablador-0.2.0/LICENSE +21 -0
- blablador-0.2.0/PKG-INFO +256 -0
- blablador-0.2.0/README.md +239 -0
- blablador-0.2.0/pyproject.toml +44 -0
- blablador-0.2.0/pyproject.toml.orig +42 -0
- blablador-0.2.0/src/blablador/__init__.py +29 -0
- blablador-0.2.0/src/blablador/__main__.py +7 -0
- blablador-0.2.0/src/blablador/_client.py +326 -0
- blablador-0.2.0/src/blablador/_errors.py +104 -0
- blablador-0.2.0/src/blablador/_models.py +127 -0
- blablador-0.2.0/src/blablador/_reasoning.py +33 -0
- blablador-0.2.0/src/blablador/_rerank.py +127 -0
- blablador-0.2.0/src/blablador/_retry.py +97 -0
- blablador-0.2.0/src/blablador/_tools.py +53 -0
- blablador-0.2.0/src/blablador/cli.py +219 -0
- blablador-0.2.0/src/blablador/py.typed +0 -0
blablador-0.2.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Forschungszentrum Juelich GmbH, Juelich Supercomputing Centre
|
|
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.
|
blablador-0.2.0/PKG-INFO
ADDED
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: blablador
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Python client for JSC's Blablador LLM API, built on the OpenAI SDK
|
|
5
|
+
Author: Applied Machine Learning Lab at JSC
|
|
6
|
+
Author-email: Applied Machine Learning Lab at JSC <ai-support@fz-juelich.de>
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Requires-Dist: openai>=3.42,<3.43
|
|
10
|
+
Requires-Dist: anyio>=4.10,<5
|
|
11
|
+
Requires-Dist: httpx2>=2.12,<3
|
|
12
|
+
Requires-Dist: typing-extensions>=4.14,<5
|
|
13
|
+
Requires-Python: >=3.10
|
|
14
|
+
Project-URL: Homepage, https://gitlab.jsc.fz-juelich.de/sdlaml/agents-on-hpc/blablador
|
|
15
|
+
Project-URL: Repository, https://gitlab.jsc.fz-juelich.de/sdlaml/agents-on-hpc/blablador.git
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
|
|
18
|
+
# blablador
|
|
19
|
+
|
|
20
|
+
Python client and command-line tool for [Blablador](https://sdlaml.pages.jsc.fz-juelich.de/ai/guides/blablador_api_access), the LLM service of the Jülich Supercomputing Centre.
|
|
21
|
+
|
|
22
|
+
Blablador's API is OpenAI-compatible, so this package is a thin layer over the official [`openai` SDK](https://github.com/openai/openai-python): everything the SDK does works unchanged, and the package adds what Blablador needs on top:
|
|
23
|
+
|
|
24
|
+
- clients preconfigured for Blablador (`Blablador`, `AsyncBlablador`)
|
|
25
|
+
- typed model listing with Blablador's fields and the current `alias-*` names
|
|
26
|
+
- the model's reasoning, whichever backend serves it (`blablador.reasoning()`)
|
|
27
|
+
- the rerank endpoint, which the OpenAI API does not have (`client.rerank`)
|
|
28
|
+
- fixes for requests the gateway rejects but the OpenAI API allows
|
|
29
|
+
- rate-limit handling that does not extend Blablador's blocks: no retry of HTTP 429 by default, and an opt-in wait for the time the server states
|
|
30
|
+
- a `blablador` command: `models`, `chat`, `transcribe`
|
|
31
|
+
|
|
32
|
+
Status: early. Requires Python 3.10 or newer. The client overrides internals of the `openai` SDK, so each release pins one minor version of `openai` (0.2.0: `openai>=3.42,<3.43`); the pin is raised once the tests pass with a newer release.
|
|
33
|
+
|
|
34
|
+
## Install
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
pip install blablador
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
This installs the library and the `blablador` command. The development version installs from the [repository](https://gitlab.jsc.fz-juelich.de/sdlaml/agents-on-hpc/blablador): `pip install "git+https://gitlab.jsc.fz-juelich.de/sdlaml/agents-on-hpc/blablador.git"`.
|
|
41
|
+
|
|
42
|
+
If you use [uv](https://docs.astral.sh/uv/), add it to your project, or install only the command, available everywhere:
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
uv add blablador
|
|
46
|
+
uv tool install blablador
|
|
47
|
+
uv add "blablador @ git+https://gitlab.jsc.fz-juelich.de/sdlaml/agents-on-hpc/blablador.git" # development version
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Getting started
|
|
51
|
+
|
|
52
|
+
You need an API key: a [Helmholtz Codebase token](https://codebase.helmholtz.cloud/-/user_settings/personal_access_tokens) with the `read_user` scope (sign in with your institution's account). Tokens are valid for at most a year. Put it in the environment:
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
export BLABLADOR_API_KEY=...
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Then use `Blablador` like `openai.OpenAI`, which it is a subclass of:
|
|
59
|
+
|
|
60
|
+
```python
|
|
61
|
+
from blablador import Blablador
|
|
62
|
+
|
|
63
|
+
client = Blablador() # reads BLABLADOR_API_KEY
|
|
64
|
+
completion = client.chat.completions.create(
|
|
65
|
+
model="alias-fast",
|
|
66
|
+
messages=[{"role": "user", "content": "Hello!"}],
|
|
67
|
+
)
|
|
68
|
+
print(completion.choices[0].message.content)
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Streaming, the Responses API, tool calls, embeddings, audio transcription and `with_options()` work as the [OpenAI SDK documentation](https://github.com/openai/openai-python) describes. `AsyncBlablador` is the same for `openai.AsyncOpenAI`.
|
|
72
|
+
|
|
73
|
+
From the shell:
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
blablador chat "What is the Jülich Supercomputing Centre?"
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## What the package adds
|
|
80
|
+
|
|
81
|
+
### Models and aliases
|
|
82
|
+
|
|
83
|
+
Blablador's models change. The `alias-*` names (`alias-fast`, `alias-code`, `alias-large`, ...) stay, and are pointed at newer models as these replace older ones, so use aliases in code and read what is behind them at runtime rather than hardcoding a table.
|
|
84
|
+
|
|
85
|
+
`client.models.list()` returns `blablador.BlabladorModel`s, which add Blablador's fields to the OpenAI ones:
|
|
86
|
+
|
|
87
|
+
| Field | Meaning |
|
|
88
|
+
|---|---|
|
|
89
|
+
| `root` | the underlying model: a Hugging Face name, a path on the server, or `None` |
|
|
90
|
+
| `max_model_len` | context length in tokens, if reported |
|
|
91
|
+
| `client_count` | number of servers currently serving the model, if reported |
|
|
92
|
+
| `is_alias` | `True` for the `alias-*` names |
|
|
93
|
+
|
|
94
|
+
```python
|
|
95
|
+
for model in client.models.list():
|
|
96
|
+
if model.is_alias:
|
|
97
|
+
print(model.id, model.root, model.max_model_len)
|
|
98
|
+
|
|
99
|
+
client.models.aliases() # {"alias-fast": "openai/gpt-oss-120b", ...}, one request
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
In chat completions, the response's `model` field holds the model's display name ("01 - GPT-OSS-120b - ..."), not the alias that was sent; embeddings responses name the alias. `client.models.retrieve()` does not work: Blablador answers 404 for a single model. The listing does not say what a model can do (chat, embeddings, rerank, speech); the names are the only hint.
|
|
103
|
+
|
|
104
|
+
### Reasoning
|
|
105
|
+
|
|
106
|
+
Reasoning models return their reasoning next to the answer, in a field the OpenAI API does not define and whose name depends on the backend (`reasoning` from vLLM, `reasoning_content` from sglang). `blablador.reasoning()` returns it from a message or a streamed delta, or `None`:
|
|
107
|
+
|
|
108
|
+
```python
|
|
109
|
+
import blablador
|
|
110
|
+
|
|
111
|
+
message = completion.choices[0].message
|
|
112
|
+
print(blablador.reasoning(message)) # how the model got there
|
|
113
|
+
print(message.content) # the answer
|
|
114
|
+
|
|
115
|
+
for chunk in client.chat.completions.create(..., stream=True):
|
|
116
|
+
if chunk.choices:
|
|
117
|
+
print(blablador.reasoning(chunk.choices[0].delta) or "", end="")
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### Rerank
|
|
121
|
+
|
|
122
|
+
`client.rerank.create()` scores documents by relevance to a query (Blablador's `/v1/rerank`), best first. Use it to pick the best few of many candidates, for example after an embedding search:
|
|
123
|
+
|
|
124
|
+
```python
|
|
125
|
+
texts = [
|
|
126
|
+
"sbatch submits a batch script to Slurm",
|
|
127
|
+
"JURECA has A100 GPUs",
|
|
128
|
+
"The cafeteria opens at 11:30",
|
|
129
|
+
]
|
|
130
|
+
ranked = client.rerank.create(
|
|
131
|
+
model="alias-qwen3-8b-reranking",
|
|
132
|
+
query="How do I submit a job on JURECA?",
|
|
133
|
+
documents=texts,
|
|
134
|
+
top_n=2, # optional: only the best two
|
|
135
|
+
)
|
|
136
|
+
for result in ranked.results: # sorted by relevance_score, highest first
|
|
137
|
+
print(f"{result.relevance_score:.3f} {texts[result.index]}")
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
`return_documents=True` adds each document's text to its result. `AsyncBlablador` has the same method as a coroutine.
|
|
141
|
+
|
|
142
|
+
### Requests the gateway rejects
|
|
143
|
+
|
|
144
|
+
Where the gateway is stricter than the OpenAI API, the client adjusts the request so that code written for OpenAI keeps working:
|
|
145
|
+
|
|
146
|
+
- Function tools without a `description` (optional for OpenAI, HTTP 400 from Blablador) are sent with `"description": ""`, for chat completions and the Responses API.
|
|
147
|
+
|
|
148
|
+
### Rate limits
|
|
149
|
+
|
|
150
|
+
Blablador limits requests per model and user, and blocks a key that exceeds the limit: 60 s for the first violation, doubling with each further one up to an hour. The OpenAI SDK would retry an HTTP 429 at once, which risks a longer block, so this client never retries 429 by default (timeouts, 5xx and connection errors are still retried, `max_retries` times). Instead it raises `blablador.RateLimitError`, a subclass of `openai.RateLimitError`, whose `retry_after` holds the wait in seconds if the server stated one, in a `Retry-After` header or in the error message ("try again in 57 seconds"):
|
|
151
|
+
|
|
152
|
+
```python
|
|
153
|
+
import time
|
|
154
|
+
import blablador
|
|
155
|
+
|
|
156
|
+
try:
|
|
157
|
+
completion = client.chat.completions.create(...)
|
|
158
|
+
except blablador.RateLimitError as err:
|
|
159
|
+
time.sleep(err.retry_after or 60) # then try again, once
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
For batch jobs, the client can do the waiting itself:
|
|
163
|
+
|
|
164
|
+
```python
|
|
165
|
+
client = blablador.Blablador(rate_limit_retries=3, max_rate_limit_wait=600)
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
A request that gets a 429 then waits the stated time (plus a second) and is sent again, up to `rate_limit_retries` times. It is not retried, and the error is raised, when the server did not say how long to wait, since a guessed wait that ends too early may extend the block, or when the wait is longer than `max_rate_limit_wait` seconds (default 3600). The client records every stated block, also one it does not wait out. While the key is blocked, other requests through the same client, from any thread, and through its `with_options()` copies wait for the block to end instead of being sent into it, or raise `RateLimitError` at once if the rest of the block is longer than `max_rate_limit_wait`. When the block ends, all waiting requests are sent together, so with many threads the client may exceed the limit again; it does not pace requests. A request whose upload cannot be read again (a stream that cannot seek back) is not retried. This applies only to clients with `rate_limit_retries` set: a copy made with `rate_limit_retries=0`, like a plain client, sends every request. Clients created separately, and other processes using the same key, do not know about the block.
|
|
169
|
+
|
|
170
|
+
How Blablador states the wait in a 429 has not been confirmed yet (no real 429 has been recorded, since that would block the key). If it uses a form the client does not recognise, `retry_after` is `None` and `rate_limit_retries` raises instead of waiting.
|
|
171
|
+
|
|
172
|
+
## Command line
|
|
173
|
+
|
|
174
|
+
The `blablador` command covers the quick uses: see which models are there, ask one question, transcribe a recording. It reads the API key from `BLABLADOR_API_KEY`, like the client, and every command sends one request. `python -m blablador` works too, `blablador COMMAND --help` lists the options, and `blablador --version` prints the versions of `blablador` and `openai` for bug reports.
|
|
175
|
+
|
|
176
|
+
```bash
|
|
177
|
+
blablador models # aliases, context length, the model behind each
|
|
178
|
+
blablador models --all --json # every model, as JSON
|
|
179
|
+
blablador chat "What is JSC?" # streams the answer
|
|
180
|
+
cat notes.txt | blablador chat "Summarize these notes" # piped text follows the prompt
|
|
181
|
+
blablador chat --reasoning -m alias-code "Is 91 prime?" # reasoning on stderr, answer on stdout
|
|
182
|
+
blablador transcribe meeting.m4a -l en # speech to text
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
### `blablador models`
|
|
186
|
+
|
|
187
|
+
Lists the `alias-*` names with their context length and the model behind each, aliases only unless `--all` is given.
|
|
188
|
+
|
|
189
|
+
| Option | Meaning |
|
|
190
|
+
|---|---|
|
|
191
|
+
| `--all` | also list the full model names, after the aliases |
|
|
192
|
+
| `--json` | print a JSON list with `id`, `root`, `max_model_len`, `owned_by` and `client_count` per model, for scripts |
|
|
193
|
+
|
|
194
|
+
### `blablador chat [PROMPT ...]`
|
|
195
|
+
|
|
196
|
+
Sends one prompt and streams the answer to stdout. The words of the prompt need no quotes. When stdin is not a terminal, it is read to its end and appended after the prompt, separated by a blank line, so `PROMPT` can be left out when the whole prompt is piped. There is no conversation: each call is a new chat.
|
|
197
|
+
|
|
198
|
+
Under `srun`, or from a script that starts `blablador` with an open stdin, nothing may ever close stdin, and the command would wait for it. After two seconds it says so on stderr; pass `--no-stdin` (or `< /dev/null`) there.
|
|
199
|
+
|
|
200
|
+
| Option | Meaning |
|
|
201
|
+
|---|---|
|
|
202
|
+
| `-m`, `--model MODEL` | model or alias; default `$BLABLADOR_MODEL`, else `alias-fast` |
|
|
203
|
+
| `-s`, `--system TEXT` | system prompt |
|
|
204
|
+
| `--reasoning` | also print the model's reasoning, to stderr, so that `> answer.txt` keeps only the answer |
|
|
205
|
+
| `--no-stream` | wait for the complete answer and print it at once |
|
|
206
|
+
| `--no-stdin` | do not read stdin, even when it is not a terminal |
|
|
207
|
+
|
|
208
|
+
### `blablador transcribe FILE`
|
|
209
|
+
|
|
210
|
+
Prints the text of an audio file (wav, mp3, m4a, ...). Blablador has no `alias-*` name for speech to text, so the default names the model itself; if it is renamed, `blablador models --all` shows the current one.
|
|
211
|
+
|
|
212
|
+
| Option | Meaning |
|
|
213
|
+
|---|---|
|
|
214
|
+
| `-m`, `--model MODEL` | speech-to-text model; default `faster-whisper-large-v3` |
|
|
215
|
+
| `-l`, `--language CODE` | language of the audio, e.g. `en` or `de`; optional |
|
|
216
|
+
|
|
217
|
+
### Exit status and errors
|
|
218
|
+
|
|
219
|
+
The exit status is 0 on success, 1 for a missing API key or an error from the API, 2 for wrong usage (unknown option, missing prompt or file) and 130 after Ctrl-C. Errors are printed to stderr without a traceback, with the same hints as the client (for example, that the key may have expired). The command does not retry rate-limited requests: a 429 is reported, with the wait if the server stated one.
|
|
220
|
+
|
|
221
|
+
## Examples
|
|
222
|
+
|
|
223
|
+
- [`examples/quickstart.py`](https://gitlab.jsc.fz-juelich.de/sdlaml/agents-on-hpc/blablador/-/blob/main/examples/quickstart.py): models, chat, streaming, tool calls, rerank and the async client, as sections you can run one at a time: `python examples/quickstart.py [models|chat|stream|tools|rerank|async ...]` (with uv: `uv run python examples/quickstart.py ...`)
|
|
224
|
+
- [`examples/agent_loop.py`](https://gitlab.jsc.fz-juelich.de/sdlaml/agents-on-hpc/blablador/-/blob/main/examples/agent_loop.py): a minimal coding agent, one `bash` tool and a loop. It runs the shell commands the model picks without any checks, so use a throwaway directory.
|
|
225
|
+
|
|
226
|
+
## Configuration
|
|
227
|
+
|
|
228
|
+
| Variable | Default |
|
|
229
|
+
|---|---|
|
|
230
|
+
| `BLABLADOR_API_KEY` | required: a [Helmholtz Codebase token](https://codebase.helmholtz.cloud/-/user_settings/personal_access_tokens) with `read_user` scope |
|
|
231
|
+
| `BLABLADOR_BASE_URL` | `https://api.blablador.fz-juelich.de/v1` |
|
|
232
|
+
| `BLABLADOR_MODEL` | `alias-fast`: the model `blablador chat` uses without `-m` |
|
|
233
|
+
|
|
234
|
+
Arguments (`Blablador(api_key=..., base_url=...)`) take precedence over the variables. `OPENAI_*` variables are ignored, so an OpenAI setup in the same shell does not send its key or headers to Blablador. When the key is missing or rejected, the error says where to get a new one.
|
|
235
|
+
|
|
236
|
+
## Development
|
|
237
|
+
|
|
238
|
+
In a virtual environment, with pip 25.1 or newer (for `--group`):
|
|
239
|
+
|
|
240
|
+
```bash
|
|
241
|
+
pip install -e . --group dev
|
|
242
|
+
pytest # offline tests, replaying recorded responses in tests/recordings/
|
|
243
|
+
pytest -m live # a few requests against the real API (needs BLABLADOR_API_KEY)
|
|
244
|
+
ruff check . && ruff format --check .
|
|
245
|
+
python -m tests.record [SCENARIO ...] # re-record tests/recordings/ from the live API
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
With uv, `uv sync --group dev` sets up the environment, and each command runs with `uv run` in front (`uv run pytest`).
|
|
249
|
+
|
|
250
|
+
GitLab CI runs ruff and the offline tests on Python 3.10 and 3.15, and installs the built wheel, for every push, merge request and tag; live tests run only locally. Blablador's rate limits are tight and repeated violations block the key for longer each time, so keep live tests and re-recordings to what you need. The recorder strips credentials and internal server paths; check the diff of `tests/recordings/` before committing.
|
|
251
|
+
|
|
252
|
+
Planned and finished work is tracked with [tatr](https://github.com/tsoding/tatr) in `tasks/`.
|
|
253
|
+
|
|
254
|
+
## License
|
|
255
|
+
|
|
256
|
+
MIT, see [LICENSE](https://gitlab.jsc.fz-juelich.de/sdlaml/agents-on-hpc/blablador/-/blob/main/LICENSE).
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
# blablador
|
|
2
|
+
|
|
3
|
+
Python client and command-line tool for [Blablador](https://sdlaml.pages.jsc.fz-juelich.de/ai/guides/blablador_api_access), the LLM service of the Jülich Supercomputing Centre.
|
|
4
|
+
|
|
5
|
+
Blablador's API is OpenAI-compatible, so this package is a thin layer over the official [`openai` SDK](https://github.com/openai/openai-python): everything the SDK does works unchanged, and the package adds what Blablador needs on top:
|
|
6
|
+
|
|
7
|
+
- clients preconfigured for Blablador (`Blablador`, `AsyncBlablador`)
|
|
8
|
+
- typed model listing with Blablador's fields and the current `alias-*` names
|
|
9
|
+
- the model's reasoning, whichever backend serves it (`blablador.reasoning()`)
|
|
10
|
+
- the rerank endpoint, which the OpenAI API does not have (`client.rerank`)
|
|
11
|
+
- fixes for requests the gateway rejects but the OpenAI API allows
|
|
12
|
+
- rate-limit handling that does not extend Blablador's blocks: no retry of HTTP 429 by default, and an opt-in wait for the time the server states
|
|
13
|
+
- a `blablador` command: `models`, `chat`, `transcribe`
|
|
14
|
+
|
|
15
|
+
Status: early. Requires Python 3.10 or newer. The client overrides internals of the `openai` SDK, so each release pins one minor version of `openai` (0.2.0: `openai>=3.42,<3.43`); the pin is raised once the tests pass with a newer release.
|
|
16
|
+
|
|
17
|
+
## Install
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
pip install blablador
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
This installs the library and the `blablador` command. The development version installs from the [repository](https://gitlab.jsc.fz-juelich.de/sdlaml/agents-on-hpc/blablador): `pip install "git+https://gitlab.jsc.fz-juelich.de/sdlaml/agents-on-hpc/blablador.git"`.
|
|
24
|
+
|
|
25
|
+
If you use [uv](https://docs.astral.sh/uv/), add it to your project, or install only the command, available everywhere:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
uv add blablador
|
|
29
|
+
uv tool install blablador
|
|
30
|
+
uv add "blablador @ git+https://gitlab.jsc.fz-juelich.de/sdlaml/agents-on-hpc/blablador.git" # development version
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Getting started
|
|
34
|
+
|
|
35
|
+
You need an API key: a [Helmholtz Codebase token](https://codebase.helmholtz.cloud/-/user_settings/personal_access_tokens) with the `read_user` scope (sign in with your institution's account). Tokens are valid for at most a year. Put it in the environment:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
export BLABLADOR_API_KEY=...
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Then use `Blablador` like `openai.OpenAI`, which it is a subclass of:
|
|
42
|
+
|
|
43
|
+
```python
|
|
44
|
+
from blablador import Blablador
|
|
45
|
+
|
|
46
|
+
client = Blablador() # reads BLABLADOR_API_KEY
|
|
47
|
+
completion = client.chat.completions.create(
|
|
48
|
+
model="alias-fast",
|
|
49
|
+
messages=[{"role": "user", "content": "Hello!"}],
|
|
50
|
+
)
|
|
51
|
+
print(completion.choices[0].message.content)
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Streaming, the Responses API, tool calls, embeddings, audio transcription and `with_options()` work as the [OpenAI SDK documentation](https://github.com/openai/openai-python) describes. `AsyncBlablador` is the same for `openai.AsyncOpenAI`.
|
|
55
|
+
|
|
56
|
+
From the shell:
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
blablador chat "What is the Jülich Supercomputing Centre?"
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## What the package adds
|
|
63
|
+
|
|
64
|
+
### Models and aliases
|
|
65
|
+
|
|
66
|
+
Blablador's models change. The `alias-*` names (`alias-fast`, `alias-code`, `alias-large`, ...) stay, and are pointed at newer models as these replace older ones, so use aliases in code and read what is behind them at runtime rather than hardcoding a table.
|
|
67
|
+
|
|
68
|
+
`client.models.list()` returns `blablador.BlabladorModel`s, which add Blablador's fields to the OpenAI ones:
|
|
69
|
+
|
|
70
|
+
| Field | Meaning |
|
|
71
|
+
|---|---|
|
|
72
|
+
| `root` | the underlying model: a Hugging Face name, a path on the server, or `None` |
|
|
73
|
+
| `max_model_len` | context length in tokens, if reported |
|
|
74
|
+
| `client_count` | number of servers currently serving the model, if reported |
|
|
75
|
+
| `is_alias` | `True` for the `alias-*` names |
|
|
76
|
+
|
|
77
|
+
```python
|
|
78
|
+
for model in client.models.list():
|
|
79
|
+
if model.is_alias:
|
|
80
|
+
print(model.id, model.root, model.max_model_len)
|
|
81
|
+
|
|
82
|
+
client.models.aliases() # {"alias-fast": "openai/gpt-oss-120b", ...}, one request
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
In chat completions, the response's `model` field holds the model's display name ("01 - GPT-OSS-120b - ..."), not the alias that was sent; embeddings responses name the alias. `client.models.retrieve()` does not work: Blablador answers 404 for a single model. The listing does not say what a model can do (chat, embeddings, rerank, speech); the names are the only hint.
|
|
86
|
+
|
|
87
|
+
### Reasoning
|
|
88
|
+
|
|
89
|
+
Reasoning models return their reasoning next to the answer, in a field the OpenAI API does not define and whose name depends on the backend (`reasoning` from vLLM, `reasoning_content` from sglang). `blablador.reasoning()` returns it from a message or a streamed delta, or `None`:
|
|
90
|
+
|
|
91
|
+
```python
|
|
92
|
+
import blablador
|
|
93
|
+
|
|
94
|
+
message = completion.choices[0].message
|
|
95
|
+
print(blablador.reasoning(message)) # how the model got there
|
|
96
|
+
print(message.content) # the answer
|
|
97
|
+
|
|
98
|
+
for chunk in client.chat.completions.create(..., stream=True):
|
|
99
|
+
if chunk.choices:
|
|
100
|
+
print(blablador.reasoning(chunk.choices[0].delta) or "", end="")
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### Rerank
|
|
104
|
+
|
|
105
|
+
`client.rerank.create()` scores documents by relevance to a query (Blablador's `/v1/rerank`), best first. Use it to pick the best few of many candidates, for example after an embedding search:
|
|
106
|
+
|
|
107
|
+
```python
|
|
108
|
+
texts = [
|
|
109
|
+
"sbatch submits a batch script to Slurm",
|
|
110
|
+
"JURECA has A100 GPUs",
|
|
111
|
+
"The cafeteria opens at 11:30",
|
|
112
|
+
]
|
|
113
|
+
ranked = client.rerank.create(
|
|
114
|
+
model="alias-qwen3-8b-reranking",
|
|
115
|
+
query="How do I submit a job on JURECA?",
|
|
116
|
+
documents=texts,
|
|
117
|
+
top_n=2, # optional: only the best two
|
|
118
|
+
)
|
|
119
|
+
for result in ranked.results: # sorted by relevance_score, highest first
|
|
120
|
+
print(f"{result.relevance_score:.3f} {texts[result.index]}")
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
`return_documents=True` adds each document's text to its result. `AsyncBlablador` has the same method as a coroutine.
|
|
124
|
+
|
|
125
|
+
### Requests the gateway rejects
|
|
126
|
+
|
|
127
|
+
Where the gateway is stricter than the OpenAI API, the client adjusts the request so that code written for OpenAI keeps working:
|
|
128
|
+
|
|
129
|
+
- Function tools without a `description` (optional for OpenAI, HTTP 400 from Blablador) are sent with `"description": ""`, for chat completions and the Responses API.
|
|
130
|
+
|
|
131
|
+
### Rate limits
|
|
132
|
+
|
|
133
|
+
Blablador limits requests per model and user, and blocks a key that exceeds the limit: 60 s for the first violation, doubling with each further one up to an hour. The OpenAI SDK would retry an HTTP 429 at once, which risks a longer block, so this client never retries 429 by default (timeouts, 5xx and connection errors are still retried, `max_retries` times). Instead it raises `blablador.RateLimitError`, a subclass of `openai.RateLimitError`, whose `retry_after` holds the wait in seconds if the server stated one, in a `Retry-After` header or in the error message ("try again in 57 seconds"):
|
|
134
|
+
|
|
135
|
+
```python
|
|
136
|
+
import time
|
|
137
|
+
import blablador
|
|
138
|
+
|
|
139
|
+
try:
|
|
140
|
+
completion = client.chat.completions.create(...)
|
|
141
|
+
except blablador.RateLimitError as err:
|
|
142
|
+
time.sleep(err.retry_after or 60) # then try again, once
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
For batch jobs, the client can do the waiting itself:
|
|
146
|
+
|
|
147
|
+
```python
|
|
148
|
+
client = blablador.Blablador(rate_limit_retries=3, max_rate_limit_wait=600)
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
A request that gets a 429 then waits the stated time (plus a second) and is sent again, up to `rate_limit_retries` times. It is not retried, and the error is raised, when the server did not say how long to wait, since a guessed wait that ends too early may extend the block, or when the wait is longer than `max_rate_limit_wait` seconds (default 3600). The client records every stated block, also one it does not wait out. While the key is blocked, other requests through the same client, from any thread, and through its `with_options()` copies wait for the block to end instead of being sent into it, or raise `RateLimitError` at once if the rest of the block is longer than `max_rate_limit_wait`. When the block ends, all waiting requests are sent together, so with many threads the client may exceed the limit again; it does not pace requests. A request whose upload cannot be read again (a stream that cannot seek back) is not retried. This applies only to clients with `rate_limit_retries` set: a copy made with `rate_limit_retries=0`, like a plain client, sends every request. Clients created separately, and other processes using the same key, do not know about the block.
|
|
152
|
+
|
|
153
|
+
How Blablador states the wait in a 429 has not been confirmed yet (no real 429 has been recorded, since that would block the key). If it uses a form the client does not recognise, `retry_after` is `None` and `rate_limit_retries` raises instead of waiting.
|
|
154
|
+
|
|
155
|
+
## Command line
|
|
156
|
+
|
|
157
|
+
The `blablador` command covers the quick uses: see which models are there, ask one question, transcribe a recording. It reads the API key from `BLABLADOR_API_KEY`, like the client, and every command sends one request. `python -m blablador` works too, `blablador COMMAND --help` lists the options, and `blablador --version` prints the versions of `blablador` and `openai` for bug reports.
|
|
158
|
+
|
|
159
|
+
```bash
|
|
160
|
+
blablador models # aliases, context length, the model behind each
|
|
161
|
+
blablador models --all --json # every model, as JSON
|
|
162
|
+
blablador chat "What is JSC?" # streams the answer
|
|
163
|
+
cat notes.txt | blablador chat "Summarize these notes" # piped text follows the prompt
|
|
164
|
+
blablador chat --reasoning -m alias-code "Is 91 prime?" # reasoning on stderr, answer on stdout
|
|
165
|
+
blablador transcribe meeting.m4a -l en # speech to text
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
### `blablador models`
|
|
169
|
+
|
|
170
|
+
Lists the `alias-*` names with their context length and the model behind each, aliases only unless `--all` is given.
|
|
171
|
+
|
|
172
|
+
| Option | Meaning |
|
|
173
|
+
|---|---|
|
|
174
|
+
| `--all` | also list the full model names, after the aliases |
|
|
175
|
+
| `--json` | print a JSON list with `id`, `root`, `max_model_len`, `owned_by` and `client_count` per model, for scripts |
|
|
176
|
+
|
|
177
|
+
### `blablador chat [PROMPT ...]`
|
|
178
|
+
|
|
179
|
+
Sends one prompt and streams the answer to stdout. The words of the prompt need no quotes. When stdin is not a terminal, it is read to its end and appended after the prompt, separated by a blank line, so `PROMPT` can be left out when the whole prompt is piped. There is no conversation: each call is a new chat.
|
|
180
|
+
|
|
181
|
+
Under `srun`, or from a script that starts `blablador` with an open stdin, nothing may ever close stdin, and the command would wait for it. After two seconds it says so on stderr; pass `--no-stdin` (or `< /dev/null`) there.
|
|
182
|
+
|
|
183
|
+
| Option | Meaning |
|
|
184
|
+
|---|---|
|
|
185
|
+
| `-m`, `--model MODEL` | model or alias; default `$BLABLADOR_MODEL`, else `alias-fast` |
|
|
186
|
+
| `-s`, `--system TEXT` | system prompt |
|
|
187
|
+
| `--reasoning` | also print the model's reasoning, to stderr, so that `> answer.txt` keeps only the answer |
|
|
188
|
+
| `--no-stream` | wait for the complete answer and print it at once |
|
|
189
|
+
| `--no-stdin` | do not read stdin, even when it is not a terminal |
|
|
190
|
+
|
|
191
|
+
### `blablador transcribe FILE`
|
|
192
|
+
|
|
193
|
+
Prints the text of an audio file (wav, mp3, m4a, ...). Blablador has no `alias-*` name for speech to text, so the default names the model itself; if it is renamed, `blablador models --all` shows the current one.
|
|
194
|
+
|
|
195
|
+
| Option | Meaning |
|
|
196
|
+
|---|---|
|
|
197
|
+
| `-m`, `--model MODEL` | speech-to-text model; default `faster-whisper-large-v3` |
|
|
198
|
+
| `-l`, `--language CODE` | language of the audio, e.g. `en` or `de`; optional |
|
|
199
|
+
|
|
200
|
+
### Exit status and errors
|
|
201
|
+
|
|
202
|
+
The exit status is 0 on success, 1 for a missing API key or an error from the API, 2 for wrong usage (unknown option, missing prompt or file) and 130 after Ctrl-C. Errors are printed to stderr without a traceback, with the same hints as the client (for example, that the key may have expired). The command does not retry rate-limited requests: a 429 is reported, with the wait if the server stated one.
|
|
203
|
+
|
|
204
|
+
## Examples
|
|
205
|
+
|
|
206
|
+
- [`examples/quickstart.py`](https://gitlab.jsc.fz-juelich.de/sdlaml/agents-on-hpc/blablador/-/blob/main/examples/quickstart.py): models, chat, streaming, tool calls, rerank and the async client, as sections you can run one at a time: `python examples/quickstart.py [models|chat|stream|tools|rerank|async ...]` (with uv: `uv run python examples/quickstart.py ...`)
|
|
207
|
+
- [`examples/agent_loop.py`](https://gitlab.jsc.fz-juelich.de/sdlaml/agents-on-hpc/blablador/-/blob/main/examples/agent_loop.py): a minimal coding agent, one `bash` tool and a loop. It runs the shell commands the model picks without any checks, so use a throwaway directory.
|
|
208
|
+
|
|
209
|
+
## Configuration
|
|
210
|
+
|
|
211
|
+
| Variable | Default |
|
|
212
|
+
|---|---|
|
|
213
|
+
| `BLABLADOR_API_KEY` | required: a [Helmholtz Codebase token](https://codebase.helmholtz.cloud/-/user_settings/personal_access_tokens) with `read_user` scope |
|
|
214
|
+
| `BLABLADOR_BASE_URL` | `https://api.blablador.fz-juelich.de/v1` |
|
|
215
|
+
| `BLABLADOR_MODEL` | `alias-fast`: the model `blablador chat` uses without `-m` |
|
|
216
|
+
|
|
217
|
+
Arguments (`Blablador(api_key=..., base_url=...)`) take precedence over the variables. `OPENAI_*` variables are ignored, so an OpenAI setup in the same shell does not send its key or headers to Blablador. When the key is missing or rejected, the error says where to get a new one.
|
|
218
|
+
|
|
219
|
+
## Development
|
|
220
|
+
|
|
221
|
+
In a virtual environment, with pip 25.1 or newer (for `--group`):
|
|
222
|
+
|
|
223
|
+
```bash
|
|
224
|
+
pip install -e . --group dev
|
|
225
|
+
pytest # offline tests, replaying recorded responses in tests/recordings/
|
|
226
|
+
pytest -m live # a few requests against the real API (needs BLABLADOR_API_KEY)
|
|
227
|
+
ruff check . && ruff format --check .
|
|
228
|
+
python -m tests.record [SCENARIO ...] # re-record tests/recordings/ from the live API
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
With uv, `uv sync --group dev` sets up the environment, and each command runs with `uv run` in front (`uv run pytest`).
|
|
232
|
+
|
|
233
|
+
GitLab CI runs ruff and the offline tests on Python 3.10 and 3.15, and installs the built wheel, for every push, merge request and tag; live tests run only locally. Blablador's rate limits are tight and repeated violations block the key for longer each time, so keep live tests and re-recordings to what you need. The recorder strips credentials and internal server paths; check the diff of `tests/recordings/` before committing.
|
|
234
|
+
|
|
235
|
+
Planned and finished work is tracked with [tatr](https://github.com/tsoding/tatr) in `tasks/`.
|
|
236
|
+
|
|
237
|
+
## License
|
|
238
|
+
|
|
239
|
+
MIT, see [LICENSE](https://gitlab.jsc.fz-juelich.de/sdlaml/agents-on-hpc/blablador/-/blob/main/LICENSE).
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "blablador"
|
|
3
|
+
version = "0.2.0"
|
|
4
|
+
description = "Python client for JSC's Blablador LLM API, built on the OpenAI SDK"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.10"
|
|
7
|
+
license = "MIT"
|
|
8
|
+
license-files = ["LICENSE"]
|
|
9
|
+
dependencies = [
|
|
10
|
+
"openai>=3.42,<3.43",
|
|
11
|
+
"anyio>=4.10,<5",
|
|
12
|
+
"httpx2>=2.12,<3",
|
|
13
|
+
"typing_extensions>=4.14,<5",
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
[[project.authors]]
|
|
17
|
+
name = "Applied Machine Learning Lab at JSC"
|
|
18
|
+
email = "ai-support@fz-juelich.de"
|
|
19
|
+
|
|
20
|
+
[project.urls]
|
|
21
|
+
Homepage = "https://gitlab.jsc.fz-juelich.de/sdlaml/agents-on-hpc/blablador"
|
|
22
|
+
Repository = "https://gitlab.jsc.fz-juelich.de/sdlaml/agents-on-hpc/blablador.git"
|
|
23
|
+
|
|
24
|
+
[project.scripts]
|
|
25
|
+
blablador = "blablador.cli:main"
|
|
26
|
+
|
|
27
|
+
[dependency-groups]
|
|
28
|
+
dev = [
|
|
29
|
+
"pytest>=8",
|
|
30
|
+
"ruff>=0.8",
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
[build-system]
|
|
34
|
+
requires = ["uv_build>=0.13.6,<0.14"]
|
|
35
|
+
build-backend = "uv_build"
|
|
36
|
+
|
|
37
|
+
[tool.pytest.ini_options]
|
|
38
|
+
testpaths = ["tests"]
|
|
39
|
+
markers = ["live: calls the real Blablador API (uses BLABLADOR_API_KEY and rate limit)"]
|
|
40
|
+
addopts = "-m 'not live'"
|
|
41
|
+
|
|
42
|
+
[tool.ruff]
|
|
43
|
+
line-length = 100
|
|
44
|
+
target-version = "py310"
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "blablador"
|
|
3
|
+
version = "0.2.0"
|
|
4
|
+
description = "Python client for JSC's Blablador LLM API, built on the OpenAI SDK"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.10"
|
|
7
|
+
license = "MIT"
|
|
8
|
+
license-files = ["LICENSE"]
|
|
9
|
+
authors = [{ name = "Applied Machine Learning Lab at JSC", email = "ai-support@fz-juelich.de" }]
|
|
10
|
+
# openai is pinned to one minor version: the client overrides SDK internals
|
|
11
|
+
# (`_client.py`), which a minor release may change. Raise the pin after the scheduled
|
|
12
|
+
# `openai-latest` CI job passes with the new release.
|
|
13
|
+
# anyio, httpx2 and typing_extensions are imported directly; the ranges are openai 3.42's own.
|
|
14
|
+
dependencies = [
|
|
15
|
+
"openai>=3.42,<3.43",
|
|
16
|
+
"anyio>=4.10,<5",
|
|
17
|
+
"httpx2>=2.12,<3",
|
|
18
|
+
"typing_extensions>=4.14,<5",
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
[project.urls]
|
|
22
|
+
Homepage = "https://gitlab.jsc.fz-juelich.de/sdlaml/agents-on-hpc/blablador"
|
|
23
|
+
Repository = "https://gitlab.jsc.fz-juelich.de/sdlaml/agents-on-hpc/blablador.git"
|
|
24
|
+
|
|
25
|
+
[project.scripts]
|
|
26
|
+
blablador = "blablador.cli:main"
|
|
27
|
+
|
|
28
|
+
[dependency-groups]
|
|
29
|
+
dev = ["pytest>=8", "ruff>=0.8"]
|
|
30
|
+
|
|
31
|
+
[build-system]
|
|
32
|
+
requires = ["uv_build>=0.13.6,<0.14"]
|
|
33
|
+
build-backend = "uv_build"
|
|
34
|
+
|
|
35
|
+
[tool.pytest.ini_options]
|
|
36
|
+
testpaths = ["tests"]
|
|
37
|
+
markers = ["live: calls the real Blablador API (uses BLABLADOR_API_KEY and rate limit)"]
|
|
38
|
+
addopts = "-m 'not live'"
|
|
39
|
+
|
|
40
|
+
[tool.ruff]
|
|
41
|
+
line-length = 100
|
|
42
|
+
target-version = "py310"
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""Python client for JSC's Blablador LLM API."""
|
|
2
|
+
|
|
3
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
4
|
+
|
|
5
|
+
from ._client import API_KEY_ENV, BASE_URL_ENV, DEFAULT_BASE_URL, AsyncBlablador, Blablador
|
|
6
|
+
from ._errors import RateLimitError
|
|
7
|
+
from ._models import BlabladorModel
|
|
8
|
+
from ._reasoning import reasoning
|
|
9
|
+
from ._rerank import RerankResponse, RerankResult, RerankUsage
|
|
10
|
+
|
|
11
|
+
try:
|
|
12
|
+
__version__ = version("blablador")
|
|
13
|
+
except PackageNotFoundError: # running from a source tree that is not installed
|
|
14
|
+
__version__ = "0+unknown"
|
|
15
|
+
|
|
16
|
+
__all__ = [
|
|
17
|
+
"API_KEY_ENV",
|
|
18
|
+
"BASE_URL_ENV",
|
|
19
|
+
"DEFAULT_BASE_URL",
|
|
20
|
+
"AsyncBlablador",
|
|
21
|
+
"Blablador",
|
|
22
|
+
"BlabladorModel",
|
|
23
|
+
"RateLimitError",
|
|
24
|
+
"RerankResponse",
|
|
25
|
+
"RerankResult",
|
|
26
|
+
"RerankUsage",
|
|
27
|
+
"__version__",
|
|
28
|
+
"reasoning",
|
|
29
|
+
]
|