@@ -85,6 +85,63 @@ def encode(self, text):
8585
8686 result = optillm_count (text , tokenizer )
8787 self .assertGreater (result , 0 , "Should fallback to character estimation" )
88+
89+ def test_count_reasoning_tokens_truncated_response (self ):
90+ """Test counting tokens when response is truncated (no closing </think> tag)"""
91+ # Test truncated think tag
92+ truncated_text = "<think>This reasoning was cut off due to max tokens"
93+
94+ result1 = optillm_count (truncated_text )
95+ result2 = inference_count (truncated_text )
96+
97+ self .assertGreater (result1 , 0 , "Should count tokens from truncated think block" )
98+ self .assertEqual (result1 , result2 , "Both functions should return same result" )
99+
100+ def test_count_reasoning_tokens_mixed_complete_and_truncated (self ):
101+ """Test with both complete and truncated think blocks"""
102+ mixed_text = """
103+ <think>First complete reasoning block</think>
104+ Some output here
105+ <think>This second block was truncated and never closed
106+ """
107+
108+ result = optillm_count (mixed_text )
109+ self .assertGreater (result , 0 , "Should count tokens from both complete and truncated blocks" )
110+
111+ # Should be more than just the first block alone
112+ first_block_only = "<think>First complete reasoning block</think>"
113+ first_result = optillm_count (first_block_only )
114+ self .assertGreater (result , first_result , "Should include truncated content" )
115+
116+ def test_count_reasoning_tokens_no_false_positives (self ):
117+ """Test that we don't count think-like content that isn't actually truncated"""
118+ # This should NOT be counted as truncated since there's a </think> later
119+ text_with_complete_blocks = "<think>First block</think>Output<think>Second complete block</think>"
120+
121+ result = optillm_count (text_with_complete_blocks )
122+
123+ # Count manually - should only be the content inside the two complete blocks
124+ manual_count = optillm_count ("<think>First blockSecond complete block</think>" )
125+ self .assertEqual (result , manual_count , "Should only count complete blocks, not detect false truncation" )
126+
127+ def test_count_reasoning_tokens_edge_cases_truncated (self ):
128+ """Test edge cases with truncated responses"""
129+ test_cases = [
130+ ("<think>" , 0 ), # Just opening tag, no content
131+ ("<think>a" , 1 ), # Minimal content
132+ ("Some output <think>reasoning here" , None ), # Truncated at end
133+ ("<think>multi\n line\n truncated" , None ), # Multiline truncated
134+ ]
135+
136+ for text , expected_min in test_cases :
137+ result = optillm_count (text )
138+ if expected_min is not None :
139+ if expected_min == 0 :
140+ self .assertEqual (result , expected_min , f"Should return { expected_min } for: { text } " )
141+ else :
142+ self .assertGreaterEqual (result , expected_min , f"Should be at least { expected_min } for: { text } " )
143+ else :
144+ self .assertGreater (result , 0 , f"Should count truncated content for: { text } " )
88145
89146
90147class TestInferenceStructures (unittest .TestCase ):
0 commit comments