mirror of
https://github.com/tiennm99/ccs.git
synced 2026-10-13 20:14:34 +00:00
feat(minimax): Add full MiniMax M2.1 support
- Add MiniMax settings template (config/base-minimax.settings.json) - Add MiniMax pricing (3 models: M2.1, M2.1-lightning, M2) - Add validateMiniMaxKey() pre-flight validator - Integrate MiniMax validation in main CLI flow - Fix model name casing: M2.1-lightning (lowercase l) Implementation follows GLM pattern: - Anthropic-compatible API (no proxy needed) - Pre-flight validation with health check - Fail-open on network errors - Actionable error messages
This commit is contained in:
1 parent
c05f27fdb3
commit
bd5c9a0033
7 files changed
+166
-4
No files matched your search
@@ -1,5 +1,6 @@
|
|||||||
{
|
{
|
||||||
"lockfileVersion": 1,
|
"lockfileVersion": 1,
|
||||||
|
"configVersion": 0,
|
||||||
"workspaces": {
|
"workspaces": {
|
||||||
"": {
|
"": {
|
||||||
"name": "@kaitranntt/ccs",
|
"name": "@kaitranntt/ccs",
|
||||||
|
|||||||
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
+27
-2
@@ -3,7 +3,7 @@ import * as path from 'path';
|
|||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
import { detectClaudeCli } from './utils/claude-detector';
|
import { detectClaudeCli } from './utils/claude-detector';
|
||||||
import { getSettingsPath, loadSettings } from './utils/config-manager';
|
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 { ErrorManager } from './utils/error-manager';
|
||||||
import { execClaudeWithCLIProxy, CLIProxyProvider } from './cliproxy';
|
import { execClaudeWithCLIProxy, CLIProxyProvider } from './cliproxy';
|
||||||
import {
|
import {
|
||||||
@@ -535,7 +535,7 @@ async function main(): Promise<void> {
|
|||||||
// Display WebSearch status (single line, equilibrium UX)
|
// Display WebSearch status (single line, equilibrium UX)
|
||||||
displayWebSearchStatus();
|
displayWebSearchStatus();
|
||||||
|
|
||||||
// Pre-flight validation for GLM/GLMT profiles
|
// Pre-flight validation for GLM/GLMT/MiniMax profiles
|
||||||
if (profileInfo.name === 'glm' || profileInfo.name === 'glmt') {
|
if (profileInfo.name === 'glm' || profileInfo.name === 'glmt') {
|
||||||
const preflightSettingsPath = getSettingsPath(profileInfo.name);
|
const preflightSettingsPath = getSettingsPath(profileInfo.name);
|
||||||
const preflightSettings = loadSettings(preflightSettingsPath);
|
const preflightSettings = loadSettings(preflightSettingsPath);
|
||||||
@@ -561,6 +561,31 @@ async function main(): Promise<void> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (profileInfo.name === 'minimax') {
|
||||||
|
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 minimax "prompt"'));
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Check if this is GLMT profile (requires proxy)
|
// Check if this is GLMT profile (requires proxy)
|
||||||
if (profileInfo.name === 'glmt') {
|
if (profileInfo.name === 'glmt') {
|
||||||
// GLMT FLOW: Settings-based with embedded proxy for thinking support
|
// GLMT FLOW: Settings-based with embedded proxy for thinking support
|
||||||
|
|||||||
@@ -128,3 +128,107 @@ export async function validateGlmKey(
|
|||||||
req.end();
|
req.end();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate MiniMax API key with quick health check
|
||||||
|
*
|
||||||
|
* @param apiKey - The ANTHROPIC_AUTH_TOKEN value
|
||||||
|
* @param baseUrl - Optional base URL (defaults to MiniMax)
|
||||||
|
* @param timeoutMs - Timeout in milliseconds (default 2000)
|
||||||
|
*/
|
||||||
|
export async function validateMiniMaxKey(
|
||||||
|
apiKey: string,
|
||||||
|
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/minimax.settings.json\n' +
|
||||||
|
'Or run: ccs config -> API Profiles -> MiniMax',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine validation endpoint
|
||||||
|
// MiniMax uses /anthropic path, we can test with a minimal request
|
||||||
|
const targetBase = baseUrl || 'https://api.minimax.io';
|
||||||
|
let url: URL;
|
||||||
|
try {
|
||||||
|
url = new URL('/anthropic/v1/models', targetBase);
|
||||||
|
} catch {
|
||||||
|
// Invalid URL - fail-open
|
||||||
|
return { valid: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
// Determine protocol - use http module for http:// URLs
|
||||||
|
const isHttps = url.protocol === 'https:';
|
||||||
|
const httpModule = isHttps ? https : http;
|
||||||
|
const defaultPort = isHttps ? 443 : 80;
|
||||||
|
|
||||||
|
const options: https.RequestOptions = {
|
||||||
|
hostname: url.hostname,
|
||||||
|
port: url.port || defaultPort,
|
||||||
|
path: url.pathname,
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${apiKey}`,
|
||||||
|
'User-Agent': 'CCS-Preflight/1.0',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const req = httpModule.request(options, (res) => {
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
|
||||||
|
if (res.statusCode === 200) {
|
||||||
|
resolve({ valid: true });
|
||||||
|
} else if (res.statusCode === 401 || res.statusCode === 403) {
|
||||||
|
resolve({
|
||||||
|
valid: false,
|
||||||
|
error: 'API key rejected by MiniMax',
|
||||||
|
suggestion:
|
||||||
|
'Your key may have expired. To fix:\n' +
|
||||||
|
' 1. Go to platform.minimax.io and regenerate your API key\n' +
|
||||||
|
' 2. Update ~/.ccs/minimax.settings.json with new key\n' +
|
||||||
|
' 3. Or run: ccs config -> API Profiles -> MiniMax',
|
||||||
|
});
|
||||||
|
} 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 });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Consume response body to free resources
|
||||||
|
res.resume();
|
||||||
|
});
|
||||||
|
|
||||||
|
req.on('error', () => {
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
// Network error - fail-open
|
||||||
|
resolve({ 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 });
|
||||||
|
}, timeoutMs);
|
||||||
|
|
||||||
|
req.end();
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -537,6 +537,28 @@ const PRICING_REGISTRY: Record<string, ModelPricing> = {
|
|||||||
cacheReadPerMillion: 0.0,
|
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
|
// DeepSeek Models - Source: better-ccusage
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
{
|
{
|
||||||
"lockfileVersion": 1,
|
"lockfileVersion": 1,
|
||||||
|
"configVersion": 0,
|
||||||
"workspaces": {
|
"workspaces": {
|
||||||
"": {
|
"": {
|
||||||
"name": "ui",
|
"name": "ui",
|
||||||
|
|||||||
@@ -23,8 +23,7 @@ const badgeVariants = cva(
|
|||||||
);
|
);
|
||||||
|
|
||||||
interface BadgeProps
|
interface BadgeProps
|
||||||
extends React.HTMLAttributes<HTMLDivElement>,
|
extends React.HTMLAttributes<HTMLDivElement>, VariantProps<typeof badgeVariants> {}
|
||||||
VariantProps<typeof badgeVariants> {}
|
|
||||||
|
|
||||||
function Badge({ className, variant, ...props }: BadgeProps) {
|
function Badge({ className, variant, ...props }: BadgeProps) {
|
||||||
return <div className={cn(badgeVariants({ variant }), className)} {...props} />;
|
return <div className={cn(badgeVariants({ variant }), className)} {...props} />;
|
||||||
|
|||||||
Reference in new issue
Block a user