Skip to content

Installation

Requirements

  • JDK 17 or newer to run. jev4k is compiled to Java 17 bytecode, so it embeds in applications on 17, 21, 25, or anything later. Building jev4k itself uses JDK 25; Gradle's toolchain support downloads it if it's missing.
  • Kotlin 2.4 or later.
  • A TypeSafe API key.

jev4k is built on the Ktor client (CIO engine), kotlinx.serialization, and kotlinx.coroutines. Ktor core, kotlinx.serialization-json and kotlinx.coroutines are exposed as api dependencies, because their types (JsonElement, HttpClientEngine, suspend functions) appear in jev4k's public API.

Embedding in an application

jev4k is a library meant to be embedded in an application, so it keeps out of the host's way.

What it puts on your classpath.

Four compile dependencies (ktor-client-core, kotlinx-serialization-json, kotlinx-coroutines-core, kotlin-stdlib) and three runtime ones (ktor-client-cio, ktor-client-content-negotiation, ktor-serialization-kotlinx-json). Nothing else: no test framework, no logging backend.

Logging

jev4k never logs. It writes nothing to stdout or stderr, installs no Ktor Logging plugin, and ships no SLF4J binding, so it can't interfere with your logging setup. slf4j-api reaches the classpath through Ktor, not jev4k; supply your own binding if you want Ktor's own output.

Your own engine

Pass one through engine and jev4k uses it instead of CIO. Closing a JevClient never closes an engine you supplied, so several clients can share one. If you do supply an engine, CIO can be dropped:

dependencies {
    implementation("com.pambrose:jev4k:0.1.0") {
        // Only if you pass your own engine: drops ktor-client-cio and ktor-network-tls. ktor-network
        // itself stays, because ktor-client-core depends on it through ktor-http-cio.
        exclude(group = "io.ktor", module = "ktor-client-cio-jvm")
    }
    implementation("io.ktor:ktor-client-okhttp:3.6.0")
}

From Java

jev4k is a Kotlin library, but the inline builder DSL works from Java through jev.getBlocking():

Calling jev4k from Java
JevClient jev = new JevClient(builder -> {
    builder.setApiKey(System.getenv("TYPESAFE_API_KEY"));
    return Unit.INSTANCE;
});

JevResult r = jev.getBlocking().query("The payout failed again and I need this fixed today.", null, qb -> {
    qb.noul("urgent", "Does this message convey urgency?");
    return Unit.INSTANCE;
});

double urgency = r.noul("urgent").getNoul();

Two Kotlin features don't cross to Java: property delegates, which a typed JevQuery is built from, and inline reified functions, which the Kotlin compiler emits as synthetic members that javac can't resolve. So four things are out of reach from Java:

  • Typed JevQuery objects can't be declared. One declared in Kotlin can still be passed to ask.
  • @Serializable states. A state must be a String or a JsonElement; the reified query, ask and jsonEntry overloads are hidden rather than compiling into a runtime failure.
  • Enum Choices through the DSL. QueryBuilder.choice<E>() is reified and enumChoiceRef is internal, so there's no route to one. Build a ChoiceQuestion with the option keys you want and add it with QueryBuilder.question(id, question) instead.
  • JevResult.enumChoice<E>(id) is reified too. Read that answer with result.choice(id), which is keyed by option string.

Everything else is callable: evaluate, models, the inline noul, choice and score builders, the other result accessors, and enums implementing JevOption.

Module name

The jar declares Automatic-Module-Name: com.pambrose.jev4k for JPMS builds.

Java version

The class files are Java 17 (org.gradle.jvm.version = 17 in the published metadata), and the compiler is held to the Java 17 API, so nothing newer can slip in.

Threads

A JevClient is immutable once built and safe to share across coroutines. jev.blocking wraps the suspend calls in runBlocking, so call it from ordinary threads, never from inside a coroutine.

Adding the dependency

jev4k is published to Maven Central as com.pambrose:jev4k.

// build.gradle.kts
dependencies {
    implementation("com.pambrose:jev4k:0.1.0")
}
<dependency>
    <groupId>com.pambrose</groupId>
    <artifactId>jev4k</artifactId>
    <version>0.1.0</version>
</dependency>

That single dependency brings the Ktor client, kotlinx.serialization and kotlinx.coroutines with it; see what it puts on your classpath for the full set, and your own engine if you'd rather not ship CIO.

Building from source

Contributors, and anyone who wants a build before the next release reaches Central, can build jev4k from a checkout:

# From a checkout of the jev4k repository (builds with JDK 25; Gradle downloads it if missing)
./gradlew build

Using an unreleased build from another project

The simplest way to depend on a checkout rather than a published artifact is a Gradle composite build. Include the jev4k checkout in your project's settings:

// settings.gradle.kts of your project: build jev4k from a sibling checkout
includeBuild("../jev4k")

Then depend on it by its coordinates, which Gradle substitutes with the included build:

// build.gradle.kts of your project
plugins {
    kotlin("jvm") version "2.4.20"
    kotlin("plugin.serialization") version "2.4.20" // only needed for @Serializable state
}

dependencies {
    implementation("com.pambrose:jev4k:0.1.1") // the version in the checkout's gradle.properties
}

kotlin {
    jvmToolchain(17) // jev4k targets Java 17, so anything 17 or newer works
}

The plugin.serialization line is needed only if you pass your own @Serializable classes as state.

API key

The client reads the key from the TYPESAFE_API_KEY environment variable:

# Create a key at https://console.typesafe.ai/settings/keys, then:
export TYPESAFE_API_KEY=ts-...

You can also set it in code; see Configuration. Keep API keys server-side: don't ship them in a browser or mobile app.

Checking your setup

The repository includes a runnable example that uses both DSL styles against the live API:

export TYPESAFE_API_KEY=ts-...
make example      # runs src/test/kotlin/com/pambrose/jev4k/examples/TriageExample.kt

Next: the Quick Start.