Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 11 additions & 4 deletions components/src/dynamo/frontend/sglang_prepost.py
Original file line number Diff line number Diff line change
Expand Up @@ -979,6 +979,7 @@ def __init__(
# incomplete byte-fallback sequence.
self._decode_context_ids = list((prompt_token_ids or [])[-5:])
self._pending_decode_ids: list[int] = []
self._has_emitted_role: bool = False
# Tool call accumulation. SGLang's streaming parser returns
# deltas (name in one chunk, argument fragments across subsequent
# chunks). However, the base detector processes at most one event
Expand Down Expand Up @@ -1016,6 +1017,12 @@ def _decode_ids(self, token_ids: list[int]) -> str:
skip_special_tokens=self._skip_special_tokens,
)

def _with_initial_role(self, delta: dict[str, Any]) -> dict[str, Any]:
if not self._has_emitted_role:
delta["role"] = "assistant"
self._has_emitted_role = True
return delta

def _incremental_decode(
self, new_token_ids: list[int], *, flush: bool = False
) -> str:
Expand Down Expand Up @@ -1112,14 +1119,14 @@ def process_output(self, engine_response: dict[str, Any]) -> dict[str, Any] | No
if delta_text:
return {
"index": 0,
"delta": {"role": "assistant", "content": delta_text},
"delta": self._with_initial_role({"content": delta_text}),
"finish_reason": finish_reason,
"logprobs": None,
}
elif finish_reason:
return {
"index": 0,
"delta": {},
"delta": self._with_initial_role({}),
"finish_reason": finish_reason,
"logprobs": None,
}
Expand Down Expand Up @@ -1162,7 +1169,7 @@ def process_output(self, engine_response: dict[str, Any]) -> dict[str, Any] | No
self._tool_call_args.setdefault(idx, []).append(tc.parameters)

# -- Assemble delta --
delta: dict[str, Any] = {"role": "assistant"}
delta: dict[str, Any] = {}
has_content = False

if content_text:
Expand Down Expand Up @@ -1336,7 +1343,7 @@ def process_output(self, engine_response: dict[str, Any]) -> dict[str, Any] | No
if has_content or effective_finish:
return {
"index": 0,
"delta": delta if has_content else {},
"delta": self._with_initial_role(delta),
"finish_reason": effective_finish,
"logprobs": None,
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2949,6 +2949,7 @@ def test_finish_reason_only(self, tokenizer):
choice = post.process_output({"token_ids": [], "finish_reason": "stop"})
assert choice is not None
assert choice["finish_reason"] == "stop"
assert choice["delta"] == {}

def test_stop_reason_not_emitted_on_choice(self, tokenizer):
"""Backend stop_reason is not part of the OpenAI choice shape."""
Expand Down Expand Up @@ -3127,6 +3128,36 @@ def test_fast_path_content_output(self, tokenizer):
assert choice["index"] == 0
assert choice["logprobs"] is None

def test_fast_path_emits_role_only_once(self, tokenizer):
"""Only the first emitted content delta includes the assistant role."""
post = SglangStreamingPostProcessor(
tokenizer=tokenizer, tool_call_parser=None, reasoning_parser=None
)
token_ids = tokenizer.encode("Hello world again", add_special_tokens=False)
assert len(token_ids) >= 2

first = post.process_output({"token_ids": token_ids[:1], "finish_reason": None})
second = post.process_output(
{"token_ids": token_ids[1:], "finish_reason": None}
)

assert first is not None
assert first["delta"]["role"] == "assistant"
assert second is not None
assert "role" not in second["delta"]

def test_finish_only_output_emits_initial_role(self, tokenizer):
"""An immediate finish still emits the stream's initial role."""
post = SglangStreamingPostProcessor(
tokenizer=tokenizer, tool_call_parser=None, reasoning_parser=None
)

choice = post.process_output({"token_ids": [], "finish_reason": "stop"})

assert choice is not None
assert choice["delta"] == {"role": "assistant"}
assert choice["finish_reason"] == "stop"


# ---------------------------------------------------------------------------
# SglangStreamingPostProcessor: reasoning parsing
Expand All @@ -3149,6 +3180,7 @@ def test_reasoning_separated(self, tokenizer):

reasoning = ""
content = ""
roles = []
for i in range(0, len(token_ids), 5):
batch = token_ids[i : i + 5]
is_last = i + 5 >= len(token_ids)
Expand All @@ -3157,11 +3189,14 @@ def test_reasoning_separated(self, tokenizer):
)
if choice:
delta = choice.get("delta", {})
if "role" in delta:
roles.append(delta["role"])
reasoning += delta.get("reasoning_content", "")
content += delta.get("content", "")

assert "think about this" in reasoning
assert "42" in content
assert roles == ["assistant"]

@pytest.mark.parametrize(
("parser_name", "reasoning_output", "expected_reasoning"),
Expand Down
Loading