EngineeringSeptember 202612 min read

We Rebuilt Jev's API on an Open Model and Used It to Play Doom

By Stephen Blum · Blocks.ai

TypeSafe AI's Jev turns unstructured input into typed decisions and probabilities. We rebuilt the API with an open model, reached 113ms median latency on an M4 Mac, and used it to play Doom.

Jev uses a language model for zero-shot classification without generating text. We toyed with a similar classifier design years ago, and recognized the Jev design. Our open-source 'replica' implements Jev's API with a base model and runs within TypeSafe's published latency range.

Fast decisions models are important for A2A, to minize latency. At Blocks.ai we noticed that LLMs often have high latency during the thinking process. This was slow and error prone for agents. If we could have a Jev-like system that can run locally, we can improve latency and accuracy for agents.

The source code for the Truetype Jev replica is available on GitHub.

We recorded the gameplay on an M4 Mac. A base model processes each prompt once. We read its next-token logits for A through Z, apply softmax over the letters, and return the results as classification probabilities.

We used Gemma4 base model (not instruct) with max_token 1. Ask question with labled answers A: AnswerA. B: AnswerB. C: AnswerC. Extract logit A-C probabilities. Softmax and present scores. The model answers questions on a quiz. The game input is decided by the model's answers.

The Flappy Bird demo makes one decision per frame. The bar at the bottom shows the phase, chosen action, confidence, and call time. Each forward pass takes 100 to 124 milliseconds, fast enough to steer the bird through the pipes.

TypeSafe AI released Jev on September 15, 2026. The company calls it a System One Model: its API accepts unstructured state and returns typed decisions with calibrated probabilities instead of prose. TypeSafe advertises 70-500ms latency and a price of $42 per billion input tokens. One launch demo used it to play Doom.

We wanted to isolate the value of the one-token interface. Our 1,200-line Python implementation runs locally, exposes the same POST /v1/systemone endpoint, supports all three question types, and plays Doom.

TypeSafe's launch

The launch tweet drew nearly 30 million views. TypeSafe presented RLCD, reinforcement learning for calibrated decisions, as an alternative to RLHF. It claimed up to 200x lower latency, 400x lower cost, free output tokens, and zero hallucinations. A benchmark slide placed Jev near frontier chat models on decision tasks. The tagline was "we're building prod not God."

The demos focused on fast decisions inside live loops. A five-hop Wikiracing run finished in about half a second, while three comparison models took four to five seconds. In another demo, 50 simulated townspeople reacted to a broadcast in under a second. TypeSafe also showed Jev sorting 150,000 Skittles and playing Super Smash Bros. Melee.

TypeSafe named chess, coding from scratch, and open-ended chat as poor fits for Jev. In one chess match, its opponent led by 16 points of material at move 29 and promoted a second queen. Jev won on time because its opponent spent 6 to 15 seconds per move while Jev answered in 2.6.

We could not verify TypeSafe's training claims. Instead, we tested whether an open base model could make useful one-token decisions within the published latency range. It passed our test suite and played the Doom demos.

What we replicated

We copied the API contract: text and questions go in; typed answers, probabilities and confidence scores come out. We did not retrain a model or reproduce TypeSafe's architecture, parallel sampler, or RLCD method.

Five stages from state to answer. A POST to /v1/systemone brings state and a map of noul, choice, and score questions, validated by api.py. service.py serializes the state, builds typed question objects, and renders one prompt per question with the four fields Choices, Question, Text, and Answer, keeping the state last so the prefix stays cacheable and leaving no space after "Answer:". engine.py loads the model once, maps A-Z to single distinct token ids, and runs one forward pass, reusing a cropped LRU prefix KV cache. Output processing gathers the 26 letter logits, checks that their probability mass is near 1.0, renormalizes over the question's legal letters at temperature 0.7, and reads the answer by type: P(yes) for noul, argmax plus a probability map for choice, and an expected value for score, each with entropy-based confidence.
The request path from API input to typed answer. Only the state tail changes between calls, so the engine can cache the prefix.

The engine loads the base Gemma 4 12B model, runs forward(), and reads the logits. It keeps the 26 token IDs for A through Z and applies a temperature-0.7 softmax over the letters allowed by the question. The code in src/ has no generate(), sampling, or decode loop. Each answer uses one output token.

The model cannot invent a label because it does not generate text. If a choice question lists returns, shipping, and billing, the response must contain one of those values. The caller does not need to validate a generated string.

FastAPI serves the API. The service serializes non-string state as JSON, converts each question into a typed specification with legal letters, renders one prompt per question, and scores it. Every prompt uses the same four fields: Choices:, Question:, Text:, and Answer:. The caller's state comes last, leaving a byte-identical prefix for the engine to cache. One forward pass returns logits for all 26 letters; the service renormalizes the legal subset and builds the typed answer. A single process owns the engine and loads the 12B weights once.

Two values for each readout. letter_mass measures how much full-vocabulary probability sits on A through Z: exp(logsumexp(letters) - logsumexp(vocab)). A low value means the engine may be ranking noise from the distribution's tail. For choice and score, confidence is normalized entropy, 1 - H / log n; 1.0 means one option received all the probability. usage reports one output token per question and estimates input tokens from character count.

The include_debug flag returns the prompt, raw letter logits, and ranked top-k letters for each question. We found most of the bugs below with this output.

Few-shot examples tune the readout. For choice and score, the renderer creates one synthetic round trip per option from the caller's criteria. It orders them low, high, then ascending to avoid teaching an A, B, C positional shortcut. For noul, it uses six fixed demonstrations with predicates unrelated to the target question. An urgency example can teach the answer format for a refund question because the target options define the task and the example labels remain truthful. The renderer also turns the caller's instruction into affirmative and negative options. "Does the text request a refund?" becomes "Yes, the text requests a refund." and "No, the text does not request a refund."

Results

The replica answers all 50 cases in our main suite: 20 noul, 20 choice, and 10 score. Two structural tests cover the one-token contract and prefix cache. It also passes a 63-case set containing the main suite, a 5-case Doom bearing probe, and 8 noul cases that use a prompt layout excluded from tuning.

Cases answered correctly / graded, by question type

The replica answers all 50 cases in the main suite and all 13 added cases, including eight that use a prompt layout excluded from tuning.

Replica (Gemma 4 12B base)items
noul20/2020
choice20/2020
score10/1010
Doom bearing probe5/55
noul, held-out layout8/88
one-token contract2/22

The `score` row has ten items and the contract row has two structural tests. Every row is at its ceiling, so the denominators matter more than the rates.

Latency, measured on an Apple Silicon Mac in bfloat16, across 55 warm states:

Metric   Value
p50      113ms
p95      134ms
max      139ms
Warm-path decision latency, against Jev's published range

The dot shows the median decision and the cap shows the 95th percentile. The replica's 113-134ms spread sits inside TypeSafe's published 70-500ms range.

  • Replica, warm cache113ms134ms
  • TypeSafe Jev, published70ms500ms
0ms150ms300ms450ms600ms

The Jev row is a vendor-published range, not a measured p50 and p95. It provides a target band rather than a like-for-like distribution. Our benchmark fails when p95 exceeds 150ms.

Measured on one Mac in bfloat16 with the prefix cache warm. The cold path is about one second per call and is excluded here; it is charted below.

Our 113ms median and 134ms p95 fall within TypeSafe's published 70-500ms range. TypeSafe's home page compares an LLM at 8.566 seconds and $0.013880 per call with Jev at 0.114 seconds and $0.000081; we did not verify those figures. Prefix caching cut our Python test suite from 39.8 seconds to 15.6 seconds and reduced one Doom decision from 1017ms to 133ms without changing the result.

Doom on ViZDoom

The demo runs Doom through ViZDoom, which ships the Freedoom IWADs. It reads object labels, health, ammo, and kill count from the engine, so it does not need screen capture or macOS Screen Recording permission. The model receives a short text summary, chooses one of three or four actions, and advances the game.

shell
pip install vizdoom
python demo/doom_demo.py                  # defend_the_center
python demo/doom_demo.py health_gathering # walk onto medkits to survive
python demo/doom_demo.py deadly_corridor  # fight down a corridor
python demo/doom_demo.py --watch          # ...and watch it play

--watch opens ViZDoom's 640x480 SDL window with the HUD and crosshair visible. It draws every skipped tic to keep motion continuous. Rendering sits outside the timed decision, so watched and headless runs have the same per-decision latency. Wall-clock throughput falls to about 5.5 decisions per second.

Scenario              Outcome                       Latency (p50)   Throughput
defend_the_center     5 kills, health 100           133ms            7.2 dec/sec
health_gathering      health held at 100, reward 160 132ms           7.5 dec/sec
deadly_corridor       2-3 kills, health 70           134ms            7.2 dec/sec

The repository includes a full-level agent for navigation and combat:

shell
python doom/doom_full_game_demo.py            # Freedoom 2 MAP01, 150 decisions
python doom/doom_full_game_demo.py --watch
python doom/doom_full_game_demo.py --decisions 300

In a 300-decision MAP01 run, the agent gets 5 kills and 8 items without dying. Engine data selects a phase (combat, stuck, or navigate), and the model answers one question for that phase. A BFS route over the level's blocking lines supplies the next waypoint, then the model chooses a local direction. With the same code and decision budget, it gets 2 kills and 2 items on Freedoom 1's cramped, door-heavy E1M1.

Doom simulates at 35 tics per second. At 7.2 decisions per second and 4 tics per decision, 40 decisions cover about 4.6 seconds of game time. The live demo runs slower than real time. You can see the delay as the agent sweeps a room for a monster and fires. TypeSafe noted the same limitation in its demo. Model inference remains the bottleneck.

The full-game run exposed bugs in the prompts and world model. An inverted bearing sign made the agent orbit its target. A 700-unit shooting threshold produced 229 shots and no kills because a monster at that distance is a three-pixel sprite. Switching targets every tick made the agent oscillate between two flanking monsters. We checked each fix against a 252-case matrix without changing the model.

Conventional code handles deterministic work. An early version asked the model which direction looked open on every tick, so it changed headings every frame and shuffled against walls. The engine now builds a walkability grid from blocking lines and runs BFS to the objective. On E1M1, that cut stuck decisions from about 33 to 3. The surrounding code selects the phase, holds a target, handles doors and reloading, and centers the crosshair. The model chooses the local direction.

Prefix caching

The first working version took 1017ms per Doom decision. The forward pass consumed 99% of the call, and a static few-shot prefix accounted for 71-85% of each prompt. The engine now caches the prefix KV and prefills only the state tail. Warm calls encode about 20 tokens instead of 370.

The engine tokenizes the full prompt as one string, compares its head with the cached prefix tokens, and falls back to a full forward pass on mismatch. On a hit, it reuses the prefix slice from a previous forward pass instead of replaying or re-tokenizing text. After use, cache.crop(-tail_len) restores the cache to the prefix length. A miss costs about the same as running without a cache.

At 369 tokens, each cached prefix uses about 127MB of KV. The cache defaults to 16 entries and has a hard ceiling of 64. Before scoring, the service expands it to cover the request's distinct questions. Without enough entries, an 8-entry cache handled a 10-question request in 7909ms; disabling the cache took 7687ms.

The benchmark produced these results:

Single question, warm cache (mean of 5):
  Doom 3-option choice   cache on   173.1ms   off  1007.4ms   5.82x
  noul refund            cache on   126.8ms   off   788.2ms   6.22x

5-question request:
  cache off (batched)         3881.0ms
  cache on, cold (all miss)   4143.8ms   1.07x vs off
  cache on, warm (all hit)     794.4ms   4.89x vs off
Latency per request, prefix cache on and off (shorter is faster)

A warm prefix cuts latency by about 6x for one question and 5x for a five-question request. A cold multi-question request is slightly slower than running without a cache.

  • Cache on, warm
  • Cache off
  • Cache on, cold (all miss)
  • Doom 3-option choicecache on, warm · mean of 5173.1ms
  • Doom 3-option choicecache off · mean of 51007.4ms
  • noul refundcache on, warm · mean of 5126.8ms
  • noul refundcache off · mean of 5788.2ms
  • 5-question requestcache on, warm (all hit)794.4ms
  • 5-question requestcache off, batched3881.0ms
  • 5-question requestcache on, cold (all miss)4143.8ms

Only the five-question request has a cold-cache measurement; the two single-question rows were measured warm against off. The forward pass is about 97% of every call, so these gaps are almost entirely tokens the model no longer has to encode.

One Mac in bfloat16. Warm calls encode about 20 tokens instead of 370. Cached and uncached paths return identical decisions.

Batching questions into one forward pass saves little on this hardware because it still processes every prompt's full prefix. Forward time scales with the total tokens processed at about 2ms per token. The service therefore scores questions in sequence against per-question cached prefixes. A cold multi-question request is about 7% slower than an uncached batch; later requests are about 5x faster.

A profile of the warm path measured 1.5ms to tokenize the full prompt, about 166ms for the forward pass over the cached prefix and tail, 4.9ms to extract the 26 letters and mass, and 0.09ms to crop the cache. The forward pass consumes about 97% of the call. Its floor sits near 60ms and changes little with prefix length; each tail token adds about 2ms. Shortening the state tail would produce the next meaningful gain. Float16 and bfloat16 perform the same here, 999ms against 1000ms, because weight memory bandwidth limits the workload.

Prefilling pays off when the model reuses a prefix. Doom uses the same prefix 40 times, so warming helps. In the text-adventure demo, warming four one-shot prefixes costs 4.8 seconds and saves 1.6 seconds during play, so that demo starts cold.

Scope and limits

The replica supports noul, choice, and score with up to 26 options. It returns probabilities normalized across legal answers and entropy-based confidence. It also handles multi-question requests, supports an optional bearer key, and ships with a Docker image and demos. A score is the expected value across ordinal levels. An expected value of 1.0 can mean confident support for level 1 or an even split between 0 and 2, so read it alongside confidence.

The model produces a full 26-letter readout on each call. The service scores all 26 letters and renormalizes over the question's legal set, so top_k cannot cut off valid options. The setting controls only the debug output.

We did not reproduce TypeSafe's parallel sampler, training, the workflow evaluations behind the 193.6x and 444.6x figures, Wikiracing, or pixel input for Doom. The replica uses a base 12B model and inherits its judgment.

Probabilities may vary between runs. Test paraphrases of the same input. Temperature applies a monotonic transform to the logits, so it cannot change the winning noul or choice option; it changes only the reported probabilities. Tune it for calibration, set probability thresholds, and escalate low-confidence cases.

Run it

shell
python -m venv .venv && source .venv/bin/activate
pip install -e ".[test]" vizdoom
truetype-api

The first start downloads google/gemma-4-12B (~22GB) and needs about 24GB of RAM. An NVIDIA GPU or Apple Silicon helps. Then:

shell
curl -s localhost:8000/v1/systemone \
  -H 'Content-Type: application/json' \
  -d '{
    "state": "I was charged twice for order A-104. Please refund the duplicate.",
    "questions": {
      "refund": {"type": "noul", "instructions": "Does the text request a refund?"},
      "team": {
        "type": "choice",
        "instructions": "Which team should handle this?",
        "criteria": {
          "returns": "Exchanges, refunds, wrong or damaged items",
          "shipping": "Delivery status, delays, lost packages",
          "billing": "Charges, invoices, payment problems"
        }
      },
      "severity": {
        "type": "score",
        "instructions": "How severe is the reported issue?",
        "criteria": [
          "Cosmetic; no impact to functionality",
          "Broken or degraded feature, but workaround exists",
          "Blocking issue; no workaround exists"
        ]
      }
    }
  }'

The response contains typed answers, probabilities, and confidence, using one output token per question. Your code can read answers.team.choice and answers.team.confidence, apply a threshold, and continue without parsing prose or retrying malformed JSON.

The replica can make frequent, small decisions inside an agent loop. If you build something with it or find a failure case, open an issue.

The Truetype Jev source code includes the API, tests, and demos.

Zefan Cai also published an independent open-source Jev implementation.