# Chat Templates & Interactive Chat Pantheon v0.3.0 adds multi-turn interactive chat with automatic template detection. Each model family uses a different chat template — Pantheon detects the right one from GGUF metadata and formats messages with correct special tokens. ## Quick Start ```bash ./pantheon --model ../models/base/SmolLM2-135M-Instruct-Q8_0.gguf \ --chat --system "You are a helpful assistant." ``` ``` Chat mode (ChatML). Type /quit to exit, /clear to reset. > What is the capital of France? The capital of France is Paris. > And Germany? The capital of Germany is Berlin. > /quit ``` ## CLI Flags | Flag | Description | |------|-------------| | `--chat` | Enter interactive chat mode | | `--system ` | System prompt (optional, chat mode only) | | `--max-tokens ` | Max tokens per response (default: 128) | Chat mode commands: | Command | Action | |---------|--------| | `/quit` or `/exit` | Exit chat | | `/clear` | Reset conversation (clears KV cache, starts fresh) | ## Supported Templates | Style | Models | System Prompt | |-------|--------|---------------| | ChatML | Qwen2.5, SmolLM2 | Yes | | Mistral | Mistral v0.x, Mixtral | No | | Phi-3 | Phi-3-mini | Yes | Detection is automatic — Pantheon reads the `tokenizer.chat_template` Jinja2 string from GGUF metadata and matches on key markers. Falls back to architecture-based detection if the template is missing. ## How It Works ### The Special Token Problem BPE models (Qwen, SmolLM2) have "added tokens" like `<|im_start|>` that exist in the vocabulary but are **not produced by BPE merge rules**. Passing `"<|im_start|>user\nHello"` through the tokenizer splits `<|im_start|>` into individual characters — wrong. Pantheon solves this by splitting formatted messages into **special** and **text** segments internally. Special tokens are looked up by exact string via `ft_token_to_id()`, text segments are encoded normally. The result is a correct `uint32_t[]` token array. SentencePiece models (Mistral, Phi-3) don't have this problem — their Viterbi algorithm matches special tokens from the vocab naturally. ### Multi-Turn Flow 1. **Turn 0:** Format `system + user + assistant_prefix` → prefill → generate until EOS 2. **EOS sampled** but not fed through forward pass (existing behavior) 3. **Turn 1+:** Format `EOT + user + assistant_prefix` → prefill from current position → generate until EOS 4. **KV cache persists** across turns — no recomputation of earlier turns ### Template Formats **ChatML** (Qwen2.5, SmolLM2): ``` <|im_start|>system {system}<|im_end|> <|im_start|>user {user}<|im_end|> <|im_start|>assistant {response}<|im_end|> <|im_start|>user {next_user}<|im_end|> <|im_start|>assistant ``` **Mistral** (Mistral, Mixtral): ``` [BOS][INST] {user} [/INST]{response} [INST] {next_user} [/INST] ``` **Phi-3**: ``` <|system|> {system}<|end|> <|user|> {user}<|end|> <|assistant|> {response}<|end|> <|user|> {next_user}<|end|> <|assistant|> ``` ## C API ### `pn_stream_tokens()` — Pre-tokenized Streaming ```c int pn_stream_tokens(pn_model *model, pn_state *state, const uint32_t *tokens, int n_tokens, int max_tokens, pn_sampling_params *params, pn_token_callback callback, void *user_data); ``` Like `pn_stream()` but takes pre-tokenized input. No BOS added — caller controls tokenization entirely. Returns number of tokens generated, or -1 on error. ### Chat Template Functions (`src/core/chat.h`) ```c /* Detect style from Jinja2 template string */ pn_chat_style pn_chat_detect_style(const char *jinja_template); /* Fallback: detect from architecture enum */ pn_chat_style pn_chat_style_from_arch(int arch); /* Format first turn (system + user + assistant prefix) */ pn_token_seq pn_chat_format_first(pn_chat_style style, ft_tokenizer *tok, const char *system, const char *user); /* Format continuation turn (EOT + user + assistant prefix) */ pn_token_seq pn_chat_format_turn(pn_chat_style style, ft_tokenizer *tok, const char *user); ``` ### Embedding Chat in Your App ```c #include "pantheon.h" #include "src/core/engine.h" #include "src/core/chat.h" pn_model *model = pn_load("model.gguf"); pn_state *state = pn_state_new(model); pn_sampling_params params = pn_sampling_defaults(); pn_chat_style style = (pn_chat_style)model->chat_style; ft_tokenizer *tok = model->tokenizer; /* First turn */ pn_token_seq seq = pn_chat_format_first(style, tok, "You are helpful.", "Hello"); pn_stream_tokens(model, state, seq.tokens, seq.n_tokens, 128, ¶ms, on_token, NULL); free(seq.tokens); /* Second turn — KV cache preserved automatically */ seq = pn_chat_format_turn(style, tok, "Tell me more."); pn_stream_tokens(model, state, seq.tokens, seq.n_tokens, 128, ¶ms, on_token, NULL); free(seq.tokens); /* Reset for new conversation */ pn_state_reset(state); ``` ## Tests 20 tests in `tests/test_chat.c` covering style detection (5), architecture mapping (6), style names (1), ChatML formatting (3), Phi-3 formatting (2), Mistral formatting (2), and passthrough (1). ```bash make USE_BLAS=1 test # runs all 50 tests including chat ```