diff --git a/tests/parser/test_harmony.py b/tests/parser/test_harmony.py index f9ca0b7b3294..40d2c5adb264 100644 --- a/tests/parser/test_harmony.py +++ b/tests/parser/test_harmony.py @@ -728,6 +728,27 @@ def test_single_channel(self, harmony_parser): (s.channel, s.recipient, s.delta) for s in result.segments if s.delta ] == [("final", None, "Hello")] + def test_constrained_output_segment_recipient_normalized(self, harmony_parser): + result = harmony_parser.process_chunk( + encode_output( + '<|channel|>final <|constrain|>json<|message|>{"result":true}<|end|>' + ) + ) + + content_segments = [segment for segment in result.segments if segment.delta] + assert all(segment.channel == "final" for segment in content_segments) + assert all(segment.recipient is None for segment in content_segments) + assert ( + "".join(segment.delta for segment in content_segments) == '{"result":true}' + ) + completed_messages = [ + segment.completed_message + for segment in result.segments + if segment.completed_message is not None + ] + assert len(completed_messages) == 1 + assert completed_messages[0].recipient is None + def test_cross_channel(self, harmony_parser): result = harmony_parser.process_chunk( encode_output( diff --git a/vllm/parser/harmony.py b/vllm/parser/harmony.py index 4919e3da7eb0..97e275d528c1 100644 --- a/vllm/parser/harmony.py +++ b/vllm/parser/harmony.py @@ -100,6 +100,7 @@ def _poll_completed_message(self) -> Message | None: if len(messages) <= self._num_processed_messages: return None msg = messages[self._num_processed_messages] + msg.recipient = self._normalize_recipient(msg.recipient) self._num_processed_messages += 1 return msg @@ -192,7 +193,9 @@ def parse_delta( *, finished: bool, ) -> DeltaMessage | None: - prev_recipient = self._harmony_parser.current_recipient + prev_recipient = self._normalize_recipient( + self._harmony_parser.current_recipient + ) result = self.process_chunk(delta_token_ids) if finished: flushed_segment = self.flush() @@ -274,7 +277,9 @@ def process_chunk(self, token_ids: Sequence[int]) -> ChunkResult: for token_id in token_ids: self._harmony_parser.process(token_id) channel = self._harmony_parser.current_channel - recipient = self._harmony_parser.current_recipient + recipient = self._normalize_recipient( + self._harmony_parser.current_recipient + ) delta = self._harmony_parser.last_content_delta or "" completed_message = self._poll_completed_message() @@ -298,3 +303,14 @@ def process_chunk(self, token_ids: Sequence[int]) -> ChunkResult: segments=segments, reasoning_token_count=reasoning_token_count, ) + + @staticmethod + def _normalize_recipient(recipient: str | None) -> str | None: + """Remove constrained formats misparsed into recipients by older Harmony.""" + if recipient is None: + return None + + constrain_index = recipient.find("<|constrain|>") + if constrain_index == -1: + return recipient + return recipient[:constrain_index].rstrip() or None