"""Unit tests for MiniMax provider support in llmcore.py.""" import json import re import sys import os import unittest from unittest.mock import patch, MagicMock sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) class TestMiniMaxTemperatureClamping(unittest.TestCase): """Test MiniMax temperature clamping in _openai_stream.""" def _make_stream_call(self, model, temperature): """Capture the payload sent by _openai_stream.""" from llmcore import _openai_stream captured = {} def fake_post(url, headers=None, json=None, stream=None, timeout=None, proxies=None): captured['payload'] = json captured['url'] = url resp = MagicMock() resp.status_code = 200 resp.iter_lines.return_value = iter([b'data: [DONE]']) resp.__enter__ = lambda s: s resp.__exit__ = MagicMock(return_value=False) return resp with patch('llmcore.requests.post', side_effect=fake_post): gen = _openai_stream( 'https://api.minimax.io/v1', 'test-key', [{"role": "user", "content": "hi"}], model, temperature=temperature ) # Drain the generator for _ in gen: pass return captured.get('payload', {}) def test_minimax_temp_zero_clamped(self): """MiniMax rejects temperature=0, should be clamped to 0.01.""" payload = self._make_stream_call('MiniMax-M2.7', 0.0) self.assertAlmostEqual(payload['temperature'], 0.01) def test_minimax_temp_negative_clamped(self): """Negative temperature should be clamped to 0.01.""" payload = self._make_stream_call('MiniMax-M2.5', -0.5) self.assertAlmostEqual(payload['temperature'], 0.01) def test_minimax_temp_normal_preserved(self): """Normal temperature (0 < t <= 1) should be preserved.""" payload = self._make_stream_call('MiniMax-M2.7', 0.5) self.assertAlmostEqual(payload['temperature'], 0.5) def test_minimax_temp_one_preserved(self): """Temperature=1.0 should be preserved.""" payload = self._make_stream_call('MiniMax-M2.7-highspeed', 1.0) self.assertAlmostEqual(payload['temperature'], 1.0) def test_minimax_temp_above_one_clamped(self): """Temperature > 1.0 should be clamped to 1.0.""" payload = self._make_stream_call('MiniMax-M2.7', 1.5) self.assertAlmostEqual(payload['temperature'], 1.0) def test_minimax_case_insensitive(self): """Model name matching should be case-insensitive.""" payload = self._make_stream_call('minimax-m2.7', 0.0) self.assertAlmostEqual(payload['temperature'], 0.01) def test_non_minimax_temp_zero_unchanged(self): """Non-MiniMax models should not have temperature clamped.""" payload = self._make_stream_call('gpt-4o', 0.0) self.assertAlmostEqual(payload['temperature'], 0.0) def test_kimi_temp_still_forced(self): """Kimi/Moonshot temp override should still work.""" payload = self._make_stream_call('kimi-2.0', 0.5) self.assertAlmostEqual(payload['temperature'], 1.0) class TestMiniMaxThinkTagHandling(unittest.TestCase): """Test ... tag stripping for MiniMax M2.7 responses.""" def test_think_tag_stripped_from_response(self): """ tags (used by MiniMax M2.7) should be stripped from content.""" from llmcore import ToolClient, LLMSession mock_cfg = { 'apikey': 'test', 'apibase': 'https://api.minimax.io/v1', 'model': 'MiniMax-M2.7', } with patch('llmcore._load_mykeys', return_value={}): session = LLMSession(mock_cfg) client = ToolClient(session) text = 'Let me reason about this task.\n\nHere is the answer.' result = client._parse_mixed_response(text) self.assertEqual(result.thinking, 'Let me reason about this task.') self.assertEqual(result.content, 'Here is the answer.') def test_thinking_tag_still_works(self): """ tags (used by Claude) should still work.""" from llmcore import ToolClient, LLMSession mock_cfg = { 'apikey': 'test', 'apibase': 'https://api.anthropic.com', 'model': 'claude-sonnet-4-20250514', } with patch('llmcore._load_mykeys', return_value={}): session = LLMSession(mock_cfg) client = ToolClient(session) text = 'Let me analyze this.\n\nThe result is 42.' result = client._parse_mixed_response(text) self.assertEqual(result.thinking, 'Let me analyze this.') self.assertEqual(result.content, 'The result is 42.') def test_think_tag_with_tool_use(self): """ tags should be separated from tool_use blocks.""" from llmcore import ToolClient, LLMSession mock_cfg = { 'apikey': 'test', 'apibase': 'https://api.minimax.io/v1', 'model': 'MiniMax-M2.7', } with patch('llmcore._load_mykeys', return_value={}): session = LLMSession(mock_cfg) client = ToolClient(session) text = 'I need to read the file first.\n\nReading config\n\n\n{"name": "file_read", "arguments": {"path": "/tmp/test.txt"}}\n' result = client._parse_mixed_response(text) self.assertEqual(result.thinking, 'I need to read the file first.') self.assertTrue(len(result.tool_calls) > 0) self.assertEqual(result.tool_calls[0].function.name, 'file_read') class TestMiniMaxCompressHistoryTags(unittest.TestCase): """Test that tags are compressed in history like tags.""" def test_think_tag_compressed_in_old_messages(self): """ tags in old messages should be truncated.""" from llmcore import compress_history_tags long_think = "A" * 2000 messages = [ {"role": "assistant", "prompt": f"{long_think}\nShort answer."}, {"role": "user", "prompt": "Follow up"}, ] + [{"role": "user", "prompt": f"msg{i}"} for i in range(12)] # Force compression (counter divisible by 5) compress_history_tags._cd = 4 result = compress_history_tags(messages, keep_recent=10, max_len=800) # The first message's content should be truncated first_content = result[0]["prompt"] self.assertIn("", first_content) self.assertIn("...", first_content) self.assertLess(len(first_content), len(f"{long_think}\nShort answer.")) class TestMiniMaxAutoMakeUrl(unittest.TestCase): """Test URL construction for MiniMax API base.""" def test_minimax_base_url(self): from llmcore import auto_make_url url = auto_make_url('https://api.minimax.io/v1', 'chat/completions') self.assertEqual(url, 'https://api.minimax.io/v1/chat/completions') def test_minimax_base_url_no_version(self): from llmcore import auto_make_url url = auto_make_url('https://api.minimax.io', 'chat/completions') self.assertEqual(url, 'https://api.minimax.io/v1/chat/completions') def test_minimax_full_url_preserved(self): from llmcore import auto_make_url url = auto_make_url('https://api.minimax.io/v1/chat/completions$', 'chat/completions') self.assertEqual(url, 'https://api.minimax.io/v1/chat/completions') class TestMiniMaxNativeOAISessionThinkTag(unittest.TestCase): """Test tag handling in NativeOAISession.""" def test_think_tag_extracted_in_native_oai(self): """NativeOAISession.ask should extract tags from MiniMax M2.7 responses.""" from llmcore import NativeOAISession cfg = { 'apikey': 'test-key', 'apibase': 'https://api.minimax.io/v1', 'model': 'MiniMax-M2.7', } session = NativeOAISession(cfg) # Mock the raw_ask to return content with tag via generator def mock_raw_ask(messages, tools=None, system=None, model=None, temperature=0.5, max_tokens=6144, **kw): content_text = "Planning the approach.\n\nHere is the result." yield content_text return [{"type": "text", "text": content_text}] session.raw_ask = mock_raw_ask msg = {"role": "user", "content": [{"type": "text", "text": "test"}]} gen = session.ask(msg) # Drain generator try: while True: next(gen) except StopIteration as e: resp = e.value self.assertEqual(resp.thinking, 'Planning the approach.') self.assertNotIn('', resp.content) self.assertIn('Here is the result.', resp.content) class TestMiniMaxLLMSessionConfig(unittest.TestCase): """Test LLMSession configuration with MiniMax settings.""" def test_llm_session_init_with_minimax(self): """LLMSession should initialize correctly with MiniMax config.""" from llmcore import LLMSession cfg = { 'apikey': 'eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9', 'apibase': 'https://api.minimax.io/v1', 'model': 'MiniMax-M2.7', 'context_win': 50000, 'max_retries': 2, 'connect_timeout': 10, 'read_timeout': 120, } session = LLMSession(cfg) self.assertEqual(session.default_model, 'MiniMax-M2.7') self.assertEqual(session.api_base, 'https://api.minimax.io/v1') self.assertEqual(session.context_win, 50000) self.assertEqual(session.max_retries, 2) def test_llm_session_minimax_highspeed(self): """LLMSession should work with MiniMax-M2.7-highspeed model.""" from llmcore import LLMSession cfg = { 'apikey': 'test-key', 'apibase': 'https://api.minimax.io/v1', 'model': 'MiniMax-M2.7-highspeed', } session = LLMSession(cfg) self.assertEqual(session.default_model, 'MiniMax-M2.7-highspeed') class TestMiniMaxNativeToolClientThinkTag(unittest.TestCase): """Test tag handling in NativeToolClient.chat.""" def test_native_tool_client_think_tag(self): """NativeToolClient should extract tags from MiniMax responses.""" from llmcore import NativeToolClient, NativeOAISession, MockResponse cfg = { 'apikey': 'test-key', 'apibase': 'https://api.minimax.io/v1', 'model': 'MiniMax-M2.7', } session = NativeOAISession(cfg) client = NativeToolClient(session) # Mock the backend.ask to yield chunks and return a MockResponse with think tags def mock_ask(msg, tools=None, model=None): text = "Analyzing the request.\n\nResult: success" yield text return MockResponse('', text, [], text) session.ask = mock_ask messages = [{"role": "user", "content": "test query"}] gen = client.chat(messages) resp = None try: while True: next(gen) except StopIteration as e: resp = e.value self.assertIsNotNone(resp) self.assertEqual(resp.thinking, 'Analyzing the request.') self.assertEqual(resp.content, 'Result: success') if __name__ == '__main__': unittest.main()