openjev-sdk 0.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. openjev_sdk-0.2.0/.gitignore +46 -0
  2. openjev_sdk-0.2.0/LICENSE +21 -0
  3. openjev_sdk-0.2.0/PKG-INFO +367 -0
  4. openjev_sdk-0.2.0/README.md +329 -0
  5. openjev_sdk-0.2.0/openjev/__init__.py +33 -0
  6. openjev_sdk-0.2.0/openjev/cli.py +286 -0
  7. openjev_sdk-0.2.0/openjev/compute.py +68 -0
  8. openjev_sdk-0.2.0/openjev/decision.py +231 -0
  9. openjev_sdk-0.2.0/openjev/decision_cli.py +179 -0
  10. openjev_sdk-0.2.0/openjev/decision_experiment.py +246 -0
  11. openjev_sdk-0.2.0/openjev/decision_mlx.py +209 -0
  12. openjev_sdk-0.2.0/openjev/decision_probability.py +223 -0
  13. openjev_sdk-0.2.0/openjev/features.py +126 -0
  14. openjev_sdk-0.2.0/openjev/head.py +122 -0
  15. openjev_sdk-0.2.0/openjev/head_torch.py +146 -0
  16. openjev_sdk-0.2.0/openjev/scorer.py +254 -0
  17. openjev_sdk-0.2.0/openjev/server.py +103 -0
  18. openjev_sdk-0.2.0/openjev/systemone.py +190 -0
  19. openjev_sdk-0.2.0/openjev/tod/__init__.py +16 -0
  20. openjev_sdk-0.2.0/openjev/tod/chat_traces.py +172 -0
  21. openjev_sdk-0.2.0/openjev/tod/cli.py +42 -0
  22. openjev_sdk-0.2.0/openjev/tod/envs/__init__.py +1 -0
  23. openjev_sdk-0.2.0/openjev/tod/envs/shopdesk.py +191 -0
  24. openjev_sdk-0.2.0/openjev/tod/rl.py +346 -0
  25. openjev_sdk-0.2.0/openjev/tod/schema.py +180 -0
  26. openjev_sdk-0.2.0/openjev/tod/spec.py +205 -0
  27. openjev_sdk-0.2.0/openjev/tod/train.py +143 -0
  28. openjev_sdk-0.2.0/openjev/torch_backend.py +119 -0
  29. openjev_sdk-0.2.0/openjev/train.py +86 -0
  30. openjev_sdk-0.2.0/pyproject.toml +58 -0
  31. openjev_sdk-0.2.0/tests/test_chess_benchmark.py +181 -0
  32. openjev_sdk-0.2.0/tests/test_chess_calibration.py +88 -0
  33. openjev_sdk-0.2.0/tests/test_decision.py +178 -0
  34. openjev_sdk-0.2.0/tests/test_decision_mlx.py +60 -0
  35. openjev_sdk-0.2.0/tests/test_decision_probability.py +125 -0
  36. openjev_sdk-0.2.0/tests/test_head_torch.py +77 -0
  37. openjev_sdk-0.2.0/tests/test_package_api.py +48 -0
  38. openjev_sdk-0.2.0/tests/test_portability.py +46 -0
  39. openjev_sdk-0.2.0/tests/test_rl_2048_game.py +57 -0
  40. openjev_sdk-0.2.0/tests/test_tod.py +191 -0
  41. openjev_sdk-0.2.0/tests/test_torch_backend.py +117 -0
@@ -0,0 +1,46 @@
1
+ .venv/
2
+ models/
3
+ __pycache__/
4
+ *.pyc
5
+ .DS_Store
6
+ runs/
7
+ *.npz
8
+ # ViZDoom writes this config into the cwd on first launch
9
+ _vizdoom.ini
10
+
11
+ # mkdocs build output
12
+ site/
13
+ adapters/
14
+ data/chess/
15
+ data/wikiracing/
16
+
17
+ # Local planning notes
18
+ /ideas/
19
+
20
+ # Built Python distributions
21
+ /dist/
22
+
23
+ # Claude Code local settings and session files
24
+ .claude/
25
+ CLAUDE.local.md
26
+
27
+ # Temp, editor and tool files
28
+ *.tmp
29
+ *.temp
30
+ *.swp
31
+ *.swo
32
+ *~
33
+ *.log
34
+ .idea/
35
+ .vscode/
36
+ .pytest_cache/
37
+ .mypy_cache/
38
+ .ruff_cache/
39
+ .ipynb_checkpoints/
40
+ .coverage
41
+ htmlcov/
42
+ *.egg-info/
43
+ build/
44
+ .env
45
+ .env.*
46
+ graphify-out/
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Dasein Labs
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,367 @@
1
+ Metadata-Version: 2.5
2
+ Name: openjev-sdk
3
+ Version: 0.2.0
4
+ Summary: Train small task-oriented decision models on your own traces: pick, calibrate, defer, and improve with RL
5
+ Project-URL: Homepage, https://github.com/daseinlabs/open-jev
6
+ Project-URL: Documentation, https://github.com/daseinlabs/open-jev/blob/main/docs/tod-sdk.md
7
+ Project-URL: Cookbooks, https://github.com/daseinlabs/open-jev/tree/main/cookbooks
8
+ Author: Dasein Labs
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Keywords: agents,cuda,decision model,fine-tuning,gemma,mlx,pytorch,reinforcement learning,tool calling
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Environment :: GPU :: NVIDIA CUDA
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: Intended Audience :: Science/Research
16
+ Classifier: Operating System :: OS Independent
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
19
+ Requires-Python: >=3.12
20
+ Requires-Dist: fastapi>=0.141.1
21
+ Requires-Dist: huggingface-hub>=1.31.0
22
+ Requires-Dist: mlx-lm>=0.24; sys_platform == 'darwin' and platform_machine == 'arm64'
23
+ Requires-Dist: mlx>=0.24; sys_platform == 'darwin' and platform_machine == 'arm64'
24
+ Requires-Dist: numpy>=2.5.3
25
+ Requires-Dist: safetensors>=0.8.0
26
+ Requires-Dist: torch>=2.6; sys_platform != 'darwin' or platform_machine != 'arm64'
27
+ Requires-Dist: transformers>=5.17.0
28
+ Requires-Dist: uvicorn[standard]>=0.53.0
29
+ Provides-Extra: doom
30
+ Requires-Dist: vizdoom>=1.2.4; extra == 'doom'
31
+ Provides-Extra: finetune
32
+ Requires-Dist: chess>=1.10; extra == 'finetune'
33
+ Requires-Dist: pyarrow>=15; extra == 'finetune'
34
+ Provides-Extra: torch
35
+ Requires-Dist: accelerate>=1.0; extra == 'torch'
36
+ Requires-Dist: torch>=2.6; extra == 'torch'
37
+ Description-Content-Type: text/markdown
38
+
39
+ # openjev
40
+
41
+ One-pass option scoring with a local Gemma 3 4B: MLX on Apple silicon, or PyTorch on Windows and Linux (CPU or GPU).
42
+ Design notes: [docs/design/one-pass-option-scoring.md](docs/design/one-pass-option-scoring.md);
43
+ per-task training: [docs/design/per-task-finetuning-with-gemma.md](docs/design/per-task-finetuning-with-gemma.md).
44
+
45
+ Given a context and a list of pre-written options, the model prefills the
46
+ context once, expands that KV cache across the option batch, and scores every
47
+ option in a single padded forward pass. No decoding. The score is the
48
+ log-probability of the option tokens given the context; a softmax over the
49
+ option scores gives a probability per option, like `jevlike-predict`.
50
+
51
+ ## Train a decision model on your own data
52
+
53
+ `openjev.tod` trains small task-oriented decision models: a frozen Gemma backbone plus a
54
+ ~2M-parameter head that picks one option from a list your code supplies (which tool,
55
+ which order, which team), reports a calibrated confidence, and defers to a bigger model
56
+ when unsure. Runs on NVIDIA/AMD GPUs (PyTorch) and Apple silicon (MLX). Install with
57
+ `pip install openjev-sdk`.
58
+
59
+ ```python
60
+ from openjev.tod import Candidate, DecisionSpec, Episode, Kind, build_dataset
61
+
62
+ TEAMS = {'billing': 'Billing and refunds', 'shipping': 'Delivery and tracking'}
63
+ spec = DecisionSpec('routing', [Kind('team', 'Which team should handle this?',
64
+ lambda state: [Candidate(k, v, payload=k) for k, v in TEAMS.items()], version='1')])
65
+ episodes = [Episode(t['id'], (spec.decision(t['id'], 'team', {'message': t['text']}, choice=t['team']),))
66
+ for t in tickets]
67
+ build_dataset(spec, episodes).write('runs/routing')
68
+ ```
69
+
70
+ ```bash
71
+ openjev tod train runs/routing --model models/gemma-3-4b-it --out runs/routing-model
72
+ ```
73
+
74
+ Chat traces, RL in a simulator, and deriving the spec from your tool schemas are covered in
75
+ the [SDK guide](https://github.com/daseinlabs/open-jev/blob/main/docs/tod-sdk.md) and the three [cookbooks](https://github.com/daseinlabs/open-jev/tree/main/cookbooks).
76
+
77
+ ## Demo: Doom in the terminal
78
+
79
+ `demo/doom/` runs ViZDoom headless, describes each frame in a line of text, and
80
+ lets the server rank the action menu with one `/score` call (or one System One
81
+ `choice` question). Start `make serve`, then `make doom`. Details and keys in
82
+ [`demo/doom/README.md`](demo/doom/README.md).
83
+
84
+ ![openjev playing Doom in the terminal: the model ranks the action menu each step](https://raw.githubusercontent.com/daseinlabs/open-jev/main/docs/media/doom-recording.gif)
85
+
86
+ Full-resolution recording: [docs/media/doom-recording.mov](docs/media/doom-recording.mov).
87
+
88
+ ## Setup
89
+
90
+ For use inside a Python application or notebook, install this checkout with
91
+ `python -m pip install .` (Python 3.12+). See the [Python package guide](docs/python-package.md)
92
+ for decision inference, feature extraction, training, and wheel installation.
93
+
94
+ ### Windows and Linux
95
+
96
+ Install Python 3.12+ and [uv](https://docs.astral.sh/uv/getting-started/installation/), then run these commands from the repository in PowerShell or a Linux shell:
97
+
98
+ ```sh
99
+ uv sync
100
+ uv run hf auth login
101
+ uv run hf download google/gemma-3-4b-it --local-dir models/gemma-3-4b-it
102
+ uv run openjev serve --backend torch --device auto --port 8000
103
+ ```
104
+
105
+ Accept the Gemma license on Hugging Face before downloading. You can also pass
106
+ `--model google/gemma-3-4b-it` to load directly from the Hugging Face cache.
107
+ No Make, Xcode, shell activation, or `.venv/bin` paths are needed here.
108
+
109
+ `--backend auto` (the default) selects MLX on Apple silicon and PyTorch elsewhere.
110
+ For PyTorch, `--device auto` selects CUDA when available, then MPS, then CPU.
111
+ Use `--device cuda` or `--device cuda:1` to require a specific GPU (fails clearly
112
+ if CUDA is unavailable), or `--device cpu` to force CPU inference.
113
+
114
+ For NVIDIA acceleration, install the GPU driver and a matching PyTorch build using
115
+ the [official PyTorch installer](https://pytorch.org/get-started/locally/).
116
+ Run its pip command in this repository's virtual environment (use `uv pip install`
117
+ in place of `pip3 install`). After a custom PyTorch install, use `uv run --no-sync`
118
+ so uv does not replace your selected build. On supported Linux AMD systems,
119
+ PyTorch ROCm builds also use the `cuda` device name.
120
+
121
+ ```sh
122
+ uv run --no-sync python -c "import torch; print(torch.__version__, torch.cuda.is_available())"
123
+ uv run --no-sync openjev serve --backend torch --device cuda --batch-size 2
124
+ uv run --no-sync openjev check --backend torch --device cuda
125
+ uv run --no-sync openjev score --backend torch --device cuda --context "The capital of France is" --option " Paris" --option " Berlin"
126
+ ```
127
+
128
+ The base 4B weights need roughly 8 GB just for GPU model weights at 16-bit precision,
129
+ plus memory for activations and the option KV caches. Reduce `--batch-size` and
130
+ context length if you run out of GPU memory. CPU runs use float32 and need more RAM.
131
+ Use original Hugging Face weights with PyTorch; MLX quantized weights and MLX LoRA
132
+ adapters are not supported by this backend.
133
+
134
+ Scoring, evaluation, benchmarks, correctness checks, both HTTP scoring endpoints,
135
+ frozen-feature extraction, head training and evaluation, `openjev decision`, and
136
+ `openjev tod` (including RL) support PyTorch. Contextual features (`--contextual`) and the
137
+ chess LoRA training workflow still require MLX on Apple silicon. The Doom terminal UI
138
+ also has its own platform dependencies; cross-platform support here covers the
139
+ scoring CLI and HTTP server.
140
+
141
+ ### Apple silicon / existing Make workflow
142
+
143
+ ```sh
144
+ make setup # uv sync (arm64 Python 3.12 venv) + download google/gemma-3-4b-it into models/ (gated; needs HF login)
145
+ make serve # start the HTTP server on :8000
146
+ make health # curl /health
147
+ make request # example curl against /score
148
+ make systemone # TypeSafe-style request against /v1/systemone
149
+ make check # verify cached batched scoring against naive re-encoding
150
+ make bench # latency benchmark
151
+ make eval DATA=data/synthetic/test.jsonl
152
+ ```
153
+
154
+ `make setup` also installs the optional `torch` extra, used only for the jevlike comparison and HF cross-checks.
155
+ `make` on macOS needs the Xcode licence accepted (`sudo xcodebuild -license accept`) or Homebrew's `gmake`.
156
+
157
+ ## Usage
158
+
159
+ ```sh
160
+ # Rank options for one context (prints probability, score, raw sum, token count)
161
+ .venv/bin/openjev score --context "The capital of France is" \
162
+ --option " Paris" --option " Berlin" --option " Lyon"
163
+
164
+ # Chat template (context as user turn, options scored as the reply) + PMI normalisation
165
+ .venv/bin/openjev score --chat --norm pmi --context "..." --option "..." --option "..."
166
+
167
+ # Predefined options: one per line in a text file, reused for every context
168
+ .venv/bin/openjev score --options-file options.txt --context "..."
169
+ .venv/bin/openjev eval contexts.jsonl --fixed-options options.txt # rows need only {"context": ...}; add "label" for accuracy
170
+
171
+ # Top-1 / top-3 accuracy on jevlike-style JSONL: {"context": ..., "options": [...], "label": 0}
172
+ .venv/bin/openjev eval data.jsonl --norm mean
173
+
174
+ # Verify the cached batched path against naive re-encoding, and benchmark it
175
+ .venv/bin/openjev check
176
+ .venv/bin/openjev bench --context-tokens 200 --options 8 --option-tokens 30
177
+ ```
178
+
179
+ ## Server
180
+
181
+ Loads the model once. The measured Apple silicon MLX path answers scoring requests in about
182
+ 90 ms each; PyTorch latency depends on your CPU/GPU. Use `uv run openjev serve` on any
183
+ supported platform, or the Make shortcut below on macOS.
184
+
185
+ ```sh
186
+ make serve # = .venv/bin/openjev serve --port 8000
187
+ curl -s localhost:8000/score -H 'content-type: application/json' -d '{
188
+ "context": "Customer: my order arrived broken. Agent:",
189
+ "options": [" I am sorry, I will send a replacement.", " Please read our returns policy."],
190
+ "norm": "mean", "chat": false, "sep": ""
191
+ }'
192
+ ```
193
+
194
+ The response has `best`, `best_index`, per-option `probability` / `logprob_sum` / `n_tokens`, and `timing`.
195
+
196
+ ## TypeSafe System One contract
197
+
198
+ `POST /v1/systemone` implements the request/response shape documented at
199
+ [docs.typesafe.ai](https://docs.typesafe.ai): a `state` (string, object or array) plus a map of typed
200
+ `questions`, answered against that state.
201
+
202
+ ```sh
203
+ make systemone # sends examples/systemone-quickstart.json; or:
204
+ curl -s localhost:8000/v1/systemone -H 'content-type: application/json' -d '{
205
+ "state": "I ordered size 10 shoes but received size 8. Please send the right size.",
206
+ "model": "jev-latest",
207
+ "questions": {
208
+ "department": {"type": "choice", "instructions": "Which department handles this?",
209
+ "criteria": {"returns": "Returns and exchanges", "shipping": "Delivery issues", "billing": "Charges and refunds"}},
210
+ "severity": {"type": "score", "instructions": "How severe is the problem?",
211
+ "criteria": ["minor", "moderate: wrong item", "major: safety or financial loss"]},
212
+ "wants_refund":{"type": "noul", "instructions": "Is the customer asking for a refund to their card?"}
213
+ }
214
+ }'
215
+ ```
216
+
217
+ | question `type` | request `criteria` | answer fields |
218
+ |---|---|---|
219
+ | `choice` | map of option name to description (string, object, array or null); up to 255 options | `choice`, `probabilities` (sum to 1), `confidence` |
220
+ | `score` | ordered array of level descriptions | `score` (probability-weighted mean of level index), `probabilities` keyed `"0".."n-1"`, `legend`, `confidence` |
221
+ | `noul` | optional `{"true": ..., "false": ...}` | `noul` = probability of yes |
222
+
223
+ Response: `{"model", "answers": {id: answer}, "usage": {"input_tokens", "output_tokens"}}`.
224
+ Set `OPENJEV_API_KEY` before `make serve` to require `Authorization: Bearer <key>` on this route.
225
+
226
+ How it maps onto the scorer: each question is rendered to a plain-text prompt (state, instructions,
227
+ options or levels, then `Answer:` and a newline) and the option names, level numbers, or `yes`/`no`
228
+ are scored as continuations in one prefix-shared batched pass per question. `confidence` is 1 minus
229
+ the normalised entropy of the distribution; TypeSafe does not publish its formula, so treat it as an
230
+ approximation. `usage.output_tokens` counts the candidate-label tokens that were scored.
231
+
232
+ Comparison on the docs' quick-start request (`examples/systemone-quickstart.json`), Gemma 3 4B
233
+ zero-shot vs the numbers TypeSafe publishes for Jev:
234
+
235
+ | answer | Jev (docs) | openjev / Gemma 3 4B |
236
+ |---|---|---|
237
+ | `department.choice` | technical, p=0.84, confidence 0.60 | technical, p=1.00, confidence 1.00 |
238
+ | `frustration.score` | 1.04 (annoyed but polite) | 2.00 (furious) |
239
+ | `is_urgent.noul` | 0.999 | 0.005 |
240
+
241
+ Same routing decision, but Gemma is over-confident and disagrees on the two judgement calls. Zero-shot
242
+ probabilities are softmaxed next-token likelihoods, not calibrated judgements. Closing the gap means
243
+ labelled data and a trained head (below).
244
+
245
+ ## Native candidate-decision model
246
+
247
+ `openjev decision` trains a shared candidate-scoring head on frozen features and
248
+ reloads it for raw state/question/candidate requests. It includes versioned task
249
+ validation, feature and artifact fingerprints, held-out evaluation against
250
+ continuation scoring, and candidate-order/repeated-request checks.
251
+
252
+ See [the native decision-head guide](docs/decision-head.md) for data formats and
253
+ commands. It runs on MLX or PyTorch (`OPENJEV_BACKEND=mlx|torch`, `OPENJEV_DEVICE=cuda|mps|cpu`).
254
+
255
+ ## Training a head (per-task, on frozen Gemma features)
256
+
257
+ ```sh
258
+ make features # one prefix-shared pass per example, cached to runs/feats/{train,validation,test}.npz
259
+ make train # jevlike's cross-attention head (MLX or PyTorch), listwise cross-entropy -> runs/head.safetensors
260
+ make eval-head # top-1/top-3, ECE, and the shuffled-context control on the test split
261
+ ```
262
+
263
+ - `openjev features DATA --out F.npz` stops Gemma before the LM head, keeps every context token's
264
+ final hidden state and the masked-mean of each option's tokens (float16). Options are encoded on
265
+ their own, as in jevlike, so the head has to do the matching. `--contextual` encodes them as
266
+ continuations of the context instead: stronger features, but the match leaks into the option
267
+ vectors and the shuffled-context control below stops meaning anything.
268
+ - `openjev train` = AdamW 5e-4 (2e-3 diverges on Gemma features, whose norms are ~115), weight decay 1e-4, grad clip 1.0, 8 epochs, batch 64, best validation
269
+ epoch kept. The checkpoint is `head.safetensors` plus `head.json` (rank, hidden size, training config).
270
+ - `openjev eval-head` reports what `jevlike-eval` reports: top-1, top-3, 10-bin expected calibration
271
+ error, and the same metrics with every example paired with another example's context. If the
272
+ shuffled number does not collapse, the head is reading option priors rather than the state.
273
+
274
+ Result on the synthetic split (2000 train / 400 validation / 400 test, 2026-09-17): features at
275
+ 77 ms per example, head training 8 epochs in about 15 s.
276
+
277
+ | | top-1 | top-3 | ECE |
278
+ |---|---|---|---|
279
+ | head on frozen Gemma features, test | 0.970 | 1.000 | 0.027 |
280
+ | same head, shuffled-context control | 0.258 | 0.698 | 0.724 |
281
+ | jevlike tiny byte encoder, test | 0.998 | 1.000 | 0.008 |
282
+ | Gemma zero-shot, `--norm sum`, test | 1.000 | 1.000 | n/a |
283
+
284
+ The control sits at chance (about 4.5 options per row), so the head really matches options to the
285
+ state, and its ECE of 0.03 is what a calibrated `confidence` looks like.
286
+
287
+ Python:
288
+
289
+ ```python
290
+ from openjev import OptionScorer
291
+ s = OptionScorer("models/gemma-3-4b-it", batch_size=8)
292
+ for r in s.score("The capital of France is", [" Paris", " Berlin"], norm="mean"):
293
+ print(r.option, r.probability, r.logprob_sum, r.n_tokens)
294
+ print(s.last_timing)
295
+ ```
296
+
297
+ ### Normalisation (`--norm`)
298
+
299
+ | norm | score | use when |
300
+ |---|---|---|
301
+ | `mean` (default) | sum of option-token log-probs divided by token count | options differ in length |
302
+ | `sum` | total log-prob | options are the same length or you want raw likelihood |
303
+ | `pmi` | sum minus the option's unconditional log-prob (BOS-only context) | options differ in base-rate plausibility; costs one extra batched pass |
304
+
305
+ ## Measured on M5 Pro, 64 GB (bf16, mlx-lm)
306
+
307
+ Workload: 202-token context, 8 options, 242 option tokens total.
308
+
309
+ | path | median latency |
310
+ |---|---|
311
+ | context cached once, options batched | 0.17 s |
312
+ | context re-encoded per option, no cache | 0.68 s |
313
+
314
+ Model load is about 1 s from a warm disk. First forward pass adds under a second of warm-up.
315
+
316
+ ## Validating against jevlike
317
+
318
+ `jevlike` is installed into the same venv (`uv pip install -e ../../vinnylarouge/jevlike`), so both
319
+ CLIs read the same JSONL. Generate its synthetic menu set, then score it both ways:
320
+
321
+ ```sh
322
+ .venv/bin/jevlike-data synthetic --output data/synthetic
323
+ .venv/bin/openjev eval data/synthetic/test.jsonl --norm sum --sep $'\nChoice: '
324
+ .venv/bin/jevlike-train data/synthetic/train.jsonl --validation data/synthetic/validation.jsonl \
325
+ --output runs/synthetic-tiny.pt --device mps
326
+ .venv/bin/jevlike-eval runs/synthetic-tiny.pt data/synthetic/test.jsonl --device mps
327
+ ```
328
+
329
+ Results on the 400-row synthetic test set (2026-09-16):
330
+
331
+ | scorer | training | top-1 | top-3 | median latency / example |
332
+ |---|---|---|---|---|
333
+ | openjev, Gemma 3 4B zero-shot, `--norm sum` | none | 1.000 | 1.000 | 0.086 s |
334
+ | openjev, `--norm mean` | none | 0.988 | 1.000 | 0.086 s |
335
+ | openjev, `--norm pmi` | none | 0.988 | 1.000 | 0.153 s |
336
+ | jevlike tiny byte encoder + head | 2000 rows, 8 epochs | 0.998 | 1.000 | well under 10 ms |
337
+
338
+ The synthetic task is easy for both. The real validation is your own labelled rows: run
339
+ `openjev eval` on them zero-shot and compare against a `jevlike-train`/`jevlike-eval` run on the
340
+ same split. If Gemma zero-shot is close to the trained head, Route B is enough; if not, train a head
341
+ (Route A) with `make features && make train && make eval-head`.
342
+
343
+ ## Layout
344
+
345
+ - `openjev/torch_backend.py`: PyTorch CPU/GPU inference and shared-prefix KV cache scoring.
346
+ - `openjev/scorer.py`: `OptionScorer` (prefill, cache expansion, batched scoring, naive reference).
347
+ - `openjev/systemone.py`: System One request/answer models, prompt renderers, zero-shot answers.
348
+ - `openjev/server.py`: FastAPI app, `/health`, `/score`, `/v1/systemone`.
349
+ - `openjev/features.py`: frozen-Gemma feature extraction and the `.npz` feature cache.
350
+ - `openjev/compute.py`: backend selection (MLX or PyTorch) for features, heads and training.
351
+ - `openjev/head.py`, `openjev/head_torch.py`: jevlike's cross-attention head in `mlx.nn` and `torch.nn`; the saved weights load in either.
352
+ - `openjev/train.py`: head training loop and evaluation (top-k, ECE, shuffled-context control).
353
+ - `openjev/cli.py`: `openjev score | eval | bench | check | serve | features | train | eval-head`.
354
+ - `demo/doom/`: Doom in the terminal, the server picks every action (`make doom`).
355
+ - `models/`: downloaded weights (git-ignored).
356
+
357
+ ## Development checks
358
+
359
+ ```sh
360
+ uv sync --extra torch --group test
361
+ uv run python -m unittest discover -s tests -v
362
+ ```
363
+
364
+ The offline tests use a tiny randomly initialized Gemma model to compare cached,
365
+ padded scoring with full re-encoding, including sliding-window attention, multiple
366
+ batch sizes, normalization, and repeated requests. CI runs them on Windows, Linux,
367
+ and macOS. Actual GPU availability and throughput must be checked on your hardware.