Docs

Running the server · Authentication · POST /v1/decide · Question types · Response · Errors · GET /health · Clients · Training your own

Running the server

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.

flagenvdefault
--base-modelJET_BASE_MODELmlx-community/Qwen3-0.6B-bf16
--adapterJET_ADAPTERnone
--model-namejet-local-0.1
--host127.0.0.1
--port8000
--cors-originJET_CORS_ORIGINSnone; 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.

Authentication

If JET_API_KEY is set, every request to /v1/decide needs the header Authorization: Bearer <key>. Without it, the endpoint is open.

POST /v1/decide

Ask one or more questions about one state. The request shape is compatible with Jev.

fieldtype
statestring, object or arrayWhat the questions are about. Objects are rendered as JSON; an array of strings is joined with blank lines.
questionsobjectNamed questions, 1–64. The names come back as keys in answers.
modelstring, optionaljet-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.

Question types

choice

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}

score

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}

noul

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}

Response

{
  "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.

Errors

statuswhen
400Empty or more than 64 questions, an invalid question, or an unknown model.
401Missing or wrong API key.
422The body isn't valid JSON or is missing state / questions.

Error bodies look like {"detail": "invalid question: ..."}.

GET /health

{"status": "ok", "model": "jet-local-0.1"}

The server also publishes its OpenAPI schema at /openapi.json.

Clients

curl

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?"}}}'

Python

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"])

JavaScript

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();

Training your own

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.