typesafe-jev
A typed Rust client for TypeSafe's System One API and its Jev model.
Jev does not generate text. You send a state and a set of typed questions about it, and get
back one calibrated answer per question. Every question in a request is evaluated against the
same state in parallel, so a request with hundreds of questions takes about as long as one with
a single question.
The three question types of the API reference, Noul, Choice
and Score, and their answers are Rust types here, field for field, so a misspelt field or a
missing criteria is a compile error rather than an HTTP 422.
This crate is the client that jevgrep (jg,
natural-language code search) uses for its default backend, extracted so that it can be used on
its own. It has no async runtime; its dependencies are serde and serde_json for the JSON,
indexmap to keep questions and options in the order you give them, and ureq with rustls for
the HTTPS. It is an independent client, not an official TypeSafe SDK.
Usage
[]
= "0.2"
use ;
Client::new(api_key, config) takes the key directly. Config sets the endpoint (a gateway in
front of TypeSafe works too), the model, the timeouts, the retry budget and the connection pool:
use Duration;
use ;
examples/ask.rs is a complete program: TYPESAFE_API_KEY=... cargo run --example ask.
Questions and answers
| Question | Asks | Answer |
|---|---|---|
Noul::new(instructions), optionally .yes(..) and .no(..) |
a yes/no question | NoulAnswer { noul }: the probability of a yes, from 0 to 1 |
Choice::new(instructions, [(option, description), ..]), or Choice::labels(instructions, [option, ..]) |
for one option out of up to 255 | ChoiceAnswer { choice, confidence, probabilities } |
Score::new(instructions, [level, ..]) |
for a rating against 2 to 10 ordered levels | ScoreAnswer { score, confidence, legend, probabilities }, with levels numbered from 0 |
-
The probabilities are calibrated: of the
noulanswers given as 0.9, about nine in ten are a yes.confidencesays how far to trust achoiceor ascore; see TypeSafe's confidence page. -
Questionskeeps its questions, and aChoiceits options, in the order you add them, and that is the order on the wire.Response::noul,choiceandscorelook an answer up by id and type;response.answersis the whole ordered map ofAnswers. -
Instructions and descriptions are
Content: a&strorStringconverts withinto(), andContent::try_from(json!({...}))takes the structured form, where a question names its own data fields in backticks:use json; use ; let instructions = try_from .expect; let question = new; -
The
stateis anything that serializes to a JSON string, object or array: a&str, aserde_json::Value, or your own#[derive(Serialize)]type. Jev reads text only. -
Every type implements
SerializeandDeserialize, so questions can live in a config file and answers in a log. A reply is read strictly: an answer that lacks a field the reference requires is anError::Api, not a silent default. Unknown fields are ignored, and the types are#[non_exhaustive], so the API can grow without breaking your build.
Retries, concurrency and cost
- Connection failures and transient HTTP statuses (408, 409, 425, 429, 5xx, 529) are retried
with exponential backoff and jitter, honouring
Retry-After, up toConfig::max_retries. - Rejected credentials are returned at once as
Error::Auth; a request too large for the model's context asError::TokenLimit, which the caller should split and retry; a body the API refuses as malformed (HTTP 422) asError::InvalidRequest. Everything else, including retries exhausted, isError::Api. The enum is#[non_exhaustive]. - Calls block. The client is
Send + Syncand shares one connection pool: callaskfrom as many threads asConfig::pool_size. AnAdaptiveLimitercaps the concurrency and halves it whenever the API throttles, growing it back as requests succeed. TypeSafe's rate limit is a sustained token budget with no quota headers, and the limiter is what keeps several processes on one key from tripping over each other. Usagecounts requests, retries and tokens across threads.Usage::cost_usdprices the input tokens at Jev's list price (USD_PER_INPUT_MTOK); output tokens are free. EachResponsealso carries theTokenUsageof its own request, and themodelthat answered it.Client::set_debug_reporterreceives one line per failed attempt. Nothing is logged otherwise.
Testing without the network
Client::with_transport takes any Transport, including a closure from the request body to a
Reply, so tests and local fakes need no server:
use ;
let client = with_transport;
let response = client.ask.unwrap;
assert_eq!;
NoulAnswer::new, ChoiceAnswer::new and ScoreAnswer::new build answers for a fake that works
above the client.
Security notes
- The key is sent as a bearer token to whatever
Config::base_urlnames, over HTTPS by default. Plainhttp://is accepted, for local fakes; do not point a real key at one. - Error messages quote at most 400 characters of a failed response body. They never quote the
request, the key or the base URL, so the state you sent and your credentials stay out of logs.
Client'sDebugoutput leaves the key out too. - Redirects are followed, but the
Authorizationheader is not sent to the redirect's target. #![forbid(unsafe_code)]; TLS is rustls with the bundled web PKI roots.
Compatibility
- The minimum supported Rust version is 1.85, checked in CI. Raising it is a minor version bump.
serde,serde_json(ValueandMapinsideContent) andindexmap(IndexMapinChoice::criteria,ChoiceAnswer::probabilitiesandResponse::answers) are part of the public API;ureqis not. The lowest versions the manifest allows pass the test suite.- The crate follows semantic versioning. Before 1.0, a breaking change bumps the minor version; see the changelog.
License
Licensed under either of the Apache License, Version 2.0 or the MIT license, at your option.
Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in this crate by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.