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
7 changes: 6 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -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
# =============================================================================
Expand All @@ -42,4 +48,3 @@ HOST=0.0.0.0
VITE_CONTEXT_WINDOW=160000
CONTEXT_WINDOW=160000


5 changes: 3 additions & 2 deletions public/api-docs.html
Original file line number Diff line number Diff line change
Expand Up @@ -489,7 +489,7 @@ <h2>Agent</h2>
<span class="endpoint-path"><span class="api-url">http://localhost:3001</span>/api/agent</span>
</div>

<p>Trigger an AI agent (Claude, Cursor, Codex, or OpenCode) to work on a project.</p>
<p>Trigger an AI agent, including MiniMax, to work on a project.</p>

<h4>Request Body Parameters</h4>
<table>
Expand Down Expand Up @@ -524,7 +524,7 @@ <h4>Request Body Parameters</h4>
<td><code>provider</code></td>
<td>string</td>
<td><span class="badge badge-optional">Optional</span></td>
<td><code>claude</code>, <code>cursor</code>, <code>codex</code>, or <code>opencode</code> (default: <code>claude</code>)</td>
<td>Supported provider identifier, including <code>minimax</code> (default: <code>claude</code>)</td>
</tr>
<tr>
<td><code>stream</code></td>
Expand Down Expand Up @@ -838,6 +838,7 @@ <h4>Branch & PR Response</h4>
{ id: 'claude', name: 'Anthropic' },
{ id: 'codex', name: 'OpenAI' },
{ id: 'cursor', name: 'Cursor' },
{ id: 'minimax', name: 'MiniMax' },
{ id: 'opencode', name: 'OpenCode' },
];

Expand Down
63 changes: 37 additions & 26 deletions server/claude-sdk.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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;
}
Expand All @@ -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,
Expand Down Expand Up @@ -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<void>}
*/
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;

Expand All @@ -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);
Expand All @@ -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 {};
}]
Expand Down Expand Up @@ -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, {
Expand All @@ -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) {
Expand Down Expand Up @@ -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
Expand All @@ -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) {
Expand All @@ -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 }));
}
}

Expand All @@ -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'
Expand All @@ -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
Expand Down
6 changes: 6 additions & 0 deletions server/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@ import {
spawnOpenCode,
abortOpenCodeSession,
} from './opencode-cli.js';
import {
queryMiniMaxSDK,
abortMiniMaxSDKSession,
} from './minimax-sdk.js';
import {
stripAnsiSequences,
normalizeDetectedUrl,
Expand Down Expand Up @@ -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,
Expand Down
39 changes: 39 additions & 0 deletions server/minimax-sdk.ts
Original file line number Diff line number Diff line change
@@ -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<typeof queryClaudeSDK>[2],
): Promise<unknown> {
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;
Loading