Examples

August 2, 2026 · View on GitHub

This directory contains runnable examples for the CLI, the public C API, and the OpenAI-compatible server. Commands below assume they are run from the repository root after make.

CLI examples

Text

# Inspect the model and the memory budget before loading it.
./waste info ~/models/k3.waste
./waste plan ~/models/k3.waste

# Generate a completion from an argument, stdin, or a file.
./waste run ~/models/k3.waste "Write a small C function that swaps two ints" -n 64
printf '%s\n' "The capital of Italy is" | ./waste run ~/models/k3.waste - -n 16
./waste run ~/models/k3.waste --file prompt.txt -n 64

# Inspect tokenization and the next-token distribution.
./waste tokenize ~/models/k3.waste "Hello, world"
./waste eval ~/models/k3.waste "The capital of France is" --top-k 5

# Keep a conversation state and inspect its cumulative I/O statistics.
./waste chat ~/models/k3.waste
> /stats
> /save session.waste
> /load session.waste

-n limits generated tokens. --budget is normally unnecessary because the engine selects a conservative default. Use --verify after copying or downloading a container to check expert-record checksums as they are read.

Multimodal

Kimi K3 can combine text with one or more images:

./waste run ~/models/k3.waste "Describe this photograph" \
    --image photo.jpg -n 64

./waste run ~/models/k3.waste "List the differences" \
    --image before.png --image after.png -n 96

./waste eval ~/models/k3.waste "A photograph of a" \
    --image coast.jpg --top-k 10

Images are placed before the text in CLI prompts. In a chat, attach an image to the next turn and then ask the question:

$ ./waste chat ~/models/k3.waste
> /image diagram.png
(diagram.png attached to the next message)
> Explain the data flow in this diagram.

Repeat /image before the message to attach several images. Once an image has been consumed, later turns can discuss it without encoding it again because its positions remain in the conversation state. /reset clears both text and image state.

An image expands into many prompt positions. At K3's default patch budget, an 896×896 image becomes 256 positions, and each costs approximately as much as a text position during prefill. See K3.md and TECHNICAL.md for measurements.

C API examples

The examples include only the public header, waste.h:

  • api_plan.c reads the memory plan without loading weights;
  • api_text.c tokenizes a raw prompt and generates text;
  • api_vision.c builds a K3 image turn, expands the image placeholder, and generates a response.

Build and run them against the static library:

make libwaste.a

cc -O2 -std=gnu11 -Isrc examples/api_plan.c libwaste.a \
    -lm -lpthread -o example-plan
cc -O2 -std=gnu11 -Isrc examples/api_text.c libwaste.a \
    -lm -lpthread -o example-text
cc -O2 -std=gnu11 -Isrc examples/api_vision.c libwaste.a \
    -lm -lpthread -o example-vision

./example-plan ~/models/k3.waste 4096
./example-text ~/models/k3.waste "The capital of Italy is"
./example-vision ~/models/k3.waste photo.jpg "What is in this image?"

The vision example sets cfg.vision = 1, calls waste_image_add, keeps K3 markup separate from untrusted user text, expands <|media_pad|> with waste_image_expand, and only then calls waste_generate. That ordering is required: one placeholder represents every embedding produced by the tower.

Conversation state can be persisted independently of generation:

waste_state_save(ctx, "session.waste");
waste_state_reset(ctx);
waste_state_load(ctx, "session.waste");

See ENGINE.md for lifecycle and threading details.

Server examples

Start the server with the vision tower when image requests are needed:

make libwaste.dylib                     # libwaste.so on Linux
python3 -m serve ~/models/k3.waste --port 8000 --vision

Chat and streaming

curl localhost:8000/v1/chat/completions \
  -H 'Content-Type: application/json' \
  -d '{"model":"k3","messages":[{"role":"user","content":"Why is the sky blue?"}],"reasoning_effort":"off"}'

curl -N localhost:8000/v1/chat/completions \
  -H 'Content-Type: application/json' \
  -d '{"model":"k3","stream":true,"stream_options":{"include_usage":true},"messages":[{"role":"user","content":"Write a haiku about local inference"}]}'

curl localhost:8000/v1/completions \
  -H 'Content-Type: application/json' \
  -d '{"model":"k3","prompt":"The capital of Italy is","max_tokens":16}'

The last request is a raw continuation and does not use the chat renderer.

Images

The server accepts base64 data: URLs. It deliberately does not fetch remote HTTP URLs. Local paths require the explicit --allow-local-images server flag.

IMAGE_B64="$(base64 < photo.jpg | tr -d '\n')"

curl localhost:8000/v1/chat/completions \
  -H 'Content-Type: application/json' \
  --data-binary "{\"model\":\"k3\",\"messages\":[{\"role\":\"user\",\"content\":[{\"type\":\"image_url\",\"image_url\":{\"url\":\"data:image/jpeg;base64,${IMAGE_B64}\"}},{\"type\":\"text\",\"text\":\"Describe this image\"}]}],\"reasoning_effort\":\"off\"}"

Tools and structured output

curl localhost:8000/v1/chat/completions \
  -H 'Content-Type: application/json' \
  -d '{"model":"k3","messages":[{"role":"user","content":"What is the weather in Rome?"}],"tools":[{"type":"function","function":{"name":"weather","description":"Get current weather","parameters":{"type":"object","properties":{"city":{"type":"string"}},"required":["city"]}}}],"tool_choice":"required"}'

curl localhost:8000/v1/chat/completions \
  -H 'Content-Type: application/json' \
  -d '{"model":"k3","messages":[{"role":"user","content":"Return the capital of Italy"}],"response_format":{"type":"json_schema","json_schema":{"name":"capital","schema":{"type":"object","properties":{"country":{"type":"string"},"capital":{"type":"string"}},"required":["country","capital"],"additionalProperties":false}}}}'

How server prompts are rendered

The HTTP server is stateless between requests. Under one engine lock it resets the conversation, resolves and encodes images, renders the request into XTML segments, tokenizes those segments, expands image placeholders, and starts generation. Requests queue because a waste_ctx is not thread-safe.

Prompt structure and user content take different tokenizer paths. XTML control segments use waste_tokenize_markup; message text uses waste_tokenize, where control-looking strings remain ordinary text. Segments are never concatenated or merged before tokenization: doing so would both permit prompt-structure injection and change BPE boundaries relative to K3's reference encoder.

The renderer emits, in order, tool declarations, thinking controls, messages, tool-choice and response-format instructions, then an open think or response element. Images become XTML media blocks whose <|media_pad|> token expands to the number of embeddings produced by the tower.

On output, serve/regions.py parses structural token IDs rather than scanning text. It incrementally separates reasoning_content, answer content, and tool calls, which allows the same parser to produce blocking responses and SSE deltas. A disconnected streaming client cancels generation immediately.

See SERVE.md for supported fields, endpoint behavior, security constraints, differential tests, and the full rendering protocol.

chat.json — the conversation format

waste chat addresses an instruct model in the format it was trained on. That format is read from chat.json inside the container, next to manifest.json. Without one the CLI says so and continues raw, which is deliberate: a guessed format is worse than a visible absence.

chat.json here is the ChatML layout, which is what a large part of the instruct ecosystem uses. Copy it into a container and edit the strings:

cp examples/chat.json ~/models/some-model.waste/chat.json

Every field is optional. Each role is a [prefix, suffix] pair, and open is what is appended after the last user turn to hand the floor to the model:

{"system":    ["<prefix>", "<suffix>"],
 "user":      ["<prefix>", "<suffix>"],
 "assistant": ["<prefix>", "<suffix>"],
 "open":      "<what starts the model's turn>"}

\n and \t are the escapes the reader understands. Whatever markup you put in these strings has to exist in the tokenizer as a single token, or it will be split into ordinary text and the model will not recognize it — waste tokenize MODEL "<|im_start|>" is the check, and the container's specials.json is the list of what is available.

chat-k3.json — Kimi K3

chat-k3.json is K3's own format, transcribed from encoding_k3.py in the release. tools/convert.py installs it for you when it recognises K3, so a fresh container already answers questions instead of continuing text. It is here for containers converted before that, and for editing:

cp examples/chat-k3.json ~/models/k3.waste/chat.json

The converter never overwrites a chat.json that is already there, so an edited one survives a re-conversion.

Neither Kimi release ships a Jinja template, which is why the converter has nothing to copy: K3 does not have one. It builds the prompt with a Python program (encoding_k3.py) that emits a token sequence directly, in XTML — an XML-like markup where the angle brackets are three reserved special tokens, with a fourth as the stop marker:

in the reporttokenid
[open]<|open|>163587
[sep]<|sep|>163589
[close]<|close|>163588
[end_of_msg]<|end_of_msg|>163586

A turn is a message element with a role attribute:

<|open|>message role="user"<|sep|> …content… <|close|>message<|sep|><|end_of_msg|>

and the model is handed the floor with an unclosed assistant message:

<|open|>message role="assistant"<|sep|><|open|>response<|sep|>

Everything that is not a control token — message, role="user", the tag names — is ordinary text, which is what lets a format this structured fit four prefix/suffix strings at all.

What this template leaves out

encoding_k3.py is 647 lines and this file covers the text conversation. Not covered: tool definitions and tool results, which are their own XTML elements with typed argument children; response_format and JSON schemas, which the program injects as synthetic system messages; and the think channel. K3 opens <|open|>think<|sep|> before the response when thinking is enabled, and the technical report measures reasoning at up to 73% of the tokens in a request — at this engine's speeds that is hours before an answer starts, so this template asks for the response channel directly. Set the open field to …<|open|>think<|sep|> if you want the reasoning, and expect to wait for it.

All of it lives in serve/, which is where a format this big belongs: four prefix/suffix strings cannot express a typed argument list, and a C engine should not be growing a JSON Schema renderer. serve/xtml.py is a port of encoding_k3.py, checked against it segment for segment, and serve/regions.py is the other half of that program — reading the reply back into reasoning, answer and tool calls. Both are reachable over HTTP:

python3 -m serve ~/models/k3.waste

See docs/SERVE.md. This file remains the answer for waste chat and waste run, which carry no Python.