Skip to content

Commit 0132e41

Browse files
committed
test(vllm): add expectedFailure test for progressive streaming with tool parser (Case 3, #582)
Signed-off-by: pos-ei-don <1822533+pos-ei-don@users.noreply.github.com>
1 parent c34e034 commit 0132e41

1 file changed

Lines changed: 84 additions & 0 deletions

File tree

backend/python/vllm/test.py

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -352,4 +352,88 @@ def contents(replies):
352352
self.assertEqual(
353353
joined.count("The capital of France is Paris."), 1,
354354
f"buffered content was duplicated: {joined!r}",
355+
)
356+
@unittest.expectedFailure
357+
def test_streaming_tool_parser_progressive_plain_text(self):
358+
"""
359+
Case 3 (TDD — currently fails, defines acceptance criterion for follow-up, see #582):
360+
361+
When a tool parser is active but the model returns plain text (no tool call),
362+
and the parser implements extract_tool_calls_streaming, tokens should be emitted
363+
progressively — not held until the final chunk.
364+
365+
Proposed interface:
366+
extract_tool_calls_streaming(delta: str, request=None)
367+
-> SimpleNamespace(is_tool_call_token=bool, content=str)
368+
369+
Fails on current code because has_tool_parser=True suppresses all intermediate
370+
deltas. Will pass after Option A or B from the follow-up (Issue #582).
371+
"""
372+
import sys, os, asyncio
373+
from types import SimpleNamespace
374+
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
375+
from backend import BackendServicer
376+
377+
def make_generate(chunks):
378+
async def gen(*a, **k):
379+
for t in chunks:
380+
yield SimpleNamespace(
381+
outputs=[SimpleNamespace(text=t, token_ids=[1], logprobs=None)],
382+
prompt_token_ids=[0],
383+
)
384+
return lambda *a, **k: gen()
385+
386+
class StreamingAwareParser:
387+
def __init__(self, tokenizer, tools=None):
388+
pass
389+
390+
def extract_tool_calls(self, c, request=None):
391+
return SimpleNamespace(tools_called=False, content=c, tool_calls=[])
392+
393+
def extract_tool_calls_streaming(self, delta, request=None):
394+
# Plain-text delta — not tool-call markup, pass through as content.
395+
return SimpleNamespace(is_tool_call_token=False, content=delta)
396+
397+
def collect(servicer, req):
398+
async def run():
399+
return [r async for r in servicer._predict(req, None, streaming=True)]
400+
return asyncio.run(run())
401+
402+
# vLLM yields cumulative text per iteration
403+
s = BackendServicer()
404+
s.reasoning_parser_cls = None
405+
s.tokenizer = None
406+
s.llm = SimpleNamespace(generate=make_generate([
407+
"Paris ",
408+
"Paris is ",
409+
"Paris is the capital of France.",
410+
]))
411+
s.tool_parser_cls = StreamingAwareParser
412+
413+
tools_json = '[{"type":"function","function":{"name":"calc"}}]'
414+
req = backend_pb2.PredictOptions(Prompt="x", Tools=tools_json)
415+
replies = collect(s, req)
416+
417+
# Key assertion: intermediate replies (before the final chunk) must carry content.
418+
# Currently fails because has_tool_parser=True suppresses all intermediate deltas.
419+
intermediate_content = [
420+
cd.content
421+
for r in replies[:-1]
422+
for cd in r.chat_deltas
423+
if cd.content
424+
]
425+
self.assertTrue(
426+
len(intermediate_content) > 0,
427+
"Plain-text response not streamed progressively — "
428+
"all content arrived in the final chunk. "
429+
"Fix: use extract_tool_calls_streaming when available (Option A).",
430+
)
431+
432+
# No duplication: assembled content equals the full text exactly once.
433+
assembled = "".join(
434+
cd.content for r in replies for cd in r.chat_deltas if cd.content
435+
)
436+
self.assertEqual(
437+
assembled, "Paris is the capital of France.",
438+
f"Content wrong or duplicated: {assembled!r}",
355439
)

0 commit comments

Comments
 (0)