diff --git a/docs/liteflow-react-agent-guide.md b/docs/liteflow-react-agent-guide.md index 4d9a668a5..a06349f6d 100644 --- a/docs/liteflow-react-agent-guide.md +++ b/docs/liteflow-react-agent-guide.md @@ -398,6 +398,50 @@ protected ModelSpec model() { 凭据来源:`liteflow.agent.openai-compatible.myvendor.api-key` / `base-url`。自定义厂商没有内置默认地址,通常需要配置 `base-url`。 +**MiniMax compatible endpoints and current models:** + +```java +@Override +protected ModelSpec model() { + return Minimax.of("MiniMax-M3"); +} +``` + +`Minimax.of(...)` uses `liteflow.agent.openai-compatible.minimax` and defaults to the global OpenAI-compatible endpoint. Both `MiniMax-M3` and `MiniMax-M2.7` can also use the Anthropic-compatible adapter: + +```java +@Override +protected ModelSpec model() { + return AnthropicCompatible.custom("minimax", "MiniMax-M2.7"); +} +``` + +`AnthropicCompatible.custom(...)` uses `liteflow.agent.anthropic-compatible.minimax`. Select the endpoint for the account region through the matching `base-url` setting: + +| Region | OpenAI-compatible base URL | Anthropic-compatible base URL | +| --- | --- | --- | +| Global | `https://api.minimax.io/v1` | `https://api.minimax.io/anthropic` | +| CN | `https://api.minimaxi.com/v1` | `https://api.minimaxi.com/anthropic` | + +The Anthropic-compatible base URLs intentionally end in `/anthropic`; the client appends `/v1/messages` when sending a request. + +The context window is the combined input and output limit. Current model capabilities are: + +| Model | Context window | Input modalities | Thinking | +| --- | ---: | --- | --- | +| `MiniMax-M3` | 1,000,000 | text, image, video | adaptive or disabled; enabled by default | +| `MiniMax-M2.7` | 204,800 | text | always on | + +Current pay-as-you-go pricing is in USD per one 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 | N/A | +| `MiniMax-M3` | standard, over 512,000 input tokens | 0.60 | 2.40 | 0.12 | N/A | +| `MiniMax-M3` | priority, up to 512,000 input tokens | 0.45 | 1.80 | 0.09 | N/A | +| `MiniMax-M3` | priority, over 512,000 input tokens | 0.90 | 3.60 | 0.18 | N/A | +| `MiniMax-M2.7` | standard | 0.30 | 1.20 | 0.06 | 0.375 | + **Anthropic Claude:** ```java @@ -480,7 +524,13 @@ liteflow: kimi: api-key: ${KIMI_API_KEY} base-url: https://api.moonshot.cn/v1 + minimax: + api-key: ${MINIMAX_API_KEY} + base-url: https://api.minimax.io/v1 anthropic-compatible: + minimax: + api-key: ${MINIMAX_API_KEY} + base-url: https://api.minimax.io/anthropic gateway: api-key: ${ANTHROPIC_GATEWAY_API_KEY} base-url: https://anthropic-gateway.example.com diff --git a/liteflow-react-agent/liteflow-react-agent-openai/src/main/java/com/yomahub/liteflow/agent/openai/Minimax.java b/liteflow-react-agent/liteflow-react-agent-openai/src/main/java/com/yomahub/liteflow/agent/openai/Minimax.java index 6c6522f6c..9878311fa 100644 --- a/liteflow-react-agent/liteflow-react-agent-openai/src/main/java/com/yomahub/liteflow/agent/openai/Minimax.java +++ b/liteflow-react-agent/liteflow-react-agent-openai/src/main/java/com/yomahub/liteflow/agent/openai/Minimax.java @@ -2,7 +2,7 @@ public final class Minimax { private static final String CONFIG_KEY = "minimax"; - private static final String BASE_URL = "https://api.minimax.chat/v1"; + private static final String BASE_URL = "https://api.minimax.io/v1"; private Minimax() {} public static OpenAICompatibleSpec of(String modelName) { diff --git a/liteflow-react-agent/liteflow-react-agent-openai/src/main/java/com/yomahub/liteflow/agent/openai/OpenAICompatiblePresets.java b/liteflow-react-agent/liteflow-react-agent-openai/src/main/java/com/yomahub/liteflow/agent/openai/OpenAICompatiblePresets.java index 3154420fd..77370c49c 100644 --- a/liteflow-react-agent/liteflow-react-agent-openai/src/main/java/com/yomahub/liteflow/agent/openai/OpenAICompatiblePresets.java +++ b/liteflow-react-agent/liteflow-react-agent-openai/src/main/java/com/yomahub/liteflow/agent/openai/OpenAICompatiblePresets.java @@ -15,6 +15,6 @@ public static OpenAIChatModel glm(String apiKey, String modelName) { return OpenAIModelFactory.custom(apiKey, "https://open.bigmodel.cn/api/paas/v4", modelName); } public static OpenAIChatModel minimax(String apiKey, String modelName) { - return OpenAIModelFactory.custom(apiKey, "https://api.minimax.chat/v1", modelName); + return OpenAIModelFactory.custom(apiKey, "https://api.minimax.io/v1", modelName); } } \ No newline at end of file diff --git a/liteflow-testcase-el/liteflow-testcase-el-react-agent/src/test/java/com/yomahub/liteflow/test/agent/platform/minimax/MinimaxEndpointTest.java b/liteflow-testcase-el/liteflow-testcase-el-react-agent/src/test/java/com/yomahub/liteflow/test/agent/platform/minimax/MinimaxEndpointTest.java new file mode 100644 index 000000000..e9db4f8e8 --- /dev/null +++ b/liteflow-testcase-el/liteflow-testcase-el-react-agent/src/test/java/com/yomahub/liteflow/test/agent/platform/minimax/MinimaxEndpointTest.java @@ -0,0 +1,144 @@ +package com.yomahub.liteflow.test.agent.platform.minimax; + +import com.yomahub.liteflow.agent.anthropic.AnthropicCompatible; +import com.yomahub.liteflow.agent.openai.Minimax; +import com.yomahub.liteflow.property.agent.AgentConfig; +import com.yomahub.liteflow.property.agent.PlatformCredential; +import io.agentscope.core.message.Msg; +import io.agentscope.core.message.MsgRole; +import io.agentscope.core.model.ExecutionConfig; +import io.agentscope.core.model.GenerateOptions; +import io.agentscope.core.model.Model; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.net.InetAddress; +import java.net.ServerSocket; +import java.net.Socket; +import java.nio.charset.StandardCharsets; +import java.time.Duration; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import reactor.core.publisher.Flux; + +public class MinimaxEndpointTest { + + private static final Duration REQUEST_TIMEOUT = Duration.ofSeconds(10); + + @Test + public void testOpenAICompatibleRequestPath() throws Exception { + try (RequestCapture capture = new RequestCapture()) { + AgentConfig config = new AgentConfig(); + config.getOpenaiCompatible() + .put("minimax", credential(capture.baseUrl("/v1"))); + + Model model = Minimax.of("MiniMax-M3").stream(false).resolve(config); + + invoke(model); + Assertions.assertEquals("/v1/chat/completions", capture.awaitPath()); + } + } + + @Test + public void testAnthropicCompatibleRequestPath() throws Exception { + try (RequestCapture capture = new RequestCapture()) { + AgentConfig config = new AgentConfig(); + config.getAnthropicCompatible() + .put("minimax", credential(capture.baseUrl("/anthropic"))); + + Model model = AnthropicCompatible.custom("minimax", "MiniMax-M2.7") + .stream(false) + .resolve(config); + + invoke(model); + Assertions.assertEquals("/anthropic/v1/messages", capture.awaitPath()); + } + } + + private static PlatformCredential credential(String baseUrl) { + PlatformCredential credential = new PlatformCredential(); + credential.setApiKey("test-key"); + credential.setBaseUrl(baseUrl); + return credential; + } + + private static void invoke(Model model) { + Msg message = Msg.builder() + .name("user") + .role(MsgRole.USER) + .textContent("ping") + .build(); + GenerateOptions options = GenerateOptions.builder() + .executionConfig(ExecutionConfig.builder() + .timeout(REQUEST_TIMEOUT) + .maxAttempts(1) + .build()) + .build(); + + model.stream(List.of(message), List.of(), options) + .onErrorResume(error -> Flux.empty()) + .blockLast(REQUEST_TIMEOUT); + } + + private static final class RequestCapture implements AutoCloseable { + + private final ServerSocket serverSocket; + private final CompletableFuture requestPath; + + private RequestCapture() throws IOException { + serverSocket = new ServerSocket(0, 1, InetAddress.getByName("127.0.0.1")); + requestPath = CompletableFuture.supplyAsync(this::captureRequest); + } + + private String baseUrl(String path) { + return "http://127.0.0.1:" + serverSocket.getLocalPort() + path; + } + + private String awaitPath() throws Exception { + return requestPath.get(REQUEST_TIMEOUT.toSeconds(), TimeUnit.SECONDS); + } + + private String captureRequest() { + try (Socket socket = serverSocket.accept(); + BufferedReader reader = new BufferedReader(new InputStreamReader( + socket.getInputStream(), StandardCharsets.UTF_8)); + OutputStream output = socket.getOutputStream()) { + String requestLine = reader.readLine(); + if (requestLine == null) { + throw new IOException("Missing HTTP request line"); + } + + String line; + while ((line = reader.readLine()) != null && !line.isEmpty()) { + // Consume request headers before returning the test response. + } + + byte[] response = ("HTTP/1.1 400 Bad Request\r\n" + + "Content-Type: application/json\r\n" + + "Content-Length: 2\r\n" + + "Connection: close\r\n" + + "\r\n{}") + .getBytes(StandardCharsets.UTF_8); + output.write(response); + output.flush(); + + String[] parts = requestLine.split(" ", 3); + if (parts.length < 2) { + throw new IOException("Invalid HTTP request line: " + requestLine); + } + return parts[1]; + } catch (IOException error) { + throw new IllegalStateException("Failed to capture HTTP request", error); + } + } + + @Override + public void close() throws IOException { + serverSocket.close(); + } + } +} diff --git a/liteflow-testcase-el/liteflow-testcase-el-react-agent/src/test/java/com/yomahub/liteflow/test/agent/platform/minimax/MinimaxPlatformAgentCmp.java b/liteflow-testcase-el/liteflow-testcase-el-react-agent/src/test/java/com/yomahub/liteflow/test/agent/platform/minimax/MinimaxPlatformAgentCmp.java index 3970d984f..09c1dd3aa 100644 --- a/liteflow-testcase-el/liteflow-testcase-el-react-agent/src/test/java/com/yomahub/liteflow/test/agent/platform/minimax/MinimaxPlatformAgentCmp.java +++ b/liteflow-testcase-el/liteflow-testcase-el-react-agent/src/test/java/com/yomahub/liteflow/test/agent/platform/minimax/MinimaxPlatformAgentCmp.java @@ -14,7 +14,7 @@ public class MinimaxPlatformAgentCmp extends ReActAgentComponent { @Override protected ModelSpec model() { - String model = LiveTestEnv.resolveOrDefault(LiveTestEnv.MINIMAX_MODEL, "MiniMax-Text-01"); + String model = LiveTestEnv.resolveOrDefault(LiveTestEnv.MINIMAX_MODEL, "MiniMax-M3"); return Minimax.of(model).temperature(0.1).maxTokens(64); }