TypeSafe for Rust
A Rust client for TypeSafe's System One API, the API behind Jev. It is unofficial and not published by TypeSafe. Its behaviour follows the official Python SDK:
- the same environment variables and defaults
- the same retry policy
- the same error kinds
It has an async client, and a blocking one for synchronous code such as a game loop or a CLI. It is the Rust sibling of typesafe-elixir-sdk.
System One models don't generate text. You send some state and a set of typed questions, and get back one typed answer per question, with probabilities your code acts on.
use json;
use ;
let client = new; // reads TYPESAFE_API_KEY
let questions = new
.ask
.ask
.ask;
let response = client.system_one?;
response.noul.unwrap.noul // 0.99
response.choice.unwrap.choice // "frustrated"
response.choice.unwrap.confidence // 0.78
response.score.unwrap.score // 1.99
response.model // "jev-1.13.0"
response.usage // Usage { input_tokens: 367, output_tokens: 75 }
Installation
[]
= "0.1"
That gives the async client. For the blocking one as well, use
{ version = "0.1", features = ["blocking"] }, or for the blocking one alone,
{ version = "0.1", default-features = false, features = ["blocking", "rustls"] }.
The library is named typesafe, so code says use typesafe::....
| Feature | What it adds |
|---|---|
async (default) |
typesafe::Client, which needs a tokio runtime with the time driver (#[tokio::main] has it) |
blocking |
typesafe::blocking::Client, which must not be called from inside an async runtime |
rustls (default), native-tls |
the TLS backend; with neither, only http:// base URLs work |
It uses reqwest, serde_json, httpdate and fastrand, plus tokio for the async
client.
Questions and answers
| Question | Asks | Answer |
|---|---|---|
Noul::new(instructions) |
whether a condition holds | NoulAnswer: noul, the probability of yes |
Choice::new(instructions) |
which one of a defined set | ChoiceAnswer: choice, probabilities, confidence |
Score::new(instructions, levels) |
how far along ordered levels | ScoreAnswer: score, legend, probabilities, confidence |
use ;
new
.option
.option
.option;
new
.yes
.no;
A few details keep answers easy to use:
- Answers use the question's ids.
response.choice("team")returns theChoiceAnswerasked under"team".response.answersholds all of them. - Choice options keep their order. They're sent in the order you added them, because the order is part of what the model reads.
- State keeps its order.
stateis a string or anything that serializes to a JSON object or array. A struct is sent with its fields in declaration order. - Score levels are indexes. Keys in
legendandprobabilitiesareusize, not the strings on the wire. - Helpers:
ChoiceAnswer::rankedgives options by probability, useful for a "did you mean" prompt.ChoiceAnswer::isandChoiceAnswer::probabilitylook up one option.ScoreAnswer::normalizedscales the score to 0..=1.ScoreAnswer::levelgives the most probable level.NoulAnswer::yesandNoulAnswer::yes_abovecompare the probability to a threshold.
Instructions and descriptions take anything that converts to serde_json::Value, so
json!({...}) works when structure makes a question clearer. Question::Raw(json) is
sent as given, and its answer comes back as Answer::Raw. That covers question types
this client predates.
Malformed questions, such as a Choice with no options, are rejected with
ErrorKind::InvalidRequest before anything is sent.
Code owns the decision
Ask independent questions together: they share one read of the state and run in parallel. Then let ordinary code apply the policy.
let team = response.choice.unwrap;
let route = if response.noul.unwrap.yes_above else if team.is || team.confidence < 0.5 else ;
confidence measures how concentrated the distribution is. It is not the probability
that the answer is right. Tune thresholds on your own data, and pin a versioned model
such as "jev-1.13.0" once you have. jev-latest moves when a new release ships.
For many items, run calls concurrently. The client is cheap to clone and shares its connection pool:
let handles: = tickets
.into_iter
.map
.collect;
Configuration
Config setter |
Environment variable | Default |
|---|---|---|
api_key |
TYPESAFE_API_KEY |
none |
base_url |
TYPESAFE_BASE_URL |
https://api.typesafe.ai |
model |
TYPESAFE_DEFAULT_MODEL |
jev-latest |
timeout |
10 s per response | |
retry |
see below | |
header |
extra request headers |
use Duration;
use ;
let client = with_config?;
Client::new() is Config::from_env() with no changes. Config::new() ignores the
environment. The API key is kept out of Debug output.
A missing key doesn't stop a client from being built, so an app can start without one.
Calls then fail with ErrorKind::NoApiKey without making a request.
client.is_configured() tells you which case you are in.
system_one_with takes CallOptions to override the model, timeout, retry policy
and headers for one call. CallOptions::extra_body adds top-level fields for API
features this client predates.
Errors and retries
Every call returns Result<_, typesafe::Error>. An error has:
kind()status(), the HTTP statusmessage(), the server's messagerequest_id(), from thex-typesafe-request-idheaderretry_after()body()
It displays as 422 questions.tone.criteria: field required (request_id=...).
ErrorKind |
When |
|---|---|
NoApiKey |
no key configured; no request made |
InvalidRequest |
a malformed call, such as a Choice with no options; no request made |
BadRequest, Authentication, PermissionDenied, NotFound |
400, 401, 403, 404 |
UnprocessableEntity |
422; the message lists each field as path: problem |
RateLimited, Overloaded, ServerError, HttpError |
429, 529, other 5xx, anything else |
Timeout, Connection |
no response |
InvalidResponse |
a 2xx missing a required field; the message names it |
Retries follow the official SDKs:
- What is retried: up to 2 retries on 408, 429 and every 5xx, and on timeouts and failed connections.
- Backoff: from 500 ms, doubling to 5 s, with 25% jitter.
- Server waits:
retry-after-msandretry-afterare honoured. - Budget: 30 s per call, including waits. A retry whose wait would pass the budget is not attempted.
- Retry count: each retry sends
x-typesafe-retry-count.
use RetryPolicy;
let patient = RetryPolicy ;
let never = disabled;
Examples
TYPESAFE_API_KEY=... TYPESAFE_API_KEY=...
Developing
TYPESAFE_API_KEY=...
TypeSafe docs: index · primitives · confidence · HTTP API
License
MIT. See LICENSE.