Fix dropped parallel tool calls in speculative streams
Browse filesApply ripple-inference PR #17 (d68cca40e1). Reconcile streamed tool calls at finish so speculative deltas cannot drop trailing calls or argument suffixes. Validated with CPU parser and chat-format regression tests on stock SGLang 0.5.19.
https://github.com/ricursive/ripple-inference/pull/17
inference/server/ripple_format.py
CHANGED
|
@@ -136,7 +136,7 @@ def _reasoning_detector_class():
|
|
| 136 |
|
| 137 |
|
| 138 |
def _tool_detector_class():
|
| 139 |
-
from sglang.srt.function_call.core_types import StructureInfo
|
| 140 |
from sglang.srt.function_call.hermes_detector import HermesDetector
|
| 141 |
|
| 142 |
class RippleToolDetector(HermesDetector):
|
|
@@ -147,6 +147,21 @@ def _tool_detector_class():
|
|
| 147 |
super().__init__()
|
| 148 |
self._held_newline = ""
|
| 149 |
self._in_calls = False
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 150 |
|
| 151 |
def detect_and_parse(self, text, tools):
|
| 152 |
res = super().detect_and_parse(text, tools)
|
|
@@ -156,7 +171,9 @@ def _tool_detector_class():
|
|
| 156 |
return res
|
| 157 |
|
| 158 |
def parse_streaming_increment(self, new_text, tools):
|
|
|
|
| 159 |
res = super().parse_streaming_increment(new_text, tools)
|
|
|
|
| 160 |
if self._in_calls: # text after the first call is dropped, as in detect_and_parse
|
| 161 |
res.normal_text = ""
|
| 162 |
return res
|
|
@@ -175,6 +192,19 @@ def _tool_detector_class():
|
|
| 175 |
|
| 176 |
def finish(self, tools):
|
| 177 |
res = super().finish(tools)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 178 |
if self._in_calls:
|
| 179 |
res.normal_text = ""
|
| 180 |
elif self._held_newline:
|
|
|
|
| 136 |
|
| 137 |
|
| 138 |
def _tool_detector_class():
|
| 139 |
+
from sglang.srt.function_call.core_types import StructureInfo, ToolCallItem
|
| 140 |
from sglang.srt.function_call.hermes_detector import HermesDetector
|
| 141 |
|
| 142 |
class RippleToolDetector(HermesDetector):
|
|
|
|
| 147 |
super().__init__()
|
| 148 |
self._held_newline = ""
|
| 149 |
self._in_calls = False
|
| 150 |
+
# Hermes can leave whole calls buffered when one speculative delta spans
|
| 151 |
+
# multiple calls. Reconcile the full output with emitted deltas at finish.
|
| 152 |
+
self._seen = ""
|
| 153 |
+
self._emitted = {}
|
| 154 |
+
|
| 155 |
+
def _record(self, calls):
|
| 156 |
+
for call in calls:
|
| 157 |
+
emitted = self._emitted.setdefault(call.tool_index, ["", ""])
|
| 158 |
+
emitted[0] += call.name or ""
|
| 159 |
+
emitted[1] += call.parameters or ""
|
| 160 |
+
# SGLang's serving layer also flushes arguments after finish(). Keep
|
| 161 |
+
# its accounting current so it cannot send the same suffix twice.
|
| 162 |
+
while len(self.streamed_args_for_tool) <= call.tool_index:
|
| 163 |
+
self.streamed_args_for_tool.append("")
|
| 164 |
+
self.streamed_args_for_tool[call.tool_index] = emitted[1]
|
| 165 |
|
| 166 |
def detect_and_parse(self, text, tools):
|
| 167 |
res = super().detect_and_parse(text, tools)
|
|
|
|
| 171 |
return res
|
| 172 |
|
| 173 |
def parse_streaming_increment(self, new_text, tools):
|
| 174 |
+
self._seen += new_text
|
| 175 |
res = super().parse_streaming_increment(new_text, tools)
|
| 176 |
+
self._record(res.calls)
|
| 177 |
if self._in_calls: # text after the first call is dropped, as in detect_and_parse
|
| 178 |
res.normal_text = ""
|
| 179 |
return res
|
|
|
|
| 192 |
|
| 193 |
def finish(self, tools):
|
| 194 |
res = super().finish(tools)
|
| 195 |
+
self._record(res.calls)
|
| 196 |
+
if self.bot_token in self._seen:
|
| 197 |
+
for i, call in enumerate(self.detect_and_parse(self._seen, tools).calls):
|
| 198 |
+
name, args = self._emitted.get(i, ["", ""])
|
| 199 |
+
want = call.parameters or ""
|
| 200 |
+
if want.startswith(args):
|
| 201 |
+
suffix = want[len(args):]
|
| 202 |
+
if suffix or not name:
|
| 203 |
+
item = ToolCallItem(
|
| 204 |
+
tool_index=i, name=None if name else call.name, parameters=suffix,
|
| 205 |
+
)
|
| 206 |
+
res.calls.append(item)
|
| 207 |
+
self._record([item])
|
| 208 |
if self._in_calls:
|
| 209 |
res.normal_text = ""
|
| 210 |
elif self._held_newline:
|