Unofficial community SDK. This project is not affiliated with, endorsed by, or sponsored by TypeSafe AI. "TypeSafe" and all related names, marks, and logos are trademarks of TypeSafe AI - used here solely to identify the API this client targets. Use at your own risk; the API may change without notice.
Java client for TypeSafe AI's System One API
(POST /v1/systemone).
- Java 21+, Maven build
java.net.http.HttpClienttransport - no extra HTTP dependency- Jackson for JSON (only runtime dependency)
- Sync + async (
CompletableFuture) APIs - Automatic retries with exponential backoff + jitter on 429/5xx, honoring
Retry-After - Sealed
Question/Answerhierarchies; unknown answer types degrade to a forward-compatibleUnknownAnswer
import dev.dosa.typesafe.TypeSafeClient;
import dev.dosa.typesafe.model.*;
TypeSafeClient client = TypeSafeClient.builder()
.apiKey(System.getenv("TYPESAFE_API_KEY"))
.defaultModel("jev-latest") // used when a request sets no model
.requestTimeout(Duration.ofSeconds(10))
.build();
SystemOneRequest request = SystemOneRequest.builder()
.state(Map.of("ticket", "I want a refund now!"))
.question("urgent", Question.noul("Does this convey urgency?")
.whenTrue("Explicitly time-sensitive")
.whenFalse("No time pressure"))
.question("dept", Question.choice("Which team?")
.option("billing", "Payment issues")
.option("technical", "Bugs"))
.question("frustration", Question.score("How frustrated?")
.level("Calm")
.level("Frustrated")
.level("Very angry"))
.build();
SystemOneResponse resp = client.systemOne(request); // sync
// CompletableFuture<SystemOneResponse> f = client.systemOneAsync(request); // async
resp.noul("urgent").ifPresent(p -> System.out.println("urgency: " + p));
resp.choice("dept").ifPresent(c -> System.out.println("team: " + c.choice()));
resp.score("frustration").ifPresent(s -> System.out.println("score: " + s.score()));
One static factory + fluent builder per question type:
Question.noul("Does this convey urgency?")
.whenTrue("Explicitly time-sensitive")
.whenFalse("No time pressure");
Question.choice("Which team?")
.option("billing", "Payment issues")
.option("technical", "Bugs");
Question.score("How frustrated?")
.level("Calm")
.level("Frustrated")
.level("Very angry");
Validation runs client-side in SystemOneRequest.builder().build(), before any
network call, and throws InvalidRequestException: empty questions map, blank
question names, choice questions with fewer than 2 or more than 255 options,
score questions with fewer than 2 levels, blank instructions, and a state
that is not a JSON string, object, or array.
A model is required by the API: set it per-request via .model("jev-latest")
or once on the client via .defaultModel(...). If neither is set, the client
throws InvalidRequestException before sending. Discover valid names with:
List<ModelInfo> models = client.models(); // GET /v1/models
models.forEach(m -> System.out.println(m.name() + " - " + m.description()));
Optional<Double> n = resp.noul("urgent"); // NoulAnswer probability
Optional<ChoiceAnswer> c = resp.choice("dept");
Optional<ScoreAnswer> s = resp.score("frustration");
Optional<Answer> a = resp.answer("anything"); // untyped lookup
Stream<Entry<String, NoulAnswer>> allNouls = resp.nouls();
Stream<Entry<String, ChoiceAnswer>> allChoices = resp.choices();
Stream<Entry<String, ScoreAnswer>> allScores = resp.scores();
Response metadata (kept separate from answer data):
resp.requestId(); // Optional<String> - x-typesafe-request-id header
resp.httpStatus(); // final HTTP status after retries
resp.attempts(); // number of HTTP attempts made
resp.usage(); // Optional<Usage> - input/output token counts
All exceptions extend the unchecked dev.dosa.typesafe.exception.TypeSafeException:
| Exception | When |
|---|---|
InvalidRequestException |
Client-side validation failure (thrown before any network) |
AuthenticationException |
HTTP 401/403 - never retried |
RateLimitException |
HTTP 429 after all retry attempts exhausted |
ApiException |
Other error statuses / unparseable body (status + body + request id) |
NetworkException |
Transport-level IOException (timeouts, DNS, refused) |
The API key is only ever sent in the Authorization header and never appears
in exception messages.
TypeSafeClient.builder()
.apiKey(...) // or TYPESAFE_API_KEY env var
.baseUrl(...) // or TYPESAFE_BASE_URL env var, then https://api.typesafe.ai
.defaultModel(...) // fallback model; one is required somewhere
.requestTimeout(Duration) // per attempt, default 30s
.connectTimeout(Duration) // default 10s
.maxAttempts(3) // retries incl. initial attempt
.initialRetryDelay(Duration) // backoff base, default 200ms
.maxRetryDelay(Duration) // backoff cap, default 10s
.httpClient(HttpClient) // custom transport
.build();
mvn test # compiles and runs the test suite (JDK-local stub HTTP server)
mvn package # builds the jar
With TYPESAFE_API_KEY set in the environment, mvn test also runs the live
integration tests in TypeSafeClientLiveTest against the real API (they are
skipped otherwise).