# jev-sdk
An unofficial Rust client for the [TypeSafe AI](https://typesafe.ai) System One API.
[Jev](https://typesafe.ai/blog/introducing-system-one-models-and-jev) is a
System One model: instead of generating text, it evaluates typed *questions*
against a *state* and returns structured decisions with calibrated
probabilities and confidence scores — in ~70–500ms. Think of it as a
frontier-intelligence function call: unstructured state in, typed
probabilistic decisions out. It can't hallucinate schema violations, because
the possible outputs are defined in advance.
## Features
- All three question primitives: **Choice**, **Score**, **Noul** — mixable in a
single parallel call
- Typed answers with full probability distributions and confidence
- Async client built on `reqwest` (rustls, no OpenSSL)
- Config via environment variables or a builder
- Automatic retries with exponential backoff + `Retry-After` for
`429`/`529`/connection failures
- Structured error types for auth, validation, and rate-limit failures
## Install
```toml
[dependencies]
jev-sdk = "0.1"
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }
```
## Quick start
Get an API key from the [dashboard](https://console.typesafe.ai/settings/keys)
and export it:
```sh
export TYPESAFE_API_KEY=apikey_...
```
```rust
use jev_sdk::{Choice, Noul, Question, Score, TypeSafeClient};
#[tokio::main]
async fn main() -> Result<(), jev_sdk::Error> {
let client = TypeSafeClient::from_env()?;
let response = client
.system_one(
"Hi, I've been trying to connect my Stripe account for 3 days \
and it keeps failing. I'm losing sales. Please help ASAP.",
[
(
"department",
Question::from(Choice::new(
"Which team should handle this",
[
("billing", Some("Payment or subscription issues")),
("technical", Some("Bugs or integration problems")),
("sales", Some("Pricing or account questions")),
],
)),
),
(
"frustration",
Question::from(Score::new(
"How frustrated the customer appears",
["Calm, just stating facts", "Frustrated but civil", "Very angry"],
)),
),
(
"is_urgent",
Question::from(Noul::new("The message conveys urgency or time-sensitivity")),
),
],
)
.await?;
let department = response.choice("department").unwrap();
println!("route to {} (confidence {:.2})", department.choice, department.confidence);
println!("frustration: {:.2}", response.score("frustration").unwrap().score);
println!("urgency: {:.3}", response.noul("is_urgent").unwrap().noul);
Ok(())
}
```
Run the included version against the live API:
```sh
TYPESAFE_API_KEY=apikey_... cargo run --example triage
```
## Question types
| `Noul` | Is this statement true? | `noul`: probability of "yes", 0–1 |
| `Choice` | Pick one option from a set | `choice`, `probabilities`, `confidence` |
| `Score` | Rate against ordered levels | `score` (probability-weighted), `legend`, `probabilities`, `confidence` |
- **State** can be a string or any serializable structure (objects, arrays —
chat logs, records, application state).
- **Instructions** accept strings or structured JSON.
- **Choice criteria** map option → optional description (`Choice::options` for
bare names).
- **Noul criteria** optionally describe what "yes" and "no" mean.
- Ask many questions in one call: they're evaluated in parallel, and adding
more barely changes latency.
```rust
use jev_sdk::{Choice, NoulCriteria};
Choice::options("Sentiment?", ["positive", "negative", "mixed"]); // no descriptions
Noul::new("Does this convey urgency?").criteria(NoulCriteria::new(
"Explicitly time-sensitive", // what "yes" means
"No urgency expressed", // what "no" means
));
```
## Using the answers
`SystemOneResponse.answers` is keyed by your question ids. Typed accessors
return `Option`s so you can't mix up question types:
```rust
# let response: jev_sdk::SystemOneResponse = todo!();
if let Some(ans) = response.choice("department") {
if ans.confidence >= 0.8 {
// act autonomously
} else {
// escalate to a human
}
}
for (id, ans) in response.nouls() { /* ... */ }
```
The model never makes type errors; every Choice/Score answer also carries a
calibrated `confidence` you can gate automation on.
## Configuration
| API key | `TYPESAFE_API_KEY` | — (required) |
| Model | `TYPESAFE_DEFAULT_MODEL` | `jev-latest` |
| Base URL | `TYPESAFE_BASE_URL` | `https://api.typesafe.ai` |
Explicit builder options take precedence over environment variables:
```rust
use std::time::Duration;
use jev_sdk::{RetryPolicy, TypeSafeClient};
let client = TypeSafeClient::builder()
.api_key("apikey_...")
.model("jev-latest")
.timeout(Duration::from_secs(30))
.retry(RetryPolicy::default()) // or RetryPolicy::none()
.build()?;
// Per-call model override:
// client.system_one_with_model("jev-preview", state, questions).await?;
```
List available models with `client.list_models().await?`.
## Errors
```rust
use jev_sdk::Error;
match client.system_one("hi", [("q", jev_sdk::Noul::new("test?").into())]).await {
Err(e) if e.is_authentication() => { /* 401: check the key */ }
Err(e) if e.is_rate_limited() => { /* 429/529: the SDK already retried; back off longer */ }
Err(e) if e.is_validation() => { /* 400/422: fix the request */ }
Err(Error::Api(api)) => eprintln!("status {}: {}", api.status, api.message),
Err(e) => return Err(e),
Ok(_) => {}
}
```
## Security
The client is hardened by default:
- **Credential protection** — the API key is stored in a sensitive
`HeaderValue` and redacted from all `Debug` output; keys containing header
control characters are rejected (no CRLF injection).
- **TLS-only transport** — base URLs must use HTTPS; plain HTTP is accepted
only for loopback hosts (local testing). TLS uses rustls with certificate
verification; the crate never exposes a way to disable it.
- **No redirects** — the built-in HTTP client never follows redirects, so the
`Authorization` header cannot be forwarded to an unexpected host.
- **Bounded memory** — response bodies are size-capped (16 MiB success /
64 KiB error) and read incrementally, so a hostile or buggy endpoint cannot
exhaust memory. Automatic decompression is not enabled (no gzip bombs).
- **Bounded waits** — retries use saturating exponential backoff with jitter;
server `Retry-After` hints are honored but capped at 60s.
- **Dependencies** — minimal, mainstream crates only (`reqwest`, `serde`,
`serde_json`, `indexmap`, `thiserror`, `tokio`), vetted with `cargo audit`.
Supplying your own `reqwest` client via `ClientBuilder::http_client`
transfers responsibility for transport hardening (TLS verification, redirect
policy, timeouts) to you.
## Testing
```sh
cargo test # unit tests
TYPESAFE_API_KEY=... cargo test # also runs live API integration tests
```
## Notes
- This crate is async (tokio). There is no blocking façade yet.
- MSRV: 1.75.
## Links
- [TypeSafe AI](https://typesafe.ai) · [Documentation](https://docs.typesafe.ai)
· [API reference](https://docs.typesafe.ai/api) ·
[Introducing System One Models & Jev](https://typesafe.ai/blog/introducing-system-one-models-and-jev)
- [Playground](https://console.typesafe.ai/playground)
## License
Dual-licensed under [MIT](LICENSE-MIT) or [Apache-2.0](LICENSE-APACHE), at
your option.