Files
wehub-resource-sync e9a2f726c9
CI / test (3.11) (push) Has been cancelled
CI / test (3.12) (push) Has been cancelled
CI / test (3.13) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:29:51 +08:00

481 lines
18 KiB
Python

# SPDX-License-Identifier: Apache-2.0
"""Tests for Harmony streaming parser (omlx.adapter.harmony)."""
from types import SimpleNamespace
from unittest.mock import MagicMock
import pytest
from omlx.adapter.harmony import (
HarmonyStreamingParser,
load_harmony_gpt_oss_encoding,
parse_tool_calls_from_tokens,
preprocess_harmony_messages,
)
@pytest.fixture(scope="session")
def encoding():
"""Load HarmonyGptOss encoding."""
return load_harmony_gpt_oss_encoding()
@pytest.fixture
def tokenizer(encoding):
"""Create a mock tokenizer that resolves Harmony special tokens."""
tok = MagicMock()
special_map = {}
for name in [
"<|start|>", "<|end|>", "<|message|>", "<|channel|>",
"<|return|>", "<|call|>", "<|constrain|>",
]:
tokens = encoding.encode(name, allowed_special="all")
if tokens:
special_map[name] = tokens[0]
def _convert(token_str):
return special_map.get(token_str, -1)
tok.convert_tokens_to_ids = _convert
return tok
@pytest.fixture
def parser(tokenizer):
"""Create a HarmonyStreamingParser instance (pre-primed with <|start|>assistant)."""
return HarmonyStreamingParser(tokenizer=tokenizer)
# ── Tool call header parsing ──────────────────────────────────────────
# The parser is pre-primed with "<|start|>assistant", so the first
# model output token is <|channel|> (regular message) or " to=..."
# (tool call continuation). Subsequent messages start with <|start|>.
class TestToolCallParsing:
"""StreamableParser with role=None correctly parses tool call headers."""
def test_tool_call_header_parsed(self, parser, encoding):
"""Tool call header 'to=functions.Write' sets commentary channel.
The first message's <|start|>assistant is already primed.
Model output for a tool call starts with ' to=functions.Write<|channel|>...'
"""
# Model continues from primed "assistant" with " to=functions.Write..."
tokens = encoding.encode(
" to=functions.Write<|channel|>commentary<|message|>args",
allowed_special="all",
)
for t in tokens:
parser.process_token(t)
assert parser.current_channel == "commentary"
assert parser.current_recipient == "functions.Write"
def test_regular_message_still_works(self, parser, encoding):
"""Regular assistant message parses correctly (primed parser)."""
# Model output starts from <|channel|> (first message)
tokens = encoding.encode(
"<|channel|>final<|message|>Hello",
allowed_special="all",
)
results = [parser.process_token(t) for t in tokens]
assert parser.current_channel == "final"
# "Hello" token should be streamed and visible
last = results[-1]
control_text, stream_token, visible_token, is_stop = last
assert stream_token is not None
assert visible_token is not None
def test_tool_call_tokens_not_streamed(self, parser, encoding):
"""Commentary channel tokens return None for stream/visible."""
tokens = encoding.encode(
" to=functions.Write<|channel|>commentary<|message|>args",
allowed_special="all",
)
results = [parser.process_token(t) for t in tokens]
# "args" is in commentary channel — should not be streamed
last = results[-1]
_, stream_token, visible_token, _ = last
assert stream_token is None
assert visible_token is None
def test_get_tool_calls_channel_rules(self):
"""Commentary always counts; analysis counts only with JSON arguments."""
parser = object.__new__(HarmonyStreamingParser)
parser._parser = SimpleNamespace(
messages=[
SimpleNamespace(
channel="analysis",
recipient="functions.Read",
content=[SimpleNamespace(text="analysis-noise")],
),
SimpleNamespace(
channel="analysis",
recipient="functions.Read",
content=[SimpleNamespace(text='{"path":"from-analysis.py"}')],
),
SimpleNamespace(
channel="commentary",
recipient="functions.Read",
content=[SimpleNamespace(text='{"path":"ok.py"}')],
),
SimpleNamespace(
channel="final",
recipient="functions.Read",
content=[SimpleNamespace(text="final-noise")],
),
]
)
assert parser.get_tool_calls() == [
{"name": "Read", "arguments": '{"path":"from-analysis.py"}'},
{"name": "Read", "arguments": '{"path":"ok.py"}'},
]
# ── Multi-message sequences ──────────────────────────────────────────
class TestMultiMessageSequence:
"""Correct channel transitions across analysis → final → tool call."""
def test_analysis_final_tool_sequence(self, parser, encoding):
"""Channels transition correctly: analysis → final → commentary.
First message uses primed header (starts from <|channel|>).
Subsequent messages include full <|start|>assistant header.
"""
# First message: analysis (primed, starts from <|channel|>)
analysis = encoding.encode(
"<|channel|>analysis<|message|>thinking<|end|>",
allowed_special="all",
)
# Second message: final (full header)
final = encoding.encode(
"<|start|>assistant<|channel|>final<|message|>result<|end|>",
allowed_special="all",
)
# Third message: tool call (full header with routing)
tool = encoding.encode(
"<|start|>assistant to=functions.Read"
'<|channel|>commentary<|message|>{"path":"f.py"}<|end|>',
allowed_special="all",
)
channels_seen = []
for t in analysis + final + tool:
parser.process_token(t)
ch = parser.current_channel
if not channels_seen or channels_seen[-1] != ch:
channels_seen.append(ch)
assert "analysis" in channels_seen
assert "final" in channels_seen
assert "commentary" in channels_seen
def test_think_tags_emitted(self, parser, encoding):
"""<think>/<think> control text emitted on analysis channel transitions."""
# First message: analysis (primed)
# Second message: final (full header)
tokens = encoding.encode(
"<|channel|>analysis<|message|>thought<|end|>"
"<|start|>assistant<|channel|>final<|message|>answer",
allowed_special="all",
)
control_texts = []
for t in tokens:
control_text, _, _, _ = parser.process_token(t)
if control_text:
control_texts.append(control_text)
assert "<think>\n" in control_texts
assert "</think>\n" in control_texts
# ── Passthrough mode ─────────────────────────────────────────────────
class TestPassthroughMode:
"""Passthrough mode activates on parser error and prevents cascading."""
def test_passthrough_activates_on_error(self, tokenizer, encoding):
"""Parser switches to passthrough when process() raises."""
parser = HarmonyStreamingParser(tokenizer=tokenizer)
# Force an error by monkey-patching the inner parser
def _failing_process(token_id):
raise RuntimeError("simulated parser failure")
parser._parser.process = _failing_process
result = parser.process_token(42)
control_text, stream_token, visible_token, is_stop = result
assert parser._passthrough_mode is True
assert stream_token is None
assert visible_token is None
def test_passthrough_buffers_silently(self, tokenizer, encoding):
"""After passthrough activation, all tokens return None."""
parser = HarmonyStreamingParser(tokenizer=tokenizer)
parser._passthrough_mode = True
for token_id in [100, 200, 300]:
control_text, stream_token, visible_token, is_stop = (
parser.process_token(token_id)
)
assert stream_token is None
assert visible_token is None
assert control_text == ""
def test_stop_token_detected_in_passthrough(self, tokenizer, encoding):
"""Stop tokens are still detected in passthrough mode."""
parser = HarmonyStreamingParser(tokenizer=tokenizer)
parser._passthrough_mode = True
stop_ids = parser.get_stop_token_ids()
assert len(stop_ids) > 0
stop_token = next(iter(stop_ids))
_, _, _, is_stop = parser.process_token(stop_token)
assert is_stop is True
def test_analysis_end_does_not_stop_generation(self, tokenizer, encoding):
"""Ending analysis closes thinking but lets the final channel continue."""
parser = HarmonyStreamingParser(tokenizer=tokenizer)
tokens = encoding.encode(
"<|channel|>analysis<|message|>thinking<|end|>",
allowed_special="all",
)
result = None
for token_id in tokens:
result = parser.process_token(token_id)
assert result is not None
control_text, stream_token, visible_token, is_stop = result
assert "</think>\n" in control_text
assert stream_token is None
assert visible_token is None
assert is_stop is False
def test_passthrough_closes_think_tag(self, tokenizer, encoding):
"""Passthrough activation while in analysis channel closes think tag."""
parser = HarmonyStreamingParser(tokenizer=tokenizer)
# Process analysis channel header (primed, starts from <|channel|>)
analysis_header = encoding.encode(
"<|channel|>analysis<|message|>",
allowed_special="all",
)
for t in analysis_header:
parser.process_token(t)
assert parser._in_think_tag is True
# Now trigger passthrough
def _failing_process(token_id):
raise RuntimeError("simulated failure")
parser._parser.process = _failing_process
control_text, _, _, _ = parser.process_token(999)
assert parser._passthrough_mode is True
assert "</think>\n" in control_text
assert parser._in_think_tag is False
def test_reset_clears_passthrough(self, tokenizer):
"""reset() clears passthrough mode and re-primes parser."""
parser = HarmonyStreamingParser(tokenizer=tokenizer)
parser._passthrough_mode = True
parser.reset()
assert parser._passthrough_mode is False
def test_reset_parser_accepts_channel_token(self, parser, encoding):
"""After reset, parser accepts <|channel|> (re-primed)."""
parser.reset()
# Should not raise — parser is re-primed with <|start|>assistant
tokens = encoding.encode(
"<|channel|>final<|message|>test",
allowed_special="all",
)
for t in tokens:
parser.process_token(t)
assert parser.current_channel == "final"
# ── Preprocessing ─────────────────────────────────────────────────────
class TestPreprocessHarmonyMessages:
"""preprocess_harmony_messages strips think tags from assistant content."""
def test_strips_think_tags(self):
msgs = [
{"role": "assistant", "content": "<think>reasoning</think>answer"}
]
result = preprocess_harmony_messages(msgs)
assert result[0]["content"] == "answer"
def test_passes_tool_messages(self):
msgs = [
{"role": "tool", "tool_call_id": "123", "content": "result"}
]
result = preprocess_harmony_messages(msgs)
assert result == msgs
# ── parse_tool_calls_from_tokens ──────────────────────────────────────
class TestParseToolCallsFromTokens:
"""Non-streaming tool call extraction from complete token sequences."""
def test_extracts_tool_call(self, encoding):
"""Extracts function name and arguments from tool call tokens."""
# Model output starts from <|channel|> (prompt includes <|start|>assistant)
tokens = encoding.encode(
'<|channel|>commentary<|message|>{"path":"t.py"}<|end|>'
"<|return|>",
allowed_special="all",
)
# prepend_start=True adds <|start|>assistant
output_text, analysis_text, tool_calls = parse_tool_calls_from_tokens(
tokens, prepend_start=True
)
assert analysis_text == ""
assert isinstance(tool_calls, list)
def test_extracts_final_text(self, encoding):
"""Extracts text from final channel."""
tokens = encoding.encode(
"<|channel|>final<|message|>Hello world<|end|>",
allowed_special="all",
)
output_text, analysis_text, tool_calls = parse_tool_calls_from_tokens(
tokens, prepend_start=True
)
assert analysis_text == ""
assert "Hello world" in output_text
def test_unknown_channel_without_recipient_falls_back_to_final(self, encoding):
"""Malformed channel names without recipients remain visible."""
tokens = encoding.encode(
"<|channel|>mardown<|message|>visible text<|return|>",
allowed_special="all",
)
output_text, analysis_text, tool_calls = parse_tool_calls_from_tokens(
tokens, prepend_start=True
)
assert "visible text" in output_text
assert analysis_text == ""
assert tool_calls == []
def test_does_not_prepend_duplicate_start_header(self, encoding):
"""Budget-forced Harmony completions may already include the start header."""
tokens = encoding.encode(
"<|start|>assistant<|channel|>analysis<|message|>thinking<|end|>",
allowed_special="all",
)
output_text, analysis_text, tool_calls = parse_tool_calls_from_tokens(
tokens, prepend_start=True
)
assert output_text == ""
assert "thinking" in analysis_text
assert tool_calls == []
def test_channel_rules_for_function_recipient(self, monkeypatch):
"""Commentary and JSON-argument analysis recipients become tool calls."""
messages = [
SimpleNamespace(
channel="analysis",
recipient="functions.Read",
content=[SimpleNamespace(text="analysis-noise")],
),
SimpleNamespace(
channel="analysis",
recipient="functions.Read",
content=[SimpleNamespace(text='{"path":"from-analysis.py"}')],
),
SimpleNamespace(
channel="commentary",
recipient="functions.Read",
content=[SimpleNamespace(text='{"path":"ok.py"}')],
),
SimpleNamespace(
channel="final",
recipient="functions.Read",
content=[SimpleNamespace(text="final-noise")],
),
SimpleNamespace(
channel="other",
recipient="functions.Read",
content=[SimpleNamespace(text="other-noise")],
),
]
class FakeEncoding:
def encode(self, text, allowed_special="all"):
return [1, 2] if text == "<|start|>assistant" else [3]
def decode(self, token_ids):
return "decoded"
def parse_messages_from_completion_tokens(self, token_ids, role, strict):
return messages
monkeypatch.setattr(
"omlx.adapter.harmony.load_harmony_gpt_oss_encoding",
lambda: FakeEncoding(),
)
output_text, analysis_text, tool_calls = parse_tool_calls_from_tokens([99])
assert analysis_text == "analysis-noise"
assert output_text == "final-noise"
assert tool_calls == [
{"name": "Read", "arguments": '{"path":"from-analysis.py"}'},
{"name": "Read", "arguments": '{"path":"ok.py"}'},
]
def test_analysis_channel_tool_call_with_recipient(self, encoding):
"""Analysis-channel tool calls with explicit recipients are honored (#2216)."""
tokens = encoding.encode(
"<|channel|>analysis<|message|>Now need files.<|end|>"
"<|start|>assistant<|channel|>analysis to=functions.read code"
'<|message|>{"filePath": "/tmp/x.py", "offset": 1}<|call|>',
allowed_special="all",
)
output_text, analysis_text, tool_calls = parse_tool_calls_from_tokens(
tokens, prepend_start=True
)
assert tool_calls == [
{"name": "read", "arguments": '{"filePath": "/tmp/x.py", "offset": 1}'}
]
assert "Now need files." in analysis_text
assert "filePath" not in analysis_text
def test_analysis_recipient_with_prose_stays_reasoning(self, encoding):
"""Analysis messages addressed to a tool without JSON args stay reasoning."""
tokens = encoding.encode(
"<|channel|>analysis<|message|>ok<|end|>"
"<|start|>assistant<|channel|>analysis to=functions.read code"
"<|message|>maybe I should read the file<|call|>",
allowed_special="all",
)
output_text, analysis_text, tool_calls = parse_tool_calls_from_tokens(
tokens, prepend_start=True
)
assert tool_calls == []
assert "maybe I should read the file" in analysis_text