Sampling controls how the model chooses the next token from its probability distribution.

Sampler presets

For common configurations, use SamplerPresets:

import ai.quaynor.Chat
import ai.quaynor.SamplerPresets

val chat = Chat.fromPath(
    modelPath = "/path/to/model.gguf",
    sampler = SamplerPresets.temperature(0.2f)
)

Available presets:

  • SamplerPresets.default()
  • SamplerPresets.dry()
  • SamplerPresets.grammar(grammar)
  • SamplerPresets.greedy()
  • SamplerPresets.json()
  • SamplerPresets.temperature(temperature)
  • SamplerPresets.topK(topK)
  • SamplerPresets.topP(topP)

The numeric presets take Float, so write 0.2f rather than 0.2.

Structured output

Use json() when you want strictly valid JSON output:

val chat = Chat.fromPath(
    modelPath = "/path/to/model.gguf",
    sampler = SamplerPresets.json()
)

For tighter formats, use a custom GBNF grammar:

val grammar = """
file ::= record (newline record)* newline?
record ::= field ("," field)*
field ::= quoted_field | unquoted_field
unquoted_field ::= [^,"\n\r]*
quoted_field ::= "\"" ([^"] | "\"\"")* "\""
newline ::= "\r\n" | "\n"
""".trimIndent()

val chat = Chat.fromPath(
    modelPath = "/path/to/model.gguf",
    sampler = SamplerPresets.grammar(grammar)
)

Grammar constraints are enforced during sampling rather than checked afterwards, so the model cannot emit output that violates them.

Custom sampler pipelines

Kotlin exposes a DSL through buildSampler. Chain any number of shift steps, then finish with a terminal step:

import ai.quaynor.buildSampler

val sampler = buildSampler {
    topK(40)
    temperature(0.8)
    minP(0.05)
    dist()
}

val chat = Chat.fromPath(
    modelPath = "/path/to/model.gguf",
    sampler = sampler
)

Shift steps: topK, topP, minP, temperature, typicalP, xtc, grammar, dry, penalties.

Terminal steps: dist, greedy, mirostatV1, mirostatV2. If you omit the terminal step, dist() is used. Calling two terminal steps throws IllegalStateException.

Order matters — steps are applied in the order you declare them, so temperature before topK behaves differently from the reverse.

Unlike the presets, the DSL takes Double for its floating-point arguments, so plain 0.8 is correct here:

val creative = buildSampler {
    temperature(1.1)
    topP(0.95)
    penalties(penaltyRepeat = 1.1)
    dist()
}

val deterministic = buildSampler { greedy() }

Changing the sampler later

You can update the sampler on an existing chat:

chat.setSamplerConfig(sampler)

And inspect the current config as JSON:

println(chat.getSamplerConfigJson())