diff --git a/README.md b/README.md index 078478e34..9451714e1 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,23 @@ SQLBot 是一款基于大语言模型和 RAG 的智能问数系统,由 DataEas - **易于集成**:支持多种集成方式,提供 Web 嵌入、弹窗嵌入、MCP 调用等能力;能够快速嵌入到 n8n、Dify、MaxKB、DataEase 等应用,让各类应用快速拥有智能问数能力。 - **越问越准**:支持自定义提示词、术语库配置,可维护 SQL 示例校准逻辑,精准匹配业务场景;高效运营,基于用户交互数据持续迭代优化,问数效果随使用逐步提升,越问越准。 +## 支持的大模型服务商 + +| 服务商 | API 兼容 | +|--------|----------| +| 阿里云百炼 | OpenAI 兼容 | +| 千帆大模型 | OpenAI 兼容 | +| DeepSeek | OpenAI 兼容 | +| 腾讯混元 | OpenAI 兼容 | +| 讯飞星火 | OpenAI 兼容 | +| Gemini | OpenAI 兼容 | +| OpenAI | 原生 | +| Kimi | OpenAI 兼容 | +| 腾讯云 | OpenAI 兼容 | +| 火山引擎 | OpenAI 兼容 | +| MiniMax | OpenAI 兼容 | +| 通用 OpenAI 兼容 | 自定义 | + ## 快速开始 ### 安装部署 diff --git a/docs/README.en.md b/docs/README.en.md index 0c6ea87e8..9848d3799 100644 --- a/docs/README.en.md +++ b/docs/README.en.md @@ -30,6 +30,23 @@ SQLBot is an intelligent data query system based on large language models and RA - **Easy Integration:** Supports multiple integration methods, providing capabilities such as web embedding, pop-up embedding, and MCP invocation. It can be quickly embedded into applications such as n8n, Dify, MaxKB, and DataEase, allowing various applications to quickly acquire intelligent data collection capabilities. - **Increasingly Accurate with Use:** Supports customizable prompts and terminology library configurations, maintainable SQL example calibration logic, and accurate matching of business scenarios. Efficient operation, based on continuous iteration and optimization using user interaction data, the data collection effect gradually improves with use, becoming more accurate with each use. +## Supported LLM Providers + +| Provider | API Compatibility | +|----------|-------------------| +| Alibaba Cloud Bailian | OpenAI Compatible | +| Qianfan Model | OpenAI Compatible | +| DeepSeek | OpenAI Compatible | +| Tencent Hunyuan | OpenAI Compatible | +| iFlytek Spark | OpenAI Compatible | +| Gemini | OpenAI Compatible | +| OpenAI | Native | +| Kimi | OpenAI Compatible | +| Tencent Cloud | OpenAI Compatible | +| Volcano Engine | OpenAI Compatible | +| MiniMax | OpenAI Compatible | +| Generic OpenAI Compatible | Custom | + ## Quick Start ### Installation and Deployment diff --git a/frontend/src/assets/model/icon_minimax_colorful.png b/frontend/src/assets/model/icon_minimax_colorful.png new file mode 100644 index 000000000..f9dd9aaeb Binary files /dev/null and b/frontend/src/assets/model/icon_minimax_colorful.png differ diff --git a/frontend/src/entity/supplier.ts b/frontend/src/entity/supplier.ts index 733cde9f4..1e5a459d3 100644 --- a/frontend/src/entity/supplier.ts +++ b/frontend/src/entity/supplier.ts @@ -10,6 +10,7 @@ import icon_txhy_colorful from '@/assets/model/icon_txhy_colorful.png' import icon_hsyq_colorful from '@/assets/model/icon_hsyq_colorful.png' // import icon_vllm_colorful from '@/assets/model/icon_vllm_colorful.png' import icon_common_openai from '@/assets/model/icon_common_openai.png' +import icon_minimax_colorful from '@/assets/model/icon_minimax_colorful.png' // import icon_azure_openAI_colorful from '@/assets/model/icon_Azure_OpenAI_colorful.png' type ModelArg = { key: string; val?: string | number; type: string; range?: string } @@ -276,6 +277,23 @@ export const supplierList: Array<{ }, }, }, + { + id: 13, + name: 'MiniMax', + i18nKey: 'supplier.minimax', + icon: icon_minimax_colorful, + model_config: { + 0: { + api_domain: 'https://api.minimax.io/v1', + common_args: [{ key: 'temperature', val: 0.7, type: 'number', range: '[0, 1]' }], + model_options: [ + { name: 'MiniMax-M2.7' }, + { name: 'MiniMax-M2.5' }, + { name: 'MiniMax-M2.5-highspeed' }, + ], + }, + }, + }, /* { id: 11, name: 'vLLM', diff --git a/frontend/src/i18n/en.json b/frontend/src/i18n/en.json index c9db6e6ca..9ec210985 100644 --- a/frontend/src/i18n/en.json +++ b/frontend/src/i18n/en.json @@ -915,6 +915,7 @@ "kimi": "Kimi", "tencent_cloud": "Tencent Cloud", "volcano_engine": "Volcano Engine", + "minimax": "MiniMax", "generic_openai": "Generic OpenAI" }, "modelType": { diff --git a/frontend/src/i18n/ko-KR.json b/frontend/src/i18n/ko-KR.json index f1241465c..310406987 100644 --- a/frontend/src/i18n/ko-KR.json +++ b/frontend/src/i18n/ko-KR.json @@ -915,6 +915,7 @@ "kimi": "Kimi", "tencent_cloud": "텐센트 클라우드", "volcano_engine": "볼케이노 엔진", + "minimax": "MiniMax", "generic_openai": "범용 OpenAI" }, "modelType": { diff --git a/frontend/src/i18n/zh-CN.json b/frontend/src/i18n/zh-CN.json index e2e222ae4..24f70ab6d 100644 --- a/frontend/src/i18n/zh-CN.json +++ b/frontend/src/i18n/zh-CN.json @@ -915,6 +915,7 @@ "kimi": "Kimi", "tencent_cloud": "腾讯云", "volcano_engine": "火山引擎", + "minimax": "MiniMax", "generic_openai": "通用OpenAI" }, "modelType": { diff --git a/tests/test_minimax_integration.py b/tests/test_minimax_integration.py new file mode 100644 index 000000000..c7a953151 --- /dev/null +++ b/tests/test_minimax_integration.py @@ -0,0 +1,91 @@ +""" +Integration tests for MiniMax LLM provider in SQLBot. + +These tests validate that the MiniMax API is reachable and functioning +correctly via the OpenAI-compatible protocol. + +Requires MINIMAX_API_KEY environment variable to be set. +""" + +import json +import os +import unittest + +import requests + +MINIMAX_API_KEY = os.environ.get("MINIMAX_API_KEY", "") +MINIMAX_BASE_URL = "https://api.minimax.io/v1" + + +def skip_without_api_key(func): + """Skip test if MINIMAX_API_KEY is not set.""" + return unittest.skipUnless(MINIMAX_API_KEY, "MINIMAX_API_KEY not set")(func) + + +class TestMiniMaxAPIConnectivity(unittest.TestCase): + """Test MiniMax API endpoint reachability.""" + + @skip_without_api_key + def test_api_endpoint_reachable(self): + """MiniMax API endpoint should be reachable (chat completions).""" + resp = requests.post( + f"{MINIMAX_BASE_URL}/chat/completions", + headers={ + "Authorization": f"Bearer {MINIMAX_API_KEY}", + "Content-Type": "application/json", + }, + json={ + "model": "MiniMax-M2.5-highspeed", + "messages": [{"role": "user", "content": "Hi"}], + "max_tokens": 1, + }, + timeout=15, + ) + self.assertEqual(resp.status_code, 200) + + @skip_without_api_key + def test_chat_completions_basic(self): + """MiniMax chat completions should return a valid response.""" + resp = requests.post( + f"{MINIMAX_BASE_URL}/chat/completions", + headers={ + "Authorization": f"Bearer {MINIMAX_API_KEY}", + "Content-Type": "application/json", + }, + json={ + "model": "MiniMax-M2.5-highspeed", + "messages": [{"role": "user", "content": "Say hello in one word."}], + "temperature": 0.7, + "max_tokens": 10, + }, + timeout=30, + ) + self.assertEqual(resp.status_code, 200) + data = resp.json() + self.assertIn("choices", data) + self.assertGreater(len(data["choices"]), 0) + content = data["choices"][0]["message"]["content"] + self.assertTrue(len(content) > 0, "Response content should not be empty") + + @skip_without_api_key + def test_temperature_zero_accepted(self): + """MiniMax API should accept temperature=0.""" + resp = requests.post( + f"{MINIMAX_BASE_URL}/chat/completions", + headers={ + "Authorization": f"Bearer {MINIMAX_API_KEY}", + "Content-Type": "application/json", + }, + json={ + "model": "MiniMax-M2.5-highspeed", + "messages": [{"role": "user", "content": "Reply with OK."}], + "temperature": 0, + "max_tokens": 5, + }, + timeout=30, + ) + self.assertEqual(resp.status_code, 200) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_supplier_config.py b/tests/test_supplier_config.py new file mode 100644 index 000000000..af1fec1c4 --- /dev/null +++ b/tests/test_supplier_config.py @@ -0,0 +1,238 @@ +""" +Unit tests for the MiniMax supplier configuration in SQLBot. + +These tests validate that the MiniMax LLM provider is correctly configured +in the frontend supplier registry, i18n translation files, and backend +model factory. +""" + +import json +import os +import re +import unittest + +# Project root relative to this test file +PROJECT_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + + +class TestMiniMaxSupplierConfig(unittest.TestCase): + """Test the MiniMax supplier entry in supplier.ts.""" + + def setUp(self): + supplier_path = os.path.join( + PROJECT_ROOT, "frontend", "src", "entity", "supplier.ts" + ) + with open(supplier_path, "r", encoding="utf-8") as f: + self.supplier_content = f.read() + + def test_minimax_icon_import_exists(self): + """MiniMax icon import statement should be present.""" + self.assertIn( + "import icon_minimax_colorful from '@/assets/model/icon_minimax_colorful.png'", + self.supplier_content, + ) + + def test_minimax_supplier_entry_exists(self): + """MiniMax supplier entry with id=13 should exist in supplierList.""" + self.assertIn("id: 13", self.supplier_content) + self.assertIn("name: 'MiniMax'", self.supplier_content) + + def test_minimax_i18n_key(self): + """MiniMax should have the correct i18n key.""" + self.assertIn("i18nKey: 'supplier.minimax'", self.supplier_content) + + def test_minimax_icon_reference(self): + """MiniMax should reference the correct icon variable.""" + self.assertIn("icon: icon_minimax_colorful", self.supplier_content) + + def test_minimax_api_domain(self): + """MiniMax API domain should be https://api.minimax.io/v1.""" + self.assertIn( + "api_domain: 'https://api.minimax.io/v1'", self.supplier_content + ) + + def test_minimax_temperature_range(self): + """MiniMax temperature should be in range [0, 1].""" + # Find the MiniMax section and check temperature config + minimax_section = self.supplier_content[ + self.supplier_content.index("id: 13") : + ] + # Limit to just MiniMax section (up to next id:) + next_id = minimax_section.index("id: 11", 10) if "id: 11" in minimax_section[10:] else len(minimax_section) + minimax_section = minimax_section[:next_id] + self.assertIn("key: 'temperature'", minimax_section) + self.assertIn("val: 0.7", minimax_section) + self.assertIn("range: '[0, 1]'", minimax_section) + + def test_minimax_model_options(self): + """MiniMax should have M2.7, M2.5, and M2.5-highspeed models.""" + self.assertIn("name: 'MiniMax-M2.7'", self.supplier_content) + self.assertIn("name: 'MiniMax-M2.5'", self.supplier_content) + self.assertIn("name: 'MiniMax-M2.5-highspeed'", self.supplier_content) + + def test_minimax_has_model_config_type_0(self): + """MiniMax should have model_config with type 0 (LLM).""" + minimax_section = self.supplier_content[ + self.supplier_content.index("id: 13") : + ] + next_section = minimax_section.find("/* {", 10) + if next_section == -1: + next_section = minimax_section.find(" {", 10) + minimax_section = minimax_section[:next_section] if next_section > 0 else minimax_section[:500] + self.assertIn("model_config:", minimax_section) + self.assertIn("0:", minimax_section) + + def test_minimax_uses_openai_protocol(self): + """MiniMax should not set type='vllm' or type='azure' (defaults to openai).""" + minimax_section = self.supplier_content[ + self.supplier_content.index("id: 13") : + ] + # Get just the MiniMax entry (roughly 20 lines) + lines = minimax_section.split("\n")[:20] + minimax_text = "\n".join(lines) + self.assertNotIn("type: 'vllm'", minimax_text) + self.assertNotIn("type: 'azure'", minimax_text) + + def test_supplier_id_13_is_unique(self): + """Supplier id 13 should appear exactly once.""" + count = self.supplier_content.count("id: 13") + self.assertEqual(count, 1, "Supplier id 13 should be unique") + + +class TestMiniMaxI18nTranslations(unittest.TestCase): + """Test that MiniMax i18n translations are present in all locale files.""" + + def _load_locale(self, locale_name): + path = os.path.join( + PROJECT_ROOT, "frontend", "src", "i18n", f"{locale_name}.json" + ) + with open(path, "r", encoding="utf-8") as f: + return json.load(f) + + def test_en_translation(self): + """English translation for MiniMax should exist.""" + data = self._load_locale("en") + self.assertIn("minimax", data["supplier"]) + self.assertEqual(data["supplier"]["minimax"], "MiniMax") + + def test_zh_cn_translation(self): + """Chinese (Simplified) translation for MiniMax should exist.""" + data = self._load_locale("zh-CN") + self.assertIn("minimax", data["supplier"]) + self.assertEqual(data["supplier"]["minimax"], "MiniMax") + + def test_ko_kr_translation(self): + """Korean translation for MiniMax should exist.""" + data = self._load_locale("ko-KR") + self.assertIn("minimax", data["supplier"]) + self.assertEqual(data["supplier"]["minimax"], "MiniMax") + + def test_all_locales_have_same_supplier_keys(self): + """All locale files should have the same set of supplier keys.""" + en = self._load_locale("en") + zh = self._load_locale("zh-CN") + ko = self._load_locale("ko-KR") + en_keys = set(en["supplier"].keys()) + zh_keys = set(zh["supplier"].keys()) + ko_keys = set(ko["supplier"].keys()) + self.assertEqual(en_keys, zh_keys, "EN and ZH-CN should have same supplier keys") + self.assertEqual(en_keys, ko_keys, "EN and KO-KR should have same supplier keys") + + +class TestMiniMaxIconFile(unittest.TestCase): + """Test that the MiniMax icon file exists and is valid.""" + + def test_icon_file_exists(self): + """MiniMax icon PNG file should exist.""" + icon_path = os.path.join( + PROJECT_ROOT, + "frontend", + "src", + "assets", + "model", + "icon_minimax_colorful.png", + ) + self.assertTrue(os.path.exists(icon_path), "MiniMax icon file should exist") + + def test_icon_is_png(self): + """MiniMax icon should be a valid PNG file.""" + icon_path = os.path.join( + PROJECT_ROOT, + "frontend", + "src", + "assets", + "model", + "icon_minimax_colorful.png", + ) + with open(icon_path, "rb") as f: + header = f.read(8) + # PNG magic number + self.assertEqual( + header[:4], b"\x89PNG", "File should have PNG magic number" + ) + + def test_icon_not_empty(self): + """MiniMax icon file should not be empty.""" + icon_path = os.path.join( + PROJECT_ROOT, + "frontend", + "src", + "assets", + "model", + "icon_minimax_colorful.png", + ) + size = os.path.getsize(icon_path) + self.assertGreater(size, 100, "Icon file should be reasonably sized") + + +class TestModelFactoryConfig(unittest.TestCase): + """Test that backend model_factory.py can support MiniMax via OpenAI protocol.""" + + def setUp(self): + factory_path = os.path.join( + PROJECT_ROOT, "backend", "apps", "ai_model", "model_factory.py" + ) + with open(factory_path, "r", encoding="utf-8") as f: + self.factory_content = f.read() + + def test_openai_type_in_factory(self): + """OpenAI type should be registered in LLMFactory._llm_types.""" + self.assertIn('"openai"', self.factory_content) + + def test_factory_supports_register(self): + """LLMFactory should have register_llm method for extensibility.""" + self.assertIn("register_llm", self.factory_content) + + def test_factory_creates_openai_llm_for_openai_type(self): + """The 'openai' type should map to OpenAILLM class.""" + self.assertIn('"openai": OpenAILLM', self.factory_content) + + def test_llm_config_has_api_base_url(self): + """LLMConfig should support api_base_url for custom endpoints.""" + self.assertIn("api_base_url", self.factory_content) + + def test_openai_llm_passes_base_url(self): + """OpenAILLM should pass base_url to BaseChatOpenAI.""" + self.assertIn("base_url=self.config.api_base_url", self.factory_content) + + +class TestReadmeContent(unittest.TestCase): + """Test that README files mention MiniMax.""" + + def test_readme_zh_mentions_minimax(self): + """Chinese README should list MiniMax as a supported provider.""" + path = os.path.join(PROJECT_ROOT, "README.md") + with open(path, "r", encoding="utf-8") as f: + content = f.read() + self.assertIn("MiniMax", content) + + def test_readme_en_mentions_minimax(self): + """English README should list MiniMax as a supported provider.""" + path = os.path.join(PROJECT_ROOT, "docs", "README.en.md") + with open(path, "r", encoding="utf-8") as f: + content = f.read() + self.assertIn("MiniMax", content) + + +if __name__ == "__main__": + unittest.main()