Fix empty content deltas and leaked section markers in streaming
Tool parser: - Case 3/4: return None instead of DeltaMessage(content='') when inside an open tool section with no parseable content yet. Empty-string content deltas pollute the response and break the content=null vs content='' contract with non-streaming. Reasoning parser: - Suppress tool-calls section markers from content forwarding. The tool parser detects them via current_text re-parsing; forwarding them as content causes double-handling. - Already-past-reasoning path: strip section markers from content for the same reason.
This commit is contained in:
@@ -249,6 +249,13 @@ class KimiK2ReasoningParser(ReasoningParser):
|
|||||||
if self.is_reasoning_end(previous_token_ids):
|
if self.is_reasoning_end(previous_token_ids):
|
||||||
# Strip any residual think tags that might appear in content
|
# Strip any residual think tags that might appear in content
|
||||||
cleaned = self._strip_think_tags(delta_text)
|
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
|
return DeltaMessage(content=cleaned) if cleaned else None
|
||||||
|
|
||||||
# ── Check for </think> in this delta ──
|
# ── Check for </think> in this delta ──
|
||||||
@@ -270,15 +277,13 @@ class KimiK2ReasoningParser(ReasoningParser):
|
|||||||
tool_idx = self._find_tool_section_start(delta_text)
|
tool_idx = self._find_tool_section_start(delta_text)
|
||||||
if tool_idx != -1:
|
if tool_idx != -1:
|
||||||
reasoning = self._strip_think_tags(delta_text[:tool_idx])
|
reasoning = self._strip_think_tags(delta_text[:tool_idx])
|
||||||
# Forward the tool section marker as content so the tool
|
# Do NOT forward the tool section marker as content. The
|
||||||
# parser can detect it.
|
# tool parser detects it via current_text re-parsing on its
|
||||||
content = delta_text[tool_idx:]
|
# own. Forwarding it causes double-handling and empty content
|
||||||
|
# deltas.
|
||||||
kwargs = {}
|
kwargs = {}
|
||||||
if reasoning:
|
if reasoning:
|
||||||
kwargs["reasoning"] = reasoning
|
kwargs["reasoning"] = reasoning
|
||||||
if content:
|
|
||||||
kwargs["content"] = content
|
|
||||||
return DeltaMessage(**kwargs) if kwargs else None
|
return DeltaMessage(**kwargs) if kwargs else None
|
||||||
|
|
||||||
# ── Still in reasoning — strip <think> tag if present ──
|
# ── Still in reasoning — strip <think> tag if present ──
|
||||||
|
|||||||
@@ -522,12 +522,17 @@ class KimiK2ToolParser(ToolParser):
|
|||||||
pre_section = current_text[len(previous_text):section_start_in_text]
|
pre_section = current_text[len(previous_text):section_start_in_text]
|
||||||
if pre_section.strip():
|
if pre_section.strip():
|
||||||
return DeltaMessage(content=pre_section)
|
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
|
# 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:
|
if in_open_section:
|
||||||
return DeltaMessage(content="")
|
return None
|
||||||
|
|
||||||
# Case 5: Section is closed and we're past it — forward any
|
# Case 5: Section is closed and we're past it — forward any
|
||||||
# new content that appeared after the section end marker.
|
# new content that appeared after the section end marker.
|
||||||
|
|||||||
Reference in New Issue
Block a user