Skip to content

fix(frontend): emit SGLang stream role once - #12741

Merged
rmccorm4 merged 2 commits into
ai-dynamo:mainfrom
xianlubird:bug/sglang-stream-role-once
Aug 6, 2026
Merged

fix(frontend): emit SGLang stream role once#12741
rmccorm4 merged 2 commits into
ai-dynamo:mainfrom
xianlubird:bug/sglang-stream-role-once

Conversation

@xianlubird

@xianlubird xianlubird commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Emit delta.role only on the first choice produced by the SGLang streaming postprocessor.
  • Preserve the initial assistant role when generation finishes before producing any content.
  • Cover plain-text streams, finish-only streams, and reasoning-parser streams with focused unit tests.

Background

PR #12639 established the chat-stream contract at the HTTP boundary: the first emitted delta for a choice carries role: assistant, while later deltas omit the role. The SGLang Python postprocessor builds streaming choice dictionaries outside the native Rust postprocessing path, so it still attached the role to every content-bearing delta and relied on the HTTP layer to remove the repeats.

That arrangement also left an edge case at the source. When SGLang finishes immediately without generating a token, the postprocessor returns a finish-only choice with an empty delta. The HTTP deduplication logic can remove duplicate roles, but it cannot recover a role that was never emitted.

This change tracks role emission in the per-request SGLang postprocessor and attaches the role only when a choice is actually returned. Buffered UTF-8 sequences and parser-buffered output do not consume the initial role. If the first returned choice is a finish-only choice, it still carries role: assistant; any later content or terminal choices omit it.

Validation

  • black --check on the modified Python files
  • isort --check-only on the modified Python files
  • ruff check on the modified Python files
  • python3 -m py_compile on the modified Python files
  • git diff --check

The targeted pytest module was not run locally because the current virtual environment does not include pytest or the SGLang test dependencies.

Summary by CodeRabbit

  • Bug Fixes

    • Improved streaming responses so the assistant role appears only once, on the first response chunk.
    • Ensured finish-only responses return empty content deltas while preserving the initial assistant role.
    • Corrected role handling for fast text, reasoning, and tool-call streaming responses.
  • Tests

    • Added coverage for role emission and finish-only streaming behavior.

Signed-off-by: xianlubird <xianlubird@gmail.com>
@copy-pr-bot

copy-pr-bot Bot commented Aug 6, 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.

@xianlubird
xianlubird temporarily deployed to external_collaborator August 6, 2026 05:38 — with GitHub Actions Inactive
@xianlubird
xianlubird temporarily deployed to external_collaborator August 6, 2026 05:38 — with GitHub Actions Inactive
@dynamo-ops

Copy link
Copy Markdown
Contributor

/ok to test b16bd34

@github-actions github-actions Bot added external-contribution Pull request is from an external contributor trusted-contributor Org-External user who is trusted to run CI without Org-member approval fix frontend `python -m dynamo.frontend` and `dynamo-run in=http|text|grpc` and removed fix labels Aug 6, 2026
@xianlubird
xianlubird marked this pull request as ready for review August 6, 2026 05:39
@xianlubird
xianlubird requested a review from a team as a code owner August 6, 2026 05:39
@github-actions github-actions Bot added the fix label Aug 6, 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: No Issues Found

Devin Review analyzed this PR and found no bugs or issues to report.

Open in Devin Review

@coderabbitai

coderabbitai Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

Changes

SGLang role emission

Layer / File(s) Summary
Centralized role injection
components/src/dynamo/frontend/sglang_prepost.py
SglangStreamingPostProcessor tracks role emission and applies "role": "assistant" only to the first delta across fast-path and parser-based responses.
Role emission coverage
components/src/dynamo/frontend/tests/test_sglang_processor_unit.py
Tests cover empty finish-only deltas, first-chunk role emission, immediate finishes, and single reasoning-stream role emission.

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 explains the change and validation, but it omits the required Related Issues section and does not identify where reviewers should start. Add the required Related Issues section and identify the specific files or code areas where reviewers should begin.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the main change: emitting the SGLang stream role once.
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.

🧹 Nitpick comments (1)
components/src/dynamo/frontend/tests/test_sglang_processor_unit.py (1)

3183-3199: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Assert that the first reasoning delta contains the role.

roles == ["assistant"] proves only that one role was emitted. It still passes if a later delta contains the role. Record emitted deltas and assert that the first returned delta has "role": "assistant" and later deltas do not. Apply the same assertion to the existing required-tool test to cover the parser path.

Suggested assertion
-        roles = []
+        emitted_deltas = []
...
-                if "role" in delta:
-                    roles.append(delta["role"])
+                emitted_deltas.append(delta)
...
-        assert roles == ["assistant"]
+        assert emitted_deltas
+        assert emitted_deltas[0]["role"] == "assistant"
+        assert all("role" not in delta for delta in emitted_deltas[1:])

The PR objective requires role emission on the first returned choice across content, finish-only, reasoning, and parser-based streams.

🤖 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/tests/test_sglang_processor_unit.py` around
lines 3183 - 3199, Update the streaming test around post.process_output to
retain each returned delta, then assert the first emitted delta contains role
"assistant" and all subsequent deltas omit role instead of only checking roles
== ["assistant"]. Apply the same first-versus-later delta assertions to the
existing required-tool test, covering both content and parser-based stream
paths.
🤖 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.

Nitpick comments:
In `@components/src/dynamo/frontend/tests/test_sglang_processor_unit.py`:
- Around line 3183-3199: Update the streaming test around post.process_output to
retain each returned delta, then assert the first emitted delta contains role
"assistant" and all subsequent deltas omit role instead of only checking roles
== ["assistant"]. Apply the same first-versus-later delta assertions to the
existing required-tool test, covering both content and parser-based stream
paths.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 75989a35-f2f8-48f5-b3d3-0aaa17d9c8c5

📥 Commits

Reviewing files that changed from the base of the PR and between 80865c9 and b16bd34.

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

@datadog-official

This comment has been minimized.

Signed-off-by: xianlubird <xianlubird@gmail.com>
@xianlubird
xianlubird temporarily deployed to external_collaborator August 6, 2026 06:11 — with GitHub Actions Inactive
@dynamo-ops

Copy link
Copy Markdown
Contributor

/ok to test 578823a

@rmccorm4 rmccorm4 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.

LGTM, thanks @xianlubird

@rmccorm4
rmccorm4 merged commit 4a34a46 into ai-dynamo:main Aug 6, 2026
103 of 104 checks passed
hhzhang16 added a commit that referenced this pull request Aug 6, 2026
dyn-3691-extract-shared-target-pid-cuda-customstorage-operation-layer

* 'main' of https://github.com/ai-dynamo/dynamo: (65 commits)
  fix(frontend): emit SGLang stream role once (#12741)
  docs(fern): promote v1.3.1 to current release (#12752)
  fix(docs): remove duplicate unscoped community-rail CSS rules (#12615)
  feat(operator): migrate CRD storage to v1beta1 (#11904)
  fix: synchronize self-benchmark capacity across DP ranks (#12021)
  chore(deps): bump dynamo-tokenizers to 1.8.0 (#12707)
  fix(frontend): preserve split UTF-8 characters (#12688)
  docs: align Kubernetes build selector with CLI (#12729)
  fix(frontend): preserve completion backend error status (#12706)
  fix(operator): replace snapshot pods after GMS restart (#11286)
  refactor(media): rename installer module, drop --packages per review
  fix(media): harden installer against three pre-redesign review findings
  fix(media): verify installs in a fresh interpreter; teach --pip-args= form
  test(serve): install test-time decoders at the validated bounds
  feat(media): explicit installer for additional media decoders
  docs(spica): correct kv_load_ratio support guidance (#12714)
  feat(operator): add experimental grove.forceScalingGroup for single-node components (#11772)
  fix(vllm): declare entry-stage engine_input_source in GLM-Image NIXL config (#12709)
  chore: bump trtllm to v1.3.0rc23 (#12532)
  perf: remove trtllm postprocessing workers from the args as post processing workers are not effective in dynamo (#12592)
  ...

Signed-off-by: Hannah Zhang <hannahz@nvidia.com>
@xianlubird
xianlubird deleted the bug/sglang-stream-role-once branch August 7, 2026 00:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

external-contribution Pull request is from an external contributor fix frontend `python -m dynamo.frontend` and `dynamo-run in=http|text|grpc` size/M trusted-contributor Org-External user who is trusted to run CI without Org-member approval

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants