执行摘要
- 一句话:Kimi K3 推理与工具调用解析及 OpenAI 服务支持
- 推荐动作:值得精读。建议关注 4 个设计点:(1)
kimik3_structural_tag.py 的 JSON Schema → XTML 双格式翻译与 OrFormat 变体构造;(2) KimiK3Detector(function_call 与 reasoning_parser 两个)的流式状态机与 buffer / 游标重置契约;(3) TokenSequenceMatcher(KMP 前缀函数)在 grammar 回滚与多 token marker 匹配中的用途;(4) #33192 的约束失败降级策略——“不强制 JSON-only schema” 的取舍。若要接入新的专用 token 格式模型,这是当前最完整的参考实现。
功能与动机
PR body 明确说明拆分动机:"This extracts the parser and directly related OpenAI serving work from #32541 so the model-support PR has a smaller review surface. The parser, constraint, auto-detection, Chat Completions, and Responses pieces are presented as one pull request because they share the same Kimi K3 wire format and need end-to-end validation together." 需要解决的三个问题:(1) Kimi K3 使用 XTML 结构化通道(think / response / tools),每个 marker 都是多 token 特殊序列,现有 parser 无法处理;(2) xgrammar 结构约束必须贴合 K3 原生编码,否则强制 JSON-only schema 时模型输出无法被解析器读回;(3) 字面 <|kimi_image_placeholder|> 文本会被上游渲染器当图像槽位,text-only 场景直接 400(issue #32960),PR 同时并入 #33003 的占位符中和修复。
实现拆解
-
wire 格式常量层(新增 python/sglang/srt/function_call/kimik3_format.py):定义 THINK_OPEN / THINK_CLOSE / RESPONSE_OPEN / RESPONSE_CLOSE / TOOLS_OPEN / TOOLS_CLOSE / MESSAGE_CLOSE 等 XTML marker 常量,并提供 3 个流式工具函数——partial_suffix_len(计算文本尾部与任一 marker 前缀的最大重叠长度)、strip_partial_marker_suffix(剥离因 max_tokens 截断残留的半截 marker)、strip_response_wrappers(剥掉 response / message 包装)。这是流式解析正确性的地基:任何 marker 都可能跨 chunk 截断。
-
结构约束层(新增 kimik3_structural_tag.py,+598 行):把 OpenAI Tool 的 JSON Schema 翻译成 xgrammar StructuralTag。关键路径是 _schema_types(推导 schema 可接受的 JSON 类型集合,处理 $ref / anyOf / oneOf / allOf / const / enum 及关键字启发)、_restrict_schema_type(按单个 JSON 类型收窄 schema,保证各类型变体互斥)、_argument_value_variants 与 _known_argument_format(为每个类型生成 TagFormat,string 用 qwen_xml 风格、其余用 JSONSchemaFormat,多类型时包成 OrFormat)。对外暴露 get_kimik3_structural_tag 与 get_kimik3_auto_tool_call_structural_tag,支持 auto / required / named 三种 tool choice、parallel_tool_calls 与 SGLANG_TOOL_STRICT_LEVEL。
-
工具调用解析层(新增 kimik3_detector.py,+234 行):KimiK3Detector 用正则提取 tools 通道中的 call / argument 块,_parse_attrs 反转义属性(& / "),string 参数保留原文、其余类型 JSON 解码(失败回退字符串)。提供一次性 detect_and_parse 与流式 parse_streaming_increment 两套接口;流式实现依赖 buffer 与 _sent_normal_idx 游标,异常路径按“重置 buffer 必须同步重置游标”的契约处理,否则后续输出会被静默丢弃。supports_structural_tag() 返回 True,parses_required_natively() 返回 False(配合 #33192 降级策略)。
-
推理解析层(修改 reasoning_parser.py,+188 行):新增同名 KimiK3Detector(职责为解析 think 通道)并注册进 ReasoningParser.DetectorMap 的 kimi_k3 项。处理 3 个边界:缺省 <|sep|> 的结束 marker 恢复、response / message 包装剥离、tools 通道原样透传给 tool-call 检测器。配套新增 utils/token_sequence_matcher.py(KMP 前缀函数)并改造 constrained/reasoner_grammar_backend.py:支持预算耗尽时逐步行走完整多 token think-end 序列、回滚后 marker 存活;schedule_batch.py::update_reasoning_tokens 改为等待完整 end 序列出现后才计入 reasoning token。
-
OpenAI 服务层(修改 serving_chat.py,+272/-42):_prepare_kimi_k3_messages 统一做消息预处理——文本与结构化字段(含 reasoning_content、tool_calls arguments)递归中和 <|kimi_image_placeholder|>、developer 消息改写为 system、system / developer 消息级 tools 透传;_effective_tools 汇总请求级与消息级工具,使校验、约束构建与流式工具处理口径一致;image_prompts 仅在确有图片时设置,避免 [] 触发上游渲染器 scan-and-consume 模式;normalize_assistant_tool_call_arguments 增加 strict 参数,K3 渲染路径允许畸形历史参数原样通过;结构约束构建失败时不强制 JSON-only schema,保留 K3 原生 stop 格式(TOOLS_CLOSE,即 #33192)。serving_responses.py 同步传递 reasoning 状态与 chat encoder 字段,protocol.py 增加 _has_message_level_tools 逻辑。
-
测试与验证:新增 test_kimik3_structural_tag.py(810 行,用 xgrammar 的 accept-string 校验 grammar 接受 / 拒绝)、test_kimik3_detector.py(221 行,覆盖单 / 多 call、未闭合 tools、流式分块 1 / 7 / 23)、test_kimik3_reasoning_parser.py(162 行,覆盖缺 sep 恢复、部分 marker 剥离、流式 split、tools 透传);扩展 serving_chat / serving_responses / protocol / reasoner_grammar_backend 测试。PR body 报告 200 + 35 + 436 + 73 用例全绿;message-level tools 校验通过 default / dsv4 / dsv32 / kimi_k3 四种编码的 subtest 覆盖;另含 Python 3.10 兼容修复(f12f3ac)。
关键文件:
python/sglang/srt/function_call/kimik3_structural_tag.py(模块 结构约束;类别 source;类型 core-logic;符号 _escape_attr, _json_type, _matches_json_type, _resolve_local_ref): 本 PR 核心新增:把 OpenAI Tool 的 JSON Schema 翻译成 Kimi K3 原生 XTML 的 xgrammar StructuralTag,支撑 auto / required / named 三种 tool choice、per-tool 严格约束与 parallel_tool_calls。
python/sglang/srt/function_call/kimik3_detector.py(模块 工具解析;类别 source;类型 core-logic;符号 _unescape_attr, _parse_attrs, KimiK3Detector, has_tool_call): Kimi K3 XTML 工具调用检测与流式解析,承担 tools 通道提取、属性反转义与非 string 参数 JSON 解码,是 serving 层 tool-call 解析的落点。
python/sglang/srt/entrypoints/openai/serving_chat.py(模块 服务层;类别 source;类型 core-logic;符号 normalize_assistant_tool_call_arguments, neutralize_kimi_k3_image_placeholder, neutralize_kimi_k3_image_placeholder_value, _effective_tools): OpenAI Chat 服务层集成:消息预处理占位符中和、message-level tools 合并、image_prompts 守卫、约束失败降级与 wire 字段透传,是全模型核心入口。
python/sglang/srt/parser/reasoning_parser.py(模块 推理解析;类别 source;类型 core-logic;符号 KimiK3Detector, _clean_content, _next_channel_idx, detect_and_parse): 新增推理侧 KimiK3Detector 解析 think 通道并注册 kimi_k3,处理多 token marker 流式 holdback、缺 sep 恢复与工具通道透传。
python/sglang/srt/constrained/reasoner_grammar_backend.py(模块 语法约束;类别 source;类型 dependency-wiring): 多 token think-end 标记的预算耗尽行走与回滚存活改造,影响所有使用 reasoner grammar 的模型,而非仅 Kimi K3。
python/sglang/srt/function_call/kimik3_format.py(模块 格式常量;类别 source;类型 core-logic;符号 partial_suffix_len, strip_partial_marker_suffix, strip_response_wrappers): XTML marker 常量与 partial marker 处理工具,是流式解析正确性的地基,被 detector 与 reasoning parser 共同依赖。
python/sglang/srt/utils/token_sequence_matcher.py(模块 匹配工具;类别 source;类型 dependency-wiring;符号 TokenSequenceMatcher, _build_prefix_lengths, advance): KMP 前缀函数实现的多 token 序列匹配器,支撑跨 chunk 识别完整 think-end 标记并在 grammar 回滚中存活。
test/registered/unit/function_call/test_kimik3_structural_tag.py(模块 约束测试;类别 test;类型 test-coverage;符号 _tool, _argument, _call, _tools_section): 810 行 grammar 接受 / 拒绝测试,覆盖 strict schema、named tool choice、$ref / anyOf / allOf 组合、非严格工具与 loose string。
test/registered/unit/entrypoints/openai/test_serving_chat.py(模块 服务层测试;类别 test;类型 test-coverage;符号 test_process_messages_records_template_reasoning_state, test_kimi_k3_constraint_failure_keeps_native_stop_format, test_kimi_k3_tool_call_stop_is_scoped_to_active_tools, test_kimi_k3_encoder_receives_wire_request_fields): 服务层行为测试:reasoning 状态记录、约束失败保留原生 stop 格式、wire 字段透传、占位符中和与跨编码 message-level tools 校验。
关键符号:KimiK3Detector(function_call).parse_streaming_increment, KimiK3Detector(function_call).detect_and_parse, KimiK3Detector(function_call)._decode_call, get_kimik3_structural_tag, get_kimik3_auto_tool_call_structural_tag, _schema_types, _restrict_schema_type, _argument_value_variants, _known_argument_format, partial_suffix_len, strip_partial_marker_suffix, strip_response_wrappers, KimiK3Detector(reasoning_parser).parse_streaming_increment, KimiK3Detector(reasoning_parser)._drain_content, TokenSequenceMatcher.advance, _prepare_kimi_k3_messages, _effective_tools, neutralize_kimi_k3_image_placeholder_value, normalize_assistant_tool_call_arguments, update_reasoning_tokens
关键源码片段
python/sglang/srt/function_call/kimik3_structural_tag.py
本 PR 核心新增:把 OpenAI Tool 的 JSON Schema 翻译成 Kimi K3 原生 XTML 的 xgrammar StructuralTag,支撑 auto / required / named 三种 tool choice、per-tool 严格约束与 parallel_tool_calls。
def _argument_value_variants(
schema: Union[bool, Dict[str, Any]],
root_schema: Dict[str, Any],
loose_strings: bool = False,
) -> List[Tuple[str, Format]]:
# 对 schema 允许的每个 JSON 类型生成一个“类型标签 + 取值格式”变体:
# K3 的 argument 头里 type 属性是显式的,因此不同 JSON 类型必须拆成
# 互斥分支,最终由调用方包成 OrFormat。
return [
(
json_type,
_value_format(restricted, json_type, loose_string=loose_strings),
)
for json_type in _schema_types(schema, root_schema)
if (restricted := _restrict_schema_type(schema, json_type, root_schema))
is not False
]
def _known_argument_format(
key: str,
schema: Union[bool, Dict[str, Any]],
root_schema: Dict[str, Any],
) -> Optional[Format]:
# 已知参数:为每个 JSON 类型构造一个 TagFormat。begin 直接是 XTML 的
# <|open|>argument key="..." type="..."<|sep|>,内容格式取决于类型:
# string 用 qwen_xml 风格(原生 XTML 文本),其余类型用 JSON 风格。
escaped_key = _escape_attr(key)
variants = [
TagFormat(
begin=(
f'<|open|>argument key="{escaped_key}" '
f'type="{_JSON_TO_XTML_TYPE[json_type]}"<|sep|>'
),
content=value_format,
end=ARGUMENT_CLOSE,
)
for json_type, value_format in _argument_value_variants(schema, root_schema)
]
if not variants:
return None
if len(variants) == 1:
return variants[0]
return OrFormat(elements=variants)
python/sglang/srt/function_call/kimik3_detector.py
Kimi K3 XTML 工具调用检测与流式解析,承担 tools 通道提取、属性反转义与非 string 参数 JSON 解码,是 serving 层 tool-call 解析的落点。
def parse_streaming_increment(self, new_text: str, tools: List[Tool]) -> StreamingParseResult:
# 流式解析入口:先把新 chunk 追加进内部 buffer,再尝试从中提取完整
# tool call。XTML 的 marker 是多 token 特殊序列,可能跨 chunk 截断,
# 因此未闭合部分必须留在 buffer 里继续累积。
self._buffer += new_text
try:
open_idx = self._buffer.find(self.bot_token)
if open_idx == -1:
# 尚未出现 tools 通道开头,按普通文本输出;_emit_normal_text
# 内部用 partial_suffix_len 做 holdback,避免把 marker 前缀
# 拦腰吐给客户端。
return StreamingParseResult(normal_text=self._emit_normal_text())
normal_text = self._emit_normal_text(limit=open_idx)
section = self._buffer[open_idx + len(self.bot_token) :]
calls = []
parsed = self._parse_calls(section)
for call in parsed[self.current_tool_id + 1 :]:
self.current_tool_id += 1
while len(self.prev_tool_call_arr) <= self.current_tool_id:
self.prev_tool_call_arr.append({})
while len(self.streamed_args_for_tool) <= self.current_tool_id:
self.streamed_args_for_tool.append('')
self.prev_tool_call_arr[self.current_tool_id] = {
'name': call['name'],
'arguments': json.loads(call['arguments']),
}
self.streamed_args_for_tool[self.current_tool_id] = call['arguments']
calls.append(
ToolCallItem(
tool_index=self.current_tool_id,
name=call['name'],
parameters=call['arguments'],
)
)
return StreamingParseResult(normal_text=normal_text, calls=calls)
except Exception as e:
logger.error(
'Error in Kimi K3 parse_streaming_increment: %s', e, exc_info=True
)
# _sent_normal_idx 索引指向 _buffer,因此必须与 buffer 一起重置;
# 否则后续每次 _emit_normal_text 都会因 limit <= _sent_normal_idx
# 而静默丢弃响应剩余部分。
self._buffer = ''
self._sent_normal_idx = 0
return StreamingParseResult()
python/sglang/srt/entrypoints/openai/serving_chat.py
OpenAI Chat 服务层集成:消息预处理占位符中和、message-level tools 合并、image_prompts 守卫、约束失败降级与 wire 字段透传,是全模型核心入口。
def neutralize_kimi_k3_image_placeholder_value(value: Any) -> Any:
# 递归中和字符串、列表、字典中的字面占位符。上游 encoding_k3.py 渲染器
# 会对文本执行占位符扫描(scan-and-consume),用户消息里出现
# "<|kimi_image_placeholder|>" 会被误当成图片槽位(issue #32960);
# 替换成带空格的变体后,渲染器把它当作惰性纯文本处理。
if isinstance(value, str):
return neutralize_kimi_k3_image_placeholder(value)
if isinstance(value, list):
return [neutralize_kimi_k3_image_placeholder_value(item) for item in value]
if isinstance(value, dict):
return {
key: neutralize_kimi_k3_image_placeholder_value(item)
for key, item in value.items()
}
return value
def _effective_tools(self, request: ChatCompletionRequest) -> List[Tool]:
# 汇总请求级 tools 与 system / developer 消息上的级联 tools,让消息级
# 工具参与校验、约束构建与流式工具调用处理;这改变了默认编码的校验
# 口径,测试覆盖 default / dsv4 / dsv32 / kimi_k3 四种编码。
tools = list(request.tools or [])
for message in request.messages:
if (
isinstance(message, ChatCompletionMessageGenericParam)
and message.role in ('system', 'developer')
and message.tools
):
tools.extend(message.tools)
return tools
评论区精华
核心交锋集中在 issue #32960 的占位符 400 排查线程:
- tancheng33 定位到根因:
serving_chat.py 无条件执行 template_kwargs['image_prompts'] = ['<|media_pad|>'] * image_count,当 image_count == 0 时传入 [],使上游 encoding_k3.py 渲染器进入 scan-and-consume 模式并立即抛 ValueError;而 image_prompts=None 会把占位符渲染为惰性纯文本。
- hnyls2002 确认该修复来自 #33003 并已落地本 PR。
- tancheng33 进一步指出残留路径:上游渲染器还会对 assistant
tool_calls 参数(json_block 与 _xtml_value 分支)以及 reasoning_content 做同样的占位符扫描,这两条路径未经过 neutralize,text-only agentic 对话一旦出现引用字面占位符的工具参数,后续每一轮请求都会 400;建议补 if image_count: 守卫。
- tancheng33 最终验证 #33183 覆盖了全部残留路径:渲染前统一中和 reasoning / 嵌套工具参数,
image_prompts 重建时加 if image_count: 守卫并丢弃过期用户值。
另一项设计决策来自 #33192(作为本 PR 最后一个提交并入):KimiK3Detector 的 parses_required_natively() 返回 False,但最终策略是让模型原生 parser 处理 required tool choice;当结构约束构建失败时,保持 K3 输出不受限(原生格式)而不是退回 JSON-only schema——因为 K3 的解析器读不回 JSON-only 输出。
- image_prompts=[] 触发上游渲染器 scan-and-consume 导致 400(issue #32960) (correctness): hnyls2002 确认 #33003 的修复已落地;tancheng33 验证 #33183 以 if image_count: 守卫 + 丢弃过期用户值方式闭合全部路径。
- 占位符中和的残留路径:assistant tool_calls 参数与 reasoning_content (correctness): 提交 5f4f76c 先尝试修复后被 revert,最终经 #33183 以“渲染前统一中和 reasoning / 嵌套工具参数”方式落地。
- 约束失败时的降级策略(#33192) (design): 以提交 af3e2d6(#33192)并入,配套测试 test_kimi_k3_constraint_failure_keeps_native_stop_format 覆盖。
- 流式解析错误路径的 buffer 与游标重置契约 (correctness): 以提交与注释形式固化,流式分块测试 test_streaming_split_markers 覆盖 1 / 7 / 23 字节分块。
- strict-thinking 下 think_excluded_tokens 的选词约束 (design): 以注释形式固化在 KimiK3Detector(reasoning_parser) 的实现中,测试 test_kimi_k3_excluded_tokens_spare_the_xtml_control_tokens 覆盖。
风险与影响
- 风险:
- 全模型 serving 入口变更:
serving_chat.py 是 /v1/chat/completions 核心路径,normalize_assistant_tool_call_arguments 新增 strict 参数、_effective_tools 把消息级工具并入校验与流式处理,默认编码路径的校验口径变化会影响所有模型的服务行为(测试覆盖了 4 种编码,但真实模型组合仍有暴露差异的可能)。
- 语法后端跨模型影响:
reasoner_grammar_backend.py 的多 token marker 行走与回滚改造服务于所有 reasoner grammar 模型,不只是 Kimi K3;配套测试覆盖了 multi-token end marker 用例,但推理预算耗尽、自重叠 marker 等极端路径仍需生产验证。
- 流式解析状态机风险:
KimiK3Detector.parse_streaming_increment 的异常路径重置 buffer 与游标,语义是“丢弃未吐出的内容”,调用方必须依赖该契约;正则 _CALL_RE / _ARG_RE 对参数正文中出现 <|close|>call<|sep|> 原始序列的场景没有显式防护。
- 复杂 JSON Schema 翻译边界:
_schema_types / _restrict_schema_type 对 allOf / anyOf / $ref 组合使用集合运算收窄(含 number / integer 互通的特殊处理),逻辑自洽但极端嵌套 schema 仍可能有类型枚举与关键字启发不一致的边界。
- reasoning token 计数口径:
schedule_batch.py::update_reasoning_tokens 改为等待完整多 token think-end 序列后计数,影响 usage 统计口径,需与前端计费逻辑核对。
- 影响:
- 用户 / 模型侧:Kimi K3 用户可开箱使用推理内容(
reasoning_content)与原生工具调用,支持 parallel_tool_calls、per-tool 严格 schema、SGLANG_TOOL_STRICT_LEVEL、消息级工具与 named tool choice;字面 <|kimi_image_placeholder|> 不再被误判为图像输入(#32960)。
- 系统侧:新增 4 个模块(kimik3_format / kimik3_structural_tag / kimik3_detector / token_sequence_matcher),改动 5 个高复用入口(serving_chat、serving_responses、reasoning_parser、reasoner_grammar_backend、schedule_batch);
ReasoningParser.DetectorMap 与 chat_encoding_spec 的自动检测机制降低了后续模型接入成本。
- 团队侧:形成“wire 常量 + schema 翻译 + 检测器 + 推理解析 + serving 接线”的完整接入范式,后续新专用 token 格式模型可复制该结构。
- 风险标记:核心服务路径变更, 跨模型语法后端影响, 流式解析状态机风险, 正则解析边界, 复杂 schema 翻译边界, reasoning token 计数口径变化
关联脉络
- PR #33192 [Kimi K3] Keep native tool-call format when constraints fail: 作为本 PR 最后一个提交(af3e2d6)并入,解决结构约束构建失败时误强制 JSON-only schema 导致解析器读不回的问题。
- PR #33183 Harden Kimi K3 parser edge cases: 作为提交 6658b91 并入(co-authored 与 A-transformer),覆盖占位符中和残留路径与 image_prompts 守卫,闭合 issue #32960。
- PR #33003 Neutralize literal kimi_image_placeholder text(标题据 PR body 描述补全): PR body 明确说明其占位符中和修复已并入本 PR,是 #32960 的根因修复。
- PR #32541 Kimi K3 model support(本 PR 从中拆分 parser 与 serving,标题据 body 描述): PR body 说明本 PR 从中提取 parser 与 OpenAI serving 工作,以缩小模型支持 PR 的审查面。
- PR #32768 Kimi K3 parser / serving draft(被本 PR 取代,标题据 body 描述): PR body 声明 Supersedes #32768 及临时堆叠草稿 #33017、#33018、#33019、#33020。
- PR #32828 [Kimi] Support DCP + DSpark (ported from kimi-k3 branch): 同属 Kimi 模型族功能线,且都修改了 speculative / serving 相关链路,可视为 Kimi 系列支持的延续。
参与讨论