A community Java client for the TypeSafe System One API. Ask small, typed questions over your application state and get calibrated probabilities back, in one round trip, with no prompt parsing.
This library is an independent, community-maintained project and is not affiliated with, endorsed by, or supported by TypeSafe AI. It follows the conventions of the official Python and JavaScript SDKs so the three read alike. TypeSafe is a trademark of its owner; the name is used here only to describe what the library connects to.
Requires Java 17 or newer. Depends only on Jackson.
Gradle:
implementation("io.github.premo-cloud:typesafe-sdk:0.1.1")Maven:
<dependency>
<groupId>io.github.premo-cloud</groupId>
<artifactId>typesafe-sdk</artifactId>
<version>0.1.1</version>
</dependency>Spring Boot users can add io.github.premo-cloud:typesafe-sdk-spring-boot-starter instead and get a TypeSafeClient bean from
typesafe.api-key and friends in application.properties.
The shape mirrors the Python and JavaScript SDKs: a client, systemOne(state, questions), and question types named
Noul, Choice, and Score that take (instructions, criteria).
TypeSafeClient client = TypeSafeClient.fromEnvironment(); // reads TYPESAFE_API_KEY
TypeSafeResponse response = client.systemOne(
Map.of("document", "I was charged twice. Please fix this ASAP."),
Map.of("category", Choice.of("What is this ticket about?", "billing", "technical", "other"),
"urgent", Noul.of("Does `document` convey urgency?")));
response.choices().get("category").choice(); // "billing"
response.noul("urgent"); // 0.0 to 1.0When a question needs structure, every type also takes a configurer, so nested requests read top to bottom with no
build() calls, in the style of the Elasticsearch and AWS Java clients:
TypeSafeResponse response = client.systemOne(r -> r
.state(Map.of(
"email", Map.of(
"from", "alerts@secure-notice.example",
"subject", "Action required: confirm your account details",
"body", "Your access will be suspended unless you confirm your details at the link below within 24 hours."),
"context", Map.of("recipient_domain", "example.com")))
.noul("is_phishing", n -> n
.instructions("Does `email` attempt to trick the recipient into revealing credentials or payment details?")
.whenTrue(c -> c.what("Impersonates a trusted organization or demands urgent verification via a link")
.examples("Confirm your details within 24 hours to avoid suspension"))
.whenFalse("A legitimate request from a known counterparty"))
.choice("category", c -> c
.instructions("Which category best describes `email`?")
.option("MARKETING", "Promotional content sent to a list")
.option("PHISHING", o -> o.what("Credential theft or impersonation").notFor("Legitimate requests to confirm a payment"))
.option("NOT_SPAM")) // an undescribed label
.score("urgency", s -> s
.instructions("How hard does `email.body` press the recipient to act immediately?")
.level("No time pressure")
.level("Mentions a deadline")
.level("Threatens loss or suspension within hours")));
double phishing = response.noul("is_phishing"); // 0.0 to 1.0
ChoiceAnswer category = response.choice("category"); // choice(), probabilities(), confidence()
ScoreAnswer urgency = response.score("urgency"); // score(), probabilities(), confidence(), legend()Everything in one request runs in parallel on the server and shares one round trip. Only start a second request when an answer is needed to build the next state.
state is any Jackson-serializable value: a String, a Map, or your own record. Give questions named fields to point
at (`email.body`) rather than one long string. state(key, value) adds a field to an object state you have already set.
Noul.of(instructions)asks yes or no;whenTrueandwhenFalsedescribe the outcomes.Choice.of(instructions, labels...)picks one label;option(label, description)describes a label,option(label)leaves it undescribed.Score.of(instructions, levels...)places the state on an ordered rubric of at least two levels.
Instructions are optional when the criteria say enough on their own. Any description can be a plain string or a
Criterion with what, notFor, and examples. Prebuilt questions are plain records and can be shared across requests.
When the rules are user-defined data rather than code, CriteriaQuestionSet puts them into the state and generates one
noul per entry that points at its own `criteria[i]` path:
TypeSafeRequest request = CriteriaQuestionSet
.over("document", documentState, rules, Rule::id,
(rule, path) -> Noul.of("Is `document` about the subject matter described in %s?".formatted(path)))
.build();Every error extends TypeSafeException. A non-2xx response after retries raises a TypeSafeApiException subclass
named for the status, TypeSafeAuthenticationException for 401, TypeSafeRateLimitException for 429 with retryAfter(),
TypeSafeInternalServerException for 5xx, and so on, each carrying status(), body(), headers(), and requestId().
Delivery failures raise TypeSafeConnectionException, or its subclass TypeSafeTimeoutException. Asking a response for a
missing key or the wrong primitive raises IllegalArgumentException.
By default the client retries twice after the first attempt on HTTP 408, 429, and 5xx, on connection failures, and on
timeouts, with exponential backoff from 500 ms capped at 5 s and 25 percent jitter, honoring Retry-After and
retry-after-ms up to one minute. Retried attempts carry an X-TypeSafe-Retry-Count header.
TypeSafeClient.builder().apiKey(key).retryPolicy(RetryPolicy.of(r -> r.maxRetries(5).backoffMax(Duration.ofSeconds(20)))).build();
TypeSafeClient.builder().apiKey(key).retryPolicy(RetryPolicy.none()).build();Any call accepts RequestOptions to override the client's timeout, retry policy, or headers for that call only:
client.systemOne(request, RequestOptions.of(o -> o.timeout(Duration.ofSeconds(30)).maxRetries(0)));
client.models().list(RequestOptions.of(o -> o.header("X-Trace", traceId)));List<ModelCard> models = client.models().list(); // name, description, releaseDateExplicit values win over environment variables, which win over defaults.
TypeSafeClient client = TypeSafeClient.builder()
.apiKey(key) // or TYPESAFE_API_KEY
.baseUrl("https://api.typesafe.ai") // or TYPESAFE_BASE_URL
.defaultModel("jev-latest") // or TYPESAFE_DEFAULT_MODEL; TypeSafeRequest.model(...) overrides per request
.timeout(Duration.ofSeconds(10)) // per attempt; default 10 s
.retryPolicy(RetryPolicy.DEFAULT)
.header("X-Team", "review") // sent with every request
.httpClient(myHttpClient) // optional: proxies, executors
.objectMapper(myObjectMapper) // optional: custom serializers for your state types
.build();Add io.github.premo-cloud:typesafe-sdk-spring-boot-starter and set one property:
typesafe.api-key=${TYPESAFE_API_KEY}A TypeSafeClient bean is then available for injection. The starter creates it only when the key is set, backs off if
you define your own, and reuses the application's ObjectMapper. Properties, customization, testing, and troubleshooting
are covered in the starter README.
./gradlew build
Tests run against an in-process stub server and need no API key.
Issues and pull requests are welcome at Premo-Cloud/typesafe-sdk-java. Please keep the public API aligned with the official SDKs' conventions and add a test for every behavior change.
MIT, Copyright (c) 2026 Garret Premo. See LICENSE.