From 5d14eb20af1e55e0549575c18dba3643a3f7b4a6 Mon Sep 17 00:00:00 2001 From: octopus Date: Sat, 21 Mar 2026 16:14:19 +0800 Subject: [PATCH] feat: add MiniMax as first-class LLM provider Add MiniMax AI as the 7th LLM provider, supporting chat (M2.7, M2.5, M2.5-highspeed) and embedding (embo-01) models via OpenAI-compatible API. Changes: - Add MINIMAX enum to ChatModeType - Add MinimaxServiceImpl chat provider (OpenAI-compat streaming) - Add MinimaxEmbeddingProvider for vector embeddings - Add SQL migration for provider and model registration - Add 14 unit tests + 3 integration tests - Update README/README_EN with MiniMax in provider list --- README.md | 2 +- README_EN.md | 2 +- docs/script/sql/minimax_provider.sql | 23 ++++++ ruoyi-modules/ruoyi-chat/pom.xml | 17 +++++ .../java/org/ruoyi/enums/ChatModeType.java | 3 +- .../impl/provider/MinimaxServiceImpl.java | 40 ++++++++++ .../embed/impl/MinimaxEmbeddingProvider.java | 17 +++++ .../org/ruoyi/enums/ChatModeTypeTest.java | 42 ++++++++++ .../integration/MinimaxIntegrationTest.java | 70 +++++++++++++++++ .../impl/provider/MinimaxServiceImplTest.java | 76 +++++++++++++++++++ .../impl/MinimaxEmbeddingProviderTest.java | 55 ++++++++++++++ 11 files changed, 344 insertions(+), 3 deletions(-) create mode 100644 docs/script/sql/minimax_provider.sql create mode 100644 ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/chat/impl/provider/MinimaxServiceImpl.java create mode 100644 ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/embed/impl/MinimaxEmbeddingProvider.java create mode 100644 ruoyi-modules/ruoyi-chat/src/test/java/org/ruoyi/enums/ChatModeTypeTest.java create mode 100644 ruoyi-modules/ruoyi-chat/src/test/java/org/ruoyi/integration/MinimaxIntegrationTest.java create mode 100644 ruoyi-modules/ruoyi-chat/src/test/java/org/ruoyi/service/chat/impl/provider/MinimaxServiceImplTest.java create mode 100644 ruoyi-modules/ruoyi-chat/src/test/java/org/ruoyi/service/embed/impl/MinimaxEmbeddingProviderTest.java diff --git a/README.md b/README.md index e1d8fa6a..4900b012 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ | 模块 | 现有能力 |:----------:|--- -| **模型管理** | 多模型接入(OpenAI/DeepSeek/通义/智谱)、多模态理解、Coze/DIFY/FastGPT平台集成 +| **模型管理** | 多模型接入(OpenAI/DeepSeek/通义/智谱/MiniMax)、多模态理解、Coze/DIFY/FastGPT平台集成 | **知识管理** | 本地RAG + 向量库(Milvus/Weaviate) + 文档解析 | **工具管理** | Mcp协议集成、Skills能力 + 可扩展工具生态 | **流程编排** | 可视化工作流设计器、节点拖拽编排、SSE流式执行,目前已经支持模型调用,邮件发送,人工审核等节点 diff --git a/README_EN.md b/README_EN.md index edd13832..d2be455d 100644 --- a/README_EN.md +++ b/README_EN.md @@ -34,7 +34,7 @@ | Module | Current Capabilities | |:---:|---| -| **Model Management** | Multi-model integration (OpenAI/DeepSeek/Tongyi/Zhipu), multi-modal understanding, Coze/DIFY/FastGPT platform integration | +| **Model Management** | Multi-model integration (OpenAI/DeepSeek/Tongyi/Zhipu/MiniMax), multi-modal understanding, Coze/DIFY/FastGPT platform integration | | **Knowledge Base** | Local RAG + Vector DB (Milvus/Weaviate) + Document parsing | | **Tool Management** | MCP protocol integration, Skills capability + Extensible tool ecosystem | | **Workflow Orchestration** | Visual workflow designer, drag-and-drop node orchestration, SSE streaming execution, currently supports model calls, email sending, manual review nodes | diff --git a/docs/script/sql/minimax_provider.sql b/docs/script/sql/minimax_provider.sql new file mode 100644 index 00000000..e7331aa2 --- /dev/null +++ b/docs/script/sql/minimax_provider.sql @@ -0,0 +1,23 @@ +-- ---------------------------- +-- Add MiniMax provider +-- ---------------------------- +INSERT INTO `chat_provider` (`id`, `provider_name`, `provider_code`, `provider_icon`, `provider_desc`, `api_host`, `status`, `sort_order`, `create_dept`, `create_time`, `create_by`, `update_by`, `update_time`, `remark`, `version`, `del_flag`, `update_ip`, `tenant_id`) +VALUES (2010000000000000001, 'MiniMax', 'minimax', NULL, 'MiniMax大模型服务,支持M2.7、M2.5等模型', 'https://api.minimax.io/v1', '0', 6, NULL, NOW(), '1', '1', NOW(), 'MiniMax厂商', NULL, '0', NULL, 0); + +-- ---------------------------- +-- Add MiniMax chat models +-- ---------------------------- +INSERT INTO `chat_model` (`id`, `category`, `model_name`, `provider_code`, `model_describe`, `model_dimension`, `model_show`, `api_host`, `api_key`, `create_dept`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`, `tenant_id`) +VALUES (2010000000000000002, 'chat', 'MiniMax-M2.7', 'minimax', 'MiniMax-M2.7', NULL, 'Y', 'https://api.minimax.io/v1', '', NULL, 1, NOW(), 1, NOW(), 'MiniMax最新旗舰模型M2.7,支持1M上下文窗口', 0); + +INSERT INTO `chat_model` (`id`, `category`, `model_name`, `provider_code`, `model_describe`, `model_dimension`, `model_show`, `api_host`, `api_key`, `create_dept`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`, `tenant_id`) +VALUES (2010000000000000003, 'chat', 'MiniMax-M2.5', 'minimax', 'MiniMax-M2.5', NULL, 'Y', 'https://api.minimax.io/v1', '', NULL, 1, NOW(), 1, NOW(), 'MiniMax M2.5模型,204K上下文窗口', 0); + +INSERT INTO `chat_model` (`id`, `category`, `model_name`, `provider_code`, `model_describe`, `model_dimension`, `model_show`, `api_host`, `api_key`, `create_dept`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`, `tenant_id`) +VALUES (2010000000000000004, 'chat', 'MiniMax-M2.5-highspeed', 'minimax', 'MiniMax-M2.5-highspeed', NULL, 'Y', 'https://api.minimax.io/v1', '', NULL, 1, NOW(), 1, NOW(), 'MiniMax M2.5高速版,204K上下文窗口,更低延迟', 0); + +-- ---------------------------- +-- Add MiniMax embedding model +-- ---------------------------- +INSERT INTO `chat_model` (`id`, `category`, `model_name`, `provider_code`, `model_describe`, `model_dimension`, `model_show`, `api_host`, `api_key`, `create_dept`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`, `tenant_id`) +VALUES (2010000000000000005, 'vector', 'embo-01', 'minimax', 'embo-01', 1536, 'N', 'https://api.minimax.io/v1', '', NULL, 1, NOW(), 1, NOW(), 'MiniMax embo-01嵌入模型,1536维度', 0); diff --git a/ruoyi-modules/ruoyi-chat/pom.xml b/ruoyi-modules/ruoyi-chat/pom.xml index ba2f10c0..c44af49b 100644 --- a/ruoyi-modules/ruoyi-chat/pom.xml +++ b/ruoyi-modules/ruoyi-chat/pom.xml @@ -146,6 +146,23 @@ mysql-connector-j + + + org.junit.jupiter + junit-jupiter + test + + + org.mockito + mockito-core + test + + + org.mockito + mockito-junit-jupiter + test + + diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/enums/ChatModeType.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/enums/ChatModeType.java index c07d9444..9b70b91c 100644 --- a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/enums/ChatModeType.java +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/enums/ChatModeType.java @@ -15,7 +15,8 @@ public enum ChatModeType { DEEP_SEEK("deepseek", "深度求索"), QIAN_WEN("qianwen", "通义千问"), OPEN_AI("openai", "openai"), - PPIO("ppio", "ppio"); + PPIO("ppio", "ppio"), + MINIMAX("minimax", "MiniMax"); private final String code; private final String description; diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/chat/impl/provider/MinimaxServiceImpl.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/chat/impl/provider/MinimaxServiceImpl.java new file mode 100644 index 00000000..81df24c5 --- /dev/null +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/chat/impl/provider/MinimaxServiceImpl.java @@ -0,0 +1,40 @@ +package org.ruoyi.service.chat.impl.provider; + +import dev.langchain4j.model.chat.StreamingChatModel; +import dev.langchain4j.model.openai.OpenAiStreamingChatModel; +import lombok.extern.slf4j.Slf4j; +import org.ruoyi.common.chat.domain.dto.request.ChatRequest; +import org.ruoyi.common.chat.domain.vo.chat.ChatModelVo; +import org.ruoyi.enums.ChatModeType; +import org.ruoyi.service.chat.AbstractChatService; +import org.springframework.stereotype.Service; + +/** + * MiniMax服务调用 + *

+ * MiniMax提供OpenAI兼容的API接口,支持MiniMax-M2.7、MiniMax-M2.5等模型。 + * API地址:https://api.minimax.io/v1 + * + * @author octopus + * @date 2026/3/21 + */ +@Service +@Slf4j +public class MinimaxServiceImpl implements AbstractChatService { + + @Override + public StreamingChatModel buildStreamingChatModel(ChatModelVo chatModelVo, ChatRequest chatRequest) { + return OpenAiStreamingChatModel.builder() + .baseUrl(chatModelVo.getApiHost()) + .apiKey(chatModelVo.getApiKey()) + .modelName(chatModelVo.getModelName()) + .returnThinking(chatRequest.getEnableThinking()) + .build(); + } + + @Override + public String getProviderName() { + return ChatModeType.MINIMAX.getCode(); + } + +} diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/embed/impl/MinimaxEmbeddingProvider.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/embed/impl/MinimaxEmbeddingProvider.java new file mode 100644 index 00000000..4d2dbec5 --- /dev/null +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/embed/impl/MinimaxEmbeddingProvider.java @@ -0,0 +1,17 @@ +package org.ruoyi.service.embed.impl; + +import org.springframework.stereotype.Component; + +/** + * MiniMax嵌入模型(兼容OpenAI接口) + *

+ * 支持embo-01模型,1536维度向量。 + * API地址:https://api.minimax.io/v1 + * + * @author octopus + * @date 2026/3/21 + */ +@Component("minimax") +public class MinimaxEmbeddingProvider extends OpenAiEmbeddingProvider { + +} diff --git a/ruoyi-modules/ruoyi-chat/src/test/java/org/ruoyi/enums/ChatModeTypeTest.java b/ruoyi-modules/ruoyi-chat/src/test/java/org/ruoyi/enums/ChatModeTypeTest.java new file mode 100644 index 00000000..9733af8b --- /dev/null +++ b/ruoyi-modules/ruoyi-chat/src/test/java/org/ruoyi/enums/ChatModeTypeTest.java @@ -0,0 +1,42 @@ +package org.ruoyi.enums; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Unit tests for ChatModeType enum + */ +class ChatModeTypeTest { + + @Test + void minimaxEnumExists() { + ChatModeType minimax = ChatModeType.MINIMAX; + assertNotNull(minimax); + } + + @Test + void minimaxCode_isMinimax() { + assertEquals("minimax", ChatModeType.MINIMAX.getCode()); + } + + @Test + void minimaxDescription_isMiniMax() { + assertEquals("MiniMax", ChatModeType.MINIMAX.getDescription()); + } + + @Test + void allProviders_haveUniqueCode() { + ChatModeType[] values = ChatModeType.values(); + long uniqueCodes = java.util.Arrays.stream(values) + .map(ChatModeType::getCode) + .distinct() + .count(); + assertEquals(values.length, uniqueCodes, "All providers must have unique codes"); + } + + @Test + void valueOf_minimax() { + assertEquals(ChatModeType.MINIMAX, ChatModeType.valueOf("MINIMAX")); + } +} diff --git a/ruoyi-modules/ruoyi-chat/src/test/java/org/ruoyi/integration/MinimaxIntegrationTest.java b/ruoyi-modules/ruoyi-chat/src/test/java/org/ruoyi/integration/MinimaxIntegrationTest.java new file mode 100644 index 00000000..7e052e6d --- /dev/null +++ b/ruoyi-modules/ruoyi-chat/src/test/java/org/ruoyi/integration/MinimaxIntegrationTest.java @@ -0,0 +1,70 @@ +package org.ruoyi.integration; + +import dev.langchain4j.model.chat.StreamingChatModel; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable; +import org.ruoyi.common.chat.domain.dto.request.ChatRequest; +import org.ruoyi.common.chat.domain.vo.chat.ChatModelVo; +import org.ruoyi.service.chat.impl.provider.MinimaxServiceImpl; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Integration tests for MiniMax provider. + * These tests require a valid MINIMAX_API_KEY environment variable. + */ +@EnabledIfEnvironmentVariable(named = "MINIMAX_API_KEY", matches = ".+") +class MinimaxIntegrationTest { + + private MinimaxServiceImpl minimaxService; + private String apiKey; + + @BeforeEach + void setUp() { + minimaxService = new MinimaxServiceImpl(); + apiKey = System.getenv("MINIMAX_API_KEY"); + } + + @Test + void buildStreamingChatModel_withRealApiKey_M27() { + ChatModelVo modelVo = new ChatModelVo(); + modelVo.setApiHost("https://api.minimax.io/v1"); + modelVo.setApiKey(apiKey); + modelVo.setModelName("MiniMax-M2.7"); + + ChatRequest request = new ChatRequest(); + request.setEnableThinking(false); + + StreamingChatModel model = minimaxService.buildStreamingChatModel(modelVo, request); + assertNotNull(model, "Should create streaming model with real API key"); + } + + @Test + void buildStreamingChatModel_withRealApiKey_M25() { + ChatModelVo modelVo = new ChatModelVo(); + modelVo.setApiHost("https://api.minimax.io/v1"); + modelVo.setApiKey(apiKey); + modelVo.setModelName("MiniMax-M2.5"); + + ChatRequest request = new ChatRequest(); + request.setEnableThinking(false); + + StreamingChatModel model = minimaxService.buildStreamingChatModel(modelVo, request); + assertNotNull(model, "Should create streaming model with M2.5"); + } + + @Test + void buildStreamingChatModel_withRealApiKey_M25Highspeed() { + ChatModelVo modelVo = new ChatModelVo(); + modelVo.setApiHost("https://api.minimax.io/v1"); + modelVo.setApiKey(apiKey); + modelVo.setModelName("MiniMax-M2.5-highspeed"); + + ChatRequest request = new ChatRequest(); + request.setEnableThinking(false); + + StreamingChatModel model = minimaxService.buildStreamingChatModel(modelVo, request); + assertNotNull(model, "Should create streaming model with M2.5-highspeed"); + } +} diff --git a/ruoyi-modules/ruoyi-chat/src/test/java/org/ruoyi/service/chat/impl/provider/MinimaxServiceImplTest.java b/ruoyi-modules/ruoyi-chat/src/test/java/org/ruoyi/service/chat/impl/provider/MinimaxServiceImplTest.java new file mode 100644 index 00000000..697f52e2 --- /dev/null +++ b/ruoyi-modules/ruoyi-chat/src/test/java/org/ruoyi/service/chat/impl/provider/MinimaxServiceImplTest.java @@ -0,0 +1,76 @@ +package org.ruoyi.service.chat.impl.provider; + +import dev.langchain4j.model.chat.StreamingChatModel; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.ruoyi.common.chat.domain.dto.request.ChatRequest; +import org.ruoyi.common.chat.domain.vo.chat.ChatModelVo; +import org.ruoyi.enums.ChatModeType; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Unit tests for MinimaxServiceImpl + */ +class MinimaxServiceImplTest { + + private MinimaxServiceImpl minimaxService; + + @BeforeEach + void setUp() { + minimaxService = new MinimaxServiceImpl(); + } + + @Test + void getProviderName_returnsMinimaxCode() { + assertEquals("minimax", minimaxService.getProviderName()); + assertEquals(ChatModeType.MINIMAX.getCode(), minimaxService.getProviderName()); + } + + @Test + void buildStreamingChatModel_returnsNonNull() { + ChatModelVo modelVo = new ChatModelVo(); + modelVo.setApiHost("https://api.minimax.io/v1"); + modelVo.setApiKey("test-api-key"); + modelVo.setModelName("MiniMax-M2.7"); + + ChatRequest request = new ChatRequest(); + request.setEnableThinking(false); + + StreamingChatModel model = minimaxService.buildStreamingChatModel(modelVo, request); + assertNotNull(model); + } + + @Test + void buildStreamingChatModel_withThinkingEnabled() { + ChatModelVo modelVo = new ChatModelVo(); + modelVo.setApiHost("https://api.minimax.io/v1"); + modelVo.setApiKey("test-api-key"); + modelVo.setModelName("MiniMax-M2.5"); + + ChatRequest request = new ChatRequest(); + request.setEnableThinking(true); + + StreamingChatModel model = minimaxService.buildStreamingChatModel(modelVo, request); + assertNotNull(model); + } + + @Test + void buildStreamingChatModel_withHighspeedModel() { + ChatModelVo modelVo = new ChatModelVo(); + modelVo.setApiHost("https://api.minimax.io/v1"); + modelVo.setApiKey("test-api-key"); + modelVo.setModelName("MiniMax-M2.5-highspeed"); + + ChatRequest request = new ChatRequest(); + request.setEnableThinking(false); + + StreamingChatModel model = minimaxService.buildStreamingChatModel(modelVo, request); + assertNotNull(model); + } + + @Test + void implementsAbstractChatService() { + assertInstanceOf(org.ruoyi.service.chat.AbstractChatService.class, minimaxService); + } +} diff --git a/ruoyi-modules/ruoyi-chat/src/test/java/org/ruoyi/service/embed/impl/MinimaxEmbeddingProviderTest.java b/ruoyi-modules/ruoyi-chat/src/test/java/org/ruoyi/service/embed/impl/MinimaxEmbeddingProviderTest.java new file mode 100644 index 00000000..e089aae1 --- /dev/null +++ b/ruoyi-modules/ruoyi-chat/src/test/java/org/ruoyi/service/embed/impl/MinimaxEmbeddingProviderTest.java @@ -0,0 +1,55 @@ +package org.ruoyi.service.embed.impl; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.ruoyi.common.chat.domain.vo.chat.ChatModelVo; +import org.ruoyi.enums.ModalityType; +import org.ruoyi.service.embed.BaseEmbedModelService; + +import java.util.Set; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Unit tests for MinimaxEmbeddingProvider + */ +class MinimaxEmbeddingProviderTest { + + private MinimaxEmbeddingProvider provider; + + @BeforeEach + void setUp() { + provider = new MinimaxEmbeddingProvider(); + } + + @Test + void implementsBaseEmbedModelService() { + assertInstanceOf(BaseEmbedModelService.class, provider); + } + + @Test + void extendsOpenAiEmbeddingProvider() { + assertInstanceOf(OpenAiEmbeddingProvider.class, provider); + } + + @Test + void getSupportedModalities_returnsText() { + Set modalities = provider.getSupportedModalities(); + assertNotNull(modalities); + assertTrue(modalities.contains(ModalityType.TEXT)); + assertEquals(1, modalities.size()); + } + + @Test + void configure_setsModelConfig() { + ChatModelVo config = new ChatModelVo(); + config.setApiHost("https://api.minimax.io/v1"); + config.setApiKey("test-api-key"); + config.setModelName("embo-01"); + config.setModelDimension(1536); + + provider.configure(config); + // configure sets internal state; verify no exception thrown + assertNotNull(provider); + } +}