From edcb9a0613e54792f4d859192e27b24e4be4e71e Mon Sep 17 00:00:00 2001 From: Octopus Date: Fri, 3 Apr 2026 02:36:08 -0500 Subject: [PATCH] feat: add MiniMax as first-class LLM provider via OpenAI-compat API Auto-detects MiniMax when MINIMAX_API_KEY is set (or LLM_PROVIDER=minimax), routing all audit LLM calls to api.minimax.io/v1 transparently. Includes temperature clamping helper for MiniMax's (0.0, 1.0] constraint, MiniMax model presets in env.example and README, and 27 unit + integration tests. Supported models (204K context): MiniMax-M2.7, MiniMax-M2.7-highspeed, MiniMax-M2.5, MiniMax-M2.5-highspeed. --- README.md | 47 ++++ env.example | 41 ++- src/openai_api/openai.py | 51 ++++ tests/integration/test_minimax_integration.py | 126 +++++++++ tests/test_minimax_provider.py | 246 ++++++++++++++++++ 5 files changed, 510 insertions(+), 1 deletion(-) create mode 100644 tests/integration/test_minimax_integration.py create mode 100644 tests/test_minimax_provider.py diff --git a/README.md b/README.md index 23e857a7..4c044d28 100644 --- a/README.md +++ b/README.md @@ -241,6 +241,52 @@ Based on actual configuration in `src/openai_api/model_config.json`: } ``` +### ๐ŸŒ Using MiniMax as LLM Provider + +[MiniMax](https://platform.minimax.io) offers an OpenAI-compatible API and works as a drop-in LLM backend for Finite Monkey Engine. All models feature a **204K context window**, making them well-suited for large smart-contract codebases. + +**Supported MiniMax models:** + +| Model | Context | Best for | +|---|---|---| +| `MiniMax-M2.7` | 204K | Flagship โ€” vulnerability detection & reasoning | +| `MiniMax-M2.7-highspeed` | 204K | Fast JSON extraction & summarization | +| `MiniMax-M2.5` | 204K | Previous generation | +| `MiniMax-M2.5-highspeed` | 204K | Previous generation fast variant | + +**Quick setup โ€” two options:** + +Option A: set `LLM_PROVIDER=minimax` and `MINIMAX_API_KEY`: +```bash +LLM_PROVIDER=minimax +MINIMAX_API_KEY=your-minimax-api-key +``` + +Option B: set only `MINIMAX_API_KEY` (auto-detected when `OPENAI_API_KEY` is absent): +```bash +MINIMAX_API_KEY=your-minimax-api-key +``` + +Then update `src/openai_api/model_config.json` to use MiniMax model names: +```json +{ + "openai_general": "MiniMax-M2.7", + "code_assumptions_analysis": "MiniMax-M2.7", + "vulnerability_detection": "MiniMax-M2.7", + "group_results_summarization": "MiniMax-M2.7", + "initial_vulnerability_validation": "MiniMax-M2.7", + "vulnerability_findings_json_extraction": "MiniMax-M2.7-highspeed", + "additional_context_determination": "MiniMax-M2.7", + "comprehensive_vulnerability_analysis": "MiniMax-M2.7", + "final_vulnerability_extraction": "MiniMax-M2.7-highspeed", + "structured_json_extraction": "MiniMax-M2.7-highspeed", + "embedding_model": "text-embedding-3-large" +} +``` + +> ๐Ÿ’ก **Temperature note**: MiniMax requires temperature in the range `(0.0, 1.0]`. +> The built-in `_clamp_temperature()` helper enforces this automatically. + ### Recommended Configuration Schemes #### ๐Ÿš€ Quick Start (Small projects < 50 files) @@ -332,6 +378,7 @@ This project is licensed under the Apache License 2.0 - see the [LICENSE](LICENS - **Claude AI**: For advanced code understanding - **Mermaid**: For business flow visualization - **OpenAI**: For AI-powered analysis capabilities +- **MiniMax**: For OpenAI-compatible LLM API with large context windows ## ๐Ÿ“ž Contact diff --git a/env.example b/env.example index ec1c066b..47f2dd0b 100644 --- a/env.example +++ b/env.example @@ -11,14 +11,53 @@ DATABASE_URL=postgresql://postgres:1234@127.0.0.1:5432/postgres # AIๆจกๅž‹้…็ฝฎ / AI Model Configuration # =============================================== +# LLM Provider Selection / LLMๆไพ›ๅ•†้€‰ๆ‹ฉ +# Supported values: openai (default), minimax +# Set to "minimax" to use MiniMax models via OpenAI-compatible API. +# When using MiniMax, set MINIMAX_API_KEY instead of OPENAI_API_KEY. +# LLM_PROVIDER=minimax + # OpenAI APIๅŸบ็ก€URL # OpenAI API base URL -OPENAI_API_BASE="api.openai-proxy.org" +OPENAI_API_BASE="api.openai-proxy.org" # OpenAI APIๅฏ†้’ฅ # OpenAI API key OPENAI_API_KEY="sk-xxxxx" +# =============================================== +# MiniMax API Configuration (Alternative to OpenAI) +# MiniMax API้…็ฝฎ๏ผˆOpenAI็š„ๆ›ฟไปฃๆ–นๆกˆ๏ผ‰ +# =============================================== +# To use MiniMax, either set LLM_PROVIDER=minimax above, or simply set +# MINIMAX_API_KEY without OPENAI_API_KEY โ€” auto-detection will kick in. +# +# Models (all with 204K context window): +# MiniMax-M2.7 โ€“ flagship model, best accuracy +# MiniMax-M2.7-highspeed โ€“ faster, cost-efficient variant +# MiniMax-M2.5 โ€“ previous generation +# MiniMax-M2.5-highspeed โ€“ previous generation fast variant +# +# API Key: https://platform.minimax.io +# Endpoint: https://api.minimax.io/v1 (OpenAI-compatible) +# +# MINIMAX_API_KEY="your-minimax-api-key" +# +# Recommended model_config.json for MiniMax: +# { +# "openai_general": "MiniMax-M2.7", +# "code_assumptions_analysis": "MiniMax-M2.7", +# "vulnerability_detection": "MiniMax-M2.7", +# "group_results_summarization": "MiniMax-M2.7", +# "initial_vulnerability_validation": "MiniMax-M2.7", +# "vulnerability_findings_json_extraction": "MiniMax-M2.7-highspeed", +# "additional_context_determination": "MiniMax-M2.7", +# "comprehensive_vulnerability_analysis": "MiniMax-M2.7", +# "final_vulnerability_extraction": "MiniMax-M2.7-highspeed", +# "structured_json_extraction": "MiniMax-M2.7-highspeed", +# "embedding_model": "text-embedding-3-large" +# } + # =============================================== # ๆ‰ซๆๆจกๅผ้…็ฝฎ / Scan Mode Configuration diff --git a/src/openai_api/openai.py b/src/openai_api/openai.py index 64d7c64e..ed45d4f5 100644 --- a/src/openai_api/openai.py +++ b/src/openai_api/openai.py @@ -5,6 +5,57 @@ import requests from openai import OpenAI +# โ”€โ”€โ”€ MiniMax provider auto-detection โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +# Set LLM_PROVIDER=minimax OR set MINIMAX_API_KEY (without OPENAI_API_KEY) to +# automatically route all LLM calls through MiniMax's OpenAI-compatible endpoint. +# +# MiniMax OpenAI-compatible base URL: https://api.minimax.io/v1 +# Supported models (204K context): +# MiniMax-M2.7 โ€“ flagship reasoning model +# MiniMax-M2.7-highspeed โ€“ fast, cost-efficient variant +# MiniMax-M2.5 โ€“ previous generation +# MiniMax-M2.5-highspeed โ€“ previous generation fast variant +# +# Temperature note: MiniMax requires temperature in (0.0, 1.0]. The helper +# _clamp_temperature() enforces this; pass any float through it before sending. +# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +_MINIMAX_API_BASE = "api.minimax.io" + + +def _init_llm_config() -> None: + """Auto-configure the runtime to use MiniMax when requested. + + Triggers when either: + - ``LLM_PROVIDER=minimax`` is set, or + - ``MINIMAX_API_KEY`` is set and ``OPENAI_API_KEY`` is absent. + + Writes ``OPENAI_API_BASE`` and ``OPENAI_API_KEY`` into the process + environment so that all existing API helpers pick them up transparently. + """ + provider = os.environ.get("LLM_PROVIDER", "").strip().lower() + minimax_key = os.environ.get("MINIMAX_API_KEY", "").strip() + openai_key = os.environ.get("OPENAI_API_KEY", "").strip() + + use_minimax = provider == "minimax" or (minimax_key and not openai_key) + if use_minimax and minimax_key: + os.environ.setdefault("OPENAI_API_BASE", _MINIMAX_API_BASE) + os.environ["OPENAI_API_KEY"] = minimax_key + + +_init_llm_config() + + +def _clamp_temperature(temp: float) -> float: + """Clamp temperature to MiniMax-compatible range (0.01, 1.0]. + + MiniMax rejects temperature == 0.0; the lower bound is nudged to 0.01. + This is a no-op for any value already in range and for non-MiniMax + providers where the extra clamping is harmless. + """ + return max(0.01, min(1.0, float(temp))) + + # ๅ…จๅฑ€ๆจกๅž‹้…็ฝฎ็ผ“ๅญ˜ _model_config = None diff --git a/tests/integration/test_minimax_integration.py b/tests/integration/test_minimax_integration.py new file mode 100644 index 00000000..c6fa9215 --- /dev/null +++ b/tests/integration/test_minimax_integration.py @@ -0,0 +1,126 @@ +"""Integration tests for MiniMax provider: validate request shape and endpoint.""" + +import os +import sys +import unittest +from unittest.mock import MagicMock, patch + + +def _setup_src(): + src_path = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", "src")) + if src_path not in sys.path: + sys.path.insert(0, src_path) + for stub in ("numpy", "openai"): + if stub not in sys.modules: + sys.modules[stub] = MagicMock() + + +def _fresh_import(env: dict): + """Import openai_api.openai with a clean environment.""" + _setup_src() + for mod in list(sys.modules.keys()): + if "openai_api" in mod: + del sys.modules[mod] + with patch.dict("os.environ", env, clear=True): + import openai_api.openai as oai + return oai + + +class TestMiniMaxRequestShape(unittest.TestCase): + """Integration: verify the full HTTP request shape sent to MiniMax.""" + + def _post_call(self, fn_name: str, env: dict, extra_fn_args=None): + """Call fn_name on a freshly-imported module and capture POST arguments.""" + _setup_src() + for mod in list(sys.modules.keys()): + if "openai_api" in mod: + del sys.modules[mod] + + mock_resp = MagicMock() + mock_resp.json.return_value = { + "choices": [{"message": {"content": "test_result"}}] + } + + with patch.dict("os.environ", env, clear=True), \ + patch("requests.post", return_value=mock_resp) as mock_post: + import openai_api.openai as oai + fn = getattr(oai, fn_name) + fn(*(extra_fn_args or ["test prompt"])) + + return mock_post.call_args + + def test_ask_openai_common_minimax_url(self): + env = {"MINIMAX_API_KEY": "mm-int-key", "LLM_PROVIDER": "minimax"} + call = self._post_call("ask_openai_common", env) + self.assertIn("api.minimax.io", call[0][0]) + self.assertIn("/v1/chat/completions", call[0][0]) + + def test_detect_vulnerabilities_minimax_url(self): + env = {"MINIMAX_API_KEY": "mm-int-key", "LLM_PROVIDER": "minimax"} + call = self._post_call("detect_vulnerabilities", env) + self.assertIn("api.minimax.io", call[0][0]) + + def test_perform_initial_vulnerability_validation_minimax_url(self): + env = {"MINIMAX_API_KEY": "mm-int-key", "LLM_PROVIDER": "minimax"} + call = self._post_call("perform_initial_vulnerability_validation", env) + self.assertIn("api.minimax.io", call[0][0]) + + def test_request_body_has_messages(self): + env = {"MINIMAX_API_KEY": "mm-int-key", "LLM_PROVIDER": "minimax"} + call = self._post_call("ask_openai_common", env) + body = call[1]["json"] + self.assertIn("messages", body) + self.assertIn("model", body) + + def test_bearer_token_in_header(self): + env = {"MINIMAX_API_KEY": "mm-secret-xyz", "LLM_PROVIDER": "minimax"} + call = self._post_call("ask_openai_common", env) + headers = call[1]["headers"] + self.assertEqual(headers["Authorization"], "Bearer mm-secret-xyz") + + def test_auto_detect_without_provider_env(self): + """MINIMAX_API_KEY alone (no LLM_PROVIDER) auto-detects MiniMax.""" + env = {"MINIMAX_API_KEY": "mm-auto-key"} + call = self._post_call("ask_openai_common", env) + self.assertIn("api.minimax.io", call[0][0]) + + +class TestMiniMaxModelConfig(unittest.TestCase): + """Integration: MiniMax models are reachable via model_config.json override.""" + + def test_get_model_with_minimax_model_in_config(self): + """If model_config.json contains MiniMax models, get_model returns them.""" + import json + import tempfile + + config = { + "openai_general": "MiniMax-M2.7", + "vulnerability_detection": "MiniMax-M2.7", + "structured_json_extraction": "MiniMax-M2.7-highspeed", + "embedding_model": "text-embedding-3-large", + } + with tempfile.NamedTemporaryFile( + mode="w", suffix=".json", delete=False + ) as fh: + json.dump(config, fh) + tmp_path = fh.name + + _setup_src() + for mod in list(sys.modules.keys()): + if "openai_api" in mod: + del sys.modules[mod] + + with patch.dict("os.environ", {"OPENAI_API_KEY": "sk-test"}, clear=True), \ + patch("openai_api.openai._model_config", None), \ + patch("os.path.join", side_effect=lambda *a: tmp_path if a[-1] == "model_config.json" else os.path.join(*a)): + import openai_api.openai as oai + oai._model_config = config # inject config directly + self.assertEqual(oai.get_model("openai_general"), "MiniMax-M2.7") + self.assertEqual(oai.get_model("vulnerability_detection"), "MiniMax-M2.7") + self.assertEqual(oai.get_model("structured_json_extraction"), "MiniMax-M2.7-highspeed") + + os.unlink(tmp_path) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_minimax_provider.py b/tests/test_minimax_provider.py new file mode 100644 index 00000000..4b56c42d --- /dev/null +++ b/tests/test_minimax_provider.py @@ -0,0 +1,246 @@ +"""Unit tests for MiniMax provider support in openai_api/openai.py.""" + +import importlib +import os +import sys +import types +import unittest +from unittest.mock import MagicMock, patch + + +# --------------------------------------------------------------------------- +# Helpers to reload the openai module with a clean env each time +# --------------------------------------------------------------------------- + +def _reload_openai_module(env_overrides: dict): + """Reload openai_api.openai with the given environment variables.""" + env = {k: v for k, v in os.environ.items()} + for key in ("OPENAI_API_BASE", "OPENAI_API_KEY", "MINIMAX_API_KEY", "LLM_PROVIDER"): + env.pop(key, None) + env.update(env_overrides) + + src_path = os.path.join(os.path.dirname(__file__), "..", "src") + src_path = os.path.abspath(src_path) + if src_path not in sys.path: + sys.path.insert(0, src_path) + + # Stub heavy optional deps so import doesn't fail in CI + for stub_name in ("numpy", "openai"): + if stub_name not in sys.modules: + sys.modules[stub_name] = MagicMock() + + # Remove cached module so _init_llm_config() re-runs + for mod_name in list(sys.modules.keys()): + if "openai_api" in mod_name: + del sys.modules[mod_name] + + with patch.dict("os.environ", env, clear=True): + import openai_api.openai as oai # noqa: F401 โ€“ loaded for side-effects + # snapshot env *inside* the patched context + captured = { + "OPENAI_API_BASE": os.environ.get("OPENAI_API_BASE", ""), + "OPENAI_API_KEY": os.environ.get("OPENAI_API_KEY", ""), + } + return oai, captured + + +class TestClampTemperature(unittest.TestCase): + """_clamp_temperature enforces MiniMax range (0.01, 1.0].""" + + def _get_clamp(self): + src_path = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "src")) + if src_path not in sys.path: + sys.path.insert(0, src_path) + for stub in ("numpy", "openai"): + if stub not in sys.modules: + sys.modules[stub] = MagicMock() + with patch.dict("os.environ", {"OPENAI_API_KEY": "sk-test"}, clear=False): + import openai_api.openai as oai + return oai._clamp_temperature + + def test_zero_clamped_to_minimum(self): + clamp = self._get_clamp() + self.assertAlmostEqual(clamp(0.0), 0.01) + + def test_negative_clamped_to_minimum(self): + clamp = self._get_clamp() + self.assertAlmostEqual(clamp(-0.5), 0.01) + + def test_value_above_one_clamped_to_one(self): + clamp = self._get_clamp() + self.assertAlmostEqual(clamp(1.5), 1.0) + + def test_valid_value_unchanged(self): + clamp = self._get_clamp() + self.assertAlmostEqual(clamp(0.7), 0.7) + + def test_boundary_one_unchanged(self): + clamp = self._get_clamp() + self.assertAlmostEqual(clamp(1.0), 1.0) + + def test_low_positive_value_unchanged(self): + clamp = self._get_clamp() + self.assertAlmostEqual(clamp(0.01), 0.01) + + +class TestInitLlmConfigMiniMaxAutoDetect(unittest.TestCase): + """_init_llm_config auto-configures MiniMax when MINIMAX_API_KEY is set alone.""" + + def test_minimax_key_only_sets_base_and_key(self): + """MINIMAX_API_KEY alone (no OPENAI_API_KEY) โ†’ OPENAI_API_BASE = api.minimax.io.""" + _, captured = _reload_openai_module({"MINIMAX_API_KEY": "mm-test-key"}) + self.assertEqual(captured["OPENAI_API_BASE"], "api.minimax.io") + self.assertEqual(captured["OPENAI_API_KEY"], "mm-test-key") + + def test_both_keys_openai_takes_priority(self): + """When both OPENAI_API_KEY and MINIMAX_API_KEY are set, OpenAI key is preserved.""" + _, captured = _reload_openai_module( + {"OPENAI_API_KEY": "sk-openai", "MINIMAX_API_KEY": "mm-key"} + ) + # OPENAI_API_KEY present โ†’ MiniMax auto-detect should NOT overwrite it + self.assertEqual(captured["OPENAI_API_KEY"], "sk-openai") + + def test_no_minimax_key_no_change(self): + """Without MINIMAX_API_KEY or LLM_PROVIDER, env is untouched.""" + _, captured = _reload_openai_module({"OPENAI_API_KEY": "sk-openai"}) + self.assertEqual(captured["OPENAI_API_KEY"], "sk-openai") + + def test_minimax_key_does_not_override_explicit_base(self): + """Explicit OPENAI_API_BASE is preserved even when MINIMAX_API_KEY is set alone.""" + _, captured = _reload_openai_module( + {"MINIMAX_API_KEY": "mm-test-key", "OPENAI_API_BASE": "my.proxy.com"} + ) + # setdefault โ†’ must not overwrite an already-set base + self.assertEqual(captured["OPENAI_API_BASE"], "my.proxy.com") + + +class TestInitLlmConfigLlmProvider(unittest.TestCase): + """_init_llm_config respects explicit LLM_PROVIDER=minimax.""" + + def test_llm_provider_minimax_routes_to_minimax(self): + _, captured = _reload_openai_module( + {"LLM_PROVIDER": "minimax", "MINIMAX_API_KEY": "mm-explicit-key"} + ) + self.assertEqual(captured["OPENAI_API_BASE"], "api.minimax.io") + self.assertEqual(captured["OPENAI_API_KEY"], "mm-explicit-key") + + def test_llm_provider_case_insensitive(self): + _, captured = _reload_openai_module( + {"LLM_PROVIDER": "MiniMax", "MINIMAX_API_KEY": "mm-key"} + ) + self.assertEqual(captured["OPENAI_API_BASE"], "api.minimax.io") + + def test_llm_provider_openai_is_no_op(self): + _, captured = _reload_openai_module( + {"LLM_PROVIDER": "openai", "OPENAI_API_KEY": "sk-openai"} + ) + self.assertEqual(captured["OPENAI_API_KEY"], "sk-openai") + self.assertNotEqual(captured["OPENAI_API_BASE"], "api.minimax.io") + + +class TestGetModelConfig(unittest.TestCase): + """get_model() returns configured model or falls back to gpt-4o-mini.""" + + def _get_fn(self): + src_path = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "src")) + if src_path not in sys.path: + sys.path.insert(0, src_path) + for stub in ("numpy", "openai"): + if stub not in sys.modules: + sys.modules[stub] = MagicMock() + # Clean module cache + for mod in list(sys.modules.keys()): + if "openai_api" in mod: + del sys.modules[mod] + with patch.dict("os.environ", {"OPENAI_API_KEY": "sk-test"}, clear=False): + import openai_api.openai as oai + return oai.get_model + + def test_known_key_returns_model(self): + get_model = self._get_fn() + model = get_model("openai_general") + self.assertIsInstance(model, str) + self.assertTrue(len(model) > 0) + + def test_unknown_key_returns_fallback(self): + get_model = self._get_fn() + model = get_model("nonexistent_key") + self.assertEqual(model, "gpt-4o-mini") + + +class TestMiniMaxEndpointUrl(unittest.TestCase): + """The MiniMax base URL resolves to a valid OpenAI-compatible endpoint.""" + + def test_base_url_constant(self): + src_path = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "src")) + if src_path not in sys.path: + sys.path.insert(0, src_path) + for stub in ("numpy", "openai"): + if stub not in sys.modules: + sys.modules[stub] = MagicMock() + for mod in list(sys.modules.keys()): + if "openai_api" in mod: + del sys.modules[mod] + with patch.dict("os.environ", {"OPENAI_API_KEY": "sk-test"}, clear=False): + import openai_api.openai as oai + self.assertEqual(oai._MINIMAX_API_BASE, "api.minimax.io") + + def test_constructed_url_is_openai_compat(self): + """https://{_MINIMAX_API_BASE}/v1/chat/completions matches expected pattern.""" + import openai_api.openai as oai # noqa: F811 + url = f"https://{oai._MINIMAX_API_BASE}/v1/chat/completions" + self.assertIn("minimax.io", url) + self.assertIn("/v1/chat/completions", url) + + +class TestAskOpenaiCommonWithMiniMax(unittest.TestCase): + """ask_openai_common routes correctly when MiniMax is configured.""" + + def _call_with_mock(self, env: dict, mock_response: dict): + src_path = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "src")) + if src_path not in sys.path: + sys.path.insert(0, src_path) + for stub in ("numpy", "openai"): + if stub not in sys.modules: + sys.modules[stub] = MagicMock() + for mod in list(sys.modules.keys()): + if "openai_api" in mod: + del sys.modules[mod] + + mock_resp = MagicMock() + mock_resp.json.return_value = mock_response + + with patch.dict("os.environ", env, clear=True), \ + patch("requests.post", return_value=mock_resp) as mock_post: + import openai_api.openai as oai + result = oai.ask_openai_common("test prompt") + + return result, mock_post + + def test_minimax_provider_sends_to_minimax_url(self): + env = {"MINIMAX_API_KEY": "mm-key", "LLM_PROVIDER": "minimax"} + mock_resp = { + "choices": [{"message": {"content": "audit result"}}] + } + result, mock_post = self._call_with_mock(env, mock_resp) + call_url = mock_post.call_args[0][0] + self.assertIn("minimax.io", call_url) + self.assertEqual(result, "audit result") + + def test_minimax_auth_header_set(self): + env = {"MINIMAX_API_KEY": "mm-abc123", "LLM_PROVIDER": "minimax"} + mock_resp = {"choices": [{"message": {"content": "ok"}}]} + _, mock_post = self._call_with_mock(env, mock_resp) + headers = mock_post.call_args[1]["headers"] + self.assertEqual(headers["Authorization"], "Bearer mm-abc123") + + def test_empty_choices_returns_empty_string(self): + env = {"MINIMAX_API_KEY": "mm-key", "LLM_PROVIDER": "minimax"} + _, mock_post = self._call_with_mock(env, {}) + # No choices key โ†’ should return '' + result, _ = self._call_with_mock(env, {}) + self.assertEqual(result, "") + + +if __name__ == "__main__": + unittest.main()