Jev API SDK Guide: Python and JavaScript Clients (typesafe-sdk)
Most Jev tutorials — including the earlier guides on this site — call the model over raw HTTP or through an OpenAI-compatible client. That works, but TypeSafe also ships two first-party SDKs, and until now this site never mentioned them. This page is the missing piece: what the Python and JavaScript SDKs actually install, what they change about the request, which client options exist, how retries and compression behave, and where the SDK is a worse choice than a thirty-line HTTP call.
Everything below was read on 2026-11-05 from the official documentation: the Python SDK page, its basic usage and advanced usage guides, the RetryPolicy reference, both SDK changelogs, the models page and the request rules and errors page. Version numbers and dates are the ones printed there, not estimates.
TL;DR:
pip install typesafe-sdk(importtypesafe_sdk) ornpm install @typesafe-ai/sdk(Node 22+). Both wrap the samePOST https://api.typesafe.ai/v1/systemonecall: you sendstateplus typedquestions(Noul,Choice,Score, andBoxon 1.14) and read typed answers back —nouls[...].noul,choices[...].choice,scores[...].scorein Python,answers.<name>.choicein JavaScript. The SDK’s real value is not the HTTP plumbing; it is typed questions, a documented retry policy, multipart asset handling for images, zstd compression over 1 KiB, and log redaction of secret headers. The catch: bodies are not redacted in debug logs, and the SDK targets the official channel — it is not a wrapper for the OpenRouter listing.
What the SDK is actually for
Jev is a System One model: it does not stream prose, it returns typed decisions. That makes the request shape unusual, and it is exactly the part a generic OpenAI-compatible client handles badly. The SDK’s job is to make the unusual part ordinary:
- You build questions as typed objects (
Noul(instructions=...),Choice(instructions=..., criteria={...}),Score(instructions=..., criteria=[...])) instead of hand-writing JSON that has to match a schema. - You read answers from typed buckets instead of
choices[0].message.content. In Python responses exposenouls,choicesandscoresdictionaries keyed by your question names; in JavaScript the singleanswersobject is typed from the questions you sent (“Answer types are inferred from your questions”, per the JS page). - Images, when you use them, become assets the SDK attaches and de-duplicates, rather than base64 blobs pasted into a prompt string.
What it does not do: it does not make Jev into a chat model, it does not add streaming text, and it does not remove the need to understand primitives. If you have not read the three primitives yet, do that first — the SDK is a convenience layer over exactly those three (plus Box) question types.
Install and first request
Python, two equivalent installs as documented:
uv add typesafe-sdk
# or
pip install typesafe-sdk
Optional extras are http2 (HTTP/2 transport), pil (send Pillow image objects) and zstd (compress request bodies). The zstd extra matters on Python earlier than 3.14 — see the compression section below.
Set the key in the environment, exactly as the docs specify:
export TYPESAFE_API_KEY="your-api-key"
Then the smallest useful call. This is the synchronous client from the quickstart, trimmed to the parts that carry information:
from typesafe_sdk import Choice, Noul, Score, TypeSafeClient
with TypeSafeClient() as client:
response = client.system_one(
state={"document": "I was charged twice. Please fix this ASAP."},
questions={
"billing": Noul(instructions="Is this ticket about billing?"),
"tone": Choice(
instructions="What is the customer's tone?",
criteria={"calm": None, "frustrated": None, "angry": None},
),
"urgency": Score(
instructions="How urgent is this ticket?",
criteria=["can wait", "this week", "today"],
),
},
)
print(response.nouls["billing"].noul)
print(response.choices["tone"].choice)
print(response.scores["urgency"].score)
Three things are worth noticing. First, state accepts a plain string or a structured object — the same rule as the raw API. Second, criteria for a Choice is a mapping of option to None in the structured form, while Score.criteria is an ordered list; that ordering is the ladder, and since SDK v0.6.0 both Python and JavaScript take it as an ordered sequence rather than a dictionary keyed by integers. Third, the answer comes back in named buckets, so no positional indexing and no parsing.
The async flavour is the same shape with AsyncTypeSafeClient and await:
import asyncio
from typesafe_sdk import AsyncTypeSafeClient, Noul
async def main() -> None:
async with AsyncTypeSafeClient() as client:
response = await client.system_one(
"I was charged twice. Please help ASAP.",
{"billing": Noul(instructions="Is this about billing?")},
)
print(response.nouls["billing"].noul)
asyncio.run(main())
JavaScript and TypeScript need Node.js 22 or newer — that is a hard requirement introduced in SDK v0.7.0:
npm install @typesafe-ai/sdk
import { choice, TypeSafeClient } from "@typesafe-ai/sdk";
const client = new TypeSafeClient();
const response = await client.systemOne({
state: { document: "I was charged twice. Please fix this ASAP." },
questions: {
category: choice("What is this ticket about?", {
billing: null,
technical: null,
other: null,
}),
},
});
console.log(response.answers.category.choice);
The JavaScript package ships ESM, CommonJS and TypeScript declarations. Note the naming difference that burns people switching between the two: Python uses system_one and returns separate nouls / choices / scores maps, JavaScript uses systemOne and one answers object with the answer’s type attached.
Client options, and the four environment variables
The Python TypeSafeClient constructor takes api_key, model, retry, timeout, headers, transport, http_client, base_url and compression. Explicit options win over environment variables, and empty or whitespace-only environment values are ignored — a useful detail if your deployment injects blank variables for unused settings.
The documented environment variables:
| Variable | Meaning |
|---|---|
TYPESAFE_API_KEY | API key; required unless passed as api_key |
TYPESAFE_DEFAULT_MODEL | Default model name |
TYPESAFE_BASE_URL | API root, if you proxy the API |
TYPESAFE_LOG_LEVEL | debug, info, warning, error, off |
Key validation is early and strict: leading and trailing whitespace is stripped, while empty keys, internal whitespace, control characters and non-ASCII characters are rejected — and per the v0.7.1 changelog the key is excluded from logged exceptions. That last point is worth knowing because it means a failing request will not print your key even in a traceback.
One gotcha in compression: the parameter accepts "zstd" or False. Defaults are zstd when available, and the client raises TypeSafeError when zstd is requested but unavailable rather than silently sending uncompressed. If you deploy to a slim container image, either install typesafe-sdk[zstd] or pass compression=False deliberately.
Typed responses: stop indexing, start naming
Because the questions are typed, the answers can be too. Python lets you hand system_one a response_model:
from typesafe_sdk import Noul, NoulAnswer, SystemOneResponse
class BillingResponse(SystemOneResponse):
billing: NoulAnswer
with TypeSafeClient() as client:
result = client.system_one(
"I was charged twice.",
{"billing": Noul(instructions="Is this about billing?")},
response_model=BillingResponse,
)
assert 0 <= result.billing.noul <= 1
You can also define a completely new model that does not inherit from SystemOneResponse, per the docs. In TypeScript the same benefit is free: answer types are inferred from the questions object, so answers.category.choice is a union of your option names rather than string.
Forward compatibility is handled with two escape hatches. extra_body sends request fields the SDK does not model yet — the documentation’s example uses an illustrative beam_width, and warns to only send fields the API supports. Raw question dictionaries work too, so a new primitive can be used before the SDK has a class for it:
client.system_one(
"I was charged twice.",
{"billing": {"type": "noul", "instructions": "About billing?", "weight": 2}},
)
If the API returns an answer kind the installed SDK does not recognise, the SDK logs a warning and skips it. The documented way to see everything is raw_http_response. That is a real operational trap: a routine SDK upgrade can add an answer type, and a pinned older SDK will quietly drop it from the parsed response while the raw body still contains it.
Retries: a policy, not a vibe
Retry behaviour is where hand-rolled clients usually rot. Python exposes it as a dataclass:
| Field | What it controls |
|---|---|
max_retries | Retries after the first attempt; 0 disables |
backoff_initial | First delay, doubled each attempt up to backoff_max; zero disables |
backoff_max | Ceiling for the delay; zero disables |
backoff_jitter | Fraction randomly subtracted, 0–1 |
http_statuses | Which HTTP codes are retried |
respect_retry_after | Honour Retry-After and retry-after-ms headers |
api_connection_error / api_timeout_error | Retry TypeSafeAPIConnectionError / TypeSafeAPITimeoutError |
exceptions | Extra exception types that trigger a retry |
predicate | Callable on the raised exception; True retries |
timeout | Total retry budget in seconds per call, including delays; None disables |
The documented example sets max_retries=3, timeout=10.0, http_statuses={429, 500, 502, 503, 504} — those are example values, not a claim about defaults. What the docs do state as behaviour: the SDKs retry rate-limit responses by default, wait longer between attempts, and follow Retry-After when the response carries one. That aligns with the API’s own guidance to back off exponentially on 429 Too Many Requests and 529 Overloaded instead of hammering.
The timeout field deserves emphasis because it is per call and includes the delays. It stops before a retry whose delay would reach or exceed the budget, then re-raises the last error. In a request path with a user waiting, that is the field you actually want to set — it bounds total latency, not each attempt.
Images and Box questions through the SDK
Image support is labelled dreamer-ready in the official docs: prototype-ready, not production-ready, with request format, answers and limits that can change between releases. Both SDKs gate it behind a header — TypeSafe-Danger-Zone-Images: true — which in Python goes in headers, and in JavaScript is passed as defaultHeaders on the client or per call. Images must be PNG or JPEG; the JavaScript page states the API detects the format from the bytes and rejects other formats such as WebP and GIF with BadRequestError.
Python places images directly in content, which is the cleanest of the three routes the docs describe:
from pathlib import Path
from typesafe_sdk import Image, Noul
photo = Image(returned_path)
with client.attach_assets(photo):
response = client.system_one(
state=f"The customer says {photo} arrived scratched.",
questions={"damaged": Noul(instructions="Is the item damaged?")},
)
The attach_assets context manager is not optional decoration. Without the with block, the SDK sends the rendered reference as plain text and the asset never attaches — the docs call this out as a warning, which suggests it is a common self-inflicted bug. Named references (Python assets={"<product>": Image(path)}) are the alternative: pick delimiters that cannot collide with ordinary text, like <product>, and every name in assets must appear in the request or the call is rejected before sending.
Box questions — the 1.14 primitive that returns a bounding box as fractions of width and height — are supported from Python SDK v0.7.5 and JavaScript SDK v0.7.0, both released 2026-10-14. The JavaScript page’s guidance is explicit that box questions should use the jev-preview model, and notes the answer’s confidence is null when the API does not report one. For what a box actually contains and how the edges map to pixels, see the 1.14 release notes rather than duplicating it here.
Performance: three settings that decide your throughput
The advanced-usage page is unusually concrete for SDK documentation, and three recommendations matter at scale.
Reuse the client. Constructing a client means new connections and TLS handshakes. One client per process, reused.
Pick your protocol deliberately. The clients default to HTTP/1.1: one request at a time per connection, a new connection per concurrent request up to the limit. HTTP/2 multiplexes everything over one socket with one handshake, but that single connection caps throughput. The documented recommendation is the counter-intuitive one — keep HTTP/1.1 for high request rates, and use HTTP/2 for low-to-moderate concurrency where a proxy or firewall limits connections.
Default limits are 100 connections, kept alive 30 seconds when idle. If you supply your own http_client, you inherit none of that and should set limits explicitly. In Python, TypeSafeClient is safe to share between threads but its pool becomes the bottleneck under heavy load, so the docs recommend a client per thread.
Compression is on by default for bodies of 1 KiB or more, using zstd. For large state payloads that is a straight win; for hundreds of tiny requests per second it is noise. Python earlier than 3.14 needs the [zstd] extra; JavaScript enables it on Node.js 22.15+ and exposes compression: false to turn it off. If you are behind a proxy that rewrites bodies, disable it rather than debugging mystery 400s.
Logging, and the redaction gap you must not miss
The SDK logs to a typesafe_sdk logger. Set logging.getLogger("typesafe_sdk").setLevel(logging.DEBUG), or set TYPESAFE_LOG_LEVEL before importing the SDK.
Read the redaction rules carefully, because they are asymmetric. At info you get one summary line per request. At debug you get request and response headers and bodies. Secret headers — authorization, API keys, cookies, and any header whose name contains token or secret — are redacted. Bodies are explicitly not redacted. Since Jev’s whole input is the state, debug logging in production means logging your customer documents. Turn it on deliberately, per environment, and never leave it on in a service that handles regulated data.
Version drift: pin the model, and pin the SDK
Two independent version surfaces move underneath you.
The model side: the aliases jev-latest and jev-preview both currently point to jev-1.14.0, and the docs say explicitly that an alias moves when a new release ships, so answers can change with no code change. Every response’s model field reports the versioned ID that answered — log it, or you will be unable to explain a shift in accuracy.
The SDK side has real breaking changes in a two-month window. Python went from msgspec to pydantic in v0.7.0 (2026-09-18), and both SDKs changed Score.criteria from an integer-keyed dictionary to an ordered sequence in v0.6.0 (2026-09-15). JavaScript v0.7.0 (2026-10-14) raised the floor to Node.js 22 and stopped exporting request payload types. If you pin versions loosely and deploy on a Friday, these are the changes that break a build.
Pricing is unchanged from the raw API: the models page lists $42 per billion input tokens ($0.042 per million), output tokens free, identical for 1.14 and 1.13, with 64k per-request and 32k per-question context, raised to 262k for both when you send TypeSafe-Danger-Zone-Long-Context: true. The SDK does not add a markup — it is a client, not a gateway.
When not to use the SDK
The SDK is the right default for a new Python or TypeScript service. It is the wrong choice in three situations, and they are common enough to name:
- You are on OpenRouter today. The SDK calls
POST https://api.typesafe.ai/v1/systemone; the OpenRouter listing fortypesafe/jev-1.14declaresoutput_modalities: decisionsand is reached through OpenRouter’s routing layer. Different endpoint, different payload. Changingbase_urldoes not bridge them — use the adapter pattern in the channels guide instead. - You need a runtime the SDKs do not target. The JavaScript SDK requires Node.js 22+; Python has both sync and async clients. Go, Rust, PHP and Java have no first-party SDK in this documentation set, so the raw HTTP call is the honest path — see the first API call.
- You want zero dependencies in a hot path. The decision payload is a flat JSON object. A thirty-line
fetch/requestswrapper covers it, and the error-code table is short enough to handle by hand. The SDK earns its dependency when you need retries, assets or typed answers — not for oneNoul.
A migration checklist from raw HTTP to the SDK
Doing it in this order avoids the two failures people hit: silently changing state contents, and losing visibility into which model answered.
- Freeze the questions first. Write the exact
Noul/Choice/Scoredefinitions and review the wording before touching code. The SDK makes questions cheap to add, which is a temptation to add ten of them. - Keep the threshold logic outside the client. Confidence handling is your business rule; the SDK only returns numbers. If you have tuned thresholds on 1.13, re-read confidence and fallback before switching to an alias.
- Log
response.modelfrom day one. It is the only reliable record of which version produced an answer. - Set an explicit
RetryPolicywith atimeoutbudget rather than inheriting defaults you have not read. - Turn on debug logging once, locally, and confirm what your bodies contain before ever enabling it in a deployed environment.
- Keep one adapter function. Whether the implementation is
TypeSafeClientorrequests.post, callers above it should not know.
That last habit is what makes the whole channel question cheap: if the SDK proves awkward, you replace one function, not a codebase.
FAQ
Which SDK should I start with, Python or JavaScript? Whichever matches your runtime. Both wrap the same endpoint and the same primitives; the differences are naming (system_one vs systemOne), response shape (nouls/choices/scores vs one answers object) and the Node.js 22 floor on the JavaScript side.
Does the SDK handle confidence thresholds for me? No. It returns the numbers — noul, option probabilities, score, and a per-answer confidence — and stops there. Thresholding is your policy, and it belongs in your code where you can test it against a labelled set.
What happens to answers the installed SDK does not recognise? The SDK logs a warning and skips them. The documentation directs you to raw_http_response to inspect the full body, which is also the recommended check when a new question type lands before an SDK release.
Can I still use raw JSON questions? Yes. Both SDKs accept raw question dictionaries alongside typed objects, and Python’s extra_body forwards request fields the SDK has not modelled. The docs’ caveat is to only send fields the API actually supports.
Does the SDK change what I pay? No. Pricing comes from the API: $42 per billion input tokens with free output tokens, the same on 1.14 and 1.13. The SDK adds no layer between you and the meter.