Running the server · Authentication · POST /v1/decide · Question types · Response · Errors · GET /health · Clients · Training your own
Jet runs on your machine. Clone the repo, train or fuse a model (see Training your own), then:
uv sync
JET_API_KEY=secret uv run jet-serve --base-model models/jet \
--cors-origin https://jach.me
The API is then on http://127.0.0.1:8000. --cors-origin lets the
demo and chess pages on this site call it from your
browser; set their Server field if it runs somewhere else. You can also link to a page with
?server=http://host:port. Chrome asks once for permission to reach your local network.
| flag | env | default |
|---|---|---|
--base-model | JET_BASE_MODEL | mlx-community/Qwen3-0.6B-bf16 |
--adapter | JET_ADAPTER | none |
--model-name | jet-local-0.1 | |
--host | 127.0.0.1 | |
--port | 8000 | |
--cors-origin | JET_CORS_ORIGINS | none; repeatable, or comma-separated in the env var |
Point --base-model at a fused model (models/jet), or pass the base model plus
--adapter adapters/jet. Calibration is read from calibration.json in either
directory. Requests are served one at a time on a single worker thread.
If JET_API_KEY is set, every request to /v1/decide needs the header
Authorization: Bearer <key>. Without it, the endpoint is open.
Ask one or more questions about one state. The request shape is compatible with Jev.
| field | type | |
|---|---|---|
state | string, object or array | What the questions are about. Objects are rendered as JSON; an array of strings is joined with blank lines. |
questions | object | Named questions, 1–64. The names come back as keys in answers. |
model | string, optional | jet-latest, jev-latest or the server's model name. |
Each question has a type, an instructions string, and for
choice and score, criteria.
States longer than 4,096 tokens are truncated in the middle rather than rejected.
Pick one of 2–255 options. criteria maps each key to a description.
{"type": "choice", "instructions": "What is the primary issue?",
"criteria": {"billing": "billing or payment problem", "bug": "the product is broken"}}
{"type": "choice", "choice": "billing",
"probabilities": {"billing": 0.97, "bug": 0.03}, "confidence": 0.8}
Place the state on an ordered scale of 2–10 levels, lowest first. score is the expected
level index (0 to n−1), level is the most likely level, and probabilities is
in the same order as criteria.
{"type": "score", "instructions": "How urgent is this?",
"criteria": ["routine", "handle today", "urgent", "critical"]}
{"type": "score", "score": 2.1, "level": "urgent",
"probabilities": [0.05, 0.15, 0.45, 0.35], "confidence": 0.2}
A yes/no question. It takes no criteria. probability is the probability
of yes.
{"type": "noul", "instructions": "Escalate to a human immediately?"}
{"type": "noul", "probability": 0.81, "confidence": 0.3}
{
"model": "jet-local-0.1",
"answers": { "<name>": { ... }, ... },
"usage": {"input_tokens": 316},
"latency_ms": 102.0
}
confidence is 1 minus the normalized entropy of the answer's distribution: 1 when all
the probability is on one answer, 0 when it's spread evenly. Probabilities are temperature-calibrated
per question type, so a 0.8 should be right about 80% of the time on data like the training set.
input_tokens counts every question's full prompt, but the shared state is only
encoded once, so asking more questions about the same state is cheap.
| status | when |
|---|---|
400 | Empty or more than 64 questions, an invalid question, or an unknown model. |
401 | Missing or wrong API key. |
422 | The body isn't valid JSON or is missing state / questions. |
Error bodies look like {"detail": "invalid question: ..."}.
{"status": "ok", "model": "jet-local-0.1"}
The server also publishes its OpenAPI schema at /openapi.json.
curl localhost:8000/v1/decide \
-H "Authorization: Bearer secret" -H "Content-Type: application/json" \
-d '{"state": "The package arrived crushed.",
"questions": {"refund": {"type": "noul", "instructions": "Should we offer a refund?"}}}'
import httpx
r = httpx.post(
"http://localhost:8000/v1/decide",
headers={"Authorization": "Bearer secret"},
json={
"state": "The package arrived crushed.",
"questions": {"refund": {"type": "noul", "instructions": "Should we offer a refund?"}},
},
)
print(r.json()["answers"]["refund"]["probability"])
const res = await fetch("http://localhost:8000/v1/decide", {
method: "POST",
headers: {"Authorization": "Bearer secret", "Content-Type": "application/json"},
body: JSON.stringify({
state: "The package arrived crushed.",
questions: {refund: {type: "noul", instructions: "Should we offer a refund?"}},
}),
});
const {answers} = await res.json();
uv run jet-data-public # public datasets → data/public.jsonl
uv run jet-split data/public.jsonl # train / val / test
uv run jet-train # LoRA on Qwen3-0.6B → adapters/jet
uv run jet-calibrate --adapter adapters/jet
uv run jet-fuse # → models/jet
uv run jet-eval --adapter adapters/jet --data data/test.jsonl
Training runs on Apple Silicon (MLX) or on Linux with an NVIDIA GPU. The README covers the full pipeline, including Claude distillation and the benchmark against Jev.