typesafe-ai-sdk
Rust client for the TypeSafe AI System One API: send a state plus named,
typed questions and get typed answers back.
| Question | Answer |
|---|---|
Noul |
probability of "yes" (0–1) |
Choice |
selected label, per-label probabilities, confidence |
Score |
probability-weighted level, legend, per-level probabilities, confidence |
Behaviour mirrors the official Python SDK (typesafe-sdk 0.6.0): the same environment variables,
defaults, retry semantics, error classification and forward-compatible response decoding.
Unofficial. Not affiliated with TypeSafe AI.
Install
[]
= "0.1" # async (bring your own Tokio runtime)
# typesafe-ai-sdk = { version = "0.1", features = ["blocking"] } # sync client
# typesafe-ai-sdk = { version = "0.1", features = ["reqwest-client"] } # bring your own reqwest::Client
The library is imported as typesafe. MSRV: Rust 1.88. TLS is rustls; HTTPS_PROXY-style
environment variables are honoured.
Quick start
use ;
async
cargo run --example triage runs the same flow against the live API.
State
state is anything Serialize: a string, json!({...}), or your own struct.
client.system_one.await?;
Structured instructions and rubrics
Instructions, option descriptions and score levels accept any JSON value:
use ;
new;
new.when_true;
from_labels;
Typed choices
let dept: Dept = res.choice.unwrap.parse?;
Per-call options
Requests implement IntoFuture, so you can .await them directly or configure them first:
client.system_one
.model
.timeout
.retry
.header
.extra_body // shallow-merged last
.await?;
Authentication and SDK-identification headers cannot be overridden.
Models
for m in client.models.list.await?.models
Blocking
let client = from_env?;
let res = client.system_one.send?;
The blocking client owns a private current-thread runtime; don't call it from inside async code.
Configuration
| Builder method | Environment variable | 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 | |
retry |
RetryPolicy::default() |
|
http_client |
a fresh reqwest::Client (feature reqwest-client) |
Explicit values win; blank environment values are ignored. Header types come from the http crate,
re-exported as typesafe::http.
Retries
RetryPolicy::default() matches the Python SDK:
- 2 retries after the first attempt,
- exponential backoff from 0.5 s to 5 s with 25 % subtractive jitter,
- retries on 408, 429 and 500–599 (including TypeSafe's
529 Overloaded), connection errors and timeouts, - honours
retry-after-msandRetry-After(seconds or HTTP date), - a 30 s total budget per call: it stops before a wait that would exceed it,
- retries send
X-TypeSafe-Retry-Count.
use Duration;
use RetryPolicy;
default
.max_retries
.backoff
.budget
.retry_if;
Errors
match client.system_one.await
| Variant | When |
|---|---|
Config |
missing API key, invalid base URL, zero timeout, invalid retry policy |
InvalidRequest |
no questions, empty choice/score criteria, malformed raw question, unencodable state |
Api |
non-2xx after retries; kind, message, body, request_id(), retry_after() |
Connection |
no response (DNS, connect, reset, body read); HTTP client error in source() |
Timeout |
an attempt exceeded its timeout |
ResponseValidation |
2xx body missing required data; field_path like answers.tone.confidence |
Error messages from FastAPI-style validation bodies are flattened, e.g.
questions.frustration.criteria: List should have at least 2 items.
Forward compatibility
- Answer types this version does not know are skipped (logged via
tracingat WARN) and remain inresponse.raw. - Unknown response fields are ignored.
Question::Raw(json!({...}))sends a hand-built question;extra_bodyadds top-level fields.
Logging
Uses tracing: INFO when a request is retried, DEBUG for each request/response line, TRACE for
headers and bodies. Secret headers are redacted; bodies (including your state) are not.
Differences from the Python SDK
- Answers are looked up with
res.noul(name)/res.choice(name)/res.score(name)or iterated withnouls()/choices()/scores(); Score maps are keyed byu32. - Typed answer maps keep server order;
response.rawusesserde_json::Mapordering. ResponseMetaexposes status, headers and the number of attempts.- No
TYPESAFE_LOG_LEVEL; configure yourtracingsubscriber instead.
Development
Releasing
Or let CI do it: push a tag matching the version in Cargo.toml
(git tag v0.1.0 && git push origin v0.1.0). That runs
.github/workflows/release.yml, which re-runs fmt, clippy and
both test configurations, checks the tag against the manifest version, and publishes with the
CARGO_REGISTRY_TOKEN repository secret.
License
MIT