Every interaction with a chat model starts by creating a Chat.

Creating a Chat

The simplest entrypoint is Chat.fromPath:

import ai.quaynor.Chat

val chat = Chat.fromPath(
    modelPath = "hf://bartowski/Qwen_Qwen3-0.6B-GGUF/Qwen_Qwen3-0.6B-Q4_K_M.gguf"
)

If you want to share one loaded model across multiple chat sessions, load the Model first:

import ai.quaynor.Chat
import ai.quaynor.Model

val model = Model.load("hf://bartowski/Qwen_Qwen3-0.6B-GGUF/Qwen_Qwen3-0.6B-Q4_K_M.gguf")
val chat1 = Chat(model = model)
val chat2 = Chat(model = model)

Loading a model is expensive in both time and memory, so sharing one Model across sessions is usually what you want.

When loading a remote model, pass a progress callback:

val model = Model.load(
    modelPath = "hf://bartowski/Qwen_Qwen3-0.6B-GGUF/Qwen_Qwen3-0.6B-Q4_K_M.gguf"
) { downloaded, total ->
    println("$downloaded / $total")
}

Asking and streaming

chat.ask returns a TokenStream.

val stream = chat.ask("Why is the sky blue?")

To wait for the full answer:

val full = stream.completed()
println(full)

To stream tokens as they arrive, collect the Flow:

import kotlinx.coroutines.flow.collect

chat.ask("Why is the sky blue?").asFlow().collect { token ->
    print(token)
}

asFlow() is cancellation-aware — cancelling the collecting coroutine stops the stream. You can also pull tokens manually, where nextToken() returns null once generation ends:

val stream = chat.ask("Count to three.")
while (true) {
    val token = stream.nextToken() ?: break
    print(token)
}

To stop generation early from elsewhere:

chat.stopGeneration()

Chat history

Quaynor keeps the conversation history inside the Chat instance.

Read it:

val messages = chat.getChatHistory()
println(messages.size)

Message is a sealed class, so you can branch on the variants exhaustively:

import ai.quaynor.Message

for (message in chat.getChatHistory()) {
    when (message) {
        is Message.User -> println("user: ${message.content}")
        is Message.Assistant -> println("assistant: ${message.content}")
        is Message.System -> println("system: ${message.content}")
        is Message.Tool -> println("tool ${message.name}: ${message.content}")
    }
}

Replace it:

chat.setChatHistory(
    listOf(
        Message.System("You are concise."),
        Message.User("Summarize the task.")
    )
)

System prompt and context

Set the system prompt when creating the chat:

val chat = Chat.fromPath(
    modelPath = "/path/to/model.gguf",
    systemPrompt = "You are a precise engineering assistant.",
    contextSize = 4096u
)

Note that contextSize is a UInt, hence the u suffix. Do not set it above what the model was trained for — read the ceiling from model.maxCtx:

val model = Model.load("/path/to/model.gguf")
val chat = Chat(model = model, contextSize = minOf(8192u, model.maxCtx))

Change the system prompt later:

chat.setSystemPrompt("You are a code reviewer.")

Reset the current context while optionally changing defaults:

chat.resetContext(systemPrompt = "You are a code reviewer.")

Or just clear the accumulated history:

chat.resetHistory()

Inspect context usage:

val stats = chat.getStats()
println("${stats.contextUsed} / ${stats.contextSize}")

Tokenize a message as the chat template would render it:

val tokens = chat.tokenize("Hello")
println(tokens)

Template variables

Some models expose extra chat-template switches such as reasoning toggles.

val chat = Chat.fromPath(
    modelPath = "/path/to/model.gguf",
    templateVariables = mapOf("enable_thinking" to true)
)

Update them later:

chat.setTemplateVariable("enable_thinking", false)
println(chat.getTemplateVariables())

GPU

GPU acceleration is enabled by default. Disable it with useGpu = false when needed:

val model = Model.load(
    modelPath = "/path/to/model.gguf",
    useGpu = false
)