Skip to content

Commit d3005d2

Browse files
committed
[Bugfix] Parse gpt-oss refusals w/ newer openai-harmony
The output generated by gpt-oss models does not always strictly follow its expected harmony chat template format. This commonly - but not exclusively - happens when gpt-oss-120b generates refusals for content that violates its built-in safety guidelines. To fix this, a non-strict mode was added to the openai-harmony library to allow attempted recovery of malformed message headers in the model output, such as a missing `<|message|>` special token before the assistant text. This will resolve some cases where the error `openai_harmony.HarmonyError: unexpected tokens remaining in message header` was previously thrown. It will not resolve all of those, as not every malformed message output can be recovered. Other ongoing work around using structured output for the Harmony format can help prevent these kinds of things in the first place, once that work lands and in the cases where the user and/or server decide to enable it. I believe it should be safe to enable this non-strict mode by default in vLLM, as the code paths that enables in the openai-harmony library only gets triggered once it's already detected malformed output. So, there shouldn't be any performance penalty in the common case. And, in the event that the malformed content cannot be properly recovered, the openai-harmony library will still end up throwing an error. This is related to #23567 as well as openai/harmony#80.
1 parent 0f872b7 commit d3005d2

File tree

4 files changed

+28
-3
lines changed

4 files changed

+28
-3
lines changed

requirements/common.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,5 +47,5 @@ ninja # Required for xgrammar, rocm, tpu, xpu
4747
pybase64 # fast base64 implementation
4848
cbor2 # Required for cross-language serialization of hashable objects
4949
setproctitle # Used to set process names for better debugging and monitoring
50-
openai-harmony >= 0.0.3 # Required for gpt-oss
50+
openai-harmony >= 0.0.8 # Required for gpt-oss
5151
anthropic == 0.71.0

requirements/test.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -620,7 +620,7 @@ omegaconf==2.3.0
620620
# lightning
621621
open-clip-torch==2.32.0
622622
# via -r requirements/test.in
623-
openai-harmony==0.0.4
623+
openai-harmony==0.0.8
624624
# via gpt-oss
625625
opencensus==0.11.4
626626
# via ray

tests/entrypoints/test_harmony_utils.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
from openai_harmony import Role
55

66
from vllm.entrypoints.harmony_utils import (
7+
get_encoding,
8+
get_streamable_parser_for_assistant,
79
has_custom_tools,
810
parse_input_to_harmony_message,
911
)
@@ -264,3 +266,26 @@ def test_has_custom_tools() -> None:
264266
assert has_custom_tools(
265267
{"web_search_preview", "code_interpreter", "container", "others"}
266268
)
269+
270+
271+
def test_malformed_refusal_message() -> None:
272+
"""Test parsing a malformed refusal message sometimes generated by gpt-oss"""
273+
output_text = (
274+
"...\n\nAccording to policy, we must refuse.<|end|>"
275+
"<|start|>assistant<|channel|>analysis<|message|>We must refuse.<|end|>"
276+
"<|start|>assistant<|channel|>final<|message|>I can't help with that.<|end|>"
277+
)
278+
output_tokens = get_encoding().encode(output_text, allowed_special="all")
279+
parser = get_streamable_parser_for_assistant()
280+
for token in output_tokens:
281+
parser.process(token)
282+
assert len(parser.messages) == 3
283+
assert parser.messages[0].author.role == Role.ASSISTANT
284+
# using "in" here instead of "==" to allow for whitespace variances
285+
assert "According to policy, we must refuse." in parser.messages[0].content[0].text
286+
assert parser.messages[1].author.role == Role.ASSISTANT
287+
assert parser.messages[1].channel == "analysis"
288+
assert parser.messages[1].content[0].text == "We must refuse."
289+
assert parser.messages[2].author.role == Role.ASSISTANT
290+
assert parser.messages[2].channel == "final"
291+
assert parser.messages[2].content[0].text == "I can't help with that."

vllm/entrypoints/harmony_utils.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -503,7 +503,7 @@ def get_stop_tokens_for_assistant_actions() -> list[int]:
503503

504504

505505
def get_streamable_parser_for_assistant() -> StreamableParser:
506-
return StreamableParser(get_encoding(), role=Role.ASSISTANT)
506+
return StreamableParser(get_encoding(), role=Role.ASSISTANT, strict=False)
507507

508508

509509
def parse_output_into_messages(token_ids: Iterable[int]) -> StreamableParser:

0 commit comments

Comments
 (0)