-
-
Notifications
You must be signed in to change notification settings - Fork 663
feat: add MiniMax as first-class LLM provider #2322
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,142 @@ | ||
| import { | ||
| BUILTIN_CHAT_MODELS, | ||
| ChatModelProviders, | ||
| ChatModels, | ||
| DEFAULT_SETTINGS, | ||
| ProviderInfo, | ||
| ProviderSettingsKeyMap, | ||
| } from "@/constants"; | ||
| import { providerAdapters, MiniMaxModelResponse } from "@/settings/providerModels"; | ||
|
|
||
| describe("MiniMax provider registration", () => { | ||
| it("should have MINIMAX in ChatModelProviders enum", () => { | ||
| expect(ChatModelProviders.MINIMAX).toBe("minimax"); | ||
| }); | ||
|
|
||
| it("should have MiniMax-M2.7 in ChatModels enum", () => { | ||
| expect(ChatModels.MINIMAX_M2_7).toBe("MiniMax-M2.7"); | ||
| }); | ||
|
|
||
| it("should have MiniMax-M2.5 in ChatModels enum", () => { | ||
| expect(ChatModels.MINIMAX_M2_5).toBe("MiniMax-M2.5"); | ||
| }); | ||
| }); | ||
|
|
||
| describe("MiniMax built-in models", () => { | ||
| const minimaxModels = BUILTIN_CHAT_MODELS.filter( | ||
| (m) => m.provider === ChatModelProviders.MINIMAX | ||
| ); | ||
|
|
||
| it("should include MiniMax models in BUILTIN_CHAT_MODELS", () => { | ||
| expect(minimaxModels.length).toBe(2); | ||
| }); | ||
|
|
||
| it("should have MiniMax-M2.7 model", () => { | ||
| const m27 = minimaxModels.find((m) => m.name === ChatModels.MINIMAX_M2_7); | ||
| expect(m27).toBeDefined(); | ||
| expect(m27!.provider).toBe(ChatModelProviders.MINIMAX); | ||
| expect(m27!.isBuiltIn).toBe(true); | ||
| }); | ||
|
|
||
| it("should have MiniMax-M2.5 model", () => { | ||
| const m25 = minimaxModels.find((m) => m.name === ChatModels.MINIMAX_M2_5); | ||
| expect(m25).toBeDefined(); | ||
| expect(m25!.provider).toBe(ChatModelProviders.MINIMAX); | ||
| expect(m25!.isBuiltIn).toBe(true); | ||
| }); | ||
|
|
||
| it("should have MiniMax models disabled by default", () => { | ||
| minimaxModels.forEach((m) => { | ||
| expect(m.enabled).toBe(false); | ||
| }); | ||
| }); | ||
| }); | ||
|
|
||
| describe("MiniMax ProviderInfo", () => { | ||
| const info = ProviderInfo[ChatModelProviders.MINIMAX]; | ||
|
|
||
| it("should have MiniMax provider info", () => { | ||
| expect(info).toBeDefined(); | ||
| }); | ||
|
|
||
| it("should have correct label", () => { | ||
| expect(info.label).toBe("MiniMax"); | ||
| }); | ||
|
|
||
| it("should have correct API host", () => { | ||
| expect(info.host).toBe("https://api.minimax.io/v1"); | ||
| }); | ||
|
|
||
| it("should have correct curlBaseURL", () => { | ||
| expect(info.curlBaseURL).toBe("https://api.minimax.io/v1"); | ||
| }); | ||
|
|
||
| it("should have key management URL", () => { | ||
| expect(info.keyManagementURL).toBeTruthy(); | ||
| expect(info.keyManagementURL).toContain("minimaxi.com"); | ||
| }); | ||
|
|
||
| it("should have list model URL", () => { | ||
| expect(info.listModelURL).toBe("https://api.minimax.io/v1/models"); | ||
| }); | ||
|
|
||
| it("should have test model set to MiniMax-M2.7", () => { | ||
| expect(info.testModel).toBe(ChatModels.MINIMAX_M2_7); | ||
| }); | ||
| }); | ||
|
|
||
| describe("MiniMax settings key mapping", () => { | ||
| it("should map minimax provider to minimaxApiKey setting", () => { | ||
| expect(ProviderSettingsKeyMap["minimax" as keyof typeof ProviderSettingsKeyMap]).toBe( | ||
| "minimaxApiKey" | ||
| ); | ||
| }); | ||
| }); | ||
|
|
||
| describe("MiniMax default settings", () => { | ||
| it("should have minimaxApiKey in DEFAULT_SETTINGS", () => { | ||
| expect(DEFAULT_SETTINGS).toHaveProperty("minimaxApiKey"); | ||
| expect(DEFAULT_SETTINGS.minimaxApiKey).toBe(""); | ||
| }); | ||
| }); | ||
|
|
||
| describe("MiniMax provider model adapter", () => { | ||
| const adapter = providerAdapters[ChatModelProviders.MINIMAX]; | ||
|
|
||
| it("should have a model adapter for MiniMax", () => { | ||
| expect(adapter).toBeDefined(); | ||
| }); | ||
|
|
||
| it("should parse MiniMax model list response", () => { | ||
| const mockResponse: MiniMaxModelResponse = { | ||
| object: "list", | ||
| data: [ | ||
| { id: "MiniMax-M2.7", object: "model", created: 1710000000, owned_by: "minimax" }, | ||
| { id: "MiniMax-M2.5", object: "model", created: 1700000000, owned_by: "minimax" }, | ||
| ], | ||
| }; | ||
|
|
||
| const models = adapter!(mockResponse); | ||
| expect(models).toHaveLength(2); | ||
| expect(models[0].id).toBe("MiniMax-M2.7"); | ||
| expect(models[0].name).toBe("MiniMax-M2.7"); | ||
| expect(models[0].provider).toBe(ChatModelProviders.MINIMAX); | ||
| expect(models[1].id).toBe("MiniMax-M2.5"); | ||
| }); | ||
|
|
||
| it("should handle empty model list", () => { | ||
| const mockResponse: MiniMaxModelResponse = { | ||
| object: "list", | ||
| data: [], | ||
| }; | ||
|
|
||
| const models = adapter!(mockResponse); | ||
| expect(models).toHaveLength(0); | ||
| }); | ||
|
|
||
| it("should handle missing data field gracefully", () => { | ||
| const mockResponse = { object: "list" } as any; | ||
| const models = adapter!(mockResponse); | ||
| expect(models).toEqual([]); | ||
| }); | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -202,6 +202,8 @@ export enum ChatModels { | |
| OPENROUTER_GROK_4_1_FAST = "x-ai/grok-4.1-fast", | ||
| SILICONFLOW_DEEPSEEK_V3 = "deepseek-ai/DeepSeek-V3", | ||
| SILICONFLOW_DEEPSEEK_R1 = "deepseek-ai/DeepSeek-R1", | ||
| MINIMAX_M2_7 = "MiniMax-M2.7", | ||
| MINIMAX_M2_5 = "MiniMax-M2.5", | ||
| } | ||
|
|
||
| // Model Providers | ||
|
|
@@ -222,6 +224,7 @@ export enum ChatModelProviders { | |
| DEEPSEEK = "deepseek", | ||
| COHEREAI = "cohereai", | ||
| SILICONFLOW = "siliconflow", | ||
| MINIMAX = "minimax", | ||
| GITHUB_COPILOT = "github-copilot", | ||
| } | ||
|
|
||
|
|
@@ -432,6 +435,18 @@ export const BUILTIN_CHAT_MODELS: CustomModel[] = [ | |
| baseUrl: "https://api.siliconflow.com/v1", | ||
| capabilities: [ModelCapability.REASONING], | ||
| }, | ||
| { | ||
| name: ChatModels.MINIMAX_M2_7, | ||
| provider: ChatModelProviders.MINIMAX, | ||
| enabled: false, | ||
| isBuiltIn: true, | ||
| }, | ||
|
Comment on lines
+439
to
+443
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
On upgrade, these new built-ins never reach persisted settings. Useful? React with 👍 / 👎. |
||
| { | ||
| name: ChatModels.MINIMAX_M2_5, | ||
| provider: ChatModelProviders.MINIMAX, | ||
| enabled: false, | ||
| isBuiltIn: true, | ||
| }, | ||
| ]; | ||
|
|
||
| export enum EmbeddingModelProviders { | ||
|
|
@@ -725,6 +740,14 @@ export const ProviderInfo: Record<Provider, ProviderMetadata> = { | |
| keyManagementURL: "", | ||
| listModelURL: "", | ||
| }, | ||
| [ChatModelProviders.MINIMAX]: { | ||
| label: "MiniMax", | ||
| host: "https://api.minimax.io/v1", | ||
| curlBaseURL: "https://api.minimax.io/v1", | ||
| keyManagementURL: "https://platform.minimaxi.com/user-center/basic-information/interface-key", | ||
| listModelURL: "https://api.minimax.io/v1/models", | ||
| testModel: ChatModels.MINIMAX_M2_7, | ||
| }, | ||
| [ChatModelProviders.GITHUB_COPILOT]: { | ||
| label: "GitHub Copilot", | ||
| host: "https://api.githubcopilot.com", | ||
|
|
@@ -749,6 +772,7 @@ export const ProviderSettingsKeyMap: Record<SettingKeyProviders, keyof CopilotSe | |
| deepseek: "deepseekApiKey", | ||
| "amazon-bedrock": "amazonBedrockApiKey", | ||
| siliconflow: "siliconflowApiKey", | ||
| minimax: "minimaxApiKey", | ||
| "github-copilot": "githubCopilotToken", | ||
| }; | ||
|
|
||
|
|
@@ -905,6 +929,7 @@ export const DEFAULT_SETTINGS: CopilotSettings = { | |
| amazonBedrockApiKey: "", | ||
| amazonBedrockRegion: "", | ||
| siliconflowApiKey: "", | ||
| minimaxApiKey: "", | ||
| // GitHub Copilot OAuth tokens | ||
| githubCopilotAccessToken: "", | ||
| githubCopilotToken: "", | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,90 @@ | ||
| /** | ||
| * MiniMax provider integration tests. | ||
| * | ||
| * These tests verify that MiniMax API calls work end-to-end. | ||
| * They require a MINIMAX_API_KEY environment variable to be set. | ||
| * | ||
| * Run with: npm run test:integration -- -t "MiniMax" | ||
| */ | ||
| import * as dotenv from "dotenv"; | ||
|
|
||
| // Add global fetch polyfill for Node.js environments | ||
| import fetch, { Headers, Request, Response } from "node-fetch"; | ||
| if (!globalThis.fetch) { | ||
| globalThis.fetch = fetch as any; | ||
| globalThis.Headers = Headers as any; | ||
| globalThis.Request = Request as any; | ||
| globalThis.Response = Response as any; | ||
| } | ||
|
|
||
| // Add TextDecoderStream polyfill for Node.js environments | ||
| import "web-streams-polyfill/dist/polyfill.js"; | ||
|
|
||
| // Load environment variables from .env.test | ||
| dotenv.config({ path: ".env.test" }); | ||
|
|
||
| import { ChatOpenAI } from "@langchain/openai"; | ||
| import { HumanMessage } from "@langchain/core/messages"; | ||
|
|
||
| const MINIMAX_API_KEY = process.env.MINIMAX_API_KEY; | ||
| const describeIntegration = MINIMAX_API_KEY ? describe : describe.skip; | ||
|
|
||
| describeIntegration("MiniMax integration", () => { | ||
| it("should complete a chat request with MiniMax-M2.7", async () => { | ||
| const chat = new ChatOpenAI({ | ||
| modelName: "MiniMax-M2.7", | ||
| apiKey: MINIMAX_API_KEY!, | ||
| configuration: { | ||
| baseURL: "https://api.minimax.io/v1", | ||
| }, | ||
| maxTokens: 50, | ||
| temperature: 0.7, | ||
| }); | ||
|
|
||
| const response = await chat.invoke([new HumanMessage("Say hello in one sentence.")]); | ||
| expect(response.content).toBeTruthy(); | ||
| expect(typeof response.content).toBe("string"); | ||
| }, 30000); | ||
|
|
||
| it("should complete a chat request with MiniMax-M2.5", async () => { | ||
| const chat = new ChatOpenAI({ | ||
| modelName: "MiniMax-M2.5", | ||
| apiKey: MINIMAX_API_KEY!, | ||
| configuration: { | ||
| baseURL: "https://api.minimax.io/v1", | ||
| }, | ||
| maxTokens: 50, | ||
| temperature: 0.7, | ||
| }); | ||
|
|
||
| const response = await chat.invoke([new HumanMessage("Say hello in one sentence.")]); | ||
| expect(response.content).toBeTruthy(); | ||
| expect(typeof response.content).toBe("string"); | ||
| }, 30000); | ||
|
|
||
| it("should support streaming with MiniMax", async () => { | ||
| const chat = new ChatOpenAI({ | ||
| modelName: "MiniMax-M2.7", | ||
| apiKey: MINIMAX_API_KEY!, | ||
| configuration: { | ||
| baseURL: "https://api.minimax.io/v1", | ||
| }, | ||
| maxTokens: 50, | ||
| temperature: 0.7, | ||
| streaming: true, | ||
| }); | ||
|
|
||
| const chunks: string[] = []; | ||
| const stream = await chat.stream([new HumanMessage("Count from 1 to 3.")]); | ||
|
|
||
| for await (const chunk of stream) { | ||
| if (typeof chunk.content === "string" && chunk.content) { | ||
| chunks.push(chunk.content); | ||
| } | ||
| } | ||
|
|
||
| expect(chunks.length).toBeGreaterThan(0); | ||
| const fullResponse = chunks.join(""); | ||
| expect(fullResponse).toBeTruthy(); | ||
| }, 30000); | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
MiniMax's OpenAI-compatible API only accepts
temperaturein(0,1], but this provider is wired through the genericChatOpenAIpath that forwards the shared Copilot temperature unchanged viagetTemperatureForModel()/baseConfig. Since the UI currently allows0..2, users who already run withtemperature=0or>1on another provider will start getting request failures as soon as they switch to MiniMax, because nothing here validates or clamps the value for that backend.Useful? React with 👍 / 👎.