TypeSafe AI Kotlin SDK

September 19, 2026 ยท View on GitHub

Kotlin SDK for TypeSafe AI. Ask typed questions about a piece of text and get typed answers back.

This is a Kotlin port of @typesafe-ai/sdk 0.6.0. See NOTICE for the full attribution and the list of deliberate divergences.

Install

The library is built on demand by JitPack. Add the repository first:

// settings.gradle.kts
dependencyResolutionManagement {
    repositories {
        google()
        mavenCentral()
        // JitPack serves this project under com.github.ufec.typesafe-sdk-kotlin,
        // which is a *subgroup* of com.github.ufec. An exact includeGroup filter
        // on com.github.ufec would exclude the group that actually carries the
        // Android variant, and the dependency would fail to resolve.
        maven("https://jitpack.io") {
            content { includeGroupByRegex("com\\.github\\.ufec.*") }
        }
    }
}

Then depend on a tag:

// build.gradle.kts
dependencies {
    implementation("com.github.ufec.typesafe-sdk-kotlin:typesafe-sdk-kotlin:v0.2.0")
}

JitPack names artifacts com.github.<user>.<repo>:<module>, so the group repeats the repository name. The Android variant (com.github.ufec.typesafe-sdk-kotlin:typesafe-sdk-kotlin-android) comes along transitively through Gradle module metadata, so there is nothing else to declare. A -jvm variant is published too, and a plain JVM project resolves it from the same coordinate.

Targets Android (minSdk 29) and the JVM. Both need a JDK 17 toolchain.

Quickstart

val client = TypeSafeClient(TypeSafeConfig(apiKey = "ts_..."))

val category = QuestionId(
    name = "category",
    question = choice(
        instructions = "Which category does this message belong to?",
        criteria = mapOf(
            "promo" to "Marketing content",
            "otp" to "A one-time passcode",
            "normal" to "Something the user expects",
        ),
    ),
)

val result = client.systemOne("[SALE] Limited-time offer, 50% off") { ask(category) }

val answer: ChoiceAnswer = result.answer(category)
println(answer.choice)       // promo
println(answer.confidence)   // 0.97

Answer types are derived from the questions, so result.answer(category) comes back as a ChoiceAnswer without a cast. The client is AutoCloseable; call close() when you are done with it.

Example

There is a runnable demo in examples/, the counterpart of the upstream JavaScript SDK's npm run demo:

export TYPESAFE_API_KEY=ts_...
./gradlew :examples:run

It lists the available models, then classifies a support ticket with one noul, one choice, and two score questions. Letting the exception escape would bury the one useful line under a Gradle failure block, so a bad key prints API error 401 (request <id>): ... and the build still succeeds.

The demo is compiled by ./gradlew build, so it cannot silently rot.

Question types

BuilderAnswerShape
noul(instructions)NoulAnswerA probability in 0..1. No confidence, because the value already is one.
choice(instructions, criteria)ChoiceAnswerThe selected key, its confidence, and the full distribution.
score(instructions, criteria)ScoreAnswerA weighted score, the bucket legend, and the distribution.

choice reports an error at request time if criteria is empty, and score requires at least two buckets.

Configuration

val client = TypeSafeClient(
    TypeSafeConfig(
        apiKey = "ts_...",
        baseUrl = DEFAULT_BASE_URL,
        defaultModel = "jev-latest",
        timeoutMs = 10_000,
        retry = RetryPolicy(maxRetries = 2),
        proxy = ProxySpec(ProxyKind.HTTP, "10.0.0.1", 8080, "user", "pass"),
        logLevel = LogLevel.INFO,
        logger = AndroidLogger(),
    ),
)

Unlike upstream, this SDK never reads the ambient environment. There is no process.env equivalent on Android, so the API key must be passed explicitly.

Data and credentials

This is a client for a hosted API, so whatever you pass as state is sent to TypeSafe. That is the point of the library, but it is worth stating plainly: an application that forwards message bodies, documents, or user input to Jev is sending that content to a third party.

Nothing else about the host is collected. Each request carries the API key, a User-Agent naming this SDK and its version, a runtime descriptor (kotlin/unknown unless the caller sets something more useful, such as android/15), and, on a retry, the retry count.

The API key lives in memory for the lifetime of the client and travels only in the Authorization header. This library never writes it to disk: persisting it is the host application's decision, which is why examples/ reads it from the TYPESAFE_API_KEY environment variable instead.

Log output masks credential headers before they reach the logger. authorization, proxy-authorization, and x-api-key keep their scheme and, for secrets longer than eight characters, their last four characters; cookie and set-cookie are masked entirely. A proxy password is never logged.

Errors

Every failure derives from TypeSafeException:

ExceptionRaised when
BadRequestException (400)The request was malformed.
AuthenticationException (401)The API key was rejected.
PermissionDeniedException (403)The account may not make this request.
NotFoundException (404)The resource does not exist.
UnprocessableEntityException (422)The request body failed validation.
RateLimitException (429)Rate limited; carries retryAfterMs.
InternalServerException (5xx)The server failed to handle the request.
APIConnectionExceptionDNS, TLS, or connection failure.
APITimeoutExceptionNo complete response within timeoutMs.

Retrying is on by default: 408, 429, and every 5xx, up to maxRetries attempts, with exponential backoff and jitter. A server-supplied Retry-After or retry-after-ms wins when it is within maxRetryAfterMs.

Coroutine cancellation is not wrapped into any of these. CancellationException propagates untouched so that structured concurrency keeps working.

Documentation

  • docs/usage.md -- a longer walkthrough of questions, answers, retries, proxies, and logging
  • docs/changelog.md -- release notes
  • NOTICE -- upstream attribution and the divergences from the JavaScript SDK
  • TypeSafe docs -- what TypeSafe itself can do

Development

./gradlew build          # compiles everything, runs ktlint, runs the tests
./gradlew ktlintCheck    # style only
./gradlew ktlintFormat   # style only, rewriting files

Style lives in .editorconfig, which is ktlint's own configuration file -- the counterpart of the upstream JavaScript SDK's biome.json. The base is ktlint's ktlint_official style with its line-breaking rules turned off; the file records which ones and why.

Releasing

Releases are tags. JitPack builds whatever tag is asked for, so there is nothing to upload; what a release adds is a GitHub release whose notes come from the changelog.

Immediately before tagging, draft the entry from the commits since the last release:

git-cliff --unreleased --tag v0.3.0 --prepend docs/changelog.md

git-cliff is configured by cliff.toml. Read the entry it produced and edit it: the commits know what changed, not why it mattered.

Then bump version in build.gradle.kts, commit, and push the tag. The release workflow builds the tagged commit, checks that the tag agrees with version and that the changelog has a matching entry, and creates the GitHub release from it.

The changelog is checked on every push too, by the same script, so a malformed entry is caught when it is written rather than on release day.

License

MIT. This port retains the upstream TypeSafe copyright as required by the MIT license; see LICENSE and NOTICE.