README.mdpreviewREADME.mdsource141 lines · 9.0 KB · raw
1# jev-http
2
3`jev-protocol` says what to ask and `jev-client` how to retry; this sends it.
4One `POST` to `https://api.typesafe.ai/v1/systemone` over HTTP/2
5(`src/transport.rs`, ported from postjevsql-pg's `https.rs` onto tokio), and a
6client that is meant to be kept so the TLS connection stays warm - a cold
7handshake costs more than the answer does.
8
9```rust
10let model = jev_protocol::ModelId::pinned("jev-1.13.0")?;            // never an alias
11let jev = jev_http::Jev::from_env("native", model).expect("TYPESAFE_API_KEY")?;   // never a literal
12let mut questions = jev_protocol::Questions::new();
13let raining = questions.noul("raining", jev_protocol::Noul::new(jev_protocol::Json::text("Is it raining?")))?;
14let answered = jev.ask(&state, &questions, "why this question").await?;
15println!("{} in {:?}", answered.response.get(raining).noul, answered.took);
16```
17
18`"native"` is which binary is asking - `native`, `player`, `jevprobe`,
19`jevhooks`, ... - recorded against every spend in `ledger`'s shared ledger.
20See "The lifetime ledger" below for what that buys.
21
22## The lifetime ledger and its guards
23
24The account behind this key has a few dollars of credit, ever. `ledger.rs` is
25an on-disk, `flock`-guarded, append-only log of every request any binary has
26ever made (`$XDG_STATE_HOME/jev/spend.jsonl`), and `Jev::ask` has it admit
27every single request before a byte is sent, whichever process is asking:
28`Ledger::admit` reads the file, checks the guards and appends a **hold** for
29the admitted request, all under one lock, so two processes asking at the same
30instant are serialised and the second sees the first one's question. The real
31cost is appended against the hold when the answer arrives (`settle`), or the
32hold is released unanswered (`release`). `Jev::new` takes a `Ledger` as a
33required argument: there is no client that skips this.
34
35| Guard | Default | When it refuses | How it clears |
36| --- | --- | --- | --- |
37| Question bucket | `JEV_MAX_QUESTIONS_PER_MINUTE` = 30 | 30 questions admitted in the last 60 s, by all processes together | **By itself.** Each question's token comes back 60 s after it was spent - the ledger's own recent lines are the bucket. `Failure::Throttled` with the seconds until the next token. |
38| Hour breaker | `JEV_MAX_DOLLARS_PER_HOUR` = $0.25 | the last hour's spend plus this request's worst case is over the limit | **By itself**, once enough of that spend is over an hour old. `Failure::Throttled`. |
39| Out of credit | - | the vendor answered "no credit" (inferred shape, digest §10) | Only when the account is topped up: `Ledger::credit_restored`. `Failure::Budget`. |
40
41**There is no lifetime cap.** An earlier version of this file refused once
42`lifetime_usd` crossed a self-imposed `JEV_LIFETIME_CAP_USD` (`$4.00`, never
43above an `ABSOLUTE_LIFETIME_CEILING_USD` of `$4.50`) - removed 2026-09-22, the
44user's own ruling on seeing it ("not this random $4 number"). A
45made-up ceiling on the account's own money was never this project's number to
46invent. What stays: the two RATE guards above (they bound how FAST money can
47go out, not how much, ever), and the vendor's own out-of-credit answer, which
48is a fact about the account rather than a guess - the only hard stop on total
49spend now. `Status::line` shows what has been spent, plainly, with no cap and
50no denominator.
51
52**Why the lifetime total is shown "estimated".** There is no vendor endpoint
53to check it against (below), so it is booked from our own recorded token
54counts plus one guessed opening line for whatever was spent before the ledger
55existed. That guess was revised once, 2026-09-22, against the user's own
56TypeSafe dashboard reading (last 7 days, $0.1876 - the account's entire
57history to date): subtracting the ledger's own real spend at that same moment
58($0.0359) leaves $0.1517 of real pre-ledger spend, not the $0.25 first
59guessed on 2026-09-20 - see `ledger.rs`'s `OPENING_ESTIMATE_USD` for the
60arithmetic. It is still a guess, which is why it stays flagged rather than
61being trusted exactly: the dashboard itself is marked "*Estimated", and nine
62more days of play will make the pre-ledger fraction of the total smaller and
63smaller regardless.
64
65Nothing a caller is refused by a throttle is retried here: the caller goes on
66without an answer (`decisions`' `Engine` falls back to that decision's own
67upstream pick) and does not ask again until the time it was told.
68
69**Why the rate guards heal themselves.** Until 2026-09-21 a trip of either
70breaker wrote `paused.json`, which blocked every process until a human
71removed it; that day an agent tripped the question breaker (two headless runs
72and the window asking together) and "cleared" it by deleting the file. A
73guard that needs a human to delete a file will have its file deleted. Now
74there is no file: the bucket is the ledger, and it refills as the window
75slides. **The hour breaker reopens by itself too, on purpose**: it is a RATE
76guard, not a budget - it bounds how fast money can go out, never how much,
77ever - so a latch a human must reset would add nothing but a file somebody
78deletes. The only thing that stops questions for good is the vendor's own
79out-of-credit answer, which no amount of waiting clears either, because it is
80a fact about the account, not a rate.
81
82A process killed mid-request leaves its hold unsettled; it keeps counting its
83worst case (a few hundredths of a cent) forever, the conservative direction.
84
85`Jev::status` / `Ledger::status` say all of this as one value, which the
86window's Bot panel shows and the MCP `budget` tool returns.
87
88**The vendor's own documented ceilings** (250,000 tokens/second, 1,200
89requests/minute - jevsnes `research/jev-api-digest.md` §4) are respected with a
90per-process client-side bucket as a courtesy against a `429`; the shared
91question bucket above is far stricter.
92
93The ledger was opened once, at $0.1517 (revised 2026-09-22; $0.25 originally),
94estimated, dated to when this mechanism was built - the true spend before any
95of this existed, since the vendor exposes no usage endpoint to read the real
96figure from (§10).
97
98**No balance/usage/credit endpoint exists to read instead - checked directly,
992026-09-22**, on top of the digest's own §10 (which had already grepped the
100vendor's full doc dump and found none):
101
102| Checked | Grade | Result |
103| --- | --- | --- |
104| `GET api.typesafe.ai/v1/{usage,billing,balance,credits,credit,account,organization(s),me,whoami,spend,api-keys,keys}` and a few nested variants, with the real key | Authoritative (the API itself) | Every one `404 {"detail":"Not Found"}`. |
105| `GET api.typesafe.ai/v1/models` | Authoritative | Lists model aliases and release dates only (§1); no account field. |
106| `docs.typesafe.ai/api` | Authoritative (vendor docs) | Documents exactly one endpoint, the evaluation POST. |
107| `docs.typesafe.ai/llms.txt` (the full site index) | Authoritative | 44 sections; none titled account, billing, usage (outside the SDK's per-response `Usage` type), credits, balance, organization, dashboard or console. |
108| `typesafe.ai` home page | Authoritative | Links a "Sign in" to `console.typesafe.ai` - a session-authenticated web dashboard, not a documented API route the key can call. This is where the user's own dashboard reading comes from. |
109| Web search for a `GET /api/v1/credits`-shaped route | Anecdotal, and almost certainly not the vendor - `jev-ai.pro`, `jevapi.org`, `jevtypesafeai.com` are unofficial products trading on the "Jev" name (the same kind of copycat the brain's `ecosystem/` bundle already catalogues by the dozen), not `api.typesafe.ai`. Probed directly against the real API anyway (above): `404`. |
110
111So `Ledger`'s file is the only account of spend this project has, and
112`Status::line`'s lifetime figure is shown "estimated" rather than exact for
113exactly this reason.
114
115## Host-only
116
117The protocol (`jev-protocol`) compiles for `wasm32-unknown-unknown`; this
118crate does not, and does not need to: `api.typesafe.ai` refuses browser
119origins outright (a CORS preflight from `https://example.com` returns HTTP 400
120"Disallowed CORS origin", measured 2026-09-20), so a web build talks to a
121relay that holds the key server-side. The relay speaks `jev-protocol`'s types
122too; only the transport differs. That is the whole reason the protocol and
123the transport are separate crates.
124
125## TLS without C
126
127rustls with the **RustCrypto** provider and **compiled-in** Mozilla roots - no
128OpenSSL, no `ring`, no `aws-lc`, nothing to build with a C compiler or cmake,
129and nothing read out of `/etc/ssl`. hyper drives the HTTP/2 connection
130directly, so a refused stream or a GOAWAY is told apart from an interrupted
131request (`TransportErrorKind::NotSent`), and resent without risking a second
132charge. `Endpoint::trusting` adds a PEM file to the roots, for jev-mock's
133throwaway CA and private endpoints.
134
135## The key
136
137`TYPESAFE_API_KEY`, from the environment and from nowhere else. It is never
138logged, never printed, never written to a file, and never put on a command
139line; the repository README says how it gets into a process
140(`op-env-run -- …`). `Jev::from_env` returns `None` when it is absent, which
141is not an error - the app runs, with the loop switched off, and says why.