# SPDX-License-Identifier: Apache-2.0 """ Tests for HarmonyStreamingParser. Tests the streaming parser for gpt-oss models using Harmony format. Uses real openai_harmony encoding for accurate testing. """ import pytest from omlx.adapter.harmony import ( HarmonyStreamingParser, load_harmony_gpt_oss_encoding, preprocess_harmony_messages, ) class RealTokenizer: """Tokenizer wrapper using real openai_harmony encoding.""" def __init__(self, enc): self._enc = enc def convert_tokens_to_ids(self, token: str) -> int: """Convert token string to ID using harmony encoding.""" try: ids = self._enc.encode(token, allowed_special="all") return ids[0] if ids else -1 except Exception: return -1 @pytest.fixture(scope="session") def harmony_encoding(): """Get the real openai_harmony encoding.""" return load_harmony_gpt_oss_encoding() @pytest.fixture def tokenizer(harmony_encoding): """Create a tokenizer that uses real openai_harmony encoding.""" return RealTokenizer(harmony_encoding) @pytest.fixture def parser(tokenizer): """Create a HarmonyStreamingParser with real tokenizer.""" return HarmonyStreamingParser(tokenizer) class TestHarmonyStreamingParser: """Tests for HarmonyStreamingParser.""" def test_get_stop_token_ids(self, parser): """Test stop token ID extraction.""" stop_ids = parser.get_stop_token_ids() # Real openai_harmony stop tokens for assistant actions assert 200002 in stop_ids # <|return|> assert 200012 in stop_ids # <|call|> assert len(stop_ids) == 2 def test_final_channel_returns_token_ids(self, parser, harmony_encoding): """Test that final channel returns token IDs for both stream and visible.""" # Encode a real harmony message sequence # Note: Model generates from <|channel|> since <|start|>assistant is in prompt tokens = harmony_encoding.encode( "<|channel|>final<|message|>Hello world<|end|>", allowed_special="all", ) # tokens: [200005, 17196, 200008, 13225, 2375, 200007] # channel final message Hello world end results = [] for token in tokens: result = parser.process_token(token) results.append(result) # Find content tokens (after <|message|>) message_idx = tokens.index(200008) # <|message|> hello_token = tokens[message_idx + 1] # Hello world_token = tokens[message_idx + 2] # world # Content tokens should return both stream and visible hello_result = results[message_idx + 1] assert hello_result[1] == hello_token # stream_token assert hello_result[2] == hello_token # visible_token assert hello_result[3] is False # not stop world_result = results[message_idx + 2] assert world_result[1] == world_token # stream_token assert world_result[2] == world_token # visible_token def test_analysis_channel_wrapped_in_think(self, parser, harmony_encoding): """Test that analysis channel outputs control and stream tokens only.""" tokens = harmony_encoding.encode( "<|channel|>analysis<|message|>thinking content<|end|>", allowed_special="all", ) results = [] for token in tokens: result = parser.process_token(token) results.append(result) # Collect all control text and stream/visible tokens control_parts = [r[0] for r in results if r[0]] stream_tokens = [r[1] for r in results if r[1] is not None] visible_tokens = [r[2] for r in results if r[2] is not None] # Should have opening and closing full_control = "".join(control_parts) assert "" in full_control assert "" in full_control # Analysis content should stream but not be visible assert len(stream_tokens) > 0 # Has stream tokens assert len(visible_tokens) == 0 # No visible tokens def test_return_token_is_stop(self, parser, harmony_encoding): """Test that <|return|> signals stop.""" tokens = harmony_encoding.encode( "<|channel|>final<|message|>Hello<|return|>", allowed_special="all", ) results = [] for token in tokens: result = parser.process_token(token) results.append(result) # Last token (<|return|>) should signal stop assert results[-1][3] is True def test_finalize_closes_think_tag(self, parser, harmony_encoding): """Test that finalize() closes open think tags.""" # Enter analysis channel without <|end|> tokens = harmony_encoding.encode( "<|channel|>analysis<|message|>thinking", allowed_special="all", ) for token in tokens: parser.process_token(token) # Parser should be in think tag assert parser._in_think_tag is True # Finalize should close it final = parser.finalize() assert final == "\n" assert parser._in_think_tag is False def test_finalize_no_output_when_no_open_tag(self, parser, harmony_encoding): """Test that finalize() returns empty when no open tags.""" # Process a complete final channel message tokens = harmony_encoding.encode( "<|channel|>final<|message|>Hello<|return|>", allowed_special="all", ) for token in tokens: parser.process_token(token) final = parser.finalize() assert final == "" def test_reset_clears_state(self, parser, harmony_encoding): """Test that reset() clears all parser state.""" # Process some tokens to set state tokens = harmony_encoding.encode( "<|channel|>analysis<|message|>thinking", allowed_special="all", ) for token in tokens: parser.process_token(token) # Verify state is set assert parser._in_think_tag is True assert parser.current_channel == "analysis" # Reset parser.reset() # Verify state is cleared assert parser._in_think_tag is False assert parser._prev_channel is None def test_multiple_messages_in_sequence(self, parser, harmony_encoding): """Test handling multiple messages in sequence.""" # analysis -> final sequence tokens = harmony_encoding.encode( "<|channel|>analysis<|message|>thinking<|end|>" "<|start|>assistant<|channel|>final<|message|>Hello<|return|>", allowed_special="all", ) control_parts = [] stream_tokens = [] visible_tokens = [] for token in tokens: control, stream_token, visible_token, _ = parser.process_token(token) if control: control_parts.append(control) if stream_token is not None: stream_tokens.append(stream_token) if visible_token is not None: visible_tokens.append(visible_token) # Should have think tags full_control = "".join(control_parts) assert "" in full_control assert "" in full_control # Stream should have both analysis and final tokens assert len(stream_tokens) > 0 # Visible should only have final channel tokens # Analysis tokens should not be in visible assert len(visible_tokens) > 0 def test_channel_transition_closes_think_tag(self, parser, harmony_encoding): """Test that transitioning from analysis to final closes think tag.""" # First: enter analysis channel tokens1 = harmony_encoding.encode( "<|channel|>analysis<|message|>thinking", allowed_special="all", ) for token in tokens1: parser.process_token(token) assert parser._in_think_tag is True # Second: transition to final channel (via new message) tokens2 = harmony_encoding.encode( "<|end|><|start|>assistant<|channel|>final<|message|>", allowed_special="all", ) control_parts = [] for token in tokens2: control, _, _, _ = parser.process_token(token) if control: control_parts.append(control) # Should have closing think tag full_control = "".join(control_parts) assert "" in full_control assert parser._in_think_tag is False class TestHarmonyToolCalling: """Tests for Harmony tool call extraction.""" def test_tool_call_extraction(self, tokenizer, harmony_encoding): """Test tool call extraction from commentary channel.""" parser = HarmonyStreamingParser(tokenizer) # Simulate tool call message format # commentary channel with recipient functions.get_weather tokens = harmony_encoding.encode( '<|channel|>commentary to=functions.get_weather<|message|>{"location": "Seoul"}<|call|>', allowed_special="all", ) for token in tokens: _, _, _, is_stop = parser.process_token(token) # Last token should be stop assert is_stop is True tool_calls = parser.get_tool_calls() assert len(tool_calls) == 1 assert tool_calls[0]["name"] == "get_weather" assert "Seoul" in tool_calls[0]["arguments"] def test_no_tool_calls_for_final_channel(self, parser, harmony_encoding): """Test that final channel doesn't produce tool calls.""" tokens = harmony_encoding.encode( "<|channel|>final<|message|>Hello<|return|>", allowed_special="all", ) for token in tokens: parser.process_token(token) tool_calls = parser.get_tool_calls() assert len(tool_calls) == 0 class TestHarmonyEdgeCases: """Edge case tests for HarmonyStreamingParser.""" def test_empty_content(self, parser, harmony_encoding): """Test handling of empty message content.""" tokens = harmony_encoding.encode( "<|channel|>final<|message|><|end|>", allowed_special="all", ) stream_tokens = [] for token in tokens: _, stream_token, _, _ = parser.process_token(token) if stream_token is not None: stream_tokens.append(stream_token) # Should have no content tokens assert stream_tokens == [] def test_unknown_channel_without_recipient_falls_back_to_final( self, parser, harmony_encoding ): """Test handling of malformed channel names.""" # Note: openai_harmony may handle unknown channels differently tokens = harmony_encoding.encode( "<|channel|>someunknownchannel<|message|>content<|end|>", allowed_special="all", ) stream_tokens = [] visible_tokens = [] for token in tokens: _, stream_token, visible_token, _ = parser.process_token(token) if stream_token is not None: stream_tokens.append(stream_token) if visible_token is not None: visible_tokens.append(visible_token) assert "content" in harmony_encoding.decode(stream_tokens) assert "content" in harmony_encoding.decode(visible_tokens) def test_consecutive_analysis_channels(self, parser, harmony_encoding): """Test multiple consecutive analysis channels.""" tokens = harmony_encoding.encode( "<|channel|>analysis<|message|>first<|end|>" "<|start|>assistant<|channel|>analysis<|message|>second<|end|>", allowed_special="all", ) control_parts = [] for token in tokens: control, _, _, _ = parser.process_token(token) if control: control_parts.append(control) # Should have proper think tag wrapping for both assert control_parts.count("\n") == 2 assert control_parts.count("\n") == 2 class TestHarmonyIntegration: """Integration-style tests for realistic Harmony sequences.""" def test_typical_response_sequence(self, parser, harmony_encoding): """Test a typical Harmony response: analysis -> final.""" tokens = harmony_encoding.encode( "<|channel|>analysis<|message|>Let me think...<|end|>" "<|start|>assistant<|channel|>final<|message|>Hello world<|return|>", allowed_special="all", ) control_output = [] stream_tokens = [] visible_tokens = [] for token in tokens: control, stream_token, visible_token, _ = parser.process_token(token) if control: control_output.append(control) if stream_token is not None: stream_tokens.append(stream_token) if visible_token is not None: visible_tokens.append(visible_token) # Control should have think tags full_control = "".join(control_output) assert "" in full_control assert "" in full_control # Stream should have analysis + final tokens assert len(stream_tokens) > 0 # Visible should only have final tokens (analysis excluded) assert len(visible_tokens) > 0 assert len(visible_tokens) < len(stream_tokens) def test_accumulate_visible_tokens(self, parser, harmony_encoding): """Test that visible tokens accumulate correctly for storage.""" tokens = harmony_encoding.encode( "<|channel|>final<|message|>Hello world<|return|>", allowed_special="all", ) visible_tokens = [] for token in tokens: _, _, visible_token, _ = parser.process_token(token) if visible_token is not None: visible_tokens.append(visible_token) # Decode accumulated tokens output_text = harmony_encoding.decode(visible_tokens) assert "Hello" in output_text assert "world" in output_text class TestHarmonyPreprocess: """Tests for preprocess_harmony_messages.""" def test_strips_think_tags_from_assistant(self): """Test that tags are stripped from assistant messages.""" messages = [ {"role": "user", "content": "Hello"}, {"role": "assistant", "content": "\nLet me think...\n\nHi there!"}, {"role": "user", "content": "How are you?"}, ] result = preprocess_harmony_messages(messages) assert result[0]["content"] == "Hello" # user unchanged assert result[1]["content"] == "Hi there!" # think stripped assert result[2]["content"] == "How are you?" # user unchanged def test_preserves_user_messages(self): """Test that user messages are not modified.""" messages = [ {"role": "user", "content": "Tell me about tags"}, ] result = preprocess_harmony_messages(messages) assert result[0]["content"] == "Tell me about tags" def test_handles_empty_content(self): """Test handling of empty or None content.""" messages = [ {"role": "assistant", "content": ""}, {"role": "assistant", "content": None}, {"role": "assistant"}, ] result = preprocess_harmony_messages(messages) assert len(result) == 3 def test_handles_no_think_tags(self): """Test that messages without think tags are unchanged.""" messages = [ {"role": "assistant", "content": "Just a normal response"}, ] result = preprocess_harmony_messages(messages) assert result[0]["content"] == "Just a normal response" def test_strips_multiple_think_blocks(self): """Test stripping multiple blocks.""" messages = [ {"role": "assistant", "content": "firstHellosecondWorld"}, ] result = preprocess_harmony_messages(messages) assert result[0]["content"] == "HelloWorld" def test_preserves_tool_messages_unchanged(self): """Test that tool role messages are preserved unchanged for chat_template.""" messages = [ {"role": "user", "content": "What's the weather?"}, { "role": "assistant", "content": "", "tool_calls": [ { "id": "call_123", "function": {"name": "get_weather", "arguments": '{"location": "Seoul"}'}, } ], }, {"role": "tool", "tool_call_id": "call_123", "content": '{"temp": 20}'}, ] result = preprocess_harmony_messages(messages) # Tool message should be preserved unchanged # Chat template expects role="tool" and uses last_tool_call.name from assistant assert result[2]["role"] == "tool" assert result[2]["tool_call_id"] == "call_123" assert result[2]["content"] == '{"temp": 20}' class TestHarmonyUTF8: """Tests for UTF-8 handling with Harmony parser. These tests verify that the parser returns token IDs (not decoded text) so the caller can use streaming detokenizer for proper UTF-8 handling. """ def test_returns_token_ids_not_text(self, parser, harmony_encoding): """Verify that parser returns token IDs, not decoded text.""" # Process a token in final channel tokens = harmony_encoding.encode( "<|channel|>final<|message|>Hello", allowed_special="all", ) result = None for token in tokens: result = parser.process_token(token) # Last token should return token ID, not string control, stream_token, visible_token, is_stop = result assert isinstance(stream_token, int) assert isinstance(visible_token, int) def test_control_text_is_string(self, parser, harmony_encoding): """Verify that control text is always a string.""" # Enter analysis channel to get control tokens = harmony_encoding.encode( "<|channel|>analysis<|message|>", allowed_special="all", ) control_found = None for token in tokens: result = parser.process_token(token) if result[0]: # control_text control_found = result[0] assert isinstance(control_found, str) assert control_found == "\n" def test_korean_text_handling(self, parser, harmony_encoding): """Test that Korean text is handled correctly via token IDs.""" # Korean text should be tokenized and returned as token IDs tokens = harmony_encoding.encode( "<|channel|>final<|message|>안녕하세요<|return|>", allowed_special="all", ) visible_tokens = [] for token in tokens: _, _, visible_token, _ = parser.process_token(token) if visible_token is not None: visible_tokens.append(visible_token) # Should have token IDs (integers) assert all(isinstance(t, int) for t in visible_tokens) # Decoding should give Korean text decoded = harmony_encoding.decode(visible_tokens) assert "안녕" in decoded class TestExtractHarmonyMessages: """Tests for extract_harmony_messages function.""" def test_preserves_tool_role_and_tool_call_id(self): """Test that tool messages preserve role and tool_call_id.""" from omlx.api.utils import extract_harmony_messages # Create mock messages class MockMessage: def __init__(self, role, content, tool_call_id=None, tool_calls=None): self.role = role self.content = content self.tool_call_id = tool_call_id self.tool_calls = tool_calls messages = [ MockMessage("user", "What's the weather?"), MockMessage( "assistant", "", tool_calls=[ { "id": "call_123", "function": {"name": "get_weather", "arguments": '{"location": "Seoul"}'}, } ], ), MockMessage("tool", '{"temp": 20}', tool_call_id="call_123"), ] result = extract_harmony_messages(messages) # Tool message should preserve role and tool_call_id assert result[2]["role"] == "tool" assert result[2]["tool_call_id"] == "call_123" # Content is parsed to dict to avoid double-encoding by chat_template |tojson assert result[2]["content"] == {"temp": 20} def test_preserves_assistant_tool_calls(self): """Test that assistant messages preserve tool_calls field.""" from omlx.api.utils import extract_harmony_messages class MockMessage: def __init__(self, role, content, tool_calls=None): self.role = role self.content = content self.tool_calls = tool_calls messages = [ MockMessage( "assistant", "Let me check the weather.", tool_calls=[ { "id": "call_456", "function": {"name": "get_weather", "arguments": '{"location": "Tokyo"}'}, } ], ), ] result = extract_harmony_messages(messages) # Assistant message should preserve tool_calls assert result[0]["role"] == "assistant" assert result[0]["content"] == "Let me check the weather." assert "tool_calls" in result[0] assert len(result[0]["tool_calls"]) == 1 assert result[0]["tool_calls"][0]["id"] == "call_456" assert result[0]["tool_calls"][0]["function"]["name"] == "get_weather" # Arguments are parsed to dict to avoid double-encoding by chat_template |tojson assert result[0]["tool_calls"][0]["function"]["arguments"] == {"location": "Tokyo"} def test_handles_content_array(self): """Test that content arrays are properly extracted.""" from omlx.api.utils import extract_harmony_messages class MockMessage: def __init__(self, role, content): self.role = role self.content = content messages = [ MockMessage("user", [ {"type": "text", "text": "Hello"}, {"type": "image_url", "image_url": {"url": "http://example.com/img.png"}}, {"type": "text", "text": "World"}, ]), ] result = extract_harmony_messages(messages) # Should extract only text parts assert result[0]["role"] == "user" assert result[0]["content"] == "Hello\nWorld" def test_handles_none_content(self): """Test that None content is handled correctly.""" from omlx.api.utils import extract_harmony_messages class MockMessage: def __init__(self, role, content, tool_calls=None): self.role = role self.content = content self.tool_calls = tool_calls messages = [ MockMessage("assistant", None, tool_calls=[ {"id": "call_789", "function": {"name": "search", "arguments": '{}'}} ]), ] result = extract_harmony_messages(messages) assert result[0]["role"] == "assistant" assert result[0]["content"] == "" assert "tool_calls" in result[0] def test_differs_from_extract_text_content(self): """Test that extract_harmony_messages differs from extract_text_content for tool messages.""" from omlx.api.utils import extract_harmony_messages, extract_text_content class MockMessage: def __init__(self, role, content, tool_call_id=None, tool_calls=None): self.role = role self.content = content self.tool_call_id = tool_call_id self.tool_calls = tool_calls messages = [ MockMessage("tool", '{"result": "success"}', tool_call_id="call_abc"), ] harmony_result = extract_harmony_messages(messages) text_result = extract_text_content(messages) # Harmony should preserve tool role assert harmony_result[0]["role"] == "tool" assert harmony_result[0]["tool_call_id"] == "call_abc" # Text content converts to user role with formatted text assert text_result[0]["role"] == "user" assert "[Tool Result" in text_result[0]["content"] def test_parses_json_tool_content_to_dict(self): """Test that JSON string content is parsed to dict to avoid double-encoding.""" from omlx.api.utils import extract_harmony_messages class MockMessage: def __init__(self, role, content, tool_call_id=None): self.role = role self.content = content self.tool_call_id = tool_call_id messages = [ MockMessage("tool", '{"temp": 20, "condition": "sunny"}', tool_call_id="call_123"), ] result = extract_harmony_messages(messages) # Content should be parsed to dict (for chat_template |tojson filter) assert result[0]["role"] == "tool" assert isinstance(result[0]["content"], dict) assert result[0]["content"]["temp"] == 20 assert result[0]["content"]["condition"] == "sunny" def test_keeps_non_json_tool_content_as_string(self): """Test that non-JSON content remains as string.""" from omlx.api.utils import extract_harmony_messages class MockMessage: def __init__(self, role, content, tool_call_id=None): self.role = role self.content = content self.tool_call_id = tool_call_id messages = [ MockMessage("tool", "The weather is sunny and 20 degrees.", tool_call_id="call_123"), ] result = extract_harmony_messages(messages) # Content should remain as string assert result[0]["role"] == "tool" assert isinstance(result[0]["content"], str) assert result[0]["content"] == "The weather is sunny and 20 degrees." def test_parses_tool_call_arguments_to_dict(self): """Test that tool_call arguments are parsed to dict to avoid double-encoding.""" from omlx.api.utils import extract_harmony_messages class MockMessage: def __init__(self, role, content, tool_calls=None): self.role = role self.content = content self.tool_calls = tool_calls messages = [ MockMessage( "assistant", "", tool_calls=[ { "id": "call_456", "function": {"name": "get_weather", "arguments": '{"location": "Seoul", "unit": "celsius"}'}, } ], ), ] result = extract_harmony_messages(messages) # Arguments should be parsed to dict (for chat_template |tojson filter) assert result[0]["role"] == "assistant" assert "tool_calls" in result[0] arguments = result[0]["tool_calls"][0]["function"]["arguments"] assert isinstance(arguments, dict) assert arguments["location"] == "Seoul" assert arguments["unit"] == "celsius"