renderers 0.1.9.dev7__tar.gz → 0.1.9.dev9__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.dev7 → renderers-0.1.9.dev9}/PKG-INFO +1 -1
- {renderers-0.1.9.dev7 → renderers-0.1.9.dev9}/renderers/_version.py +2 -2
- {renderers-0.1.9.dev7 → renderers-0.1.9.dev9}/renderers/base.py +10 -2
- {renderers-0.1.9.dev7 → renderers-0.1.9.dev9}/renderers/parsing.py +44 -1
- renderers-0.1.9.dev9/tests/test_glm_tool_name_validation.py +183 -0
- {renderers-0.1.9.dev7 → renderers-0.1.9.dev9}/uv.lock +3 -3
- {renderers-0.1.9.dev7 → renderers-0.1.9.dev9}/.github/workflows/publish-dev.yml +0 -0
- {renderers-0.1.9.dev7 → renderers-0.1.9.dev9}/.github/workflows/publish.yml +0 -0
- {renderers-0.1.9.dev7 → renderers-0.1.9.dev9}/.github/workflows/style.yml +0 -0
- {renderers-0.1.9.dev7 → renderers-0.1.9.dev9}/.github/workflows/test.yml +0 -0
- {renderers-0.1.9.dev7 → renderers-0.1.9.dev9}/.gitignore +0 -0
- {renderers-0.1.9.dev7 → renderers-0.1.9.dev9}/.pre-commit-config.yaml +0 -0
- {renderers-0.1.9.dev7 → renderers-0.1.9.dev9}/LICENSE +0 -0
- {renderers-0.1.9.dev7 → renderers-0.1.9.dev9}/README.md +0 -0
- {renderers-0.1.9.dev7 → renderers-0.1.9.dev9}/docs/renderer-config.md +0 -0
- {renderers-0.1.9.dev7 → renderers-0.1.9.dev9}/examples/README.md +0 -0
- {renderers-0.1.9.dev7 → renderers-0.1.9.dev9}/examples/sglang/multiturn_generate_sglang.py +0 -0
- {renderers-0.1.9.dev7 → renderers-0.1.9.dev9}/examples/sglang/online_multiturn_sglang.py +0 -0
- {renderers-0.1.9.dev7 → renderers-0.1.9.dev9}/examples/tinker/multiturn_generate_tinker.py +0 -0
- {renderers-0.1.9.dev7 → renderers-0.1.9.dev9}/examples/transformers/multiturn_generate_transformers.py +0 -0
- {renderers-0.1.9.dev7 → renderers-0.1.9.dev9}/examples/vllm/multiturn_generate_vllm.py +0 -0
- {renderers-0.1.9.dev7 → renderers-0.1.9.dev9}/pyproject.toml +0 -0
- {renderers-0.1.9.dev7 → renderers-0.1.9.dev9}/renderers/__init__.py +0 -0
- {renderers-0.1.9.dev7 → renderers-0.1.9.dev9}/renderers/client.py +0 -0
- {renderers-0.1.9.dev7 → renderers-0.1.9.dev9}/renderers/configs.py +0 -0
- {renderers-0.1.9.dev7 → renderers-0.1.9.dev9}/renderers/deepseek_r1.py +0 -0
- {renderers-0.1.9.dev7 → renderers-0.1.9.dev9}/renderers/deepseek_v3.py +0 -0
- {renderers-0.1.9.dev7 → renderers-0.1.9.dev9}/renderers/default.py +0 -0
- {renderers-0.1.9.dev7 → renderers-0.1.9.dev9}/renderers/glm45.py +0 -0
- {renderers-0.1.9.dev7 → renderers-0.1.9.dev9}/renderers/glm5.py +0 -0
- {renderers-0.1.9.dev7 → renderers-0.1.9.dev9}/renderers/gpt_oss.py +0 -0
- {renderers-0.1.9.dev7 → renderers-0.1.9.dev9}/renderers/hy3.py +0 -0
- {renderers-0.1.9.dev7 → renderers-0.1.9.dev9}/renderers/kimi_k2.py +0 -0
- {renderers-0.1.9.dev7 → renderers-0.1.9.dev9}/renderers/kimi_k25.py +0 -0
- {renderers-0.1.9.dev7 → renderers-0.1.9.dev9}/renderers/laguna_xs2.py +0 -0
- {renderers-0.1.9.dev7 → renderers-0.1.9.dev9}/renderers/llama_3.py +0 -0
- {renderers-0.1.9.dev7 → renderers-0.1.9.dev9}/renderers/minimax_m2.py +0 -0
- {renderers-0.1.9.dev7 → renderers-0.1.9.dev9}/renderers/nemotron3.py +0 -0
- {renderers-0.1.9.dev7 → renderers-0.1.9.dev9}/renderers/parsers.py +0 -0
- {renderers-0.1.9.dev7 → renderers-0.1.9.dev9}/renderers/prime_qwen3.py +0 -0
- {renderers-0.1.9.dev7 → renderers-0.1.9.dev9}/renderers/qwen3.py +0 -0
- {renderers-0.1.9.dev7 → renderers-0.1.9.dev9}/renderers/qwen35.py +0 -0
- {renderers-0.1.9.dev7 → renderers-0.1.9.dev9}/renderers/qwen36.py +0 -0
- {renderers-0.1.9.dev7 → renderers-0.1.9.dev9}/renderers/qwen3_vl.py +0 -0
- {renderers-0.1.9.dev7 → renderers-0.1.9.dev9}/tests/conftest.py +0 -0
- {renderers-0.1.9.dev7 → renderers-0.1.9.dev9}/tests/test_bridge.py +0 -0
- {renderers-0.1.9.dev7 → renderers-0.1.9.dev9}/tests/test_build_helpers.py +0 -0
- {renderers-0.1.9.dev7 → renderers-0.1.9.dev9}/tests/test_client.py +0 -0
- {renderers-0.1.9.dev7 → renderers-0.1.9.dev9}/tests/test_deepseek_r1.py +0 -0
- {renderers-0.1.9.dev7 → renderers-0.1.9.dev9}/tests/test_disabled_thinking_stability.py +0 -0
- {renderers-0.1.9.dev7 → renderers-0.1.9.dev9}/tests/test_gpt_oss_harmony_parity.py +0 -0
- {renderers-0.1.9.dev7 → renderers-0.1.9.dev9}/tests/test_hy3.py +0 -0
- {renderers-0.1.9.dev7 → renderers-0.1.9.dev9}/tests/test_incremental.py +0 -0
- {renderers-0.1.9.dev7 → renderers-0.1.9.dev9}/tests/test_is_content.py +0 -0
- {renderers-0.1.9.dev7 → renderers-0.1.9.dev9}/tests/test_kimi_k25_tool_schema.py +0 -0
- {renderers-0.1.9.dev7 → renderers-0.1.9.dev9}/tests/test_laguna_xs21.py +0 -0
- {renderers-0.1.9.dev7 → renderers-0.1.9.dev9}/tests/test_llama_3.py +0 -0
- {renderers-0.1.9.dev7 → renderers-0.1.9.dev9}/tests/test_load_tokenizer.py +0 -0
- {renderers-0.1.9.dev7 → renderers-0.1.9.dev9}/tests/test_message_indices.py +0 -0
- {renderers-0.1.9.dev7 → renderers-0.1.9.dev9}/tests/test_message_tool_names.py +0 -0
- {renderers-0.1.9.dev7 → renderers-0.1.9.dev9}/tests/test_multimodal.py +0 -0
- {renderers-0.1.9.dev7 → renderers-0.1.9.dev9}/tests/test_nemotron3_parity.py +0 -0
- {renderers-0.1.9.dev7 → renderers-0.1.9.dev9}/tests/test_nemotron3_ultra.py +0 -0
- {renderers-0.1.9.dev7 → renderers-0.1.9.dev9}/tests/test_parse_response.py +0 -0
- {renderers-0.1.9.dev7 → renderers-0.1.9.dev9}/tests/test_parse_response_robustness.py +0 -0
- {renderers-0.1.9.dev7 → renderers-0.1.9.dev9}/tests/test_parsers.py +0 -0
- {renderers-0.1.9.dev7 → renderers-0.1.9.dev9}/tests/test_preserve_thinking.py +0 -0
- {renderers-0.1.9.dev7 → renderers-0.1.9.dev9}/tests/test_prime_qwen3_parity.py +0 -0
- {renderers-0.1.9.dev7 → renderers-0.1.9.dev9}/tests/test_qwen35_size_coverage.py +0 -0
- {renderers-0.1.9.dev7 → renderers-0.1.9.dev9}/tests/test_render_ids.py +0 -0
- {renderers-0.1.9.dev7 → renderers-0.1.9.dev9}/tests/test_renderer_config.py +0 -0
- {renderers-0.1.9.dev7 → renderers-0.1.9.dev9}/tests/test_renderer_config_parity.py +0 -0
- {renderers-0.1.9.dev7 → renderers-0.1.9.dev9}/tests/test_roundtrip.py +0 -0
- {renderers-0.1.9.dev7 → renderers-0.1.9.dev9}/tests/test_sampled_mask.py +0 -0
- {renderers-0.1.9.dev7 → renderers-0.1.9.dev9}/tests/test_tokens_per_message.py +0 -0
- {renderers-0.1.9.dev7 → renderers-0.1.9.dev9}/tests/test_tool_arg_type_preservation.py +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.dev9'
|
|
22
|
+
__version_tuple__ = version_tuple = (0, 1, 9, 'dev9')
|
|
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
|
|
@@ -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]
|
|
@@ -1464,11 +1464,11 @@ wheels = [
|
|
|
1464
1464
|
|
|
1465
1465
|
[[package]]
|
|
1466
1466
|
name = "setuptools"
|
|
1467
|
-
version = "
|
|
1467
|
+
version = "83.0.0"
|
|
1468
1468
|
source = { registry = "https://pypi.org/simple" }
|
|
1469
|
-
sdist = { url = "https://files.pythonhosted.org/packages/
|
|
1469
|
+
sdist = { url = "https://files.pythonhosted.org/packages/34/26/f5d29e25ffdb535afef2d35cdb55b325298f96debd670da4c325e08d70f4/setuptools-83.0.0.tar.gz", hash = "sha256:025bccbbf0fa05b6192bc64ae1e7b16e001fd6d6d4d5de03c97b1c1ade523bef", size = 1154254, upload-time = "2026-07-04T15:31:22.699Z" }
|
|
1470
1470
|
wheels = [
|
|
1471
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
1471
|
+
{ url = "https://files.pythonhosted.org/packages/5d/40/e1e72872c6354b306daef1703549e8e83b4d43cfea356311bf722a043752/setuptools-83.0.0-py3-none-any.whl", hash = "sha256:29b23c360f22f414dc7336bb39178cc7bcbf6021ed2733cde173f09dba19abb3", size = 1008090, upload-time = "2026-07-04T15:31:20.885Z" },
|
|
1472
1472
|
]
|
|
1473
1473
|
|
|
1474
1474
|
[[package]]
|
|
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
|