Kotlin SDK
Build local AI apps with Kotlin coroutines and self-contained Android runtimes.
Overview
The Kotlin SDK (io.tether:qvac-sdk-kotlin) is the Kotlin client for QVAC. It
drives the same worker contract as the JS/TS and Python clients, and provides
Kotlin data classes, Flow streams, structured completion runs, and an
isolated Android service host.
Requirements
- Android:
arm64-v8a, API 29 or newer. - Desktop JVM: experimental. The client resolves and starts a local
@qvac/sdkworker. - A Kotlin 2.3 or newer compiler. The artifacts depend on
bare-rpc-kotlin, which is compiled with Kotlin 2.4.
Kotlin/Native targets are not published.
Install
The Kotlin SDK is not published to a public Maven registry. Each SDK release on GitHub carries it as Maven repository archives that you download and point Gradle at. There is one archive per Android runtime profile:
| Archive | Dependency | Capabilities |
|---|---|---|
qvac-sdk-android-0.23.0-maven.zip | io.tether:qvac-sdk-android | all current SDK addons/capabilities |
qvac-sdk-android-assistant-0.23.0-maven.zip | io.tether:qvac-sdk-android-assistant | chat and transcription |
qvac-sdk-android-llm-0.23.0-maven.zip | io.tether:qvac-sdk-android-llm | completion and embeddings |
qvac-sdk-android-speech-0.23.0-maven.zip | io.tether:qvac-sdk-android-speech | Whisper, Parakeet and BCI transcription, translation, and TTS |
qvac-sdk-android-vision-0.23.0-maven.zip | io.tether:qvac-sdk-android-vision | multimodal completion, OCR, and classification |
qvac-sdk-android-media-0.23.0-maven.zip | io.tether:qvac-sdk-android-media | audio, image, video, world generation, and upscaling |
qvac-sdk-android-robotics-0.23.0-maven.zip | io.tether:qvac-sdk-android-robotics | VLA inference |
For the experimental desktop JVM client, see the package README.
Download the archive of the profile your app needs, and the release's
checksums, from the
sdk-v0.23.0 release.
For example, the LLM profile:
curl -fLO https://github.com/tetherto/qvac/releases/download/sdk-v0.23.0/qvac-sdk-android-llm-0.23.0-maven.zip
curl -fLO https://github.com/tetherto/qvac/releases/download/sdk-v0.23.0/SHA256SUMS
sha256sum --check --ignore-missing SHA256SUMS
# macOS: shasum -a 256 --check --ignore-missing SHA256SUMSExtract it. The archive is a Maven repository: it holds the runtime and the Kotlin client modules it depends on.
unzip qvac-sdk-android-llm-0.23.0-maven.zip -d qvac-maven-repositoryDeclare the extracted directory as a Maven repository, next to JitPack, which
serves the bare-rpc-kotlin dependency the archive does not include:
// settings.gradle.kts
dependencyResolutionManagement {
repositories {
google()
mavenCentral()
exclusiveContent {
forRepository { maven("https://jitpack.io") } // bare-rpc-kotlin
filter { includeGroupByRegex("com\\.github\\.holepunchto\\..*") }
}
maven { url = uri("/absolute/path/to/qvac-maven-repository") }
}
}Add the dependency of the archive you downloaded:
// app/build.gradle.kts
dependencies {
implementation("io.tether:qvac-sdk-android-llm:0.23.0")
}A project on AGP 9 compiles with the Kotlin Gradle plugin on its classpath, and bundles Kotlin 2.2 when none is declared. Add the plugin without applying it:
// app/build.gradle.kts
plugins {
id("com.android.application")
id("org.jetbrains.kotlin.android") version "2.4.20" apply false
}All artifacts use the io.tether.qvac.sdk namespace, so the Kotlin API is the
same whichever profile you choose. Each profile embeds a capability manifest
that is checked before each request; calling a capability the profile does not
contain throws UnsupportedCapabilityException. Inspect it with
client.runtimeProfile.
To build the artifacts yourself instead, see the package README.
Connect
val transport = AndroidServiceTransport.connect(applicationContext)
val client = QvacClient(transport)
try {
println(client.heartbeat())
} finally {
client.close()
}The service runs in the app's private :qvac_worker process. Desktop JVM uses
JvmBareRpcTransport.connectResolved(), which starts the resolved @qvac/sdk
worker over a loopback channel. Kotlin/Native targets are not published until
they have tested worker hosts.
Completion and multimodal input
val run = client.completion.run(
modelId = modelId,
history = listOf(
QvacMessage.system("Be concise."),
QvacMessage.user(
"What is in this image?",
attachments = listOf(QvacAttachment("/data/user/0/app/files/photo.jpg")),
),
),
)
run.tokens.collect(::print)
val answer = run.final.await()
println("${answer.stopReason}: ${answer.stats?.tokensPerSecond} tok/s")Generation defaults to predict = -1, so chat does not stop at an arbitrary
token count. A run exposes events, tokens, toolCallEvents, accumulated
text, thinking text, tool calls, statistics, stop reason, and cancel().
Tool handlers
val weather = QvacTool(
name = "weather",
description = "Read local weather",
parameters = mapOf(
"city" to QvacToolParameter(QvacToolParameterType.STRING),
),
required = setOf("city"),
handler = { args -> JsonPrimitive("Sunny in ${args.getValue("city")}") },
)
val run = client.completion.orchestrate(
modelId = modelId,
history = listOf(QvacMessage.user("Do I need an umbrella in Rome?")),
tools = listOf(weather),
)
println(run.text())Capability examples
val transcript = client.speech.transcribe(
modelId = whisperModelId,
audio = QvacDataInput.FilePath("/data/user/0/app/files/sample.wav"),
)
val speech = client.speech.synthesize(ttsModelId, "Hello from QVAC")
val page = client.vision.ocr(ocrModelId, QvacDataInput.FilePath("/data/user/0/app/files/page.png"))
val vector = client.embeddings.embed(embeddingModelId, "private local AI")
val translation = client.translation.run(nmtModelId, "hello", modelType = "nmt")
val imageRun = client.media.diffusion(diffusionModelId, "a small red robot")
val imageBase64 = imageRun.final.await().data.first()
val classes = client.vision.classify(classifierModelId, imageBase64)
val hparams = client.vla.hparams(vlaModelId).hparams
val actions = client.vla.run(
modelId = vlaModelId,
images = listOf(frontCamera, wristCamera),
imgWidth = hparams.visionImageSize,
imgHeight = hparams.visionImageSize,
state = qvacVlaPadState(robotState, hparams.maxStateDim),
tokens = instructionTokens,
mask = instructionMask,
).actionsclient.media also wraps audio generation, video/world generation, and
upscaling. client.bci handles buffered and duplex neural transcription;
client.vla handles VLA hparams, inference, and embodiment switching.
The Android Feature Lab sample exercises completion, multimodal vision, OCR,
transcription, TTS, embeddings, translation, and classification with small
models suitable for a Pixel 8a.
API surface
| Kotlin entry point | Purpose |
|---|---|
client.models | registry, download, load, unload, and progress |
client.completion | completion runs and tool orchestration |
client.speech | transcription and TTS results |
client.vision | OCR and classification results |
client.translation | streaming translation run and cancellation |
client.embeddings | single and batch embeddings |
client.media | audio, diffusion, video, and upscale runs |
client.bci | buffered and duplex BCI transcription |
client.vla | VLA preprocessing, hparams, inference, and embodiment switching |
| generated client methods | typed operation for every contract method |
client.raw | JSON call, stream, and duplex escape hatch |
Generated request and response types live in io.tether.qvac.sdk.generated.schema;
io.tether.qvac.sdk.generated contains the model constants, error codes, and call
shapes in the shared contract.