README.mdpreviewREADME.mdsource112 lines · 6.0 KB · raw
1# Chapter 17½: inside jev-protocol/src — ten files, no side effects
2
3Every file here is pure: functions take what they need (a clock reading, a
4jitter value, a byte slice) as arguments and return a value. Nothing opens a
5socket, reads a file or asks the time: the dependency list has nothing that
6could, and [lib.rs](lib.rs) opens with `#![forbid(unsafe_code)]`.
7
8Read them in the order a request lives: name the model, build the
9questions, write the bytes, then read what came back.
10
11```mermaid
12flowchart LR
13  model["model.rs<br/>ModelId"] --> request
14  json["json.rs<br/>Json"] --> question
15  jcs["jcs.rs<br/>RFC 8785"] --> json
16  question["question.rs<br/>Noul, Choice, Score"] --> request["request.rs<br/>Questions, Key, request_bytes"]
17  request --> response["response.rs<br/>Response::parse"]
18  answer["answer.rs<br/>*Answer, Usage"] --> response
19  error["error.rs<br/>ApiError, ProtocolError"]
20  retry["retry.rs<br/>next_delay"]
21```
22
23## The files, in reading order
24
251. **[lib.rs](lib.rs)**: start here. The module list, every public
26   re-export, and four constants: `SYSTEM_ONE_PATH` (`/v1/systemone`),
27   `ENDPOINT` (`https://api.typesafe.ai/v1/systemone`), `REQUEST_ID_HEADER`
28   (`x-typesafe-request-id`) and `RETRY_COUNT_HEADER`
29   (`x-typesafe-retry-count`). `request_id(headers)` reads the id a response
30   carries. At the bottom, the `never_panics` property tests.
312. **[model.rs](model.rs)**: `ModelId::pinned`, which accepts a versioned
32   model such as `jev-1.13.0` and refuses an alias.
333. **[json.rs](json.rs)**: `Json`, a JSON value kept as the exact bytes it
34   will be sent as. `Json::text` wraps a string, `Json::verbatim` keeps the
35   caller's bytes after checking they are JSON, `Json::canonical` rewrites
36   them in RFC 8785 form.
374. **[question.rs](question.rs)**: the `Question` enum and its three kinds.
38   `Noul::new(..).yes_means(..).no_means(..)`, `Choice::new(instructions,
39   options)` (2 to `MAX_CHOICE_OPTIONS` = 255), `Score::new(instructions,
40   levels)` (2 to 10). Also each kind's JSON shape, `criteria` included: a
41   `Noul`'s `{"true", "false"}`, a `Choice`'s label-to-description map in the
42   caller's order, a `Score`'s array of levels.
435. **[request.rs](request.rs)**: `Questions`, a named set sent together.
44   `questions.noul(id, q)` (and `choice`, `score`) returns a typed `Key`; ids
45   must be non-empty and unique. `request_bytes(model, state, questions)`
46   writes `{"model","questions","state"}`; `question_bytes` writes one
47   question alone; `worst_case_tokens` and `worst_case_dollars` price a
48   request before it is sent, capped by `MAX_REQUEST_TOKENS` (64,000).
496. **[response.rs](response.rs)**: `Response::parse(pinned, questions,
50   body)`, which verifies a 200 body against the questions and the pinned
51   model, and `Response::get(key)`, which reads one answer by its typed key.
52   `model()` and `usage()` read the rest.
537. **[answer.rs](answer.rs)**: what a verified answer holds. `NoulAnswer
54   { noul }`, `ChoiceAnswer { choice, confidence, probabilities }` (in option
55   order), `ScoreAnswer { score, confidence, probabilities, legend }` with
56   `normalized()` putting the score on 0 to 1. `Usage { input_tokens,
57   output_tokens }` with `dollars()`, and `DOLLARS_PER_MTOK` ($0.042). The
58   sealed `FromAnswer` trait is what lets a `Key<A>` read only its own kind.
598. **[error.rs](error.rs)**: `ProtocolError` (a request that cannot be
60   built, a response that does not answer, a model mismatch) and `ApiError`,
61   built from any non-200 by `ApiError::from_response(status, headers,
62   body)`, with its `ApiErrorKind` and any `FieldError`s.
639. **[retry.rs](retry.rs)**: the vendor SDKs' retry policy as constants
64   (`MAX_RETRIES` = 2, `ATTEMPT_TIMEOUT` = 10 s, `BUDGET` = 30 s) and one
65   function, `next_delay(outcome, retries_so_far, jitter, now)`, which
66   returns how long to wait before the next attempt, or `None` for no retry.
67   `retryable(status)` is 408, 429 and 5xx.
6810. **[jcs.rs](jcs.rs)**: the deep end. A small JSON parser that keeps each
69    number's original text, and a writer that emits RFC 8785 canonical JSON.
70    Keys are sorted by UTF-16 code units, as RFC 8785 says, and a repeated
71    key is refused. A number is written the ECMAScript way (through `ryu-js`)
72    when that text has exactly the original value (`1.10` becomes `1.1`);
73    otherwise it becomes a string with every significant digit
74    (`9007199254740993` becomes `"9007199254740993"`), so a `bigint` loses
75    nothing and equal values give equal bytes. Nesting deeper than 256 is
76    refused.
77
78> **Aside.** `Questions::new` stamps each set with a number from a global
79> counter, and every `Key` carries it. That is how `Response::get` can tell a
80> key from another set of questions: the type system knows the kind of
81> answer, the stamp knows which set.
82
83## Try it
84
85```
86cargo test -p jev-protocol
87```
88
89Each file carries its own tests at the bottom, in a `mod tests` block. The
90`exact_bytes` test in [request.rs](request.rs) is the shortest complete
91example: one question, one canonical state, and the exact line that goes on
92the wire.
93
94## For the people who maintain it
95
96### In this folder
97
98| File | What |
99| --- | --- |
100| [lib.rs](lib.rs) | Module list, re-exports, endpoint and header constants, fuzz tests. |
101| [model.rs](model.rs) | `ModelId`: pinned versions only. |
102| [json.rs](json.rs) | `Json`: exact bytes, verbatim or canonical. |
103| [question.rs](question.rs) | `Noul`, `Choice`, `Score`, with the API's limits. |
104| [request.rs](request.rs) | `Questions`, `Key`, `request_bytes`, `question_bytes`, worst-case cost. |
105| [response.rs](response.rs) | `Response::parse` and `get`. |
106| [answer.rs](answer.rs) | The answer types, `Usage`, the price. |
107| [error.rs](error.rs) | `ProtocolError`, `ApiError`, `ApiErrorKind`, `FieldError`. |
108| [retry.rs](retry.rs) | The retry policy. |
109| [jcs.rs](jcs.rs) | RFC 8785 canonical JSON that never rounds a number. |
110| [CLAUDE.md](CLAUDE.md) | Invariants for agents. |
111
112← Previous: [Chapter 17: jev-protocol](../) · Up: [jev-protocol](../) · Next: [Chapter 18: jev-client](../../jev-client/) →