open-jev README

The project's README from github.com/daseinlabs/open-jev. As one markdown file: README.md.

openjev

One-pass option scoring with a local Gemma 3 4B on Apple silicon via MLX. Design notes: docs/design/one-pass-option-scoring.md; per-task training: docs/design/per-task-finetuning-with-gemma.md.

Given a context and a list of pre-written options, the model prefills the context once, expands that KV cache across the option batch, and scores every option in a single padded forward pass. No decoding. The score is the log-probability of the option tokens given the context; a softmax over the option scores gives a probability per option, like jevlike-predict.

Setup

make setup       # uv sync (arm64 Python 3.12 venv) + download google/gemma-3-4b-it into models/ (gated; needs HF login)
make serve       # start the HTTP server on :8000
make health      # curl /health
make request     # example curl against /score
make systemone   # TypeSafe-style request against /v1/systemone
make check       # verify cached batched scoring against naive re-encoding
make bench       # latency benchmark
make eval DATA=data/synthetic/test.jsonl

make setup also installs the optional torch extra, used only for the jevlike comparison and HF cross-checks. make on macOS needs the Xcode licence accepted (sudo xcodebuild -license accept) or Homebrew's gmake.

Usage

# Rank options for one context (prints probability, score, raw sum, token count)
.venv/bin/openjev score --context "The capital of France is" \
    --option " Paris" --option " Berlin" --option " Lyon"

# Chat template (context as user turn, options scored as the reply) + PMI normalisation
.venv/bin/openjev score --chat --norm pmi --context "..." --option "..." --option "..."

# Predefined options: one per line in a text file, reused for every context
.venv/bin/openjev score --options-file options.txt --context "..."
.venv/bin/openjev eval contexts.jsonl --fixed-options options.txt   # rows need only {"context": ...}; add "label" for accuracy

# Top-1 / top-3 accuracy on jevlike-style JSONL: {"context": ..., "options": [...], "label": 0}
.venv/bin/openjev eval data.jsonl --norm mean

# Verify the cached batched path against naive re-encoding, and benchmark it
.venv/bin/openjev check
.venv/bin/openjev bench --context-tokens 200 --options 8 --option-tokens 30

Server

Loads the model once and answers scoring requests in about 90 ms each. Runs natively on macOS with Metal; there is no container path because Linux containers cannot reach the Apple GPU.

make serve                                     # = .venv/bin/openjev serve --port 8000
curl -s localhost:8000/score -H 'content-type: application/json' -d '{
  "context": "Customer: my order arrived broken. Agent:",
  "options": [" I am sorry, I will send a replacement.", " Please read our returns policy."],
  "norm": "mean", "chat": false, "sep": ""
}'

The response has best, best_index, per-option probability / logprob_sum / n_tokens, and timing.

TypeSafe System One contract

POST /v1/systemone implements the request/response shape documented at docs.typesafe.ai: a state (string, object or array) plus a map of typed questions, answered against that state.

make systemone     # sends examples/systemone-quickstart.json; or:
curl -s localhost:8000/v1/systemone -H 'content-type: application/json' -d '{
  "state": "I ordered size 10 shoes but received size 8. Please send the right size.",
  "model": "jev-latest",
  "questions": {
    "department":  {"type": "choice", "instructions": "Which department handles this?",
                    "criteria": {"returns": "Returns and exchanges", "shipping": "Delivery issues", "billing": "Charges and refunds"}},
    "severity":    {"type": "score",  "instructions": "How severe is the problem?",
                    "criteria": ["minor", "moderate: wrong item", "major: safety or financial loss"]},
    "wants_refund":{"type": "noul",   "instructions": "Is the customer asking for a refund to their card?"}
  }
}'
question type request criteria answer fields
choice map of option name to description (string, object, array or null); up to 255 options choice, probabilities (sum to 1), confidence
score ordered array of level descriptions score (probability-weighted mean of level index), probabilities keyed "0".."n-1", legend, confidence
noul optional {"true": ..., "false": ...} noul = probability of yes

Response: {"model", "answers": {id: answer}, "usage": {"input_tokens", "output_tokens"}}. Set OPENJEV_API_KEY before make serve to require Authorization: Bearer <key> on this route.

How it maps onto the scorer: each question is rendered to a plain-text prompt (state, instructions, options or levels, then Answer: and a newline) and the option names, level numbers, or yes/no are scored as continuations in one prefix-shared batched pass per question. confidence is 1 minus the normalised entropy of the distribution; TypeSafe does not publish its formula, so treat it as an approximation. usage.output_tokens counts the candidate-label tokens that were scored.

Comparison on the docs' quick-start request (examples/systemone-quickstart.json), Gemma 3 4B zero-shot vs the numbers TypeSafe publishes for Jev:

answer Jev (docs) openjev / Gemma 3 4B
department.choice technical, p=0.84, confidence 0.60 technical, p=1.00, confidence 1.00
frustration.score 1.04 (annoyed but polite) 2.00 (furious)
is_urgent.noul 0.999 0.005

Same routing decision, but Gemma is over-confident and disagrees on the two judgement calls. Zero-shot probabilities are softmaxed next-token likelihoods, not calibrated judgements. Closing the gap means labelled data and a trained head (below).

Training a head (per-task, on frozen Gemma features)

make features    # one prefix-shared pass per example, cached to runs/feats/{train,validation,test}.npz
make train       # jevlike's cross-attention head in MLX, listwise cross-entropy -> runs/head.safetensors
make eval-head   # top-1/top-3, ECE, and the shuffled-context control on the test split

Result on the synthetic split (2000 train / 400 validation / 400 test, 2026-09-17): features at 77 ms per example, head training 8 epochs in about 15 s.

top-1 top-3 ECE
head on frozen Gemma features, test 0.970 1.000 0.027
same head, shuffled-context control 0.258 0.698 0.724
jevlike tiny byte encoder, test 0.998 1.000 0.008
Gemma zero-shot, --norm sum, test 1.000 1.000 n/a

The control sits at chance (about 4.5 options per row), so the head really matches options to the state, and its ECE of 0.03 is what a calibrated confidence looks like.

Python:

from openjev import OptionScorer
s = OptionScorer("models/gemma-3-4b-it", batch_size=8)
for r in s.score("The capital of France is", [" Paris", " Berlin"], norm="mean"):
    print(r.option, r.probability, r.logprob_sum, r.n_tokens)
print(s.last_timing)

Normalisation (--norm)

norm score use when
mean (default) sum of option-token log-probs divided by token count options differ in length
sum total log-prob options are the same length or you want raw likelihood
pmi sum minus the option's unconditional log-prob (BOS-only context) options differ in base-rate plausibility; costs one extra batched pass

Measured on M5 Pro, 64 GB (bf16, mlx-lm)

Workload: 202-token context, 8 options, 242 option tokens total.

path median latency
context cached once, options batched 0.17 s
context re-encoded per option, no cache 0.68 s

Model load is about 1 s from a warm disk. First forward pass adds under a second of warm-up.

Validating against jevlike

jevlike is installed into the same venv (uv pip install -e ../../vinnylarouge/jevlike), so both CLIs read the same JSONL. Generate its synthetic menu set, then score it both ways:

.venv/bin/jevlike-data synthetic --output data/synthetic
.venv/bin/openjev eval data/synthetic/test.jsonl --norm sum --sep $'\nChoice: '
.venv/bin/jevlike-train data/synthetic/train.jsonl --validation data/synthetic/validation.jsonl \
    --output runs/synthetic-tiny.pt --device mps
.venv/bin/jevlike-eval runs/synthetic-tiny.pt data/synthetic/test.jsonl --device mps

Results on the 400-row synthetic test set (2026-09-16):

scorer training top-1 top-3 median latency / example
openjev, Gemma 3 4B zero-shot, --norm sum none 1.000 1.000 0.086 s
openjev, --norm mean none 0.988 1.000 0.086 s
openjev, --norm pmi none 0.988 1.000 0.153 s
jevlike tiny byte encoder + head 2000 rows, 8 epochs 0.998 1.000 well under 10 ms

The synthetic task is easy for both. The real validation is your own labelled rows: run openjev eval on them zero-shot and compare against a jevlike-train/jevlike-eval run on the same split. If Gemma zero-shot is close to the trained head, Route B is enough; if not, train a head (Route A) with make features && make train && make eval-head.

Demo: Doom in the terminal

demo/doom/ runs ViZDoom headless, describes each frame in a line of text, and lets the server rank the action menu with one /score call (or one System One choice question). Start make serve, then make doom. Details and keys in demo/doom/README.md.

openjev playing Doom in the terminal: the model ranks the action menu each step

Full-resolution recording: docs/media/doom-recording.mov.

Layout

open-jev (daseinlabs/open-jev)

Searching Database…

Что это такое?Открытая реализация Jev: локальная модель Gemma 3 4B (через MLX) на чипах Apple silicon оценивает список заранее заготовленных вариантов относительно контекста за один пакетный проход — без генерации текста, только лог-вероятности по каждому варианту.
Это Python-библиотека, утилита командной строки или сервер?Всё сразу, в одном репозитории: Python-пакет (openjev), CLI с подкомандами для scoring, eval, bench, features, training и serving, а также сервер на FastAPI с маршрутами /score и /v1/systemone.
Как запустить это на своём Mac?Склонируйте репозиторий и выполните make setup, затем make serve. Если make падает на macOS, примите лицензию Xcode (sudo xcodebuild -license accept) или используйте gmake из Homebrew; задайте OPENJEVAPIKEY перед make serve, чтобы включить проверку Bearer-авторизации.
Какое железо нужно, и можно ли запустить это в Docker или на Linux?Нужен Mac на Apple silicon — программа работает на macOS напрямую через Metal. Контейнерного варианта нет: Linux-контейнеры не имеют доступа к GPU Apple, так что ни Docker, ни обычный Linux не подойдут.
Что за демо с Doom?demo/doom запускает ViZDoom без графики (headless), описывает каждый кадр строкой текста, а сервер выбирает каждое действие, ранжируя меню одним вызовом /score или одним вопросом System One. Запуск: сначала make serve, затем make doom; запись в полном разрешении лежит в docs/media/doom-recording.mov.
Что такое jevlike и зачем с ним сравнивать?jevlike — родственный скорер: крошечный байтовый энкодер плюс обученная голова. Он ставится в тот же venv, чтобы оба CLI читали один и тот же JSONL. На синтетическом тестовом наборе из 400 строк jevlike дал top-1 0,998 заметно меньше чем за 10 мс на пример, а openjev в zero-shot режиме — 1,000 примерно за 0,086 с. Настоящая проверка — ваши собственные размеченные данные.
Как выглядит ответ на скоринг?Маршрут /score возвращает best, bestindex, вероятности по каждому варианту, logprobsum и ntokens, а также тайминги. В ответах System One дополнительно есть поле model, ответы с ключами по id вопросов и usage, где outputtokens — это число токенов вариантов-меток, которые были оценены.
Creator
daseinlabs
Size
7.8 MB
Stars
134
Updated
CommitSep 30
Age
1mo
Commits · 90 Days