Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions src/LLMProviders/chatModelManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ const CHAT_PROVIDER_CONSTRUCTORS = {
[ChatModelProviders.GROQ]: ChatGroq,
[ChatModelProviders.OPENAI_FORMAT]: ChatOpenAI,
[ChatModelProviders.SILICONFLOW]: ChatOpenAI,
[ChatModelProviders.MINIMAX]: ChatOpenAI,
[ChatModelProviders.COPILOT_PLUS]: ChatOpenRouter,
[ChatModelProviders.MISTRAL]: ChatMistralAI,
[ChatModelProviders.DEEPSEEK]: ChatDeepSeek,
Expand Down Expand Up @@ -144,6 +145,7 @@ export default class ChatModelManager {
[ChatModelProviders.DEEPSEEK]: () => getSettings().deepseekApiKey,
[ChatModelProviders.AMAZON_BEDROCK]: () => getSettings().amazonBedrockApiKey,
[ChatModelProviders.SILICONFLOW]: () => getSettings().siliconflowApiKey,
[ChatModelProviders.MINIMAX]: () => getSettings().minimaxApiKey,
[ChatModelProviders.GITHUB_COPILOT]: () =>
getSettings().githubCopilotToken || getSettings().githubCopilotAccessToken,
} as const;
Expand Down Expand Up @@ -398,6 +400,14 @@ export default class ChatModelManager {
customModel
),
},
[ChatModelProviders.MINIMAX]: {
modelName: modelName,
apiKey: await getDecryptedKey(customModel.apiKey || settings.minimaxApiKey),
configuration: {
baseURL: customModel.baseUrl || ProviderInfo[ChatModelProviders.MINIMAX].host,
Comment on lines +403 to +407

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Clamp MiniMax temperature to the backend's accepted range

MiniMax's OpenAI-compatible API only accepts temperature in (0,1], but this provider is wired through the generic ChatOpenAI path that forwards the shared Copilot temperature unchanged via getTemperatureForModel()/baseConfig. Since the UI currently allows 0..2, users who already run with temperature=0 or >1 on 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 👍 / 👎.

fetch: customModel.enableCors ? safeFetch : undefined,
},
},
[ChatModelProviders.COPILOT_PLUS]: {
modelName: modelName,
apiKey: await getDecryptedKey(settings.plusLicenseKey),
Expand Down Expand Up @@ -598,6 +608,7 @@ export default class ChatModelManager {
ChatModelProviders.MISTRAL,
ChatModelProviders.DEEPSEEK,
ChatModelProviders.SILICONFLOW,
ChatModelProviders.MINIMAX,
].includes(provider)
) {
params.topP = customModel.topP;
Expand All @@ -618,6 +629,7 @@ export default class ChatModelManager {
ChatModelProviders.MISTRAL,
ChatModelProviders.DEEPSEEK,
ChatModelProviders.SILICONFLOW,
ChatModelProviders.MINIMAX,
].includes(provider)
) {
params.frequencyPenalty = customModel.frequencyPenalty;
Expand Down
142 changes: 142 additions & 0 deletions src/LLMProviders/minimax.test.ts
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([]);
});
});
25 changes: 25 additions & 0 deletions src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -222,6 +224,7 @@ export enum ChatModelProviders {
DEEPSEEK = "deepseek",
COHEREAI = "cohereai",
SILICONFLOW = "siliconflow",
MINIMAX = "minimax",
GITHUB_COPILOT = "github-copilot",
}

Expand Down Expand Up @@ -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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Add MiniMax built-ins to existing users' active model list

On upgrade, these new built-ins never reach persisted settings. setSettings() rehydrates through mergeActiveModels() in src/settings/model.ts, and that function only seeds the map with builtInModels.filter(model => model.core) before merging the saved activeModels. Because this new MiniMax entry (and the adjacent M2.5 entry) is non-core and disabled, any existing vault that already has an activeModels array will never see MiniMax in Model Settings, so the advertised “enable MiniMax-M2.7/M2.5” path only works on fresh installs unless the user manually imports the models.

Useful? React with 👍 / 👎.

{
name: ChatModels.MINIMAX_M2_5,
provider: ChatModelProviders.MINIMAX,
enabled: false,
isBuiltIn: true,
},
];

export enum EmbeddingModelProviders {
Expand Down Expand Up @@ -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",
Expand All @@ -749,6 +772,7 @@ export const ProviderSettingsKeyMap: Record<SettingKeyProviders, keyof CopilotSe
deepseek: "deepseekApiKey",
"amazon-bedrock": "amazonBedrockApiKey",
siliconflow: "siliconflowApiKey",
minimax: "minimaxApiKey",
"github-copilot": "githubCopilotToken",
};

Expand Down Expand Up @@ -905,6 +929,7 @@ export const DEFAULT_SETTINGS: CopilotSettings = {
amazonBedrockApiKey: "",
amazonBedrockRegion: "",
siliconflowApiKey: "",
minimaxApiKey: "",
// GitHub Copilot OAuth tokens
githubCopilotAccessToken: "",
githubCopilotToken: "",
Expand Down
90 changes: 90 additions & 0 deletions src/integration_tests/minimax.test.ts
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);
});
1 change: 1 addition & 0 deletions src/settings/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ export interface CopilotSettings {
amazonBedrockApiKey: string;
amazonBedrockRegion: string;
siliconflowApiKey: string;
minimaxApiKey: string;
// GitHub Copilot OAuth tokens
githubCopilotAccessToken: string;
githubCopilotToken: string;
Expand Down
Loading