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.
@@ -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.
@@ -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
+ ]
@@ -0,0 +1,7 @@
1
+ """`python -m blablador` runs the command-line tool."""
2
+
3
+ import sys
4
+
5
+ from .cli import main
6
+
7
+ sys.exit(main())