feat(models): add minimax and moonshot providers

This commit is contained in:
William Valentin
2026-02-15 19:18:48 -08:00
parent 94020cb7ea
commit 0470647ee7
11 changed files with 214 additions and 5 deletions
+62
View File
@@ -384,6 +384,68 @@ models:
}
});
it('reports WARN when MiniMax has no available API key sources', async () => {
const originalKey = process.env.MINIMAX_API_KEY;
delete process.env.MINIMAX_API_KEY;
try {
mkdirSync(testDir, { recursive: true });
const configPath = join(testDir, 'minimax-missing.yaml');
writeFileSync(configPath, `
telegram:
bot_token: "test-token"
allowed_chat_ids: [123]
models:
default:
provider: minimax
model: MiniMax-M1
`);
const ctx: DoctorContext = { configPath, dataDir: testDir };
const results = await runChecks(ctx);
const modelCheck = results.find(r => r.label.includes('Model connectivity')) as CheckResult | undefined;
expect(modelCheck?.status).toBe('warn');
expect(modelCheck?.detail).toContain('MINIMAX_API_KEY');
} finally {
if (originalKey) {
process.env.MINIMAX_API_KEY = originalKey;
} else {
delete process.env.MINIMAX_API_KEY;
}
}
});
it('reports WARN when Moonshot has no available API key sources', async () => {
const originalKey = process.env.MOONSHOT_API_KEY;
delete process.env.MOONSHOT_API_KEY;
try {
mkdirSync(testDir, { recursive: true });
const configPath = join(testDir, 'moonshot-missing.yaml');
writeFileSync(configPath, `
telegram:
bot_token: "test-token"
allowed_chat_ids: [123]
models:
default:
provider: moonshot
model: moonshot-v1-8k
`);
const ctx: DoctorContext = { configPath, dataDir: testDir };
const results = await runChecks(ctx);
const modelCheck = results.find(r => r.label.includes('Model connectivity')) as CheckResult | undefined;
expect(modelCheck?.status).toBe('warn');
expect(modelCheck?.detail).toContain('MOONSHOT_API_KEY');
} finally {
if (originalKey) {
process.env.MOONSHOT_API_KEY = originalKey;
} else {
delete process.env.MOONSHOT_API_KEY;
}
}
});
it('reports FAIL when Anthropic auth_mode=oauth has no available auth token sources', async () => {
const originalHome = process.env.HOME;
const originalToken = process.env.ANTHROPIC_AUTH_TOKEN;
+3 -1
View File
@@ -304,13 +304,15 @@ const checkModelConnectivity: Check = async (ctx) => {
}
// Providers with API-key style auth (no auth store integration yet)
const needsKey = ['gemini', 'openrouter', 'vercel', 'xai', 'github'];
const needsKey = ['gemini', 'openrouter', 'vercel', 'xai', 'minimax', 'moonshot', 'github'];
if (needsKey.includes(provider)) {
const envVarMap: Record<string, string> = {
gemini: 'GEMINI_API_KEY',
openrouter: 'OPENROUTER_API_KEY',
vercel: 'AI_GATEWAY_API_KEY',
xai: 'XAI_API_KEY',
minimax: 'MINIMAX_API_KEY',
moonshot: 'MOONSHOT_API_KEY',
github: 'GITHUB_TOKEN',
};
const envVar = envVarMap[provider];
+14
View File
@@ -75,4 +75,18 @@ describe('setupProviders', () => {
expect(config.models.default.endpoint).toBe('https://ai-gateway.vercel.sh/v1');
expect(config.models.default.model).toBe('openai/gpt-4.1');
});
it('configures minimax as default provider (second tier)', async () => {
// Pick "More providers..." then pick "MiniMax".
// Prompts: api key, endpoint, model, then confirm fast tier.
const rl = mockReadline(['4', '6', 'sk-minimax-test123', '', '', 'n']);
const p = createPrompter(rl);
const builder = new ConfigBuilder();
await setupProviders(p, builder);
const config = builder.build();
expect(config.models.default.provider).toBe('minimax');
expect(config.models.default.api_key).toBe('sk-minimax-test123');
expect(config.models.default.endpoint).toBe('https://api.minimax.io/v1');
expect(config.models.default.model).toBe('MiniMax-M1');
});
});
+4
View File
@@ -24,6 +24,8 @@ const SECOND_TIER: ProviderDef[] = [
{ name: 'Vercel AI Gateway', provider: 'vercel', defaultModel: 'openai/gpt-4.1', fastModel: 'openai/gpt-4.1-mini', needsApiKey: true, needsEndpoint: true, defaultEndpoint: 'https://ai-gateway.vercel.sh/v1', apiKeyLabel: 'Vercel AI Gateway API key' },
{ name: 'Z.AI (GLM)', provider: 'zhipuai', defaultModel: 'glm-4.7', needsApiKey: true, needsEndpoint: true, defaultEndpoint: 'https://api.z.ai/api/paas/v4', apiKeyLabel: 'Z.AI API key' },
{ name: 'xAI (Grok)', provider: 'xai', defaultModel: 'grok-3', fastModel: 'grok-3-mini', needsApiKey: true, needsEndpoint: false, apiKeyLabel: 'xAI API key' },
{ name: 'MiniMax', provider: 'minimax', defaultModel: 'MiniMax-M1', needsApiKey: true, needsEndpoint: true, defaultEndpoint: 'https://api.minimax.io/v1', apiKeyLabel: 'MiniMax API key' },
{ name: 'Moonshot (Kimi)', provider: 'moonshot', defaultModel: 'moonshot-v1-8k', needsApiKey: true, needsEndpoint: true, defaultEndpoint: 'https://api.moonshot.cn/v1', apiKeyLabel: 'Moonshot API key' },
{ name: 'Amazon Bedrock', provider: 'bedrock', defaultModel: 'anthropic.claude-sonnet-4-20250514-v1:0', needsApiKey: false, needsEndpoint: false },
{ name: 'GitHub Models', provider: 'github', defaultModel: 'claude-sonnet-4-20250514', needsApiKey: false, needsEndpoint: false },
];
@@ -37,6 +39,8 @@ const PROVIDER_HELP: Record<string, string> = {
vercel: 'Vercel AI Gateway uses an API key (AI_GATEWAY_API_KEY) and an OpenAI-compatible base URL (default: https://ai-gateway.vercel.sh/v1)',
zhipuai: 'Get your API key at https://z.ai/manage-apikey/apikey-list (Coding Plan endpoint: https://api.z.ai/api/coding/paas/v4)',
xai: 'Get your API key at https://console.x.ai',
minimax: 'Set MINIMAX_API_KEY (or paste it here) and optionally override endpoint if your deployment uses a custom host',
moonshot: 'Set MOONSHOT_API_KEY (or paste it here) and optionally override endpoint if your deployment uses a custom host',
bedrock: 'Uses AWS credentials from environment (~/.aws/credentials or IAM role)',
github: 'Uses GitHub Copilot — authenticate via OAuth device flow on first use',
};
+18
View File
@@ -179,6 +179,24 @@ describe('configSchema — models auth_mode', () => {
});
expect(result.models.default.provider).toBe('vercel');
});
it('accepts minimax and moonshot provider ids', () => {
const minimax = configSchema.parse({
...minimalConfig,
models: {
default: { provider: 'minimax', model: 'MiniMax-M1', api_key: 'test-key' },
},
});
expect(minimax.models.default.provider).toBe('minimax');
const moonshot = configSchema.parse({
...minimalConfig,
models: {
default: { provider: 'moonshot', model: 'moonshot-v1-8k', api_key: 'test-key' },
},
});
expect(moonshot.models.default.provider).toBe('moonshot');
});
});
describe('configSchema — matrix', () => {
+1 -1
View File
@@ -39,7 +39,7 @@ const serverSchema = z.object({
});
/** All supported model provider identifiers. Used by the config schema and TUI autocompletion. */
export const MODEL_PROVIDERS = ['anthropic', 'openai', 'gemini', 'ollama', 'llamacpp', 'openrouter', 'vercel', 'bedrock', 'github', 'zhipuai', 'xai', 'synthetic'] as const;
export const MODEL_PROVIDERS = ['anthropic', 'openai', 'gemini', 'ollama', 'llamacpp', 'openrouter', 'vercel', 'bedrock', 'github', 'zhipuai', 'xai', 'minimax', 'moonshot', 'synthetic'] as const;
export type ModelProvider = (typeof MODEL_PROVIDERS)[number];
+40
View File
@@ -185,6 +185,46 @@ describe('createClientFromConfig', () => {
}
});
it('creates OpenAIClient for minimax provider', async () => {
const prev = process.env.MINIMAX_API_KEY;
process.env.MINIMAX_API_KEY = 'test-key';
try {
const { createClientFromConfig } = await loadFactory();
const client = createClientFromConfig({
provider: 'minimax',
model: 'MiniMax-M1',
});
expect(client.constructor.name).toBe('OpenAIClient');
} finally {
if (prev === undefined) {
delete process.env.MINIMAX_API_KEY;
} else {
process.env.MINIMAX_API_KEY = prev;
}
}
});
it('creates OpenAIClient for moonshot provider', async () => {
const prev = process.env.MOONSHOT_API_KEY;
process.env.MOONSHOT_API_KEY = 'test-key';
try {
const { createClientFromConfig } = await loadFactory();
const client = createClientFromConfig({
provider: 'moonshot',
model: 'moonshot-v1-8k',
});
expect(client.constructor.name).toBe('OpenAIClient');
} finally {
if (prev === undefined) {
delete process.env.MOONSHOT_API_KEY;
} else {
process.env.MOONSHOT_API_KEY = prev;
}
}
});
it('creates BedrockClient for bedrock provider', async () => {
const { createClientFromConfig } = await loadFactory();
const client = createClientFromConfig({
+12
View File
@@ -223,6 +223,18 @@ export function createClientFromConfig(cfg: ModelConfig): ModelClient {
apiKey: requireApiKey(cfg, 'XAI_API_KEY'),
baseURL: cfg.endpoint ?? 'https://api.x.ai/v1',
});
case 'minimax':
return new OpenAIClient({
model: cfg.model,
apiKey: requireApiKey(cfg, 'MINIMAX_API_KEY'),
baseURL: cfg.endpoint ?? 'https://api.minimax.io/v1',
});
case 'moonshot':
return new OpenAIClient({
model: cfg.model,
apiKey: requireApiKey(cfg, 'MOONSHOT_API_KEY'),
baseURL: cfg.endpoint ?? 'https://api.moonshot.cn/v1',
});
case 'bedrock':
return new BedrockClient({
model: cfg.model,