1# Chapter 17: jev-protocol — the grammar book that never posts a letter 2 3`jev-protocol` is the TypeSafe System One protocol (`POST /v1/systemone`) 4with no network, no files and no clock. It builds the exact bytes of a 5request, checks that a response really answers the questions that were 6asked, classifies the API's errors, and decides whether and when to retry. 7Something else does the sending. This crate only knows what a correct letter 8looks like and how to read the reply. 9 10## Three kinds of question 11 12Jev never writes prose back. You hand it a *state* (whatever it is judging: 13a string, an object, an array) and one or more named questions, and every 14answer is a probability. There are three shapes of question: 15 16| Type | You write | Jev answers | 17| --- | --- | --- | 18| `Noul` | instructions, and optionally what yes and no mean | `noul`: the probability of yes | 19| `Choice` | instructions and 2 to 255 labelled options | the winning label, a confidence, and a probability for every option | 20| `Score` | instructions and 2 to 10 ordered levels, lowest first | a score from 0 to levels − 1, a confidence, a probability and a vendor description for every level | 21 22The limits are checked when a question is built: `Choice::new` refuses one 23option, 256 options, an empty label or a repeated one, and `Score::new` 24refuses one level or eleven. A request that breaks an API rule cannot be 25constructed, so it is never sent. 26 27A request is one JSON object, written in this order: 28 29```json 30{"model":"jev-1.13.0","questions":{"q":{"type":"noul","instructions":"Urgent?"}},"state":{"body":"hi","id":1}} 31``` 32 33That line is the exact output of the `exact_bytes` test in 34[src/request.rs](src/request.rs). 35 36## Why the bytes matter so much 37 38postjevsql keeps a cache of Jev's answers keyed by a hash of what was asked, 39and lmjtfy keeps an archive keyed by the exact request body. Both only work 40if the same question always produces the same bytes. So: 41 42- `state` goes on the wire byte for byte as the caller gave it (`Json`). 43- `Json::canonical` writes a value in RFC 8785 form: sorted keys, no 44 whitespace, ECMAScript number formatting. A number a double cannot hold 45 exactly (a `bigint` past 2^53, a `numeric(38,20)`) becomes a string with 46 every digit, so nothing is rounded away and equal values give equal bytes. 47- `Choice` options are sent in the caller's order and never pass through a 48 map type that sorts. Order is part of the question: Jev's answers show an 49 order bias. 50- `question_bytes` gives one question exactly as it appears inside the 51 request, for a cache key that covers a single question. 52 53> **Aside.** Generic JSON canonicalizers parse numbers into `f64` and 54> silently round. That is why [src/jcs.rs](src/jcs.rs) is hand-written: it 55> keeps every number's original text until it knows a double can hold it. 56 57## Reading the answer back 58 59```mermaid 60flowchart LR 61 Q["Questions<br/>noul / choice / score"] -->|"request_bytes"| B["exact bytes"] 62 B -->|"a transport sends them"| W(("Jev")) 63 W -->|"200 body"| P["Response::parse"] 64 P -->|"Key<NoulAnswer>"| A["get(key).noul"] 65 W -->|"any other status"| E["ApiError::from_response"] 66``` 67 68Adding a question returns a `Key<A>`, typed by the kind of answer it reads 69(`Key<NoulAnswer>`, `Key<ChoiceAnswer>`, `Key<ScoreAnswer>`). It is the Rust 70version of a TypeScript generic that makes `answer.choice` a compile error on 71a yes-or-no question. A key belongs to the `Questions` that issued it; using 72it on another set's response panics. 73 74`Response::parse` refuses a reply that: 75 76- comes from a different model than the pinned one (`ModelMismatch`), so a 77 moved model is never recorded under the pinned version; 78- leaves a question unanswered, or answers one that was not asked; 79- answers a question as another type; 80- picks a `Choice` label that was not offered, or leaves one without a 81 probability; 82- gives a probability or confidence outside [0, 1], or a score outside its 83 levels. 84 85What passes is ordered like the question: a `Choice`'s probabilities come 86back in the options' order, a `Score`'s lowest level first. 87 88## Model ids, errors, retries and the price 89 90- **`ModelId::pinned`** takes `<name>-<major>.<minor>.<patch>`, with an 91 optional `-suffix` (`jev-1.13.0`, `jev-2.0.0-rc1`). It refuses aliases 92 (`jev-latest`, `jev-preview`) because the vendor says an alias moves when a 93 new release ships. 94- **`ApiError::from_response`** reads a non-200 body in all three shapes the 95 API sends (`{"detail": {"error_type", "message"}}`, a list of field 96 errors, or anything else, quoted up to 500 characters) and classifies it: 97 98 | Status | `ApiErrorKind` | 99 | --- | --- | 100 | 400 with `error_type` `max_tokens_exceeded` | `ContextOverflow` (split the request; never resent) | 101 | 400 | `BadRequest` | 102 | 402, or 403 whose message mentions "credit", "balance", "insufficient funds" or "quota exceeded" | `OutOfCredit` (inferred; the vendor documents no such response) | 103 | 401, other 403 | `Authentication` | 104 | 404, 408, 422, 429, 529 | `NotFound`, `RequestTimeout`, `Unprocessable`, `RateLimited`, `Overloaded` | 105 | other 5xx | `Server` | 106 107- **`retry`** is the vendor SDKs' policy (Python `typesafe-sdk` 0.7.1, JS 108 `@typesafe-ai/sdk` 0.6.0): at most 2 retries, 10 s per attempt, 30 s for 109 everything. 408, 429 and every 5xx are retried, and so is a request that 110 got no response. The wait is `retry-after-ms`, else `Retry-After` in 111 seconds or as an HTTP date, capped at 60 s; without either it is 0.5 s 112 doubling to 5 s, less up to 25% jitter. `next_delay` takes the jitter and 113 the wall clock as arguments, so the policy itself has no clock. 114- **The price** is `DOLLARS_PER_MTOK`, $0.042 per million input tokens; 115 output tokens are free. `Usage::dollars` prices an answer. 116 `worst_case_dollars` prices a request before it is sent: its length in 117 characters (no token is shorter than one character), capped at the 118 documented 64,000-token ceiling, times the 3 attempts the retry policy 119 may make. 120 121## Try it 122 123``` 124cargo test -p jev-protocol 125``` 126 127Besides the unit tests, `never_panics` in [src/lib.rs](src/lib.rs) feeds 128random bytes to the four parsers that meet the network (`Response::parse`, 129`ApiError::from_response`, `retry::next_delay`, `Json::canonical`): they must 130reject, never panic. 131 132This crate builds for `wasm32-unknown-unknown` as it is: it has no I/O to 133port. 134 135## For the people who maintain it 136 137### In this folder 138 139| Path | What | 140| --- | --- | 141| [src/](src/) | The code, file by file: Chapter 17½. | 142| [Cargo.toml](Cargo.toml) | Dependencies: `http`, `httpdate`, `ryu-js`, `serde`, `serde_json` (with `preserve_order` and `raw_value`); `proptest` for tests. | 143| [CLAUDE.md](CLAUDE.md) | Invariants an agent must keep. | 144 145Design points taken from prior art, both MIT OR Apache-2.0: typed answer 146keys, limits checked before sending, responses verified against the 147questions and structured API errors from JedimEmO/typesafe-client; fuzzed 148parsers from kunobi-ninja/kunobi-jev. Unlike both, the state is sent as 149caller-supplied bytes, so the bytes on the wire are exactly the bytes a cache 150key hashes. 151 152← Previous: [Chapter 16: jevcrates](../) · Up: [jevcrates](../) · Next: [Chapter 17½: inside jev-protocol/src](src/) →