lib.rsannotatedlib.rssource404 lines · 18.2 KB · raw

The Jev API over HTTPS for a native process: jev-client's policy on this crate's transport (transport.rs: HTTP/2 over pure-Rust TLS on tokio) and runtime, and the shared spend ledger in front of both.

WHAT to ask is jev-protocol's; how to retry is jev-client's. This is only how the bytes get there, and what they cost. The browser build cannot reach the API directly anyway (api.typesafe.ai refuses browser origins), so a web page gets a relay and never links this.

Every request is checked against [ledger::Ledger] before it is sent. This account's credit is finite, so a client that could be built and used without a shared, on-disk record of what has already been spent would make overspending a matter of which binary you happened to run - see ledger's own doc comment for why the record has to be a file and not a number kept in memory. Jev::new/Jev::from_env are the only ways to get a client, and both build the ledger check into it, so there is no path to ask that skips it. There is no self-imposed lifetime cap here (removed 2026-09-22, the user's own ruling) - the only hard stop is the vendor's own out-of-credit answer, which is a fact about the account, not a guess.

21pub mod ledger;
22mod runtime;
23mod transport;
25use std::sync::{Arc, Mutex, PoisonError};
26use std::time::{Duration, Instant};
27
28use jev_client::{Client, ClientError};
29use jev_protocol::{ApiErrorKind, Json, ModelId, Questions, Response, worst_case_dollars};
30
31pub use ledger::{Entry, Guards, Hold, Ledger, NoCredit, Refusal, Status, Window};
32pub use runtime::TokioRuntime;
33pub use transport::{ENDPOINT, Endpoint, HttpsTransport};

The environment variable the key arrives in. On this machine it reaches a process through op-env-run and nowhere else - never a dotfile, never a command line: op-env-run -- <command> (jev-http's README).

38pub const KEY: &str = "TYPESAFE_API_KEY";

What a response's headers said about how much of the vendor's own rate limit is left, when they said anything at all. Digest §10: as of 2026-09-20 the vendor documents no such headers on a successful response - only Retry-After/retry-after-ms on a 429/529 - so every field here is commonly None. It stays a typed, if usually-empty, snapshot rather than nothing at all so that the day the vendor adds one, this starts reporting it with no code changed at every call site.

47#[derive(
48    Debug, Clone, Copy, Default, PartialEq, serde::Serialize, serde::Deserialize, schemars::JsonSchema,
49)]
50pub struct RateLimit {
51    pub limit: Option<u64>,
52    pub remaining: Option<u64>,

Seconds until the window resets, exactly as the header gave it - not resolved to a clock time, since that would claim a precision about when the header was actually read that nothing here can back up.

56    pub reset_seconds: Option<f64>,
57}

A client, and one to reuse: it holds the connection, and a warm connection is worth hundreds of milliseconds per decision.

61pub struct Jev {
62    client: Client<HttpsTransport, TokioRuntime>,

Which binary this is, for the ledger's records - native, player, jevprobe, ...

65    binary: String,
66    ledger: Ledger,
67    guards: Guards,

The most recent parsed [RateLimit], kept for [Jev::last_rate_limit] so a caller (the budget MCP tool, in particular) does not have to wait for the next request to see what the last one learned.

71    last_rate_limit: Mutex<Option<RateLimit>>,
72}

What one answered request cost in time as well as tokens.

75#[derive(Debug)]
76pub struct Answered {

The answers, verified against the questions asked.

78    pub response: Response,

The whole round trip, retries and waits included.

80    pub took: Duration,

How many times the request had to be sent. 1 is the happy path.

82    pub attempts: u32,

The response exactly as it arrived. Kept so that a field the protocol does not know about is visible rather than silently dropped by the parse - which is how the digest gets corrected.

86    pub body: String,

Every response header, name and value, with anything that looks like a credential redacted. apps/jevprobe --headers exists to print this.

89    pub headers: Vec<(String, String)>,

x-typesafe-request-id, if the response carried one (digest §1).

91    pub request_id: Option<String>,
92    pub rate_limit: Option<RateLimit>,
93}

Why an ask produced no answer. The first two never touch the network.

96#[derive(Debug)]
97pub enum Failure {

A shared guard that clears itself refused: the same request would be admitted after retry_in. The caller goes on without an answer.

100    Throttled { why: String, retry_in: Duration },

Nothing is admitted until a human acts: the vendor said the account is out of credit, or the ledger cannot be read or written (a guard that cannot see the spend fails closed).

104    Budget(String),

The request went out and failed, as jev-client judged it after its retries.

107    Client(ClientError),
108}
110impl std::fmt::Display for Failure {
111    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
112        match self {
113            Self::Throttled { why, retry_in } => {
114                write!(f, "not sent, throttled: {why}; clears in {:.0}s", retry_in.as_secs_f64())
115            }
116            Self::Budget(why) => write!(f, "not sent: {why}"),
117            Self::Client(e) => e.fmt(f),
118        }
119    }
120}
121
122impl std::error::Error for Failure {}
123
124impl Jev {

Read the key from the environment and build a client for the API, its ledger opened from $XDG_STATE_HOME (see ledger::Ledger::open). None when the key is not there, which is not an error: the app runs without it and says on screen that it cannot play.

129    pub fn from_env(binary: impl Into<String>, model: ModelId) -> Option<Result<Self, String>> {
130        let key = std::env::var(KEY).ok().filter(|key| !key.trim().is_empty())?;
131        Some(
132            Ledger::open()
133                .map_err(|e| format!("opening the spend ledger: {e}"))
134                .and_then(|ledger| Self::new(key.trim(), binary, ledger, model, Endpoint::api())),
135        )
136    }

Build a client against an already-open [Ledger]. Every real caller wants [Self::from_env]; this is the constructor that makes the dependency explicit rather than reached for internally, so a test can hand it a ledger rooted in a private directory instead of the real $XDG_STATE_HOME/jev - and so there is no Jev value anywhere that was not required, at construction, to say where its spend is kept.

144    pub fn new(
145        key: &str,
146        binary: impl Into<String>,
147        ledger: Ledger,
148        model: ModelId,
149        endpoint: Endpoint,
150    ) -> Result<Self, String> {
151        // The trust anchors are compiled in rather than read from the host, so
152        // what this trusts is the same on every machine and in a sandbox with
153        // no /etc/ssl at all. The provider is RustCrypto: pure Rust, no ring,
154        // no aws-lc, nothing to cross-compile for the web build later.
155        let mut roots = rustls::RootCertStore::empty();
156        roots.extend(webpki_roots::TLS_SERVER_ROOTS.iter().cloned());
157        if let Some(path) = endpoint.ca_file() {
158            use rustls::pki_types::CertificateDer;
159            use rustls::pki_types::pem::PemObject;
160            let shown = path.display();
161            for cert in CertificateDer::pem_file_iter(path).map_err(|e| format!("CA file {shown}: {e}"))? {
162                let cert = cert.map_err(|e| format!("CA file {shown}: {e}"))?;
163                roots.add(cert).map_err(|e| format!("CA file {shown}: {e}"))?;
164            }
165        }
166        let mut tls = rustls::ClientConfig::builder_with_provider(Arc::new(rustls_rustcrypto::provider()))
167            .with_safe_default_protocol_versions()
168            .map_err(|e| format!("rustls protocol versions: {e}"))?
169            .with_root_certificates(roots)
170            .with_no_client_auth();
171        tls.alpn_protocols = vec![b"h2".to_vec()];
172        let transport = HttpsTransport::new(endpoint, Arc::new(tls));
173        let client = Client::new(transport, TokioRuntime, model, key).map_err(|e| e.to_string())?;
174        Ok(Self {
175            client,
176            binary: binary.into(),
177            ledger,
178            guards: Guards::from_env(),
179            last_rate_limit: Mutex::new(None),
180        })
181    }

The same client under other guards than the environment's.

184    #[must_use]
185    pub fn with_guards(mut self, guards: Guards) -> Self {
186        self.guards = guards;
187        self
188    }
190    pub fn model(&self) -> &ModelId {
191        self.client.model()
192    }
193
194    pub fn ledger(&self) -> &Ledger {
195        &self.ledger
196    }
197
198    pub fn guards(&self) -> Guards {
199        self.guards
200    }

What the shared guards say right now - every process's questions, not only this one's.

204    pub fn status(&self) -> Result<Status, String> {
205        self.ledger.status(ledger::now(), self.guards)
206    }

The most recent [RateLimit] this client has seen, if any response ever carried one.

210    pub fn last_rate_limit(&self) -> Option<RateLimit> {
211        *self.last_rate_limit.lock().unwrap_or_else(PoisonError::into_inner)
212    }

Ask, under jev-client's retry policy - but first, have the shared ledger admit it ([Ledger::admit]: question bucket, hour breaker, out of credit - there is no lifetime cap, per the user's own ruling, 2026-09-22), before a single byte goes over the network. A refusal that clears itself is [Failure::Throttled], one that does not (out of credit) is [Failure::Budget]; neither is retried here.

why is recorded against the spend in the ledger's reason. It is a required argument, not an option, so that every line of the ledger can say why the money went: the breaker trip of 2026-09-20 had to be reconstructed from token counts because no line said.

225    pub async fn ask(&self, state: &Json, questions: &Questions, why: &str) -> Result<Answered, Failure> {
226        let worst_case = worst_case_dollars(self.client.model(), state, questions);
227        let hold = self.ledger.admit(ledger::now(), &self.guards, worst_case, &self.binary, why).map_err(
228            |refusal| match refusal {
229                Refusal::Throttled { why, retry_in } => Failure::Throttled {
230                    why,
231                    retry_in: Duration::try_from_secs_f64(retry_in).unwrap_or(Duration::MAX),
232                },
233                other => Failure::Budget(other.to_string()),
234            },
235        )?;
236
237        let started = Instant::now();
238        match self.client.ask(state, questions).await {
239            Ok(answered) => {
240                let rate_limit = parse_rate_limit(&answered.headers);
241                if let Some(rate_limit) = rate_limit {
242                    *self.last_rate_limit.lock().unwrap_or_else(PoisonError::into_inner) = Some(rate_limit);
243                    if let Err(e) = self.ledger.note_rate_limit(&rate_limit) {
244                        eprintln!("jev ledger: could not record a rate-limit snapshot: {e}");
245                    }
246                }
247                let usage = answered.response.usage();
248                let cost = usage.dollars();
249                let entry = Entry {
250                    time: ledger::now(),
251                    binary: self.binary.clone(),
252                    input_tokens: usage.input_tokens,
253                    output_tokens: usage.output_tokens,
254                    cost_usd: cost,
255                    request_id: answered.request_id.clone(),
256                    estimated: false,
257                    reason: Some(why.to_owned()),
258                    hold: None,
259                };
260                if let Err(e) = self.ledger.settle(&hold, entry) {
261                    // The request already happened and was paid for
262                    // whether or not this write succeeds; refusing to
263                    // return the answer would not undo that, it would
264                    // just also lose the decision. The hold stays
265                    // unsettled, so the ledger goes on counting its
266                    // worst case - more than it cost. Loudly logged,
267                    // since a ledger that cannot be written to is a
268                    // problem worth a human's attention on its own.
269                    eprintln!("jev ledger: could not record a ${cost:.6} spend: {e}");
270                }
271                Ok(Answered {
272                    took: started.elapsed(),
273                    attempts: answered.attempts,
274                    body: String::from_utf8_lossy(&answered.body).into_owned(),
275                    headers: collect_headers(&answered.headers),
276                    request_id: answered.request_id,
277                    rate_limit,
278                    response: answered.response,
279                })
280            }
281            Err(error) => {
282                if let ClientError::Api { error: api, .. } = &error
283                    && api.kind == ApiErrorKind::OutOfCredit
284                    && let Err(e) = self.ledger.mark_out_of_credit("the vendor reports no credit remaining")
285                {
286                    eprintln!("jev ledger: could not record the vendor's out-of-credit answer: {e}");
287                }
288                // `release` books the hold at nothing, which is true only
289                // when nothing can have been billed: the request was never
290                // built, or its one attempt was refused outright. After an
291                // interrupted or timed-out attempt the cost is unknown (the
292                // vendor reports usage only on success), so the hold stays
293                // and goes on counting its worst case, as a crashed
294                // process's does.
295                if error.nothing_billed()
296                    && let Err(e) = self.ledger.release(&hold, ledger::now(), &error.to_string())
297                {
298                    eprintln!("jev ledger: could not release an unanswered hold: {e}");
299                }
300                Err(Failure::Client(error))
301            }
302        }
303    }
304}

Any of the conventional rate-limit header spellings, read opportunistically. Digest §10: none of these are documented anywhere in the vendor's own reference as of 2026-09-20, so this is expected to return None on every real response; it stays general rather than named after one convention so that if the vendor starts sending any of them, this starts reporting it with no code changed anywhere that calls ask.

312fn parse_rate_limit(headers: &http::HeaderMap) -> Option<RateLimit> {
313    let text = |name: &str| headers.get(name)?.to_str().ok().map(str::to_owned);
314    let as_u64 = |name: &str| text(name)?.trim().parse::<u64>().ok();
315    let as_f64 = |name: &str| text(name)?.trim().parse::<f64>().ok();
316    let limit = as_u64("x-ratelimit-limit-requests")
317        .or_else(|| as_u64("x-ratelimit-limit"))
318        .or_else(|| as_u64("ratelimit-limit"));
319    let remaining = as_u64("x-ratelimit-remaining-requests")
320        .or_else(|| as_u64("x-ratelimit-remaining"))
321        .or_else(|| as_u64("ratelimit-remaining"));
322    let reset_seconds = as_f64("x-ratelimit-reset-requests")
323        .or_else(|| as_f64("x-ratelimit-reset"))
324        .or_else(|| as_f64("ratelimit-reset"));
325    (limit.is_some() || remaining.is_some() || reset_seconds.is_some())
326        .then_some(RateLimit { limit, remaining, reset_seconds })
327}

Every header name and value, redacting anything that could be a credential by name - a bearer token echoed back, a cookie, anything with "key"/"token"/"secret" in it. apps/jevprobe --headers is the one caller that prints this; Answered always carries it so that mode needs no separate request.

334fn collect_headers(headers: &http::HeaderMap) -> Vec<(String, String)> {
335    let sensitive = |name: &str| {
336        let lower = name.to_ascii_lowercase();
337        ["authorization", "cookie", "token", "secret", "-key", "api-key"]
338            .iter()
339            .any(|word| lower.contains(word))
340    };
341    headers
342        .iter()
343        .map(|(name, value)| {
344            let name = name.as_str().to_owned();
345            let value = if sensitive(&name) {
346                "«redacted»".to_owned()
347            } else {
348                value.to_str().unwrap_or("«non-utf8»").to_owned()
349            };
350            (name, value)
351        })
352        .collect()
353}
355#[cfg(test)]
356mod tests {
357    use super::*;
358
359    fn model() -> ModelId {
360        ModelId::pinned("jev-1.13.0").expect("a pinned model")
361    }
362
363    fn sandbox_ledger() -> Ledger {
364        let dir = std::env::temp_dir()
365            .join(format!("jev-http-test-{}", std::process::id()))
366            .join(ledger::now().to_string());
367        Ledger::at(dir).expect("opening a sandbox ledger")
368    }
369
370    #[test]
371    fn a_missing_key_is_not_an_error() {
372        // Whatever this machine's environment happens to hold, an empty one
373        // means "no client", not "a broken client".
374        assert!(
375            Jev::new("", "test", sandbox_ledger(), model(), Endpoint::api()).is_ok(),
376            "an empty key still builds; the API refuses it"
377        );
378    }
379
380    #[test]
381    fn rate_limit_headers_are_read_when_present_and_absent_otherwise() {
382        let empty = http::HeaderMap::new();
383        assert_eq!(parse_rate_limit(&empty), None, "no header, no snapshot");
384
385        let mut headers = http::HeaderMap::new();
386        headers.insert("x-ratelimit-remaining-requests", "37".parse().expect("a header value"));
387        let rate_limit = parse_rate_limit(&headers).expect("a snapshot");
388        assert_eq!(rate_limit.remaining, Some(37));
389        assert_eq!(rate_limit.limit, None, "only what was actually sent is filled in");
390    }
391
392    #[test]
393    fn a_bearer_token_echoed_back_would_be_redacted() {
394        let mut headers = http::HeaderMap::new();
395        headers.insert("x-typesafe-request-id", "req_abc123".parse().expect("a header value"));
396        headers.insert("set-cookie", "session=deadbeef".parse().expect("a header value"));
397        let collected = collect_headers(&headers);
398        let value_of = |name: &str| {
399            collected.iter().find(|(n, _)| n == name).map(|(_, v)| v.clone()).unwrap()
400        };
401        assert_eq!(value_of("x-typesafe-request-id"), "req_abc123");
402        assert_eq!(value_of("set-cookie"), "«redacted»");
403    }
404}