1# Chapter 20½: inside jev-http/src — a road, a clock and a ledger 2 3Four files. [lib.rs](lib.rs) is the front door (`Jev`), and the other three 4are what it is built from: the road ([transport.rs](transport.rs)), the 5clock ([runtime.rs](runtime.rs)) and the meter ([ledger.rs](ledger.rs)). 6 7```mermaid 8flowchart TD 9 jev["lib.rs: Jev"] --> client["jev_client::Client"] 10 jev --> ledger["ledger.rs: Ledger"] 11 client --> transport["transport.rs: HttpsTransport"] 12 client --> runtime["runtime.rs: TokioRuntime"] 13 ledger --> file[("$XDG_STATE_HOME/jev/")] 14``` 15 16## The files, in reading order 17 18### 1. [lib.rs](lib.rs): `Jev`, the client you keep 19 20- `KEY` is `"TYPESAFE_API_KEY"`, the variable the key arrives in. 21- `Jev::from_env(binary, model)` reads the key, opens the ledger with 22 `Ledger::open`, and builds a client for the real API. It returns `None` when 23 the key is absent or blank. 24- `Jev::new(key, binary, ledger, model, endpoint)` is the same with every 25 part given, which is how a test points it at jev-mock and a sandbox 26 ledger. It builds the rustls config: compiled-in `webpki-roots`, any PEM 27 from `Endpoint::trusting`, the RustCrypto provider, ALPN `h2`. 28- `with_guards(guards)` swaps the environment's `Guards` for others. 29- `ask(state, questions, why)` is the sequence drawn in Chapter 20: admit, 30 ask through `jev_client::Client`, then settle, or release only when 31 `ClientError::nothing_billed` says so. An out-of-credit API answer also 32 writes the ledger's out-of-credit flag. A ledger write that fails after an 33 answer arrived is logged to stderr and the answer is still returned: the 34 request was already paid for. 35- `Answered` adds to `jev-client`'s: `took` (the whole round trip), the body 36 as text, every header with credentials redacted (any name containing 37 `authorization`, `cookie`, `token`, `secret`, `-key` or `api-key` shows as 38 `«redacted»`), the request id, and a `RateLimit`. 39- `Failure` is `Throttled { why, retry_in }`, `Budget(why)` or 40 `Client(ClientError)`. The first two never touch the network. 41- `RateLimit { limit, remaining, reset_seconds }` is read from the 42 conventional `x-ratelimit-*` and `ratelimit-*` headers. The vendor 43 documents none of them on a successful response (digest §10), so it is 44 usually `None`; it exists so that the day the vendor sends one, it is 45 reported with no call site changed. `last_rate_limit()` returns the last 46 one seen, and the ledger keeps a copy in `last-rate-limit.json`. 47 48### 2. [transport.rs](transport.rs): `HttpsTransport` and `Endpoint` 49 50- `Endpoint::api()` is `jev_protocol::ENDPOINT`; `Endpoint::parse(url)` 51 accepts any `https` URL and refuses anything else; `trusting(pem)` adds a 52 CA file. 53- `HttpsTransport` keeps one HTTP/2 connection warm and reopens it when it 54 has closed, has been idle 300 s (Cloudflare closes idle connections at 55 400 s), or the client distrusts it. 56- Connecting tries each resolved address in turn, giving every address but 57 the last 2 s, so one blackholed address (a broken IPv6 route) cannot spend 58 a whole attempt. It sends an HTTP/2 PING after 20 s without a frame and 59 closes the connection if the PING goes 10 s unanswered. Flow-control 60 windows are adaptive, because hyper's fixed windows are smaller than an 61 8 MiB answer. 62- A response body over 8 MiB is `TooLarge`. A request hyper hands back 63 unsent, or one refused with `REFUSED_STREAM` or cut by a server GOAWAY, is 64 `NotSent`; any other break mid-exchange is `Interrupted`. A TLS refusal by 65 rustls, or a server that does not negotiate `h2`, is `Tls`. 66- `admit` waits on two per-process token buckets under the vendor's 67 documented ceilings, 1,200 requests a minute and 250,000 tokens a second, 68 counting the body's characters as tokens. 69 70### 3. [runtime.rs](runtime.rs): `TokioRuntime` 71 72`now` and `wall_clock` from std, `sleep` and `timeout` from tokio, and 73`jitter` from the clock's nanoseconds, which is random enough to spread 74retries and needs no dependency. 75 76### 4. [ledger.rs](ledger.rs): `Ledger`, the shared meter 77 78Its doc comment explains the design (why a file, why `flock`, which guards 79heal by themselves, why the total is re-read every time); read it first. 80Then: 81 82- `Ledger::open()` uses `$XDG_STATE_HOME/jev`, or `~/.local/state/jev`; 83 `Ledger::at(dir)` uses any directory. Either creates it and, the first time 84 ever, seeds `spend.jsonl` with one estimated opening line 85 (`OPENING_ESTIMATE_USD`, $0.1517). `create_new` makes the seed race-free. 86- Each line of `spend.jsonl` is JSON: an `Entry` (a real cost, or the 87 opening estimate) or a `Hold` (a question admitted and not yet settled), 88 told apart by the hold's `hold_id`. A line that fails to parse, such as a 89 torn write from a killed process, is skipped with a message on stderr 90 rather than failing every later read. 91- `admit(now, guards, worst_case, binary, reason)` checks the out-of-credit 92 flag, then, under one `flock`, reads every line into a `Window`, asks 93 `Window::verdict`, and appends the `Hold`. `settle(hold, entry)` books the 94 real cost; `release(hold, now, why)` books nothing but still counts the 95 question. 96- `Window` is the recent past in one pass: `lifetime_usd`, the admission 97 times of the last minute, the amounts of the last hour, and the count of 98 unsettled holds. `bucket_empty_for` and `hour_closed_for` say how long 99 until each guard would admit. 100- `Guards::from_env()` reads `JEV_MAX_QUESTIONS_PER_MINUTE` (30) and 101 `JEV_MAX_DOLLARS_PER_HOUR` (0.25). 102- `status(now, guards)` builds a `Status`, with `line()`, `stopped()` and 103 `stopped_for_good()` for display. Its hour-breaker figure is for an 104 ordinary question of a thousand input tokens. 105- `mark_out_of_credit` writes `out-of-credit.json` (the first reason is 106 kept), `credit_restored` removes it, and `out_of_credit` reads it. 107- `now()` is seconds since the epoch as an `f64`. Every guard function takes 108 `now` as an argument instead, so the tests run on any clock they like. 109 110## Try it 111 112``` 113cargo test -p jev-http 114``` 115 116The ledger's tests each get their own directory under the system temp dir, 117so they never touch your real ledger. 118 119## For the people who maintain it 120 121### In this folder 122 123| File | What | 124| --- | --- | 125| [lib.rs](lib.rs) | `Jev`, `Answered`, `Failure`, `RateLimit`, `KEY`; header redaction and rate-limit parsing. | 126| [transport.rs](transport.rs) | `Endpoint`, `HttpsTransport`, the courtesy buckets, connecting. | 127| [runtime.rs](runtime.rs) | `TokioRuntime`. | 128| [ledger.rs](ledger.rs) | `Ledger`, `Entry`, `Hold`, `Guards`, `Window`, `Refusal`, `Status`, `NoCredit`. | 129| [CLAUDE.md](CLAUDE.md) | Invariants for agents. | 130 131← Previous: [Chapter 20: jev-http](../) · Up: [jev-http](../) · Next: [Chapter 20¾: jev-http/tests](../tests/) →