Merge pull request #250 from jellydn/feat/minimax-full-support

feat(minimax): Add full MiniMax M2.1 support
This commit is contained in:
Kai (Tam Nhu) Tran
2026-01-02 17:06:48 -08:00
committed by GitHub
9 changed files with 141 additions and 45 deletions
+1
View File
@@ -1,5 +1,6 @@
{
"lockfileVersion": 1,
"configVersion": 0,
"workspaces": {
"": {
"name": "@kaitranntt/ccs",
+10
View File
@@ -0,0 +1,10 @@
{
"env": {
"ANTHROPIC_BASE_URL": "https://api.minimax.io/anthropic",
"ANTHROPIC_AUTH_TOKEN": "YOUR_MINIMAX_API_KEY_HERE",
"ANTHROPIC_MODEL": "MiniMax-M2.1",
"ANTHROPIC_DEFAULT_OPUS_MODEL": "MiniMax-M2.1",
"ANTHROPIC_DEFAULT_SONNET_MODEL": "MiniMax-M2.1",
"ANTHROPIC_DEFAULT_HAIKU_MODEL": "MiniMax-M2.1-lightning"
}
}
+3 -3
View File
@@ -99,13 +99,13 @@ export const PROVIDER_PRESETS: ProviderPreset[] = [
category: 'alternative',
},
{
id: 'minimax',
id: 'mm',
name: 'Minimax',
description: 'M2.1/M2.1-lightning/M2 - multilang coding (1M context)',
baseUrl: 'https://api.minimax.io/anthropic',
defaultProfileName: 'minimax',
defaultProfileName: 'mm',
defaultModel: 'MiniMax-M2.1',
apiKeyPlaceholder: 'YOUR_MINIMAX_API_KEY',
apiKeyPlaceholder: 'YOUR_MINIMAX_API_KEY_HERE',
apiKeyHint: 'Get your API key at platform.minimax.io',
category: 'alternative',
},
+27 -2
View File
@@ -3,7 +3,7 @@ import * as path from 'path';
import * as fs from 'fs';
import { detectClaudeCli } from './utils/claude-detector';
import { getSettingsPath, loadSettings } from './utils/config-manager';
import { validateGlmKey } from './utils/api-key-validator';
import { validateGlmKey, validateMiniMaxKey } from './utils/api-key-validator';
import { ErrorManager } from './utils/error-manager';
import { execClaudeWithCLIProxy, CLIProxyProvider } from './cliproxy';
import {
@@ -535,7 +535,7 @@ async function main(): Promise<void> {
// Display WebSearch status (single line, equilibrium UX)
displayWebSearchStatus();
// Pre-flight validation for GLM/GLMT profiles
// Pre-flight validation for GLM/GLMT/MiniMax profiles
if (profileInfo.name === 'glm' || profileInfo.name === 'glmt') {
const preflightSettingsPath = getSettingsPath(profileInfo.name);
const preflightSettings = loadSettings(preflightSettingsPath);
@@ -561,6 +561,31 @@ async function main(): Promise<void> {
}
}
if (profileInfo.name === 'mm') {
const preflightSettingsPath = getSettingsPath(profileInfo.name);
const preflightSettings = loadSettings(preflightSettingsPath);
const apiKey = preflightSettings.env?.['ANTHROPIC_AUTH_TOKEN'];
if (apiKey) {
const validation = await validateMiniMaxKey(
apiKey,
preflightSettings.env?.['ANTHROPIC_BASE_URL']
);
if (!validation.valid) {
console.error('');
console.error(fail(validation.error || 'API key validation failed'));
if (validation.suggestion) {
console.error('');
console.error(validation.suggestion);
}
console.error('');
console.error(info('To skip validation: CCS_SKIP_PREFLIGHT=1 ccs mm "prompt"'));
process.exit(1);
}
}
}
// Check if this is GLMT profile (requires proxy)
if (profileInfo.name === 'glmt') {
// GLMT FLOW: Settings-based with embedded proxy for thinking support
+73 -35
View File
@@ -19,53 +19,58 @@ export interface ValidationResult {
const DEFAULT_PLACEHOLDERS = [
'YOUR_GLM_API_KEY_HERE',
'YOUR_KIMI_API_KEY_HERE',
'YOUR_MINIMAX_API_KEY_HERE',
'YOUR_API_KEY_HERE',
'YOUR-API-KEY-HERE',
'PLACEHOLDER',
'',
];
/**
* Validate GLM API key with quick health check
*
* @param apiKey - The ANTHROPIC_AUTH_TOKEN value
* @param baseUrl - Optional base URL (defaults to Z.AI)
* @param timeoutMs - Timeout in milliseconds (default 2000)
*/
export async function validateGlmKey(
interface ProviderConfig {
name: string;
profile: string;
defaultBaseUrl: string;
path: string;
displayName: string;
dashboardUrl: string;
}
async function validateProviderKey(
apiKey: string,
config: ProviderConfig,
baseUrl?: string,
timeoutMs = 2000
): Promise<ValidationResult> {
// Skip if disabled
if (process.env.CCS_SKIP_PREFLIGHT === '1') {
return { valid: true };
}
// Basic format check - detect placeholders
if (!apiKey || DEFAULT_PLACEHOLDERS.includes(apiKey.toUpperCase())) {
return {
valid: false,
error: 'API key not configured',
suggestion:
'Set ANTHROPIC_AUTH_TOKEN in ~/.ccs/glm.settings.json\n' +
'Or run: ccs config -> API Profiles -> GLM',
`Set ANTHROPIC_AUTH_TOKEN in ~/.ccs/${config.profile}.settings.json\n` +
`Or run: ccs config -> API Profiles -> ${config.name}`,
};
}
// Determine validation endpoint
// Z.AI uses /api/anthropic path, we can test with a minimal request
const targetBase = baseUrl || 'https://api.z.ai';
const targetBase = baseUrl || config.defaultBaseUrl;
let url: URL;
try {
url = new URL('/api/anthropic/v1/models', targetBase);
url = new URL(config.path, targetBase);
} catch {
// Invalid URL - fail-open
return { valid: true };
}
return new Promise((resolve) => {
// Determine protocol - use http module for http:// URLs
let resolved = false;
const safeResolve = (result: ValidationResult) => {
if (resolved) return;
resolved = true;
resolve(result);
};
const isHttps = url.protocol === 'https:';
const httpModule = isHttps ? https : http;
const defaultPort = isHttps ? 443 : 80;
@@ -85,46 +90,79 @@ export async function validateGlmKey(
clearTimeout(timeoutId);
if (res.statusCode === 200) {
resolve({ valid: true });
safeResolve({ valid: true });
} else if (res.statusCode === 401 || res.statusCode === 403) {
resolve({
safeResolve({
valid: false,
error: 'API key rejected by Z.AI',
error: `API key rejected by ${config.displayName}`,
suggestion:
'Your key may have expired. To fix:\n' +
' 1. Go to Z.AI dashboard and regenerate your API key\n' +
' 2. Update ~/.ccs/glm.settings.json with the new key\n' +
' 3. Or run: ccs config -> API Profiles -> GLM',
`Your key may have expired. To fix:\n` +
` 1. Go to ${config.dashboardUrl} and regenerate your API key\n` +
` 2. Update ~/.ccs/${config.profile}.settings.json with new key\n` +
` 3. Or run: ccs config -> API Profiles -> ${config.name}`,
});
} else {
// Other errors (404, 500, etc.) - fail-open, let Claude CLI handle
// Debug log for diagnostics when CCS_DEBUG is set
if (process.env.CCS_DEBUG === '1') {
console.error(
`[CCS-Preflight] Unexpected status ${res.statusCode} from ${url.href} - fail-open`
);
}
resolve({ valid: true });
safeResolve({ valid: true });
}
// Consume response body to free resources
res.resume();
});
req.on('error', () => {
clearTimeout(timeoutId);
// Network error - fail-open
resolve({ valid: true });
safeResolve({ valid: true });
});
// Set timeout after request is created so we can destroy it on timeout
const timeoutId = setTimeout(() => {
// Abort request to prevent TCP connection leak
req.destroy();
// Fail-open on timeout - let Claude CLI handle it
resolve({ valid: true });
safeResolve({ valid: true });
}, timeoutMs);
req.end();
});
}
export async function validateGlmKey(
apiKey: string,
baseUrl?: string,
timeoutMs?: number
): Promise<ValidationResult> {
return validateProviderKey(
apiKey,
{
name: 'GLM',
profile: 'glm',
defaultBaseUrl: 'https://api.z.ai',
path: '/api/anthropic/v1/models',
displayName: 'Z.AI',
dashboardUrl: 'Z.AI dashboard',
},
baseUrl,
timeoutMs
);
}
export async function validateMiniMaxKey(
apiKey: string,
baseUrl?: string,
timeoutMs?: number
): Promise<ValidationResult> {
return validateProviderKey(
apiKey,
{
name: 'MiniMax',
profile: 'mm',
defaultBaseUrl: 'https://api.minimax.io',
path: '/anthropic/v1/models',
displayName: 'MiniMax',
dashboardUrl: 'platform.minimax.io',
},
baseUrl,
timeoutMs
);
}
+22
View File
@@ -537,6 +537,28 @@ const PRICING_REGISTRY: Record<string, ModelPricing> = {
cacheReadPerMillion: 0.0,
},
// ---------------------------------------------------------------------------
// MiniMax Models - Source: https://platform.minimax.io/docs/pricing/pay-as-you-go
// ---------------------------------------------------------------------------
'MiniMax-M2.1': {
inputPerMillion: 0.3,
outputPerMillion: 1.2,
cacheCreationPerMillion: 0.375,
cacheReadPerMillion: 0.03,
},
'MiniMax-M2.1-lightning': {
inputPerMillion: 0.3,
outputPerMillion: 2.4,
cacheCreationPerMillion: 0.375,
cacheReadPerMillion: 0.03,
},
'MiniMax-M2': {
inputPerMillion: 0.3,
outputPerMillion: 1.2,
cacheCreationPerMillion: 0.375,
cacheReadPerMillion: 0.03,
},
// ---------------------------------------------------------------------------
// DeepSeek Models - Source: better-ccusage
// ---------------------------------------------------------------------------
+1
View File
@@ -1,5 +1,6 @@
{
"lockfileVersion": 1,
"configVersion": 0,
"workspaces": {
"": {
"name": "ui",
+1 -2
View File
@@ -23,8 +23,7 @@ const badgeVariants = cva(
);
interface BadgeProps
extends React.HTMLAttributes<HTMLDivElement>,
VariantProps<typeof badgeVariants> {}
extends React.HTMLAttributes<HTMLDivElement>, VariantProps<typeof badgeVariants> {}
function Badge({ className, variant, ...props }: BadgeProps) {
return <div className={cn(badgeVariants({ variant }), className)} {...props} />;
+3 -3
View File
@@ -98,15 +98,15 @@ export const PROVIDER_PRESETS: ProviderPreset[] = [
category: 'alternative',
},
{
id: 'minimax',
id: 'mm',
name: 'Minimax',
description: 'M2.1/M2.1-lightning/M2 - multilang coding (1M context)',
baseUrl: 'https://api.minimax.io/anthropic',
defaultProfileName: 'minimax',
defaultProfileName: 'mm',
badge: '1M context',
defaultModel: 'MiniMax-M2.1',
requiresApiKey: true,
apiKeyPlaceholder: 'YOUR_MINIMAX_API_KEY',
apiKeyPlaceholder: 'YOUR_MINIMAX_API_KEY_HERE',
apiKeyHint: 'Get your API key at platform.minimax.io',
category: 'alternative',
},