From cce7c67cd7ae348ee94da60ffa6cc335978e7088 Mon Sep 17 00:00:00 2001 From: Ultron09 Date: Fri, 28 Aug 2026 22:47:15 +0530 Subject: [PATCH] fix(serve): parse tool calls with empty/omitted argument markers in ToolParser When a tool call contains no arguments (e.g. <|tool_call_begin|>functions.func:0<|tool_call_end|>) or when a stream ends in header state, _parse_header() was previously bypassed because ARG_BEGIN was not encountered, causing the tool call to be silently dropped. Also catch EngineError in version() and build_info() when libwaste is unbuilt so server health endpoints and test runners degrade gracefully. Add regression tests in tests/serve/test_chatfmt.py. --- serve/engine.py | 10 ++++++++-- serve/kimitools.py | 11 +++++++++++ tests/serve/test_chatfmt.py | 34 ++++++++++++++++++++++++++++++++++ 3 files changed, 53 insertions(+), 2 deletions(-) diff --git a/serve/engine.py b/serve/engine.py index de3430842..0865d5f9f 100644 --- a/serve/engine.py +++ b/serve/engine.py @@ -281,11 +281,17 @@ def _bind(lib) -> None: def version() -> str: - return _lib().waste_version().decode() + try: + return _lib().waste_version().decode() + except EngineError: + return "unknown" def build_info() -> str: - return _lib().waste_build_info().decode() + try: + return _lib().waste_build_info().decode() + except EngineError: + return "unbuilt" def physical_ram() -> int: diff --git a/serve/kimitools.py b/serve/kimitools.py index c302edf7f..fac03de94 100644 --- a/serve/kimitools.py +++ b/serve/kimitools.py @@ -205,11 +205,19 @@ def feed_marker(self, marker: str, delta: Delta) -> bool: self.calls.append(self._current) self._state = "arguments" elif marker == _CALL_END: + if self._state == "header": + self._current = self._parse_header() + self.calls.append(self._current) if self._current is not None: self._current.json_block = self._arguments self._current = None self._state = "section" elif marker == _SECTION_END: + if self._state == "header": + self._current = self._parse_header() + self.calls.append(self._current) + if self._current is not None: + self._current.json_block = self._arguments self._current = None self._state = "content" else: @@ -232,6 +240,9 @@ def feed_text(self, piece: str, delta: Delta) -> bool: def finish(self) -> None: """Flush a call whose arguments the stream ended in the middle of.""" + if self._state == "header": + self._current = self._parse_header() + self.calls.append(self._current) if self._current is not None: self._current.json_block = self._arguments diff --git a/tests/serve/test_chatfmt.py b/tests/serve/test_chatfmt.py index 9276ee6a8..c394e14e1 100644 --- a/tests/serve/test_chatfmt.py +++ b/tests/serve/test_chatfmt.py @@ -693,3 +693,37 @@ def test_kimi_tool_call_delta_reports_change(self): ) self.assertIn(0, delta.tool_calls) + + def test_kimi_tool_call_without_arguments_marker(self): + p = self.parser() + + self.feed(p, [ + (1002, "<|tool_calls_section_begin|>"), + (1004, "<|tool_call_begin|>"), + (2001, "functions.get_time:0"), + (1006, "<|tool_call_end|>"), + (1003, "<|tool_calls_section_end|>"), + (1001, "<|im_end|>"), + ]) + + self.assertEqual(len(p.tool_calls), 1) + call = p.tool_calls[0] + self.assertEqual(call.name, "get_time") + self.assertEqual(call.index, 0) + self.assertEqual(call.json_block, "") + + def test_kimi_tool_call_stream_ended_in_header(self): + p = self.parser() + + self.feed(p, [ + (1002, "<|tool_calls_section_begin|>"), + (1004, "<|tool_call_begin|>"), + (2001, "functions.get_time:0"), + ]) + p.finish() + + self.assertEqual(len(p.tool_calls), 1) + call = p.tool_calls[0] + self.assertEqual(call.name, "get_time") + self.assertEqual(call.index, 0) + self.assertEqual(call.json_block, "")