New: QVAC SDK now supports Kotlin. Build local AI into your JVM and Android apps.
QVAC Logo
SDKKotlin SDK
v0.23, the current release

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/sdk worker.
  • 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:

ArchiveDependencyCapabilities
qvac-sdk-android-0.23.0-maven.zipio.tether:qvac-sdk-androidall current SDK addons/capabilities
qvac-sdk-android-assistant-0.23.0-maven.zipio.tether:qvac-sdk-android-assistantchat and transcription
qvac-sdk-android-llm-0.23.0-maven.zipio.tether:qvac-sdk-android-llmcompletion and embeddings
qvac-sdk-android-speech-0.23.0-maven.zipio.tether:qvac-sdk-android-speechWhisper, Parakeet and BCI transcription, translation, and TTS
qvac-sdk-android-vision-0.23.0-maven.zipio.tether:qvac-sdk-android-visionmultimodal completion, OCR, and classification
qvac-sdk-android-media-0.23.0-maven.zipio.tether:qvac-sdk-android-mediaaudio, image, video, world generation, and upscaling
qvac-sdk-android-robotics-0.23.0-maven.zipio.tether:qvac-sdk-android-roboticsVLA 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 SHA256SUMS

Extract 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-repository

Declare 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,
).actions

client.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 pointPurpose
client.modelsregistry, download, load, unload, and progress
client.completioncompletion runs and tool orchestration
client.speechtranscription and TTS results
client.visionOCR and classification results
client.translationstreaming translation run and cancellation
client.embeddingssingle and batch embeddings
client.mediaaudio, diffusion, video, and upscale runs
client.bcibuffered and duplex BCI transcription
client.vlaVLA preprocessing, hparams, inference, and embodiment switching
generated client methodstyped operation for every contract method
client.rawJSON 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.

On this page

Ask anything about QVAC.