cd /news/ai-tools/show-hn-typesafe-java-sdk-unofficial · home topics ai-tools article
[ARTICLE · art-134287] src=github.com ↗ pub= topic=ai-tools verified=true sentiment=· neutral

Show HN: Typesafe Java SDK (Unofficial)

A developer has released an unofficial, community-built Java 21+ SDK for TypeSafe AI's System One API (POST /v1/systemone), offering sync and async calls, automatic retries with exponential backoff and jitter on 429/5xx responses, and client-side validation via InvalidRequestException. The Maven-built client uses java.net.http.HttpClient for transport with Jackson as its only runtime dependency, and requires a model to be set per-request or via defaultModel. The project is not affiliated with, endorsed by, or sponsored by TypeSafe AI, and its author warns the API may change without notice.

read3 min views1 publishedSep 19, 2026
Show HN: Typesafe Java SDK (Unofficial)
Image: Michielbdejong (auto-discovered)

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.HttpClient transport - 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 /Answer hierarchies; 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).

── more in #ai-tools 4 stories · sorted by recency
── more on @typesafe ai 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
Live at https://your-agent.zahid.host
Get free account → Pricing
from €0/mo · no card required
LIVE [news/show-hn-typesafe-jav…] indexed:0 read:3min 2026-09-19 ·