typesafe-rs
Rust client for TypeSafe System One.
Evaluate a state against named questions (noul, choice, score) and get one typed answer per question.
This is a community SDK. It is not an official TypeSafe product. Env vars, defaults, retries, identification headers, and error kinds match the official Python SDK and @typesafe-ai/sdk.
Install
[]
= "0.1"
= { = "1", = ["macros", "rt-multi-thread"] }
# optional: blocking client
= { = "0.1", = ["blocking"] }
# tests: in-process mock of POST /v1/systemone and GET /v1/models
[]
= "0.1"
MSRV is 1.85 (edition 2024). TLS is rustls by default (native-tls is available).
Quick start
use ;
async
state can be a string, a JSON object, or an array. Keys you pick on the question map come back on answers.
List models:
let models = client.models.list.await?;
Call client.warm_up().await? once at process start if you want the first real request to reuse a pooled connection.
Configuration
Client::from_env() and Client::new resolve fields as code, then env, then default.
| Setting | Env | Default |
|---|---|---|
| API key | TYPESAFE_API_KEY |
required |
| Base URL | TYPESAFE_BASE_URL |
https://api.typesafe.ai |
| Model | TYPESAFE_DEFAULT_MODEL |
jev-latest |
| Timeout | — | 10 s per attempt, including the body |
use Duration;
use ;
let client = new
.api_key
.default_model
.timeout
.retry
.build?;
Per-call overrides: CallOptions (timeout, retry, headers, model) with system_one_with. Authorization, Accept, User-Agent, X-TypeSafe-SDK, X-TypeSafe-Runtime, and X-TypeSafe-Retry-Count cannot be overridden.
Every request sends:
Authorization: Bearer <key>Accept: application/jsonUser-Agent: typesafe-rs/<version>X-TypeSafe-SDK: typesafe-rs/<version>X-TypeSafe-Runtime: rust/<rustc>; <os>-<arch>X-TypeSafe-Retry-Count: <n>on retries only (nstarts at 1)
API keys are redacted in Debug. Error display never includes the key or the request body.
Retries
Default policy matches the official SDKs:
| Property | Value |
|---|---|
| Max retries | 2 (3 attempts total) |
| Retry on | HTTP 408, 429, all 5xx; connection errors; timeouts |
| Backoff | 500 ms initial, 5 s cap, 25% jitter |
| Server delay | retry-after-ms, then Retry-After (seconds or HTTP date), capped at 60 s |
Presets:
RetryPolicy::default(): official behaviourRetryPolicy::none(): no retriesRetryPolicy::conservative(): 408/429 and pre-send connection errors only (no 5xx, no timeouts)
Dropping the future cancels further attempts. Retrying 5xx on POST /v1/systemone can duplicate billable work; use conservative() if that matters more than completing the call.
Errors
match client.system_one.await
| Variant | When |
|---|---|
MissingApiKey |
No key in config or TYPESAFE_API_KEY |
InvalidRequest |
Client-side validation (empty questions, Choice with fewer than 2 options, Score with fewer than 2 levels, …) before any network call |
Connection |
DNS, TLS, or connect failure |
Timeout |
Attempt exceeded the timeout |
Api |
Non-2xx after retries are exhausted (401 → Authentication, 429 → RateLimit, 5xx → InternalServer, …) |
Decode |
Body is not JSON of the expected type |
UnexpectedShape |
JSON parsed but is missing a documented field (for example GET /v1/models without models) |
Unknown answer type values deserialize as Answer::Unknown instead of failing.
Testing
typesafe-rs-mock is an in-process HTTP server. Tests talk to a real Client over loopback, not a stub of the SDK.
use ;
use ;
let mock = start.await;
mock.on_system_one.respond;
let client = new
.api_key
.base_url
.build?;
let response = client
.system_one
.await?;
assert_eq!;
Script a 429 then a success, including retry-after-ms:
mock.on_system_one
.respond_status
.header
.times;
mock.on_system_one.respond.times;
mock.journal() records method, path, headers, JSON body, and timestamps. Use it to assert X-TypeSafe-Retry-Count and attempt order.
Run the bundled example (no live key):
Blocking client
= { = "0.1", = ["blocking"] }
use ;
let client = from_env?;
let response = client.system_one?;
Do not construct BlockingClient inside an existing Tokio runtime (block_on will panic).
Crates
| Crate | Role |
|---|---|
typesafe-rs |
Async Client, optional BlockingClient, wire types, retry, errors. #![forbid(unsafe_code)] |
typesafe-rs-mock |
In-process mock for tests |
Backend is implemented for Client so callers can depend on the trait rather than the HTTP type.
Feature flags
| Feature | Default | Notes |
|---|---|---|
rustls |
yes | TLS via rustls (platform verifier) |
native-tls |
no | Platform TLS instead |
tracing |
yes | typesafe.request span and retry_scheduled events |
blocking |
no | BlockingClient |
Conformance
JSON fixtures in conformance/fixtures/ drive the real client against the real mock. Coverage includes 429 + retry-after-ms, non-retryable 400, env precedence, unknown answer types, missing models array, and retry-count headers.
Development
See CONTRIBUTING.md. Behaviour marked [parity] in SPEC.md must match the official Python and TypeScript SDKs.
License
Licensed under either of Apache License, Version 2.0 or MIT at your option.