TypeSafe Java SDK

September 18, 2026 · View on GitHub

Zero-dependency Java client for the TypeSafe AI System One API. Requires Java 21 or newer.

TypeSafe turns natural language and application state into typed judgments: yes/no probabilities, choices with full distributions, and scores on rubrics you define. Learn what you can build in the TypeSafe docs.

Quickstart

Add the dependency:

<dependency>
    <groupId>io.github.kgonia</groupId>
    <artifactId>typesafe-sdk</artifactId>
    <version>0.1.0</version>
</dependency>

Set TYPESAFE_API_KEY in your environment (create a key in the console), then:

import io.github.kgonia.typesafe.TypeSafeClient;
import io.github.kgonia.typesafe.systemone.*;
import java.util.Map;

try (TypeSafeClient client = TypeSafeClient.create()) {
    SystemOneResponse response = client.systemOne(
        Map.of("document", "I was charged twice. Please fix this ASAP."),
        Map.of(
            "billing", Question.noul("Is this ticket about billing?"),
            "tone", Question.choice("What is the customer's tone?", "calm", "frustrated", "angry"),
            "urgency", Question.score("How urgent is this ticket?", "can wait", "this week", "today")));

    System.out.println(response.noul("billing").noul());      // 0.97
    System.out.println(response.choice("tone").choice());     // "frustrated"
    System.out.println(response.score("urgency").score());    // 1.8
}

Questions and answers

NeedQuestionAnswer
Whether a condition holdsQuestion.noul(instructions)Answer.Noul: noul() probability of yes
One of a defined setQuestion.choice(instructions, options…)Answer.Choice: choice(), probabilities(), confidence()
Degree along an ordered rubricQuestion.score(instructions, levels…)Answer.Score: score(), legend(), probabilities(), confidence()

Instructions and criteria descriptions can be a String, a Map (JSON object), or a List (JSON array). The state can be a String, a Map, a List, or a record.

Question.Choice tone = Question.Choice.builder("What is the customer's tone?")
    .option("calm")
    .option("frustrated", "Annoyed but polite")
    .option("angry", "Hostile or threatening")
    .build();

Question.Noul spam = Question.noul("Is this message spam?",
    "Unsolicited advertising or scams",     // what a yes means
    "A genuine message from a person");     // what a no means

Answers form a sealed hierarchy, so you can switch over them exhaustively:

for (var entry : response.answers().entrySet()) {
    String line = switch (entry.getValue()) {
        case Answer.Noul n -> "yes with p=" + n.noul();
        case Answer.Choice c -> "chose " + c.choice() + " (confidence " + c.confidence() + ")";
        case Answer.Score s -> "scored " + s.score() + " = " + s.nearestLevelDescription();
        case Answer.Unknown u -> "unrecognized answer type " + u.type();
    };
    System.out.println(entry.getKey() + ": " + line);
}

Configuration

Explicit builder settings win over environment variables, which win over SDK defaults. Blank environment values are ignored.

VariableConfiguresDefault
TYPESAFE_API_KEYAPI key (required)none
TYPESAFE_BASE_URLAPI root URLhttps://api.typesafe.ai
TYPESAFE_DEFAULT_MODELModel when a request names nonejev-latest
TYPESAFE_LOG_LEVELSDK log levelwarn
TypeSafeClient client = TypeSafeClient.builder()
    .apiKey("...")
    .defaultModel("jev-1.13.0")                       // pin a version instead of the alias
    .timeout(Duration.ofSeconds(20))                  // per attempt
    .retryPolicy(RetryPolicy.builder().maxRetries(5).build())
    .defaultHeader("X-Team", "support-bot")
    .logLevel(LogLevel.INFO)
    .build();

The client is thread-safe. Create one per application and share it. Close it when you are done; close() shuts down the HTTP client the SDK created. An HttpClient you pass through builder().httpClient(...) stays open for you to manage.

Per-request options

SystemOneRequest request = SystemOneRequest.builder()
    .state(ticket)
    .question("billing", Question.noul("Is this about billing?"))
    .model("jev-preview")
    .build();

RequestOptions options = RequestOptions.builder()
    .timeout(Duration.ofSeconds(30))
    .retryPolicy(RetryPolicy.none())
    .header("X-Request-Source", "batch-job")
    .build();

SystemOneResponse response = client.systemOne(request, options);

Async

Every call has an Async variant returning a CompletableFuture. Cancelling the future cancels the in-flight request and any pending retry.

CompletableFuture<SystemOneResponse> future = client.systemOneAsync(state, questions);
future.thenAccept(r -> System.out.println(r.choice("tone").choice()));

The synchronous methods block on the same machinery and park cleanly on virtual threads.

Errors

All exceptions are unchecked and extend TypeSafeException.

import io.github.kgonia.typesafe.errors.*;

try {
    client.systemOne(state, questions);
} catch (RateLimitException e) {
    // 429 after all retries; e.retryAfter() holds the server's requested delay
} catch (TypeSafeApiException e) {
    System.err.println(e.statusCode() + " " + e.requestId().orElse("-") + ": " + e.getMessage());
    Object body = e.body(); // parsed JSON, text, or null
} catch (TypeSafeTimeoutException e) {
    // an attempt exceeded its timeout, after all retries
} catch (TypeSafeConnectionException e) {
    // the request never got a response, after all retries
}
StatusException
400BadRequestException
401AuthenticationException
403PermissionDeniedException
404NotFoundException
422UnprocessableEntityException
429RateLimitException
5xxInternalServerException
otherTypeSafeApiException

A 2xx response whose body does not match the expected shape raises ResponseValidationException with the offending fieldPath().

Retries

By default the SDK retries HTTP 408, 429, and 5xx responses, connection errors, and timeouts up to two times, waiting 500ms, then 1s, capped at 5s, with 25% jitter. It honors Retry-After and retry-after-ms headers up to 60 seconds. Retried attempts carry an X-TypeSafe-Retry-Count header.

RetryPolicy policy = RetryPolicy.defaults().toBuilder()
    .maxRetries(4)
    .maxElapsed(Duration.ofSeconds(45))   // total budget per call, including delays
    .build();

RetryPolicy.none() disables retries.

Models

for (ModelMetadata model : client.models().list().models()) {
    System.out.println(model.name() + "  " + model.releaseDate() + "  " + model.description());
}

The model field on a response reports the versioned ID that answered, so you can log which model produced each result even when you request an alias.

Logging

The SDK logs through System.getLogger("io.github.kgonia.typesafe"), which reaches java.util.logging by default and SLF4J or Log4j when they provide a System.LoggerFinder. info logs one line per attempt; debug adds headers and bodies. Credential headers are redacted from log output. Request and response bodies are not.

Set TYPESAFE_LOG_LEVEL, call builder().logLevel(LogLevel.DEBUG), or pass your own System.Logger with builder().logger(...).

Forward compatibility

Use SystemOneRequest.Builder.extraBodyField for request fields this SDK version predates, and Question.raw for question fields it does not model:

Question weighted = Question.raw(Map.of("type", "noul", "instructions", "About billing?", "weight", 2));

Answer types the SDK does not recognize arrive as Answer.Unknown with their full JSON. Unknown fields on known responses are ignored, and the raw body is always available through response.metadata().rawBody().

Example

examples/Demo.java is a complete program. Build the SDK and run it as a single-file program:

mvn -q package -DskipTests
TYPESAFE_API_KEY=... java -cp target/classes examples/Demo.java

Repository layout

typesafe-sdk-java/
├── pom.xml          parent: shared plugin versions and Java 21 floor
├── typesafe-sdk/    the library; the only published artifact (io.github.kgonia:typesafe-sdk)
├── examples/        runnable samples, built with the SDK, never published
└── test-jpms/       a modular consumer that verifies the SDK on the module path

Inside the library, the root package holds only the entry point. Each API feature has its own package, HTTP-level settings are shared in one place, and everything under internal is implementation that the module does not export.

PackageContents
io.github.kgonia.typesafeTypeSafeClient (interface and builder), LogLevel. Start here.
io.github.kgonia.typesafe.systemoneQuestion, Answer, SystemOneRequest, SystemOneResponse, Usage
io.github.kgonia.typesafe.modelsModels (interface), ModelMetadata, ListModelsResponse
io.github.kgonia.typesafe.httpRequestOptions, RetryPolicy, ResponseMetadata
io.github.kgonia.typesafe.errorsTypeSafeException and its subclasses
io.github.kgonia.typesafe.internalClient and resource implementations, transport, codec. Not exported.

TypeSafeClient and Models are interfaces, so code that depends on them can be tested with a mock. Question and answer kinds are nested in their sealed parents: Question.Noul, Question.Choice, Question.Score, Question.Raw, and Answer.Noul, Answer.Choice, Answer.Score, Answer.Unknown.

Modular applications

The SDK is a named module, io.github.kgonia.typesafe. Add requires io.github.kgonia.typesafe; to your module-info.java. If you pass records as state, the SDK reads their components reflectively, so open their package to it:

opens com.example.tickets to io.github.kgonia.typesafe;

Without that, the SDK raises a TypeSafeException naming the package to open. Passing a Map needs no opens.

Example

examples/ holds a complete program. Install the SDK locally once, then run it:

mvn -q install -DskipTests
TYPESAFE_API_KEY=... mvn -q -pl examples exec:java

Development

mvn verify                       # unit tests, module-path tests, Javadoc, sources jar, zero-dependency check
TYPESAFE_API_KEY=... mvn verify  # also runs the live integration test

Releasing

One-time setup: create an account at central.sonatype.com, verify the io.github.kgonia namespace, generate a user token, and put it in ~/.m2/settings.xml as <server><id>central</id>...</server>. Generate a GPG key and publish it to a keyserver.

mvn -P release deploy

This signs and uploads the library and its parent pom, then waits for you to press Publish in the portal. Bump project.build.outputTimestamp in the parent pom for each release so builds stay reproducible.

License

MIT