[Bugfix][Frontend] Normalize constrained Harmony recipients (#45657)

Signed-off-by: shaojunjie <626650687@qq.com> Co-authored-by: Ben Browning <bbrownin@redhat.com>

tarjan1 committed Jul 1, 2026 at 05:33 UTC 28242824e00856cf4f3d3f45c959ab1e6501a91b
2 files changed +39 -2
tests/parser/test_harmony.py
+21
@@ -728,6 +728,27 @@ class TestProcessChunk:
728 (s.channel, s.recipient, s.delta) for s in result.segments if s.delta
729 ] == [("final", None, "Hello")]
730
731 + def test_constrained_output_segment_recipient_normalized(self, harmony_parser):
732 + result = harmony_parser.process_chunk(
733 + encode_output(
734 + '<|channel|>final <|constrain|>json<|message|>{"result":true}<|end|>'
735 + )
736 + )
737 +
738 + content_segments = [segment for segment in result.segments if segment.delta]
739 + assert all(segment.channel == "final" for segment in content_segments)
740 + assert all(segment.recipient is None for segment in content_segments)
741 + assert (
742 + "".join(segment.delta for segment in content_segments) == '{"result":true}'
743 + )
744 + completed_messages = [
745 + segment.completed_message
746 + for segment in result.segments
747 + if segment.completed_message is not None
748 + ]
749 + assert len(completed_messages) == 1
750 + assert completed_messages[0].recipient is None
751 +
752 def test_cross_channel(self, harmony_parser):
753 result = harmony_parser.process_chunk(
754 encode_output(
vllm/parser/harmony.py
+18 -2
@@ -100,6 +100,7 @@ class HarmonyParser(DelegatingParser):
100 if len(messages) <= self._num_processed_messages:
101 return None
102 msg = messages[self._num_processed_messages]
103 + msg.recipient = self._normalize_recipient(msg.recipient)
104 self._num_processed_messages += 1
105 return msg
106
@@ -192,7 +193,9 @@ class HarmonyParser(DelegatingParser):
193 *,
194 finished: bool,
195 ) -> DeltaMessage | None:
195 - prev_recipient = self._harmony_parser.current_recipient
196 + prev_recipient = self._normalize_recipient(
197 + self._harmony_parser.current_recipient
198 + )
199 result = self.process_chunk(delta_token_ids)
200 if finished:
201 flushed_segment = self.flush()
@@ -274,7 +277,9 @@ class HarmonyParser(DelegatingParser):
277 for token_id in token_ids:
278 self._harmony_parser.process(token_id)
279 channel = self._harmony_parser.current_channel
277 - recipient = self._harmony_parser.current_recipient
280 + recipient = self._normalize_recipient(
281 + self._harmony_parser.current_recipient
282 + )
283 delta = self._harmony_parser.last_content_delta or ""
284 completed_message = self._poll_completed_message()
285
@@ -298,3 +303,14 @@ class HarmonyParser(DelegatingParser):
303 segments=segments,
304 reasoning_token_count=reasoning_token_count,
305 )
306 +
307 + @staticmethod
308 + def _normalize_recipient(recipient: str | None) -> str | None:
309 + """Remove constrained formats misparsed into recipients by older Harmony."""
310 + if recipient is None:
311 + return None
312 +
313 + constrain_index = recipient.find("<|constrain|>")
314 + if constrain_index == -1:
315 + return recipient
316 + return recipient[:constrain_index].rstrip() or None