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.
Files changed (72) hide show
  1. remuda-0.1.0/LICENSE +21 -0
  2. remuda-0.1.0/PKG-INFO +221 -0
  3. remuda-0.1.0/README.md +195 -0
  4. remuda-0.1.0/pyproject.toml +135 -0
  5. remuda-0.1.0/remuda/__init__.py +128 -0
  6. remuda-0.1.0/remuda/api.py +275 -0
  7. remuda-0.1.0/remuda/catalog/__init__.py +52 -0
  8. remuda-0.1.0/remuda/catalog/adapters.py +199 -0
  9. remuda-0.1.0/remuda/catalog/errors.py +32 -0
  10. remuda-0.1.0/remuda/catalog/models.py +70 -0
  11. remuda-0.1.0/remuda/catalog/resolve.py +244 -0
  12. remuda-0.1.0/remuda/cli/__init__.py +5 -0
  13. remuda-0.1.0/remuda/cli/app.py +76 -0
  14. remuda-0.1.0/remuda/cli/commands/__init__.py +20 -0
  15. remuda-0.1.0/remuda/cli/commands/check.py +69 -0
  16. remuda-0.1.0/remuda/cli/commands/init_job.py +59 -0
  17. remuda-0.1.0/remuda/cli/commands/inline_run.py +164 -0
  18. remuda-0.1.0/remuda/cli/commands/models.py +95 -0
  19. remuda-0.1.0/remuda/cli/commands/pools.py +162 -0
  20. remuda-0.1.0/remuda/cli/commands/preview.py +81 -0
  21. remuda-0.1.0/remuda/cli/commands/render.py +121 -0
  22. remuda-0.1.0/remuda/cli/commands/report.py +47 -0
  23. remuda-0.1.0/remuda/cli/commands/run.py +295 -0
  24. remuda-0.1.0/remuda/cli/commands/stats.py +202 -0
  25. remuda-0.1.0/remuda/cli/console.py +72 -0
  26. remuda-0.1.0/remuda/cli/options.py +74 -0
  27. remuda-0.1.0/remuda/cli/progress.py +11 -0
  28. remuda-0.1.0/remuda/cli/reporting.py +82 -0
  29. remuda-0.1.0/remuda/cli/scaffold.py +113 -0
  30. remuda-0.1.0/remuda/engine/__init__.py +30 -0
  31. remuda-0.1.0/remuda/engine/ladder.py +241 -0
  32. remuda-0.1.0/remuda/engine/lanes.py +163 -0
  33. remuda-0.1.0/remuda/engine/packing.py +131 -0
  34. remuda-0.1.0/remuda/engine/plan.py +82 -0
  35. remuda-0.1.0/remuda/engine/progress.py +40 -0
  36. remuda-0.1.0/remuda/engine/runner.py +406 -0
  37. remuda-0.1.0/remuda/errors.py +9 -0
  38. remuda-0.1.0/remuda/health.py +102 -0
  39. remuda-0.1.0/remuda/inline.py +148 -0
  40. remuda-0.1.0/remuda/inspection.py +120 -0
  41. remuda-0.1.0/remuda/ledger/__init__.py +55 -0
  42. remuda-0.1.0/remuda/ledger/models.py +123 -0
  43. remuda-0.1.0/remuda/ledger/render.py +220 -0
  44. remuda-0.1.0/remuda/ledger/resume.py +89 -0
  45. remuda-0.1.0/remuda/ledger/store.py +298 -0
  46. remuda-0.1.0/remuda/preflight.py +53 -0
  47. remuda-0.1.0/remuda/py.typed +0 -0
  48. remuda-0.1.0/remuda/registry/__init__.py +46 -0
  49. remuda-0.1.0/remuda/registry/bootstrap.py +158 -0
  50. remuda-0.1.0/remuda/registry/errors.py +25 -0
  51. remuda-0.1.0/remuda/registry/loader.py +167 -0
  52. remuda-0.1.0/remuda/registry/models.py +200 -0
  53. remuda-0.1.0/remuda/registry/registry.py +193 -0
  54. remuda-0.1.0/remuda/rows.py +153 -0
  55. remuda-0.1.0/remuda/spec/__init__.py +55 -0
  56. remuda-0.1.0/remuda/spec/errors.py +30 -0
  57. remuda-0.1.0/remuda/spec/lint.py +99 -0
  58. remuda-0.1.0/remuda/spec/loader.py +210 -0
  59. remuda-0.1.0/remuda/spec/models.py +297 -0
  60. remuda-0.1.0/remuda/spec/prompt.py +60 -0
  61. remuda-0.1.0/remuda/spec/shapes.py +140 -0
  62. remuda-0.1.0/remuda/stats.py +152 -0
  63. remuda-0.1.0/remuda/transport/__init__.py +35 -0
  64. remuda-0.1.0/remuda/transport/errors.py +33 -0
  65. remuda-0.1.0/remuda/transport/models.py +60 -0
  66. remuda-0.1.0/remuda/transport/openai_compat.py +234 -0
  67. remuda-0.1.0/remuda/transport/opencode.py +191 -0
  68. remuda-0.1.0/remuda/validate/__init__.py +13 -0
  69. remuda-0.1.0/remuda/validate/answers.py +229 -0
  70. remuda-0.1.0/remuda/validate/preconditions.py +31 -0
  71. remuda-0.1.0/remuda/validate/verdict.py +30 -0
  72. 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
+ [![Test](https://github.com/daviunx/remuda/actions/workflows/test.yml/badge.svg)](https://github.com/daviunx/remuda/actions/workflows/test.yml)
31
+ [![PyPI](https://img.shields.io/pypi/v/remuda)](https://pypi.org/project/remuda/)
32
+ [![Python](https://img.shields.io/pypi/pyversions/remuda)](https://pypi.org/project/remuda/)
33
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](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
+ [![Test](https://github.com/daviunx/remuda/actions/workflows/test.yml/badge.svg)](https://github.com/daviunx/remuda/actions/workflows/test.yml)
6
+ [![PyPI](https://img.shields.io/pypi/v/remuda)](https://pypi.org/project/remuda/)
7
+ [![Python](https://img.shields.io/pypi/pyversions/remuda)](https://pypi.org/project/remuda/)
8
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](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
+ ]