chore: Upstream Cohere parser fixes + tests (#51998)
Signed-off-by: Jason Ozuzu <jasonozuzu@cohere.com>
JasonCohere committed
Aug 13, 2026 at 13:05 UTC
5fee0a872dc31dd3476d61bb72b782b2d9d47492
10 files changed
+533
-54
requirements/test/cpu.txt
+1
-1
@@ -126,7 +126,7 @@ cloudpickle==3.1.2
126
# via -r requirements/test/../common.txt
127
cohere==7.0.8
128
# via -r requirements/test/cuda.in
129
-cohere-melody==0.9.0
129
+cohere-melody==0.11.1
130
# via -r requirements/test/cuda.in
131
colorama==0.4.6
132
# via
requirements/test/cuda.in
+1
-1
@@ -71,7 +71,7 @@ gpt-oss >= 0.0.7; python_version > '3.11'
71
72
perceptron # required for isaac test
73
kaldi-native-fbank >= 1.18.7 # required for fireredasr2 test
74
-cohere_melody>=0.9.0 # required for cohere command reasoning parser test
74
+cohere_melody>=0.11.1 # required for cohere command reasoning/tool parser tests
75
cohere>=7.0.0 # required for cohere chat v2 api tests (protocol/serving import cohere.types)
76
77
# Newer versions of datasets require torchcoded, that makes the tests fail in CI because of a missing library.
requirements/test/cuda.txt
+1
-1
@@ -131,7 +131,7 @@ cloudpickle==3.1.2
131
# via -r requirements/test/../common.txt
132
cohere==7.0.8
133
# via -r requirements/test/cuda.in
134
-cohere-melody==0.9.0
134
+cohere-melody==0.11.1
135
# via -r requirements/test/cuda.in
136
colorama==0.4.6
137
# via
requirements/test/rocm.in
+1
-1
@@ -69,7 +69,7 @@ gpt-oss>=0.0.7; python_version > '3.11'
69
70
perceptron # required for isaac test
71
kaldi-native-fbank>=1.18.7 # required for fireredasr2 test
72
-cohere_melody>=0.9.0 # required for cohere command reasoning parser test
72
+cohere_melody>=0.11.1 # required for cohere command reasoning/tool parser tests
73
cohere>=7.0.0 # required for cohere chat v2 api tests (protocol/serving import cohere.types)
74
75
# Newer versions of datasets require torchcoded, that makes the tests fail in CI because of a missing library.
requirements/test/rocm.txt
+1
-1
@@ -131,7 +131,7 @@ cloudpickle==3.1.2
131
# tilelang
132
cohere==7.0.8
133
# via -r requirements/test/rocm.in
134
-cohere-melody==0.9.0
134
+cohere-melody==0.11.1
135
# via -r requirements/test/rocm.in
136
colorama==0.4.6
137
# via
tests/reasoning/test_cohere_command_reasoning_parser.py
+3
-38
@@ -5,7 +5,7 @@ from __future__ import annotations
5
6
import json
7
from collections import UserDict
8
-from dataclasses import dataclass, field
8
+from dataclasses import dataclass
9
from types import SimpleNamespace
10
from typing import Any
11
@@ -34,20 +34,12 @@ from vllm.reasoning.cohere_command_reasoning_parser import (
34
from vllm.sampling_params import StructuredOutputsParams
35
36
37
-@dataclass
38
-class ExpectedToolCall:
39
- id: str
40
- name: str
41
- arguments: dict
42
-
43
-
37
@dataclass
38
class ReasoningCase:
39
parser_cls: Any
40
model_output: str
41
expected_reasoning: str | None
42
expected_content: str | None
50
- expected_tool_calls: list[ExpectedToolCall] = field(default_factory=list)
43
44
45
REASONING_CASES = [
@@ -67,9 +59,6 @@ REASONING_CASES = [
59
{"tool_call_id": "0", "tool_name": "foo", "parameters": {"query": "query1"}}
60
]
61
<|END_ACTION|>""",
70
- expected_tool_calls=[
71
- ExpectedToolCall(id="0", name="foo", arguments={"query": "query1"}),
72
- ],
62
),
63
id="cmd3-single_tool_call",
64
),
@@ -89,9 +78,6 @@ REASONING_CASES = [
78
{"tool_call_id": "0", "tool_name": "foo", "parameters": {"query": "query1"}}
79
]
80
<|END_ACTION|>""",
92
- expected_tool_calls=[
93
- ExpectedToolCall(id="0", name="foo", arguments={"query": "query1"}),
94
- ],
81
),
82
id="cmd4-single_tool_call",
83
),
@@ -236,29 +222,8 @@ class TestExtractReasoning:
222
assert reasoning == case.expected_reasoning
223
224
content = "".join(content_parts) if content_parts else None
239
- if case.expected_tool_calls:
240
- assert content is None or content == ""
241
- else:
242
- assert content == case.expected_content
243
-
244
- accumulated: dict[int, dict] = {}
245
- for d in tool_call_deltas:
246
- idx = d["index"]
247
- if idx not in accumulated:
248
- accumulated[idx] = {"id": "", "name": "", "arguments": ""}
249
- if d["id"]:
250
- accumulated[idx]["id"] = d["id"]
251
- if d["name"]:
252
- accumulated[idx]["name"] = d["name"]
253
- if d["arguments"]:
254
- accumulated[idx]["arguments"] += d["arguments"]
255
-
256
- assert len(accumulated) == len(case.expected_tool_calls)
257
- for i, expected_tc in enumerate(case.expected_tool_calls):
258
- tc = accumulated[i]
259
- assert tc["id"] == expected_tc.id
260
- assert tc["name"] == expected_tc.name
261
- assert json.loads(tc["arguments"]) == expected_tc.arguments
225
+ assert content == case.expected_content
226
+ assert tool_call_deltas == []
227
228
229
class TestIsReasoningEnd:
tests/tool_parsers/test_cohere_command_tool_parser.py
new
+510
@@ -0,0 +1,510 @@
1
+# SPDX-License-Identifier: Apache-2.0
2
+# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
+
4
+from __future__ import annotations
5
+
6
+import json
7
+from dataclasses import dataclass, field
8
+from typing import Any
9
+
10
+import pytest
11
+
12
+from vllm.entrypoints.openai.chat_completion.protocol import ChatCompletionRequest
13
+from vllm.tool_parsers.cohere_command_tool_parser import (
14
+ CohereCommand3ToolParser,
15
+ CohereCommand4ToolParser,
16
+)
17
+
18
+
19
+@dataclass
20
+class ExpectedToolCall:
21
+ id: str
22
+ name: str
23
+ arguments: dict
24
+
25
+
26
+@dataclass
27
+class ToolCallCase:
28
+ parser_cls: Any
29
+ model_output: str
30
+ expected_tool_calls: list[ExpectedToolCall] = field(default_factory=list)
31
+ expected_reasoning: str | None = None
32
+ expected_content: str | None = None
33
+
34
+
35
+TOOL_CALL_CASES = [
36
+ pytest.param(
37
+ ToolCallCase(
38
+ parser_cls=CohereCommand3ToolParser,
39
+ model_output="""\
40
+<|START_THINKING|> i will call foo with query1<|END_THINKING|><|START_ACTION|>
41
+[
42
+ {"tool_call_id": "0", "tool_name": "foo", "parameters": {"query": "query1"}}
43
+]
44
+<|END_ACTION|>""",
45
+ expected_tool_calls=[
46
+ ExpectedToolCall(id="0", name="foo", arguments={"query": "query1"}),
47
+ ],
48
+ expected_reasoning="i will call foo with query1",
49
+ ),
50
+ id="cmd3-single_tool_call",
51
+ ),
52
+ pytest.param(
53
+ ToolCallCase(
54
+ parser_cls=CohereCommand4ToolParser,
55
+ model_output="""\
56
+<|START_THINKING|> i will call foo with query1<|END_THINKING|><|START_ACTION|>
57
+[
58
+ {"tool_call_id": "0", "tool_name": "foo", "parameters": {"query": "query1"}}
59
+]
60
+<|END_ACTION|>""",
61
+ expected_tool_calls=[
62
+ ExpectedToolCall(id="0", name="foo", arguments={"query": "query1"}),
63
+ ],
64
+ expected_reasoning="i will call foo with query1",
65
+ ),
66
+ id="cmd4-single_tool_call",
67
+ ),
68
+ pytest.param(
69
+ ToolCallCase(
70
+ parser_cls=CohereCommand3ToolParser,
71
+ model_output="""\
72
+<|START_THINKING|>This is a rainbow <co>emoji: 🌈</co: 0:[1]><|END_THINKING|>
73
+<|START_RESPONSE|>foo <co>bar</co: 0:[1,2],1:[3,4]><|END_RESPONSE|>""",
74
+ expected_reasoning="This is a rainbow emoji: 🌈",
75
+ expected_content="foo bar",
76
+ ),
77
+ id="cmd3-citations_no_tool_calls",
78
+ ),
79
+ pytest.param(
80
+ ToolCallCase(
81
+ parser_cls=CohereCommand4ToolParser,
82
+ model_output="""\
83
+<|START_THINKING|>This is a rainbow <co>emoji: 🌈</co: 0:[1]><|END_THINKING|>
84
+<|START_RESPONSE|>foo <co>bar</co: 0:[1,2],1:[3,4]><|END_RESPONSE|>""",
85
+ expected_reasoning="This is a rainbow emoji: 🌈",
86
+ expected_content="foo bar",
87
+ ),
88
+ id="cmd4-citations_no_tool_calls",
89
+ ),
90
+ pytest.param(
91
+ ToolCallCase(
92
+ parser_cls=CohereCommand3ToolParser,
93
+ model_output="""\
94
+<|START_THINKING|>first I think about foo<|END_THINKING|><|START_ACTION|>
95
+[
96
+ {"tool_call_id": "0", "tool_name": "foo", "parameters": {"query": "query1"}},
97
+ {"tool_call_id": "1", "tool_name": "bar", "parameters": {"x": 42}}
98
+]
99
+<|END_ACTION|>""",
100
+ expected_tool_calls=[
101
+ ExpectedToolCall(id="0", name="foo", arguments={"query": "query1"}),
102
+ ExpectedToolCall(id="1", name="bar", arguments={"x": 42}),
103
+ ],
104
+ expected_reasoning="first I think about foo",
105
+ ),
106
+ id="cmd3-multiple_tool_calls",
107
+ ),
108
+ pytest.param(
109
+ ToolCallCase(
110
+ parser_cls=CohereCommand4ToolParser,
111
+ model_output="""\
112
+<|START_THINKING|>first I think about foo<|END_THINKING|><|START_ACTION|>
113
+[
114
+ {"tool_call_id": "0", "tool_name": "foo", "parameters": {"query": "query1"}},
115
+ {"tool_call_id": "1", "tool_name": "bar", "parameters": {"x": 42}}
116
+]
117
+<|END_ACTION|>""",
118
+ expected_tool_calls=[
119
+ ExpectedToolCall(id="0", name="foo", arguments={"query": "query1"}),
120
+ ExpectedToolCall(id="1", name="bar", arguments={"x": 42}),
121
+ ],
122
+ expected_reasoning="first I think about foo",
123
+ ),
124
+ id="cmd4-multiple_tool_calls",
125
+ ),
126
+ pytest.param(
127
+ ToolCallCase(
128
+ parser_cls=CohereCommand3ToolParser,
129
+ model_output="""\
130
+<|START_THINKING|>just think, no response<|END_THINKING|>""",
131
+ expected_reasoning="just think, no response",
132
+ ),
133
+ id="cmd3-reasoning_only",
134
+ ),
135
+ pytest.param(
136
+ ToolCallCase(
137
+ parser_cls=CohereCommand4ToolParser,
138
+ model_output="""\
139
+<|START_THINKING|>just think, no response<|END_THINKING|>""",
140
+ expected_reasoning="just think, no response",
141
+ ),
142
+ id="cmd4-reasoning_only",
143
+ ),
144
+]
145
+
146
+
147
+class MockCohereTokenizer:
148
+ """Minimal byte-level stand-in for the Cohere tokenizer."""
149
+
150
+ def get_vocab(self) -> dict[str, int]:
151
+ return {}
152
+
153
+ def encode(self, text: str, add_special_tokens: bool = False) -> list[int]:
154
+ return list(text.encode("utf-8"))
155
+
156
+ def decode(self, ids: list[int], skip_special_tokens: bool = False) -> str:
157
+ return bytes(ids).decode("utf-8", errors="replace")
158
+
159
+
160
+@pytest.fixture(scope="module")
161
+def tokenizer() -> MockCohereTokenizer:
162
+ return MockCohereTokenizer()
163
+
164
+
165
+@pytest.fixture
166
+def request_obj() -> ChatCompletionRequest:
167
+ return ChatCompletionRequest(messages=[], model="test-model")
168
+
169
+
170
+REPLACEMENT_CHAR = "\ufffd"
171
+
172
+
173
+def _token_deltas(tokenizer: MockCohereTokenizer, text: str) -> list[str]:
174
+ """Decode per-token string deltas, buffering incomplete multi-byte chars."""
175
+ ids = tokenizer.encode(text, add_special_tokens=False)
176
+ deltas: list[str] = []
177
+ prev = ""
178
+ for i in range(1, len(ids) + 1):
179
+ current = tokenizer.decode(ids[:i], skip_special_tokens=False)
180
+ if current.endswith(REPLACEMENT_CHAR):
181
+ continue
182
+ delta = current[len(prev) :]
183
+ if delta:
184
+ deltas.append(delta)
185
+ prev = current
186
+ return deltas
187
+
188
+
189
+@dataclass
190
+class StreamingResult:
191
+ tool_calls: dict[int, dict]
192
+ reasoning: str | None
193
+ content: str | None
194
+
195
+
196
+def _run_streaming_over_deltas(
197
+ parser,
198
+ deltas: list[str],
199
+ request_obj: ChatCompletionRequest,
200
+) -> StreamingResult:
201
+ accumulated: dict[int, dict] = {}
202
+ reasoning_parts: list[str] = []
203
+ content_parts: list[str] = []
204
+ previous_text = ""
205
+
206
+ for token_str in deltas:
207
+ current_text = previous_text + token_str
208
+ delta = parser.extract_tool_calls_streaming(
209
+ previous_text=previous_text,
210
+ current_text=current_text,
211
+ delta_text=token_str,
212
+ previous_token_ids=[],
213
+ current_token_ids=[],
214
+ delta_token_ids=[],
215
+ request=request_obj,
216
+ )
217
+ if delta is not None:
218
+ if delta.reasoning is not None:
219
+ reasoning_parts.append(delta.reasoning)
220
+ if delta.content is not None:
221
+ content_parts.append(delta.content)
222
+ for tc in delta.tool_calls:
223
+ idx = tc.index
224
+ if idx not in accumulated:
225
+ accumulated[idx] = {"id": "", "name": "", "arguments": ""}
226
+ if tc.id:
227
+ accumulated[idx]["id"] = tc.id
228
+ if tc.function and tc.function.name:
229
+ accumulated[idx]["name"] = tc.function.name
230
+ if tc.function and tc.function.arguments:
231
+ accumulated[idx]["arguments"] += tc.function.arguments
232
+ previous_text = current_text
233
+
234
+ return StreamingResult(
235
+ tool_calls=accumulated,
236
+ reasoning="".join(reasoning_parts) if reasoning_parts else None,
237
+ content="".join(content_parts) if content_parts else None,
238
+ )
239
+
240
+
241
+def _run_streaming(
242
+ parser,
243
+ tokenizer: MockCohereTokenizer,
244
+ model_output: str,
245
+ request_obj: ChatCompletionRequest,
246
+) -> StreamingResult:
247
+ return _run_streaming_over_deltas(
248
+ parser,
249
+ _token_deltas(tokenizer, model_output),
250
+ request_obj,
251
+ )
252
+
253
+
254
+@pytest.mark.parametrize("case", TOOL_CALL_CASES)
255
+class TestExtractToolCalls:
256
+ def test_streaming(
257
+ self,
258
+ tokenizer: MockCohereTokenizer,
259
+ request_obj: ChatCompletionRequest,
260
+ case: ToolCallCase,
261
+ ):
262
+ parser = case.parser_cls(tokenizer)
263
+ streamed = _run_streaming(parser, tokenizer, case.model_output, request_obj)
264
+
265
+ assert len(streamed.tool_calls) == len(case.expected_tool_calls)
266
+
267
+ for i, expected_tc in enumerate(case.expected_tool_calls):
268
+ tc = streamed.tool_calls[i]
269
+ assert tc["id"] == expected_tc.id
270
+ assert tc["name"] == expected_tc.name
271
+ assert json.loads(tc["arguments"]) == expected_tc.arguments
272
+
273
+ def test_streaming_reasoning(
274
+ self,
275
+ tokenizer: MockCohereTokenizer,
276
+ request_obj: ChatCompletionRequest,
277
+ case: ToolCallCase,
278
+ ):
279
+ parser = case.parser_cls(tokenizer)
280
+ streamed = _run_streaming(parser, tokenizer, case.model_output, request_obj)
281
+
282
+ assert streamed.reasoning == case.expected_reasoning
283
+
284
+ def test_streaming_content(
285
+ self,
286
+ tokenizer: MockCohereTokenizer,
287
+ request_obj: ChatCompletionRequest,
288
+ case: ToolCallCase,
289
+ ):
290
+ parser = case.parser_cls(tokenizer)
291
+ streamed = _run_streaming(parser, tokenizer, case.model_output, request_obj)
292
+
293
+ assert streamed.content == case.expected_content
294
+
295
+ def test_nonstreaming(
296
+ self,
297
+ request_obj: ChatCompletionRequest,
298
+ tokenizer: MockCohereTokenizer,
299
+ case: ToolCallCase,
300
+ ):
301
+ parser = case.parser_cls(tokenizer)
302
+ result = parser.extract_tool_calls(case.model_output, request_obj)
303
+
304
+ assert result.tools_called == (len(case.expected_tool_calls) > 0)
305
+ assert len(result.tool_calls) == len(case.expected_tool_calls)
306
+
307
+ for actual_tc, expected_tc in zip(result.tool_calls, case.expected_tool_calls):
308
+ assert actual_tc.type == "function"
309
+ assert actual_tc.function.name == expected_tc.name
310
+ assert json.loads(actual_tc.function.arguments) == expected_tc.arguments
311
+
312
+ def test_streaming_nonstreaming_agree(
313
+ self,
314
+ tokenizer: MockCohereTokenizer,
315
+ request_obj: ChatCompletionRequest,
316
+ case: ToolCallCase,
317
+ ):
318
+ parser_streaming = case.parser_cls(tokenizer)
319
+ parser_nonstreaming = case.parser_cls(tokenizer)
320
+
321
+ streamed = _run_streaming(
322
+ parser_streaming,
323
+ tokenizer,
324
+ case.model_output,
325
+ request_obj,
326
+ )
327
+ result = parser_nonstreaming.extract_tool_calls(
328
+ case.model_output,
329
+ request_obj,
330
+ )
331
+
332
+ assert len(streamed.tool_calls) == len(result.tool_calls)
333
+
334
+ for i, actual_tc in enumerate(result.tool_calls):
335
+ assert streamed.tool_calls[i]["name"] == actual_tc.function.name
336
+ assert json.loads(streamed.tool_calls[i]["arguments"]) == json.loads(
337
+ actual_tc.function.arguments
338
+ )
339
+
340
+
341
+SPECIAL_TOKEN_MARKERS = (
342
+ "<|START_THINKING|>",
343
+ "<|END_THINKING|>",
344
+ "<|START_RESPONSE|>",
345
+ "<|END_RESPONSE|>",
346
+ "<|START_ACTION|>",
347
+ "<|END_ACTION|>",
348
+ "<|START_TEXT|>",
349
+ "<|END_TEXT|>",
350
+)
351
+
352
+
353
+def _multi_token_deltas(
354
+ tokenizer: MockCohereTokenizer,
355
+ text: str,
356
+ chunk_size: int,
357
+) -> list[str]:
358
+ ids = tokenizer.encode(text, add_special_tokens=False)
359
+ deltas: list[str] = []
360
+ prev = ""
361
+ i = 0
362
+ while i < len(ids):
363
+ end = min(len(ids), i + chunk_size)
364
+ current = tokenizer.decode(ids[:end], skip_special_tokens=False)
365
+ i = end
366
+ if current.endswith(REPLACEMENT_CHAR):
367
+ continue
368
+ delta = current[len(prev) :]
369
+ if delta:
370
+ deltas.append(delta)
371
+ prev = current
372
+ return deltas
373
+
374
+
375
+class TestSpeculativeDecodingMultiTokenDelta:
376
+ MODEL_OUTPUT = (
377
+ "<|START_THINKING|> i will call foo with query1<|END_THINKING|>"
378
+ "<|START_ACTION|>\n"
379
+ '[\n {"tool_call_id": "0", "tool_name": "foo", '
380
+ '"parameters": {"query": "query1"}}\n]\n'
381
+ "<|END_ACTION|>"
382
+ )
383
+
384
+ @pytest.mark.parametrize(
385
+ "parser_cls",
386
+ [CohereCommand3ToolParser, CohereCommand4ToolParser],
387
+ ids=["cmd3", "cmd4"],
388
+ )
389
+ @pytest.mark.parametrize("chunk_size", [2, 3, 4, 6])
390
+ def test_no_special_token_leak_in_streaming_deltas(
391
+ self,
392
+ tokenizer: MockCohereTokenizer,
393
+ request_obj: ChatCompletionRequest,
394
+ parser_cls,
395
+ chunk_size: int,
396
+ ):
397
+ parser = parser_cls(tokenizer)
398
+ chunked = _multi_token_deltas(tokenizer, self.MODEL_OUTPUT, chunk_size)
399
+
400
+ previous_text = ""
401
+ for token_str in chunked:
402
+ current_text = previous_text + token_str
403
+ delta = parser.extract_tool_calls_streaming(
404
+ previous_text=previous_text,
405
+ current_text=current_text,
406
+ delta_text=token_str,
407
+ previous_token_ids=[],
408
+ current_token_ids=[],
409
+ delta_token_ids=[],
410
+ request=request_obj,
411
+ )
412
+ previous_text = current_text
413
+ if delta is None:
414
+ continue
415
+
416
+ fields: list[tuple[str, str | None]] = [
417
+ ("reasoning", delta.reasoning),
418
+ ("content", delta.content),
419
+ ]
420
+ for tc in delta.tool_calls or []:
421
+ if tc.function:
422
+ fields.append(("tool_call.name", tc.function.name))
423
+ fields.append(("tool_call.arguments", tc.function.arguments))
424
+
425
+ for marker in SPECIAL_TOKEN_MARKERS:
426
+ for field_name, value in fields:
427
+ assert value is None or marker not in value, (
428
+ f"special token {marker!r} leaked into {field_name} "
429
+ f"with chunk_size={chunk_size} delta={delta!r}"
430
+ )
431
+
432
+ @pytest.mark.parametrize(
433
+ "parser_cls",
434
+ [CohereCommand3ToolParser, CohereCommand4ToolParser],
435
+ ids=["cmd3", "cmd4"],
436
+ )
437
+ @pytest.mark.parametrize("chunk_size", [2, 3, 4, 6])
438
+ def test_multi_token_chunks_still_produce_correct_tool_call(
439
+ self,
440
+ tokenizer: MockCohereTokenizer,
441
+ request_obj: ChatCompletionRequest,
442
+ parser_cls,
443
+ chunk_size: int,
444
+ ):
445
+ parser = parser_cls(tokenizer)
446
+ chunked = _multi_token_deltas(tokenizer, self.MODEL_OUTPUT, chunk_size)
447
+ streamed = _run_streaming_over_deltas(parser, chunked, request_obj)
448
+
449
+ assert len(streamed.tool_calls) == 1
450
+ tc = streamed.tool_calls[0]
451
+ assert tc["id"] == "0"
452
+ assert tc["name"] == "foo"
453
+ assert json.loads(tc["arguments"]) == {"query": "query1"}
454
+
455
+
456
+class TestStreamingDeltaShape:
457
+ @pytest.mark.parametrize(
458
+ "parser_cls",
459
+ [CohereCommand3ToolParser, CohereCommand4ToolParser],
460
+ ids=["cmd3", "cmd4"],
461
+ )
462
+ def test_reasoning_and_tool_calls_are_separate_deltas(
463
+ self,
464
+ tokenizer: MockCohereTokenizer,
465
+ request_obj: ChatCompletionRequest,
466
+ parser_cls,
467
+ ):
468
+ parser = parser_cls(tokenizer)
469
+ model_output = (
470
+ "<|START_THINKING|> i will call foo with query1<|END_THINKING|>"
471
+ "<|START_ACTION|>\n"
472
+ '[\n {"tool_call_id": "0", "tool_name": "foo", '
473
+ '"parameters": {"query": "query1"}}\n]\n'
474
+ "<|END_ACTION|>"
475
+ )
476
+
477
+ token_strings = _token_deltas(tokenizer, model_output)
478
+ previous_text = ""
479
+ saw_reasoning = False
480
+ saw_tool_call = False
481
+
482
+ for token_str in token_strings:
483
+ current_text = previous_text + token_str
484
+ delta = parser.extract_tool_calls_streaming(
485
+ previous_text=previous_text,
486
+ current_text=current_text,
487
+ delta_text=token_str,
488
+ previous_token_ids=[],
489
+ current_token_ids=[],
490
+ delta_token_ids=[],
491
+ request=request_obj,
492
+ )
493
+ if delta is not None:
494
+ populated = [
495
+ delta.content is not None,
496
+ delta.reasoning is not None,
497
+ bool(delta.tool_calls),
498
+ ]
499
+ assert sum(populated) == 1, (
500
+ "A single streaming delta must carry exactly one of "
501
+ f"content/reasoning/tool_calls, got {delta!r}"
502
+ )
503
+ if delta.reasoning is not None:
504
+ saw_reasoning = True
505
+ if delta.tool_calls:
506
+ saw_tool_call = True
507
+ previous_text = current_text
508
+
509
+ assert saw_reasoning, "expected at least one reasoning delta"
510
+ assert saw_tool_call, "expected at least one tool-call delta"
vllm/reasoning/cohere_command_reasoning_parser.py
+9
-5
@@ -16,7 +16,7 @@ except ImportError as e:
16
raise ImportError(
17
"The Cohere reasoning parser requires the `cohere_melody` "
18
"package, which is not installed. Install it with:\n"
19
- " pip install cohere_melody"
19
+ " pip install 'cohere-melody>=0.11.1'"
20
) from e
21
22
@@ -132,7 +132,7 @@ def collect_tool_schema(tool_schema: list[CohereNormalizedTool]) -> str:
132
tool_dictionary[tool_name] = f"{tool_name} ::= {tool_name}root\n{tool_grammar}"
133
# Emitted grammar shape:
134
# root ::= tools
135
- # tools ::= ws "[" ws tool ws ("," ws tool)* ws "]" ws
135
+ # tools ::= ws "[" ws tool (ws "," ws tool)* ws "]" ws
136
# ws ::= (" " | "\t" | "\n")*
137
# tool ::= <tool_a> | <tool_b> | ... (one alternative per input)
138
# <tool_x> ::= <tool_x>root (per-tool xgrammar rules)
@@ -140,7 +140,7 @@ def collect_tool_schema(tool_schema: list[CohereNormalizedTool]) -> str:
140
tool_alternatives = "tool ::= " + " | ".join(tool_dictionary.keys())
141
tool_rules = "\n ".join(tool_dictionary.values())
142
grammar = f"""root ::= tools
143
- tools ::= ws "[" ws tool ws ("," ws tool)* ws "]" ws
143
+ tools ::= ws "[" ws tool (ws "," ws tool)* ws "]" ws
144
ws ::= (" " | "\\t" | "\\n")*
145
{tool_alternatives}
146
{tool_rules}
@@ -699,7 +699,9 @@ class CohereCommand3ReasoningParser(BaseCohereCommandReasoningParser):
699
super().__init__(
700
tokenizer,
701
*args,
702
- streaming_opts=PyFilterOptions().cmd3().stream_non_grounded_answer(),
702
+ streaming_opts=(
703
+ PyFilterOptions().cmd3().no_tools().stream_non_grounded_answer()
704
+ ),
705
unary_opts=PyFilterOptions().cmd3().no_tools(),
706
**kwargs,
707
)
@@ -710,7 +712,9 @@ class CohereCommand4ReasoningParser(BaseCohereCommandReasoningParser):
712
super().__init__(
713
tokenizer,
714
*args,
713
- streaming_opts=PyFilterOptions().cmd4().stream_non_grounded_answer(),
715
+ streaming_opts=(
716
+ PyFilterOptions().cmd4().no_tools().stream_non_grounded_answer()
717
+ ),
718
unary_opts=PyFilterOptions().cmd4().no_tools(),
719
**kwargs,
720
)
vllm/renderers/cohere.py
+1
-1
@@ -209,7 +209,7 @@ def _try_import_melody():
209
raise ImportError(
210
"The `cohere` tokenizer/renderer mode requires the "
211
"`cohere_melody` package. Install it via "
212
- "`pip install cohere-melody` or build from "
212
+ "`pip install 'cohere-melody>=0.11.1'` or build from "
213
"https://github.com/cohere-ai/melody."
214
) from e
215
vllm/tool_parsers/cohere_command_tool_parser.py
+5
-5
@@ -9,7 +9,7 @@ except ImportError as e:
9
raise ImportError(
10
"The Cohere tool parser requires the `cohere_melody` "
11
"package, which is not installed. Install it with:\n"
12
- " pip install cohere_melody"
12
+ " pip install 'cohere-melody>=0.11.1'"
13
) from e
14
15
from vllm.entrypoints.openai.chat_completion.protocol import (
@@ -130,8 +130,8 @@ class CohereCommand3ToolParser(BaseCohereCommandToolParser):
130
):
131
super().__init__(
132
tokenizer,
133
- streaming_opts=PyFilterOptions().cmd3(),
134
- unary_opts=PyFilterOptions().cmd3(),
133
+ streaming_opts=PyFilterOptions().cmd3().start_in_answer(),
134
+ unary_opts=PyFilterOptions().cmd3().start_in_answer(),
135
)
136
137
@@ -143,6 +143,6 @@ class CohereCommand4ToolParser(BaseCohereCommandToolParser):
143
):
144
super().__init__(
145
tokenizer,
146
- streaming_opts=PyFilterOptions().cmd4(),
147
- unary_opts=PyFilterOptions().cmd4(),
146
+ streaming_opts=PyFilterOptions().cmd4().start_in_answer(),
147
+ unary_opts=PyFilterOptions().cmd4().start_in_answer(),
148
)