Tokenize text and apply a chat template
Your model consumes token ids, and a chat model expects its prompt formatted exactly the way it was trained. ClikaRT::Tokenizer covers both: one loader reads a HuggingFace model directory, encode/decode convert text to ids and back, and the model's own chat template renders conversations. No Python and no external tokenizer library are involved.
The programs below use the tokenizer of SmolLM2-135M-Instruct, the model from the GGUF guide. Two small files are all a tokenizer needs:
mkdir -p SmolLM2-135M-Instruct && cd SmolLM2-135M-Instruct
curl -LO "https://huggingface.co/HuggingFaceTB/SmolLM2-135M-Instruct/resolve/main/tokenizer.json"
curl -LO "https://huggingface.co/HuggingFaceTB/SmolLM2-135M-Instruct/resolve/main/tokenizer_config.json"
cd ..
The bundle also ships a self-contained tokenizer under examples/src/tokenizer/data/hf_model, if you would rather not download anything. The C samples abbreviate the api-table bootstrap that tutorial part 1 shows in full.
Load a tokenizer and round-trip some text
Tokenizer::from_huggingface takes the model directory, detects the artifact inside it (tokenizer.json, tokenizer.model, tekken.json, or vocab.json plus merges.txt), and overlays tokenizer_config.json for the special-token ids and the chat template. Tokenizer::from_file loads one tokenizer file (or a directory holding one) directly, detecting its format the same way.
- C++
- C
- Python
- Kotlin
- Go
- Rust
#include <cstdint>
#include <cstdio>
#include <string>
#include <vector>
#include "ClikaRT/clika_rt.h"
using ClikaRT::tokenizer::Tokenizer;
int main() {
Tokenizer tok = Tokenizer::from_huggingface("SmolLM2-135M-Instruct");
std::printf("vocab %lld, bos %lld, eos %lld, chat template: %s\n",
(long long)tok.vocab_size(), (long long)tok.bos_id(),
(long long)tok.eos_id(), tok.has_chat_template() ? "yes" : "no");
const std::string text = "ClikaRT runs the same code on every backend.";
const std::vector<std::int32_t> ids = tok.encode(text);
std::printf("encoded %zu tokens:", ids.size());
for (std::int32_t id : ids) std::printf(" %d", id);
std::printf("\ndecoded: %s\n", tok.decode(ids).c_str());
return 0;
}
#include <stdio.h>
#include <string.h>
#include "clika_rt/clika_rt_core.h"
/* api, check(): the bootstrap from tutorial part 1 (dlopen + clika_rt_get_api). */
extern const clika_rt_api* api;
extern void check(clika_rt_error* e, const char* where);
int main(void) {
clika_rt_tokenizer* tok = NULL;
check(api->tokenizer_from_huggingface("SmolLM2-135M-Instruct", 21, &tok), "from_hf");
printf("vocab %lld, bos %lld, eos %lld\n",
(long long)api->tokenizer_vocab_size(tok),
(long long)api->tokenizer_bos_id(tok),
(long long)api->tokenizer_eos_id(tok));
const char* text = "ClikaRT runs the same code on every backend.";
/* Two-call sizing: NULL buffer asks for the count, then fill. */
size_t count = 0;
check(api->tokenizer_encode(tok, text, strlen(text), 1, NULL, &count), "size");
int32_t ids[128];
check(api->tokenizer_encode(tok, text, strlen(text), 1, ids, &count), "encode");
printf("encoded %zu tokens:", count);
for (size_t i = 0; i < count; ++i) printf(" %d", ids[i]);
char out[512];
size_t olen = sizeof out;
check(api->tokenizer_decode(tok, ids, count, 1, out, &olen), "decode");
printf("\ndecoded: %s\n", out);
api->tokenizer_free(tok);
return 0;
}
import clika_runtime as crt
def main() -> None:
tok = crt.tokenizer.Tokenizer.from_huggingface("SmolLM2-135M-Instruct")
print(f"vocab {tok.vocab_size}, bos {tok.bos_id}, eos {tok.eos_id}, "
f"chat template: {'yes' if tok.has_chat_template else 'no'}")
text = "ClikaRT runs the same code on every backend."
ids = tok.encode(text)
print(f"encoded {len(ids)} tokens:", *ids)
print(f"decoded: {tok.decode(ids)}")
if __name__ == "__main__":
main()
import io.clika.runtime.ClikaRtGen
fun main() {
// Tokenizers ride the generated handle tier; frees are explicit.
ClikaRtGen.load()
val tok = ClikaRtGen.tokenizerFromHuggingface("SmolLM2-135M-Instruct")
println("vocab ${ClikaRtGen.tokenizerVocabSize(tok)}, " +
"bos ${ClikaRtGen.tokenizerBosId(tok)}, eos ${ClikaRtGen.tokenizerEosId(tok)}, " +
"chat template: ${if (ClikaRtGen.tokenizerHasChatTemplate(tok)) "yes" else "no"}")
val text = "ClikaRT runs the same code on every backend."
val ids = ClikaRtGen.tokenizerEncode(tok, text, true)
println("encoded ${ids.size} tokens: ${ids.joinToString(" ")}")
println("decoded: ${ClikaRtGen.tokenizerDecode(tok, ids, true)}")
ClikaRtGen.tokenizerFree(tok)
}
package main
import (
"fmt"
"log"
"github.com/Clika/clika_runtime/bindings/go/clikart"
)
func must[T any](v T, err error) T {
if err != nil {
log.Fatal(err)
}
return v
}
func main() {
api := must(clikart.Load("libClikaRT.so"))
tok := must(api.TokenizerFromHuggingface("SmolLM2-135M-Instruct"))
fmt.Printf("vocab %d, bos %d, eos %d\n",
api.TokenizerVocabSize(tok), api.TokenizerBosId(tok), api.TokenizerEosId(tok))
text := "ClikaRT runs the same code on every backend."
ids := must(api.TokenizerEncode(tok, text, true))
fmt.Printf("encoded %d tokens: ", len(ids))
for _, id := range ids {
fmt.Printf("%d ", id)
}
fmt.Printf("\ndecoded: %s\n", must(api.TokenizerDecode(tok, ids, true)))
}
use clika_rt::Api;
fn main() -> clika_rt::Result<()> {
let api = Api::load("libClikaRT.so")?;
let tok = api.f_tokenizer_from_huggingface("SmolLM2-135M-Instruct")?;
println!("vocab {}, bos {}, eos {}",
api.tokenizer_vocab_size(&tok),
api.tokenizer_bos_id(&tok),
api.tokenizer_eos_id(&tok));
let text = "ClikaRT runs the same code on every backend.";
let ids = api.f_tokenizer_encode(&tok, text, true)?;
print!("encoded {} tokens:", ids.len());
for id in &ids {
print!(" {id}");
}
println!("\ndecoded: {}", api.f_tokenizer_decode(&tok, &ids, true)?);
Ok(())
}
vocab 49152, bos 1, eos 2, chat template: yes
encoded 12 tokens: 51 1418 6335 16895 7313 260 1142 2909 335 897 25817 30
decoded: ClikaRT runs the same code on every backend.
See where each token came from
tokenize returns one Token per piece: the id, the surface string, and the byte span [begin, end) in the original text. Slice the original by that span when you need alignment (highlighting, span labeling, streaming cursors); the spans line up exactly, dropped spaces included.
- C++
- C
- Python
- Kotlin
- Go
- Rust
#include <cstdio>
#include <string>
#include <string_view>
#include <vector>
#include "ClikaRT/clika_rt.h"
using ClikaRT::tokenizer::Token;
using ClikaRT::tokenizer::Tokenizer;
int main() {
Tokenizer tok = Tokenizer::from_huggingface("SmolLM2-135M-Instruct");
const std::string text = "Quantized weights stay packed.";
const std::vector<Token> tokens = tok.tokenize(text, /*add_special_tokens=*/false);
std::printf(" id [begin,end) source span\n");
for (const Token& t : tokens) {
const std::string_view span(text.data() + t.begin, t.end - t.begin);
std::printf(" %-6d [%2zu,%2zu) \"%.*s\"\n",
t.id, t.begin, t.end, (int)span.size(), span.data());
}
return 0;
}
#include <stdio.h>
#include <string.h>
#include "clika_rt/clika_rt_core.h"
extern const clika_rt_api* api;
extern void check(clika_rt_error* e, const char* where);
int main(void) {
clika_rt_tokenizer* tok = NULL;
check(api->tokenizer_from_huggingface("SmolLM2-135M-Instruct", 21, &tok), "from_hf");
const char* text = "Quantized weights stay packed.";
clika_rt_token_list* tokens = NULL;
check(api->tokenizer_tokenize(tok, text, strlen(text),
/*add_special_tokens=*/0, &tokens), "tokenize");
printf(" id [begin,end) source span\n");
for (size_t i = 0; i < api->token_list_count(tokens); ++i) {
const size_t begin = api->token_list_begin_at(tokens, i);
const size_t end = api->token_list_end_at(tokens, i);
printf(" %-6d [%2zu,%2zu) \"%.*s\"\n",
api->token_list_id_at(tokens, i), begin, end,
(int)(end - begin), text + begin);
}
api->token_list_free(tokens);
api->tokenizer_free(tok);
return 0;
}
import clika_runtime as crt
def main() -> None:
tok = crt.tokenizer.Tokenizer.from_huggingface("SmolLM2-135M-Instruct")
text = "Quantized weights stay packed."
ids = tok.encode(text, add_special_tokens=False)
# The python binding returns ids; id_to_token shows each piece. A
# leading 'G-with-breve' marks a token that starts with a space; the
# byte-span view is the C++ tokenize surface.
print(" id token")
for i in ids:
print(f" {i:<6} {tok.id_to_token(i)!r}")
if __name__ == "__main__":
main()
import io.clika.runtime.ClikaRtGen
fun main() {
ClikaRtGen.load()
val tok = ClikaRtGen.tokenizerFromHuggingface("SmolLM2-135M-Instruct")
val text = "Quantized weights stay packed."
val tokens = ClikaRtGen.tokenizerTokenize(tok, text, false)
println(" id [begin,end) source span")
for (i in 0 until ClikaRtGen.tokenListCount(tokens)) {
val begin = ClikaRtGen.tokenListBeginAt(tokens, i).toInt()
val end = ClikaRtGen.tokenListEndAt(tokens, i).toInt()
println(" %-6d [%2d,%2d) \"%s\"".format(
ClikaRtGen.tokenListIdAt(tokens, i), begin, end,
text.substring(begin, end)))
}
ClikaRtGen.tokenListFree(tokens)
ClikaRtGen.tokenizerFree(tok)
}
package main
import (
"fmt"
"log"
"github.com/Clika/clika_runtime/bindings/go/clikart"
)
func must[T any](v T, err error) T {
if err != nil {
log.Fatal(err)
}
return v
}
func main() {
api := must(clikart.Load("libClikaRT.so"))
tok := must(api.TokenizerFromHuggingface("SmolLM2-135M-Instruct"))
text := "Quantized weights stay packed."
tokens := must(api.TokenizerTokenize(tok, text, false))
fmt.Println(" id [begin,end) source span")
for i := uint(0); i < api.TokenListCount(tokens); i++ {
begin, end := api.TokenListBeginAt(tokens, i), api.TokenListEndAt(tokens, i)
fmt.Printf(" %-6d [%2d,%2d) %q\n",
api.TokenListIdAt(tokens, i), begin, end, text[begin:end])
}
}
use clika_rt::Api;
fn main() -> clika_rt::Result<()> {
let api = Api::load("libClikaRT.so")?;
let tok = api.f_tokenizer_from_huggingface("SmolLM2-135M-Instruct")?;
let text = "Quantized weights stay packed.";
let tokens = api.f_tokenizer_tokenize(&tok, text, false)?;
println!(" id [begin,end) source span");
for i in 0..api.token_list_count(&tokens) {
let (begin, end) = (api.token_list_begin_at(&tokens, i),
api.token_list_end_at(&tokens, i));
println!(" {:<6} [{:2},{:2}) {:?}",
api.token_list_id_at(&tokens, i), begin, end, &text[begin..end]);
}
Ok(())
}
id [begin,end) source span
24696 [ 0, 5) "Quant"
1005 [ 5, 9) "ized"
10379 [ 9,17) " weights"
2951 [17,22) " stay"
13448 [22,29) " packed"
30 [29,30) "."
Render a conversation with the model's chat template
A chat model's prompt format (its role markers, turn separators, generation priming) ships with the model as a Jinja2 template in tokenizer_config.json, and the loader attached it above. apply_chat_template renders a messages array the OpenAI-API shape into the exact prompt string; encode_chat goes straight to ids. Never hand-build these markers: the template is the model's contract.
- C++
- C
- Python
- Kotlin
- Go
- Rust
#include <cstdint>
#include <cstdio>
#include <string>
#include <vector>
#include "ClikaRT/clika_rt.h"
using ClikaRT::json::Json;
using ClikaRT::tokenizer::Tokenizer;
int main() {
Tokenizer tok = Tokenizer::from_huggingface("SmolLM2-135M-Instruct");
Json messages = Json::array();
Json system = Json::object();
system["role"] = "system";
system["content"] = "You are a concise assistant.";
messages.push_back(std::move(system));
Json user = Json::object();
user["role"] = "user";
user["content"] = "What does a tokenizer do?";
messages.push_back(std::move(user));
const std::string prompt = tok.apply_chat_template(messages);
std::printf("=== rendered prompt ===\n%s\n=======================\n", prompt.c_str());
const std::vector<std::int32_t> ids =
tok.encode_chat(messages, /*add_generation_prompt=*/true,
/*add_special_tokens=*/false);
std::printf("encode_chat produced %zu tokens\n", ids.size());
return 0;
}
#include <stdio.h>
#include <string.h>
#include "clika_rt/clika_rt_core.h"
extern const clika_rt_api* api;
extern void check(clika_rt_error* e, const char* where);
int main(void) {
clika_rt_tokenizer* tok = NULL;
check(api->tokenizer_from_huggingface("SmolLM2-135M-Instruct", 21, &tok), "from_hf");
/* The messages array travels as JSON text at this surface. */
const char* messages =
"[{\"role\": \"system\", \"content\": \"You are a concise assistant.\"},"
" {\"role\": \"user\", \"content\": \"What does a tokenizer do?\"}]";
char prompt[4096];
size_t plen = sizeof prompt;
check(api->tokenizer_apply_chat_template_messages_add_generation_prompt(
tok, messages, strlen(messages), /*add_generation_prompt=*/1,
prompt, &plen), "apply_chat_template");
printf("=== rendered prompt ===\n%s\n=======================\n", prompt);
size_t count = 0;
check(api->tokenizer_encode_chat_messages(tok, messages, strlen(messages),
1, 0, NULL, &count), "size");
int32_t ids[512];
check(api->tokenizer_encode_chat_messages(tok, messages, strlen(messages),
1, 0, ids, &count), "encode_chat");
printf("encode_chat produced %zu tokens\n", count);
api->tokenizer_free(tok);
return 0;
}
import json
import clika_runtime as crt
def main() -> None:
tok = crt.tokenizer.Tokenizer.from_huggingface("SmolLM2-135M-Instruct")
# The messages array travels as JSON text at this surface.
messages = json.dumps([
{"role": "system", "content": "You are a concise assistant."},
{"role": "user", "content": "What does a tokenizer do?"},
])
prompt = tok.apply_chat_template(messages)
print(f"=== rendered prompt ===\n{prompt}\n=======================")
ids = tok.encode_chat(messages, add_generation_prompt=True,
add_special_tokens=False)
print(f"encode_chat produced {len(ids)} tokens")
if __name__ == "__main__":
main()
import io.clika.runtime.ClikaRtGen
fun main() {
ClikaRtGen.load()
val tok = ClikaRtGen.tokenizerFromHuggingface("SmolLM2-135M-Instruct")
// The messages array travels as JSON text at this surface.
val messages = """
[{"role": "system", "content": "You are a concise assistant."},
{"role": "user", "content": "What does a tokenizer do?"}]
""".trimIndent()
val prompt = ClikaRtGen.tokenizerApplyChatTemplateMessagesAddGenerationPrompt(
tok, messages, true)
println("=== rendered prompt ===\n$prompt\n=======================")
val ids = ClikaRtGen.tokenizerEncodeChatMessages(tok, messages, true, false)
println("encode_chat produced ${ids.size} tokens")
ClikaRtGen.tokenizerFree(tok)
}
package main
import (
"fmt"
"log"
"github.com/Clika/clika_runtime/bindings/go/clikart"
)
func must[T any](v T, err error) T {
if err != nil {
log.Fatal(err)
}
return v
}
func main() {
api := must(clikart.Load("libClikaRT.so"))
tok := must(api.TokenizerFromHuggingface("SmolLM2-135M-Instruct"))
// The messages array travels as JSON text at this surface.
messages := `[{"role": "system", "content": "You are a concise assistant."},
{"role": "user", "content": "What does a tokenizer do?"}]`
prompt := must(api.TokenizerApplyChatTemplateMessagesAddGenerationPrompt(tok, messages, true))
fmt.Printf("=== rendered prompt ===\n%s\n=======================\n", prompt)
ids := must(api.TokenizerEncodeChatMessages(tok, messages, true, false))
fmt.Printf("encode_chat produced %d tokens\n", len(ids))
}
use clika_rt::Api;
fn main() -> clika_rt::Result<()> {
let api = Api::load("libClikaRT.so")?;
let tok = api.f_tokenizer_from_huggingface("SmolLM2-135M-Instruct")?;
// The messages array travels as JSON text at this surface.
let messages = r#"[{"role": "system", "content": "You are a concise assistant."},
{"role": "user", "content": "What does a tokenizer do?"}]"#;
let prompt = api.f_tokenizer_apply_chat_template_messages_add_generation_prompt(
&tok, messages, true)?;
println!("=== rendered prompt ===\n{prompt}\n=======================");
let ids = api.f_tokenizer_encode_chat_messages(&tok, messages, true, false)?;
println!("encode_chat produced {} tokens", ids.len());
Ok(())
}
=== rendered prompt ===
<|im_start|>system
You are a concise assistant.<|im_end|>
<|im_start|>user
What does a tokenizer do?<|im_end|>
<|im_start|>assistant
=======================
encode_chat produced 26 tokens
The rendered prompt ends with the assistant-turn priming (add_generation_prompt defaults to true), so the model continues as the assistant. For tool calling, extra template variables, or a reproducible clock, pass a ChatTemplateInputs instead of the bare messages array; the two-argument form above covers plain conversations.
Batch for a model
Feeding a model takes tensors, not vectors. encode_batch with return_tensors produces the standard quartet: padded input_ids [B, S], an attention_mask, per-sequence lengths, and the cu_seqlens prefix-sum table. varlen = true skips padding entirely and lays the ids out flat, the shape variable-length attention consumes.
- C++
- C
- Python
- Kotlin
- Go
- Rust
#include <cstdio>
#include <string_view>
#include <vector>
#include "ClikaRT/clika_rt.h"
using ClikaRT::tokenizer::EncodeOptions;
using ClikaRT::tokenizer::Tokenizer;
int main() {
Tokenizer tok = Tokenizer::from_huggingface("SmolLM2-135M-Instruct");
const std::vector<std::string_view> texts = {
"Short prompt.",
"A somewhat longer prompt that pads the short one.",
};
EncodeOptions opts;
opts.return_tensors = true;
// Decoder-only checkpoints often ship no pad token; designate one (eos is
// the usual choice) or the padded encode raises ClikaRT::Error.
opts.pad_id = static_cast<std::int32_t>(tok.eos_id());
const ClikaRT::tokenizer::Encoded batch = tok.encode_batch(texts, opts);
std::printf("input_ids %s\n", batch.input_ids->to_string().c_str());
std::printf("attention_mask %s\n", batch.attention_mask->to_string().c_str());
std::printf("seq_lengths %s\n", batch.seq_lengths->to_string().c_str());
opts.varlen = true;
const ClikaRT::tokenizer::Encoded flat = tok.encode_batch(texts, opts);
std::printf("varlen ids %s\n", flat.input_ids->to_string().c_str());
std::printf("cu_seqlens %s\n", flat.cu_seqlens->to_string().c_str());
return 0;
}
#include <stdio.h>
#include <string.h>
#include "clika_rt/clika_rt_core.h"
extern const clika_rt_api* api;
extern void check(clika_rt_error* e, const char* where);
static void print_tensor(const char* label, clika_rt_tensor* t) {
char buf[512];
size_t blen = sizeof buf;
api->tensor_to_string(t, buf, &blen);
printf("%-14s %s\n", label, buf);
}
int main(void) {
clika_rt_tokenizer* tok = NULL;
check(api->tokenizer_from_huggingface("SmolLM2-135M-Instruct", 21, &tok), "from_hf");
const char* texts[2] = {
"Short prompt.",
"A somewhat longer prompt that pads the short one.",
};
const size_t lens[2] = {strlen(texts[0]), strlen(texts[1])};
clika_rt_encode_options opts = {0};
opts.add_special_tokens = 1;
opts.return_tensors = 1;
/* Decoder-only checkpoints often ship no pad token; designate one (eos
* is the usual choice) or the padded encode fails typed. */
opts.has_pad_id = 1;
opts.pad_id = (int32_t)api->tokenizer_eos_id(tok);
clika_rt_encoded* batch = NULL;
check(api->tokenizer_encode_batch(tok, texts, lens, 2, &opts, &batch), "encode_batch");
print_tensor("input_ids", api->encoded_input_ids(batch));
print_tensor("attention_mask", api->encoded_attention_mask(batch));
print_tensor("seq_lengths", api->encoded_seq_lengths(batch));
opts.varlen = 1;
clika_rt_encoded* flat = NULL;
check(api->tokenizer_encode_batch(tok, texts, lens, 2, &opts, &flat), "encode_batch");
print_tensor("varlen ids", api->encoded_input_ids(flat));
print_tensor("cu_seqlens", api->encoded_cu_seqlens(flat));
api->encoded_free(flat);
api->encoded_free(batch);
api->tokenizer_free(tok);
return 0;
}
import clika_runtime as crt
def main() -> None:
tok = crt.tokenizer.Tokenizer.from_huggingface("SmolLM2-135M-Instruct")
texts = [
"Short prompt.",
"A somewhat longer prompt that pads the short one.",
]
# The python binding returns the ragged ids, one list per text; pad on
# the tensor side with the lengths below. The padded quartet (input_ids,
# attention_mask, seq_lengths, cu_seqlens) is the C++ and C surface.
batch = tok.encode_batch(texts)
for row, ids in enumerate(batch.ids):
print(f"text {row}: {len(ids):2} ids {ids}")
if __name__ == "__main__":
main()
The generated tier skips the string-array batch entry point, so Kotlin has no multi-text encode_batch today. ClikaRtGen.tokenizerEncodeText builds the same tensor columns (encodedInputIds, encodedAttentionMask, encodedSeqLengths, encodedCuSeqlens) for one text at a time; the C tab is the full batch story this binding rides.
package main
import (
"fmt"
"log"
"github.com/Clika/clika_runtime/bindings/go/clikart"
)
func must[T any](v T, err error) T {
if err != nil {
log.Fatal(err)
}
return v
}
func main() {
api := must(clikart.Load("libClikaRT.so"))
tok := must(api.TokenizerFromHuggingface("SmolLM2-135M-Instruct"))
texts := []string{
"Short prompt.",
"A somewhat longer prompt that pads the short one.",
}
// Decoder-only checkpoints often ship no pad token; designate one (eos
// is the usual choice) or the padded encode fails typed.
opts := &clikart.EncodeOptions{
AddSpecialTokens: true,
ReturnTensors: true,
HasPadId: true,
PadId: int32(api.TokenizerEosId(tok)),
}
batch := must(api.TokenizerEncodeBatch(tok, texts, opts))
fmt.Printf("input_ids %s\n", api.EncodedInputIds(batch))
fmt.Printf("attention_mask %s\n", api.EncodedAttentionMask(batch))
fmt.Printf("seq_lengths %s\n", api.EncodedSeqLengths(batch))
opts.Varlen = true
flat := must(api.TokenizerEncodeBatch(tok, texts, opts))
fmt.Printf("varlen ids %s\n", api.EncodedInputIds(flat))
fmt.Printf("cu_seqlens %s\n", api.EncodedCuSeqlens(flat))
}
use clika_rt::{sys, Api};
fn main() -> clika_rt::Result<()> {
let api = Api::load("libClikaRT.so")?;
let tok = api.f_tokenizer_from_huggingface("SmolLM2-135M-Instruct")?;
let texts = [
"Short prompt.",
"A somewhat longer prompt that pads the short one.",
];
// Decoder-only checkpoints often ship no pad token; designate one (eos
// is the usual choice) or the padded encode fails typed.
let mut opts = sys::clika_rt_encode_options {
add_special_tokens: 1,
return_tensors: 1,
varlen: 0,
has_pad_id: 1,
pad_id: api.tokenizer_eos_id(&tok) as i32,
};
let batch = api.f_tokenizer_encode_batch(&tok, &texts, Some(&opts))?;
let ids = api.encoded_input_ids(&batch).expect("input_ids");
let mask = api.encoded_attention_mask(&batch).expect("attention_mask");
let lens = api.encoded_seq_lengths(&batch).expect("seq_lengths");
println!("input_ids {}", api.tensor_to_string(&ids)?);
println!("attention_mask {}", api.tensor_to_string(&mask)?);
println!("seq_lengths {}", api.tensor_to_string(&lens)?);
opts.varlen = 1;
let flat = api.f_tokenizer_encode_batch(&tok, &texts, Some(&opts))?;
let flat_ids = api.encoded_input_ids(&flat).expect("input_ids");
let cu = api.encoded_cu_seqlens(&flat).expect("cu_seqlens");
println!("varlen ids {}", api.tensor_to_string(&flat_ids)?);
println!("cu_seqlens {}", api.tensor_to_string(&cu)?);
Ok(())
}
input_ids Tensor(shape=[2, 10], dtype=Int32, device=CPU, numel=20, data=[20355, 6011, 30, 2, 2, 2, ...])
attention_mask Tensor(shape=[2, 10], dtype=Int32, device=CPU, numel=20, data=[1, 1, 1, 0, 0, 0, ...])
seq_lengths Tensor(shape=[2], dtype=Int32, device=CPU, numel=2, data=[3, 10])
varlen ids Tensor(shape=[13], dtype=Int32, device=CPU, numel=13, data=[20355, 6011, 30, 49, 7932, 2848, ...])
cu_seqlens Tensor(shape=[3], dtype=Int32, device=CPU, numel=3, data=[0, 3, 13])
EncodeOptions also carries truncation (max_length, truncation_side), the padding side (Left suits decoder-only batch generation), and a target device so the tensors land where the model computes. The bundle's tokenizer example walks each of these one chapter at a time, and the templating example covers the Jinja2-compatible engine behind apply_chat_template on its own.
Stream the decode of a generation loop
A generation loop produces ids one at a time, and decode(ids) over the growing list re-decodes everything on every step. Tokenizer::streaming_decoder is the incremental form: push(id) returns exactly the newly-stable text, and the pieces concatenate to what decode would have produced. The catch it handles for you is the UTF-8 boundary: one code point can span tokens, so push holds bytes back until they are displayable and returns an empty string meanwhile; you never emit half a character. finish() flushes whatever the tail held (a trailing incomplete sequence as-is) and resets the decoder for a fresh stream; a decoder serves one stream at a time.
- C++
- C
- Python
- Kotlin
- Go
- Rust
#include <cstdint>
#include <cstdio>
#include <string>
#include <vector>
#include "ClikaRT/clika_rt.h"
using ClikaRT::tokenizer::StreamingDecoder;
using ClikaRT::tokenizer::Tokenizer;
int main() {
Tokenizer tok = Tokenizer::from_huggingface("SmolLM2-135M-Instruct");
// Stand-in for a generation loop: the ids a real decoder would emit one
// at a time (the roundtrip section's sentence, so the ids match).
const std::vector<std::int32_t> ids =
tok.encode("ClikaRT runs the same code on every backend.");
StreamingDecoder stream = tok.streaming_decoder();
std::string assembled;
int emitted = 0;
for (std::int32_t id : ids) {
// push returns exactly the newly-stable text: empty while a
// multi-byte code point is still incomplete, never a torn character.
const std::string piece = stream.push(id);
if (!piece.empty()) ++emitted;
assembled += piece;
}
assembled += stream.finish(); // flush the tail; the decoder resets
std::printf("%zu ids -> %d incremental pieces\n", ids.size(), emitted);
std::printf("assembled: %s\n", assembled.c_str());
std::printf("assembled == decode(ids): %s\n",
assembled == tok.decode(ids) ? "yes" : "no");
return 0;
}
#include <stdio.h>
#include <string.h>
#include "clika_rt/clika_rt_core.h"
/* api, check(): the bootstrap from tutorial part 1 (dlopen + clika_rt_get_api). */
extern const clika_rt_api* api;
extern void check(clika_rt_error* e, const char* where);
/* Read the one entry of a piece list into buf and free the list. */
static void take_piece(clika_rt_string_list* piece, char* buf, size_t cap) {
size_t blen = cap;
api->string_list_get_at(piece, 0, buf, &blen);
api->string_list_free(piece);
}
int main(void) {
clika_rt_tokenizer* tok = NULL;
check(api->tokenizer_from_huggingface("SmolLM2-135M-Instruct", 21, &tok), "from_hf");
/* Stand-in for a generation loop: the ids a real decoder would emit one
* at a time (the roundtrip section's sentence, so the ids match). */
const char* text = "ClikaRT runs the same code on every backend.";
size_t count = 0;
check(api->tokenizer_encode(tok, text, strlen(text), 1, NULL, &count), "size");
int32_t ids[128];
check(api->tokenizer_encode(tok, text, strlen(text), 1, ids, &count), "encode");
/* One generation stream at a time; finish resets it. */
clika_rt_streaming_decoder* stream = api->tokenizer_streaming_decoder(tok, 1);
char assembled[512] = "";
char buf[128];
int emitted = 0;
for (size_t i = 0; i < count; ++i) {
/* push answers a one-entry string list: the newly-stable text. */
clika_rt_string_list* piece = NULL;
check(api->streaming_decoder_push(stream, ids[i], &piece), "push");
take_piece(piece, buf, sizeof buf);
if (buf[0] != '\0') ++emitted;
strncat(assembled, buf, sizeof assembled - strlen(assembled) - 1);
}
clika_rt_string_list* tail = NULL;
check(api->streaming_decoder_finish(stream, &tail), "finish");
take_piece(tail, buf, sizeof buf); /* flush; the decoder resets */
strncat(assembled, buf, sizeof assembled - strlen(assembled) - 1);
printf("%zu ids -> %d incremental pieces\n", count, emitted);
printf("assembled: %s\n", assembled);
api->streaming_decoder_free(stream);
api->tokenizer_free(tok);
return 0;
}
import clika_runtime as crt
def main() -> None:
tok = crt.tokenizer.Tokenizer.from_huggingface("SmolLM2-135M-Instruct")
# Stand-in for a generation loop: the ids a real decoder would emit one
# at a time (the roundtrip section's sentence, so the ids match).
ids = tok.encode("ClikaRT runs the same code on every backend.")
stream = tok.streaming_decoder()
pieces = [stream.push(i) for i in ids] # "" while a code point is incomplete
assembled = "".join(pieces) + stream.finish() # flush; the decoder resets
print(f"{len(ids)} ids -> {sum(1 for p in pieces if p)} incremental pieces")
print(f"assembled: {assembled}")
print(f"assembled == decode(ids): "
f"{'yes' if assembled == tok.decode(ids) else 'no'}")
if __name__ == "__main__":
main()
import io.clika.runtime.ClikaRtGen
fun main() {
ClikaRtGen.load()
val tok = ClikaRtGen.tokenizerFromHuggingface("SmolLM2-135M-Instruct")
// Stand-in for a generation loop: the ids a real decoder would emit one
// at a time (the roundtrip section's sentence, so the ids match).
val ids = ClikaRtGen.tokenizerEncode(
tok, "ClikaRT runs the same code on every backend.", true)
// One generation stream at a time; finish resets it.
val stream = ClikaRtGen.tokenizerStreamingDecoder(tok, true)
// push answers a one-entry string list: the newly-stable text.
fun take(list: Long): String {
val piece = ClikaRtGen.stringListGetAt(list, 0)
ClikaRtGen.stringListFree(list)
return piece
}
var assembled = ""
var emitted = 0
for (id in ids) {
val piece = take(ClikaRtGen.streamingDecoderPush(stream, id))
if (piece.isNotEmpty()) emitted++
assembled += piece
}
assembled += take(ClikaRtGen.streamingDecoderFinish(stream)) // flush; resets
println("${ids.size} ids -> $emitted incremental pieces")
println("assembled: $assembled")
println("assembled == decode(ids): " +
if (assembled == ClikaRtGen.tokenizerDecode(tok, ids, true)) "yes" else "no")
ClikaRtGen.streamingDecoderFree(stream)
ClikaRtGen.tokenizerFree(tok)
}
package main
import (
"fmt"
"log"
"strings"
"github.com/Clika/clika_runtime/bindings/go/clikart"
)
func must[T any](v T, err error) T {
if err != nil {
log.Fatal(err)
}
return v
}
// piece reads the one entry of a push/finish answer.
func piece(api *clikart.Api, l *clikart.StringList) string {
s := must(api.StringListGetAt(l, 0))
return s
}
func main() {
api := must(clikart.Load("libClikaRT.so"))
tok := must(api.TokenizerFromHuggingface("SmolLM2-135M-Instruct"))
// Stand-in for a generation loop: the ids a real decoder would emit one
// at a time (the roundtrip section's sentence, so the ids match).
ids := must(api.TokenizerEncode(tok, "ClikaRT runs the same code on every backend.", true))
// One generation stream at a time; finish resets it.
stream := api.TokenizerStreamingDecoder(tok, true)
var assembled strings.Builder
emitted := 0
for _, id := range ids {
// push answers exactly the newly-stable text: empty while a
// multi-byte code point is still incomplete.
p := piece(api, must(api.StreamingDecoderPush(stream, id)))
if p != "" {
emitted++
}
assembled.WriteString(p)
}
assembled.WriteString(piece(api, must(api.StreamingDecoderFinish(stream)))) // flush; resets
fmt.Printf("%d ids -> %d incremental pieces\n", len(ids), emitted)
fmt.Printf("assembled: %s\n", assembled.String())
fmt.Printf("assembled == decode(ids): %v\n",
assembled.String() == must(api.TokenizerDecode(tok, ids, true)))
}
use clika_rt::Api;
fn main() -> clika_rt::Result<()> {
let api = Api::load("libClikaRT.so")?;
let tok = api.f_tokenizer_from_huggingface("SmolLM2-135M-Instruct")?;
// Stand-in for a generation loop: the ids a real decoder would emit one
// at a time (the roundtrip section's sentence, so the ids match).
let ids = api.f_tokenizer_encode(&tok, "ClikaRT runs the same code on every backend.", true)?;
// One generation stream at a time; finish resets it.
let stream = api.tokenizer_streaming_decoder(&tok, true).expect("streaming decoder");
let mut assembled = String::new();
let mut emitted = 0;
for id in &ids {
// push answers a one-entry string list: the newly-stable text, empty
// while a multi-byte code point is still incomplete.
let piece = api.f_string_list_get_at(&api.f_streaming_decoder_push(&stream, *id)?, 0)?;
if !piece.is_empty() {
emitted += 1;
}
assembled.push_str(&piece);
}
let tail = api.f_streaming_decoder_finish(&stream)?;
assembled.push_str(&api.f_string_list_get_at(&tail, 0)?); // flush; resets
println!("{} ids -> {emitted} incremental pieces", ids.len());
println!("assembled: {assembled}");
println!("assembled == decode(ids): {}",
if assembled == api.f_tokenizer_decode(&tok, &ids, true)? { "yes" } else { "no" });
Ok(())
}
12 ids -> 12 incremental pieces
assembled: ClikaRT runs the same code on every backend.
assembled == decode(ids): yes
Every push emitted text here because the sentence is plain ASCII; text with accents, CJK, or emoji is where the empty returns appear, and exactly why the boundary handling exists. The decoder skips special tokens by default (streaming_decoder(false) keeps them), and the handle stays valid even after the Tokenizer that made it is gone, so a generation worker can own just the decoder.
This is the producer half of token streaming: each non-empty piece is one frame for the transport. The serving guide sends exactly these pieces as token events over server-sent events.