An idiomatic Java port of typesafe-ai/typesafe-sdk-js
v0.6.0, by way of the .NET community port. The library targets Java 21, builds with Maven, and has
no runtime dependencies: the HTTP client is java.net.http, and JSON is handled by a small
parser and writer inside the SDK.
Important
This is an unofficial community port and is not supported by TypeSafe. Use it at your own risk.
import ai.typesafe.*;
import ai.typesafe.json.Json;
try (TypeSafeClient client = new TypeSafeClient(
TypeSafeClientOptions.builder().apiKey("your-api-key").build())) {
SystemOneResult result = client.systemOne(SystemOneRequest.builder()
.state(Json.object().put("text", "Evaluate this statement.").build())
.question("safe", Questions.noul(
"Assess safety.",
NoulCriteria.of("The statement is safe.", "The statement is unsafe.")))
.question("topic", Questions.choiceOf(
"Choose a topic.",
"science", "Scientific content",
"other", null))
.question("quality", Questions.score(
"Rate quality.", "poor", "good", "excellent"))
.build());
double safeProbability = ((NoulAnswer) result.answers().get("safe")).noul();
List<ModelCard> models = client.models();
}Answers are a sealed hierarchy, so a switch over them is exhaustive and needs no default:
String describe(Answer answer) {
return switch (answer) {
case NoulAnswer noul -> "noul " + noul.noul();
case ChoiceAnswer choice -> choice.choice() + " @ " + choice.confidence();
case ScoreAnswer score -> score.score() + " @ " + score.confidence();
};
}State, instructions, and criterion descriptions are JsonValues: a JSON string, object, array, or
null. A number or boolean is rejected, as upstream. Build them with Json.of, Json.object(),
Json.array(...), or Json.from(...) for a plain Map, List, or array:
JsonValue state = Json.from(Map.of("subject", "Checkout fails", "tier", "enterprise"));Question maps cannot be empty, choice criteria cannot be empty, and score criteria need at least two
ordered entries. Noul criteria are optional and use the JSON keys true and false.
SystemOneRequest.Builder.additionalProperty forwards extra top-level JSON fields.
Every call comes in both forms:
SystemOneResult result = client.systemOne(request);
CompletableFuture<SystemOneResult> pending = client.systemOneAsync(request);Cancelling the future, or interrupting a thread blocked on the synchronous form, raises
ApiUserAbortException rather than CancellationException, so callers see one exception hierarchy.
In-flight HTTP is cancelled with it.
Each setting resolves to the explicit value, then a non-blank environment variable, then the default:
| Option | Environment | Default |
|---|---|---|
| API key | TYPESAFE_API_KEY |
required |
| Base URL | TYPESAFE_BASE_URL |
https://api.typesafe.ai |
| Model | TYPESAFE_DEFAULT_MODEL |
jev-latest |
| Log level | TYPESAFE_LOG_LEVEL (trace, debug, info, warn, error, off) |
WARN |
| Timeout | — | 10 seconds |
A blank explicit value is an error rather than a fall through to the environment.
The retry defaults are two retries, 500 ms initial exponential backoff, a 5 second delay cap, 0.25
subtractive jitter, statuses 408/429/500–599, Retry-After support with a 60 second cap, and retries
for connection failures and timeouts. A syntactically valid, non-negative retry-after-ms takes
precedence over Retry-After; an invalid value falls through to Retry-After, while a valid value
above the cap falls back to backoff.
TypeSafeClientOptions.retry(...) partially overrides the defaults. RequestOptions.retry(...) then
partially overrides the client policy for one call, including every retry setting. Retry requests
carry a 1-based X-TypeSafe-Retry-Count.
List<ModelCard> models = client.models(RequestOptions.builder()
.timeout(Duration.ofSeconds(20))
.retry(RetryOptions.builder().maxRetries(1).build())
.header("X-Trace-Id", traceId)
.build());Resolved settings are readable from the client through baseUrl(), defaultModel(), logLevel(),
timeout(), retry(), and defaultHeaders(). Returned headers and retry status sets are
unmodifiable copies taken at construction.
Authentication, Accept, content type, User-Agent, X-TypeSafe-SDK, runtime, and retry headers are
protected from option overrides. User-Agent and X-TypeSafe-SDK are typesafe-sdk/0.6.0.
An injected HttpClient is never closed by TypeSafeClient; one it creates itself is closed with it.
systemOneWithResponse and modelsWithResponse return an ApiResponse<T> holding the parsed data,
the status code, the response headers, the raw body text, and requestId() from
x-typesafe-request-id. Nothing needs closing: the body has already been read.
Provide a TypeSafeLogger for logging integration. Without one, the SDK writes to System.err,
filtered by the log level. Credential logging keeps the authentication scheme and, only for secrets
longer than eight characters, the final four characters. Cookie and Set-Cookie values are fully
redacted.
API errors preserve the parsed JSON, the raw non-JSON text, or a null body. Validation message
locations omit the leading body segment. Unknown answer types and malformed response shapes fail
with TypeSafeException.
| JavaScript SDK | .NET SDK | Java SDK |
|---|---|---|
TypeSafe |
TypeSafeClient |
TypeSafeClient |
client.systemOne({ state, questions }) |
client.SystemOneAsync(...) |
client.systemOne(...) / systemOneAsync(...) |
client.models.list() |
client.ModelsAsync() |
client.models() / modelsAsync() |
APIPromise.withResponse() |
*WithResponseAsync() |
*WithResponse / *WithResponseAsync |
APIError |
ApiError |
ApiException |
BadRequestError |
BadRequestError |
BadRequestException |
AuthenticationError |
AuthenticationError |
AuthenticationException |
PermissionDeniedError |
PermissionDeniedError |
PermissionDeniedException |
NotFoundError |
NotFoundError |
NotFoundException |
UnprocessableEntityError |
UnprocessableEntityError |
UnprocessableEntityException |
RateLimitError |
RateLimitError |
RateLimitException |
InternalServerError |
InternalServerError |
InternalServerException |
APIConnectionError |
ApiConnectionError |
ApiConnectionException |
APITimeoutError |
ApiTimeoutError |
ApiTimeoutException |
APIUserAbortError |
ApiUserAbortError |
ApiUserAbortException |
Every SDK exception is unchecked and descends from TypeSafeException, so a call site can catch one
type or none at all.
- Timeouts. .NET can inherit a timeout from an injected
HttpClient.java.net.http.HttpClienthas no such setting, so the timeout always comes fromTypeSafeClientOptionsorRequestOptionsand is applied per request. It covers reading the response body, not only the headers. - Cancellation. There is no
CancellationTokenparameter. Cancel the returnedCompletableFuture, or interrupt the thread blocked on a synchronous call. - Disposal.
ApiResponse<T>is a plain record and is not closeable, because the body is already buffered. - JSON. Instead of
object?plus reflection, the API takesJsonValue, which makes the "string, object, array, or null" rule a compile-time matter wherever possible and keeps the jar dependency free.
| Path | Contents |
|---|---|
typesafe-sdk |
The library and its tests |
typesafe-sample |
A runnable console example |
typesafe-sample lists the available models, evaluates a support ticket with one noul, one
choice, and one score question, prints the typed answers, request id, and token usage, and maps
every SDK error to a distinct exit code.
export TYPESAFE_API_KEY="sk-..."
mvn -q -pl typesafe-sample -am exec:javaPoint the sample at a different host with TYPESAFE_BASE_URL, or change the model with
TYPESAFE_DEFAULT_MODEL. Press Ctrl+C to cancel an in-flight request; the sample reports that as
ApiUserAbortException and exits with 130.
mvn verify
mvn -pl typesafe-sdk test
mvn -pl typesafe-sdk packageThe unit tests need no network: they run against a stub HttpClient.
LiveApiTest calls the real API and is skipped unless TYPESAFE_API_KEY is set, so mvn verify
is safe without a key and exercises the service when one is present:
export TYPESAFE_API_KEY="sk-..."
mvn -pl typesafe-sdk test -Dtest=LiveApiTestIt lists the models and asks one noul, one choice, and one score question, which is two API
calls in total.