renderers 0.1.9.dev8__tar.gz → 0.1.9.dev10__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {renderers-0.1.9.dev8 → renderers-0.1.9.dev10}/PKG-INFO +1 -1
- {renderers-0.1.9.dev8 → renderers-0.1.9.dev10}/renderers/_version.py +2 -2
- {renderers-0.1.9.dev8 → renderers-0.1.9.dev10}/renderers/base.py +10 -2
- {renderers-0.1.9.dev8 → renderers-0.1.9.dev10}/renderers/client.py +12 -17
- {renderers-0.1.9.dev8 → renderers-0.1.9.dev10}/renderers/parsing.py +44 -1
- renderers-0.1.9.dev10/tests/test_glm_tool_name_validation.py +183 -0
- {renderers-0.1.9.dev8 → renderers-0.1.9.dev10}/.github/workflows/publish-dev.yml +0 -0
- {renderers-0.1.9.dev8 → renderers-0.1.9.dev10}/.github/workflows/publish.yml +0 -0
- {renderers-0.1.9.dev8 → renderers-0.1.9.dev10}/.github/workflows/style.yml +0 -0
- {renderers-0.1.9.dev8 → renderers-0.1.9.dev10}/.github/workflows/test.yml +0 -0
- {renderers-0.1.9.dev8 → renderers-0.1.9.dev10}/.gitignore +0 -0
- {renderers-0.1.9.dev8 → renderers-0.1.9.dev10}/.pre-commit-config.yaml +0 -0
- {renderers-0.1.9.dev8 → renderers-0.1.9.dev10}/LICENSE +0 -0
- {renderers-0.1.9.dev8 → renderers-0.1.9.dev10}/README.md +0 -0
- {renderers-0.1.9.dev8 → renderers-0.1.9.dev10}/docs/renderer-config.md +0 -0
- {renderers-0.1.9.dev8 → renderers-0.1.9.dev10}/examples/README.md +0 -0
- {renderers-0.1.9.dev8 → renderers-0.1.9.dev10}/examples/sglang/multiturn_generate_sglang.py +0 -0
- {renderers-0.1.9.dev8 → renderers-0.1.9.dev10}/examples/sglang/online_multiturn_sglang.py +0 -0
- {renderers-0.1.9.dev8 → renderers-0.1.9.dev10}/examples/tinker/multiturn_generate_tinker.py +0 -0
- {renderers-0.1.9.dev8 → renderers-0.1.9.dev10}/examples/transformers/multiturn_generate_transformers.py +0 -0
- {renderers-0.1.9.dev8 → renderers-0.1.9.dev10}/examples/vllm/multiturn_generate_vllm.py +0 -0
- {renderers-0.1.9.dev8 → renderers-0.1.9.dev10}/pyproject.toml +0 -0
- {renderers-0.1.9.dev8 → renderers-0.1.9.dev10}/renderers/__init__.py +0 -0
- {renderers-0.1.9.dev8 → renderers-0.1.9.dev10}/renderers/configs.py +0 -0
- {renderers-0.1.9.dev8 → renderers-0.1.9.dev10}/renderers/deepseek_r1.py +0 -0
- {renderers-0.1.9.dev8 → renderers-0.1.9.dev10}/renderers/deepseek_v3.py +0 -0
- {renderers-0.1.9.dev8 → renderers-0.1.9.dev10}/renderers/default.py +0 -0
- {renderers-0.1.9.dev8 → renderers-0.1.9.dev10}/renderers/glm45.py +0 -0
- {renderers-0.1.9.dev8 → renderers-0.1.9.dev10}/renderers/glm5.py +0 -0
- {renderers-0.1.9.dev8 → renderers-0.1.9.dev10}/renderers/gpt_oss.py +0 -0
- {renderers-0.1.9.dev8 → renderers-0.1.9.dev10}/renderers/hy3.py +0 -0
- {renderers-0.1.9.dev8 → renderers-0.1.9.dev10}/renderers/kimi_k2.py +0 -0
- {renderers-0.1.9.dev8 → renderers-0.1.9.dev10}/renderers/kimi_k25.py +0 -0
- {renderers-0.1.9.dev8 → renderers-0.1.9.dev10}/renderers/laguna_xs2.py +0 -0
- {renderers-0.1.9.dev8 → renderers-0.1.9.dev10}/renderers/llama_3.py +0 -0
- {renderers-0.1.9.dev8 → renderers-0.1.9.dev10}/renderers/minimax_m2.py +0 -0
- {renderers-0.1.9.dev8 → renderers-0.1.9.dev10}/renderers/nemotron3.py +0 -0
- {renderers-0.1.9.dev8 → renderers-0.1.9.dev10}/renderers/parsers.py +0 -0
- {renderers-0.1.9.dev8 → renderers-0.1.9.dev10}/renderers/prime_qwen3.py +0 -0
- {renderers-0.1.9.dev8 → renderers-0.1.9.dev10}/renderers/qwen3.py +0 -0
- {renderers-0.1.9.dev8 → renderers-0.1.9.dev10}/renderers/qwen35.py +0 -0
- {renderers-0.1.9.dev8 → renderers-0.1.9.dev10}/renderers/qwen36.py +0 -0
- {renderers-0.1.9.dev8 → renderers-0.1.9.dev10}/renderers/qwen3_vl.py +0 -0
- {renderers-0.1.9.dev8 → renderers-0.1.9.dev10}/tests/conftest.py +0 -0
- {renderers-0.1.9.dev8 → renderers-0.1.9.dev10}/tests/test_bridge.py +0 -0
- {renderers-0.1.9.dev8 → renderers-0.1.9.dev10}/tests/test_build_helpers.py +0 -0
- {renderers-0.1.9.dev8 → renderers-0.1.9.dev10}/tests/test_client.py +0 -0
- {renderers-0.1.9.dev8 → renderers-0.1.9.dev10}/tests/test_deepseek_r1.py +0 -0
- {renderers-0.1.9.dev8 → renderers-0.1.9.dev10}/tests/test_disabled_thinking_stability.py +0 -0
- {renderers-0.1.9.dev8 → renderers-0.1.9.dev10}/tests/test_gpt_oss_harmony_parity.py +0 -0
- {renderers-0.1.9.dev8 → renderers-0.1.9.dev10}/tests/test_hy3.py +0 -0
- {renderers-0.1.9.dev8 → renderers-0.1.9.dev10}/tests/test_incremental.py +0 -0
- {renderers-0.1.9.dev8 → renderers-0.1.9.dev10}/tests/test_is_content.py +0 -0
- {renderers-0.1.9.dev8 → renderers-0.1.9.dev10}/tests/test_kimi_k25_tool_schema.py +0 -0
- {renderers-0.1.9.dev8 → renderers-0.1.9.dev10}/tests/test_laguna_xs21.py +0 -0
- {renderers-0.1.9.dev8 → renderers-0.1.9.dev10}/tests/test_llama_3.py +0 -0
- {renderers-0.1.9.dev8 → renderers-0.1.9.dev10}/tests/test_load_tokenizer.py +0 -0
- {renderers-0.1.9.dev8 → renderers-0.1.9.dev10}/tests/test_message_indices.py +0 -0
- {renderers-0.1.9.dev8 → renderers-0.1.9.dev10}/tests/test_message_tool_names.py +0 -0
- {renderers-0.1.9.dev8 → renderers-0.1.9.dev10}/tests/test_multimodal.py +0 -0
- {renderers-0.1.9.dev8 → renderers-0.1.9.dev10}/tests/test_nemotron3_parity.py +0 -0
- {renderers-0.1.9.dev8 → renderers-0.1.9.dev10}/tests/test_nemotron3_ultra.py +0 -0
- {renderers-0.1.9.dev8 → renderers-0.1.9.dev10}/tests/test_parse_response.py +0 -0
- {renderers-0.1.9.dev8 → renderers-0.1.9.dev10}/tests/test_parse_response_robustness.py +0 -0
- {renderers-0.1.9.dev8 → renderers-0.1.9.dev10}/tests/test_parsers.py +0 -0
- {renderers-0.1.9.dev8 → renderers-0.1.9.dev10}/tests/test_preserve_thinking.py +0 -0
- {renderers-0.1.9.dev8 → renderers-0.1.9.dev10}/tests/test_prime_qwen3_parity.py +0 -0
- {renderers-0.1.9.dev8 → renderers-0.1.9.dev10}/tests/test_qwen35_size_coverage.py +0 -0
- {renderers-0.1.9.dev8 → renderers-0.1.9.dev10}/tests/test_render_ids.py +0 -0
- {renderers-0.1.9.dev8 → renderers-0.1.9.dev10}/tests/test_renderer_config.py +0 -0
- {renderers-0.1.9.dev8 → renderers-0.1.9.dev10}/tests/test_renderer_config_parity.py +0 -0
- {renderers-0.1.9.dev8 → renderers-0.1.9.dev10}/tests/test_roundtrip.py +0 -0
- {renderers-0.1.9.dev8 → renderers-0.1.9.dev10}/tests/test_sampled_mask.py +0 -0
- {renderers-0.1.9.dev8 → renderers-0.1.9.dev10}/tests/test_tokens_per_message.py +0 -0
- {renderers-0.1.9.dev8 → renderers-0.1.9.dev10}/tests/test_tool_arg_type_preservation.py +0 -0
- {renderers-0.1.9.dev8 → renderers-0.1.9.dev10}/uv.lock +0 -0
|
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
|
|
|
18
18
|
commit_id: str | None
|
|
19
19
|
__commit_id__: str | None
|
|
20
20
|
|
|
21
|
-
__version__ = version = '0.1.9.
|
|
22
|
-
__version_tuple__ = version_tuple = (0, 1, 9, '
|
|
21
|
+
__version__ = version = '0.1.9.dev10'
|
|
22
|
+
__version_tuple__ = version_tuple = (0, 1, 9, 'dev10')
|
|
23
23
|
|
|
24
24
|
__commit_id__ = commit_id = None
|
|
@@ -558,8 +558,15 @@ class ToolCallParseStatus(str, enum.Enum):
|
|
|
558
558
|
"""Per-attempt outcome of parsing a single ``<tool_call>`` block.
|
|
559
559
|
|
|
560
560
|
The renderer parser's job is JSON-syntax → ``dict`` (the parser-level
|
|
561
|
-
contract). Schema validation — required fields, argument types
|
|
562
|
-
|
|
561
|
+
contract). Schema validation — required fields, argument types — is
|
|
562
|
+
the *tool*'s job and is intentionally not done here. Tool-*name*
|
|
563
|
+
lookup is the one exception, and only where the reference inference
|
|
564
|
+
parser does it: vLLM ≥ 0.24 aliases ``glm45``/``glm47`` to a parser
|
|
565
|
+
with ``validate_tool_names=True`` that silently drops any call whose
|
|
566
|
+
name isn't in the request's tool list. ``parse_glm`` mirrors that as
|
|
567
|
+
``UNKNOWN_TOOL`` (when ``tools`` is passed) so train-side parsing
|
|
568
|
+
agrees with what an eval client sees from the engine — but keeps the
|
|
569
|
+
attempt visible instead of swallowing it.
|
|
563
570
|
See ``ParsedToolCall.status`` for what each value means.
|
|
564
571
|
|
|
565
572
|
Diverges from vLLM/SGLang on purpose. Both engines collapse parse
|
|
@@ -576,6 +583,7 @@ class ToolCallParseStatus(str, enum.Enum):
|
|
|
576
583
|
UNCLOSED_BLOCK = "unclosed_block" # opening delim hit EOS / stop
|
|
577
584
|
MISSING_NAME = "missing_name" # parsed structurally, but no function name
|
|
578
585
|
MALFORMED_STRUCTURE = "malformed_structure" # format-specific shape error
|
|
586
|
+
UNKNOWN_TOOL = "unknown_tool" # name not in the provided tools list
|
|
579
587
|
|
|
580
588
|
|
|
581
589
|
@dataclass
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""Renderer-based generate client for vLLM
|
|
1
|
+
"""Renderer-based generate client for vLLM's /inference/v1/generate.
|
|
2
2
|
|
|
3
3
|
messages → Renderer.render_ids() → token IDs → POST /inference/v1/generate
|
|
4
4
|
→ completion tokens → Renderer.parse_response() → structured message
|
|
@@ -58,8 +58,7 @@ class OverlongPromptError(Exception):
|
|
|
58
58
|
self.prompt_len = prompt_len
|
|
59
59
|
self.max_prompt_len = max_prompt_len
|
|
60
60
|
super().__init__(
|
|
61
|
-
f"Prompt length ({prompt_len}) exceeds maximum "
|
|
62
|
-
f"context length ({max_prompt_len})."
|
|
61
|
+
f"Prompt length ({prompt_len}) exceeds maximum context length ({max_prompt_len})."
|
|
63
62
|
)
|
|
64
63
|
|
|
65
64
|
|
|
@@ -187,32 +186,26 @@ def _parse_completion_logprobs(
|
|
|
187
186
|
expected_token = f"token_id:{completion_ids[index]}"
|
|
188
187
|
if entry.get("token") != expected_token:
|
|
189
188
|
raise MalformedGenerateResponseError(
|
|
190
|
-
"Engine response "
|
|
191
|
-
f"choice.logprobs.content[{index}].token must be {expected_token!r}."
|
|
189
|
+
f"Engine response choice.logprobs.content[{index}].token must be {expected_token!r}."
|
|
192
190
|
)
|
|
193
191
|
raw_logprob = entry.get("logprob")
|
|
194
192
|
if isinstance(raw_logprob, bool) or not isinstance(raw_logprob, (int, float)):
|
|
195
193
|
raise MalformedGenerateResponseError(
|
|
196
|
-
"Engine response "
|
|
197
|
-
f"choice.logprobs.content[{index}].logprob must be a number."
|
|
194
|
+
f"Engine response choice.logprobs.content[{index}].logprob must be a number."
|
|
198
195
|
)
|
|
199
196
|
try:
|
|
200
197
|
logprob = float(raw_logprob)
|
|
201
198
|
except OverflowError as exc:
|
|
202
199
|
raise MalformedGenerateResponseError(
|
|
203
|
-
"Engine response "
|
|
204
|
-
f"choice.logprobs.content[{index}].logprob must be finite."
|
|
200
|
+
f"Engine response choice.logprobs.content[{index}].logprob must be finite."
|
|
205
201
|
) from exc
|
|
206
202
|
if not math.isfinite(logprob):
|
|
207
203
|
raise MalformedGenerateResponseError(
|
|
208
|
-
"Engine response "
|
|
209
|
-
f"choice.logprobs.content[{index}].logprob must be finite."
|
|
204
|
+
f"Engine response choice.logprobs.content[{index}].logprob must be finite."
|
|
210
205
|
)
|
|
211
206
|
if logprob == VLLM_LOGPROB_SENTINEL:
|
|
212
207
|
raise MalformedGenerateResponseError(
|
|
213
|
-
"Engine response "
|
|
214
|
-
f"choice.logprobs.content[{index}].logprob does not contain "
|
|
215
|
-
"sampling evidence."
|
|
208
|
+
f"Engine response choice.logprobs.content[{index}].logprob does not contain sampling evidence."
|
|
216
209
|
)
|
|
217
210
|
completion_logprobs.append(logprob)
|
|
218
211
|
return completion_logprobs
|
|
@@ -424,9 +417,9 @@ def _build_mm_features(
|
|
|
424
417
|
model-family specific. For now we dispatch on the renderer class;
|
|
425
418
|
extend the dispatch table as more multimodal renderers land.
|
|
426
419
|
|
|
427
|
-
NOTE — future engine pluggability: this encoder is vLLM
|
|
420
|
+
NOTE — future engine pluggability: this encoder is vLLM-specific
|
|
428
421
|
(uses ``vllm.multimodal.inputs.MultiModalKwargsItems``,
|
|
429
|
-
``vllm.entrypoints.
|
|
422
|
+
``vllm.entrypoints.scale_out.token_in_token_out.mm_serde.encode_mm_kwargs_item``, and
|
|
430
423
|
``_create_qwen2vl_field_factory``). When a second inference engine
|
|
431
424
|
arrives (SGLang, MAX, ...) the renderer client should be parameterized
|
|
432
425
|
on engine: either (a) move the encoder onto the renderer as
|
|
@@ -473,7 +466,9 @@ def _build_qwen_vl_features(
|
|
|
473
466
|
try:
|
|
474
467
|
import torch
|
|
475
468
|
from transformers.feature_extraction_utils import BatchFeature
|
|
476
|
-
from vllm.entrypoints.
|
|
469
|
+
from vllm.entrypoints.scale_out.token_in_token_out.mm_serde import (
|
|
470
|
+
encode_mm_kwargs_item,
|
|
471
|
+
)
|
|
477
472
|
from vllm.model_executor.models.qwen2_vl import _create_qwen2vl_field_factory
|
|
478
473
|
from vllm.multimodal.inputs import MultiModalKwargsItems
|
|
479
474
|
except ImportError as exc:
|
|
@@ -59,6 +59,26 @@ def _build_param_type_index(
|
|
|
59
59
|
return index
|
|
60
60
|
|
|
61
61
|
|
|
62
|
+
def _extract_tool_names(tools: list[ToolSpec] | None) -> set[str] | None:
|
|
63
|
+
"""Set of declared tool names, or ``None`` when ``tools`` is empty.
|
|
64
|
+
|
|
65
|
+
``None`` disables name validation — mirroring vLLM's
|
|
66
|
+
``ParserEngine._is_valid_tool_name``, which returns ``True`` whenever
|
|
67
|
+
the request carries no tools. Accepts both flat ``ToolSpec`` and the
|
|
68
|
+
OpenAI ``{"type": "function", "function": {...}}`` envelope, like
|
|
69
|
+
``_build_param_type_index`` (but independent of it: a tool with no
|
|
70
|
+
``parameters.properties`` still counts as a known name).
|
|
71
|
+
"""
|
|
72
|
+
if not tools:
|
|
73
|
+
return None
|
|
74
|
+
names: set[str] = set()
|
|
75
|
+
for tool in tools:
|
|
76
|
+
spec = tool.get("function", tool) if isinstance(tool, dict) else None
|
|
77
|
+
if isinstance(spec, dict) and isinstance(spec.get("name"), str):
|
|
78
|
+
names.add(spec["name"])
|
|
79
|
+
return names
|
|
80
|
+
|
|
81
|
+
|
|
62
82
|
def _coerce_arg_value(
|
|
63
83
|
text: str, param_schema: dict[str, Any] | None
|
|
64
84
|
) -> tuple[Any, bool]:
|
|
@@ -434,7 +454,23 @@ def parse_glm(
|
|
|
434
454
|
arg_value_end_id: int,
|
|
435
455
|
tools: list[ToolSpec] | None = None,
|
|
436
456
|
) -> ParsedResponse:
|
|
437
|
-
"""Parse GLM completion tokens. Token-level thinking + arg_key/arg_value tool calls.
|
|
457
|
+
"""Parse GLM completion tokens. Token-level thinking + arg_key/arg_value tool calls.
|
|
458
|
+
|
|
459
|
+
When ``tools`` is passed, tool names are validated against it: a call
|
|
460
|
+
whose name isn't declared gets ``status=UNKNOWN_TOOL`` instead of
|
|
461
|
+
``OK``. This mirrors vLLM ≥ 0.24, where the ``glm45``/``glm47`` tool
|
|
462
|
+
parsers run with ``validate_tool_names=True`` and silently drop
|
|
463
|
+
unknown-name calls (``vllm/parser/glm47_moe.py``,
|
|
464
|
+
``ParserEngine._is_valid_tool_name``) — so a completion that yields no
|
|
465
|
+
tool call from the engine also yields no ``OK`` call here, and
|
|
466
|
+
downstream finish-reason promotion (``renderers/client.py``) agrees
|
|
467
|
+
with an OpenAI chat-completions client talking to the same engine.
|
|
468
|
+
Notably this covers the missing-``<arg_key>`` shape
|
|
469
|
+
(``<tool_call>bash\\n<arg_value>...</arg_value></tool_call>``): both
|
|
470
|
+
this parser and vLLM's resolve the whole block as the name, which then
|
|
471
|
+
fails validation. Without ``tools``, no validation happens (vLLM
|
|
472
|
+
behaves the same when the request carries no tools).
|
|
473
|
+
"""
|
|
438
474
|
ids = _strip_stop_tokens(token_ids, stop_ids)
|
|
439
475
|
|
|
440
476
|
reasoning = None
|
|
@@ -468,6 +504,7 @@ def parse_glm(
|
|
|
468
504
|
arg_value_end_id,
|
|
469
505
|
section_offset=parse_offset + tc_start,
|
|
470
506
|
param_index=_build_param_type_index(tools),
|
|
507
|
+
known_names=_extract_tool_names(tools),
|
|
471
508
|
)
|
|
472
509
|
else:
|
|
473
510
|
content_text = _decode(tokenizer, ids).strip()
|
|
@@ -491,6 +528,7 @@ def _parse_glm_tool_calls(
|
|
|
491
528
|
*,
|
|
492
529
|
section_offset: int,
|
|
493
530
|
param_index: dict[str, dict[str, dict[str, Any]]],
|
|
531
|
+
known_names: set[str] | None = None,
|
|
494
532
|
) -> list[ParsedToolCall]:
|
|
495
533
|
"""Parse GLM-style tool calls: name + arg_key/arg_value pairs, all by token ID."""
|
|
496
534
|
tool_calls: list[ParsedToolCall] = []
|
|
@@ -548,6 +586,11 @@ def _parse_glm_tool_calls(
|
|
|
548
586
|
j += 1
|
|
549
587
|
if not name:
|
|
550
588
|
status = ToolCallParseStatus.MISSING_NAME
|
|
589
|
+
elif known_names is not None and name not in known_names:
|
|
590
|
+
# vLLM ≥ 0.24 drops the call entirely here; we keep the
|
|
591
|
+
# attempt visible but deny it OK so consumers agree with
|
|
592
|
+
# the engine on "no tool was called."
|
|
593
|
+
status = ToolCallParseStatus.UNKNOWN_TOOL
|
|
551
594
|
elif structure_broke:
|
|
552
595
|
status = ToolCallParseStatus.MALFORMED_STRUCTURE
|
|
553
596
|
elif any_json_fallback:
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
"""GLM tool-name validation against the provided tools list.
|
|
2
|
+
|
|
3
|
+
vLLM 0.24 re-aliased ``glm45`` from the lenient ``Glm4MoeModelToolParser``
|
|
4
|
+
to ``Glm47MoeModelToolParser``, whose engine config sets
|
|
5
|
+
``validate_tool_names=True`` (``vllm/parser/glm47_moe.py``): a completed
|
|
6
|
+
``<tool_call>`` block whose name isn't in the request's tool list is
|
|
7
|
+
silently dropped — no tool call is emitted and the response reads as a
|
|
8
|
+
voluntary stop. Before this, unknown-name calls were surfaced to the
|
|
9
|
+
harness, which answered with a recoverable "unknown tool" error.
|
|
10
|
+
|
|
11
|
+
``parse_glm`` mirrors the ≥ 0.24 behavior when ``tools`` is passed:
|
|
12
|
+
unknown names get ``status=UNKNOWN_TOOL`` — never ``OK`` — so the
|
|
13
|
+
client-side stop→tool_calls finish-reason promotion
|
|
14
|
+
(``renderers/client.py``) agrees with what an OpenAI chat-completions
|
|
15
|
+
client sees from the engine, while the attempt itself stays visible for
|
|
16
|
+
verifier / RL-loss code. Without ``tools`` there is no validation, also
|
|
17
|
+
matching vLLM (``ParserEngine._is_valid_tool_name`` returns ``True``
|
|
18
|
+
when the request has no tools).
|
|
19
|
+
|
|
20
|
+
Cross-validated end-to-end against the real vLLM v0.23.0 and v0.24.0
|
|
21
|
+
parsers (vendored from the release tags) on identical completions.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
from functools import lru_cache
|
|
27
|
+
|
|
28
|
+
from renderers.base import ToolCallParseStatus
|
|
29
|
+
|
|
30
|
+
# Both GLM renderers share ``parse_glm`` and are served by the same
|
|
31
|
+
# strict vLLM parser (the ``glm45`` and ``glm47`` aliases both resolve
|
|
32
|
+
# to ``Glm47MoeModelToolParser`` in vLLM ≥ 0.24).
|
|
33
|
+
_MODELS = [
|
|
34
|
+
("THUDM/GLM-4.5-Air", "auto"),
|
|
35
|
+
("zai-org/GLM-5", "auto"),
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
_TOOLS = [
|
|
39
|
+
{
|
|
40
|
+
"name": "bash",
|
|
41
|
+
"description": "Run a shell command.",
|
|
42
|
+
"parameters": {
|
|
43
|
+
"type": "object",
|
|
44
|
+
"properties": {
|
|
45
|
+
"command": {"type": "string"},
|
|
46
|
+
"timeout": {"type": "integer"},
|
|
47
|
+
},
|
|
48
|
+
"required": ["command"],
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
# A declared tool with no parameters must still count as a known
|
|
52
|
+
# name (name lookup is independent of the param-type index).
|
|
53
|
+
{"name": "submit", "description": "Finish the episode."},
|
|
54
|
+
]
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@lru_cache(maxsize=None)
|
|
58
|
+
def _load(model: str, renderer_name: str):
|
|
59
|
+
from renderers import config_from_name, create_renderer
|
|
60
|
+
from renderers.base import load_tokenizer
|
|
61
|
+
|
|
62
|
+
tok = load_tokenizer(model)
|
|
63
|
+
return tok, create_renderer(tok, config_from_name(renderer_name))
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def pytest_generate_tests(metafunc):
|
|
67
|
+
if "model" in metafunc.fixturenames:
|
|
68
|
+
metafunc.parametrize(
|
|
69
|
+
"model,renderer_name",
|
|
70
|
+
_MODELS,
|
|
71
|
+
ids=[m for m, _ in _MODELS],
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _parse(model: str, renderer_name: str, text: str, tools):
|
|
76
|
+
tok, renderer = _load(model, renderer_name)
|
|
77
|
+
ids = tok.encode(text, add_special_tokens=False)
|
|
78
|
+
return renderer.parse_response(ids, tools=tools)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _statuses(parsed):
|
|
82
|
+
return [tc.status for tc in parsed.tool_calls]
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def test_known_name_is_ok(model, renderer_name):
|
|
86
|
+
parsed = _parse(
|
|
87
|
+
model,
|
|
88
|
+
renderer_name,
|
|
89
|
+
"<tool_call>bash\n"
|
|
90
|
+
"<arg_key>command</arg_key>\n<arg_value>pwd</arg_value>\n"
|
|
91
|
+
"</tool_call>",
|
|
92
|
+
_TOOLS,
|
|
93
|
+
)
|
|
94
|
+
assert _statuses(parsed) == [ToolCallParseStatus.OK]
|
|
95
|
+
assert parsed.tool_calls[0].name == "bash"
|
|
96
|
+
assert parsed.tool_calls[0].arguments == {"command": "pwd"}
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def test_known_name_without_parameters_is_ok(model, renderer_name):
|
|
100
|
+
parsed = _parse(model, renderer_name, "<tool_call>submit\n</tool_call>", _TOOLS)
|
|
101
|
+
assert _statuses(parsed) == [ToolCallParseStatus.OK]
|
|
102
|
+
assert parsed.tool_calls[0].name == "submit"
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def test_unknown_name_is_flagged(model, renderer_name):
|
|
106
|
+
parsed = _parse(
|
|
107
|
+
model,
|
|
108
|
+
renderer_name,
|
|
109
|
+
"<tool_call>read\n"
|
|
110
|
+
"<arg_key>lines</arg_key>\n<arg_value>10</arg_value>\n"
|
|
111
|
+
"</tool_call>",
|
|
112
|
+
_TOOLS,
|
|
113
|
+
)
|
|
114
|
+
assert _statuses(parsed) == [ToolCallParseStatus.UNKNOWN_TOOL]
|
|
115
|
+
# The attempt stays visible (unlike vLLM's silent drop): name and
|
|
116
|
+
# parsed arguments are preserved for verifier / RL-loss consumers.
|
|
117
|
+
assert parsed.tool_calls[0].name == "read"
|
|
118
|
+
assert parsed.tool_calls[0].arguments == {"lines": 10}
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def test_unknown_name_without_args_is_flagged(model, renderer_name):
|
|
122
|
+
parsed = _parse(model, renderer_name, "<tool_call>finish\n</tool_call>", _TOOLS)
|
|
123
|
+
assert _statuses(parsed) == [ToolCallParseStatus.UNKNOWN_TOOL]
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def test_missing_arg_key_block_is_flagged(model, renderer_name):
|
|
127
|
+
# No <arg_key> token ⇒ the whole block resolves as the name — both
|
|
128
|
+
# here and in vLLM's engine (unmatched terminals in TOOL_NAME state
|
|
129
|
+
# accumulate into the name) — and then fails validation.
|
|
130
|
+
parsed = _parse(
|
|
131
|
+
model,
|
|
132
|
+
renderer_name,
|
|
133
|
+
"<tool_call>bash\n<arg_value>pwd</arg_value>\n</tool_call>",
|
|
134
|
+
_TOOLS,
|
|
135
|
+
)
|
|
136
|
+
assert _statuses(parsed) == [ToolCallParseStatus.UNKNOWN_TOOL]
|
|
137
|
+
assert "bash" in (parsed.tool_calls[0].name or "")
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def test_mixed_calls_flag_only_unknown(model, renderer_name):
|
|
141
|
+
parsed = _parse(
|
|
142
|
+
model,
|
|
143
|
+
renderer_name,
|
|
144
|
+
"<tool_call>bash\n"
|
|
145
|
+
"<arg_key>command</arg_key>\n<arg_value>ls</arg_value>\n"
|
|
146
|
+
"</tool_call>"
|
|
147
|
+
"<tool_call>read\n"
|
|
148
|
+
"<arg_key>path</arg_key>\n<arg_value>x</arg_value>\n"
|
|
149
|
+
"</tool_call>",
|
|
150
|
+
_TOOLS,
|
|
151
|
+
)
|
|
152
|
+
assert _statuses(parsed) == [
|
|
153
|
+
ToolCallParseStatus.OK,
|
|
154
|
+
ToolCallParseStatus.UNKNOWN_TOOL,
|
|
155
|
+
]
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def test_no_tools_means_no_validation(model, renderer_name):
|
|
159
|
+
# vLLM skips name validation when the request carries no tools; so
|
|
160
|
+
# do we — this also keeps tools-less parse_response calls (the
|
|
161
|
+
# common test / SFT path) byte-for-byte backward compatible.
|
|
162
|
+
parsed = _parse(
|
|
163
|
+
model,
|
|
164
|
+
renderer_name,
|
|
165
|
+
"<tool_call>read\n"
|
|
166
|
+
"<arg_key>lines</arg_key>\n<arg_value>10</arg_value>\n"
|
|
167
|
+
"</tool_call>",
|
|
168
|
+
None,
|
|
169
|
+
)
|
|
170
|
+
assert _statuses(parsed) == [ToolCallParseStatus.OK]
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def test_openai_envelope_tools_are_recognized(model, renderer_name):
|
|
174
|
+
wrapped = [{"type": "function", "function": t} for t in _TOOLS]
|
|
175
|
+
parsed = _parse(
|
|
176
|
+
model,
|
|
177
|
+
renderer_name,
|
|
178
|
+
"<tool_call>bash\n"
|
|
179
|
+
"<arg_key>command</arg_key>\n<arg_value>pwd</arg_value>\n"
|
|
180
|
+
"</tool_call>",
|
|
181
|
+
wrapped,
|
|
182
|
+
)
|
|
183
|
+
assert _statuses(parsed) == [ToolCallParseStatus.OK]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|