Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
111 changes: 60 additions & 51 deletions lmdeploy/serve/parsers/tool_parser/glm47_tool_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
)

from .tool_parser import ToolParserManager
from .xml_tool_parser import XmlToolParser
from .xml_tool_parser import XmlParseResult, XmlToolParser


@ToolParserManager.register_module(['glm47'])
Expand All @@ -27,14 +27,8 @@ class Glm47ToolParser(XmlToolParser):
)

def _reset_incremental_state(self) -> None:
self._func_name: str | None = None
self._args: dict[str, str] = {}
self._arg_name: str | None = None
self._value_parts: list[str] = []
self._phase = 'function'
self._stream_started = False
self._stream_pending_ws = ''
self._stream_blocked = False
self._reset_value_stream_state()

@classmethod
def get_tool_open_tag(cls) -> str | None:
Expand All @@ -53,13 +47,11 @@ def _reset_value_stream_state(self) -> None:
self._stream_pending_ws = ''
self._stream_blocked = False

def _stream_arg_delta(self, raw: str) -> str:
if self._stream_blocked:
return ''
def _block_stream_arg_delta(self) -> None:
self._stream_blocked = True

schema_type = self._get_param_schema_type(self._func_name, self._arg_name or '')
if schema_type not in (None, 'string'):
self._stream_blocked = True
def _normalize_stream_arg_delta(self, raw: str, schema_type: str | None) -> str:
if self._stream_blocked:
return ''

text = self._stream_pending_ws + raw
Expand Down Expand Up @@ -94,77 +86,94 @@ def _stream_arg_delta(self, raw: str) -> str:
self._stream_started = True
return text

def _consume_function(self, payload: str, pos: int, final: bool) -> int | None:
def _consume_function(self, payload: str, pos: int, final: bool) -> XmlParseResult:
"""Read the GLM function name before the first ``<arg_key>``.

Returns no position when the function name is still split across chunks. A final no-argument payload can
complete with only a function name.
"""
arg_key_start = payload.find('<arg_key>', pos)
if arg_key_start >= 0:
name = payload[pos:arg_key_start].strip()
if name:
self._func_name = name
self._phase = 'arg_start'
return arg_key_start
return XmlParseResult(
next_pos=arg_key_start,
next_phase='arg_start',
func_name=name or None,
)

remaining = payload[pos:]
if final and remaining.strip():
self._func_name = remaining.strip()
return len(payload)
return None
return XmlParseResult(next_pos=len(payload), func_name=remaining.strip())
return XmlParseResult(next_pos=None)

def _consume_arg_start(self, payload: str, pos: int) -> int | None:
def _consume_arg_start(self, payload: str, pos: int) -> XmlParseResult:
"""Find the next GLM ``<arg_key>`` marker and enter arg-name
parsing."""
arg_key_start = payload.find('<arg_key>', pos)
if arg_key_start < 0:
return None
return XmlParseResult(next_pos=None)

self._phase = 'arg_name'
return arg_key_start + len('<arg_key>')
return XmlParseResult(
next_pos=arg_key_start + len('<arg_key>'),
next_phase='arg_name',
)

def _consume_arg_name(self, payload: str, pos: int) -> int | None:
def _consume_arg_name(self, payload: str, pos: int) -> XmlParseResult:
"""Read ``<arg_key>`` content and advance to the following value body.

The method waits for both ``</arg_key>`` and ``<arg_value>`` so the base
class only receives an argument name once the value phase can start.
"""
key_end = payload.find('</arg_key>', pos)
if key_end < 0:
return None
return XmlParseResult(next_pos=None)

value_start = payload.find('<arg_value>', key_end + len('</arg_key>'))
if value_start < 0:
return None
return XmlParseResult(next_pos=None)

self._arg_name = payload[pos:key_end].strip()
self._value_parts.clear()
self._reset_value_stream_state()
self._phase = 'arg_value'
return value_start + len('<arg_value>')

def _consume_arg_value(self, payload: str, pos: int, arg_delta_parts: list[str]) -> tuple[int | None, bool]:
"""Consume an argument value.

Returns ``(next_pos, should_stop)``. ``should_stop`` is true after
streaming an open value delta, because the next bytes may be the
argument close tag and must be checked with the next chunk.
return XmlParseResult(
next_pos=value_start + len('<arg_value>'),
next_phase='arg_value',
arg_name=payload[pos:key_end].strip(),
)

def _consume_arg_value(self, payload: str, pos: int) -> XmlParseResult:
"""Consume GLM argument value text.

Closed values return the completed raw value for the base class to attach
to the active argument name. Open values return only safe raw deltas and
leave a possible partial ``</arg_value>`` suffix buffered for the next
chunk.
"""
value_end = payload.find('</arg_value>', pos)

if value_end >= 0:
raw = payload[pos:value_end]
if raw:
self._value_parts.append(raw)
if self._arg_name:
self._args[self._arg_name] = ''.join(self._value_parts)
self._arg_name = None
value = ''.join(self._value_parts)
self._value_parts.clear()
self._reset_value_stream_state()
self._phase = 'function'
return value_end + len('</arg_value>'), False
return XmlParseResult(
next_pos=value_end + len('</arg_value>'),
next_phase='function',
completed_arg_value=value,
)

# Open value: keep any partial "</arg_value>" suffix buffered instead
# of emitting it as argument text.
raw_end = self._trim_partial_close_tag_suffix(payload, pos, '</arg_value>')
if raw_end == pos:
return None, True
return XmlParseResult(next_pos=None, should_stop=True)

raw_delta = payload[pos:raw_end]
self._value_parts.append(raw_delta)
stream_delta = self._stream_arg_delta(raw_delta)
if stream_delta:
arg_delta_parts.append(stream_delta)
return raw_end, True
return XmlParseResult(
next_pos=raw_end,
raw_arg_delta=raw_delta,
should_stop=True,
)

def parse_tool_call_complete(self, payload: str) -> ToolCall | None:
func_name, raw_args_dict = self._extract_complete_args(payload)
Expand Down
114 changes: 63 additions & 51 deletions lmdeploy/serve/parsers/tool_parser/qwen3coder_tool_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
)

from .tool_parser import ToolParserManager
from .xml_tool_parser import XmlToolParser
from .xml_tool_parser import XmlParseResult, XmlToolParser


@ToolParserManager.register_module(['qwen3coder'])
Expand All @@ -23,14 +23,8 @@ class Qwen3CoderToolParser(XmlToolParser):
)

def _reset_incremental_state(self) -> None:
self._func_name: str | None = None
self._args: dict[str, str] = {}
self._arg_name: str | None = None
self._value_parts: list[str] = []
self._phase = 'function'
self._stream_started = False
self._stream_pending_ws = ''
self._stream_blocked = False
self._reset_value_stream_state()

# Qwen3Coder closes tool argument JSON only when the model emits the
# explicit function end marker (</function>). We intentionally avoid
Expand All @@ -56,13 +50,11 @@ def _reset_value_stream_state(self) -> None:
self._stream_pending_ws = ''
self._stream_blocked = False

def _stream_arg_delta(self, raw: str) -> str:
if self._stream_blocked:
return ''
def _block_stream_arg_delta(self) -> None:
self._stream_blocked = True

schema_type = self._get_param_schema_type(self._func_name, self._arg_name or '')
if schema_type not in (None, 'string'):
self._stream_blocked = True
def _normalize_stream_arg_delta(self, raw: str, schema_type: str | None) -> str:
if self._stream_blocked:
return ''

text = self._stream_pending_ws + raw
Expand All @@ -84,79 +76,99 @@ def _stream_arg_delta(self, raw: str) -> str:
self._stream_started = True
return stable

def _consume_function(self, payload: str, pos: int, final: bool) -> int | None:
def _consume_function(self, payload: str, pos: int, final: bool) -> XmlParseResult:
"""Read a Qwen ``<function=name>`` opener and publish the function
name.

The parser waits when either the opener or its closing ``>`` is split
across chunks.
"""
start = payload.find('<function=', pos)
if start < 0:
return None
return XmlParseResult(next_pos=None)

name_start = start + len('<function=')
name_end = payload.find('>', name_start)
if name_end < 0:
return None
return XmlParseResult(next_pos=None)

self._func_name = payload[name_start:name_end].strip()
self._phase = 'arg_start'
return name_end + 1
return XmlParseResult(
next_pos=name_end + 1,
next_phase='arg_start',
func_name=payload[name_start:name_end].strip(),
)

def _consume_arg_start(self, payload: str, pos: int) -> int | None:
def _consume_arg_start(self, payload: str, pos: int) -> XmlParseResult:
"""Find the next Qwen parameter opener or the function close marker.

``</function>`` closes the XML payload only when it appears before the next
``<parameter=`` marker.
"""
param_start = payload.find('<parameter=', pos)
func_end = payload.find('</function>', pos)

if func_end >= 0 and (param_start < 0 or func_end < param_start):
self._payload_closed = True
self._phase = 'done'
return func_end + len('</function>')
return XmlParseResult(
next_pos=func_end + len('</function>'),
next_phase='done',
payload_closed=True,
)

if param_start < 0:
return None
return XmlParseResult(next_pos=None)

self._phase = 'arg_name'
return param_start + len('<parameter=')
return XmlParseResult(
next_pos=param_start + len('<parameter='),
next_phase='arg_name',
)

def _consume_arg_name(self, payload: str, pos: int) -> int | None:
def _consume_arg_name(self, payload: str, pos: int) -> XmlParseResult:
"""Read the Qwen parameter name and advance to its value body."""
name_end = payload.find('>', pos)
if name_end < 0:
return None
return XmlParseResult(next_pos=None)

self._arg_name = payload[pos:name_end].strip()
self._value_parts.clear()
self._reset_value_stream_state()
self._phase = 'arg_value'
return name_end + 1

def _consume_arg_value(self, payload: str, pos: int, arg_delta_parts: list[str]) -> tuple[int | None, bool]:
"""Consume a parameter value.

Returns ``(next_pos, should_stop)``. ``should_stop`` is true after
streaming an open value delta, because the next bytes may be the
parameter close tag and must be checked with the next chunk.
return XmlParseResult(
next_pos=name_end + 1,
next_phase='arg_value',
arg_name=payload[pos:name_end].strip(),
)

def _consume_arg_value(self, payload: str, pos: int) -> XmlParseResult:
"""Consume Qwen parameter value text.

Closed values are stripped to preserve Qwen's current XML formatting
behavior. Open values leave a possible partial ``</parameter>`` suffix
buffered so split close tags are not emitted as argument text.
"""
value_end = payload.find('</parameter>', pos)

if value_end >= 0:
raw = payload[pos:value_end]
if raw:
self._value_parts.append(raw)
if self._arg_name:
self._args[self._arg_name] = ''.join(self._value_parts).strip()
self._arg_name = None
value = ''.join(self._value_parts).strip()
self._value_parts.clear()
self._reset_value_stream_state()
self._phase = 'arg_start'
return value_end + len('</parameter>'), False
return XmlParseResult(
next_pos=value_end + len('</parameter>'),
next_phase='arg_start',
completed_arg_value=value,
)

# Open value: keep any partial "</parameter>" suffix buffered instead
# of emitting it as argument text.
raw_end = self._trim_partial_close_tag_suffix(payload, pos, '</parameter>')
if raw_end == pos:
return None, True
return XmlParseResult(next_pos=None, should_stop=True)

raw_delta = payload[pos:raw_end]
self._value_parts.append(raw_delta)
stream_delta = self._stream_arg_delta(raw_delta)
if stream_delta:
arg_delta_parts.append(stream_delta)
return raw_end, True
return XmlParseResult(
next_pos=raw_end,
raw_arg_delta=raw_delta,
should_stop=True,
)

def parse_tool_call_complete(self, payload: str) -> ToolCall | None:
func_name, raw_args_dict, _ = self._extract_params(payload)
Expand Down
Loading
Loading