From d080265973e0010768167e86262f1de8028cb98e Mon Sep 17 00:00:00 2001
From: octo-patch <266937838+octo-patch@users.noreply.github.com>
Date: Mon, 27 Jul 2026 11:11:29 +0800
Subject: [PATCH] feat(providers): add MiniMax support
---
.env.example | 7 +-
public/api-docs.html | 5 +-
server/claude-sdk.js | 63 +++++----
server/index.js | 6 +
server/minimax-sdk.ts | 39 ++++++
.../claude-session-synchronizer.provider.ts | 52 ++++++-
.../list/minimax/minimax-auth.provider.ts | 48 +++++++
.../providers/list/minimax/minimax-config.ts | 112 +++++++++++++++
.../list/minimax/minimax-models.provider.ts | 71 ++++++++++
.../list/minimax/minimax.provider.ts | 127 ++++++++++++++++++
server/modules/providers/provider.registry.ts | 2 +
server/modules/providers/provider.routes.ts | 1 +
.../services/provider-capabilities.service.ts | 10 ++
.../session-conversations-search.service.ts | 16 ++-
.../services/session-synchronizer.service.ts | 1 +
server/modules/providers/tests/mcp.test.ts | 4 +-
.../providers/tests/minimax-provider.test.ts | 51 +++++++
server/routes/agent.js | 20 ++-
server/routes/commands.js | 3 +-
server/shared/types.ts | 2 +-
.../chat/hooks/useChatComposerState.ts | 39 ++----
.../chat/hooks/useChatProviderState.ts | 33 ++++-
src/components/chat/view/ChatInterface.tsx | 29 ++--
.../view/subcomponents/ChatMessagesPane.tsx | 30 +----
.../view/subcomponents/CommandResultModal.tsx | 1 +
.../view/subcomponents/MessageComponent.tsx | 3 +-
.../ProviderSelectionEmptyState.tsx | 71 +++-------
.../llm-logo-provider/MiniMaxLogo.tsx | 25 ++++
.../llm-logo-provider/SessionProviderLogo.tsx | 5 +
src/components/mcp/constants.ts | 5 +
.../subcomponents/AgentConnectionsStep.tsx | 7 +
src/components/provider-auth/types.ts | 4 +-
.../provider-auth/view/ProviderLoginModal.tsx | 5 +
.../settings/constants/constants.ts | 2 +-
.../tabs/agents-settings/AgentListItem.tsx | 9 +-
.../agents-settings/AgentsSettingsTab.tsx | 7 +-
.../sections/AgentCategoryContentSection.tsx | 2 +-
.../sections/AgentSelectorSection.tsx | 2 +
.../sections/content/AccountContent.tsx | 9 ++
src/components/skills/view/ProviderSkills.tsx | 2 +
src/types/app.ts | 2 +-
41 files changed, 745 insertions(+), 187 deletions(-)
create mode 100644 server/minimax-sdk.ts
create mode 100644 server/modules/providers/list/minimax/minimax-auth.provider.ts
create mode 100644 server/modules/providers/list/minimax/minimax-config.ts
create mode 100644 server/modules/providers/list/minimax/minimax-models.provider.ts
create mode 100644 server/modules/providers/list/minimax/minimax.provider.ts
create mode 100644 server/modules/providers/tests/minimax-provider.test.ts
create mode 100644 src/components/llm-logo-provider/MiniMaxLogo.tsx
diff --git a/.env.example b/.env.example
index 7e1d124c72..09820939ad 100755
--- a/.env.example
+++ b/.env.example
@@ -28,6 +28,12 @@ HOST=0.0.0.0
# Uncomment the following line if you have a custom claude cli path other than the default "claude"
# CLAUDE_CLI_PATH=claude
+# MiniMax provider configuration. The default region is global; use "cn" for
+# the China endpoint or set an explicit Anthropic-compatible base URL.
+# MINIMAX_API_KEY=
+# MINIMAX_REGION=global
+# MINIMAX_ANTHROPIC_BASE_URL=https://api.minimax.io/anthropic
+
# =============================================================================
# DATABASE CONFIGURATION
# =============================================================================
@@ -42,4 +48,3 @@ HOST=0.0.0.0
VITE_CONTEXT_WINDOW=160000
CONTEXT_WINDOW=160000
-
diff --git a/public/api-docs.html b/public/api-docs.html
index c2d60173e0..8d3c58a714 100644
--- a/public/api-docs.html
+++ b/public/api-docs.html
@@ -489,7 +489,7 @@
Agent
http://localhost:3001/api/agent
- Trigger an AI agent (Claude, Cursor, Codex, or OpenCode) to work on a project.
+ Trigger an AI agent, including MiniMax, to work on a project.
Request Body Parameters
@@ -524,7 +524,7 @@ Request Body Parameters
provider |
string |
Optional |
- claude, cursor, codex, or opencode (default: claude) |
+ Supported provider identifier, including minimax (default: claude) |
stream |
@@ -838,6 +838,7 @@ Branch & PR Response
{ id: 'claude', name: 'Anthropic' },
{ id: 'codex', name: 'OpenAI' },
{ id: 'cursor', name: 'Cursor' },
+ { id: 'minimax', name: 'MiniMax' },
{ id: 'opencode', name: 'OpenCode' },
];
diff --git a/server/claude-sdk.js b/server/claude-sdk.js
index 6e5bb5cee5..293176a3b4 100644
--- a/server/claude-sdk.js
+++ b/server/claude-sdk.js
@@ -165,6 +165,10 @@ function mapCliOptionsToSDK(options = {}) {
// Forward all host env vars (e.g. ANTHROPIC_BASE_URL) to the subprocess.
// Since SDK 0.2.113, options.env replaces process.env instead of overlaying it.
sdkOptions.env = { ...process.env };
+ for (const environmentName of options.unsetEnvironment || []) {
+ delete sdkOptions.env[environmentName];
+ }
+ Object.assign(sdkOptions.env, options.environment || {});
// Resolve the executable eagerly on Windows because the SDK uses raw child_process.spawn,
// which does not reliably follow npm's shell wrappers like cross-spawn does.
@@ -208,7 +212,7 @@ function mapCliOptionsToSDK(options = {}) {
sdkOptions.disallowedTools = settings.disallowedTools || [];
- sdkOptions.model = options.model || CLAUDE_FALLBACK_MODELS.DEFAULT;
+ sdkOptions.model = options.model || options.defaultModel || CLAUDE_FALLBACK_MODELS.DEFAULT;
const resolvedEffort = resolveClaudeEffort(
sdkOptions.model,
@@ -301,7 +305,7 @@ function readNumber(value) {
* @param {Object} sdkMessage - SDK stream message
* @returns {Object|null} Token budget object or null
*/
-function extractTokenBudget(sdkMessage) {
+function extractTokenBudget(sdkMessage, contextWindowOverride) {
if (!sdkMessage || typeof sdkMessage !== 'object') {
return null;
}
@@ -315,7 +319,9 @@ function extractTokenBudget(sdkMessage) {
const inputTokens = directInputTokens + cacheTokens;
const outputTokens = readNumber(messageUsage.output_tokens ?? messageUsage.outputTokens);
const totalUsed = inputTokens + outputTokens;
- const contextWindow = parseInt(process.env.CONTEXT_WINDOW, 10) || 160000;
+ const contextWindow = Number.isFinite(contextWindowOverride)
+ ? contextWindowOverride
+ : parseInt(process.env.CONTEXT_WINDOW, 10) || 160000;
return {
used: totalUsed,
@@ -455,10 +461,12 @@ async function loadMcpConfig(cwd) {
* @param {string} command - User prompt/command
* @param {Object} options - Query options
* @param {Object} ws - WebSocket connection
+ * @param {Object} runtimeConfig - Provider identity and compatible runtime overrides
* @returns {Promise}
*/
-async function queryClaudeSDK(command, options = {}, ws) {
+async function queryClaudeSDK(command, options = {}, ws, runtimeConfig = {}) {
const { sessionId, sessionSummary } = options;
+ const provider = runtimeConfig.provider || 'claude';
let capturedSessionId = sessionId;
let sessionCreatedSent = false;
@@ -472,21 +480,24 @@ async function queryClaudeSDK(command, options = {}, ws) {
try {
const resolvedModel = await providerModelsService.resolveResumeModel(
- 'claude',
+ provider,
sessionId,
options.model,
);
let effortModels = CLAUDE_FALLBACK_MODELS;
try {
- effortModels = (await providerModelsService.getProviderModels('claude')).models;
+ effortModels = (await providerModelsService.getProviderModels(provider)).models;
} catch (error) {
- console.warn('[Claude SDK] Unable to load provider models for effort validation:', error);
+ console.warn(`[${provider}] Unable to load provider models for effort validation:`, error);
}
const sdkOptions = mapCliOptionsToSDK({
...options,
model: resolvedModel || options.model,
effortModels,
+ defaultModel: runtimeConfig.defaultModel,
+ environment: runtimeConfig.environment,
+ unsetEnvironment: runtimeConfig.unsetEnvironment,
});
const mcpServers = await loadMcpConfig(options.cwd);
@@ -503,16 +514,16 @@ async function queryClaudeSDK(command, options = {}, ws) {
Notification: [{
matcher: '',
hooks: [async (input) => {
- const message = typeof input?.message === 'string' ? input.message : 'Claude requires your attention.';
+ const message = typeof input?.message === 'string' ? input.message : 'The provider requires your attention.';
emitNotification(createNotificationEvent({
- provider: 'claude',
+ provider,
sessionId: capturedSessionId || sessionId || null,
kind: 'action_required',
code: 'agent.notification',
meta: { message, sessionName: sessionSummary },
severity: 'warning',
requiresUserAction: true,
- dedupeKey: `claude:hook:notification:${capturedSessionId || sessionId || 'none'}:${message}`
+ dedupeKey: `${provider}:hook:notification:${capturedSessionId || sessionId || 'none'}:${message}`
}));
return {};
}]
@@ -549,16 +560,16 @@ async function queryClaudeSDK(command, options = {}, ws) {
}
const requestId = createRequestId();
- ws.send(createNormalizedMessage({ kind: 'permission_request', requestId, toolName, input, sessionId: capturedSessionId || sessionId || null, provider: 'claude' }));
+ ws.send(createNormalizedMessage({ kind: 'permission_request', requestId, toolName, input, sessionId: capturedSessionId || sessionId || null, provider }));
emitNotification(createNotificationEvent({
- provider: 'claude',
+ provider,
sessionId: capturedSessionId || sessionId || null,
kind: 'action_required',
code: 'permission.required',
meta: { toolName, sessionName: sessionSummary },
severity: 'warning',
requiresUserAction: true,
- dedupeKey: `claude:permission:${capturedSessionId || sessionId || 'none'}:${requestId}`
+ dedupeKey: `${provider}:permission:${capturedSessionId || sessionId || 'none'}:${requestId}`
}));
const decision = await waitForToolApproval(requestId, {
@@ -571,7 +582,7 @@ async function queryClaudeSDK(command, options = {}, ws) {
_receivedAt: new Date(),
},
onCancel: (reason) => {
- ws.send(createNormalizedMessage({ kind: 'permission_cancelled', requestId, reason, sessionId: capturedSessionId || sessionId || null, provider: 'claude' }));
+ ws.send(createNormalizedMessage({ kind: 'permission_cancelled', requestId, reason, sessionId: capturedSessionId || sessionId || null, provider }));
}
});
if (!decision) {
@@ -647,7 +658,7 @@ async function queryClaudeSDK(command, options = {}, ws) {
// Send session-created event only once for new sessions
if (!sessionId && !sessionCreatedSent) {
sessionCreatedSent = true;
- ws.send(createNormalizedMessage({ kind: 'session_created', newSessionId: capturedSessionId, sessionId: capturedSessionId, provider: 'claude' }));
+ ws.send(createNormalizedMessage({ kind: 'session_created', newSessionId: capturedSessionId, sessionId: capturedSessionId, provider }));
}
} else {
// session_id already captured
@@ -658,7 +669,7 @@ async function queryClaudeSDK(command, options = {}, ws) {
const sid = capturedSessionId || sessionId || null;
// Use adapter to normalize SDK events into NormalizedMessage[]
- const normalized = sessionsService.normalizeMessage('claude', transformedMessage, sid);
+ const normalized = sessionsService.normalizeMessage(provider, transformedMessage, sid);
for (const msg of normalized) {
// Preserve parentToolUseId from SDK wrapper for subagent tool grouping
if (transformedMessage.parentToolUseId && !msg.parentToolUseId) {
@@ -668,9 +679,9 @@ async function queryClaudeSDK(command, options = {}, ws) {
}
// Extract and send token budget updates from assistant/result usage payloads
- const tokenBudgetData = extractTokenBudget(message);
+ const tokenBudgetData = extractTokenBudget(message, runtimeConfig.contextWindow);
if (tokenBudgetData) {
- ws.send(createNormalizedMessage({ kind: 'status', text: 'token_budget', tokenBudget: tokenBudgetData, sessionId: capturedSessionId || sessionId || null, provider: 'claude' }));
+ ws.send(createNormalizedMessage({ kind: 'status', text: 'token_budget', tokenBudget: tokenBudgetData, sessionId: capturedSessionId || sessionId || null, provider }));
}
}
@@ -683,11 +694,11 @@ async function queryClaudeSDK(command, options = {}, ws) {
// terminal `complete` (aborted: true) was already sent by abort-session.
const wasAborted = capturedSessionId ? abortedSessionIds.delete(capturedSessionId) : false;
if (!wasAborted) {
- ws.send(createCompleteMessage({ provider: 'claude', sessionId: capturedSessionId || sessionId || null, exitCode: 0 }));
+ ws.send(createCompleteMessage({ provider, sessionId: capturedSessionId || sessionId || null, exitCode: 0 }));
}
notifyRunStopped({
userId: ws?.userId || null,
- provider: 'claude',
+ provider,
sessionId: capturedSessionId || sessionId || null,
sessionName: sessionSummary,
stopReason: wasAborted ? 'aborted' : 'completed'
@@ -709,18 +720,18 @@ async function queryClaudeSDK(command, options = {}, ws) {
return;
}
- // Check if Claude CLI is installed for a clearer error message
- const installed = await providerAuthService.isProviderInstalled('claude');
+ // Check whether the compatible runtime is installed for a clearer error message.
+ const installed = await providerAuthService.isProviderInstalled(provider);
const errorContent = !installed
- ? 'Claude Code is not installed. Please install it first: https://docs.anthropic.com/en/docs/claude-code'
+ ? 'The compatible coding runtime is not installed.'
: error.message;
// Send error to WebSocket, then the terminal complete
- ws.send(createNormalizedMessage({ kind: 'error', content: errorContent, sessionId: capturedSessionId || sessionId || null, provider: 'claude' }));
- ws.send(createCompleteMessage({ provider: 'claude', sessionId: capturedSessionId || sessionId || null, exitCode: 1 }));
+ ws.send(createNormalizedMessage({ kind: 'error', content: errorContent, sessionId: capturedSessionId || sessionId || null, provider }));
+ ws.send(createCompleteMessage({ provider, sessionId: capturedSessionId || sessionId || null, exitCode: 1 }));
notifyRunFailed({
userId: ws?.userId || null,
- provider: 'claude',
+ provider,
sessionId: capturedSessionId || sessionId || null,
sessionName: sessionSummary,
error
diff --git a/server/index.js b/server/index.js
index 0832c4905b..55f4cabad5 100755
--- a/server/index.js
+++ b/server/index.js
@@ -39,6 +39,10 @@ import {
spawnOpenCode,
abortOpenCodeSession,
} from './opencode-cli.js';
+import {
+ queryMiniMaxSDK,
+ abortMiniMaxSDKSession,
+} from './minimax-sdk.js';
import {
stripAnsiSequences,
normalizeDetectedUrl,
@@ -113,12 +117,14 @@ const wss = createWebSocketServer(server, {
claude: queryClaudeSDK,
cursor: spawnCursor,
codex: queryCodex,
+ minimax: queryMiniMaxSDK,
opencode: spawnOpenCode,
},
abortFns: {
claude: abortClaudeSDKSession,
cursor: abortCursorSession,
codex: abortCodexSession,
+ minimax: abortMiniMaxSDKSession,
opencode: abortOpenCodeSession,
},
resolveToolApproval,
diff --git a/server/minimax-sdk.ts b/server/minimax-sdk.ts
new file mode 100644
index 0000000000..b1fb852f3b
--- /dev/null
+++ b/server/minimax-sdk.ts
@@ -0,0 +1,39 @@
+import {
+ buildMiniMaxRuntimeEnvironment,
+ readMiniMaxSettingsEnvironment,
+} from '@/modules/providers/list/minimax/minimax-config.js';
+import {
+ MINIMAX_MODELS,
+ resolveMiniMaxContextWindow,
+} from '@/modules/providers/list/minimax/minimax-models.provider.js';
+import type { AnyRecord } from '@/shared/types.js';
+
+import { abortClaudeSDKSession, queryClaudeSDK } from './claude-sdk.js';
+
+export async function queryMiniMaxSDK(
+ command: string,
+ options: AnyRecord = {},
+ writer: Parameters[2],
+): Promise {
+ const model = typeof options.model === 'string' && options.model.trim()
+ ? options.model.trim()
+ : MINIMAX_MODELS.DEFAULT;
+ const contextWindow = resolveMiniMaxContextWindow(model);
+ const settingsEnvironment = await readMiniMaxSettingsEnvironment();
+ const environment = buildMiniMaxRuntimeEnvironment(
+ model,
+ contextWindow,
+ process.env,
+ settingsEnvironment,
+ );
+
+ return queryClaudeSDK(command, { ...options, model }, writer, {
+ provider: 'minimax',
+ defaultModel: MINIMAX_MODELS.DEFAULT,
+ contextWindow,
+ environment,
+ unsetEnvironment: ['ANTHROPIC_API_KEY', 'ANTHROPIC_AUTH_TOKEN'],
+ });
+}
+
+export const abortMiniMaxSDKSession = abortClaudeSDKSession;
diff --git a/server/modules/providers/list/claude/claude-session-synchronizer.provider.ts b/server/modules/providers/list/claude/claude-session-synchronizer.provider.ts
index 9320a2fe02..aa4a8d848b 100644
--- a/server/modules/providers/list/claude/claude-session-synchronizer.provider.ts
+++ b/server/modules/providers/list/claude/claude-session-synchronizer.provider.ts
@@ -3,6 +3,7 @@ import path from 'node:path';
import { readFile } from 'node:fs/promises';
import { sessionsDb } from '@/modules/database/index.js';
+import { isMiniMaxModel } from '@/modules/providers/list/minimax/minimax-models.provider.js';
import {
buildLookupMap,
extractFirstValidJsonlData,
@@ -11,10 +12,12 @@ import {
readFileTimestamps,
} from '@/shared/utils.js';
import type { IProviderSessionSynchronizer } from '@/shared/interfaces.js';
+import type { LLMProvider } from '@/shared/types.js';
type ParsedSession = {
sessionId: string;
projectPath: string;
+ provider: LLMProvider;
sessionName?: string;
};
@@ -22,7 +25,6 @@ type ParsedSession = {
* Session indexer for Claude transcript artifacts.
*/
export class ClaudeSessionSynchronizer implements IProviderSessionSynchronizer {
- private readonly provider = 'claude' as const;
private readonly claudeHome = path.join(os.homedir(), '.claude');
/**
@@ -65,7 +67,7 @@ export class ClaudeSessionSynchronizer implements IProviderSessionSynchronizer {
const timestamps = await readFileTimestamps(filePath);
sessionsDb.createSession(
parsed.sessionId,
- this.provider,
+ parsed.provider,
parsed.projectPath,
parsed.sessionName,
timestamps.createdAt,
@@ -98,7 +100,7 @@ export class ClaudeSessionSynchronizer implements IProviderSessionSynchronizer {
const timestamps = await readFileTimestamps(filePath);
return sessionsDb.createSession(
parsed.sessionId,
- this.provider,
+ parsed.provider,
parsed.projectPath,
parsed.sessionName,
timestamps.createdAt,
@@ -137,11 +139,18 @@ export class ClaudeSessionSynchronizer implements IProviderSessionSynchronizer {
// ids must be resolved through the provider-id mapping first.
const existingSession = sessionsDb.getSessionByProviderSessionId(parsed.sessionId)
?? sessionsDb.getSessionById(parsed.sessionId);
+ const provider = existingSession?.provider === 'minimax'
+ ? 'minimax'
+ : await this.detectCompatibleProvider(filePath);
+ const defaultSessionName = provider === 'minimax'
+ ? 'Untitled MiniMax Session'
+ : 'Untitled Claude Session';
const existingSessionName = existingSession?.custom_name;
- if (existingSessionName && existingSessionName !== 'Untitled Claude Session') {
+ if (existingSessionName && existingSessionName !== defaultSessionName) {
return {
...parsed,
- sessionName: normalizeSessionName(existingSessionName, 'Untitled Claude Session'),
+ provider,
+ sessionName: normalizeSessionName(existingSessionName, defaultSessionName),
};
}
@@ -152,10 +161,41 @@ export class ClaudeSessionSynchronizer implements IProviderSessionSynchronizer {
return {
...parsed,
- sessionName: normalizeSessionName(sessionName, 'Untitled Claude Session'),
+ provider,
+ sessionName: normalizeSessionName(sessionName, defaultSessionName),
};
}
+ private async detectCompatibleProvider(filePath: string): Promise {
+ try {
+ const content = await readFile(filePath, 'utf8');
+ for (const line of content.split(/\r?\n/)) {
+ if (!line.trim()) {
+ continue;
+ }
+
+ let parsed: unknown;
+ try {
+ parsed = JSON.parse(line);
+ } catch {
+ continue;
+ }
+
+ const data = parsed as Record;
+ const message = data.message && typeof data.message === 'object'
+ ? data.message as Record
+ : null;
+ if (isMiniMaxModel(data.model) || isMiniMaxModel(message?.model)) {
+ return 'minimax';
+ }
+ }
+ } catch {
+ // Unreadable transcripts remain attached to the default compatible provider.
+ }
+
+ return 'claude';
+ }
+
private async extractSessionAiTitleFromEnd(
filePath: string,
sessionId: string
diff --git a/server/modules/providers/list/minimax/minimax-auth.provider.ts b/server/modules/providers/list/minimax/minimax-auth.provider.ts
new file mode 100644
index 0000000000..093d44ada6
--- /dev/null
+++ b/server/modules/providers/list/minimax/minimax-auth.provider.ts
@@ -0,0 +1,48 @@
+import spawn from 'cross-spawn';
+
+import {
+ readMiniMaxSettingsEnvironment,
+ resolveMiniMaxCredential,
+} from '@/modules/providers/list/minimax/minimax-config.js';
+import { resolveClaudeCodeExecutablePath } from '@/shared/claude-cli-path.js';
+import type { IProviderAuth } from '@/shared/interfaces.js';
+import type { ProviderAuthStatus } from '@/shared/types.js';
+
+export class MiniMaxProviderAuth implements IProviderAuth {
+ private checkInstalled(): boolean {
+ const cliPath = resolveClaudeCodeExecutablePath(process.env.CLAUDE_CLI_PATH);
+ try {
+ spawn.sync(cliPath, ['--version'], { stdio: 'ignore', timeout: 5000 });
+ return true;
+ } catch {
+ return false;
+ }
+ }
+
+ async getStatus(): Promise {
+ const installed = this.checkInstalled();
+ if (!installed) {
+ return {
+ installed,
+ provider: 'minimax',
+ authenticated: false,
+ email: null,
+ method: null,
+ error: 'The compatible coding CLI is not installed',
+ };
+ }
+
+ const settingsEnvironment = await readMiniMaxSettingsEnvironment();
+ const credential = resolveMiniMaxCredential(process.env, settingsEnvironment);
+ return {
+ installed,
+ provider: 'minimax',
+ authenticated: Boolean(credential),
+ email: credential ? 'API Key Auth' : null,
+ method: credential ? 'api_key' : null,
+ error: credential
+ ? undefined
+ : 'Configure MINIMAX_API_KEY or a MiniMax endpoint and token in the coding CLI settings.',
+ };
+ }
+}
diff --git a/server/modules/providers/list/minimax/minimax-config.ts b/server/modules/providers/list/minimax/minimax-config.ts
new file mode 100644
index 0000000000..32fd48e75c
--- /dev/null
+++ b/server/modules/providers/list/minimax/minimax-config.ts
@@ -0,0 +1,112 @@
+import { readFile } from 'node:fs/promises';
+import os from 'node:os';
+import path from 'node:path';
+
+import { readObjectRecord, readOptionalString } from '@/shared/utils.js';
+
+export const MINIMAX_ANTHROPIC_ENDPOINTS = {
+ global: 'https://api.minimax.io/anthropic',
+ cn: 'https://api.minimaxi.com/anthropic',
+} as const;
+
+type MiniMaxEnvironment = Record;
+
+const normalizeBaseUrl = (value: string): string => value.trim().replace(/\/+$/, '');
+
+export const isMiniMaxBaseUrl = (value: string | undefined): boolean => {
+ if (!value?.trim()) {
+ return false;
+ }
+
+ const normalized = normalizeBaseUrl(value);
+ return Object.values(MINIMAX_ANTHROPIC_ENDPOINTS).some(
+ (endpoint) => normalizeBaseUrl(endpoint) === normalized,
+ );
+};
+
+export const resolveMiniMaxBaseUrl = (
+ environment: MiniMaxEnvironment = process.env,
+ settingsEnvironment: MiniMaxEnvironment = {},
+): string => {
+ const explicitBaseUrl = readOptionalString(environment.MINIMAX_ANTHROPIC_BASE_URL);
+ if (explicitBaseUrl) {
+ return normalizeBaseUrl(explicitBaseUrl);
+ }
+
+ const existingBaseUrl = readOptionalString(environment.ANTHROPIC_BASE_URL);
+ if (existingBaseUrl && isMiniMaxBaseUrl(existingBaseUrl)) {
+ return normalizeBaseUrl(existingBaseUrl);
+ }
+
+ const settingsBaseUrl = readOptionalString(settingsEnvironment.ANTHROPIC_BASE_URL);
+ if (settingsBaseUrl && isMiniMaxBaseUrl(settingsBaseUrl)) {
+ return normalizeBaseUrl(settingsBaseUrl);
+ }
+
+ return environment.MINIMAX_REGION?.trim().toLowerCase() === 'cn'
+ ? MINIMAX_ANTHROPIC_ENDPOINTS.cn
+ : MINIMAX_ANTHROPIC_ENDPOINTS.global;
+};
+
+export const readMiniMaxSettingsEnvironment = async (): Promise => {
+ try {
+ const settingsPath = path.join(os.homedir(), '.claude', 'settings.json');
+ const settings = readObjectRecord(JSON.parse(await readFile(settingsPath, 'utf8')));
+ const environment = readObjectRecord(settings?.env) ?? {};
+ return Object.fromEntries(
+ Object.entries(environment).filter(
+ (entry): entry is [string, string] => typeof entry[1] === 'string',
+ ),
+ );
+ } catch {
+ return {};
+ }
+};
+
+export const resolveMiniMaxCredential = (
+ environment: MiniMaxEnvironment,
+ settingsEnvironment: MiniMaxEnvironment = {},
+): string | null => {
+ const dedicatedKey = readOptionalString(environment.MINIMAX_API_KEY);
+ if (dedicatedKey) {
+ return dedicatedKey;
+ }
+
+ const environmentBaseUrl = readOptionalString(environment.ANTHROPIC_BASE_URL);
+ if (isMiniMaxBaseUrl(environmentBaseUrl)) {
+ return readOptionalString(environment.ANTHROPIC_AUTH_TOKEN)
+ ?? readOptionalString(environment.ANTHROPIC_API_KEY)
+ ?? null;
+ }
+
+ const settingsBaseUrl = readOptionalString(settingsEnvironment.ANTHROPIC_BASE_URL);
+ if (isMiniMaxBaseUrl(settingsBaseUrl)) {
+ return readOptionalString(settingsEnvironment.ANTHROPIC_AUTH_TOKEN)
+ ?? readOptionalString(settingsEnvironment.ANTHROPIC_API_KEY)
+ ?? null;
+ }
+
+ return null;
+};
+
+export const buildMiniMaxRuntimeEnvironment = (
+ model: string,
+ contextWindow: number,
+ environment: MiniMaxEnvironment,
+ settingsEnvironment: MiniMaxEnvironment = {},
+): Record => {
+ const runtimeEnvironment: Record = {
+ ANTHROPIC_BASE_URL: resolveMiniMaxBaseUrl(environment, settingsEnvironment),
+ ANTHROPIC_MODEL: model,
+ ANTHROPIC_DEFAULT_OPUS_MODEL: model,
+ ANTHROPIC_DEFAULT_SONNET_MODEL: model,
+ ANTHROPIC_DEFAULT_HAIKU_MODEL: model,
+ CLAUDE_CODE_AUTO_COMPACT_WINDOW: String(contextWindow),
+ };
+ const credential = resolveMiniMaxCredential(environment, settingsEnvironment);
+ if (credential) {
+ runtimeEnvironment.ANTHROPIC_AUTH_TOKEN = credential;
+ }
+
+ return runtimeEnvironment;
+};
diff --git a/server/modules/providers/list/minimax/minimax-models.provider.ts b/server/modules/providers/list/minimax/minimax-models.provider.ts
new file mode 100644
index 0000000000..1709c41525
--- /dev/null
+++ b/server/modules/providers/list/minimax/minimax-models.provider.ts
@@ -0,0 +1,71 @@
+import type { IProviderModels } from '@/shared/interfaces.js';
+import type {
+ ProviderChangeActiveModelInput,
+ ProviderCurrentActiveModel,
+ ProviderModelsDefinition,
+ ProviderSessionActiveModelChange,
+} from '@/shared/types.js';
+import {
+ buildDefaultProviderCurrentActiveModel,
+ writeProviderSessionActiveModelChange,
+} from '@/shared/utils.js';
+
+export const MINIMAX_MODEL_IDS = ['MiniMax-M3', 'MiniMax-M2.7'] as const;
+
+export const MINIMAX_MODEL_CONTEXT_WINDOWS: Record<(typeof MINIMAX_MODEL_IDS)[number], number> = {
+ 'MiniMax-M3': 1_000_000,
+ 'MiniMax-M2.7': 204_800,
+};
+
+export const MINIMAX_MODELS: ProviderModelsDefinition = {
+ OPTIONS: [
+ {
+ value: MINIMAX_MODEL_IDS[0],
+ label: MINIMAX_MODEL_IDS[0],
+ description: '1,000,000-token context · $0.60 input / $2.40 output per million tokens',
+ },
+ {
+ value: MINIMAX_MODEL_IDS[1],
+ label: MINIMAX_MODEL_IDS[1],
+ description: '204,800-token context · $0.30 input / $1.20 output per million tokens',
+ },
+ ],
+ DEFAULT: MINIMAX_MODEL_IDS[0],
+};
+
+export const isMiniMaxModel = (model: unknown): boolean => {
+ if (typeof model !== 'string') {
+ return false;
+ }
+
+ const normalized = model.trim();
+ return MINIMAX_MODEL_IDS.some(
+ (modelId) => normalized === modelId || normalized.startsWith(`${modelId}[`),
+ );
+};
+
+export const resolveMiniMaxContextWindow = (model: string): number => {
+ const normalized = model.trim();
+ const modelId = MINIMAX_MODEL_IDS.find(
+ (candidate) => normalized === candidate || normalized.startsWith(`${candidate}[`),
+ );
+ return modelId
+ ? MINIMAX_MODEL_CONTEXT_WINDOWS[modelId]
+ : MINIMAX_MODEL_CONTEXT_WINDOWS[MINIMAX_MODELS.DEFAULT as keyof typeof MINIMAX_MODEL_CONTEXT_WINDOWS];
+};
+
+export class MiniMaxProviderModels implements IProviderModels {
+ async getSupportedModels(): Promise {
+ return MINIMAX_MODELS;
+ }
+
+ async getCurrentActiveModel(): Promise {
+ return buildDefaultProviderCurrentActiveModel(MINIMAX_MODELS);
+ }
+
+ async changeActiveModel(
+ input: ProviderChangeActiveModelInput,
+ ): Promise {
+ return writeProviderSessionActiveModelChange('minimax', input);
+ }
+}
diff --git a/server/modules/providers/list/minimax/minimax.provider.ts b/server/modules/providers/list/minimax/minimax.provider.ts
new file mode 100644
index 0000000000..d7180c9fed
--- /dev/null
+++ b/server/modules/providers/list/minimax/minimax.provider.ts
@@ -0,0 +1,127 @@
+import { ClaudeMcpProvider } from '@/modules/providers/list/claude/claude-mcp.provider.js';
+import { ClaudeSessionsProvider } from '@/modules/providers/list/claude/claude-sessions.provider.js';
+import { ClaudeSkillsProvider } from '@/modules/providers/list/claude/claude-skills.provider.js';
+import { MiniMaxProviderAuth } from '@/modules/providers/list/minimax/minimax-auth.provider.js';
+import { MiniMaxProviderModels } from '@/modules/providers/list/minimax/minimax-models.provider.js';
+import { AbstractProvider } from '@/modules/providers/shared/base/abstract.provider.js';
+import type {
+ IProviderMcp,
+ IProviderSessionSynchronizer,
+ IProviderSessions,
+ IProviderSkills,
+} from '@/shared/interfaces.js';
+import type {
+ FetchHistoryOptions,
+ FetchHistoryResult,
+ McpScope,
+ NormalizedMessage,
+ ProviderMcpServer,
+ ProviderSkill,
+ ProviderSkillCreateInput,
+ ProviderSkillListOptions,
+ ProviderSkillRemoveInput,
+ UpsertProviderMcpServerInput,
+} from '@/shared/types.js';
+
+const remapMcpServer = (server: ProviderMcpServer): ProviderMcpServer => ({
+ ...server,
+ provider: 'minimax',
+});
+
+class MiniMaxMcpProvider implements IProviderMcp {
+ private readonly delegate = new ClaudeMcpProvider();
+
+ async listServers(options?: { workspacePath?: string }): Promise> {
+ const grouped = await this.delegate.listServers(options);
+ return Object.fromEntries(
+ Object.entries(grouped).map(([scope, servers]) => [scope, servers.map(remapMcpServer)]),
+ ) as Record;
+ }
+
+ async listServersForScope(
+ scope: McpScope,
+ options?: { workspacePath?: string },
+ ): Promise {
+ return (await this.delegate.listServersForScope(scope, options)).map(remapMcpServer);
+ }
+
+ async upsertServer(input: UpsertProviderMcpServerInput): Promise {
+ return remapMcpServer(await this.delegate.upsertServer(input));
+ }
+
+ async removeServer(
+ input: { name: string; scope?: McpScope; workspacePath?: string },
+ ): Promise<{ removed: boolean; provider: 'minimax'; name: string; scope: McpScope }> {
+ const result = await this.delegate.removeServer(input);
+ return { ...result, provider: 'minimax' };
+ }
+}
+
+const remapSkill = (skill: ProviderSkill): ProviderSkill => ({
+ ...skill,
+ provider: 'minimax',
+});
+
+class MiniMaxSkillsProvider implements IProviderSkills {
+ private readonly delegate = new ClaudeSkillsProvider();
+
+ async listSkills(options?: ProviderSkillListOptions): Promise {
+ return (await this.delegate.listSkills(options)).map(remapSkill);
+ }
+
+ async addSkills(input: ProviderSkillCreateInput): Promise {
+ return (await this.delegate.addSkills(input)).map(remapSkill);
+ }
+
+ async removeSkill(
+ input: ProviderSkillRemoveInput,
+ ): Promise<{ removed: boolean; provider: 'minimax'; directoryName: string }> {
+ const result = await this.delegate.removeSkill(input);
+ return { ...result, provider: 'minimax' };
+ }
+}
+
+class MiniMaxSessionsProvider implements IProviderSessions {
+ private readonly delegate = new ClaudeSessionsProvider();
+
+ normalizeMessage(raw: unknown, sessionId: string | null): NormalizedMessage[] {
+ return this.delegate.normalizeMessage(raw, sessionId).map((message) => ({
+ ...message,
+ provider: 'minimax',
+ }));
+ }
+
+ async fetchHistory(
+ sessionId: string,
+ options?: FetchHistoryOptions,
+ ): Promise {
+ const result = await this.delegate.fetchHistory(sessionId, options);
+ return {
+ ...result,
+ messages: result.messages.map((message) => ({ ...message, provider: 'minimax' })),
+ };
+ }
+}
+
+class MiniMaxSessionSynchronizer implements IProviderSessionSynchronizer {
+ async synchronize(): Promise {
+ return 0;
+ }
+
+ async synchronizeFile(): Promise {
+ return null;
+ }
+}
+
+export class MiniMaxProvider extends AbstractProvider {
+ readonly models = new MiniMaxProviderModels();
+ readonly mcp = new MiniMaxMcpProvider();
+ readonly auth = new MiniMaxProviderAuth();
+ readonly skills = new MiniMaxSkillsProvider();
+ readonly sessions = new MiniMaxSessionsProvider();
+ readonly sessionSynchronizer = new MiniMaxSessionSynchronizer();
+
+ constructor() {
+ super('minimax');
+ }
+}
diff --git a/server/modules/providers/provider.registry.ts b/server/modules/providers/provider.registry.ts
index 8c333fcb72..7fe2f5bacc 100644
--- a/server/modules/providers/provider.registry.ts
+++ b/server/modules/providers/provider.registry.ts
@@ -1,6 +1,7 @@
import { ClaudeProvider } from '@/modules/providers/list/claude/claude.provider.js';
import { CodexProvider } from '@/modules/providers/list/codex/codex.provider.js';
import { CursorProvider } from '@/modules/providers/list/cursor/cursor.provider.js';
+import { MiniMaxProvider } from '@/modules/providers/list/minimax/minimax.provider.js';
import { OpenCodeProvider } from '@/modules/providers/list/opencode/opencode.provider.js';
import type { IProvider } from '@/shared/interfaces.js';
import type { LLMProvider } from '@/shared/types.js';
@@ -10,6 +11,7 @@ const providers: Record = {
claude: new ClaudeProvider(),
codex: new CodexProvider(),
cursor: new CursorProvider(),
+ minimax: new MiniMaxProvider(),
opencode: new OpenCodeProvider(),
};
diff --git a/server/modules/providers/provider.routes.ts b/server/modules/providers/provider.routes.ts
index 2352f3a141..98f29294b9 100644
--- a/server/modules/providers/provider.routes.ts
+++ b/server/modules/providers/provider.routes.ts
@@ -285,6 +285,7 @@ const parseProvider = (value: unknown): LLMProvider => {
normalized === 'claude'
|| normalized === 'codex'
|| normalized === 'cursor'
+ || normalized === 'minimax'
|| normalized === 'opencode'
) {
return normalized;
diff --git a/server/modules/providers/services/provider-capabilities.service.ts b/server/modules/providers/services/provider-capabilities.service.ts
index a1446062f2..aca5f131b6 100644
--- a/server/modules/providers/services/provider-capabilities.service.ts
+++ b/server/modules/providers/services/provider-capabilities.service.ts
@@ -62,6 +62,16 @@ const PROVIDER_CAPABILITIES: Record = {
supportsTokenUsage: true,
supportsEffort: true,
},
+ minimax: {
+ provider: 'minimax',
+ permissionModes: ['default', 'auto', 'acceptEdits', 'bypassPermissions', 'plan'],
+ defaultPermissionMode: 'default',
+ supportsImages: true,
+ supportsAbort: true,
+ supportsPermissionRequests: true,
+ supportsTokenUsage: true,
+ supportsEffort: false,
+ },
opencode: {
provider: 'opencode',
// Mapped by the runtime onto OpenCode's controls: `--agent plan` (plan),
diff --git a/server/modules/providers/services/session-conversations-search.service.ts b/server/modules/providers/services/session-conversations-search.service.ts
index 1854eaddc9..f85170e8ca 100644
--- a/server/modules/providers/services/session-conversations-search.service.ts
+++ b/server/modules/providers/services/session-conversations-search.service.ts
@@ -8,7 +8,7 @@ import { rgPath } from '@vscode/ripgrep';
import { projectsDb, sessionsDb } from '@/modules/database/index.js';
type AnyRecord = Record;
-type SearchableProvider = 'claude' | 'codex';
+type SearchableProvider = 'claude' | 'codex' | 'minimax';
type SearchSnippetHighlight = {
start: number;
@@ -82,7 +82,7 @@ type ProjectBucket = {
sessions: SearchableSessionRow[];
};
-const SUPPORTED_PROVIDERS = new Set(['claude', 'codex']);
+const SUPPORTED_PROVIDERS = new Set(['claude', 'codex', 'minimax']);
const MAX_MATCHES_PER_SESSION = 2;
const RIPGREP_FILE_CHUNK_SIZE = 40;
const RIPGREP_CHUNK_CONCURRENCY = 6;
@@ -789,8 +789,10 @@ async function parseClaudeSessionMatches(
const targetSessionIds = new Set(targetSessions.map((candidate) => candidate.session_id));
const customNameBySessionId = new Map();
+ const providerBySessionId = new Map();
for (const candidate of targetSessions) {
customNameBySessionId.set(candidate.session_id, candidate.custom_name ?? null);
+ providerBySessionId.set(candidate.session_id, candidate.provider);
}
type ClaudeSessionSearchState = {
@@ -897,7 +899,7 @@ async function parseClaudeSessionMatches(
snippet,
highlights,
timestamp: entry.timestamp ? String(entry.timestamp) : null,
- provider: 'claude',
+ provider: providerBySessionId.get(entrySessionId) ?? session.provider,
messageUuid: entry.uuid ? String(entry.uuid) : null,
});
}
@@ -914,7 +916,7 @@ async function parseClaudeSessionMatches(
fileResults.set(sessionId, {
sessionId,
- provider: 'claude',
+ provider: providerBySessionId.get(sessionId) ?? session.provider,
sessionSummary: toSummaryText(
customNameBySessionId.get(sessionId) ?? null,
state.resolvedSummary || state.fallbackUserText || state.fallbackAssistantText,
@@ -1054,7 +1056,7 @@ async function parseSessionMatches(
session: SearchableSessionRow,
runtime: SearchRuntime,
): Promise {
- if (session.provider === 'claude') {
+ if (session.provider === 'claude' || session.provider === 'minimax') {
return parseClaudeSessionMatches(session, runtime);
}
if (session.provider === 'codex') {
@@ -1146,7 +1148,9 @@ export async function searchConversations(
};
for (const [fileKey, sessions] of sessionsByPathKey.entries()) {
- const claudeSessions = sessions.filter((session) => session.provider === 'claude');
+ const claudeSessions = sessions.filter(
+ (session) => session.provider === 'claude' || session.provider === 'minimax',
+ );
if (claudeSessions.length > 0) {
runtime.claudeSessionsByFileKey.set(fileKey, claudeSessions);
}
diff --git a/server/modules/providers/services/session-synchronizer.service.ts b/server/modules/providers/services/session-synchronizer.service.ts
index 5bb2b8645f..760975d9c0 100644
--- a/server/modules/providers/services/session-synchronizer.service.ts
+++ b/server/modules/providers/services/session-synchronizer.service.ts
@@ -21,6 +21,7 @@ export const sessionSynchronizerService = {
claude: 0,
codex: 0,
cursor: 0,
+ minimax: 0,
opencode: 0,
};
const failures: string[] = [];
diff --git a/server/modules/providers/tests/mcp.test.ts b/server/modules/providers/tests/mcp.test.ts
index 439966acd4..66fd4f769b 100644
--- a/server/modules/providers/tests/mcp.test.ts
+++ b/server/modules/providers/tests/mcp.test.ts
@@ -313,8 +313,9 @@ test('providerMcpService global adder writes to all providers and rejects unsupp
workspacePath,
});
- assert.equal(globalResult.length, 4);
+ assert.equal(globalResult.length, 5);
assert.ok(globalResult.every((entry) => entry.created === true));
+ assert.ok(globalResult.some((entry) => entry.provider === 'minimax'));
const claudeProject = await readJson(path.join(workspacePath, '.mcp.json'));
assert.ok((claudeProject.mcpServers as Record)['global-http']);
@@ -346,4 +347,3 @@ test('providerMcpService global adder writes to all providers and rejects unsupp
await fs.rm(tempRoot, { recursive: true, force: true });
}
});
-
diff --git a/server/modules/providers/tests/minimax-provider.test.ts b/server/modules/providers/tests/minimax-provider.test.ts
new file mode 100644
index 0000000000..f08309a2c8
--- /dev/null
+++ b/server/modules/providers/tests/minimax-provider.test.ts
@@ -0,0 +1,51 @@
+import assert from 'node:assert/strict';
+import test from 'node:test';
+
+import {
+ MINIMAX_ANTHROPIC_ENDPOINTS,
+ buildMiniMaxRuntimeEnvironment,
+ resolveMiniMaxBaseUrl,
+} from '@/modules/providers/list/minimax/minimax-config.js';
+import {
+ MINIMAX_MODEL_IDS,
+ MiniMaxProviderModels,
+ isMiniMaxModel,
+ resolveMiniMaxContextWindow,
+} from '@/modules/providers/list/minimax/minimax-models.provider.js';
+import { providerRegistry } from '@/modules/providers/provider.registry.js';
+
+test('MiniMax provider exposes the configured text models in priority order', async () => {
+ const models = await new MiniMaxProviderModels().getSupportedModels();
+
+ assert.deepEqual(models.OPTIONS.map((option) => option.value), [...MINIMAX_MODEL_IDS]);
+ assert.equal(models.DEFAULT, MINIMAX_MODEL_IDS[0]);
+ assert.equal(providerRegistry.resolveProvider('minimax').id, 'minimax');
+});
+
+test('MiniMax endpoint resolution supports global, China, and explicit endpoints', () => {
+ assert.equal(resolveMiniMaxBaseUrl({}), MINIMAX_ANTHROPIC_ENDPOINTS.global);
+ assert.equal(resolveMiniMaxBaseUrl({ MINIMAX_REGION: 'cn' }), MINIMAX_ANTHROPIC_ENDPOINTS.cn);
+ assert.equal(
+ resolveMiniMaxBaseUrl({ MINIMAX_ANTHROPIC_BASE_URL: `${MINIMAX_ANTHROPIC_ENDPOINTS.global}/` }),
+ MINIMAX_ANTHROPIC_ENDPOINTS.global,
+ );
+ assert.equal(
+ resolveMiniMaxBaseUrl({}, { ANTHROPIC_BASE_URL: `${MINIMAX_ANTHROPIC_ENDPOINTS.cn}/` }),
+ MINIMAX_ANTHROPIC_ENDPOINTS.cn,
+ );
+});
+
+test('MiniMax runtime environment applies the selected model and context window', () => {
+ const contextWindow = resolveMiniMaxContextWindow(MINIMAX_MODEL_IDS[1]);
+ const environment = buildMiniMaxRuntimeEnvironment(MINIMAX_MODEL_IDS[1], contextWindow, {});
+
+ assert.equal(environment.ANTHROPIC_BASE_URL, MINIMAX_ANTHROPIC_ENDPOINTS.global);
+ assert.equal(environment.ANTHROPIC_MODEL, MINIMAX_MODEL_IDS[1]);
+ assert.equal(environment.CLAUDE_CODE_AUTO_COMPACT_WINDOW, '204800');
+});
+
+test('MiniMax transcript model detection accepts the configured model variants', () => {
+ assert.equal(isMiniMaxModel(MINIMAX_MODEL_IDS[0]), true);
+ assert.equal(isMiniMaxModel(`${MINIMAX_MODEL_IDS[0]}[1m]`), true);
+ assert.equal(isMiniMaxModel('unrelated-model'), false);
+});
diff --git a/server/routes/agent.js b/server/routes/agent.js
index 1ae605b69e..dab6ce3f8a 100644
--- a/server/routes/agent.js
+++ b/server/routes/agent.js
@@ -7,6 +7,7 @@ import { promises as fs } from 'fs';
import crypto from 'crypto';
import { userDb, apiKeysDb, githubTokensDb, projectsDb } from '../modules/database/index.js';
import { queryClaudeSDK } from '../claude-sdk.js';
+import { queryMiniMaxSDK } from '../minimax-sdk.js';
import { spawnCursor } from '../cursor-cli.js';
import { queryCodex } from '../openai-codex.js';
import { spawnOpenCode } from '../opencode-cli.js';
@@ -636,7 +637,7 @@ class ResponseCollector {
* - Source for auto-generated branch names (if createBranch=true and no branchName)
* - Fallback for PR title if no commits are made
*
- * @param {string} provider - (Optional) AI provider to use. Options: 'claude' | 'cursor' | 'codex' | 'opencode'
+ * @param {string} provider - Optional supported provider identifier.
* Default: 'claude'
*
* @param {boolean} stream - (Optional) Enable Server-Sent Events (SSE) streaming for real-time updates.
@@ -759,7 +760,7 @@ class ResponseCollector {
* Input Validations (400 Bad Request):
* - Either githubUrl OR projectPath must be provided (not neither)
* - message must be non-empty string
- * - provider must be 'claude', 'cursor', 'codex', or 'opencode'
+ * - provider must be a supported provider identifier
* - createBranch/createPR requires githubUrl OR projectPath (not neither)
* - branchName must pass Git naming rules (if provided)
*
@@ -870,8 +871,8 @@ router.post('/', validateExternalApiKey, async (req, res) => {
return res.status(400).json({ error: 'message is required' });
}
- if (!['claude', 'cursor', 'codex', 'opencode'].includes(provider)) {
- return res.status(400).json({ error: 'provider must be "claude", "cursor", "codex", or "opencode"' });
+ if (!['claude', 'cursor', 'codex', 'minimax', 'opencode'].includes(provider)) {
+ return res.status(400).json({ error: 'provider is not supported' });
}
// Validate GitHub branch/PR creation requirements
@@ -950,6 +951,7 @@ router.post('/', validateExternalApiKey, async (req, res) => {
}
const codexModels = (await providerModelsService.getProviderModels('codex')).models;
+ const minimaxModels = (await providerModelsService.getProviderModels('minimax')).models;
const opencodeModels = (await providerModelsService.getProviderModels('opencode')).models;
// Start the appropriate session
@@ -986,6 +988,16 @@ router.post('/', validateExternalApiKey, async (req, res) => {
effort,
permissionMode: 'bypassPermissions'
}, writer);
+ } else if (provider === 'minimax') {
+ console.log('Starting MiniMax session');
+
+ await queryMiniMaxSDK(message.trim(), {
+ projectPath: finalProjectPath,
+ cwd: finalProjectPath,
+ sessionId: sessionId || null,
+ model: model || minimaxModels.DEFAULT,
+ permissionMode: 'bypassPermissions'
+ }, writer);
} else if (provider === 'opencode') {
console.log('Starting OpenCode CLI session');
diff --git a/server/routes/commands.js b/server/routes/commands.js
index 722fd545bc..6592181f22 100644
--- a/server/routes/commands.js
+++ b/server/routes/commands.js
@@ -15,12 +15,13 @@ const APP_ROOT = findAppRoot(__dirname);
const router = express.Router();
-const MODEL_PROVIDERS = ["claude", "cursor", "codex", "opencode"];
+const MODEL_PROVIDERS = ["claude", "cursor", "codex", "minimax", "opencode"];
const MODEL_PROVIDER_LABELS = {
claude: "Claude",
cursor: "Cursor",
codex: "Codex",
+ minimax: "MiniMax",
opencode: "OpenCode",
};
diff --git a/server/shared/types.ts b/server/shared/types.ts
index 45092ecf73..8aef73bb67 100644
--- a/server/shared/types.ts
+++ b/server/shared/types.ts
@@ -65,7 +65,7 @@ export type AuthenticatedWebSocketRequest = IncomingMessage & {
* Use this as the source of truth whenever a function or payload needs to identify
* a specific LLM integration.
*/
-export type LLMProvider = 'claude' | 'codex' | 'cursor' | 'opencode';
+export type LLMProvider = 'claude' | 'codex' | 'cursor' | 'minimax' | 'opencode';
/**
* One selectable model row in a provider model catalog.
diff --git a/src/components/chat/hooks/useChatComposerState.ts b/src/components/chat/hooks/useChatComposerState.ts
index 7543ebae77..3e11dc9855 100644
--- a/src/components/chat/hooks/useChatComposerState.ts
+++ b/src/components/chat/hooks/useChatComposerState.ts
@@ -41,11 +41,8 @@ interface UseChatComposerStateArgs {
permissionMode: PermissionMode | string;
cyclePermissionMode: () => void;
resolvePermissionModeForProvider: (provider: LLMProvider, requestedMode: PermissionMode | string) => PermissionMode;
- cursorModel: string;
- claudeModel: string;
- codexModel: string;
+ providerModels: Record;
currentProviderEffort: string;
- opencodeModel: string;
isLoading: boolean;
canAbortSession: boolean;
tokenBudget: Record | null;
@@ -193,11 +190,8 @@ export function useChatComposerState({
permissionMode,
cyclePermissionMode,
resolvePermissionModeForProvider,
- cursorModel,
- claudeModel,
- codexModel,
+ providerModels,
currentProviderEffort,
- opencodeModel,
isLoading,
canAbortSession,
tokenBudget,
@@ -369,13 +363,7 @@ export function useChatComposerState({
projectId: selectedProject.projectId,
sessionId: currentSessionId,
provider,
- model: provider === 'cursor'
- ? cursorModel
- : provider === 'codex'
- ? codexModel
- : provider === 'opencode'
- ? opencodeModel
- : claudeModel,
+ model: providerModels[provider],
tokenUsage: tokenBudget,
};
@@ -424,15 +412,12 @@ export function useChatComposerState({
}
},
[
- claudeModel,
- codexModel,
currentSessionId,
- cursorModel,
- opencodeModel,
handleBuiltInCommand,
handleCustomCommand,
input,
provider,
+ providerModels,
selectedProject,
addMessage,
tokenBudget,
@@ -595,6 +580,8 @@ export function useChatComposerState({
? 'cursor-tools-settings'
: provider === 'codex'
? 'codex-settings'
+ : provider === 'minimax'
+ ? 'claude-settings'
: provider === 'opencode'
? 'opencode-settings'
: 'claude-settings';
@@ -614,14 +601,7 @@ export function useChatComposerState({
};
const toolsSettings = getToolsSettings();
- const model =
- provider === 'cursor'
- ? cursorModel
- : provider === 'codex'
- ? codexModel
- : provider === 'opencode'
- ? opencodeModel
- : claudeModel;
+ const model = providerModels[provider];
return {
model,
@@ -632,13 +612,10 @@ export function useChatComposerState({
sessionSummary: getNotificationSessionSummary(selectedSession, currentInput),
};
}, [
- claudeModel,
- codexModel,
currentProviderEffort,
- cursorModel,
- opencodeModel,
permissionMode,
provider,
+ providerModels,
resolvePermissionModeForProvider,
selectedSession,
]);
diff --git a/src/components/chat/hooks/useChatProviderState.ts b/src/components/chat/hooks/useChatProviderState.ts
index 1d0aac5af5..e226efa20a 100644
--- a/src/components/chat/hooks/useChatProviderState.ts
+++ b/src/components/chat/hooks/useChatProviderState.ts
@@ -20,10 +20,11 @@ const FALLBACK_DEFAULT_MODEL: Record = {
claude: 'default',
cursor: 'gpt-5.3-codex',
codex: 'gpt-5.4',
+ minimax: 'MiniMax-M3',
opencode: 'anthropic/claude-sonnet-4-5',
};
-const PROVIDERS: LLMProvider[] = ['claude', 'cursor', 'codex', 'opencode'];
+const PROVIDERS: LLMProvider[] = ['claude', 'cursor', 'codex', 'minimax', 'opencode'];
const readStoredProvider = (): LLMProvider => {
const storedProvider = localStorage.getItem('selected-provider');
@@ -42,6 +43,7 @@ const FALLBACK_PERMISSION_MODES: Record = {
claude: ['default', 'auto', 'acceptEdits', 'bypassPermissions', 'plan'],
cursor: ['default', 'acceptEdits', 'bypassPermissions', 'plan'],
codex: ['default', 'acceptEdits', 'bypassPermissions'],
+ minimax: ['default', 'auto', 'acceptEdits', 'bypassPermissions', 'plan'],
opencode: ['default', 'acceptEdits', 'bypassPermissions', 'plan'],
};
@@ -100,6 +102,9 @@ export function useChatProviderState({ selectedSession, selectedProject: _select
const [codexModel, setCodexModel] = useState(() => {
return localStorage.getItem('codex-model') || FALLBACK_DEFAULT_MODEL.codex;
});
+ const [minimaxModel, setMiniMaxModel] = useState(() => {
+ return localStorage.getItem('minimax-model') || FALLBACK_DEFAULT_MODEL.minimax;
+ });
const [providerEfforts, setProviderEfforts] = useState>>(() => {
return PROVIDERS.reduce>>((acc, targetProvider) => {
acc[targetProvider] = localStorage.getItem(`${targetProvider}-effort`) || DEFAULT_EFFORT_VALUE;
@@ -151,6 +156,12 @@ export function useChatProviderState({ selectedSession, selectedProject: _select
return;
}
+ if (targetProvider === 'minimax') {
+ setMiniMaxModel(model);
+ localStorage.setItem('minimax-model', model);
+ return;
+ }
+
setOpenCodeModel(model);
localStorage.setItem('opencode-model', model);
}, []);
@@ -354,8 +365,9 @@ export function useChatProviderState({ selectedSession, selectedProject: _select
claude: claudeModel,
cursor: cursorModel,
codex: codexModel,
+ minimax: minimaxModel,
opencode: opencodeModel,
- }), [claudeModel, cursorModel, codexModel, opencodeModel]);
+ }), [claudeModel, cursorModel, codexModel, minimaxModel, opencodeModel]);
useEffect(() => {
const claude = providerModelCatalog.claude;
@@ -396,6 +408,19 @@ export function useChatProviderState({ selectedSession, selectedProject: _select
}
}, [providerModelCatalog.codex, codexModel]);
+ useEffect(() => {
+ const minimax = providerModelCatalog.minimax;
+ if (minimax) {
+ const next = pickStoredOrCurrent('minimax-model', minimaxModel, minimax);
+ if (next !== minimaxModel) {
+ setMiniMaxModel(next);
+ }
+ if (localStorage.getItem('minimax-model') !== next) {
+ localStorage.setItem('minimax-model', next);
+ }
+ }
+ }, [providerModelCatalog.minimax, minimaxModel]);
+
useEffect(() => {
const opencode = providerModelCatalog.opencode;
if (opencode) {
@@ -567,10 +592,14 @@ export function useChatProviderState({ selectedSession, selectedProject: _select
setClaudeModel,
codexModel,
setCodexModel,
+ minimaxModel,
+ setMiniMaxModel,
currentProviderEffort,
currentProviderEffortOptions,
opencodeModel,
setOpenCodeModel,
+ providerModels,
+ setStoredProviderModel,
permissionMode,
setPermissionMode,
pendingPermissionRequests,
diff --git a/src/components/chat/view/ChatInterface.tsx b/src/components/chat/view/ChatInterface.tsx
index 2d7a8dcbb7..33f62c1de2 100644
--- a/src/components/chat/view/ChatInterface.tsx
+++ b/src/components/chat/view/ChatInterface.tsx
@@ -63,16 +63,10 @@ function ChatInterface({
const {
provider,
setProvider,
- cursorModel,
- setCursorModel,
- claudeModel,
- setClaudeModel,
- codexModel,
- setCodexModel,
+ providerModels,
+ setStoredProviderModel,
currentProviderEffort,
currentProviderEffortOptions,
- opencodeModel,
- setOpenCodeModel,
permissionMode,
pendingPermissionRequests,
setPendingPermissionRequests,
@@ -198,11 +192,8 @@ function ChatInterface({
provider,
permissionMode,
cyclePermissionMode,
- cursorModel,
- claudeModel,
- codexModel,
+ providerModels,
currentProviderEffort,
- opencodeModel,
isLoading: isProcessing,
canAbortSession,
tokenBudget,
@@ -299,6 +290,8 @@ function ChatInterface({
? t('messageTypes.codex')
: provider === 'opencode'
? t('messageTypes.opencode', { defaultValue: 'OpenCode' })
+ : provider === 'minimax'
+ ? t('messageTypes.minimax', { defaultValue: 'MiniMax' })
: t('messageTypes.claude');
return (
@@ -331,14 +324,8 @@ function ChatInterface({
provider={provider}
setProvider={(nextProvider) => setProvider(nextProvider as Provider)}
textareaRef={textareaRef}
- claudeModel={claudeModel}
- setClaudeModel={setClaudeModel}
- cursorModel={cursorModel}
- setCursorModel={setCursorModel}
- codexModel={codexModel}
- setCodexModel={setCodexModel}
- opencodeModel={opencodeModel}
- setOpenCodeModel={setOpenCodeModel}
+ providerModels={providerModels}
+ setProviderModel={setStoredProviderModel}
providerModelCatalog={providerModelCatalog}
providerModelsLoading={providerModelsLoading}
tasksEnabled={tasksEnabled}
@@ -446,6 +433,8 @@ function ChatInterface({
? t('messageTypes.codex')
: provider === 'opencode'
? t('messageTypes.opencode', { defaultValue: 'OpenCode' })
+ : provider === 'minimax'
+ ? t('messageTypes.minimax', { defaultValue: 'MiniMax' })
: t('messageTypes.claude'),
})}
isTextareaExpanded={isTextareaExpanded}
diff --git a/src/components/chat/view/subcomponents/ChatMessagesPane.tsx b/src/components/chat/view/subcomponents/ChatMessagesPane.tsx
index 23ffaef34e..e0f4dba4fc 100644
--- a/src/components/chat/view/subcomponents/ChatMessagesPane.tsx
+++ b/src/components/chat/view/subcomponents/ChatMessagesPane.tsx
@@ -32,14 +32,8 @@ interface ChatMessagesPaneProps {
provider: LLMProvider;
setProvider: (provider: LLMProvider) => void;
textareaRef: RefObject;
- claudeModel: string;
- setClaudeModel: (model: string) => void;
- cursorModel: string;
- setCursorModel: (model: string) => void;
- codexModel: string;
- setCodexModel: (model: string) => void;
- opencodeModel: string;
- setOpenCodeModel: (model: string) => void;
+ providerModels: Record;
+ setProviderModel: (provider: LLMProvider, model: string) => void;
providerModelCatalog: Partial>;
providerModelsLoading: boolean;
tasksEnabled: boolean;
@@ -80,14 +74,8 @@ function ChatMessagesPane({
provider,
setProvider,
textareaRef,
- claudeModel,
- setClaudeModel,
- cursorModel,
- setCursorModel,
- codexModel,
- setCodexModel,
- opencodeModel,
- setOpenCodeModel,
+ providerModels,
+ setProviderModel,
providerModelCatalog,
providerModelsLoading,
tasksEnabled,
@@ -179,14 +167,8 @@ function ChatMessagesPane({
provider={provider}
setProvider={setProvider}
textareaRef={textareaRef}
- claudeModel={claudeModel}
- setClaudeModel={setClaudeModel}
- cursorModel={cursorModel}
- setCursorModel={setCursorModel}
- codexModel={codexModel}
- setCodexModel={setCodexModel}
- opencodeModel={opencodeModel}
- setOpenCodeModel={setOpenCodeModel}
+ providerModels={providerModels}
+ setProviderModel={setProviderModel}
providerModelCatalog={providerModelCatalog}
providerModelsLoading={providerModelsLoading}
tasksEnabled={tasksEnabled}
diff --git a/src/components/chat/view/subcomponents/CommandResultModal.tsx b/src/components/chat/view/subcomponents/CommandResultModal.tsx
index 5096734db6..84b0adbc59 100644
--- a/src/components/chat/view/subcomponents/CommandResultModal.tsx
+++ b/src/components/chat/view/subcomponents/CommandResultModal.tsx
@@ -61,6 +61,7 @@ const PROVIDER_LABELS: Record = {
claude: 'Claude',
cursor: 'Cursor',
codex: 'Codex',
+ minimax: 'MiniMax',
opencode: 'OpenCode',
};
diff --git a/src/components/chat/view/subcomponents/MessageComponent.tsx b/src/components/chat/view/subcomponents/MessageComponent.tsx
index 61cf8ee37b..c09332b6f3 100644
--- a/src/components/chat/view/subcomponents/MessageComponent.tsx
+++ b/src/components/chat/view/subcomponents/MessageComponent.tsx
@@ -154,6 +154,8 @@ const MessageComponent = memo(({ message, prevMessage, createDiff, onFileOpen, s
? t('messageTypes.cursor')
: provider === 'codex'
? t('messageTypes.codex')
+ : provider === 'minimax'
+ ? t('messageTypes.minimax', { defaultValue: 'MiniMax' })
: provider === 'opencode'
? t('messageTypes.opencode', { defaultValue: 'OpenCode' })
: t('messageTypes.claude'))}
@@ -401,4 +403,3 @@ const MessageComponent = memo(({ message, prevMessage, createDiff, onFileOpen, s
});
export default MessageComponent;
-
diff --git a/src/components/chat/view/subcomponents/ProviderSelectionEmptyState.tsx b/src/components/chat/view/subcomponents/ProviderSelectionEmptyState.tsx
index 6772ae09b9..3b72e91fae 100644
--- a/src/components/chat/view/subcomponents/ProviderSelectionEmptyState.tsx
+++ b/src/components/chat/view/subcomponents/ProviderSelectionEmptyState.tsx
@@ -27,6 +27,7 @@ const PROVIDER_META: { id: LLMProvider; name: string }[] = [
{ id: "claude", name: "Anthropic" },
{ id: "codex", name: "OpenAI" },
{ id: "cursor", name: "Cursor" },
+ { id: "minimax", name: "MiniMax" },
{ id: "opencode", name: "OpenCode" },
];
@@ -50,14 +51,8 @@ type ProviderSelectionEmptyStateProps = {
provider: LLMProvider;
setProvider: (next: LLMProvider) => void;
textareaRef: React.RefObject;
- claudeModel: string;
- setClaudeModel: (model: string) => void;
- cursorModel: string;
- setCursorModel: (model: string) => void;
- codexModel: string;
- setCodexModel: (model: string) => void;
- opencodeModel: string;
- setOpenCodeModel: (model: string) => void;
+ providerModels: Record;
+ setProviderModel: (provider: LLMProvider, model: string) => void;
providerModelCatalog: Partial>;
providerModelsLoading: boolean;
tasksEnabled: boolean;
@@ -80,23 +75,11 @@ function getModelConfig(
return entry ?? { OPTIONS: [], DEFAULT: "" };
}
-function getCurrentModel(
- p: LLMProvider,
- c: string,
- cu: string,
- co: string,
- o: string,
-) {
- if (p === "claude") return c;
- if (p === "codex") return co;
- if (p === "opencode") return o;
- return cu;
-}
-
function getProviderDisplayName(p: LLMProvider) {
if (p === "claude") return "Claude";
if (p === "cursor") return "Cursor";
if (p === "codex") return "Codex";
+ if (p === "minimax") return "MiniMax";
if (p === "opencode") return "OpenCode";
return "Claude";
}
@@ -107,14 +90,8 @@ export default function ProviderSelectionEmptyState({
provider,
setProvider,
textareaRef,
- claudeModel,
- setClaudeModel,
- cursorModel,
- setCursorModel,
- codexModel,
- setCodexModel,
- opencodeModel,
- setOpenCodeModel,
+ providerModels,
+ setProviderModel,
providerModelCatalog,
providerModelsLoading,
tasksEnabled,
@@ -137,13 +114,7 @@ export default function ProviderSelectionEmptyState({
defaultValue: "Start the next task",
});
- const currentModel = getCurrentModel(
- provider,
- claudeModel,
- cursorModel,
- codexModel,
- opencodeModel,
- );
+ const currentModel = providerModels[provider];
const currentModelLabel = useMemo(() => {
const config = getModelConfig(provider, providerModelCatalog);
@@ -155,21 +126,9 @@ export default function ProviderSelectionEmptyState({
const setModelForProvider = useCallback(
(providerId: LLMProvider, modelValue: string) => {
- if (providerId === "claude") {
- setClaudeModel(modelValue);
- localStorage.setItem("claude-model", modelValue);
- } else if (providerId === "codex") {
- setCodexModel(modelValue);
- localStorage.setItem("codex-model", modelValue);
- } else if (providerId === "opencode") {
- setOpenCodeModel(modelValue);
- localStorage.setItem("opencode-model", modelValue);
- } else {
- setCursorModel(modelValue);
- localStorage.setItem("cursor-model", modelValue);
- }
+ setProviderModel(providerId, modelValue);
},
- [setClaudeModel, setCursorModel, setCodexModel, setOpenCodeModel],
+ [setProviderModel],
);
const handleModelSelect = useCallback(
@@ -304,16 +263,20 @@ export default function ProviderSelectionEmptyState({
{
{
claude: t("providerSelection.readyPrompt.claude", {
- model: claudeModel,
+ model: providerModels.claude,
}),
cursor: t("providerSelection.readyPrompt.cursor", {
- model: cursorModel,
+ model: providerModels.cursor,
}),
codex: t("providerSelection.readyPrompt.codex", {
- model: codexModel,
+ model: providerModels.codex,
+ }),
+ minimax: t("providerSelection.readyPrompt.minimax", {
+ model: providerModels.minimax,
+ defaultValue: "Ready with MiniMax {{model}}",
}),
opencode: t("providerSelection.readyPrompt.opencode", {
- model: opencodeModel,
+ model: providerModels.opencode,
defaultValue: "Ready with OpenCode {{model}}",
}),
}[provider]
diff --git a/src/components/llm-logo-provider/MiniMaxLogo.tsx b/src/components/llm-logo-provider/MiniMaxLogo.tsx
new file mode 100644
index 0000000000..9c10f63c89
--- /dev/null
+++ b/src/components/llm-logo-provider/MiniMaxLogo.tsx
@@ -0,0 +1,25 @@
+type MiniMaxLogoProps = {
+ className?: string;
+};
+
+const MiniMaxLogo = ({ className = 'w-5 h-5' }: MiniMaxLogoProps) => (
+
+);
+
+export default MiniMaxLogo;
diff --git a/src/components/llm-logo-provider/SessionProviderLogo.tsx b/src/components/llm-logo-provider/SessionProviderLogo.tsx
index b93d9eb56e..cc916c4501 100644
--- a/src/components/llm-logo-provider/SessionProviderLogo.tsx
+++ b/src/components/llm-logo-provider/SessionProviderLogo.tsx
@@ -2,6 +2,7 @@ import type { LLMProvider } from '../../types/app';
import ClaudeLogo from './ClaudeLogo';
import CodexLogo from './CodexLogo';
import CursorLogo from './CursorLogo';
+import MiniMaxLogo from './MiniMaxLogo';
import OpenCodeLogo from './OpenCodeLogo';
type SessionProviderLogoProps = {
@@ -25,5 +26,9 @@ export default function SessionProviderLogo({
return ;
}
+ if (provider === 'minimax') {
+ return ;
+ }
+
return ;
}
diff --git a/src/components/mcp/constants.ts b/src/components/mcp/constants.ts
index 5a1848d6f2..4a4713e779 100644
--- a/src/components/mcp/constants.ts
+++ b/src/components/mcp/constants.ts
@@ -4,6 +4,7 @@ export const MCP_PROVIDER_NAMES: Record = {
claude: 'Claude',
cursor: 'Cursor',
codex: 'Codex',
+ minimax: 'MiniMax',
opencode: 'OpenCode',
};
@@ -11,6 +12,7 @@ export const MCP_SUPPORTED_SCOPES: Record = {
claude: ['user', 'project', 'local'],
cursor: ['user', 'project'],
codex: ['user', 'project'],
+ minimax: ['user', 'project', 'local'],
opencode: ['user', 'project'],
};
@@ -18,6 +20,7 @@ export const MCP_SUPPORTED_TRANSPORTS: Record = {
claude: ['stdio', 'http', 'sse'],
cursor: ['stdio', 'http'],
codex: ['stdio', 'http'],
+ minimax: ['stdio', 'http', 'sse'],
opencode: ['stdio', 'http'],
};
@@ -29,6 +32,7 @@ export const MCP_PROVIDER_BUTTON_CLASSES: Record = {
claude: 'bg-primary text-primary-foreground hover:bg-primary/90',
cursor: 'bg-primary text-primary-foreground hover:bg-primary/90',
codex: 'bg-primary text-primary-foreground hover:bg-primary/90',
+ minimax: 'bg-primary text-primary-foreground hover:bg-primary/90',
opencode: 'bg-primary text-primary-foreground hover:bg-primary/90',
};
@@ -36,6 +40,7 @@ export const MCP_SUPPORTS_WORKING_DIRECTORY: Record = {
claude: false,
cursor: false,
codex: true,
+ minimax: false,
opencode: false,
};
diff --git a/src/components/onboarding/view/subcomponents/AgentConnectionsStep.tsx b/src/components/onboarding/view/subcomponents/AgentConnectionsStep.tsx
index d9148a9559..0a8f771714 100644
--- a/src/components/onboarding/view/subcomponents/AgentConnectionsStep.tsx
+++ b/src/components/onboarding/view/subcomponents/AgentConnectionsStep.tsx
@@ -30,6 +30,13 @@ const providerCards = [
iconContainerClassName: 'bg-gray-100 dark:bg-gray-800',
loginButtonClassName: 'bg-gray-800 hover:bg-gray-900 dark:bg-gray-700 dark:hover:bg-gray-600',
},
+ {
+ provider: 'minimax' as const,
+ title: 'MiniMax',
+ connectedClassName: 'bg-red-50 dark:bg-red-900/20 border-red-200 dark:border-red-800',
+ iconContainerClassName: 'bg-red-100 dark:bg-red-900/30',
+ loginButtonClassName: 'bg-red-600 hover:bg-red-700',
+ },
{
provider: 'opencode' as const,
title: 'OpenCode',
diff --git a/src/components/provider-auth/types.ts b/src/components/provider-auth/types.ts
index 5511c1848f..44e5714109 100644
--- a/src/components/provider-auth/types.ts
+++ b/src/components/provider-auth/types.ts
@@ -10,12 +10,13 @@ export type ProviderAuthStatus = {
export type ProviderAuthStatusMap = Record;
-export const CLI_PROVIDERS: LLMProvider[] = ['claude', 'cursor', 'codex', 'opencode'];
+export const CLI_PROVIDERS: LLMProvider[] = ['claude', 'cursor', 'codex', 'minimax', 'opencode'];
export const PROVIDER_AUTH_STATUS_ENDPOINTS: Record = {
claude: '/api/providers/claude/auth/status',
cursor: '/api/providers/cursor/auth/status',
codex: '/api/providers/codex/auth/status',
+ minimax: '/api/providers/minimax/auth/status',
opencode: '/api/providers/opencode/auth/status',
};
@@ -23,5 +24,6 @@ export const createInitialProviderAuthStatusMap = (loading = true): ProviderAuth
claude: { authenticated: false, email: null, method: null, error: null, loading },
cursor: { authenticated: false, email: null, method: null, error: null, loading },
codex: { authenticated: false, email: null, method: null, error: null, loading },
+ minimax: { authenticated: false, email: null, method: null, error: null, loading },
opencode: { authenticated: false, email: null, method: null, error: null, loading },
});
diff --git a/src/components/provider-auth/view/ProviderLoginModal.tsx b/src/components/provider-auth/view/ProviderLoginModal.tsx
index 2a0cfc2ae0..386352d14d 100644
--- a/src/components/provider-auth/view/ProviderLoginModal.tsx
+++ b/src/components/provider-auth/view/ProviderLoginModal.tsx
@@ -41,6 +41,10 @@ const getProviderCommand = ({
return 'opencode auth login';
}
+ if (provider === 'minimax') {
+ return 'claude --version';
+ }
+
return 'claude --dangerously-skip-permissions /login';
};
@@ -49,6 +53,7 @@ const getProviderTitle = (provider: LLMProvider) => {
if (provider === 'cursor') return 'Cursor CLI Login';
if (provider === 'codex') return 'Codex CLI Login';
if (provider === 'opencode') return 'OpenCode CLI Login';
+ if (provider === 'minimax') return 'MiniMax Configuration';
return 'Claude CLI Login';
};
diff --git a/src/components/settings/constants/constants.ts b/src/components/settings/constants/constants.ts
index 8c429083df..dc73f0078c 100644
--- a/src/components/settings/constants/constants.ts
+++ b/src/components/settings/constants/constants.ts
@@ -39,7 +39,7 @@ export const SETTINGS_MAIN_TABS: SettingsMainTabMeta[] = [
{ id: 'about', label: 'About', keywords: 'about version info', icon: Info },
];
-export const AGENT_PROVIDERS: AgentProvider[] = ['claude', 'cursor', 'codex', 'opencode'];
+export const AGENT_PROVIDERS: AgentProvider[] = ['claude', 'cursor', 'codex', 'minimax', 'opencode'];
export const AGENT_CATEGORIES: AgentCategory[] = ['account', 'permissions', 'mcp'];
export const DEFAULT_PROJECT_SORT_ORDER: ProjectSortOrder = 'name';
diff --git a/src/components/settings/view/tabs/agents-settings/AgentListItem.tsx b/src/components/settings/view/tabs/agents-settings/AgentListItem.tsx
index 52e87c3272..688ad72812 100644
--- a/src/components/settings/view/tabs/agents-settings/AgentListItem.tsx
+++ b/src/components/settings/view/tabs/agents-settings/AgentListItem.tsx
@@ -12,7 +12,7 @@ type AgentListItemProps = {
type AgentConfig = {
name: string;
- color: 'blue' | 'purple' | 'gray' | 'zinc';
+ color: 'blue' | 'purple' | 'gray' | 'red' | 'zinc';
};
const agentConfig: Record = {
@@ -28,6 +28,10 @@ const agentConfig: Record = {
name: 'Codex',
color: 'gray',
},
+ minimax: {
+ name: 'MiniMax',
+ color: 'red',
+ },
opencode: {
name: 'OpenCode',
color: 'zinc',
@@ -44,6 +48,9 @@ const colorClasses = {
gray: {
dot: 'bg-foreground/60',
},
+ red: {
+ dot: 'bg-red-500',
+ },
zinc: {
dot: 'bg-zinc-500',
},
diff --git a/src/components/settings/view/tabs/agents-settings/AgentsSettingsTab.tsx b/src/components/settings/view/tabs/agents-settings/AgentsSettingsTab.tsx
index 31216a103b..33c67e3b6a 100644
--- a/src/components/settings/view/tabs/agents-settings/AgentsSettingsTab.tsx
+++ b/src/components/settings/view/tabs/agents-settings/AgentsSettingsTab.tsx
@@ -27,7 +27,7 @@ export default function AgentsSettingsTab({
), [selectedAgent]);
const visibleAgents = useMemo(() => {
- return ['claude', 'cursor', 'codex', 'opencode'];
+ return ['claude', 'cursor', 'codex', 'minimax', 'opencode'];
}, []);
const agentContextById = useMemo>(() => ({
@@ -43,6 +43,10 @@ export default function AgentsSettingsTab({
authStatus: providerAuthStatus.codex,
onLogin: () => onProviderLogin('codex'),
},
+ minimax: {
+ authStatus: providerAuthStatus.minimax,
+ onLogin: () => onProviderLogin('minimax'),
+ },
opencode: {
authStatus: providerAuthStatus.opencode,
onLogin: () => onProviderLogin('opencode'),
@@ -52,6 +56,7 @@ export default function AgentsSettingsTab({
providerAuthStatus.claude,
providerAuthStatus.codex,
providerAuthStatus.cursor,
+ providerAuthStatus.minimax,
providerAuthStatus.opencode,
]);
diff --git a/src/components/settings/view/tabs/agents-settings/sections/AgentCategoryContentSection.tsx b/src/components/settings/view/tabs/agents-settings/sections/AgentCategoryContentSection.tsx
index 5bf0de2a0b..7460e88b97 100644
--- a/src/components/settings/view/tabs/agents-settings/sections/AgentCategoryContentSection.tsx
+++ b/src/components/settings/view/tabs/agents-settings/sections/AgentCategoryContentSection.tsx
@@ -29,7 +29,7 @@ export default function AgentCategoryContentSection({
/>
)}
- {selectedCategory === 'permissions' && selectedAgent === 'claude' && (
+ {selectedCategory === 'permissions' && (selectedAgent === 'claude' || selectedAgent === 'minimax') && (
= {
claude: 'Claude',
cursor: 'Cursor',
codex: 'Codex',
+ minimax: 'MiniMax',
opencode: 'OpenCode',
};
@@ -23,6 +24,7 @@ export default function AgentSelectorSection({
const dotColor =
agent === 'claude' ? 'bg-blue-500' :
agent === 'cursor' ? 'bg-purple-500' :
+ agent === 'minimax' ? 'bg-red-500' :
agent === 'opencode' ? 'bg-zinc-500' : 'bg-foreground/60';
return (
diff --git a/src/components/settings/view/tabs/agents-settings/sections/content/AccountContent.tsx b/src/components/settings/view/tabs/agents-settings/sections/content/AccountContent.tsx
index 83fbb24910..af4415598e 100644
--- a/src/components/settings/view/tabs/agents-settings/sections/content/AccountContent.tsx
+++ b/src/components/settings/view/tabs/agents-settings/sections/content/AccountContent.tsx
@@ -45,6 +45,15 @@ const agentConfig: Record = {
subtextClass: 'text-gray-700 dark:text-gray-300',
buttonClass: 'bg-gray-800 hover:bg-gray-900 active:bg-gray-950 dark:bg-gray-700 dark:hover:bg-gray-600 dark:active:bg-gray-500',
},
+ minimax: {
+ name: 'MiniMax',
+ description: 'MiniMax coding assistant',
+ bgClass: 'bg-red-50 dark:bg-red-900/20',
+ borderClass: 'border-red-200 dark:border-red-800',
+ textClass: 'text-red-900 dark:text-red-100',
+ subtextClass: 'text-red-700 dark:text-red-300',
+ buttonClass: 'bg-red-600 hover:bg-red-700 active:bg-red-800',
+ },
opencode: {
name: 'OpenCode',
description: 'OpenCode CLI assistant',
diff --git a/src/components/skills/view/ProviderSkills.tsx b/src/components/skills/view/ProviderSkills.tsx
index 908ed07625..abf89e9bee 100644
--- a/src/components/skills/view/ProviderSkills.tsx
+++ b/src/components/skills/view/ProviderSkills.tsx
@@ -59,6 +59,7 @@ const PROVIDER_NAMES: Record = {
claude: 'Claude',
codex: 'Codex',
cursor: 'Cursor',
+ minimax: 'MiniMax',
opencode: 'OpenCode',
};
@@ -66,6 +67,7 @@ const PROVIDER_SKILL_PATHS: Record, string>
claude: '~/.claude/skills//SKILL.md',
codex: '~/.agents/skills//SKILL.md',
cursor: '~/.cursor/skills//SKILL.md',
+ minimax: '~/.claude/skills//SKILL.md',
};
const SCOPE_LABELS: Record = {
diff --git a/src/types/app.ts b/src/types/app.ts
index 8fa4a68430..1c0d908585 100644
--- a/src/types/app.ts
+++ b/src/types/app.ts
@@ -1,4 +1,4 @@
-export type LLMProvider = 'claude' | 'cursor' | 'codex' | 'opencode';
+export type LLMProvider = 'claude' | 'cursor' | 'codex' | 'minimax' | 'opencode';
export type ProviderModelOption = {
value: string;