Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 47 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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

Expand Down
41 changes: 40 additions & 1 deletion env.example
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
51 changes: 51 additions & 0 deletions src/openai_api/openai.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
126 changes: 126 additions & 0 deletions tests/integration/test_minimax_integration.py
Original file line number Diff line number Diff line change
@@ -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()
Loading