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