jev-sdk 0.1.0

Unofficial Rust client for the TypeSafe AI System One API (Jev): typed decisions with calibrated probabilities
Documentation
# 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

| Type | Purpose | Answer |
|------|---------|--------|
| `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

| Setting | Environment variable | Default |
|---------|----------------------|---------|
| 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.