Skip to content

feat(frontend): honor continuous_usage_stats in vLLM chat processor#11708

Open
nguyenthai-duong wants to merge 2 commits into
ai-dynamo:mainfrom
nguyenthai-duong:feat/vllm-processor-continuous-usage-stats
Open

feat(frontend): honor continuous_usage_stats in vLLM chat processor#11708
nguyenthai-duong wants to merge 2 commits into
ai-dynamo:mainfrom
nguyenthai-duong:feat/vllm-processor-continuous-usage-stats

Conversation

@nguyenthai-duong

@nguyenthai-duong nguyenthai-duong commented Jul 15, 2026

Copy link
Copy Markdown

Overview

stream_options.continuous_usage_stats=true had no effect when the frontend
runs with --dyn-chat-processor vllm: every intermediate streamed chunk came
back with usage: null, and usage was reported only once on the final chunk.
vLLM's and SGLang's native OpenAI servers emit a usage object on every chunk
when this flag is set, so the vLLM chat processor path diverged from expected
OpenAI-compatible behavior.

Details

Root cause is in VllmProcessor._generate_and_stream
(components/src/dynamo/frontend/vllm_processor.py). A chunk only received a
usage field when the engine reported completion_usage, which the vLLM engine
sends on the final chunk only:

if usage := engine_response.get("completion_usage"):
    dynamo_out["usage"] = usage

The flag was never read on this path, so it was silently ignored.

The fix reads stream_options.continuous_usage_stats from the request and, when
set, synthesizes a per-chunk usage object from the running token counters the
processor already maintains (input_tokens + cumulative_output_tokens).
Engine-provided completion_usage still takes precedence on the final chunk, so
authoritative counts are preserved and the OpenAI-required final usage chunk is
unchanged.

Scope is intentionally minimal (8 lines of logic); it reuses existing counters
and adds no new abstractions.

Where should the reviewer start?

components/src/dynamo/frontend/vllm_processor.py — the two edited blocks in
_generate_and_stream.

Related Issues

N/A

Summary by CodeRabbit

  • New Features
    • Streaming chat responses via the vLLM processor now include per-chunk token
      usage statistics when stream_options.continuous_usage_stats is enabled,
      matching vLLM/SGLang native OpenAI-server behavior.

Validation

  • pre-commit run --files components/src/dynamo/frontend/vllm_processor.py components/src/dynamo/frontend/tests/test_vllm_processor_unit.py — all hooks pass (isort, black, flake8, ruff, codespell, pytest-marker report).
  • Added three unit tests to TestRoutedEnginePath in
    test_vllm_processor_unit.py, driving _generate_and_stream with the existing
    FakeRoutedEngine harness (CPU-only, no GPU/model):
    • flag off → intermediate chunks omit usage
    • flag on → every chunk carries running {prompt_tokens, completion_tokens, total_tokens}
    • flag on + engine completion_usage on final chunk → engine value used verbatim
  • Reproduced the original bug and confirmed the fix against a live deployment
    (--dyn-chat-processor vllm): before, all content chunks returned
    "usage":null; after, per-chunk usage is populated while the final chunk is
    unchanged.

Open in Devin Review

Summary by CodeRabbit

  • New Features

    • Added continuous usage statistics for streaming responses.
    • When enabled, each streamed chunk includes running prompt, completion, and total token counts.
    • Preserves usage data provided by the backend when available.
  • Bug Fixes

    • Ensured intermediate streaming chunks omit usage data when continuous statistics are disabled.

The vLLM chat processor (`--dyn-chat-processor vllm`) only attached a
`usage` object to a streamed chunk when the engine reported
`completion_usage`, which vLLM sends on the final chunk only. As a
result, requests with `stream_options.continuous_usage_stats=true`
received `usage: null` on every intermediate chunk, unlike vLLM's and
SGLang's native OpenAI servers.

Read `stream_options.continuous_usage_stats` from the request and, when
set, synthesize a per-chunk `usage` object from the running token
counters (prompt tokens + cumulative output tokens) the processor
already tracks. Engine-provided `completion_usage` still takes
precedence on the final chunk so authoritative counts are preserved.

Add unit tests covering the flag off (no intermediate usage), flag on
(running per-chunk usage), and engine-usage precedence.

Signed-off-by: nguyenthai-duong <63569535+nguyenthai-duong@users.noreply.github.com>
@nguyenthai-duong nguyenthai-duong requested a review from a team as a code owner July 15, 2026 14:09
@copy-pr-bot

copy-pr-bot Bot commented Jul 15, 2026

Copy link
Copy Markdown

This pull request requires additional validation before any workflows can run on NVIDIA's runners.

Pull request vetters can view their responsibilities here.

Contributors can view more details about this message here.

@github-actions

Copy link
Copy Markdown
Contributor

👋 Hi nguyenthai-duong! Thank you for contributing to ai-dynamo/dynamo.

Just a reminder: The NVIDIA Test Github Validation CI runs an essential subset of the testing framework to quickly catch errors.Your PR reviewers may elect to test the changes comprehensively before approving your changes.

🚀

@github-actions github-actions Bot added feat external-contribution Pull request is from an external contributor frontend `python -m dynamo.frontend` and `dynamo-run in=http|text|grpc` and removed external-contribution Pull request is from an external contributor feat labels Jul 15, 2026

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 1 potential issue.

Open in Devin Review

Comment on lines +678 to +681
stream_options = request.get("stream_options") or {}
continuous_usage_stats = bool(
stream_options.get("continuous_usage_stats", False)
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔍 Synthesized usage emitted even when include_usage is false

The new code at components/src/dynamo/frontend/vllm_processor.py:678-681 reads only continuous_usage_stats from stream_options, but the OpenAI API spec defines continuous_usage_stats as a sub-option of include_usage — it should only take effect when include_usage is also true. The current implementation will emit per-chunk usage if a client sends {"continuous_usage_stats": true} without "include_usage": true. In practice this is unlikely to cause problems (clients that set the sub-option without the parent are already violating the API contract), but it diverges from vLLM's own handling where include_usage gates all usage emission. If strict OpenAI compatibility matters, an additional and stream_options.get("include_usage", False) check would be warranted.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

@coderabbitai

coderabbitai Bot commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

Changes

Continuous usage statistics

Layer / File(s) Summary
Usage generation and engine override
components/src/dynamo/frontend/vllm_processor.py
Streaming reads continuous_usage_stats, synthesizes running prompt and completion token usage when needed, and preserves engine-provided completion_usage.
Streaming usage behavior tests
components/src/dynamo/frontend/tests/test_vllm_processor_unit.py
Tests cover disabled usage reporting, synthesized per-chunk usage, and exact engine-provided completion usage.

Estimated code review effort: 2 (Simple) | ~10 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description is thorough, but it does not follow the required Related Issues template path and leaves issue linkage unresolved. Replace N/A with either a real issue reference or the required no-related-issue checkbox statement, and keep only one template path.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title is concise and accurately describes the main change: honoring continuous_usage_stats in the vLLM chat processor.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@components/src/dynamo/frontend/vllm_processor.py`:
- Around line 678-681: Update the stream-options parsing near
continuous_usage_stats so it is enabled only when include_usage is truthy, while
preserving the existing default behavior when either option is absent. Add
coverage for continuous_usage_stats=true with include_usage missing and with
include_usage=false, verifying that per-chunk usage is not emitted.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 389c2bf3-75c1-4166-beae-d943c4718f11

📥 Commits

Reviewing files that changed from the base of the PR and between ce068df and c5fdd36.

📒 Files selected for processing (2)
  • components/src/dynamo/frontend/tests/test_vllm_processor_unit.py
  • components/src/dynamo/frontend/vllm_processor.py

Comment on lines +678 to +681
stream_options = request.get("stream_options") or {}
continuous_usage_stats = bool(
stream_options.get("continuous_usage_stats", False)
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Map the file structure and inspect the relevant area with line numbers.
git ls-files 'components/src/dynamo/frontend/vllm_processor.py' 'components/src/dynamo/frontend/*test*' 'components/tests/*' | sed -n '1,120p'

echo '--- vllm_processor.py around the cited lines ---'
sed -n '640,730p' components/src/dynamo/frontend/vllm_processor.py

echo '--- search for include_usage / continuous_usage_stats ---'
rg -n "include_usage|continuous_usage_stats|stream_options" components/src/dynamo/frontend components/tests -S

Repository: ai-dynamo/dynamo

Length of output: 7463


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- file outline ---'
ast-grep outline components/src/dynamo/frontend/vllm_processor.py --view expanded || true

echo '--- relevant slices ---'
python3 - <<'PY'
from pathlib import Path
p = Path('components/src/dynamo/frontend/vllm_processor.py')
lines = p.read_text().splitlines()
for start,end in [(650,720),(720,790)]:
    print(f'\n### {start}-{end}')
    for i in range(start, min(end, len(lines))+1):
        print(f'{i:4d}: {lines[i-1]}')
PY

echo '--- tests mentioning include_usage / continuous_usage_stats ---'
rg -n "include_usage|continuous_usage_stats" components/src/dynamo/frontend components/tests -S

Repository: ai-dynamo/dynamo

Length of output: 10368


🏁 Script executed:

#!/bin/bash
set -euo pipefail
rg -n "include_usage|continuous_usage_stats|usage" components/src/dynamo/frontend/vllm_processor.py -S

Repository: ai-dynamo/dynamo

Length of output: 917


🌐 Web query:

vLLM continuous_usage_stats include_usage stream_options docs

💡 Result:

In vLLM, the stream_options parameter allows users to configure how token usage statistics are returned during streaming responses [1][2]. This parameter is part of the OpenAI-compatible API and can be included in chat completion and completion requests when stream=True [2][3]. Key components of stream_options include: include_usage: When set to true, vLLM sends a final usage chunk (containing token usage metrics) after all content chunks have been emitted [1][4]. If this is not enabled, streaming responses typically do not include usage data [1]. continuous_usage_stats: When enabled, this option provides cumulative usage statistics in every streamed token chunk, rather than only in the final usage chunk [5][4]. To function, this generally requires include_usage to also be set to true [5]. These options are managed within the vLLM OpenAI-compatible frontend, specifically within the StreamOptions protocol definition [6][2]. They ensure compatibility with the standard OpenAI API streaming behaviors while providing extended observability for real-time applications [5][7]. Developers can utilize these options by passing them in the stream_options dictionary during the API request [1].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '790,845p' components/src/dynamo/frontend/vllm_processor.py

echo '--- include_usage in file ---'
rg -n "include_usage" components/src/dynamo/frontend/vllm_processor.py components/src/dynamo/frontend/tests/test_vllm_processor_unit.py -S

echo '--- relevant tests around continuous usage stats ---'
sed -n '540,670p' components/src/dynamo/frontend/tests/test_vllm_processor_unit.py

Repository: ai-dynamo/dynamo

Length of output: 7505


Gate continuous_usage_stats on include_usage.

continuous_usage_stats is evaluated independently here, so stream_options: {"continuous_usage_stats": true} can emit per-chunk usage without opting into usage reporting. Tie it to include_usage, and add a test for continuous_usage_stats=true with include_usage missing or false.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@components/src/dynamo/frontend/vllm_processor.py` around lines 678 - 681,
Update the stream-options parsing near continuous_usage_stats so it is enabled
only when include_usage is truthy, while preserving the existing default
behavior when either option is absent. Add coverage for
continuous_usage_stats=true with include_usage missing and with
include_usage=false, verifying that per-chunk usage is not emitted.

# asked for per-chunk stats: emit running counts.
dynamo_out["usage"] = {
"prompt_tokens": input_tokens,
"completion_tokens": cumulative_output_tokens,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Synthesized continuous usage uses the request-wide cumulative_output_tokens, so n > 1 streams report tokens generated for other choices in each per-choice chunk. Fix: track completion tokens per output_idx and use the current choice's running total when building continuous usage.

🤖 AI Fix

In components/src/dynamo/frontend/vllm_processor.py, inside VllmProcessor._generate_and_stream, add a dict[int, int] counter keyed by output_idx; after deriving and validating output_idx, increment both the existing request-wide cumulative_output_tokens for metrics and the per-choice counter for that output_idx, then in the continuous_usage_stats branch set completion_tokens and total_tokens from the current output_idx counter instead of cumulative_output_tokens.

…hoice

Address review feedback on the vLLM chat processor per-chunk usage:

- continuous_usage_stats is a sub-option of include_usage per the OpenAI
  spec, so only emit synthesized per-chunk usage when include_usage is
  also true (matches vLLM's native gating). (Devin, CodeRabbit)

- For n > 1, choices interleave in the stream; the request-wide
  cumulative counter would report other choices' tokens in each
  per-choice chunk. Track completion tokens per output index and report
  the current choice's running total in continuous usage. (dynamo-review-agent)

Add a unit test asserting continuous_usage_stats is ignored without
include_usage.

Signed-off-by: nguyenthai-duong <63569535+nguyenthai-duong@users.noreply.github.com>
@nguyenthai-duong nguyenthai-duong temporarily deployed to external_collaborator July 15, 2026 17:12 — with GitHub Actions Inactive
@nguyenthai-duong

Copy link
Copy Markdown
Author

Thanks for the reviews. Addressed in b1dbdc1:

  • include_usage gating (Devin, CodeRabbit): continuous_usage_stats is now only honored when include_usage is also true, matching the OpenAI spec (it's a sub-option of include_usage) and vLLM's native gating.
  • Per-choice counting for n > 1 (dynamo-review-agent): added a per_choice_output_tokens counter keyed by output index; continuous usage now reports each choice's own running completion tokens instead of the request-wide sum. The request-wide cumulative_output_tokens is still used for metrics.

Added a unit test asserting continuous_usage_stats is ignored without include_usage.

Note: the lychee check failure is a pre-existing broken link in docs/features/disaggregated-serving/aiconfigurator.md, unrelated to this PR (which only touches vllm_processor.py and its unit test).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feat frontend `python -m dynamo.frontend` and `dynamo-run in=http|text|grpc` size/L

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant