- * MiniMax提供OpenAI兼容的API接口,支持MiniMax-M2.7、MiniMax-M2.5等模型。
- * API地址:https://api.minimax.io/v1
+ * Supports MiniMax-M3 and MiniMax-M2.7 through the OpenAI-compatible and
+ * Anthropic-compatible APIs in both global and China regions.
*
* @author octopus
* @date 2026/3/21
@@ -25,14 +32,57 @@ import java.util.List;
@Slf4j
public class MinimaxServiceImpl implements AbstractChatService {
+ private static final String MINIMAX_M3 = "MiniMax-M3";
+ private static final String ANTHROPIC_PATH_SUFFIX = "/anthropic";
+ private static final Duration DEFAULT_TIMEOUT = Duration.ofSeconds(120);
+
@Override
public StreamingChatModel buildStreamingChatModel(ChatModelVo chatModelVo, ChatRequest chatRequest) {
- return OpenAiStreamingChatModel.builder()
- .baseUrl(chatModelVo.getApiHost())
+ String baseUrl = normalizeBaseUrl(chatModelVo.getApiHost());
+ boolean thinkingEnabled = Boolean.TRUE.equals(chatRequest.getEnableThinking());
+ String thinkingType = thinkingType(chatModelVo.getModelName(), thinkingEnabled);
+
+ if (isAnthropicBaseUrl(baseUrl)) {
+ return AnthropicStreamingChatModel.builder()
+ .baseUrl(toAnthropicClientBaseUrl(baseUrl))
+ .apiKey(chatModelVo.getApiKey())
+ .modelName(chatModelVo.getModelName())
+ .timeout(DEFAULT_TIMEOUT)
+ .listeners(List.of(new MyChatModelListener()))
+ .thinkingType(thinkingType)
+ .returnThinking(thinkingEnabled)
+ .build();
+ }
+
+ OpenAiStreamingChatModel.OpenAiStreamingChatModelBuilder builder = OpenAiStreamingChatModel.builder()
+ .baseUrl(baseUrl)
.apiKey(chatModelVo.getApiKey())
.modelName(chatModelVo.getModelName())
+ .timeout(DEFAULT_TIMEOUT)
.listeners(List.of(new MyChatModelListener()))
- .returnThinking(chatRequest.getEnableThinking())
+ .returnThinking(thinkingEnabled);
+ if (thinkingType != null) {
+ builder.customParameters(Map.of("thinking", Map.of("type", thinkingType)));
+ }
+ return builder.build();
+ }
+
+ @Override
+ public ChatModel buildChatModel(ChatModelVo chatModelVo) {
+ String baseUrl = normalizeBaseUrl(chatModelVo.getApiHost());
+ if (isAnthropicBaseUrl(baseUrl)) {
+ return AnthropicChatModel.builder()
+ .baseUrl(toAnthropicClientBaseUrl(baseUrl))
+ .apiKey(chatModelVo.getApiKey())
+ .modelName(chatModelVo.getModelName())
+ .timeout(DEFAULT_TIMEOUT)
+ .build();
+ }
+ return OpenAiChatModel.builder()
+ .baseUrl(baseUrl)
+ .apiKey(chatModelVo.getApiKey())
+ .modelName(chatModelVo.getModelName())
+ .timeout(DEFAULT_TIMEOUT)
.build();
}
@@ -41,4 +91,40 @@ public class MinimaxServiceImpl implements AbstractChatService {
return ChatModeType.MINIMAX.getCode();
}
+ static String thinkingType(String modelName, boolean thinkingEnabled) {
+ if (!MINIMAX_M3.equals(modelName)) {
+ return null;
+ }
+ return thinkingEnabled ? "adaptive" : "disabled";
+ }
+
+ private static String normalizeBaseUrl(String baseUrl) {
+ if (baseUrl == null || baseUrl.isBlank()) {
+ throw new IllegalArgumentException("MiniMax API Host must not be blank");
+ }
+ String normalized = baseUrl.trim();
+ while (normalized.endsWith("/")) {
+ normalized = normalized.substring(0, normalized.length() - 1);
+ }
+ URI uri;
+ try {
+ uri = URI.create(normalized);
+ } catch (IllegalArgumentException exception) {
+ throw new IllegalArgumentException("MiniMax API Host must be a valid URL", exception);
+ }
+ if (uri.getScheme() == null || uri.getHost() == null) {
+ throw new IllegalArgumentException("MiniMax API Host must be an absolute URL");
+ }
+ return uri.toString();
+ }
+
+ private static boolean isAnthropicBaseUrl(String baseUrl) {
+ return URI.create(baseUrl).getPath().endsWith(ANTHROPIC_PATH_SUFFIX);
+ }
+
+ private static String toAnthropicClientBaseUrl(String baseUrl) {
+ // LangChain4j appends /messages, while MiniMax exposes /anthropic/v1/messages.
+ return baseUrl + "/v1";
+ }
+
}
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
index 7e052e6d..2e866d8e 100644
--- 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
@@ -2,8 +2,9 @@ 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.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.CsvSource;
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;
@@ -26,45 +27,28 @@ class MinimaxIntegrationTest {
apiKey = System.getenv("MINIMAX_API_KEY");
}
- @Test
- void buildStreamingChatModel_withRealApiKey_M27() {
+ @ParameterizedTest
+ @CsvSource({
+ "https://api.minimax.io/v1, MiniMax-M3, false",
+ "https://api.minimax.io/v1, MiniMax-M2.7, true",
+ "https://api.minimaxi.com/v1, MiniMax-M3, true",
+ "https://api.minimaxi.com/v1, MiniMax-M2.7, true",
+ "https://api.minimax.io/anthropic, MiniMax-M3, false",
+ "https://api.minimax.io/anthropic, MiniMax-M2.7, true",
+ "https://api.minimaxi.com/anthropic, MiniMax-M3, true",
+ "https://api.minimaxi.com/anthropic, MiniMax-M2.7, true"
+ })
+ void buildStreamingChatModel_withConfiguredApiKey(
+ String apiHost, String modelName, boolean enableThinking) {
ChatModelVo modelVo = new ChatModelVo();
- modelVo.setApiHost("https://api.minimax.io/v1");
+ modelVo.setApiHost(apiHost);
modelVo.setApiKey(apiKey);
- modelVo.setModelName("MiniMax-M2.7");
+ modelVo.setModelName(modelName);
ChatRequest request = new ChatRequest();
- request.setEnableThinking(false);
+ request.setEnableThinking(enableThinking);
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");
+ assertNotNull(model, "Should create streaming model for " + modelName + " at " + apiHost);
}
}
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
index 697f52e2..ad621086 100644
--- 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
@@ -1,12 +1,28 @@
package org.ruoyi.service.chat.impl.provider;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.sun.net.httpserver.HttpServer;
+import dev.langchain4j.model.anthropic.AnthropicChatModel;
+import dev.langchain4j.model.anthropic.AnthropicStreamingChatModel;
+import dev.langchain4j.model.chat.ChatModel;
import dev.langchain4j.model.chat.StreamingChatModel;
+import dev.langchain4j.model.openai.OpenAiChatModel;
+import dev.langchain4j.model.openai.OpenAiChatRequestParameters;
+import dev.langchain4j.model.openai.OpenAiStreamingChatModel;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.CsvSource;
import org.ruoyi.common.chat.domain.dto.request.ChatRequest;
import org.ruoyi.common.chat.domain.vo.chat.ChatModelVo;
import org.ruoyi.enums.ChatModeType;
+import java.net.InetSocketAddress;
+import java.nio.charset.StandardCharsets;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicReference;
+
import static org.junit.jupiter.api.Assertions.*;
/**
@@ -14,6 +30,8 @@ import static org.junit.jupiter.api.Assertions.*;
*/
class MinimaxServiceImplTest {
+ private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
+
private MinimaxServiceImpl minimaxService;
@BeforeEach
@@ -27,50 +45,126 @@ class MinimaxServiceImplTest {
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");
-
+ @ParameterizedTest
+ @CsvSource({
+ "https://api.minimax.io/v1, MiniMax-M3, false, openai",
+ "https://api.minimax.io/v1, MiniMax-M2.7, true, openai",
+ "https://api.minimaxi.com/v1, MiniMax-M3, true, openai",
+ "https://api.minimaxi.com/v1, MiniMax-M2.7, true, openai",
+ "https://api.minimax.io/anthropic, MiniMax-M3, false, anthropic",
+ "https://api.minimax.io/anthropic, MiniMax-M2.7, true, anthropic",
+ "https://api.minimaxi.com/anthropic, MiniMax-M3, true, anthropic",
+ "https://api.minimaxi.com/anthropic, MiniMax-M2.7, true, anthropic"
+ })
+ void buildStreamingChatModel_supportsCurrentModelsRegionsAndProtocols(
+ String apiHost, String modelName, boolean enableThinking, String protocol) {
ChatRequest request = new ChatRequest();
- request.setEnableThinking(false);
+ request.setEnableThinking(enableThinking);
- StreamingChatModel model = minimaxService.buildStreamingChatModel(modelVo, request);
- assertNotNull(model);
+ StreamingChatModel model = minimaxService.buildStreamingChatModel(
+ modelVo(apiHost, modelName), request);
+
+ if ("anthropic".equals(protocol)) {
+ assertInstanceOf(AnthropicStreamingChatModel.class, model);
+ } else {
+ assertInstanceOf(OpenAiStreamingChatModel.class, model);
+ }
+ }
+
+ @ParameterizedTest
+ @CsvSource({
+ "https://api.minimax.io/v1, openai",
+ "https://api.minimaxi.com/v1, openai",
+ "https://api.minimax.io/anthropic, anthropic",
+ "https://api.minimaxi.com/anthropic, anthropic"
+ })
+ void buildChatModel_supportsRegionalProtocolEndpoints(String apiHost, String protocol) {
+ ChatModel model = minimaxService.buildChatModel(modelVo(apiHost, "MiniMax-M3"));
+
+ if ("anthropic".equals(protocol)) {
+ assertInstanceOf(AnthropicChatModel.class, model);
+ } else {
+ assertInstanceOf(OpenAiChatModel.class, model);
+ }
+ }
+
+ @ParameterizedTest
+ @CsvSource({"true, adaptive", "false, disabled"})
+ void buildStreamingChatModel_mapsM3ThinkingSetting(boolean enabled, String expectedType) {
+ ChatRequest request = new ChatRequest();
+ request.setEnableThinking(enabled);
+
+ OpenAiStreamingChatModel model = assertInstanceOf(OpenAiStreamingChatModel.class,
+ minimaxService.buildStreamingChatModel(
+ modelVo("https://api.minimax.io/v1", "MiniMax-M3"), request));
+ OpenAiChatRequestParameters parameters = assertInstanceOf(
+ OpenAiChatRequestParameters.class, model.defaultRequestParameters());
+ Map, ?> thinking = assertInstanceOf(
+ Map.class, parameters.customParameters().get("thinking"));
+
+ assertEquals(expectedType, thinking.get("type"));
+ assertEquals(expectedType, MinimaxServiceImpl.thinkingType("MiniMax-M3", enabled));
}
@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");
-
+ void buildStreamingChatModel_leavesM27ThinkingAlwaysOn() {
ChatRequest request = new ChatRequest();
request.setEnableThinking(false);
- StreamingChatModel model = minimaxService.buildStreamingChatModel(modelVo, request);
- assertNotNull(model);
+ OpenAiStreamingChatModel model = assertInstanceOf(OpenAiStreamingChatModel.class,
+ minimaxService.buildStreamingChatModel(
+ modelVo("https://api.minimax.io/v1", "MiniMax-M2.7"), request));
+ OpenAiChatRequestParameters parameters = assertInstanceOf(
+ OpenAiChatRequestParameters.class, model.defaultRequestParameters());
+
+ assertNull(MinimaxServiceImpl.thinkingType("MiniMax-M2.7", false));
+ assertTrue(parameters.customParameters() == null || parameters.customParameters().isEmpty());
+ }
+
+ @Test
+ void buildChatModel_appendsAnthropicMessagesPathInternally() throws Exception {
+ AtomicReference