remuda 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- remuda-0.1.0/LICENSE +21 -0
- remuda-0.1.0/PKG-INFO +221 -0
- remuda-0.1.0/README.md +195 -0
- remuda-0.1.0/pyproject.toml +135 -0
- remuda-0.1.0/remuda/__init__.py +128 -0
- remuda-0.1.0/remuda/api.py +275 -0
- remuda-0.1.0/remuda/catalog/__init__.py +52 -0
- remuda-0.1.0/remuda/catalog/adapters.py +199 -0
- remuda-0.1.0/remuda/catalog/errors.py +32 -0
- remuda-0.1.0/remuda/catalog/models.py +70 -0
- remuda-0.1.0/remuda/catalog/resolve.py +244 -0
- remuda-0.1.0/remuda/cli/__init__.py +5 -0
- remuda-0.1.0/remuda/cli/app.py +76 -0
- remuda-0.1.0/remuda/cli/commands/__init__.py +20 -0
- remuda-0.1.0/remuda/cli/commands/check.py +69 -0
- remuda-0.1.0/remuda/cli/commands/init_job.py +59 -0
- remuda-0.1.0/remuda/cli/commands/inline_run.py +164 -0
- remuda-0.1.0/remuda/cli/commands/models.py +95 -0
- remuda-0.1.0/remuda/cli/commands/pools.py +162 -0
- remuda-0.1.0/remuda/cli/commands/preview.py +81 -0
- remuda-0.1.0/remuda/cli/commands/render.py +121 -0
- remuda-0.1.0/remuda/cli/commands/report.py +47 -0
- remuda-0.1.0/remuda/cli/commands/run.py +295 -0
- remuda-0.1.0/remuda/cli/commands/stats.py +202 -0
- remuda-0.1.0/remuda/cli/console.py +72 -0
- remuda-0.1.0/remuda/cli/options.py +74 -0
- remuda-0.1.0/remuda/cli/progress.py +11 -0
- remuda-0.1.0/remuda/cli/reporting.py +82 -0
- remuda-0.1.0/remuda/cli/scaffold.py +113 -0
- remuda-0.1.0/remuda/engine/__init__.py +30 -0
- remuda-0.1.0/remuda/engine/ladder.py +241 -0
- remuda-0.1.0/remuda/engine/lanes.py +163 -0
- remuda-0.1.0/remuda/engine/packing.py +131 -0
- remuda-0.1.0/remuda/engine/plan.py +82 -0
- remuda-0.1.0/remuda/engine/progress.py +40 -0
- remuda-0.1.0/remuda/engine/runner.py +406 -0
- remuda-0.1.0/remuda/errors.py +9 -0
- remuda-0.1.0/remuda/health.py +102 -0
- remuda-0.1.0/remuda/inline.py +148 -0
- remuda-0.1.0/remuda/inspection.py +120 -0
- remuda-0.1.0/remuda/ledger/__init__.py +55 -0
- remuda-0.1.0/remuda/ledger/models.py +123 -0
- remuda-0.1.0/remuda/ledger/render.py +220 -0
- remuda-0.1.0/remuda/ledger/resume.py +89 -0
- remuda-0.1.0/remuda/ledger/store.py +298 -0
- remuda-0.1.0/remuda/preflight.py +53 -0
- remuda-0.1.0/remuda/py.typed +0 -0
- remuda-0.1.0/remuda/registry/__init__.py +46 -0
- remuda-0.1.0/remuda/registry/bootstrap.py +158 -0
- remuda-0.1.0/remuda/registry/errors.py +25 -0
- remuda-0.1.0/remuda/registry/loader.py +167 -0
- remuda-0.1.0/remuda/registry/models.py +200 -0
- remuda-0.1.0/remuda/registry/registry.py +193 -0
- remuda-0.1.0/remuda/rows.py +153 -0
- remuda-0.1.0/remuda/spec/__init__.py +55 -0
- remuda-0.1.0/remuda/spec/errors.py +30 -0
- remuda-0.1.0/remuda/spec/lint.py +99 -0
- remuda-0.1.0/remuda/spec/loader.py +210 -0
- remuda-0.1.0/remuda/spec/models.py +297 -0
- remuda-0.1.0/remuda/spec/prompt.py +60 -0
- remuda-0.1.0/remuda/spec/shapes.py +140 -0
- remuda-0.1.0/remuda/stats.py +152 -0
- remuda-0.1.0/remuda/transport/__init__.py +35 -0
- remuda-0.1.0/remuda/transport/errors.py +33 -0
- remuda-0.1.0/remuda/transport/models.py +60 -0
- remuda-0.1.0/remuda/transport/openai_compat.py +234 -0
- remuda-0.1.0/remuda/transport/opencode.py +191 -0
- remuda-0.1.0/remuda/validate/__init__.py +13 -0
- remuda-0.1.0/remuda/validate/answers.py +229 -0
- remuda-0.1.0/remuda/validate/preconditions.py +31 -0
- remuda-0.1.0/remuda/validate/verdict.py +30 -0
- remuda-0.1.0/remuda/version.py +3 -0
remuda-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 David Marsa / Neomanex
|
|
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.
|
remuda-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: remuda
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Bulk LLM inference over pools of free/cheap models: ride one until it tires, swap to the next
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
License-File: LICENSE
|
|
7
|
+
Keywords: llm,inference,bulk,openrouter,classification,csv,cli
|
|
8
|
+
Author: David Marsa
|
|
9
|
+
Author-email: david.marsa@neomanex.com
|
|
10
|
+
Requires-Python: >=3.12,<3.14
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
14
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
15
|
+
Requires-Dist: httpx (>=0.28,<0.29)
|
|
16
|
+
Requires-Dist: jinja2 (>=3.1,<4.0)
|
|
17
|
+
Requires-Dist: pydantic (>=2.10,<3.0)
|
|
18
|
+
Requires-Dist: pyyaml (>=6.0,<7.0)
|
|
19
|
+
Requires-Dist: rich (>=13.9,<15.0)
|
|
20
|
+
Requires-Dist: typer (>=0.15,<0.20)
|
|
21
|
+
Project-URL: Changelog, https://github.com/daviunx/remuda/blob/main/CHANGELOG.md
|
|
22
|
+
Project-URL: Issues, https://github.com/daviunx/remuda/issues
|
|
23
|
+
Project-URL: Repository, https://github.com/daviunx/remuda
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
|
|
26
|
+
# remuda
|
|
27
|
+
|
|
28
|
+
Bulk LLM inference over pools of free/cheap models. Ride one until it tires, swap to the next.
|
|
29
|
+
|
|
30
|
+
[](https://github.com/daviunx/remuda/actions/workflows/test.yml)
|
|
31
|
+
[](https://pypi.org/project/remuda/)
|
|
32
|
+
[](https://pypi.org/project/remuda/)
|
|
33
|
+
[](LICENSE)
|
|
34
|
+
|
|
35
|
+
A *remuda* is the herd of spare horses a working cowboy draws from: when one
|
|
36
|
+
tires, you saddle the next and keep moving. This tool does that with language
|
|
37
|
+
models. Point it at a pool of free or cheap models, hand it a thousand rows,
|
|
38
|
+
and it classifies, extracts, scores or generates a column for every one:
|
|
39
|
+
validating every answer, retrying with feedback, rotating past rate limits and
|
|
40
|
+
broken models, and never reporting a partial result as success.
|
|
41
|
+
|
|
42
|
+
## Why
|
|
43
|
+
|
|
44
|
+
Free-tier models are individually unreliable and collectively excellent. On
|
|
45
|
+
any given day a quarter of a free pool answers well, another quarter is rate
|
|
46
|
+
limited, and the rest return markdown essays when you asked for one word.
|
|
47
|
+
remuda's job is to make that mess dependable: a validation ladder per answer,
|
|
48
|
+
rotation across the pool, a crash-safe ledger, and resume that retries only
|
|
49
|
+
what failed, at no repeat cost.
|
|
50
|
+
|
|
51
|
+
## Install
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
pipx install remuda # recommended for the CLI
|
|
55
|
+
pip install remuda # or as a library
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Quick start
|
|
59
|
+
|
|
60
|
+
With `OPENROUTER_API_KEY` exported and no configuration at all, remuda
|
|
61
|
+
self-configures: an implicit `openrouter` provider and a `free` pool
|
|
62
|
+
discovered from its free tier. A local model server answering on
|
|
63
|
+
`127.0.0.1:11434` (Ollama, for instance) adds an implicit `local` provider.
|
|
64
|
+
Every implicit resolution is announced on stderr, an explicit registry entry
|
|
65
|
+
of the same name always wins, and `--no-bootstrap` (or `REMUDA_NO_BOOTSTRAP=1`)
|
|
66
|
+
turns it off.
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
# one question: the answer is all that reaches stdout
|
|
70
|
+
remuda run "Name the capital of Spain" -p free
|
|
71
|
+
|
|
72
|
+
# inline bulk: CSV in, the same CSV out with one new column
|
|
73
|
+
remuda run "Severity of this complaint: {{ complaint }}" \
|
|
74
|
+
-i complaints.csv --field severity --vocab high,low -p free -o enriched.csv
|
|
75
|
+
|
|
76
|
+
# a repeatable job directory
|
|
77
|
+
remuda init jobs/my-job # scaffold a commented job directory
|
|
78
|
+
remuda check jobs/my-job # lint the spec, its input file and its pools
|
|
79
|
+
remuda preview jobs/my-job -n 3 # see the exact prompts, zero model calls
|
|
80
|
+
remuda run jobs/my-job # derive every field for every row
|
|
81
|
+
remuda render .runs/my-job/<ts> --enrich -o out.csv # input + new columns
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
All three shapes are one engine. A run resumes by default: re-invoke the same
|
|
85
|
+
command and only what is left is computed. Any key the pool could not answer
|
|
86
|
+
makes the run exit non-zero. A partial result is never reported as success.
|
|
87
|
+
|
|
88
|
+
## The ladder
|
|
89
|
+
|
|
90
|
+
An answer that fails validation is retried on the same model with feedback
|
|
91
|
+
describing what was wrong, then the chunk rotates to the next model in the
|
|
92
|
+
pool, then to the pool's mop-up model, and only then is the key recorded as
|
|
93
|
+
failed with its last reason. Within a packed call, valid answers are banked
|
|
94
|
+
immediately; only the keys still invalid re-enter the ladder. Rate limits and
|
|
95
|
+
transport failures cool a model down and redistribute its work without
|
|
96
|
+
consuming a validation attempt.
|
|
97
|
+
|
|
98
|
+
Each chunk prints a progress line to stderr as it lands, naming the model that
|
|
99
|
+
answered. Everything a run decides is appended to
|
|
100
|
+
`.runs/<job>/<timestamp>/ledger.jsonl` as it happens, so a killed run loses
|
|
101
|
+
nothing.
|
|
102
|
+
|
|
103
|
+
## What the models actually did
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
remuda runs list --runs-dir jobs/my-job/.runs
|
|
107
|
+
remuda stats --runs-dir jobs/my-job/.runs # success rate, latency, cost
|
|
108
|
+
remuda runs prune --keep 5 --runs-dir jobs/my-job/.runs # dry; --write deletes
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
`stats` ranks models by how they have really performed, so YOU can reorder a
|
|
112
|
+
pool. remuda never reorders one itself: a tool that rewrote its own
|
|
113
|
+
configuration from yesterday's latency would be impossible to reason about.
|
|
114
|
+
|
|
115
|
+
## Embedding
|
|
116
|
+
|
|
117
|
+
```python
|
|
118
|
+
from remuda import Job, Registry, run_sync
|
|
119
|
+
|
|
120
|
+
report = run_sync(job, rows, registry, sink=collect, only=["severity"])
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
Rows are plain mappings from any source, results arrive per completed row, and
|
|
124
|
+
the `Report` is the same object the CLI persists. No files are required. The
|
|
125
|
+
registry is also constructible entirely in code; configuration files are one
|
|
126
|
+
loader over it, never the only way in.
|
|
127
|
+
|
|
128
|
+
## Job directory
|
|
129
|
+
|
|
130
|
+
```
|
|
131
|
+
my-job/
|
|
132
|
+
├── job.yaml # name, input, fields
|
|
133
|
+
├── input.csv # rows (CSV, JSONL or a JSON array)
|
|
134
|
+
└── vocab/labels.txt # closed vocabulary for a `classify` field
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
Field kinds: `classify` (closed vocabulary), `extract` (declared shape),
|
|
138
|
+
`generate` (free text under constraints), `score` (number in a range), and
|
|
139
|
+
`map` (deterministic lookup, no model involved). A field may declare a `when:`
|
|
140
|
+
precondition and may `depends_on` one previously derived field (one level, no
|
|
141
|
+
chains).
|
|
142
|
+
|
|
143
|
+
A prompt template can only read the columns listed in `prompt.inputs`. Any
|
|
144
|
+
other column on the row is unreachable from the template, and a template
|
|
145
|
+
variable outside that allowlist is refused at load time.
|
|
146
|
+
|
|
147
|
+
## Registry
|
|
148
|
+
|
|
149
|
+
Endpoints, credentials and model quirks are named once, never inside a job:
|
|
150
|
+
|
|
151
|
+
```
|
|
152
|
+
~/.config/remuda/ # user level
|
|
153
|
+
./.remuda/ # project level, overrides the user level by name
|
|
154
|
+
├── providers.yaml
|
|
155
|
+
├── models.yaml
|
|
156
|
+
└── pools.yaml
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
Credentials are never written in a file: a provider names the environment
|
|
160
|
+
variable its key is read from (`key_env`). Pools are ordered model references
|
|
161
|
+
with a `scatter` or `waterfall` strategy, an optional paid mop-up model, and
|
|
162
|
+
optional `discover:` queries resolved against a provider's live catalog when a
|
|
163
|
+
run starts.
|
|
164
|
+
|
|
165
|
+
### Discovering models
|
|
166
|
+
|
|
167
|
+
A pool entry can be a query against a provider's live catalog instead of a
|
|
168
|
+
model name:
|
|
169
|
+
|
|
170
|
+
```yaml
|
|
171
|
+
free-big:
|
|
172
|
+
strategy: scatter
|
|
173
|
+
entries:
|
|
174
|
+
- discover: {provider: openrouter, free: true, min_context: 32000,
|
|
175
|
+
sort: context, take: 4}
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
Each provider declares which catalog shape it serves (`openrouter`,
|
|
179
|
+
`openai_compat`, `ollama`). A filter the catalog cannot answer (free-tier
|
|
180
|
+
filtering against a bare `/v1/models` list, for instance) is **refused**
|
|
181
|
+
naming the filter and the provider, never silently ignored.
|
|
182
|
+
|
|
183
|
+
Queries resolve when a run starts, and the resolved membership is snapshotted
|
|
184
|
+
into the run directory: a resumed run reuses exactly the models the first
|
|
185
|
+
attempt used, because free-tier membership churns week to week.
|
|
186
|
+
|
|
187
|
+
```bash
|
|
188
|
+
remuda pools show free-big # materialized membership
|
|
189
|
+
remuda pools check free-big # one minimal request per member
|
|
190
|
+
remuda models list openrouter --free
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
### Transports
|
|
194
|
+
|
|
195
|
+
| Provider kind | Runs where |
|
|
196
|
+
|---|---|
|
|
197
|
+
| `openai_compat` (OpenRouter, Ollama, vLLM, NIM) | anywhere |
|
|
198
|
+
| `opencode` (`opencode run -m <model>`) | the operator's machine only |
|
|
199
|
+
|
|
200
|
+
The opencode transport builds its command as an argument list and never
|
|
201
|
+
invokes a shell, so row data reaching the prompt cannot become a command. Its
|
|
202
|
+
version is probed once before a run that uses it.
|
|
203
|
+
|
|
204
|
+
## Documentation
|
|
205
|
+
|
|
206
|
+
The full CLI surface is documented in [docs/usage.md](docs/usage.md), and
|
|
207
|
+
recorded design exceptions in [docs/decisions.md](docs/decisions.md).
|
|
208
|
+
|
|
209
|
+
## Development
|
|
210
|
+
|
|
211
|
+
```bash
|
|
212
|
+
poetry install
|
|
213
|
+
poetry run pytest -q
|
|
214
|
+
poetry run ruff check . && poetry run ruff format --check .
|
|
215
|
+
poetry run mypy .
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
## License
|
|
219
|
+
|
|
220
|
+
[MIT](LICENSE)
|
|
221
|
+
|
remuda-0.1.0/README.md
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
# remuda
|
|
2
|
+
|
|
3
|
+
Bulk LLM inference over pools of free/cheap models. Ride one until it tires, swap to the next.
|
|
4
|
+
|
|
5
|
+
[](https://github.com/daviunx/remuda/actions/workflows/test.yml)
|
|
6
|
+
[](https://pypi.org/project/remuda/)
|
|
7
|
+
[](https://pypi.org/project/remuda/)
|
|
8
|
+
[](LICENSE)
|
|
9
|
+
|
|
10
|
+
A *remuda* is the herd of spare horses a working cowboy draws from: when one
|
|
11
|
+
tires, you saddle the next and keep moving. This tool does that with language
|
|
12
|
+
models. Point it at a pool of free or cheap models, hand it a thousand rows,
|
|
13
|
+
and it classifies, extracts, scores or generates a column for every one:
|
|
14
|
+
validating every answer, retrying with feedback, rotating past rate limits and
|
|
15
|
+
broken models, and never reporting a partial result as success.
|
|
16
|
+
|
|
17
|
+
## Why
|
|
18
|
+
|
|
19
|
+
Free-tier models are individually unreliable and collectively excellent. On
|
|
20
|
+
any given day a quarter of a free pool answers well, another quarter is rate
|
|
21
|
+
limited, and the rest return markdown essays when you asked for one word.
|
|
22
|
+
remuda's job is to make that mess dependable: a validation ladder per answer,
|
|
23
|
+
rotation across the pool, a crash-safe ledger, and resume that retries only
|
|
24
|
+
what failed, at no repeat cost.
|
|
25
|
+
|
|
26
|
+
## Install
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
pipx install remuda # recommended for the CLI
|
|
30
|
+
pip install remuda # or as a library
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Quick start
|
|
34
|
+
|
|
35
|
+
With `OPENROUTER_API_KEY` exported and no configuration at all, remuda
|
|
36
|
+
self-configures: an implicit `openrouter` provider and a `free` pool
|
|
37
|
+
discovered from its free tier. A local model server answering on
|
|
38
|
+
`127.0.0.1:11434` (Ollama, for instance) adds an implicit `local` provider.
|
|
39
|
+
Every implicit resolution is announced on stderr, an explicit registry entry
|
|
40
|
+
of the same name always wins, and `--no-bootstrap` (or `REMUDA_NO_BOOTSTRAP=1`)
|
|
41
|
+
turns it off.
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
# one question: the answer is all that reaches stdout
|
|
45
|
+
remuda run "Name the capital of Spain" -p free
|
|
46
|
+
|
|
47
|
+
# inline bulk: CSV in, the same CSV out with one new column
|
|
48
|
+
remuda run "Severity of this complaint: {{ complaint }}" \
|
|
49
|
+
-i complaints.csv --field severity --vocab high,low -p free -o enriched.csv
|
|
50
|
+
|
|
51
|
+
# a repeatable job directory
|
|
52
|
+
remuda init jobs/my-job # scaffold a commented job directory
|
|
53
|
+
remuda check jobs/my-job # lint the spec, its input file and its pools
|
|
54
|
+
remuda preview jobs/my-job -n 3 # see the exact prompts, zero model calls
|
|
55
|
+
remuda run jobs/my-job # derive every field for every row
|
|
56
|
+
remuda render .runs/my-job/<ts> --enrich -o out.csv # input + new columns
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
All three shapes are one engine. A run resumes by default: re-invoke the same
|
|
60
|
+
command and only what is left is computed. Any key the pool could not answer
|
|
61
|
+
makes the run exit non-zero. A partial result is never reported as success.
|
|
62
|
+
|
|
63
|
+
## The ladder
|
|
64
|
+
|
|
65
|
+
An answer that fails validation is retried on the same model with feedback
|
|
66
|
+
describing what was wrong, then the chunk rotates to the next model in the
|
|
67
|
+
pool, then to the pool's mop-up model, and only then is the key recorded as
|
|
68
|
+
failed with its last reason. Within a packed call, valid answers are banked
|
|
69
|
+
immediately; only the keys still invalid re-enter the ladder. Rate limits and
|
|
70
|
+
transport failures cool a model down and redistribute its work without
|
|
71
|
+
consuming a validation attempt.
|
|
72
|
+
|
|
73
|
+
Each chunk prints a progress line to stderr as it lands, naming the model that
|
|
74
|
+
answered. Everything a run decides is appended to
|
|
75
|
+
`.runs/<job>/<timestamp>/ledger.jsonl` as it happens, so a killed run loses
|
|
76
|
+
nothing.
|
|
77
|
+
|
|
78
|
+
## What the models actually did
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
remuda runs list --runs-dir jobs/my-job/.runs
|
|
82
|
+
remuda stats --runs-dir jobs/my-job/.runs # success rate, latency, cost
|
|
83
|
+
remuda runs prune --keep 5 --runs-dir jobs/my-job/.runs # dry; --write deletes
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
`stats` ranks models by how they have really performed, so YOU can reorder a
|
|
87
|
+
pool. remuda never reorders one itself: a tool that rewrote its own
|
|
88
|
+
configuration from yesterday's latency would be impossible to reason about.
|
|
89
|
+
|
|
90
|
+
## Embedding
|
|
91
|
+
|
|
92
|
+
```python
|
|
93
|
+
from remuda import Job, Registry, run_sync
|
|
94
|
+
|
|
95
|
+
report = run_sync(job, rows, registry, sink=collect, only=["severity"])
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Rows are plain mappings from any source, results arrive per completed row, and
|
|
99
|
+
the `Report` is the same object the CLI persists. No files are required. The
|
|
100
|
+
registry is also constructible entirely in code; configuration files are one
|
|
101
|
+
loader over it, never the only way in.
|
|
102
|
+
|
|
103
|
+
## Job directory
|
|
104
|
+
|
|
105
|
+
```
|
|
106
|
+
my-job/
|
|
107
|
+
├── job.yaml # name, input, fields
|
|
108
|
+
├── input.csv # rows (CSV, JSONL or a JSON array)
|
|
109
|
+
└── vocab/labels.txt # closed vocabulary for a `classify` field
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Field kinds: `classify` (closed vocabulary), `extract` (declared shape),
|
|
113
|
+
`generate` (free text under constraints), `score` (number in a range), and
|
|
114
|
+
`map` (deterministic lookup, no model involved). A field may declare a `when:`
|
|
115
|
+
precondition and may `depends_on` one previously derived field (one level, no
|
|
116
|
+
chains).
|
|
117
|
+
|
|
118
|
+
A prompt template can only read the columns listed in `prompt.inputs`. Any
|
|
119
|
+
other column on the row is unreachable from the template, and a template
|
|
120
|
+
variable outside that allowlist is refused at load time.
|
|
121
|
+
|
|
122
|
+
## Registry
|
|
123
|
+
|
|
124
|
+
Endpoints, credentials and model quirks are named once, never inside a job:
|
|
125
|
+
|
|
126
|
+
```
|
|
127
|
+
~/.config/remuda/ # user level
|
|
128
|
+
./.remuda/ # project level, overrides the user level by name
|
|
129
|
+
├── providers.yaml
|
|
130
|
+
├── models.yaml
|
|
131
|
+
└── pools.yaml
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
Credentials are never written in a file: a provider names the environment
|
|
135
|
+
variable its key is read from (`key_env`). Pools are ordered model references
|
|
136
|
+
with a `scatter` or `waterfall` strategy, an optional paid mop-up model, and
|
|
137
|
+
optional `discover:` queries resolved against a provider's live catalog when a
|
|
138
|
+
run starts.
|
|
139
|
+
|
|
140
|
+
### Discovering models
|
|
141
|
+
|
|
142
|
+
A pool entry can be a query against a provider's live catalog instead of a
|
|
143
|
+
model name:
|
|
144
|
+
|
|
145
|
+
```yaml
|
|
146
|
+
free-big:
|
|
147
|
+
strategy: scatter
|
|
148
|
+
entries:
|
|
149
|
+
- discover: {provider: openrouter, free: true, min_context: 32000,
|
|
150
|
+
sort: context, take: 4}
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
Each provider declares which catalog shape it serves (`openrouter`,
|
|
154
|
+
`openai_compat`, `ollama`). A filter the catalog cannot answer (free-tier
|
|
155
|
+
filtering against a bare `/v1/models` list, for instance) is **refused**
|
|
156
|
+
naming the filter and the provider, never silently ignored.
|
|
157
|
+
|
|
158
|
+
Queries resolve when a run starts, and the resolved membership is snapshotted
|
|
159
|
+
into the run directory: a resumed run reuses exactly the models the first
|
|
160
|
+
attempt used, because free-tier membership churns week to week.
|
|
161
|
+
|
|
162
|
+
```bash
|
|
163
|
+
remuda pools show free-big # materialized membership
|
|
164
|
+
remuda pools check free-big # one minimal request per member
|
|
165
|
+
remuda models list openrouter --free
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
### Transports
|
|
169
|
+
|
|
170
|
+
| Provider kind | Runs where |
|
|
171
|
+
|---|---|
|
|
172
|
+
| `openai_compat` (OpenRouter, Ollama, vLLM, NIM) | anywhere |
|
|
173
|
+
| `opencode` (`opencode run -m <model>`) | the operator's machine only |
|
|
174
|
+
|
|
175
|
+
The opencode transport builds its command as an argument list and never
|
|
176
|
+
invokes a shell, so row data reaching the prompt cannot become a command. Its
|
|
177
|
+
version is probed once before a run that uses it.
|
|
178
|
+
|
|
179
|
+
## Documentation
|
|
180
|
+
|
|
181
|
+
The full CLI surface is documented in [docs/usage.md](docs/usage.md), and
|
|
182
|
+
recorded design exceptions in [docs/decisions.md](docs/decisions.md).
|
|
183
|
+
|
|
184
|
+
## Development
|
|
185
|
+
|
|
186
|
+
```bash
|
|
187
|
+
poetry install
|
|
188
|
+
poetry run pytest -q
|
|
189
|
+
poetry run ruff check . && poetry run ruff format --check .
|
|
190
|
+
poetry run mypy .
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
## License
|
|
194
|
+
|
|
195
|
+
[MIT](LICENSE)
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "remuda"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Bulk LLM inference over pools of free/cheap models: ride one until it tires, swap to the next"
|
|
5
|
+
authors = [{name = "David Marsa", email = "david.marsa@neomanex.com"}]
|
|
6
|
+
readme = "README.md"
|
|
7
|
+
license = "MIT"
|
|
8
|
+
requires-python = ">=3.12,<3.14"
|
|
9
|
+
keywords = ["llm", "inference", "bulk", "openrouter", "classification", "csv", "cli"]
|
|
10
|
+
classifiers = [
|
|
11
|
+
"Development Status :: 3 - Alpha",
|
|
12
|
+
"Programming Language :: Python :: 3.12",
|
|
13
|
+
"Programming Language :: Python :: 3.13",
|
|
14
|
+
"Topic :: Scientific/Engineering :: Artificial Intelligence",
|
|
15
|
+
]
|
|
16
|
+
dependencies = [
|
|
17
|
+
"pydantic (>=2.10,<3.0)",
|
|
18
|
+
"pyyaml (>=6.0,<7.0)",
|
|
19
|
+
"jinja2 (>=3.1,<4.0)",
|
|
20
|
+
"typer (>=0.15,<0.20)",
|
|
21
|
+
"rich (>=13.9,<15.0)",
|
|
22
|
+
"httpx (>=0.28,<0.29)",
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
[project.urls]
|
|
26
|
+
Repository = "https://github.com/daviunx/remuda"
|
|
27
|
+
Issues = "https://github.com/daviunx/remuda/issues"
|
|
28
|
+
Changelog = "https://github.com/daviunx/remuda/blob/main/CHANGELOG.md"
|
|
29
|
+
|
|
30
|
+
[project.scripts]
|
|
31
|
+
remuda = "remuda.cli.app:main"
|
|
32
|
+
|
|
33
|
+
[dependency-groups]
|
|
34
|
+
dev = [
|
|
35
|
+
"pytest (>=8.3.4,<10.0.0)",
|
|
36
|
+
"pytest-asyncio (>=1.1.0,<2.0.0)",
|
|
37
|
+
"pytest-cov (>=6.0.0,<7.0.0)",
|
|
38
|
+
"mypy (>=1.14,<2.0)",
|
|
39
|
+
"types-PyYAML (>=6.0,<7.0)",
|
|
40
|
+
# Pinned in the manifest and installed from it by CI. A gate that resolves
|
|
41
|
+
# its linter at run time reddens on someone else's release date
|
|
42
|
+
# (documentation/standards/python/03-tooling.md).
|
|
43
|
+
"ruff (>=0.16.0,<0.17.0)",
|
|
44
|
+
]
|
|
45
|
+
|
|
46
|
+
[build-system]
|
|
47
|
+
requires = ["poetry-core>=2.0.0,<3.0.0"]
|
|
48
|
+
build-backend = "poetry.core.masonry.api"
|
|
49
|
+
|
|
50
|
+
[tool.ruff]
|
|
51
|
+
line-length = 88
|
|
52
|
+
target-version = "py312"
|
|
53
|
+
exclude = [".git", ".venv", "__pycache__"]
|
|
54
|
+
|
|
55
|
+
[tool.ruff.lint]
|
|
56
|
+
# EXPLICIT, never inherited — a tool's default rule set is not a stable contract
|
|
57
|
+
# across versions (documentation/standards/python/03-tooling.md).
|
|
58
|
+
select = [
|
|
59
|
+
"E", # pycodestyle errors
|
|
60
|
+
"W", # pycodestyle warnings
|
|
61
|
+
"F", # Pyflakes
|
|
62
|
+
"I", # isort
|
|
63
|
+
"B", # flake8-bugbear
|
|
64
|
+
"C4", # flake8-comprehensions
|
|
65
|
+
"UP", # pyupgrade
|
|
66
|
+
"ARG", # flake8-unused-arguments
|
|
67
|
+
"SIM", # flake8-simplify
|
|
68
|
+
"TC", # flake8-type-checking
|
|
69
|
+
"PTH", # flake8-use-pathlib
|
|
70
|
+
"ERA", # eradicate (commented code)
|
|
71
|
+
"PL", # Pylint
|
|
72
|
+
"RUF", # Ruff-specific rules
|
|
73
|
+
]
|
|
74
|
+
ignore = [
|
|
75
|
+
"E501", # Line too long (handled by formatter)
|
|
76
|
+
"PLR0913", # Too many arguments
|
|
77
|
+
"PLR2004", # Magic value comparison
|
|
78
|
+
]
|
|
79
|
+
|
|
80
|
+
[tool.ruff.lint.per-file-ignores]
|
|
81
|
+
"tests/*" = ["ARG", "PLR2004"]
|
|
82
|
+
|
|
83
|
+
[tool.ruff.lint.isort]
|
|
84
|
+
known-first-party = ["remuda"]
|
|
85
|
+
|
|
86
|
+
[tool.ruff.format]
|
|
87
|
+
quote-style = "double"
|
|
88
|
+
indent-style = "space"
|
|
89
|
+
skip-magic-trailing-comma = false
|
|
90
|
+
line-ending = "auto"
|
|
91
|
+
|
|
92
|
+
[tool.mypy]
|
|
93
|
+
python_version = "3.12"
|
|
94
|
+
strict = true
|
|
95
|
+
warn_return_any = true
|
|
96
|
+
warn_unused_ignores = true
|
|
97
|
+
disallow_untyped_defs = true
|
|
98
|
+
disallow_incomplete_defs = true
|
|
99
|
+
check_untyped_defs = true
|
|
100
|
+
disallow_untyped_decorators = true
|
|
101
|
+
no_implicit_optional = true
|
|
102
|
+
warn_redundant_casts = true
|
|
103
|
+
warn_unused_configs = true
|
|
104
|
+
plugins = ["pydantic.mypy"]
|
|
105
|
+
|
|
106
|
+
[tool.pytest.ini_options]
|
|
107
|
+
testpaths = ["tests"]
|
|
108
|
+
asyncio_mode = "auto"
|
|
109
|
+
asyncio_default_fixture_loop_scope = "function"
|
|
110
|
+
addopts = ["-v", "--tb=short", "--strict-markers"]
|
|
111
|
+
markers = [
|
|
112
|
+
"slow: marks tests as slow",
|
|
113
|
+
"integration: marks tests as integration tests",
|
|
114
|
+
]
|
|
115
|
+
|
|
116
|
+
[tool.coverage.run]
|
|
117
|
+
source = ["remuda"]
|
|
118
|
+
branch = true
|
|
119
|
+
# The engine appends the ledger through asyncio.to_thread, so lines execute on
|
|
120
|
+
# worker threads; without this, coverage loses track of them
|
|
121
|
+
# (documentation/standards/testing/overview.md → Coverage Enforcement).
|
|
122
|
+
# `greenlet` is deliberately absent — remuda has no greenlet dependency and
|
|
123
|
+
# coverage errors out on a concurrency mode whose module is not installed.
|
|
124
|
+
concurrency = ["thread"]
|
|
125
|
+
omit = ["*/tests/*", "*/__pycache__/*"]
|
|
126
|
+
|
|
127
|
+
[tool.coverage.report]
|
|
128
|
+
exclude_lines = [
|
|
129
|
+
"pragma: no cover",
|
|
130
|
+
"def __repr__",
|
|
131
|
+
"raise NotImplementedError",
|
|
132
|
+
"if TYPE_CHECKING:",
|
|
133
|
+
"if __name__ == .__main__.:",
|
|
134
|
+
]
|
|
135
|
+
fail_under = 80
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
"""remuda — bulk LLM inference over pools of free/cheap models.
|
|
2
|
+
|
|
3
|
+
The herd of spare horses on a cattle drive: ride one until it tires, swap to
|
|
4
|
+
the next. Public API surface lives here; internal module paths are not a
|
|
5
|
+
contract.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from remuda.api import JobRunOutcome, run, run_job_dir, run_sync
|
|
9
|
+
from remuda.engine.plan import EngineError
|
|
10
|
+
from remuda.engine.progress import ProgressEvent
|
|
11
|
+
from remuda.engine.runner import RowResult, Runner
|
|
12
|
+
from remuda.errors import RemudaError
|
|
13
|
+
from remuda.inspection import (
|
|
14
|
+
CheckReport,
|
|
15
|
+
PreviewReport,
|
|
16
|
+
RenderedPrompt,
|
|
17
|
+
check_job_dir,
|
|
18
|
+
preview_job,
|
|
19
|
+
)
|
|
20
|
+
from remuda.ledger import (
|
|
21
|
+
LedgerEntry,
|
|
22
|
+
RenderError,
|
|
23
|
+
RenderRequest,
|
|
24
|
+
Report,
|
|
25
|
+
ResumeRefusedError,
|
|
26
|
+
RunStore,
|
|
27
|
+
SpecLock,
|
|
28
|
+
render,
|
|
29
|
+
)
|
|
30
|
+
from remuda.registry import (
|
|
31
|
+
DiscoverQuery,
|
|
32
|
+
ModelConfig,
|
|
33
|
+
Pool,
|
|
34
|
+
PoolEntry,
|
|
35
|
+
Provider,
|
|
36
|
+
Registry,
|
|
37
|
+
RegistryError,
|
|
38
|
+
RegistryValidationError,
|
|
39
|
+
default_config_dirs,
|
|
40
|
+
load_registry,
|
|
41
|
+
)
|
|
42
|
+
from remuda.rows import RowSourceError, read_columns, read_rows
|
|
43
|
+
from remuda.spec import (
|
|
44
|
+
ExtractSchema,
|
|
45
|
+
FieldSpec,
|
|
46
|
+
GenerateConstraints,
|
|
47
|
+
InputSpec,
|
|
48
|
+
Job,
|
|
49
|
+
MapTable,
|
|
50
|
+
Precondition,
|
|
51
|
+
PromptRenderError,
|
|
52
|
+
PromptSpec,
|
|
53
|
+
ScoreRange,
|
|
54
|
+
SpecValidationError,
|
|
55
|
+
)
|
|
56
|
+
from remuda.transport import (
|
|
57
|
+
CompletionRequest,
|
|
58
|
+
CompletionResult,
|
|
59
|
+
PermanentTransportError,
|
|
60
|
+
RateLimitedError,
|
|
61
|
+
TransientTransportError,
|
|
62
|
+
Transport,
|
|
63
|
+
TransportError,
|
|
64
|
+
Usage,
|
|
65
|
+
)
|
|
66
|
+
from remuda.validate import Outcome, Verdict, validate_answer
|
|
67
|
+
from remuda.version import __version__
|
|
68
|
+
|
|
69
|
+
__all__ = [
|
|
70
|
+
"CheckReport",
|
|
71
|
+
"CompletionRequest",
|
|
72
|
+
"CompletionResult",
|
|
73
|
+
"DiscoverQuery",
|
|
74
|
+
"EngineError",
|
|
75
|
+
"ExtractSchema",
|
|
76
|
+
"FieldSpec",
|
|
77
|
+
"GenerateConstraints",
|
|
78
|
+
"InputSpec",
|
|
79
|
+
"Job",
|
|
80
|
+
"JobRunOutcome",
|
|
81
|
+
"LedgerEntry",
|
|
82
|
+
"MapTable",
|
|
83
|
+
"ModelConfig",
|
|
84
|
+
"Outcome",
|
|
85
|
+
"PermanentTransportError",
|
|
86
|
+
"Pool",
|
|
87
|
+
"PoolEntry",
|
|
88
|
+
"Precondition",
|
|
89
|
+
"PreviewReport",
|
|
90
|
+
"ProgressEvent",
|
|
91
|
+
"PromptRenderError",
|
|
92
|
+
"PromptSpec",
|
|
93
|
+
"Provider",
|
|
94
|
+
"RateLimitedError",
|
|
95
|
+
"Registry",
|
|
96
|
+
"RegistryError",
|
|
97
|
+
"RegistryValidationError",
|
|
98
|
+
"RemudaError",
|
|
99
|
+
"RenderError",
|
|
100
|
+
"RenderRequest",
|
|
101
|
+
"RenderedPrompt",
|
|
102
|
+
"Report",
|
|
103
|
+
"ResumeRefusedError",
|
|
104
|
+
"RowResult",
|
|
105
|
+
"RowSourceError",
|
|
106
|
+
"RunStore",
|
|
107
|
+
"Runner",
|
|
108
|
+
"ScoreRange",
|
|
109
|
+
"SpecLock",
|
|
110
|
+
"SpecValidationError",
|
|
111
|
+
"TransientTransportError",
|
|
112
|
+
"Transport",
|
|
113
|
+
"TransportError",
|
|
114
|
+
"Usage",
|
|
115
|
+
"Verdict",
|
|
116
|
+
"__version__",
|
|
117
|
+
"check_job_dir",
|
|
118
|
+
"default_config_dirs",
|
|
119
|
+
"load_registry",
|
|
120
|
+
"preview_job",
|
|
121
|
+
"read_columns",
|
|
122
|
+
"read_rows",
|
|
123
|
+
"render",
|
|
124
|
+
"run",
|
|
125
|
+
"run_job_dir",
|
|
126
|
+
"run_sync",
|
|
127
|
+
"validate_answer",
|
|
128
|
+
]
|