Skip to content

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

Merged
sfeng33 merged 3 commits into
vllm-project:mainfrom
tarjan1:fix/harmony-parser-recipient-normalization
Jun 30, 2026
Merged

[Bugfix][Frontend] Normalize constrained Harmony recipients#45657
sfeng33 merged 3 commits into
vllm-project:mainfrom
tarjan1:fix/harmony-parser-recipient-normalization

Conversation

@tarjan1

@tarjan1 tarjan1 commented Jun 15, 2026

Copy link
Copy Markdown
Contributor

Summary

Normalize standalone <|constrain|>... markers that older openai-harmony versions misparse as recipients at the shared HarmonyParser boundary.

This keeps constrained final output classified as regular content while exposing recipient=None through HarmonyParser.current_recipient and Segment.recipient. It covers non-streaming parsing, streaming parser state, and emitted segments.

Related to #45570. This is the separate HarmonyParser follow-up requested in #45570 (comment).

Duplicate check

Tests

Run remotely on Python 3.12.8 against vLLM main at 64833f8158236a7bddeeb89efc6a3bde5d16f468:

PYTHONPATH="$WT" .venv-codex/bin/python -m pytest tests/parser/test_harmony.py -q
# openai-harmony 0.0.4: 38 passed, 17 warnings

PYTHONPATH="$SITE08:$WT" .venv-codex/bin/python -m pytest tests/parser/test_harmony.py -q
# openai-harmony 0.0.8: 38 passed, 17 warnings

Also run on the changed files:

ruff check: passed
ruff format --check: passed
typos: passed
git diff --check: passed

AI assistance disclosure: OpenAI Codex assisted with investigation, implementation, test execution, and drafting this PR. I reviewed the changed lines and validated the behavior and tests.

@github-actions

Copy link
Copy Markdown

👋 Hi! Thank you for contributing to the vLLM project.

💬 Join our developer Slack at https://slack.vllm.ai to discuss your PR in #pr-reviews, coordinate on features in #feat- channels, or join special interest groups in #sig- channels.

PRs do not trigger a full CI run by default. Once the PR is approved and ready to go, your PR reviewer(s) can run CI to test the changes comprehensively before merging.

To run CI, PR reviewers can either: Add ready label to the PR or enable auto-merge.

If you have any questions, please reach out to us on Slack at https://slack.vllm.ai.

Agent Guidelines

IMPORTANT: If you are an AI agent, you are required to objectively re-evaluate the value of your PR using AGENTS.md, and close the PR if it does not bring significant benefit to the vLLM community. Failure to do so may result in an immediate ban.

🚀

@yzong-rh yzong-rh 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.

Minor comments. Thanks for the work.

Comment thread vllm/parser/harmony.py Outdated
Comment on lines +296 to +297
if recipient and recipient.startswith("<|constrain|>"):
return None

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.

Should we discard anything that starts with <|constrain|>? or should be prune the recipient that may contain <|constrain|> ...?

Not sure if a case like to=function.foo <|constrain|> ... is buggy and possible. If it is, stripping away the ending <|constrain|> ... might work better.

Comment thread vllm/parser/harmony.py Outdated
reasoning_token_count=reasoning_token_count,
)

def _normalize_recipient(self, recipient: str | None) -> str | None:

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.

Doesn't need the self here. A static method might be better.

Comment thread vllm/parser/harmony.py
channel=channel,
recipient=recipient,
delta=delta,
completed_message=completed_message,

This comment was marked as resolved.

(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):

This comment was marked as resolved.

@bbrowning

Copy link
Copy Markdown
Collaborator

@yzong-rh As a general note, if we were to swap over from openai-harmony to oss-harmony, these tokens would stop leaking into the vLLM layer at all (at least not in this way). Just an FYI, and not a reason to not fix this in this layer as well.

@tarjan1
tarjan1 force-pushed the fix/harmony-parser-recipient-normalization branch 2 times, most recently from 36ae696 to 5ef0cd4 Compare June 16, 2026 09:46
@tarjan1

tarjan1 commented Jun 16, 2026

Copy link
Copy Markdown
Contributor Author

Hi @yzong-rh , thanks again for your previous guidance!

I’ve addressed all the requested changes in the latest push. Please feel free to let me know if there are any further suggestions or concerns.

@tarjan1

tarjan1 commented Jun 17, 2026

Copy link
Copy Markdown
Contributor Author

Hi @yzong-rh , Sorry for missing this detail earlier. I looked into the recipient pruning question more closely.

I tested these representative parser-level cases against openai_harmony 0.0.4 and 0.0.8:

  1. Standalone constrained final output:
    <|channel|>final <|constrain|>json<|message|>{"result":true}<|end|>

    Result: Harmony returns recipient='<|constrain|>json' and content_type=None.
    This is the reproduced bad case.

  2. Legal tool call with a space before the constraint marker:
    <|channel|>commentary to=functions.get_weather <|constrain|>json<|message|>{"location":"San Francisco"}<|call|>

    Result: Harmony returns recipient='functions.get_weather' and
    content_type='<|constrain|>json'.

  3. Official docs-style analysis + tool call sequence:
    <|channel|>analysis<|message|>Need to use function get_weather.<|end|><|start|>assistant<|channel|>commentary to=functions.get_weather <|constrain|>json<|message|>{"location":"San Francisco"}<|call|>

    Result: the tool-call message returns recipient='functions.get_weather' and
    content_type='<|constrain|>json'.

  4. Tool call without a space before the constraint marker:
    <|channel|>commentary to=functions.get_weather<|constrain|>json<|message|>{"location":"San Francisco"}<|call|>

    Result: Harmony still returns recipient='functions.get_weather' and
    content_type='<|constrain|>json'.

  5. Constraint marker before to=...:
    <|channel|>commentary <|constrain|>json to=functions.get_weather<|message|>{"location":"San Francisco"}<|call|>

    Result: Harmony rejects this with HarmonyError: unexpected tokens remaining in message header.
    It does not produce a recipient.

So I did not find a real parsed case where Harmony returns something like
recipient='functions.foo <|constrain|>...'.

However, I agree that pruning at the first <|constrain|> is more robust than only checking startswith.
The updated normalization prunes at the first <|constrain|>, so a hypothetical
functions.foo <|constrain|>json becomes functions.foo, while
<|constrain|>json ... still becomes None because there is no trusted recipient
before the marker.

I also normalized completed_message.recipient and kept the test coverage in
TestProcessChunk to assert both Segment.recipient and
completed_message.recipient.

@tarjan1
tarjan1 force-pushed the fix/harmony-parser-recipient-normalization branch from 47c48a7 to 63e9762 Compare June 17, 2026 02:36

@yzong-rh yzong-rh 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.

Sorry for the delay. This LGTM, thanks

cc maintainer @bbrowning

@tarjan1

tarjan1 commented Jun 25, 2026

Copy link
Copy Markdown
Contributor Author

Sorry for the delay. This LGTM, thanks

cc maintainer @bbrowning

Hi @bbrowning, could you please take a look when you have a chance?

This PR has been approved by @yzong-rh. Please let me know if there’s anything else I should update before it can move forward.

Thanks!

cc @sfeng33

@mergify

mergify Bot commented Jun 26, 2026

Copy link
Copy Markdown
Contributor

This pull request has merge conflicts that must be resolved before it can be
merged. Please rebase the PR, @tarjan1.

https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/syncing-a-fork

@mergify mergify Bot added the needs-rebase label Jun 26, 2026
Normalize constrained content markers that older openai-harmony versions misparse as recipients at the HarmonyParser boundary. Add coverage for non-streaming parsing, streaming parser state, and emitted segments.

Assisted-by: OpenAI Codex

Signed-off-by: shaojunjie <626650687@qq.com>
@tarjan1
tarjan1 force-pushed the fix/harmony-parser-recipient-normalization branch from 63e9762 to e27a5d2 Compare June 27, 2026 10:11
@mergify mergify Bot removed the needs-rebase label Jun 27, 2026
@bbrowning bbrowning added the verified Run pre-commit for new contributors without triggering other tests label Jun 29, 2026
Comment thread vllm/parser/harmony.py
constrain_index = recipient.find("<|constrain|>")
if constrain_index == -1:
return recipient
return recipient[:constrain_index].rstrip() or None

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this work in the real world? Here's a prior example I happened to have written down of how the constrain often leaks into the wrong place:

<|channel|>analysis<|message|>User asks: "What's the weather like in Paris today?" We need to get weather info. Use get_weather function. We need coordinates for Paris. Latitude 48.8566, longitude 2.3522.<|end|><|start|>assistant<|channel|>commentary <|constrain|>functions.get_weather<|message|>{"latitude":48.8566,"longitude":2.3522}<|call|>

I'm worried about the find and rstrip here in cases like this. It would potentially catch some cases, but I'm unsure how much it will hold up in the typical long context real-world failure scenarios we frequently see with these models. Do we have evidence this helps those broadly?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this work in the real world? Here's a prior example I happened to have written down of how the constrain often leaks into the wrong place:

<|channel|>analysis<|message|>User asks: "What's the weather like in Paris today?" We need to get weather info. Use get_weather function. We need coordinates for Paris. Latitude 48.8566, longitude 2.3522.<|end|><|start|>assistant<|channel|>commentary <|constrain|>functions.get_weather<|message|>{"latitude":48.8566,"longitude":2.3522}<|call|>

I'm worried about the find and rstrip here in cases like this. It would potentially catch some cases, but I'm unsure how much it will hold up in the typical long context real-world failure scenarios we frequently see with these models. Do we have evidence this helps those broadly?

Hi @bbrowning

Thanks for confirming, this works for the specific case addressed here. This PR is only a narrow fix for #45570.

I’ll continue looking into the broader <|constrain|> leakage issue and follow up with a more robust solution in a separate PR.

@bbrowning bbrowning left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm approving this as it does help the cases where constrains leak into the final channel as recipients, which is what #45570 was about.

I noted an example where this does not solve the more generic problem of constrain tokens leaking into recipients for tool calling scenarios, but realize that was out of scope for the original intent of this PR so am ok with the narrower fix for the misidentified as MCP issue.

@github-project-automation github-project-automation Bot moved this from To Triage to Ready in gpt-oss Issues & Enhancements Jun 29, 2026
@bbrowning bbrowning added the ready ONLY add when PR is ready to merge/full CI is needed label Jun 29, 2026
@tarjan1

tarjan1 commented Jun 30, 2026

Copy link
Copy Markdown
Contributor Author

I'm approving this as it does help the cases where constrains leak into the final channel as recipients, which is what #45570 was about.

I noted an example where this does not solve the more generic problem of constrain tokens leaking into recipients for tool calling scenarios, but realize that was out of scope for the original intent of this PR so am ok with the narrower fix for the misidentified as MCP issue.

Hi @bbrowning, Thank you for the approval and clarification.

The current CI failures appear unrelated to the changes in this PR. Would it be reasonable to proceed with the merge if you agree, or would you prefer another CI rerun?

@chaunceyjiang

Copy link
Copy Markdown
Collaborator

ci error fix: #47126

@sfeng33
sfeng33 merged commit 2824282 into vllm-project:main Jun 30, 2026
54 checks passed
rjrock pushed a commit to rjrock/vllm that referenced this pull request Jul 1, 2026
…ject#45657)

Signed-off-by: shaojunjie <626650687@qq.com>
Co-authored-by: Ben Browning <bbrownin@redhat.com>
jakki-amd pushed a commit to jakki-amd/vllm that referenced this pull request Jul 6, 2026
…ject#45657)

Signed-off-by: shaojunjie <626650687@qq.com>
Co-authored-by: Ben Browning <bbrownin@redhat.com>
ankrovv added a commit to ankrovv/vllm that referenced this pull request Jul 6, 2026
A constrained final message (<|channel|>final<|constrain|>json<|message|>) can
be parsed by openai-harmony with recipient="<|constrain|>json", which the
Responses converter routed to an mcp_call - leaking the control token into
name/server_label and putting the JSON answer in arguments instead of
output_text. Strip the <|constrain|> marker from the recipient before dispatch
so the message is returned as normal output. Backport of upstream vllm-project#45657
(issue vllm-project#45570), adapted to the old-layout responses/harmony.py converter.
lkk12014402 pushed a commit to lkk12014402/vllm that referenced this pull request Jul 8, 2026
…ject#45657)

Signed-off-by: shaojunjie <626650687@qq.com>
Co-authored-by: Ben Browning <bbrownin@redhat.com>
noooop pushed a commit to noooop/vllm that referenced this pull request Jul 9, 2026
…ject#45657)

Signed-off-by: shaojunjie <626650687@qq.com>
Co-authored-by: Ben Browning <bbrownin@redhat.com>
Signed-off-by: wang.yuqi <yuqi.wang@daocloud.io>
philippesic pushed a commit to philippesic/vllm-semantic-cache that referenced this pull request Jul 19, 2026
…ject#45657)

Signed-off-by: shaojunjie <626650687@qq.com>
Co-authored-by: Ben Browning <bbrownin@redhat.com>
plasticchris pushed a commit to plasticchris/vllm that referenced this pull request Jul 20, 2026
…ject#45657)

Signed-off-by: shaojunjie <626650687@qq.com>
Co-authored-by: Ben Browning <bbrownin@redhat.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working gpt-oss Related to GPT-OSS models ready ONLY add when PR is ready to merge/full CI is needed tool-calling verified Run pre-commit for new contributors without triggering other tests

Projects

Status: Done
Status: Done

Development

Successfully merging this pull request may close these issues.

5 participants