@tduncan / Nvidia-Optimization-Stack-VLLM Mirror · read-only / commits / 3bbe2e1e6e
Fork 0

A high-throughput and memory-efficient inference and serving engine for LLMs

Mirrored from https://github.com/tduncan-lum/Nvidia-Optimization-Stack---VLLM.git

Synced Nov 6, 05:45 UTC

[Test] Consolidate tool parser unit tests to tests/tool_parsers (#37834)

Signed-off-by: Ben Browning <bbrownin@redhat.com>

Ben Browning committed Mar 23, 2026 at 00:24 UTC 3bbe2e1e6e36c87b49eff3919c6bb1fa93df2a4d
11 files changed +376 -353
tests/entrypoints/openai/tool_parsers/conftest.py deleted
-12
@@ -1,12 +0,0 @@
1 -# SPDX-License-Identifier: Apache-2.0
2 -# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 -
4 -import pytest
5 -from transformers import AutoTokenizer
6 -
7 -from vllm.tokenizers import TokenizerLike
8 -
9 -
10 -@pytest.fixture(scope="function")
11 -def default_tokenizer() -> TokenizerLike:
12 - return AutoTokenizer.from_pretrained("gpt2")
tests/entrypoints/openai/tool_parsers/test_granite4_tool_parser.py
-140
@@ -1,18 +1,9 @@
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 import json
4 -import random
5 -from typing import Any
4
5 import openai
6 import pytest
9 -from transformers import AutoTokenizer
10 -
11 -from vllm.entrypoints.openai.chat_completion.protocol import ChatCompletionRequest
12 -from vllm.entrypoints.openai.engine.protocol import (
13 - DeltaMessage,
14 -)
15 -from vllm.tool_parsers.granite4_tool_parser import Granite4ToolParser
7
8 from ....utils import RemoteOpenAIServer
9
@@ -38,137 +29,6 @@ def server():
29 yield server
30
31
41 -def create_complex_input(create_string_args: bool):
42 - coord_arg: dict | str = {
43 - "coordinates": [[23.54, 43.1], [-12.2, 54.3], [4, 5]],
44 - "coordinate_type": "latlong",
45 - }
46 - if create_string_args:
47 - # test granite behavior
48 - coord_arg = json.dumps(coord_arg)
49 - return [
50 - {"name": "find_bbox", "arguments": coord_arg},
51 - {
52 - "name": "get_stock_price",
53 - "arguments": {
54 - "symbol": "AAPL",
55 - "start_date": "2021-01-01",
56 - "end_date": "2021-12-31",
57 - },
58 - },
59 - {"name": "find_bbox", "arguments": coord_arg},
60 - ]
61 -
62 -
63 -def random_chunks(s: str, min_len: int, max_len: int):
64 - chunks = []
65 - i = 0
66 - n = len(s)
67 -
68 - while i < n:
69 - size = random.randint(min_len, max_len)
70 - chunks.append(s[i : i + size])
71 - i += size
72 -
73 - return chunks
74 -
75 -
76 -@pytest.fixture(scope="module")
77 -def tokenizer():
78 - return AutoTokenizer.from_pretrained(MODEL)
79 -
80 -
81 -# create a variety of input chunk sizes
82 -@pytest.mark.parametrize(
83 - "min_chunk, max_chunk",
84 - [
85 - (1, 1),
86 - (1, 2),
87 - (5, 7),
88 - (6, 20),
89 - ],
90 -)
91 -def test_tool_call_parser_complex(min_chunk: int, max_chunk: int, tokenizer):
92 - input_dicts = create_complex_input(True)
93 -
94 - formatted_tcs = [
95 - "<tool_call> " + json.dumps(call) + " </tool_call>" for call in input_dicts
96 - ]
97 -
98 - text_messages = [
99 - "Here goes the bbox call: \n",
100 - " Now the stock price call: \n ",
101 - " Now another bbox call: \n ",
102 - " See? I'm a helpful assistant.",
103 - ]
104 -
105 - test_input = (
106 - text_messages[0]
107 - + formatted_tcs[0]
108 - + text_messages[1]
109 - + formatted_tcs[1]
110 - + text_messages[2]
111 - + formatted_tcs[2]
112 - + text_messages[3]
113 - )
114 -
115 - any_chat_request = ChatCompletionRequest(
116 - seed=42,
117 - model=MODEL,
118 - messages=[],
119 - )
120 -
121 - parser = Granite4ToolParser(tokenizer=tokenizer)
122 -
123 - delta_messages = list[DeltaMessage]()
124 - for text in random_chunks(test_input, min_chunk, max_chunk):
125 - delta = parser.extract_tool_calls_streaming(
126 - previous_text="",
127 - current_text="",
128 - delta_text=text,
129 - previous_token_ids=[],
130 - current_token_ids=[],
131 - delta_token_ids=[],
132 - request=any_chat_request,
133 - )
134 - if delta is not None:
135 - delta_messages.append(delta)
136 -
137 - content = ""
138 - tool_calls = list[dict[str, Any]]()
139 -
140 - current_name = "__start__"
141 - current_args = ""
142 -
143 - for msg in delta_messages:
144 - if msg.content:
145 - content += msg.content
146 - for tool_call in msg.tool_calls:
147 - if delta_func := tool_call.function:
148 - if delta_func.name is not None:
149 - if current_name == "__start__":
150 - current_name = delta_func.name
151 -
152 - if delta_func.name != current_name:
153 - tool_calls.append(
154 - {
155 - "name": current_name,
156 - "arguments": json.loads(current_args),
157 - }
158 - )
159 - current_name = delta_func.name
160 - current_args = ""
161 -
162 - if delta_func.arguments:
163 - current_args += delta_func.arguments
164 -
165 - if current_name != "__start__":
166 - tool_calls.append({"name": current_name, "arguments": json.loads(current_args)})
167 -
168 - assert content == "".join(text_messages)
169 - assert tool_calls == create_complex_input(False)
170 -
171 -
32 tools = [
33 {
34 "type": "function",
tests/entrypoints/openai/tool_parsers/test_hermes_tool_parser.py
-201
@@ -9,8 +9,6 @@ import pytest_asyncio
9 from huggingface_hub import snapshot_download
10 from typing_extensions import TypedDict
11
12 -from vllm.entrypoints.openai.chat_completion.protocol import ChatCompletionRequest
13 -from vllm.tokenizers import TokenizerLike
12 from vllm.tool_parsers.abstract_tool_parser import ToolParser
13 from vllm.tool_parsers.granite4_tool_parser import Granite4ToolParser
14 from vllm.tool_parsers.hermes_tool_parser import Hermes2ProToolParser
@@ -325,202 +323,3 @@ async def test_streaming_product_tool_call(
323 print("\n[Streaming Product Test Passed]")
324 print(f"Reconstructed Tool Call: {reconstructed_tool_call['name']}")
325 print(f"Reconstructed Arguments: {arguments}")
328 -
329 -
330 -@pytest.fixture
331 -def qwen_tokenizer() -> TokenizerLike:
332 - from vllm.tokenizers import get_tokenizer
333 -
334 - return get_tokenizer("Qwen/Qwen3-32B")
335 -
336 -
337 -@pytest.fixture(params=CONFIGS.keys())
338 -def hermes_parser(request, qwen_tokenizer: TokenizerLike) -> ToolParser:
339 - config = CONFIGS[request.param]
340 - return config["tool_parser"](qwen_tokenizer)
341 -
342 -
343 -@pytest.fixture
344 -def any_chat_request() -> ChatCompletionRequest:
345 - return ChatCompletionRequest(
346 - seed=42,
347 - model="Qwen/Qwen3-32B",
348 - messages=[],
349 - )
350 -
351 -
352 -def test_hermes_parser_streaming_just_forward_text(
353 - qwen_tokenizer: TokenizerLike,
354 - hermes_parser: ToolParser,
355 - any_chat_request: ChatCompletionRequest,
356 -) -> None:
357 - text = """This is some prior text that has nothing to do with tool calling."""
358 - tokens = qwen_tokenizer.encode(text)
359 - previous_text = ""
360 - delta_messages = []
361 - for token in tokens:
362 - delta_text = qwen_tokenizer.decode([token])
363 - current_text = previous_text + delta_text
364 - delta = hermes_parser.extract_tool_calls_streaming(
365 - previous_text=previous_text,
366 - current_text=current_text,
367 - delta_text=delta_text,
368 - previous_token_ids=[],
369 - current_token_ids=[],
370 - delta_token_ids=[],
371 - request=any_chat_request,
372 - )
373 - previous_text = current_text
374 - delta_messages.append(delta)
375 -
376 - for delta in delta_messages:
377 - assert delta is not None
378 - assert not delta.tool_calls
379 -
380 - print(delta_messages)
381 - assert "".join([delta.content for delta in delta_messages]) == text
382 -
383 -
384 -def test_hermes_parser_streaming_failure_case_bug_19056(
385 - qwen_tokenizer: TokenizerLike,
386 - hermes_parser: ToolParser,
387 - any_chat_request: ChatCompletionRequest,
388 -) -> None:
389 - text = """<tool_call>
390 -{"name": "final_answer", "arguments": {"trigger": true}}
391 -</tool_call>"""
392 - tokens = qwen_tokenizer.encode(text)
393 - previous_text = ""
394 - delta_messages = []
395 - for token in tokens:
396 - text = qwen_tokenizer.decode([token])
397 - current_text = previous_text + text
398 - delta = hermes_parser.extract_tool_calls_streaming(
399 - previous_text=previous_text,
400 - current_text=current_text,
401 - delta_text=text,
402 - previous_token_ids=[],
403 - current_token_ids=[],
404 - delta_token_ids=[],
405 - request=any_chat_request,
406 - )
407 - previous_text = current_text
408 - if delta is not None:
409 - delta_messages.append(delta)
410 -
411 - assert delta_messages[0].tool_calls[0].function.name == "final_answer"
412 - tool_call_args = "".join(
413 - delta.tool_calls[0].function.arguments or "" for delta in delta_messages
414 - )
415 - assert tool_call_args == '{"trigger": true}'
416 -
417 -
418 -def test_hermes_parser_streaming(
419 - qwen_tokenizer: TokenizerLike,
420 - hermes_parser: ToolParser,
421 - any_chat_request: ChatCompletionRequest,
422 -) -> None:
423 - text = '<tool_call>\
424 -{"name": "get_current_temperature",\
425 -"arguments": {"location":\
426 -"San Francisco, California, United States", "unit": "celsius"}}\
427 -</tool_call>'
428 -
429 - tokens = qwen_tokenizer.encode(text)
430 - previous_text = ""
431 - delta_messages = []
432 - for token in tokens:
433 - text = qwen_tokenizer.decode([token])
434 - current_text = previous_text + text
435 - delta = hermes_parser.extract_tool_calls_streaming(
436 - previous_text=previous_text,
437 - current_text=current_text,
438 - delta_text=text,
439 - previous_token_ids=[],
440 - current_token_ids=[],
441 - delta_token_ids=[],
442 - request=any_chat_request,
443 - )
444 - previous_text = current_text
445 - if delta is not None:
446 - delta_messages.append(delta)
447 - print(delta_messages)
448 - assert delta_messages[0].tool_calls[0].function.name == "get_current_temperature"
449 - # load to normalize whitespace
450 - tool_call_args = json.loads(
451 - "".join(
452 - delta.tool_calls[0].function.arguments or "" for delta in delta_messages
453 - )
454 - )
455 - assert tool_call_args == {
456 - "location": "San Francisco, California, United States",
457 - "unit": "celsius",
458 - }
459 -
460 -
461 -def test_hermes_parser_non_streaming_no_tool_call(
462 - hermes_parser: ToolParser,
463 - any_chat_request: ChatCompletionRequest,
464 -) -> None:
465 - text = """This is not a tool call."""
466 - tool_call = hermes_parser.extract_tool_calls(
467 - model_output=text,
468 - request=any_chat_request,
469 - )
470 -
471 - assert tool_call is not None
472 - assert not tool_call.tools_called
473 -
474 -
475 -def test_hermes_parser_non_streaming_tool_call_between_tags(
476 - hermes_parser: ToolParser,
477 - any_chat_request: ChatCompletionRequest,
478 -) -> None:
479 - text = """<tool_call>
480 -{"name": "final_answer", "arguments": {"trigger": true}}
481 -</tool_call>"""
482 - tool_call = hermes_parser.extract_tool_calls(
483 - model_output=text,
484 - request=any_chat_request,
485 - )
486 -
487 - assert tool_call is not None
488 - assert tool_call.tools_called
489 - assert tool_call.tool_calls[0].function.name == "final_answer"
490 - assert tool_call.tool_calls[0].function.arguments == '{"trigger": true}'
491 -
492 -
493 -def test_hermes_parser_non_streaming_tool_call_until_eos(
494 - hermes_parser: ToolParser,
495 - any_chat_request: ChatCompletionRequest,
496 -) -> None:
497 - if isinstance(hermes_parser, Granite4ToolParser):
498 - pytest.skip(reason="The Granite4 tool parser enforces a complete response")
499 -
500 - text = """<tool_call>
501 -{"name": "final_answer", "arguments": {"trigger": true}}"""
502 - tool_call = hermes_parser.extract_tool_calls(
503 - model_output=text,
504 - request=any_chat_request,
505 - )
506 -
507 - assert tool_call is not None
508 - assert tool_call.tools_called
509 - assert tool_call.tool_calls[0].function.name == "final_answer"
510 - assert tool_call.tool_calls[0].function.arguments == '{"trigger": true}'
511 -
512 -
513 -def test_hermes_parser_non_streaming_tool_call_invalid_json(
514 - hermes_parser: ToolParser,
515 - any_chat_request: ChatCompletionRequest,
516 -) -> None:
517 - # Missing closing brace to trigger exception
518 - text = """<tool_call>
519 -{"name": "final_answer", "arguments": {"trigger": true}"""
520 - tool_call = hermes_parser.extract_tool_calls(
521 - model_output=text,
522 - request=any_chat_request,
523 - )
524 -
525 - assert tool_call is not None
526 - assert not tool_call.tools_called
tests/tool_parsers/test_gigachat3_tool_parser.py renamed
+9
@@ -4,6 +4,7 @@
4 import json
5
6 import pytest
7 +from transformers import AutoTokenizer
8
9 from tests.tool_parsers.utils import (
10 run_tool_extraction,
@@ -13,6 +14,14 @@ from vllm.entrypoints.openai.engine.protocol import FunctionCall
14 from vllm.tokenizers import TokenizerLike
15 from vllm.tool_parsers import ToolParser, ToolParserManager
16
17 +
18 +@pytest.fixture(scope="function")
19 +def default_tokenizer() -> TokenizerLike:
20 + """Override module-scoped default_tokenizer because gigachat tests
21 + mutate the tokenizer via ``add_tokens``."""
22 + return AutoTokenizer.from_pretrained("gpt2")
23 +
24 +
25 MSG_SEP_TOKEN = "<|message_sep|>\n\n"
26 ROLE_SEP_TOKEN = "<|role_sep|>\n"
27 EOS_TOKEN = "</s>"
tests/tool_parsers/test_granite4_tool_parser.py new
+147
@@ -0,0 +1,147 @@
1 +# SPDX-License-Identifier: Apache-2.0
2 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 +import json
4 +import random
5 +from typing import Any
6 +
7 +import pytest
8 +from transformers import AutoTokenizer
9 +
10 +from vllm.entrypoints.openai.chat_completion.protocol import ChatCompletionRequest
11 +from vllm.entrypoints.openai.engine.protocol import (
12 + DeltaMessage,
13 +)
14 +from vllm.tool_parsers.granite4_tool_parser import Granite4ToolParser
15 +
16 +MODEL = "ibm-granite/granite-4.0-h-tiny"
17 +
18 +
19 +def create_complex_input(create_string_args: bool):
20 + coord_arg: dict | str = {
21 + "coordinates": [[23.54, 43.1], [-12.2, 54.3], [4, 5]],
22 + "coordinate_type": "latlong",
23 + }
24 + if create_string_args:
25 + # test granite behavior
26 + coord_arg = json.dumps(coord_arg)
27 + return [
28 + {"name": "find_bbox", "arguments": coord_arg},
29 + {
30 + "name": "get_stock_price",
31 + "arguments": {
32 + "symbol": "AAPL",
33 + "start_date": "2021-01-01",
34 + "end_date": "2021-12-31",
35 + },
36 + },
37 + {"name": "find_bbox", "arguments": coord_arg},
38 + ]
39 +
40 +
41 +def random_chunks(s: str, min_len: int, max_len: int):
42 + chunks = []
43 + i = 0
44 + n = len(s)
45 +
46 + while i < n:
47 + size = random.randint(min_len, max_len)
48 + chunks.append(s[i : i + size])
49 + i += size
50 +
51 + return chunks
52 +
53 +
54 +@pytest.fixture(scope="module")
55 +def tokenizer():
56 + return AutoTokenizer.from_pretrained(MODEL)
57 +
58 +
59 +# create a variety of input chunk sizes
60 +@pytest.mark.parametrize(
61 + "min_chunk, max_chunk",
62 + [
63 + (1, 1),
64 + (1, 2),
65 + (5, 7),
66 + (6, 20),
67 + ],
68 +)
69 +def test_tool_call_parser_complex(min_chunk: int, max_chunk: int, tokenizer):
70 + input_dicts = create_complex_input(True)
71 +
72 + formatted_tcs = [
73 + "<tool_call> " + json.dumps(call) + " </tool_call>" for call in input_dicts
74 + ]
75 +
76 + text_messages = [
77 + "Here goes the bbox call: \n",
78 + " Now the stock price call: \n ",
79 + " Now another bbox call: \n ",
80 + " See? I'm a helpful assistant.",
81 + ]
82 +
83 + test_input = (
84 + text_messages[0]
85 + + formatted_tcs[0]
86 + + text_messages[1]
87 + + formatted_tcs[1]
88 + + text_messages[2]
89 + + formatted_tcs[2]
90 + + text_messages[3]
91 + )
92 +
93 + any_chat_request = ChatCompletionRequest(
94 + seed=42,
95 + model=MODEL,
96 + messages=[],
97 + )
98 +
99 + parser = Granite4ToolParser(tokenizer=tokenizer)
100 +
101 + delta_messages = list[DeltaMessage]()
102 + for text in random_chunks(test_input, min_chunk, max_chunk):
103 + delta = parser.extract_tool_calls_streaming(
104 + previous_text="",
105 + current_text="",
106 + delta_text=text,
107 + previous_token_ids=[],
108 + current_token_ids=[],
109 + delta_token_ids=[],
110 + request=any_chat_request,
111 + )
112 + if delta is not None:
113 + delta_messages.append(delta)
114 +
115 + content = ""
116 + tool_calls = list[dict[str, Any]]()
117 +
118 + current_name = "__start__"
119 + current_args = ""
120 +
121 + for msg in delta_messages:
122 + if msg.content:
123 + content += msg.content
124 + for tool_call in msg.tool_calls:
125 + if delta_func := tool_call.function:
126 + if delta_func.name is not None:
127 + if current_name == "__start__":
128 + current_name = delta_func.name
129 +
130 + if delta_func.name != current_name:
131 + tool_calls.append(
132 + {
133 + "name": current_name,
134 + "arguments": json.loads(current_args),
135 + }
136 + )
137 + current_name = delta_func.name
138 + current_args = ""
139 +
140 + if delta_func.arguments:
141 + current_args += delta_func.arguments
142 +
143 + if current_name != "__start__":
144 + tool_calls.append({"name": current_name, "arguments": json.loads(current_args)})
145 +
146 + assert content == "".join(text_messages)
147 + assert tool_calls == create_complex_input(False)
tests/tool_parsers/test_hermes_tool_parser.py new
+220
@@ -0,0 +1,220 @@
1 +# SPDX-License-Identifier: Apache-2.0
2 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 +
4 +import json
5 +
6 +import pytest
7 +
8 +from vllm.entrypoints.openai.chat_completion.protocol import ChatCompletionRequest
9 +from vllm.tokenizers import TokenizerLike
10 +from vllm.tool_parsers.abstract_tool_parser import ToolParser
11 +from vllm.tool_parsers.granite4_tool_parser import Granite4ToolParser
12 +from vllm.tool_parsers.hermes_tool_parser import Hermes2ProToolParser
13 +
14 +CONFIGS = {
15 + "llama": {
16 + "tool_parser": Hermes2ProToolParser,
17 + },
18 + "granite4": {
19 + "tool_parser": Granite4ToolParser,
20 + },
21 +}
22 +
23 +
24 +@pytest.fixture
25 +def qwen_tokenizer() -> TokenizerLike:
26 + from vllm.tokenizers import get_tokenizer
27 +
28 + return get_tokenizer("Qwen/Qwen3-32B")
29 +
30 +
31 +@pytest.fixture(params=CONFIGS.keys())
32 +def hermes_parser(request, qwen_tokenizer: TokenizerLike) -> ToolParser:
33 + config = CONFIGS[request.param]
34 + return config["tool_parser"](qwen_tokenizer)
35 +
36 +
37 +@pytest.fixture
38 +def any_chat_request() -> ChatCompletionRequest:
39 + return ChatCompletionRequest(
40 + seed=42,
41 + model="Qwen/Qwen3-32B",
42 + messages=[],
43 + )
44 +
45 +
46 +def test_hermes_parser_streaming_just_forward_text(
47 + qwen_tokenizer: TokenizerLike,
48 + hermes_parser: ToolParser,
49 + any_chat_request: ChatCompletionRequest,
50 +) -> None:
51 + text = """This is some prior text that has nothing to do with tool calling."""
52 + tokens = qwen_tokenizer.encode(text)
53 + previous_text = ""
54 + delta_messages = []
55 + for token in tokens:
56 + delta_text = qwen_tokenizer.decode([token])
57 + current_text = previous_text + delta_text
58 + delta = hermes_parser.extract_tool_calls_streaming(
59 + previous_text=previous_text,
60 + current_text=current_text,
61 + delta_text=delta_text,
62 + previous_token_ids=[],
63 + current_token_ids=[],
64 + delta_token_ids=[],
65 + request=any_chat_request,
66 + )
67 + previous_text = current_text
68 + delta_messages.append(delta)
69 +
70 + for delta in delta_messages:
71 + assert delta is not None
72 + assert not delta.tool_calls
73 +
74 + print(delta_messages)
75 + assert "".join([delta.content for delta in delta_messages]) == text
76 +
77 +
78 +def test_hermes_parser_streaming_failure_case_bug_19056(
79 + qwen_tokenizer: TokenizerLike,
80 + hermes_parser: ToolParser,
81 + any_chat_request: ChatCompletionRequest,
82 +) -> None:
83 + text = """<tool_call>
84 +{"name": "final_answer", "arguments": {"trigger": true}}
85 +</tool_call>"""
86 + tokens = qwen_tokenizer.encode(text)
87 + previous_text = ""
88 + delta_messages = []
89 + for token in tokens:
90 + text = qwen_tokenizer.decode([token])
91 + current_text = previous_text + text
92 + delta = hermes_parser.extract_tool_calls_streaming(
93 + previous_text=previous_text,
94 + current_text=current_text,
95 + delta_text=text,
96 + previous_token_ids=[],
97 + current_token_ids=[],
98 + delta_token_ids=[],
99 + request=any_chat_request,
100 + )
101 + previous_text = current_text
102 + if delta is not None:
103 + delta_messages.append(delta)
104 +
105 + assert delta_messages[0].tool_calls[0].function.name == "final_answer"
106 + tool_call_args = "".join(
107 + delta.tool_calls[0].function.arguments or "" for delta in delta_messages
108 + )
109 + assert tool_call_args == '{"trigger": true}'
110 +
111 +
112 +def test_hermes_parser_streaming(
113 + qwen_tokenizer: TokenizerLike,
114 + hermes_parser: ToolParser,
115 + any_chat_request: ChatCompletionRequest,
116 +) -> None:
117 + text = '<tool_call>\
118 +{"name": "get_current_temperature",\
119 +"arguments": {"location":\
120 +"San Francisco, California, United States", "unit": "celsius"}}\
121 +</tool_call>'
122 +
123 + tokens = qwen_tokenizer.encode(text)
124 + previous_text = ""
125 + delta_messages = []
126 + for token in tokens:
127 + text = qwen_tokenizer.decode([token])
128 + current_text = previous_text + text
129 + delta = hermes_parser.extract_tool_calls_streaming(
130 + previous_text=previous_text,
131 + current_text=current_text,
132 + delta_text=text,
133 + previous_token_ids=[],
134 + current_token_ids=[],
135 + delta_token_ids=[],
136 + request=any_chat_request,
137 + )
138 + previous_text = current_text
139 + if delta is not None:
140 + delta_messages.append(delta)
141 + print(delta_messages)
142 + assert delta_messages[0].tool_calls[0].function.name == "get_current_temperature"
143 + # load to normalize whitespace
144 + tool_call_args = json.loads(
145 + "".join(
146 + delta.tool_calls[0].function.arguments or "" for delta in delta_messages
147 + )
148 + )
149 + assert tool_call_args == {
150 + "location": "San Francisco, California, United States",
151 + "unit": "celsius",
152 + }
153 +
154 +
155 +def test_hermes_parser_non_streaming_no_tool_call(
156 + hermes_parser: ToolParser,
157 + any_chat_request: ChatCompletionRequest,
158 +) -> None:
159 + text = """This is not a tool call."""
160 + tool_call = hermes_parser.extract_tool_calls(
161 + model_output=text,
162 + request=any_chat_request,
163 + )
164 +
165 + assert tool_call is not None
166 + assert not tool_call.tools_called
167 +
168 +
169 +def test_hermes_parser_non_streaming_tool_call_between_tags(
170 + hermes_parser: ToolParser,
171 + any_chat_request: ChatCompletionRequest,
172 +) -> None:
173 + text = """<tool_call>
174 +{"name": "final_answer", "arguments": {"trigger": true}}
175 +</tool_call>"""
176 + tool_call = hermes_parser.extract_tool_calls(
177 + model_output=text,
178 + request=any_chat_request,
179 + )
180 +
181 + assert tool_call is not None
182 + assert tool_call.tools_called
183 + assert tool_call.tool_calls[0].function.name == "final_answer"
184 + assert tool_call.tool_calls[0].function.arguments == '{"trigger": true}'
185 +
186 +
187 +def test_hermes_parser_non_streaming_tool_call_until_eos(
188 + hermes_parser: ToolParser,
189 + any_chat_request: ChatCompletionRequest,
190 +) -> None:
191 + if isinstance(hermes_parser, Granite4ToolParser):
192 + pytest.skip(reason="The Granite4 tool parser enforces a complete response")
193 +
194 + text = """<tool_call>
195 +{"name": "final_answer", "arguments": {"trigger": true}}"""
196 + tool_call = hermes_parser.extract_tool_calls(
197 + model_output=text,
198 + request=any_chat_request,
199 + )
200 +
201 + assert tool_call is not None
202 + assert tool_call.tools_called
203 + assert tool_call.tool_calls[0].function.name == "final_answer"
204 + assert tool_call.tool_calls[0].function.arguments == '{"trigger": true}'
205 +
206 +
207 +def test_hermes_parser_non_streaming_tool_call_invalid_json(
208 + hermes_parser: ToolParser,
209 + any_chat_request: ChatCompletionRequest,
210 +) -> None:
211 + # Missing closing brace to trigger exception
212 + text = """<tool_call>
213 +{"name": "final_answer", "arguments": {"trigger": true}"""
214 + tool_call = hermes_parser.extract_tool_calls(
215 + model_output=text,
216 + request=any_chat_request,
217 + )
218 +
219 + assert tool_call is not None
220 + assert not tool_call.tools_called
tests/tool_parsers/test_hunyuan_a13b_tool_parser.py renamed
tests/tool_parsers/test_llama3_json_tool_parser.py renamed
tests/tool_parsers/test_llama4_pythonic_tool_parser.py renamed
tests/tool_parsers/test_olmo3_tool_parser.py renamed
tests/tool_parsers/test_pythonic_tool_parser.py renamed