[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