typesafe_jev/lib.rs
1//! Client for [TypeSafe](https://typesafe.ai)'s System One API and its Jev model.
2//!
3//! Jev does not generate text. A request carries a JSON `state` and a map of typed questions
4//! about it; the reply carries one calibrated answer per question. Every question in a request is
5//! evaluated against the same state in parallel, so a request with hundreds of questions takes
6//! about as long as one with a single question.
7//!
8//! ```no_run
9//! use serde_json::{json, Map};
10//! use typesafe_jev::{Client, Config};
11//!
12//! # fn main() -> Result<(), typesafe_jev::Error> {
13//! let client = Client::from_env(Config::default())?; // reads TYPESAFE_API_KEY
14//!
15//! let state = json!({
16//! "code": "1| import os\n2| def cwd():\n3| return os.getcwd()",
17//! "query": "where is the working directory read",
18//! });
19//! let mut questions = Map::new();
20//! questions.insert("line3".into(), json!({
21//! "type": "noul",
22//! "instructions": "Does line 3 of the code directly answer the query?",
23//! }));
24//! questions.insert("relevance".into(), json!({
25//! "type": "score",
26//! "instructions": "How relevant is the code to the query?",
27//! "criteria": ["unrelated", "tangential", "relevant", "exactly what was asked"],
28//! }));
29//!
30//! let answers = client.ask(&state, &questions)?;
31//! let p = answers["line3"]["noul"].as_f64().unwrap_or(0.0); // probability in [0, 1]
32//! let score = answers["relevance"]["score"].as_f64().unwrap_or(0.0); // position among the criteria
33//! println!("line 3 answers the query with p={p:.2}; relevance {score:.1}");
34//! println!("{} input tokens, ~${:.4}", client.usage().input_tokens(), client.usage().cost_usd());
35//! # Ok(())
36//! # }
37//! ```
38//!
39//! # Questions and answers
40//!
41//! Two question types are supported:
42//!
43//! - `noul` asks a yes/no question. The answer is
44//! `{"type": "noul", "noul": p}`, with `p` the probability in [0, 1] that the answer is yes.
45//! - `score` asks for a judgment on an ordered scale. The question lists its `criteria`, one
46//! per level, in order; the answer is `{"type": "score", "score": s, "confidence": c,
47//! "probabilities": {...}, "legend": {...}}`, with `s` a position on that scale counted from
48//! the first criterion.
49//!
50//! The probabilities are calibrated: a `noul` of 0.9 is right about nine times in ten. The
51//! `questions` map is passed through as given, so any field the API accepts can be sent.
52//!
53//! # Retries, concurrency and cost
54//!
55//! [`Client::ask`] retries connection failures and transient HTTP statuses with exponential
56//! backoff and jitter, honouring `Retry-After`. Rejected credentials come back at once as
57//! [`Error::Auth`]; a request too large for the model's context as [`Error::TokenLimit`], which
58//! the caller should split and retry. A key or a [`Config`] that no request could be sent with is
59//! reported by [`Client::new`], as [`Error::Auth`] or [`Error::InvalidConfig`], not retried.
60//!
61//! Calls are blocking. The client is `Send + Sync` and shares one connection pool, so call it from
62//! as many threads as [`Config::pool_size`]. An [`AdaptiveLimiter`] caps the concurrency and halves
63//! it whenever the API throttles, growing it back as requests succeed.
64//!
65//! [`Usage`] counts requests, retries and tokens across threads; [`Usage::cost_usd`] prices the
66//! input tokens at Jev's list price, [`USD_PER_INPUT_MTOK`].
67//!
68//! # Testing without the network
69//!
70//! [`Client::with_transport`] takes any [`Transport`], including a closure from the request body
71//! to a [`Reply`]:
72//!
73//! ```
74//! use serde_json::{json, Map};
75//! use typesafe_jev::{Client, Config, Reply};
76//!
77//! let client = Client::with_transport(
78//! |_body: &[u8]| Ok(Reply { status: 200, retry_after: None, body: r#"{"answers": {"q": {"type": "noul", "noul": 0.5}}}"#.into() }),
79//! Config::default(),
80//! );
81//! let mut questions = Map::new();
82//! questions.insert("q".into(), json!({"type": "noul", "instructions": "Is the state empty?"}));
83//! let answers = client.ask(&json!({}), &questions).unwrap();
84//! assert_eq!(answers["q"]["noul"], 0.5);
85//! ```
86
87#![warn(missing_docs, missing_debug_implementations)]
88
89mod client;
90mod error;
91mod limiter;
92mod transport;
93mod usage;
94
95pub use client::{Client, Config};
96pub use error::Error;
97pub use limiter::AdaptiveLimiter;
98pub use transport::{Reply, Transport};
99pub use usage::Usage;
100
101/// TypeSafe's hosted System One endpoint.
102pub const DEFAULT_BASE_URL: &str = "https://api.typesafe.ai/v1/systemone";
103/// The model name that follows TypeSafe's latest Jev release.
104pub const DEFAULT_MODEL: &str = "jev-latest";
105/// The environment variable [`Client::from_env`] reads.
106pub const API_KEY_VAR: &str = "TYPESAFE_API_KEY";
107/// Jev's list price per million input tokens, in dollars. Output tokens are free.
108pub const USD_PER_INPUT_MTOK: f64 = 0.042;
109
110/// The README's examples compile and run with the crate's own tests.
111#[cfg(doctest)]
112#[doc = include_str!("../README.md")]
113struct ReadmeDoctests;