Native tool-calling convention of Moonshot AI’s Kimi K2 family (moonshotai/Kimi-K2-Instruct and -Base, model_type: "kimi_k2", 1T-param MoE). It is a ChatML-like envelope built on a TikToken tokenizer (160K vocab): every turn is <|im_{class}|>{name}<|im_middle|>{body}<|im_end|>, and tool calls are emitted inside the assistant turn wrapped by a dedicated <|tool_calls_section_begin|>…<|tool_calls_section_end|> block. All control tokens are plain ASCII <|…|> forms (no fullwidth/unicode variants, unlike DeepSeek). An inference server turns the raw stream into OpenAI-style tool_calls with a parser: vLLM and SGLang both expose --tool-call-parser kimi_k2 (vLLM additionally requires --enable-auto-tool-choice). The chat template (a standalone chat_template.jinja since the 2025.8.11 update) injects the tool schemas and renders the per-turn markers.
This document was verified against the model card, the official docs/tool_call_guidance.md and docs/deploy_guidance.md (GitHub MoonshotAI/Kimi-K2), the raw chat_template.jinja and tokenizer_config.json from the HF repo (rendered locally for the byte-exact streams below), and the vLLM kimi_k2 tool parser source.
Special tokens
The five tool-call markers required for manual parsing, plus the ChatML envelope markers. Token IDs are from tokenizer_config.json (added_tokens_decoder).
| Token (verbatim) | ID | Purpose |
|---|---|---|
<|tool_calls_section_begin|> | 163595 | Opens the tool-call section inside an assistant turn |
<|tool_call_begin|> | 163597 | Opens one individual tool call |
<|tool_call_argument_begin|> | 163598 | Separates the tool-call ID from its JSON arguments |
<|tool_call_end|> | 163599 | Closes one individual tool call |
<|tool_calls_section_end|> | 163596 | Closes the tool-call section |
<|im_system|> | 163594 | Start marker for system-class turns (system, tool, tool_declare) |
<|im_user|> | 163587 | Start marker for a user turn |
<|im_assistant|> | 163588 | Start marker for an assistant turn |
<|im_middle|> | 163601 | Separates the role/name header from the message body |
<|im_end|> | 163586 | Ends any turn |
[BOS] | 163584 | Sequence-begin token (see notes; not emitted by the chat template) |
[EOS] | 163585 | Sequence-end token |
Notes on exactness:
- The five tool tokens use ASCII pipe
|(U+007C) and underscores; reproduce them exactly. There are no fullwidth pipe (|) or▁variants in Kimi K2. <|im_middle|>is the only envelope token whose ID (163601) is out of sequence with the others (163586–163599); a163600slot is unused.- Image inputs render via a content macro as the literal sequence
<|media_start|>image<|media_content|><|media_pad|><|media_end|>. These media markers appear in the template but are not registered inadded_tokens_decoder, so they tokenize as ordinary text rather than single special tokens. They are irrelevant to text tool calling and are listed here only for completeness.
Roles / channels / turn structure
Kimi K2 uses a ChatML-style envelope. Every message is rendered as:
<|im_{class}|>{name}<|im_middle|>{body}<|im_end|>- There are exactly three start-marker tokens, chosen by
role:user→<|im_user|>assistant→<|im_assistant|>- everything else (
system,tool, and the synthetictool_declare) →<|im_system|>
- The
{name}segment between the marker and<|im_middle|>ismessage.name or message.role. This is the only “channel”/sub-role label Kimi K2 has. For ordinary turns it is literallysystem,user, orassistant; for a tool-result turn it is the tool’sname(the function name) when supplied, otherwisetool; for the tool-schema turn it is the literaltool_declare. <|im_end|>terminates every turn. The chat template does not emit[BOS]/[EOS]; turn boundaries are purely<|im_*|>markers (the tokenizer is TikToken-based withadd_bos_token/add_eos_tokenunset, and the manual-parse flow feeds the rendered template straight to/completions).- Default system prompt: if the first message is not a
systemmessage, the template injects<|im_system|>system<|im_middle|>You are Kimi, an AI assistant created by Moonshot AI.<|im_end|>before the first turn. - Generation prompt: with
add_generation_prompt=Truethe template ends with<|im_assistant|>assistant<|im_middle|>, and the model generates from there. - Thinking/reasoning:
Kimi-K2-Instructis a “reflex-grade” model with no long thinking, so there is no reasoning channel in this format. (Thinking variants are handled separately — vLLM ships a distinctkimi_k2reasoning parser keyed on a</think>token — but that is out of scope for the Instruct tool-call format documented here.)
Tool definitions
Available tools are advertised in a single dedicated turn placed at the very top of the prompt (before any system/user turn), using the synthetic tool_declare sub-role under the <|im_system|> marker:
<|im_system|>tool_declare<|im_middle|>{TOOLS_JSON}<|im_end|>{TOOLS_JSON} is the standard OpenAI-style tools array serialized to JSON with compact separators (',', ':') (no spaces). The array elements are passed through verbatim, i.e. each is {"type":"function","function":{"name":…,"description":…,"parameters":{…}}} with a JSON-Schema parameters object. Example (single tool, exactly as emitted):
<|im_system|>tool_declare<|im_middle|>[{"type":"function","function":{"name":"get_weather","description":"Get weather information. Call this tool when the user needs to get weather information","parameters":{"type":"object","required":["city"],"properties":{"city":{"type":"string","description":"City name"}}}}}]<|im_end|>The tool_declare turn is rendered only when tools is non-empty.
Tool-call format
When the model decides to call a function, it emits — inside the assistant turn, after any natural-language content — a tool-calls section. Minimal single call (this is the assistant generation that follows <|im_assistant|>assistant<|im_middle|>):
<|tool_calls_section_begin|><|tool_call_begin|>functions.get_weather:0<|tool_call_argument_begin|>{"city": "Beijing"}<|tool_call_end|><|tool_calls_section_end|>Anatomy of one call:
<|tool_call_begin|> functions.{func_name}:{idx} <|tool_call_argument_begin|> {JSON arguments} <|tool_call_end|>- The token between
<|tool_call_begin|>and<|tool_call_argument_begin|>is the tool-call ID, with the fixed formfunctions.{func_name}:{idx}.functions.is a literal prefix (it is not derived from the tool schema).{func_name}is the called function’s name; the function name is recovered by parsing it back out of this ID, not from a separate field.{idx}is the 0-based call index within the current assistant turn (0for the first call,1for the second, …).
- After
<|tool_call_argument_begin|>comes the raw JSON arguments object (e.g.{"city": "Beijing"}), terminated by<|tool_call_end|>. - All calls of the turn live between one
<|tool_calls_section_begin|>/<|tool_calls_section_end|>pair. Any assistant text content precedes<|tool_calls_section_begin|>. - The whole assistant turn is still closed by
<|im_end|>and the completion’sfinish_reasonbecomestool_calls.
Multiple / parallel tool calls
Two or more calls in one turn are emitted as consecutive <|tool_call_begin|>…<|tool_call_end|> blocks inside a single section, with the index incrementing per call. Raw assistant emission for two parallel calls:
<|tool_calls_section_begin|><|tool_call_begin|>functions.get_weather:0<|tool_call_argument_begin|>{"city": "Beijing"}<|tool_call_end|><|tool_call_begin|>functions.get_weather:1<|tool_call_argument_begin|>{"city": "Shanghai"}<|tool_call_end|><|tool_calls_section_end|>Note the IDs functions.get_weather:0 and functions.get_weather:1 — same function, distinct trailing index. The index is per-turn (it resets to 0 in the next assistant turn).
Tool-result format
Tool execution results are fed back as a turn with role: "tool". Because tool is not user/assistant, it renders under the <|im_system|> marker; the sub-role label is the message’s name (the function name) when present, else tool. The body is a literal ## Return of {tool_call_id} header line followed by the result content:
<|im_system|>get_weather<|im_middle|>## Return of functions.get_weather:0
{"weather": "Sunny"}<|im_end|>{tool_call_id}echoes the exact ID from the originating call (functions.get_weather:0), which is how the model correlates a result with the call that produced it.- The result
contentis inserted verbatim on the line after the header; callers typically pass a JSON string (e.g.json.dumps(tool_result)). - If the
toolmessage omitsname, the envelope becomes<|im_system|>tool<|im_middle|>## Return of ….
End-to-end example
A complete multi-turn weather exchange. These are the exact rendered streams (system + user supplied explicitly; line breaks inside a turn are literal, turns are otherwise contiguous).
Stage 1 — prompt fed to the model (tools set, add_generation_prompt=True):
<|im_system|>tool_declare<|im_middle|>[{"type":"function","function":{"name":"get_weather","description":"Get weather information. Call this tool when the user needs to get weather information","parameters":{"type":"object","required":["city"],"properties":{"city":{"type":"string","description":"City name"}}}}}]<|im_end|><|im_system|>system<|im_middle|>You are Kimi, an AI assistant created by Moonshot AI.<|im_end|><|im_user|>user<|im_middle|>What's the weather like in Beijing today? Use the tool to check.<|im_end|><|im_assistant|>assistant<|im_middle|>Assistant generation (model output; server reports finish_reason: "tool_calls"):
<|tool_calls_section_begin|><|tool_call_begin|>functions.get_weather:0<|tool_call_argument_begin|>{"city": "Beijing"}<|tool_call_end|><|tool_calls_section_end|><|im_end|>Stage 2 — prompt for the next turn, after appending the assistant tool-call turn and the tool result turn (add_generation_prompt=True):
<|im_system|>tool_declare<|im_middle|>[{"type":"function","function":{"name":"get_weather","description":"Get weather information. Call this tool when the user needs to get weather information","parameters":{"type":"object","required":["city"],"properties":{"city":{"type":"string","description":"City name"}}}}}]<|im_end|><|im_system|>system<|im_middle|>You are Kimi, an AI assistant created by Moonshot AI.<|im_end|><|im_user|>user<|im_middle|>What's the weather like in Beijing today? Use the tool to check.<|im_end|><|im_assistant|>assistant<|im_middle|><|tool_calls_section_begin|><|tool_call_begin|>functions.get_weather:0<|tool_call_argument_begin|>{"city": "Beijing"}<|tool_call_end|><|tool_calls_section_end|><|im_end|><|im_system|>get_weather<|im_middle|>## Return of functions.get_weather:0
{"weather": "Sunny"}<|im_end|><|im_assistant|>assistant<|im_middle|>Final assistant generation (model produces natural-language answer terminated by <|im_end|>; finish_reason: "stop"):
It's sunny in Beijing today.<|im_end|>OpenAI-compatible API mapping
With a server parser active (--tool-call-parser kimi_k2), the raw stream maps onto the Chat Completions shape as follows:
-
choices[].finish_reason="tool_calls"when the turn contained a tool-calls section (otherwise"stop"). -
choices[].message.tool_calls[]— one entry per<|tool_call_begin|>…<|tool_call_end|>block:.id= the raw call ID verbatim, e.g."functions.get_weather:0"..type="function"..function.name= the function name parsed out of the ID. vLLM computesid.split(":")[0].split(".")[-1]→"get_weather"..function.arguments= a JSON string (the raw text captured between<|tool_call_argument_begin|>and<|tool_call_end|>), e.g."{\"city\": \"Beijing\"}". Clientsjson.loads()it before use.
-
Tool results are sent back as messages of the form:
{"role": "tool", "tool_call_id": "functions.get_weather:0", "name": "get_weather", "content": "{\"weather\": \"Sunny\"}"}tool_call_idmust equal theidreturned for the call;namebecomes the<|im_system|>{name}<|im_middle|>sub-role;contentbecomes the body after## Return of …. -
Streaming: deltas arrive as
choices[].delta.tool_calls[]with anindex; the functionname/idstream once the call header is complete, thenfunction.argumentsstreams as incremental string fragments to be concatenated (standard OpenAI tool-call streaming assembly).
Moonshot’s hosted API (platform.moonshot.ai) exposes both OpenAI- and Anthropic-compatible endpoints; the Anthropic-compatible one scales temperature as real_temperature = request_temperature * 0.6. Recommended sampling temperature for Kimi-K2-Instruct is 0.6.
Parsing notes & gotchas
- ID → name parsing differs between references. The official
tool_call_guidance.mdextracts the name withfunction_id.split('.')[1].split(':')[0], which assumes the ID is exactlyfunctions.{name}with no extra dots. vLLM uses the more robustfunction_id.split(":")[0].split(".")[-1](takes the last dot-segment before:{idx}). Prefer the vLLM form so function names containing.are handled. - Extraction regexes differ too. Guidance:
<\|tool_call_begin\|>\s*(?P<tool_call_id>[\w\.]+:\d+)\s*<\|tool_call_argument_begin\|>\s*(?P<function_arguments>.*?)\s*<\|tool_call_end\|>. vLLM: ID class is[^<]+:\d+and the argument body uses a negative lookahead(?:(?!<\|tool_call_begin\|>).)*?so adjacent calls aren’t merged. Both run withDOTALL. skip_special_tokensmust be False. The parser depends on the literal marker text surviving detokenization; vLLM forcesskip_special_tokens = Falsewhen tools are enabled andtool_choice != "none". If markers are stripped, no tool call is detected.- Arguments are unvalidated raw text. Whatever the model emits between the argument marker and
<|tool_call_end|>is passed straight through as theargumentsstring; it must be valid JSON for downstreamjson.loads, and the model can emit malformed/truncated JSON. Validate before executing. - Index semantics.
{idx}is the per-turn call counter starting at0; it is not a global counter and resets each assistant turn. Do not assume IDs are unique across turns — disambiguate by turn when persisting history. - Streaming marker splits. Section and call markers can be split across token boundaries. vLLM holds back any trailing suffix that partially matches a marker (
partial_tag_overlap) to avoid leaking marker bytes into streamed content, and only streams a call’s name once its header is fully received. finish_reasonvaries by engine. The official guide explicitly warns the terminalfinish_reasonfor tool calls “may vary across different engines”; loop onfinish_reason == "tool_calls"but be defensive.- Engine fallback. Kimi K2 reuses the DeepSeek-V3 architecture;
config.jsonsetsmodel_type: "kimi_k2"so engines apply the right parser. If you forcemodel_type: "deepseek_v3"as a compatibility workaround, no native Kimi tool parser is available and you must parse the<|tool_calls_section_*|>markers manually. - Parser availability. vLLM ships both a Python (
KimiK2ToolParser) and a newer Rust tool parser; SGLang implements its ownkimi_k2parser. All key off the same five markers and thefunctions.{name}:{idx}ID convention documented here. - Whitespace artifact. When no
systemmessage is supplied, the template injects the default system prompt and a small\n(newline + two spaces) can appear before the first<|im_user|>marker. It is harmless (tokenizes around the markers), but supplying an explicit system message yields the clean streams shown above.
Sources
- Model card (Tool Calling section, OpenAI-style example, deployment/API notes): https://huggingface.co/moonshotai/Kimi-K2-Instruct
- Official tool-call guidance (markers, ID convention, manual parser,
extract_tool_call_info): https://raw.githubusercontent.com/MoonshotAI/Kimi-K2/main/docs/tool_call_guidance.md (the HFresolve/blobpaths redirected to the model card; verified against this GitHub raw file) - Deployment guide (
--tool-call-parser kimi_k2,--enable-auto-tool-choice, SGLang flag,model_typefallback): https://raw.githubusercontent.com/MoonshotAI/Kimi-K2/main/docs/deploy_guidance.md - Chat template (
chat_template.jinja, rendered locally for byte-exact streams): https://huggingface.co/moonshotai/Kimi-K2-Instruct/resolve/main/chat_template.jinja - Tokenizer config (special-token IDs in
added_tokens_decoder): https://huggingface.co/moonshotai/Kimi-K2-Instruct/resolve/main/tokenizer_config.json - vLLM
kimi_k2tool parser (markers, regex, name-parsing,skip_special_tokens, streaming): https://github.com/vllm-project/vllm/blob/main/vllm/tool_parsers/kimi_k2_tool_parser.py - vLLM PR adding the parser: https://github.com/vllm-project/vllm/pull/20789
- vLLM tool-calling docs: https://docs.vllm.ai/en/latest/features/tool_calling/