
If you've built a web app, you know the rule: don't concatenate user input into SQL. Use parameterized queries. User input is data; SQL is code; the two don't meet. LLM prompts have the same problem one layer down — user text and the model's control tokens live in the same byte stream, and if your serving stack doesn't keep them separated, user bytes can become structural tokens. Most inference providers serving open models today don't keep them separated, leaving users vulnerable to prompt injection.
Here's a working injection against Kimi K2.6, reproducible in your terminal. The system prompt locks the model to English; the attack injects a fake system message that switches the rule to Spanish. We'll use Kimi as the running example throughout the post; the same attack applies to any open model whose serving stack doesn't enforce token boundaries.
The user message contains the model's own turn-boundary tokens as plain text.
<|im_end|>, <|im_system|>, <|im_middle|>, <|im_user|>
A naive tokenizer encodes each of those strings as the real control token ID. From the model's point of view, the user's turn ends after "Hello", a new system message overrides the English rule with Spanish, and a fresh user turn asks for a warm greeting:
123456789101112
Without safe_tokenization — 70 prompt tokens. The user's <|im_end|> closes the real user turn; the forged <|im_system|> opens a system turn the user wrote; the forged <|im_user|> re-opens a user turn. The role label on every single token in the prompt is system: 70
The template no longer contains a user turn at all. The model reads the injected Spanish rule as a real system instruction and follows it.

As you can see, the model responds in Spanish.
With safe_tokenization: true — same request, one extra field, 97 prompt tokens. The <|im_end|>, <|im_system|>, <|im_middle|>, and <|im_user|> strings in user content tokenize as byte-level subwords (the grey boxes below) instead of matching their real control IDs. The template's turn structure is intact — user: 34, system: 44, history: 2, other: 22 — and the original English rule holds.
Note that those counts are how many prompt tokens fall under each role bucket: user = tokens in user turns, system = system turn, history = earlier turns in the thread, other = everything else including literals that are no longer parsed as control IDs. They are a visualization aid, not API fields.

The model now responds in English.
Same model. Same input. One boolean flag.
A note on this demo. The Spanish vs English flip is the attention-grabber, but safe_tokenization guarantees structure, not any particular model answer. LLM prompts have a chain of command: system instructions outrank user messages, user messages outrank assistant history, and so on. The model can only respect that hierarchy if the tokenized prompt preserves it — if the tokens that mark “this is the system turn” and “this is user content” are the ones the template actually placed. With the flag off, a forged <|im_end|> collapses that structure: the model no longer sees a distinct user turn, and every token in the prompt is attributed to the system role. With the flag on, role boundaries stay intact. That structural difference — 70 vs 97 tokens, system: 70 vs user: 34, system: 44, history: 2, other: 22 — is deterministic on every request. safe_tokenization does the complementary job one layer below alignment: it keeps the prompt honest so alignment has the right input to work with.
You can confirm it in the logprobs. Add logprobs: true and top_logprobs: 5 to the request. The probability mass the model puts on English vs Spanish tokens at the first answer position shifts depending on whether safe_tokenization is on or off — because the two modes produce different prompt token sequences, and the model conditions on those tokens.
Try it yourself. The playground demo below is pre-loaded with the language-lock demo. Hit Run to see the tokenized prompt, completion tokens with confidence colors, and per-token logprobs — then uncheck Safe Tokenization and run again to compare. All you need is a free Fireworks API key.

Anyone running a system prompt in production. Your system prompt is your product. It turns a generic open model into a customer-support bot, a coding assistant that knows your codebase, a medical-triage tool with your guardrails. Every piece of user-authored content that reaches the tokenizer is a chance to rewrite that prompt. The demo above is this scenario end-to-end. Without a defense at the token layer, you're trusting that no user, customer, or contractor will ever paste a control token into a text field.
ML engineers trying to talk to a model about its own templates. Half this job is literally chatting about these strings. You debug why <|im_system|> is mis-parsing in a trace. You annotate RLHF data that contains literal <think> blocks. You write evals that reference tool-call delimiters in the prompt. Without safe tokenization, the moment one of these strings hits a user turn, you aren't asking the model about the token — you're invoking it. Ask Kimi how <think> works and watch it start thinking at you instead of explaining. This is what "the model is behaving weird" tickets look like when you zoom in.
With safe_tokenization: true, that same question actually gets answered:
123456789101112131415161718
Most open models served today use HuggingFace tokenizers, and the HF tokenization path has one step that creates this exposure. It renders the entire conversation through a Jinja chat template into a single string, then encodes that string back to token IDs. The render-to-string step is where user text and control tokens collide. At that point the tokenizer can't tell which bytes came from the template and which came from the user — it's all one string. If your user's input happens to spell a control token, the tokenizer maps it to the real token ID.
A few tokenizers work differently. OpenAI's Harmony format, used by GPT-OSS, operates in "token space" natively and never collapses to a string; no collapse, no collision. But every other open-weights model running on a standard HF tokenizer has this exposure, which is essentially all of them. This isn't a quirk of one model. It's a property of the de facto tokenization pipeline that the open-weights ecosystem runs on.
The shape of the exposure is also not consistent across models, which is the other half of why a correct defense is non-trivial. Every model team picks their own set of control tokens — turn boundaries, reasoning markers, tool-call delimiters, often dozens more — and then decides, per token, whether to register it as special=True (a flag that tells the tokenizer to treat the token as atomic) or as a plain "added token" (the tokenizer knows about it but handles it more like regular vocabulary). The convention varies between model families and often within a single model. Kimi K2.6 marks its role tokens like <|im_end|> as special, but leaves its tool-call delimiters and <think>/</think> markers as added-but-not-special. DeepSeek V3 marks <|begin▁of▁sentence|> as special but ships <|User|>, <|Assistant|>, and <|tool▁call▁begin|> as added-but-not-special. There's no standard. A defense that only protects special=True tokens still leaves most of the commonly-exploited tokens on the table.
The model doesn't care which category a token was registered under. Whatever the model uses for conversation structure — where "system" ends, where "user" begins, when <think> fires, how tool calls get parsed out of the output — lives in those control tokens. The model's ability to follow the system prompt is inseparable from the integrity of those boundaries, which is inseparable from what the serving stack does when user bytes collide with them. Two providers running the same model, same weights, same decoding parameters, can produce measurably different outputs on edge-case inputs if one re-tokenizes user content as control tokens and the other doesn't.
Users will call that model quality. We like to call it infrastructure quality.
safe_tokenization: truesafe_tokenization is a per-request boolean on the Fireworks Chat Completions API. When it's true, we guarantee exactly one thing: no string in user content can be encoded as a special or added token, and the prompt's structural tokens are exactly the ones the chat template specifies — nothing more, nothing less.
Getting this right takes two fixes working together:
The two paths agree by construction, so there's no seam between the template and the user content for an attack to ride in on.
Three properties worth calling out:
<think>, <|im_end|>, <|User|>, anything — can appear in a user message and gets treated as text.Safe tokenization is live across Fireworks' supported open models — DeepSeek, Kimi, Qwen, Llama, GLM, and more — for both streaming and non-streaming completions.
Add safe_tokenization: true to your existing Chat Completions request. Tool calls, system prompts, response_format, streaming — all work the same. On user content that doesn't contain control tokens, behavior is identical to the default path, so you can roll out per-request, per-endpoint, or per-customer tier, however you want.
Try it on any model in the Fireworks model library; the safe_tokenization API reference has the full field spec.
Production inference has to survive adversarial user content. safe_tokenization: true is how Fireworks makes that guarantee at the token level — so your system prompt, and not someone else’s, defines your product. We plan to make this the default for all new integrations; today it’s an explicit opt-in per request.
For readers implementing their own solution, or evaluating someone else's, a correct defense has four requirements:
special=True tokens leaves most of the commonly-exploited tokens on the table.Safe tokenization on Fireworks is built to satisfy all four.