From a3d82e092e3df005ba675c5d78c3eae881b1d6c0 Mon Sep 17 00:00:00 2001 From: Octopus Date: Wed, 15 Jul 2026 17:43:51 +0800 Subject: [PATCH] Refresh MiniMax models and protocol support (#313) Co-authored-by: octo-patch <266937838+octo-patch@users.noreply.github.com> --- README_EN.md | 28 ++++ docs/script/sql/ruoyi-ai-v3_mysql8.sql | 2 +- ruoyi-modules/ruoyi-chat/pom.xml | 6 + .../impl/provider/MinimaxServiceImpl.java | 98 ++++++++++- .../integration/MinimaxIntegrationTest.java | 54 +++--- .../impl/provider/MinimaxServiceImplTest.java | 158 ++++++++++++++---- 6 files changed, 272 insertions(+), 74 deletions(-) diff --git a/README_EN.md b/README_EN.md index 428f1a33..7f193a9d 100644 --- a/README_EN.md +++ b/README_EN.md @@ -176,6 +176,34 @@ docker-compose -f docker-compose-all.yaml logs -f [service-name] docker-compose -f docker-compose-all.yaml restart [service-name] ``` +### MiniMax Configuration + +The built-in MiniMax provider accepts one API Host value and selects the matching protocol adapter. Use a Base URL from this table: + +| Region | OpenAI-compatible Base URL | Anthropic-compatible Base URL | +| --- | --- | --- | +| Global | `https://api.minimax.io/v1` | `https://api.minimax.io/anthropic` | +| China | `https://api.minimaxi.com/v1` | `https://api.minimaxi.com/anthropic` | + +For Anthropic-compatible requests, configure the Base URL ending in `/anthropic`. Do not append `/v1` or `/v1/messages`; the provider adapter derives the request path internally. + +| Model ID | Total context | Input modalities | Thinking | +| --- | ---: | --- | --- | +| `MiniMax-M3` | 1,000,000 tokens | Text, image, video | Adaptive or disabled | +| `MiniMax-M2.7` | 204,800 tokens | Text | Always on | + +Current pay-as-you-go prices are in USD per million tokens: + +| Model | Service tier and input range | Input | Output | Cache read | Cache write | +| --- | --- | ---: | ---: | ---: | ---: | +| `MiniMax-M3` | Standard, up to 512,000 input tokens | $0.30 | $1.20 | $0.06 | Not listed | +| `MiniMax-M3` | Standard, over 512,000 input tokens | $0.60 | $2.40 | $0.12 | Not listed | +| `MiniMax-M3` | Priority, up to 512,000 input tokens | $0.45 | $1.80 | $0.09 | Not listed | +| `MiniMax-M3` | Priority, over 512,000 input tokens | $0.90 | $3.60 | $0.18 | Not listed | +| `MiniMax-M2.7` | Standard | $0.30 | $1.20 | $0.06 | $0.375 | + +See the [official API overview](https://platform.minimax.io/docs/api-reference/api-overview) and [pay-as-you-go pricing](https://platform.minimax.io/docs/guides/pricing-paygo) for current details. + ## 📚 Documentation Want to learn more about installation, deployment, configuration, and secondary development? diff --git a/docs/script/sql/ruoyi-ai-v3_mysql8.sql b/docs/script/sql/ruoyi-ai-v3_mysql8.sql index 3ddcc310..8a659ade 100644 --- a/docs/script/sql/ruoyi-ai-v3_mysql8.sql +++ b/docs/script/sql/ruoyi-ai-v3_mysql8.sql @@ -153,7 +153,7 @@ INSERT INTO `chat_provider` VALUES (12, '智谱AI', 'zhipu', 'https://example-12 INSERT INTO `chat_provider` VALUES (13, '小米MIMO', 'xiaomi', 'https://example-1234567890.cos.ap-guangzhou.myqcloud.com/2026/04/19/18dd39365ce244e3ae5e030da036760e.png', '小米官方API', 'https://api.xiaomimimo.com/v1', '0', 3, 103, '2026-04-19 12:48:24', '1', '1', '2026-04-19 13:14:22', '小米官方API', NULL, '0', NULL, 0); INSERT INTO `chat_provider` VALUES (14, '阿里云百炼', 'qianwen', 'https://example-1234567890.cos.ap-guangzhou.myqcloud.com/2026/02/25/de2aa7e649de44f3ba5c6380ac6acd04.png', '阿里云百炼大模型服务', 'https://dashscope.aliyuncs.com', '0', 2, 103, '2025-12-14 21:48:11', '1', '1', '2026-02-25 20:49:13', '阿里云厂商', NULL, '0', NULL, 0); INSERT INTO `chat_provider` VALUES (15, 'Atlas Cloud', 'atlas', 'https://example-1234567890.cos.ap-guangzhou.myqcloud.com/2026/04/19/atlascloud.png', '全模态AI推理平台', 'https://api.atlascloud.ai/v1', '0', 5, 103, '2025-12-15 23:13:42', '1', '1', '2026-02-25 20:49:01', 'Atlas Cloud AI平台', NULL, '0', NULL, 0); -INSERT INTO `chat_provider` VALUES (16, 'MiniMax', 'minimax', 'https://example-1234567890.cos.ap-guangzhou.myqcloud.com/2026/04/19/fdc712e90e0e4d78b05862ad230884e5.png', 'MiniMax大模型服务,支持M2.7、M2.5等模型', 'https://api.minimax.io/v1', '0', 6, 103, '2026-04-19 12:50:12', '1', '1', '2026-04-19 13:14:59', 'MiniMax厂商', NULL, '0', NULL, 0); +INSERT INTO `chat_provider` VALUES (16, 'MiniMax', 'minimax', 'https://example-1234567890.cos.ap-guangzhou.myqcloud.com/2026/04/19/fdc712e90e0e4d78b05862ad230884e5.png', 'MiniMax model service supporting MiniMax-M3 and MiniMax-M2.7', 'https://api.minimax.io/v1', '0', 6, 103, '2026-04-19 12:50:12', '1', '1', '2026-04-19 13:14:59', 'MiniMax provider', NULL, '0', NULL, 0); INSERT INTO `chat_provider` VALUES (17, 'ollama', 'ollama', 'https://example-1234567890.cos.ap-guangzhou.myqcloud.com/2026/02/25/afecabebc8014d80b0f06b4796a74c5d.png', 'ollama大模型', 'http://127.0.0.1:11434', '0', 7, 103, '2025-12-14 21:48:11', '1', '1', '2026-02-25 20:48:48', 'ollama厂商', NULL, '0', NULL, 0); INSERT INTO `chat_provider` VALUES (18, '自定义厂商', 'custom_api', 'https://example-1234567890.cos.ap-guangzhou.myqcloud.com/2026/04/19/c1a8e122510f4e2f90deb36958af710b.png', 'OPENAI兼容格式', '自定义', '0', 8, 103, '2026-04-19 12:35:57', '1', '1', '2026-04-19 13:17:20', 'OPENAI兼容格式', NULL, '0', NULL, 0); INSERT INTO `chat_provider` VALUES (2070700000000000001, 'Dify', 'dify', NULL, 'Dify 应用 API 服务', 'https://api.dify.ai/v1', '0', 9, 103, '2026-07-14 11:03:44', '1', '1', '2026-07-14 11:03:44', 'Dify 应用 API 服务商', NULL, '0', NULL, 0); diff --git a/ruoyi-modules/ruoyi-chat/pom.xml b/ruoyi-modules/ruoyi-chat/pom.xml index 566d74be..144240a2 100644 --- a/ruoyi-modules/ruoyi-chat/pom.xml +++ b/ruoyi-modules/ruoyi-chat/pom.xml @@ -35,6 +35,12 @@ ${langchain4j.version} + + dev.langchain4j + langchain4j-anthropic + ${langchain4j.version} + + dev.langchain4j langchain4j-ollama 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 index d83d9900..5012e47a 100644 --- 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 @@ -1,6 +1,10 @@ package org.ruoyi.service.chat.impl.provider; +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.OpenAiStreamingChatModel; import lombok.extern.slf4j.Slf4j; import org.ruoyi.common.chat.domain.dto.request.ChatRequest; @@ -10,13 +14,16 @@ import org.ruoyi.observability.MyChatModelListener; import org.ruoyi.service.chat.AbstractChatService; import org.springframework.stereotype.Service; +import java.net.URI; +import java.time.Duration; import java.util.List; +import java.util.Map; /** - * MiniMax服务调用 + * MiniMax chat service. *

- * 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 requestPath = new AtomicReference<>(); + AtomicReference requestBody = new AtomicReference<>(); + HttpServer server = HttpServer.create(new InetSocketAddress("127.0.0.1", 0), 0); + server.createContext("/", exchange -> { + requestPath.set(exchange.getRequestURI().getPath()); + requestBody.set(new String(exchange.getRequestBody().readAllBytes(), StandardCharsets.UTF_8)); + byte[] response = ("{\"id\":\"msg_test\",\"type\":\"message\"," + + "\"role\":\"assistant\",\"model\":\"MiniMax-M3\"," + + "\"content\":[{\"type\":\"text\",\"text\":\"ok\"}]," + + "\"stop_reason\":\"end_turn\",\"stop_sequence\":null," + + "\"usage\":{\"input_tokens\":1,\"output_tokens\":1}}") + .getBytes(StandardCharsets.UTF_8); + exchange.getResponseHeaders().set("Content-Type", "application/json"); + exchange.sendResponseHeaders(200, response.length); + exchange.getResponseBody().write(response); + exchange.close(); + }); + server.start(); + + try { + String baseUrl = "http://127.0.0.1:" + server.getAddress().getPort() + "/anthropic"; + ChatModel model = minimaxService.buildChatModel(modelVo(baseUrl, "MiniMax-M3")); + + assertEquals("ok", model.chat("Hello")); + assertEquals("/anthropic/v1/messages", requestPath.get()); + JsonNode body = OBJECT_MAPPER.readTree(requestBody.get()); + assertEquals("MiniMax-M3", body.path("model").asText()); + } finally { + server.stop(0); + } } @Test void implementsAbstractChatService() { assertInstanceOf(org.ruoyi.service.chat.AbstractChatService.class, minimaxService); } + + private static ChatModelVo modelVo(String apiHost, String modelName) { + ChatModelVo modelVo = new ChatModelVo(); + modelVo.setApiHost(apiHost); + modelVo.setApiKey("test-api-key"); + modelVo.setModelName(modelName); + return modelVo; + } }