diff --git a/kimi_k2_reasoning_parser.py b/kimi_k2_reasoning_parser.py index 1a43eaf..2a2b9c7 100644 --- a/kimi_k2_reasoning_parser.py +++ b/kimi_k2_reasoning_parser.py @@ -249,6 +249,13 @@ class KimiK2ReasoningParser(ReasoningParser): if self.is_reasoning_end(previous_token_ids): # Strip any residual think tags that might appear in content cleaned = self._strip_think_tags(delta_text) + if not cleaned: + return None + # If tool-calls section markers are present, suppress them + # from content — the tool parser handles them via current_text + # re-parsing and does not need them forwarded as content. + for variant in self._tool_section_start_variants: + cleaned = cleaned.replace(variant, "") return DeltaMessage(content=cleaned) if cleaned else None # ── Check for in this delta ── @@ -270,15 +277,13 @@ class KimiK2ReasoningParser(ReasoningParser): tool_idx = self._find_tool_section_start(delta_text) if tool_idx != -1: reasoning = self._strip_think_tags(delta_text[:tool_idx]) - # Forward the tool section marker as content so the tool - # parser can detect it. - content = delta_text[tool_idx:] - + # Do NOT forward the tool section marker as content. The + # tool parser detects it via current_text re-parsing on its + # own. Forwarding it causes double-handling and empty content + # deltas. kwargs = {} if reasoning: kwargs["reasoning"] = reasoning - if content: - kwargs["content"] = content return DeltaMessage(**kwargs) if kwargs else None # ── Still in reasoning — strip tag if present ── diff --git a/kimi_k2_tool_parser.py b/kimi_k2_tool_parser.py index 3128bdb..137860e 100644 --- a/kimi_k2_tool_parser.py +++ b/kimi_k2_tool_parser.py @@ -522,12 +522,17 @@ class KimiK2ToolParser(ToolParser): pre_section = current_text[len(previous_text):section_start_in_text] if pre_section.strip(): return DeltaMessage(content=pre_section) - return DeltaMessage(content="") + # No real content before the section — return None instead of + # an empty-string delta. Empty content deltas confuse clients + # that distinguish content=null from content="". + return None # Case 4: Inside an open tool section but tool calls aren't - # parseable yet — emit empty delta to keep the stream alive. + # parseable yet — return None. The serving layer will emit + # its own keep-alive if needed; we should not emit empty-string + # content deltas that pollute the response. if in_open_section: - return DeltaMessage(content="") + return None # Case 5: Section is closed and we're past it — forward any # new content that appeared after the section end marker.