Jev is a decision model. It takes a state and a set of questions with fixed options, and returns one option per question with a probability for every option. The app turns each turn of the game into exactly that shape: the board is the state, every legal placement is an option, and the browser judges the answer.
Ctrl/Cmd + wheel to zoom. Drag to pan when zoomed. Double-click to fit.
The state field is structured JSON. The board is nine strings with a dot for an empty cell. The goal sentence is the only rule text. No solution, no hint about the right move.
| · | · | · | 6 | · | 5 | · | · | · |
| · | 1 | · | · | · | 7 | · | 6 | 2 |
| · | · | · | 8 | 2 | 1 | · | · | 9 |
| · | · | 8 | 9 | 7 | · | 4 | · | · |
| 4 | · | · | 2 | 1 | · | · | · | · |
| 1 | · | 6 | · | 3 | 4 | · | 2 | · |
| · | 2 | · | · | 6 | 3 | · | 1 | 7 |
| 3 | 4 | · | 1 | · | 9 | · | · | 6 |
| · | 8 | 1 | 7 | · | · | 3 | · | 4 |
Real puzzle from a captured request. The orange cell is the move Jev chose in the answer below.
// request body, state part (captured) { "model": "typesafe/jev-1.13", "state": { "game": "sudoku", "goal": "Fill every empty cell (.) so that each row, each column, and each 3x3 box holds the digits 1 to 9 once.", "rows": [ ". . . 6 . . . . .", ". 1 . . . 7 . 6 2", ". . . 8 2 1 . . 9", ". . 8 9 7 . 4 . .", "4 . . 2 1 . . . .", "1 . 6 . 3 4 . 2 .", ". 2 . . 6 3 . 1 7", "3 4 . 1 . 9 . . 6", ". 8 1 7 . . 3 . 4" ], "empty_cells": 45 }, "questions": { ... see below ... } }
The backend computes the candidates. Only placements that break no row, column, or box rule become options, minus the ones the browser already rejected. This puzzle had 135 legal placements, so one question. An option id reads like a sentence, and its description is a small object: the position, the digit, and every digit that still fits in that cell.
"questions": { "move_batch_1": { "type": "choice", "instructions": "Which placement is certainly correct for this sudoku? Prefer a cell whose cell_candidates has one value. Otherwise pick the placement that the row, column, and box rules force.", "criteria": { // 135 entries, one per legal placement "row_1_col_1_value_2": { "row": 1, "column": 1, "value": 2, "cell_candidates": [2,5,7,8,9] }, "row_1_col_1_value_5": { "row": 1, "column": 1, "value": 5, "cell_candidates": [2,5,7,8,9] }, ... "row_1_col_6_value_5": { "row": 1, "column": 6, "value": 5, "cell_candidates": [5] }, ... } } }
Jev accepts at most 255 options per choice question. An empty board has 729 placements. The backend slices the list into move_batch_1, move_batch_2, ... and sends all batches in one request. Jev answers them in parallel.
cell_candidatesJev does not count or calculate. The code computes which digits fit in each cell and puts the list in every option. A single-digit list is the visible signal that the instructions ask it to prefer.
row_1_col_6_value_5 is the option key Jev returns. The backend parses it back into {row: 0, col: 5, value: 5} with a regex. No free text ever comes back.
Jev put half its mass on the one cell with a single candidate. The next three are the same digit 5 in other cells, so it also reads the row and column pattern, not just the list length.
// response body (captured, trimmed) { "model": "typesafe/jev-1.13-20260917", "answers": { "move_batch_1": { "type": "choice", "choice": "row_1_col_6_value_5", "confidence": 0.5, "probabilities": { "row_1_col_6_value_5": 0.51, "row_9_col_5_value_5": 0.19, "row_6_col_4_value_5": 0.16, ... 132 more ... } } }, "usage": { "input_tokens": 7251, "output_tokens": 2053, "cost": 0.000304 } } // backend → browser { "type": "decision", "move": { "row": 0, "col": 5, "value": 5 }, "probability": 0.51, "confidence": 0.5, "options_considered": 135, "questions_asked": 1, "latency_ms": ~350 }
With several batches, each question names its own winner. The backend ignores the choice fields and takes the single highest probability across all batches, then reports the confidence of the batch it came from.
The move matches the hidden solution. The cell fills in blue and the next state goes out after a short delay so the board stays readable.
Cell stays empty, shown red with a strike. The move joins rejected, so the backend never offers it again. Mistakes count goes up.
No empty cell left. Status becomes won, the loop stops, and the Solve button disables until New game.
Three mistakes. Status becomes lost. The server also declares a loss when no legal move remains after the rejections.
Feedback reaches Jev only indirectly: a wrong option disappears from the next list, and a right one shows up as a filled digit in the next state rows.
| Endpoint | POST https://openrouter.ai/api/alpha/decisions. The chat completions endpoint rejects the model with a 400. |
|---|---|
| Model id | typesafe/jev-1.13, served as jev-1.13-20260917 |
| Options in this request | 135 legal placements, 1 question (limit 255 per question) |
| Tokens | 7,251 input, 2,053 output. Output tokens are free on this model. |
| Cost per move | $0.0003. A 45-cell puzzle solved without mistakes costs about $0.014. |
| Latency | 300 to 700 ms per decision, measured in the backend around the fetch call |
| Where the logic lives | backend/src/jev.ts builds the request and picks the best option. shared/src/sudoku.ts computes legal moves. frontend/src/game.ts judges the move. |