# jevapi
An unofficial asynchronous Rust binding for the
[TypeSafe AI HTTP API](https://docs.typesafe.ai/api), built on Tokio and reqwest
with rustls enabled by default.
[crates.io](https://crates.io/crates/jevapi) · [API documentation](https://docs.rs/jevapi) ·
[GitHub](https://github.com/a1393323447/jevapi)
## Quick start
Add the dependencies from crates.io:
```sh
cargo add jevapi
cargo add tokio --features macros,rt-multi-thread
```
Or configure them in `Cargo.toml`:
```toml
[dependencies]
jevapi = "0.1"
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }
```
Set the `TYPESAFE_API_KEY` environment variable, then evaluate your input:
```rust,no_run
use jevapi::{Answer, Client, EvaluationRequest, Question};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let client = Client::from_env()?;
let request = EvaluationRequest::new("Our production payment service is down. Please act now.")
.question("urgent", Question::noul("Does this need immediate attention?"))
.question("team", Question::choice("Which team should handle this?", [
("engineering", Some("Outages and technical issues".into())),
("sales", None),
]))
.question("severity", Question::score("How severe is the issue?", [
"Minor impact", "Some functionality unavailable", "Production outage",
]));
let response = client.evaluate(&request).await?;
if let Some(Answer::Noul { noul }) = response.answers.get("urgent") {
println!("Urgency probability: {noul:.3}");
}
println!("Model: {}, usage: {:?}", response.model, response.usage);
Ok(())
}
```
## Design
- `Client` reuses its connection pool, is cheap to clone, and implements `Send + Sync`. `evaluate(&request).await` borrows the request so it can be reused. A Tokio runtime with I/O and time enabled is required.
- `ClientBuilder` configures the API key, base URL, overall timeout, retry policy, and an optional custom `reqwest::Client`. Building returns a `Result` for invalid configuration. `Client::new(key)` uses an explicit key; `Client::from_env()` reads `TYPESAFE_API_KEY`.
- `EvaluationRequest` requires a state at construction and defaults to `jev-latest`. Use `.model(...)` to specify any model version and chain `.question(...)` calls to add questions. Repeated identifiers replace earlier entries. Public fields and Serde deserialization are also supported.
- `Question` and `Answer` are enums tagged with `type`, covering Noul, Choice, and Score. Use `match` to access the appropriate fields. Score legends and probabilities retain the API's string indices; numeric answers use `f64`.
- `Content` accepts strings, objects, or arrays at the top level. Nested object and array values can contain any JSON value. Use `Content::try_from(serde_json::json!(...))?` for structured input. State, instructions, and criteria all support structured content.
- Local validation requires a nonempty model, at least one question, 1–255 Choice options, and 2–10 Score levels. The nonempty checks are SDK constraints; the API documentation specifies the Choice upper limit and Score range. Every request is validated before sending, including requests constructed through deserialization or direct field updates.
- `Error` distinguishes configuration, validation, HTTP transport, API status, response decoding, and overall timeout failures. `ApiError` preserves the status, response headers, and text body. Its `.json()` method parses error details without assuming a fixed error schema.
Provide optional descriptions of yes and no for a Noul question:
```rust
use jevapi::{NoulCriteria, Question};
let question = Question::noul_with_criteria(
"Is this urgent?",
NoulCriteria::default().yes("Needs immediate attention").no("Can wait"),
);
```
## Configuration, retries, and concurrency
```rust,no_run
use std::time::Duration;
use jevapi::{Client, RetryPolicy};
# fn main() -> Result<(), jevapi::Error> {
let client = Client::builder()
.base_url("https://api.typesafe.ai/v1")
.timeout(Duration::from_secs(30))
.retry_policy(RetryPolicy {
max_retries: 2,
initial_delay: Duration::from_millis(500),
max_delay: Duration::from_secs(4),
})
.build()?;
# Ok(())
# }
```
The default overall timeout is 60 seconds, covering all HTTP attempts, response
body reads, and retry delays. Only HTTP 429 and 529 are retried automatically,
with up to three additional attempts by default.
The backoff ceiling starts at 500 ms and doubles up to 8 seconds. Each delay is
randomly chosen between 50% and 100% of that ceiling. If the response includes
`Retry-After` as integer seconds or an HTTP date, the client waits at least that
long, subject to the overall timeout. Use `RetryPolicy::none()` to disable retries.
HTTP 401, 422, other HTTP errors, transport errors, and invalid successful
responses are returned immediately. The default client disables redirects and
reqwest's built-in transport retries to avoid implicit POST replay. When you
supply a custom HTTP client, its redirect, transport retry, and timeout settings
remain under your control. Dropping a future cancels local requests and waits,
but server processing that has already started may continue.
A shared client supports concurrent calls; an additional `Arc` is usually unnecessary:
```rust,no_run
# async fn example(client: jevapi::Client, a: jevapi::EvaluationRequest, b: jevapi::EvaluationRequest) -> Result<(), jevapi::Error> {
let (a, b) = tokio::try_join!(client.evaluate(&a), client.evaluate(&b))?;
# Ok(())
# }
```
API keys are omitted from `Debug` output for clients and builders. API errors omit
the response body from `Display`; their `Debug` output and public body/header
fields may still contain application data.
## TLS and scope
The default feature is `rustls-tls`. To use the system TLS backend:
```toml
jevapi = { version = "0.1", default-features = false, features = ["native-tls"] }
```
Disabling all TLS features leaves HTTP support only. The TypeSafe endpoint uses
HTTPS and requires a TLS backend.
This crate implements the documented `POST /v1/systemone` endpoint. The API
documentation does not define a streaming endpoint, so the crate provides no
SSE or streaming API. It targets native Tokio applications; WASM support is not claimed.
## Examples and verification
```text
cargo run --example evaluate
cargo run --example concurrent
cargo test --locked
cargo fmt --check
cargo clippy --all-targets --all-features --locked -- -D warnings
cargo doc --no-deps --locked
```
Both examples call the live API and require an API key in the environment. The
regular tests use a local mock HTTP server and do not require credentials.
The `evaluate` example covers all three question types in one request;
`concurrent` demonstrates two concurrent evaluations.
On 2026-09-22, a live API check using the `evaluate` example returned model
`jev-1.13.0`. All three answer types decoded successfully, using 418 input tokens
and 68 output tokens. This records one verification run; model aliases and
inference results may change over time.
API references: [API reference](https://docs.typesafe.ai/api) and
[Models](https://docs.typesafe.ai/models).
## License
This project is licensed under the [MIT License](LICENSE).