poolprobe 1.0.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.
- poolprobe-1.0.0/LICENSE +21 -0
- poolprobe-1.0.0/PKG-INFO +259 -0
- poolprobe-1.0.0/README.md +209 -0
- poolprobe-1.0.0/poolprobe/__init__.py +137 -0
- poolprobe-1.0.0/poolprobe/__main__.py +8 -0
- poolprobe-1.0.0/poolprobe/adapter.py +525 -0
- poolprobe-1.0.0/poolprobe/bench.py +368 -0
- poolprobe-1.0.0/poolprobe/chains.py +414 -0
- poolprobe-1.0.0/poolprobe/cli.py +1008 -0
- poolprobe-1.0.0/poolprobe/probe.py +352 -0
- poolprobe-1.0.0/poolprobe/report.py +1096 -0
- poolprobe-1.0.0/poolprobe/safewrite.py +66 -0
- poolprobe-1.0.0/poolprobe/state.py +272 -0
- poolprobe-1.0.0/poolprobe/taxonomy.py +312 -0
- poolprobe-1.0.0/poolprobe.egg-info/PKG-INFO +259 -0
- poolprobe-1.0.0/poolprobe.egg-info/SOURCES.txt +20 -0
- poolprobe-1.0.0/poolprobe.egg-info/dependency_links.txt +1 -0
- poolprobe-1.0.0/poolprobe.egg-info/entry_points.txt +2 -0
- poolprobe-1.0.0/poolprobe.egg-info/requires.txt +3 -0
- poolprobe-1.0.0/poolprobe.egg-info/top_level.txt +1 -0
- poolprobe-1.0.0/pyproject.toml +44 -0
- poolprobe-1.0.0/setup.cfg +4 -0
poolprobe-1.0.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 ameobius
|
|
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.
|
poolprobe-1.0.0/PKG-INFO
ADDED
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: poolprobe
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: A doctor for OpenAI-compatible LLM gateways: probe the pool, classify every failure, and only then talk about pruning.
|
|
5
|
+
License: MIT License
|
|
6
|
+
|
|
7
|
+
Copyright (c) 2026 ameobius
|
|
8
|
+
|
|
9
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
10
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
11
|
+
in the Software without restriction, including without limitation the rights
|
|
12
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
13
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
14
|
+
furnished to do so, subject to the following conditions:
|
|
15
|
+
|
|
16
|
+
The above copyright notice and this permission notice shall be included in all
|
|
17
|
+
copies or substantial portions of the Software.
|
|
18
|
+
|
|
19
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
20
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
21
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
22
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
23
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
24
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
25
|
+
SOFTWARE.
|
|
26
|
+
|
|
27
|
+
Project-URL: Homepage, https://gitlab.com/ameobius-ai/poolprobe
|
|
28
|
+
Project-URL: Source, https://gitlab.com/ameobius-ai/poolprobe
|
|
29
|
+
Project-URL: Issues, https://gitlab.com/ameobius-ai/poolprobe/-/issues
|
|
30
|
+
Keywords: llm,gateway,openai,cliproxy,healthcheck,ops,fallback
|
|
31
|
+
Classifier: Development Status :: 4 - Beta
|
|
32
|
+
Classifier: Environment :: Console
|
|
33
|
+
Classifier: Intended Audience :: Developers
|
|
34
|
+
Classifier: Intended Audience :: System Administrators
|
|
35
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
36
|
+
Classifier: Operating System :: OS Independent
|
|
37
|
+
Classifier: Programming Language :: Python :: 3
|
|
38
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
39
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
40
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
41
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
42
|
+
Classifier: Topic :: System :: Monitoring
|
|
43
|
+
Classifier: Topic :: System :: Systems Administration
|
|
44
|
+
Requires-Python: >=3.10
|
|
45
|
+
Description-Content-Type: text/markdown
|
|
46
|
+
License-File: LICENSE
|
|
47
|
+
Provides-Extra: cliproxy
|
|
48
|
+
Requires-Dist: PyYAML>=6.0; extra == "cliproxy"
|
|
49
|
+
Dynamic: license-file
|
|
50
|
+
|
|
51
|
+
<div align="center">
|
|
52
|
+
|
|
53
|
+
# poolprobe
|
|
54
|
+
|
|
55
|
+
**A doctor for OpenAI-compatible LLM gateways.**
|
|
56
|
+
Probe the pool, classify every failure, and only *then* talk about pruning.
|
|
57
|
+
|
|
58
|
+
[](https://www.python.org/downloads/)
|
|
59
|
+
[](#install)
|
|
60
|
+
[](LICENSE)
|
|
61
|
+
|
|
62
|
+
</div>
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
A pool of routed models rots quietly. An upstream drops a model, a key hits an
|
|
67
|
+
account tier, a reasoning model starts spending its whole token budget on
|
|
68
|
+
thinking, a gateway route begins answering SSE to a non-streaming request. None
|
|
69
|
+
of that raises an exception anywhere. The agent just gets an empty reply, or a
|
|
70
|
+
slower one, or falls through to a worse model — and nobody notices for a week.
|
|
71
|
+
|
|
72
|
+
`poolprobe` looks at every model in the pool, tells you *why* each one is
|
|
73
|
+
failing, and refuses to delete anything until five separate guards agree it is
|
|
74
|
+
dead. It grew out of running a live [cliproxy] pool on a schedule and learning,
|
|
75
|
+
the hard way, how many "dead model" verdicts were really weather.
|
|
76
|
+
|
|
77
|
+
## The three rules
|
|
78
|
+
|
|
79
|
+
Everything in the tool obeys these. They are the reason it exists.
|
|
80
|
+
|
|
81
|
+
1. **A verdict is not a death sentence.** Every failure is classified into a
|
|
82
|
+
kind — `ALIVE`, `ALIVE-STARVED`, `PERMANENT`, `TRANSIENT`, `CREDENTIAL`,
|
|
83
|
+
`MODALITY`, `UNKNOWN`. Only `PERMANENT` (the upstream genuinely does not know
|
|
84
|
+
this model) is ever a prune candidate. A 401 is a dead *key*, not a dead
|
|
85
|
+
*model*; pruning on it deletes a good alias because a credential ran dry.
|
|
86
|
+
|
|
87
|
+
2. **One observation is not a verdict.** A measured pool produced **544**
|
|
88
|
+
scheduled liveness runs over six weeks; **39%** went red, and *every single
|
|
89
|
+
red was transient*. `poolprobe` burst-confirms by default — five attempts in
|
|
90
|
+
one run, best answer wins — before it will call anything dead.
|
|
91
|
+
|
|
92
|
+
3. **A number without evidence is not reported.** Throughput comes from the
|
|
93
|
+
gateway's own `usage` block, or it is marked as a lower bound (`>=`), or it
|
|
94
|
+
renders as `-`. Never estimated from SSE chunk counts and dressed up as a
|
|
95
|
+
measurement.
|
|
96
|
+
|
|
97
|
+
## Install
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
pip install poolprobe # stdlib engine, generic gateway lane
|
|
101
|
+
pip install 'poolprobe[cliproxy]' # + PyYAML, to read a cliproxy-style config
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
No PyPI release yet? Install straight from the checkout:
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
git clone https://gitlab.com/ameobius-ai/poolprobe
|
|
108
|
+
cd poolprobe && pip install '.[cliproxy]'
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
The engine has **zero runtime dependencies**. PyYAML is optional and only needed
|
|
112
|
+
to attribute models to providers and to plan config edits from a gateway YAML.
|
|
113
|
+
|
|
114
|
+
## Quickstart
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
# 1. Is the gateway even up, and what does it publish? (no chat request sent)
|
|
118
|
+
poolprobe ping -u http://127.0.0.1:8317/v1 -k "$KEY"
|
|
119
|
+
|
|
120
|
+
# 2. Probe the whole pool, burst-confirmed, and keep a self-contained HTML report
|
|
121
|
+
poolprobe sweep -u http://127.0.0.1:8317/v1 -k "$KEY" --html out.html
|
|
122
|
+
|
|
123
|
+
# 3. Give it memory: fail streaks across runs are what make `prune` safe
|
|
124
|
+
poolprobe sweep -u ... -k "$KEY" --state ~/.cache/poolprobe.json
|
|
125
|
+
|
|
126
|
+
# 4. Only list what passed all five guards — nothing on disk changes without --apply
|
|
127
|
+
poolprobe prune -u ... -k "$KEY" --state ~/.cache/poolprobe.json \
|
|
128
|
+
--adapter cliproxy --config ~/.config/gateway.yaml
|
|
129
|
+
|
|
130
|
+
# 5. Validate agent fallback chains against the lane rules that actually read them
|
|
131
|
+
poolprobe chains ~/.hermes/config.yaml --cross-check -u ... -k "$KEY"
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
`-u` also reads `POOLPROBE_BASE_URL`; `-k` reads `POOLPROBE_API_KEY` then
|
|
135
|
+
`OPENAI_API_KEY`; `--state` reads `POOLPROBE_STATE`. The bearer key is masked in
|
|
136
|
+
every argv echo — reports are meant to be pasted into issues.
|
|
137
|
+
|
|
138
|
+
### What a sweep looks like
|
|
139
|
+
|
|
140
|
+
```
|
|
141
|
+
poolprobe 1.0.0 · sweep
|
|
142
|
+
http://127.0.0.1:8317/v1 · generic adapter (read-only)
|
|
143
|
+
|
|
144
|
+
5 models · 4 alive · 0 starved · 1 dead · 0 prunable · median 5.43s · run #1
|
|
145
|
+
|
|
146
|
+
VERDICT MODEL PROVIDER LAT BURST STREAK KIND NOTE
|
|
147
|
+
------------ ------------------- --------- ------- ----- ------ ---------- --------------------
|
|
148
|
+
OK gpt-5.4 - 2.74s ✓ ok1 - alive
|
|
149
|
+
OK gemini-3.8-flash - 6.12s ✓ ok1 - alive
|
|
150
|
+
OK grok-4.6 - 27.3s ✓ ok1 - alive
|
|
151
|
+
ERR suno-music - 0.13s ✗ x1 CREDENTIAL {"error":{"message"…
|
|
152
|
+
OK deepseek-v4-flash - 4.73s ✓ ok1 - alive
|
|
153
|
+
|
|
154
|
+
prunable: none — nothing met all five guards (dead, PERMANENT, streak, unprotected, chat modality)
|
|
155
|
+
|
|
156
|
+
what to do
|
|
157
|
+
CREDENTIAL (1): credential or account level: rotate/top up the key, never prune the model
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
`grok-4.6` answering `OK` here is the whole point: a prior single-shot check had
|
|
161
|
+
written it off as dead. The burst said otherwise.
|
|
162
|
+
|
|
163
|
+
## Commands
|
|
164
|
+
|
|
165
|
+
| Command | What it does | Sends a chat request? |
|
|
166
|
+
|---------|--------------|:---------------------:|
|
|
167
|
+
| `ping` | Reach the gateway, list the published catalog, break it down by provider. | no |
|
|
168
|
+
| `sweep [models…]` | Burst-confirmed liveness probe; one verdict + kind per model. | yes |
|
|
169
|
+
| `bench [models…]` | Latency and throughput; `--repeat N` reports the median, `--stream` measures time-to-first-token. | yes |
|
|
170
|
+
| `prune [models…]` | List models that passed all five guards and plan the config edit. `--apply` writes it (previous copy kept as `.bak`). | yes |
|
|
171
|
+
| `chains PATH` | Validate agent fallback chains against the lane rules that read them. `--cross-check` diffs rungs against the live catalog. | no |
|
|
172
|
+
| `selfcheck` | Offline assert suite — taxonomy, burst logic, prune guards, argv redaction, all three renderers. **This is what CI runs.** | no |
|
|
173
|
+
|
|
174
|
+
Common options: `--adapter {auto,cliproxy,generic}`, `--config PATH`,
|
|
175
|
+
`--threads N`, `--timeout S`, `--max-tokens N`, `--prompt TEXT`, `--state PATH`,
|
|
176
|
+
`--streak-min N`, `--protect MODEL` (repeatable), `--json PATH`, `--html PATH`,
|
|
177
|
+
`--color {auto,always,never}`, `-q/--quiet`.
|
|
178
|
+
|
|
179
|
+
`--max-tokens` is a **floor, not a cap**: a reasoning model spends it on thinking
|
|
180
|
+
and answers `EMPTY-LENGTH`, which `poolprobe` reads as *alive but starved*, never
|
|
181
|
+
as dead.
|
|
182
|
+
|
|
183
|
+
## Exit codes
|
|
184
|
+
|
|
185
|
+
A cron contract. Red is a finding, not noise.
|
|
186
|
+
|
|
187
|
+
| Code | Meaning |
|
|
188
|
+
|:----:|---------|
|
|
189
|
+
| `0` | Nothing dead — or nothing that met every guard. |
|
|
190
|
+
| `1` | Something dead (burst-confirmed), or a fallback chain is broken. |
|
|
191
|
+
| `2` | Usage error, unreadable config, or PyYAML missing for a config read. |
|
|
192
|
+
| `3` | The gateway could not be reached, or published an empty catalog. |
|
|
193
|
+
| `130` | Interrupted. |
|
|
194
|
+
|
|
195
|
+
## The five prune guards
|
|
196
|
+
|
|
197
|
+
`prune` will not name a model unless **all five** agree. A report that says
|
|
198
|
+
"prunable" while the prune cycle deletes something else is the worst outcome for
|
|
199
|
+
a tool like this, so the guards live in one function, checked in one place:
|
|
200
|
+
|
|
201
|
+
1. **dead verdict** — `EMPTY`, `ERR`, or `TIMEOUT` (`OK`/`EMPTY-LENGTH` never qualify);
|
|
202
|
+
2. **kind `PERMANENT`** — the upstream does not know this model;
|
|
203
|
+
3. **fail streak** — dead for at least `--streak-min` consecutive *runs* (needs `--state`);
|
|
204
|
+
4. **not protected** — absent from `--protect` (a namespaced alias's base name matches too);
|
|
205
|
+
5. **not a generation modality** — image/video/tts/music models legitimately fail a chat probe.
|
|
206
|
+
|
|
207
|
+
Without `--state` there is no memory, every streak reads `0`, and guard 3 can
|
|
208
|
+
never pass — so `prune` lists candidates but changes nothing. That is the safe
|
|
209
|
+
default.
|
|
210
|
+
|
|
211
|
+
## Reading a report
|
|
212
|
+
|
|
213
|
+
**Verdict** — what the probe saw:
|
|
214
|
+
|
|
215
|
+
`OK` · `EMPTY` (200, empty body) · `EMPTY-LENGTH` (empty because the token budget
|
|
216
|
+
ran out — alive) · `ERR` (non-2xx) · `TIMEOUT`.
|
|
217
|
+
|
|
218
|
+
**Kind** — what it means:
|
|
219
|
+
|
|
220
|
+
| Kind | Reading | Prunable? |
|
|
221
|
+
|------|---------|:---------:|
|
|
222
|
+
| `ALIVE` | Answered. | no |
|
|
223
|
+
| `ALIVE-STARVED` | Answered `EMPTY-LENGTH` — raise `--max-tokens`. | no |
|
|
224
|
+
| `PERMANENT` | Upstream does not know this model (`404`, "does not exist"). | **yes** |
|
|
225
|
+
| `TRANSIENT` | Timeout, `429`, `5xx` — weather. Burst-confirm before believing it. | no |
|
|
226
|
+
| `CREDENTIAL` | `401`/`403`, "invalid api key" — rotate the key, never the model. | no |
|
|
227
|
+
| `MODALITY` | Model lives on another endpoint (`/v1/images/…`), not chat. | no |
|
|
228
|
+
| `UNKNOWN` | None of the above; reported, never acted on. | no |
|
|
229
|
+
|
|
230
|
+
Three renderers, one report: the terminal table (ANSI, honours `NO_COLOR` /
|
|
231
|
+
`FORCE_COLOR` / `TERM=dumb` / isatty), a machine-readable `--json`, and a
|
|
232
|
+
self-contained `--html` you can attach to a post. They cannot disagree about
|
|
233
|
+
what a run recorded.
|
|
234
|
+
|
|
235
|
+
## Library use
|
|
236
|
+
|
|
237
|
+
```python
|
|
238
|
+
from poolprobe import Gateway, sweep, taxonomy
|
|
239
|
+
|
|
240
|
+
gw = Gateway("http://127.0.0.1:8317/v1", api_key="local")
|
|
241
|
+
for result in sweep(gw, ["gpt-5.4", "glm-5.3-flash"], attempts=5):
|
|
242
|
+
print(result.model, result.verdict, result.kind, result.advice)
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
## Development
|
|
246
|
+
|
|
247
|
+
```bash
|
|
248
|
+
python -m poolprobe selfcheck # the whole offline contract; CI runs exactly this
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
`selfcheck` needs no gateway, no network and no secrets. If it passes, the
|
|
252
|
+
taxonomy, the burst "best truth wins" logic, the five prune guards, the argv
|
|
253
|
+
redaction and all three renderers are behaving.
|
|
254
|
+
|
|
255
|
+
## License
|
|
256
|
+
|
|
257
|
+
MIT — see [LICENSE](LICENSE).
|
|
258
|
+
|
|
259
|
+
[cliproxy]: https://github.com/router-for-me/CLIProxyAPI
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
|
|
3
|
+
# poolprobe
|
|
4
|
+
|
|
5
|
+
**A doctor for OpenAI-compatible LLM gateways.**
|
|
6
|
+
Probe the pool, classify every failure, and only *then* talk about pruning.
|
|
7
|
+
|
|
8
|
+
[](https://www.python.org/downloads/)
|
|
9
|
+
[](#install)
|
|
10
|
+
[](LICENSE)
|
|
11
|
+
|
|
12
|
+
</div>
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
A pool of routed models rots quietly. An upstream drops a model, a key hits an
|
|
17
|
+
account tier, a reasoning model starts spending its whole token budget on
|
|
18
|
+
thinking, a gateway route begins answering SSE to a non-streaming request. None
|
|
19
|
+
of that raises an exception anywhere. The agent just gets an empty reply, or a
|
|
20
|
+
slower one, or falls through to a worse model — and nobody notices for a week.
|
|
21
|
+
|
|
22
|
+
`poolprobe` looks at every model in the pool, tells you *why* each one is
|
|
23
|
+
failing, and refuses to delete anything until five separate guards agree it is
|
|
24
|
+
dead. It grew out of running a live [cliproxy] pool on a schedule and learning,
|
|
25
|
+
the hard way, how many "dead model" verdicts were really weather.
|
|
26
|
+
|
|
27
|
+
## The three rules
|
|
28
|
+
|
|
29
|
+
Everything in the tool obeys these. They are the reason it exists.
|
|
30
|
+
|
|
31
|
+
1. **A verdict is not a death sentence.** Every failure is classified into a
|
|
32
|
+
kind — `ALIVE`, `ALIVE-STARVED`, `PERMANENT`, `TRANSIENT`, `CREDENTIAL`,
|
|
33
|
+
`MODALITY`, `UNKNOWN`. Only `PERMANENT` (the upstream genuinely does not know
|
|
34
|
+
this model) is ever a prune candidate. A 401 is a dead *key*, not a dead
|
|
35
|
+
*model*; pruning on it deletes a good alias because a credential ran dry.
|
|
36
|
+
|
|
37
|
+
2. **One observation is not a verdict.** A measured pool produced **544**
|
|
38
|
+
scheduled liveness runs over six weeks; **39%** went red, and *every single
|
|
39
|
+
red was transient*. `poolprobe` burst-confirms by default — five attempts in
|
|
40
|
+
one run, best answer wins — before it will call anything dead.
|
|
41
|
+
|
|
42
|
+
3. **A number without evidence is not reported.** Throughput comes from the
|
|
43
|
+
gateway's own `usage` block, or it is marked as a lower bound (`>=`), or it
|
|
44
|
+
renders as `-`. Never estimated from SSE chunk counts and dressed up as a
|
|
45
|
+
measurement.
|
|
46
|
+
|
|
47
|
+
## Install
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
pip install poolprobe # stdlib engine, generic gateway lane
|
|
51
|
+
pip install 'poolprobe[cliproxy]' # + PyYAML, to read a cliproxy-style config
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
No PyPI release yet? Install straight from the checkout:
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
git clone https://gitlab.com/ameobius-ai/poolprobe
|
|
58
|
+
cd poolprobe && pip install '.[cliproxy]'
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
The engine has **zero runtime dependencies**. PyYAML is optional and only needed
|
|
62
|
+
to attribute models to providers and to plan config edits from a gateway YAML.
|
|
63
|
+
|
|
64
|
+
## Quickstart
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
# 1. Is the gateway even up, and what does it publish? (no chat request sent)
|
|
68
|
+
poolprobe ping -u http://127.0.0.1:8317/v1 -k "$KEY"
|
|
69
|
+
|
|
70
|
+
# 2. Probe the whole pool, burst-confirmed, and keep a self-contained HTML report
|
|
71
|
+
poolprobe sweep -u http://127.0.0.1:8317/v1 -k "$KEY" --html out.html
|
|
72
|
+
|
|
73
|
+
# 3. Give it memory: fail streaks across runs are what make `prune` safe
|
|
74
|
+
poolprobe sweep -u ... -k "$KEY" --state ~/.cache/poolprobe.json
|
|
75
|
+
|
|
76
|
+
# 4. Only list what passed all five guards — nothing on disk changes without --apply
|
|
77
|
+
poolprobe prune -u ... -k "$KEY" --state ~/.cache/poolprobe.json \
|
|
78
|
+
--adapter cliproxy --config ~/.config/gateway.yaml
|
|
79
|
+
|
|
80
|
+
# 5. Validate agent fallback chains against the lane rules that actually read them
|
|
81
|
+
poolprobe chains ~/.hermes/config.yaml --cross-check -u ... -k "$KEY"
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
`-u` also reads `POOLPROBE_BASE_URL`; `-k` reads `POOLPROBE_API_KEY` then
|
|
85
|
+
`OPENAI_API_KEY`; `--state` reads `POOLPROBE_STATE`. The bearer key is masked in
|
|
86
|
+
every argv echo — reports are meant to be pasted into issues.
|
|
87
|
+
|
|
88
|
+
### What a sweep looks like
|
|
89
|
+
|
|
90
|
+
```
|
|
91
|
+
poolprobe 1.0.0 · sweep
|
|
92
|
+
http://127.0.0.1:8317/v1 · generic adapter (read-only)
|
|
93
|
+
|
|
94
|
+
5 models · 4 alive · 0 starved · 1 dead · 0 prunable · median 5.43s · run #1
|
|
95
|
+
|
|
96
|
+
VERDICT MODEL PROVIDER LAT BURST STREAK KIND NOTE
|
|
97
|
+
------------ ------------------- --------- ------- ----- ------ ---------- --------------------
|
|
98
|
+
OK gpt-5.4 - 2.74s ✓ ok1 - alive
|
|
99
|
+
OK gemini-3.8-flash - 6.12s ✓ ok1 - alive
|
|
100
|
+
OK grok-4.6 - 27.3s ✓ ok1 - alive
|
|
101
|
+
ERR suno-music - 0.13s ✗ x1 CREDENTIAL {"error":{"message"…
|
|
102
|
+
OK deepseek-v4-flash - 4.73s ✓ ok1 - alive
|
|
103
|
+
|
|
104
|
+
prunable: none — nothing met all five guards (dead, PERMANENT, streak, unprotected, chat modality)
|
|
105
|
+
|
|
106
|
+
what to do
|
|
107
|
+
CREDENTIAL (1): credential or account level: rotate/top up the key, never prune the model
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
`grok-4.6` answering `OK` here is the whole point: a prior single-shot check had
|
|
111
|
+
written it off as dead. The burst said otherwise.
|
|
112
|
+
|
|
113
|
+
## Commands
|
|
114
|
+
|
|
115
|
+
| Command | What it does | Sends a chat request? |
|
|
116
|
+
|---------|--------------|:---------------------:|
|
|
117
|
+
| `ping` | Reach the gateway, list the published catalog, break it down by provider. | no |
|
|
118
|
+
| `sweep [models…]` | Burst-confirmed liveness probe; one verdict + kind per model. | yes |
|
|
119
|
+
| `bench [models…]` | Latency and throughput; `--repeat N` reports the median, `--stream` measures time-to-first-token. | yes |
|
|
120
|
+
| `prune [models…]` | List models that passed all five guards and plan the config edit. `--apply` writes it (previous copy kept as `.bak`). | yes |
|
|
121
|
+
| `chains PATH` | Validate agent fallback chains against the lane rules that read them. `--cross-check` diffs rungs against the live catalog. | no |
|
|
122
|
+
| `selfcheck` | Offline assert suite — taxonomy, burst logic, prune guards, argv redaction, all three renderers. **This is what CI runs.** | no |
|
|
123
|
+
|
|
124
|
+
Common options: `--adapter {auto,cliproxy,generic}`, `--config PATH`,
|
|
125
|
+
`--threads N`, `--timeout S`, `--max-tokens N`, `--prompt TEXT`, `--state PATH`,
|
|
126
|
+
`--streak-min N`, `--protect MODEL` (repeatable), `--json PATH`, `--html PATH`,
|
|
127
|
+
`--color {auto,always,never}`, `-q/--quiet`.
|
|
128
|
+
|
|
129
|
+
`--max-tokens` is a **floor, not a cap**: a reasoning model spends it on thinking
|
|
130
|
+
and answers `EMPTY-LENGTH`, which `poolprobe` reads as *alive but starved*, never
|
|
131
|
+
as dead.
|
|
132
|
+
|
|
133
|
+
## Exit codes
|
|
134
|
+
|
|
135
|
+
A cron contract. Red is a finding, not noise.
|
|
136
|
+
|
|
137
|
+
| Code | Meaning |
|
|
138
|
+
|:----:|---------|
|
|
139
|
+
| `0` | Nothing dead — or nothing that met every guard. |
|
|
140
|
+
| `1` | Something dead (burst-confirmed), or a fallback chain is broken. |
|
|
141
|
+
| `2` | Usage error, unreadable config, or PyYAML missing for a config read. |
|
|
142
|
+
| `3` | The gateway could not be reached, or published an empty catalog. |
|
|
143
|
+
| `130` | Interrupted. |
|
|
144
|
+
|
|
145
|
+
## The five prune guards
|
|
146
|
+
|
|
147
|
+
`prune` will not name a model unless **all five** agree. A report that says
|
|
148
|
+
"prunable" while the prune cycle deletes something else is the worst outcome for
|
|
149
|
+
a tool like this, so the guards live in one function, checked in one place:
|
|
150
|
+
|
|
151
|
+
1. **dead verdict** — `EMPTY`, `ERR`, or `TIMEOUT` (`OK`/`EMPTY-LENGTH` never qualify);
|
|
152
|
+
2. **kind `PERMANENT`** — the upstream does not know this model;
|
|
153
|
+
3. **fail streak** — dead for at least `--streak-min` consecutive *runs* (needs `--state`);
|
|
154
|
+
4. **not protected** — absent from `--protect` (a namespaced alias's base name matches too);
|
|
155
|
+
5. **not a generation modality** — image/video/tts/music models legitimately fail a chat probe.
|
|
156
|
+
|
|
157
|
+
Without `--state` there is no memory, every streak reads `0`, and guard 3 can
|
|
158
|
+
never pass — so `prune` lists candidates but changes nothing. That is the safe
|
|
159
|
+
default.
|
|
160
|
+
|
|
161
|
+
## Reading a report
|
|
162
|
+
|
|
163
|
+
**Verdict** — what the probe saw:
|
|
164
|
+
|
|
165
|
+
`OK` · `EMPTY` (200, empty body) · `EMPTY-LENGTH` (empty because the token budget
|
|
166
|
+
ran out — alive) · `ERR` (non-2xx) · `TIMEOUT`.
|
|
167
|
+
|
|
168
|
+
**Kind** — what it means:
|
|
169
|
+
|
|
170
|
+
| Kind | Reading | Prunable? |
|
|
171
|
+
|------|---------|:---------:|
|
|
172
|
+
| `ALIVE` | Answered. | no |
|
|
173
|
+
| `ALIVE-STARVED` | Answered `EMPTY-LENGTH` — raise `--max-tokens`. | no |
|
|
174
|
+
| `PERMANENT` | Upstream does not know this model (`404`, "does not exist"). | **yes** |
|
|
175
|
+
| `TRANSIENT` | Timeout, `429`, `5xx` — weather. Burst-confirm before believing it. | no |
|
|
176
|
+
| `CREDENTIAL` | `401`/`403`, "invalid api key" — rotate the key, never the model. | no |
|
|
177
|
+
| `MODALITY` | Model lives on another endpoint (`/v1/images/…`), not chat. | no |
|
|
178
|
+
| `UNKNOWN` | None of the above; reported, never acted on. | no |
|
|
179
|
+
|
|
180
|
+
Three renderers, one report: the terminal table (ANSI, honours `NO_COLOR` /
|
|
181
|
+
`FORCE_COLOR` / `TERM=dumb` / isatty), a machine-readable `--json`, and a
|
|
182
|
+
self-contained `--html` you can attach to a post. They cannot disagree about
|
|
183
|
+
what a run recorded.
|
|
184
|
+
|
|
185
|
+
## Library use
|
|
186
|
+
|
|
187
|
+
```python
|
|
188
|
+
from poolprobe import Gateway, sweep, taxonomy
|
|
189
|
+
|
|
190
|
+
gw = Gateway("http://127.0.0.1:8317/v1", api_key="local")
|
|
191
|
+
for result in sweep(gw, ["gpt-5.4", "glm-5.3-flash"], attempts=5):
|
|
192
|
+
print(result.model, result.verdict, result.kind, result.advice)
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
## Development
|
|
196
|
+
|
|
197
|
+
```bash
|
|
198
|
+
python -m poolprobe selfcheck # the whole offline contract; CI runs exactly this
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
`selfcheck` needs no gateway, no network and no secrets. If it passes, the
|
|
202
|
+
taxonomy, the burst "best truth wins" logic, the five prune guards, the argv
|
|
203
|
+
redaction and all three renderers are behaving.
|
|
204
|
+
|
|
205
|
+
## License
|
|
206
|
+
|
|
207
|
+
MIT — see [LICENSE](LICENSE).
|
|
208
|
+
|
|
209
|
+
[cliproxy]: https://github.com/router-for-me/CLIProxyAPI
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
"""poolprobe — a doctor for OpenAI-compatible LLM gateways.
|
|
2
|
+
|
|
3
|
+
A pool of routed models rots quietly. An upstream drops a model, a key hits an
|
|
4
|
+
account tier, a reasoning model starts eating its whole token budget on
|
|
5
|
+
thinking, a gateway route begins answering SSE to a non-streaming request. None
|
|
6
|
+
of that raises an exception anywhere: the agent just gets an empty reply, or a
|
|
7
|
+
slower one, or falls through to a worse model and nobody notices for a week.
|
|
8
|
+
|
|
9
|
+
poolprobe probes the pool and refuses to guess. Three rules drive everything
|
|
10
|
+
here:
|
|
11
|
+
|
|
12
|
+
* **A verdict is not a death sentence.** Every failure is classified into a
|
|
13
|
+
kind — ``ALIVE``, ``ALIVE-STARVED``, ``PERMANENT``, ``TRANSIENT``,
|
|
14
|
+
``CREDENTIAL``, ``MODALITY``, ``UNKNOWN`` — and only ``PERMANENT`` (the
|
|
15
|
+
upstream does not know this model) is ever a prune candidate.
|
|
16
|
+
* **One observation is not a verdict.** A measured pool produced 544 scheduled
|
|
17
|
+
liveness runs in six weeks; 39% went red and every single red was transient.
|
|
18
|
+
Burst-confirm before believing anything.
|
|
19
|
+
* **A number without evidence is not reported.** Throughput comes from the
|
|
20
|
+
gateway's own ``usage`` block or it is marked as a lower bound or it is
|
|
21
|
+
``0.0``. Never estimated from chunk counts and presented as a measurement.
|
|
22
|
+
|
|
23
|
+
Library use::
|
|
24
|
+
|
|
25
|
+
from poolprobe import Gateway, sweep, taxonomy
|
|
26
|
+
|
|
27
|
+
gw = Gateway("http://127.0.0.1:8317/v1", api_key="local")
|
|
28
|
+
for result in sweep(gw, ["gpt-5.4", "glm-5.3-flash"], attempts=5):
|
|
29
|
+
print(result.model, result.verdict, result.kind, result.advice)
|
|
30
|
+
|
|
31
|
+
CLI use::
|
|
32
|
+
|
|
33
|
+
poolprobe sweep --base-url http://127.0.0.1:8317/v1 --attempts 5 --html out.html
|
|
34
|
+
poolprobe prune --base-url ... --adapter cliproxy --config ~/.config/gateway.yaml
|
|
35
|
+
poolprobe selfcheck
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
from __future__ import annotations
|
|
39
|
+
|
|
40
|
+
__version__ = "1.0.0"
|
|
41
|
+
|
|
42
|
+
# Submodules first: probe, bench and adapter all resolve ``taxonomy`` through
|
|
43
|
+
# the package attribute at import time, and a partially initialised package must
|
|
44
|
+
# not leave that lookup to CPython's submodule fallback.
|
|
45
|
+
from . import chains, taxonomy
|
|
46
|
+
from .adapter import (
|
|
47
|
+
Adapter,
|
|
48
|
+
Attribution,
|
|
49
|
+
CliproxyAdapter,
|
|
50
|
+
ConfigError,
|
|
51
|
+
EditPlan,
|
|
52
|
+
LiveAdapter,
|
|
53
|
+
build_adapter,
|
|
54
|
+
)
|
|
55
|
+
from .bench import (
|
|
56
|
+
BenchResult,
|
|
57
|
+
bench_many,
|
|
58
|
+
bench_one,
|
|
59
|
+
summarize,
|
|
60
|
+
)
|
|
61
|
+
from .chains import (
|
|
62
|
+
load_config,
|
|
63
|
+
validate as validate_chains,
|
|
64
|
+
validate_file as validate_chains_file,
|
|
65
|
+
)
|
|
66
|
+
from .probe import (
|
|
67
|
+
DEFAULT_MAX_TOKENS,
|
|
68
|
+
DEFAULT_PROMPT,
|
|
69
|
+
DEFAULT_TIMEOUT,
|
|
70
|
+
Gateway,
|
|
71
|
+
GatewayError,
|
|
72
|
+
ProbeResult,
|
|
73
|
+
burst,
|
|
74
|
+
list_models,
|
|
75
|
+
parallel_map,
|
|
76
|
+
ping,
|
|
77
|
+
probe,
|
|
78
|
+
sweep,
|
|
79
|
+
)
|
|
80
|
+
from .report import (
|
|
81
|
+
Report,
|
|
82
|
+
RunMeta,
|
|
83
|
+
render_html,
|
|
84
|
+
render_json,
|
|
85
|
+
render_terminal,
|
|
86
|
+
)
|
|
87
|
+
from .safewrite import write_atomic
|
|
88
|
+
from .state import ModelState, RunStats, State
|
|
89
|
+
|
|
90
|
+
__all__ = [
|
|
91
|
+
"__version__",
|
|
92
|
+
# taxonomy
|
|
93
|
+
"taxonomy",
|
|
94
|
+
# transport
|
|
95
|
+
"Gateway",
|
|
96
|
+
"GatewayError",
|
|
97
|
+
"ProbeResult",
|
|
98
|
+
"probe",
|
|
99
|
+
"burst",
|
|
100
|
+
"sweep",
|
|
101
|
+
"list_models",
|
|
102
|
+
"ping",
|
|
103
|
+
"parallel_map",
|
|
104
|
+
"DEFAULT_PROMPT",
|
|
105
|
+
"DEFAULT_MAX_TOKENS",
|
|
106
|
+
"DEFAULT_TIMEOUT",
|
|
107
|
+
# memory
|
|
108
|
+
"State",
|
|
109
|
+
"ModelState",
|
|
110
|
+
"RunStats",
|
|
111
|
+
# bench
|
|
112
|
+
"BenchResult",
|
|
113
|
+
"bench_one",
|
|
114
|
+
"bench_many",
|
|
115
|
+
"summarize",
|
|
116
|
+
# adapters
|
|
117
|
+
"Adapter",
|
|
118
|
+
"LiveAdapter",
|
|
119
|
+
"CliproxyAdapter",
|
|
120
|
+
"Attribution",
|
|
121
|
+
"EditPlan",
|
|
122
|
+
"ConfigError",
|
|
123
|
+
"build_adapter",
|
|
124
|
+
# chains
|
|
125
|
+
"chains",
|
|
126
|
+
"validate_chains",
|
|
127
|
+
"validate_chains_file",
|
|
128
|
+
"load_config",
|
|
129
|
+
# io
|
|
130
|
+
"write_atomic",
|
|
131
|
+
# reporting
|
|
132
|
+
"Report",
|
|
133
|
+
"RunMeta",
|
|
134
|
+
"render_terminal",
|
|
135
|
+
"render_json",
|
|
136
|
+
"render_html",
|
|
137
|
+
]
|