[model] Apertus Tool/Function and Reasoning parser (#25100)
This commit is contained in:
@@ -597,6 +597,221 @@ class _PoolsideV1Detector(Qwen3Detector):
|
||||
self.reasoning_default = "explicit_enable_thinking"
|
||||
|
||||
|
||||
class Apertus2509Detector(BaseReasoningFormatDetector):
|
||||
"""
|
||||
Detector for Apertus 2509 models
|
||||
|
||||
Reasoning blocks are delimited by:
|
||||
<|inner_prefix|> ... <|inner_suffix|>
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
stream_reasoning: bool = True,
|
||||
force_reasoning: bool = False,
|
||||
continue_final_message: bool = False,
|
||||
previous_content: str = "",
|
||||
force_nonempty_content: bool = False,
|
||||
):
|
||||
super().__init__(
|
||||
"<|inner_prefix|>",
|
||||
"<|inner_suffix|>",
|
||||
force_reasoning=False,
|
||||
stream_reasoning=stream_reasoning,
|
||||
continue_final_message=continue_final_message,
|
||||
previous_content=previous_content,
|
||||
)
|
||||
self._force_reasoning = force_reasoning
|
||||
self._force_nonempty_content = force_nonempty_content
|
||||
self._tool_start_token = "<|tools_prefix|>["
|
||||
self._tool_end_token = "<|tools_suffix|>"
|
||||
self._reasoning_acc: str = ""
|
||||
self._in_inner_tool: bool = False
|
||||
|
||||
@staticmethod
|
||||
def _ends_with_partial_token(buffer: str, token: str) -> int:
|
||||
for i in range(1, min(len(buffer) + 1, len(token))):
|
||||
if token.startswith(buffer[-i:]):
|
||||
return i
|
||||
return 0
|
||||
|
||||
def detect_and_parse(self, text: str) -> StreamingParseResult:
|
||||
blocks = self.detect_and_parse_block_sequence(text)
|
||||
reasoning_parts = [t for k, t in blocks if k == "reasoning"]
|
||||
text_parts = [t for k, t in blocks if k == "text"]
|
||||
ret = StreamingParseResult(
|
||||
normal_text="".join(text_parts),
|
||||
reasoning_text="".join(reasoning_parts),
|
||||
)
|
||||
if self._force_nonempty_content and not ret.normal_text:
|
||||
ret.normal_text, ret.reasoning_text = ret.reasoning_text, ret.normal_text
|
||||
return ret
|
||||
|
||||
def detect_and_parse_block_sequence(self, text: str) -> list[tuple[str, str]]:
|
||||
"""Return an ordered sequence of blocks: [("reasoning"|"text", content), ...]"""
|
||||
start_tok = self.think_start_token
|
||||
end_tok = self.think_end_token
|
||||
blocks: list[tuple[str, str]] = []
|
||||
cursor = 0
|
||||
|
||||
# continue_final_message can resume inside an existing inner
|
||||
if self._in_reasoning:
|
||||
if (e := text.find(end_tok, cursor)) == -1:
|
||||
blocks.extend(self._split_inner_reasoning(text[cursor:]))
|
||||
blocks.append(("text", ""))
|
||||
return blocks
|
||||
blocks.extend(self._split_inner_reasoning(text[cursor:e]))
|
||||
cursor = e + len(end_tok)
|
||||
|
||||
while True:
|
||||
if (s := text.find(start_tok, cursor)) == -1:
|
||||
# Always include the trailing text block (may be empty)
|
||||
blocks.append(("text", text[cursor:]))
|
||||
break
|
||||
if s > cursor:
|
||||
blocks.append(("text", text[cursor:s]))
|
||||
|
||||
cursor = s + len(start_tok)
|
||||
if (e := text.find(end_tok, cursor)) == -1:
|
||||
blocks.extend(self._split_inner_reasoning(text[cursor:]))
|
||||
blocks.append(("text", ""))
|
||||
break
|
||||
blocks.extend(self._split_inner_reasoning(text[cursor:e]))
|
||||
cursor = e + len(end_tok)
|
||||
|
||||
last_idx = len(blocks) - 1
|
||||
blocks = [
|
||||
(k, t)
|
||||
for i, (k, t) in enumerate(blocks)
|
||||
if not (k == "text" and t == "" and i != last_idx)
|
||||
]
|
||||
|
||||
return blocks
|
||||
|
||||
def _split_inner_reasoning(self, inner_text: str) -> list[tuple[str, str]]:
|
||||
"""
|
||||
Split content inside <|inner_prefix|>...<|inner_suffix|> into:
|
||||
- ("reasoning", <thoughts text>)
|
||||
- ("text", <|tools_prefix|>[...]<|tools_suffix|>) for any tool calls inside reasoning
|
||||
"""
|
||||
tool_start = self._tool_start_token
|
||||
tool_end = self._tool_end_token
|
||||
out: list[tuple[str, str]] = []
|
||||
cursor = 0
|
||||
|
||||
while True:
|
||||
if (s := inner_text.find(tool_start, cursor)) == -1:
|
||||
if (tail := inner_text[cursor:]) != "":
|
||||
out.append(("reasoning", tail))
|
||||
break
|
||||
if s > cursor:
|
||||
out.append(("reasoning", inner_text[cursor:s]))
|
||||
|
||||
if (e := inner_text.find(tool_end, s)) == -1:
|
||||
out.append(("text", inner_text[s:]))
|
||||
break
|
||||
|
||||
out.append(("text", inner_text[s : e + len(tool_end)]))
|
||||
cursor = e + len(tool_end)
|
||||
|
||||
return out
|
||||
|
||||
def parse_streaming_increment(self, new_text: str) -> StreamingParseResult:
|
||||
self._buffer += new_text
|
||||
|
||||
out_reasoning = ""
|
||||
out_normal = ""
|
||||
|
||||
start_tok = self.think_start_token
|
||||
end_tok = self.think_end_token
|
||||
tool_start = self._tool_start_token
|
||||
tool_end = self._tool_end_token
|
||||
|
||||
while True:
|
||||
if not self._in_reasoning:
|
||||
if (s := self._buffer.find(start_tok)) == -1:
|
||||
if partial := self._ends_with_partial_token(
|
||||
self._buffer, start_tok
|
||||
):
|
||||
out_normal += self._buffer[:-partial]
|
||||
self._buffer = self._buffer[-partial:]
|
||||
else:
|
||||
out_normal += self._buffer
|
||||
self._buffer = ""
|
||||
return StreamingParseResult(
|
||||
normal_text=out_normal, reasoning_text=out_reasoning
|
||||
)
|
||||
|
||||
out_normal += self._buffer[:s]
|
||||
self._buffer = self._buffer[s + len(start_tok) :]
|
||||
self._in_reasoning = True
|
||||
self._reasoning_acc = ""
|
||||
self._in_inner_tool = False
|
||||
continue
|
||||
|
||||
if self._in_inner_tool:
|
||||
if (end_pos := self._buffer.find(tool_end)) == -1:
|
||||
if (
|
||||
hold := self._ends_with_partial_token(self._buffer, tool_end)
|
||||
) != 0:
|
||||
out_normal += self._buffer[:-hold]
|
||||
self._buffer = self._buffer[-hold:]
|
||||
else:
|
||||
out_normal += self._buffer
|
||||
self._buffer = ""
|
||||
return StreamingParseResult(
|
||||
normal_text=out_normal, reasoning_text=out_reasoning
|
||||
)
|
||||
|
||||
out_normal += self._buffer[: end_pos + len(tool_end)]
|
||||
self._buffer = self._buffer[end_pos + len(tool_end) :]
|
||||
self._in_inner_tool = False
|
||||
continue
|
||||
|
||||
pos_tool = self._buffer.find(tool_start)
|
||||
pos_end = self._buffer.find(end_tok)
|
||||
|
||||
if pos_tool == -1 and pos_end == -1:
|
||||
if self.stream_reasoning:
|
||||
if (
|
||||
hold := max(
|
||||
self._ends_with_partial_token(self._buffer, end_tok),
|
||||
self._ends_with_partial_token(self._buffer, tool_start),
|
||||
)
|
||||
) != 0:
|
||||
out_reasoning += self._buffer[:-hold]
|
||||
self._buffer = self._buffer[-hold:]
|
||||
else:
|
||||
out_reasoning += self._buffer
|
||||
self._buffer = ""
|
||||
return StreamingParseResult(
|
||||
normal_text=out_normal, reasoning_text=out_reasoning
|
||||
)
|
||||
|
||||
next_pos = min(p for p in [pos_tool, pos_end] if p != -1)
|
||||
|
||||
if pos_end != -1 and pos_end == next_pos:
|
||||
reasoning_chunk = self._buffer[:pos_end]
|
||||
if self.stream_reasoning:
|
||||
out_reasoning += reasoning_chunk
|
||||
else:
|
||||
self._reasoning_acc += reasoning_chunk
|
||||
out_reasoning += self._reasoning_acc
|
||||
self._reasoning_acc = ""
|
||||
self._buffer = self._buffer[pos_end + len(end_tok) :]
|
||||
self._in_reasoning = False
|
||||
continue
|
||||
|
||||
reasoning_chunk = self._buffer[:pos_tool]
|
||||
if self.stream_reasoning:
|
||||
out_reasoning += reasoning_chunk
|
||||
else:
|
||||
self._reasoning_acc += reasoning_chunk
|
||||
self._buffer = self._buffer[pos_tool:]
|
||||
self._in_inner_tool = True
|
||||
continue
|
||||
|
||||
|
||||
class CohereCommand4Detector(BaseReasoningFormatDetector):
|
||||
"""Detector for Cohere Command4 / Command-A family (incl. cohere2_moe and
|
||||
cohere2_vision Command-A-Plus).
|
||||
@@ -848,6 +1063,7 @@ class ReasoningParser:
|
||||
"""
|
||||
|
||||
DetectorMap: Dict[str, Type[BaseReasoningFormatDetector]] = {
|
||||
"apertus2509": Apertus2509Detector,
|
||||
"deepseek-r1": DeepSeekR1Detector,
|
||||
"deepseek-v3": _DeepSeekV3Detector,
|
||||
"deepseek-v4": _DeepSeekV3Detector,
|
||||
@@ -918,6 +1134,19 @@ class ReasoningParser:
|
||||
ret = self.detector.detect_and_parse(full_text)
|
||||
return ret.reasoning_text, ret.normal_text
|
||||
|
||||
def parse_non_stream_blocks(self, full_text: str) -> list[dict]:
|
||||
"""Non-streaming call: return an ordered sequence of reasoning/text blocks"""
|
||||
if hasattr(self.detector, "detect_and_parse_block_sequence"):
|
||||
seq = self.detector.detect_and_parse_block_sequence(full_text)
|
||||
return [{"type": k, "text": t} for k, t in seq]
|
||||
|
||||
ret = self.detector.detect_and_parse(full_text)
|
||||
blocks: list[dict] = []
|
||||
if ret.reasoning_text:
|
||||
blocks.append({"type": "reasoning", "text": ret.reasoning_text})
|
||||
blocks.append({"type": "text", "text": ret.normal_text or ""})
|
||||
return blocks
|
||||
|
||||
def parse_stream_chunk(
|
||||
self, chunk_text: str
|
||||
) -> Tuple[Optional[str], Optional[str]]:
|
||||
|
||||
Reference in New Issue
Block a user