feat(models): add minimax and moonshot providers
This commit is contained in:
@@ -159,6 +159,8 @@ If you want a fast mental model of where to start as an AI agent / contributor:
|
||||
| Ollama | `provider: ollama`, `model`, optional `endpoint` |
|
||||
| Zhipu AI (GLM) | `provider: zhipuai`, `api_key` or `ZHIPUAI_API_KEY`, optional `endpoint` |
|
||||
| xAI (Grok) | `provider: xai`, `api_key` or `XAI_API_KEY` |
|
||||
| MiniMax | `provider: minimax`, `api_key` or `MINIMAX_API_KEY`, optional `endpoint` |
|
||||
| Moonshot (Kimi) | `provider: moonshot`, `api_key` or `MOONSHOT_API_KEY`, optional `endpoint` |
|
||||
| llama.cpp | `provider: llamacpp`, `endpoint` |
|
||||
|
||||
### Model Tiers
|
||||
@@ -313,7 +315,7 @@ Switch providers and models on the fly without editing config or restarting:
|
||||
/model local ollama/glm-4.7-flash
|
||||
```
|
||||
|
||||
The provider name must match a supported provider (`anthropic`, `openai`, `gemini`, `ollama`, `llamacpp`, `openrouter`, `bedrock`, `github`, `zhipuai`, `xai`). Tab completion is available for both tiers and provider names.
|
||||
The provider name must match a supported provider (`anthropic`, `openai`, `gemini`, `ollama`, `llamacpp`, `openrouter`, `vercel`, `bedrock`, `github`, `zhipuai`, `xai`, `minimax`, `moonshot`, `synthetic`). Tab completion is available for both tiers and provider names.
|
||||
|
||||
For cloud Zhipu models, ensure `ZHIPUAI_API_KEY` is set or `api_key` is configured in the relevant tier.
|
||||
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
# MiniMax + Moonshot Provider Checklist
|
||||
|
||||
Date: 2026-02-16
|
||||
Status: completed
|
||||
|
||||
## Scope
|
||||
|
||||
- Add first-class `minimax` and `moonshot` model providers.
|
||||
- Wire providers through OpenAI-compatible client path with provider-specific API key env vars.
|
||||
- Update setup and doctor surfaces.
|
||||
- Add tests and docs.
|
||||
|
||||
## Completed
|
||||
|
||||
- Added provider ids to config schema (`minimax`, `moonshot`).
|
||||
- Added model factory wiring in `src/daemon/models.ts`:
|
||||
- `minimax` -> `MINIMAX_API_KEY`, default endpoint `https://api.minimax.io/v1`
|
||||
- `moonshot` -> `MOONSHOT_API_KEY`, default endpoint `https://api.moonshot.cn/v1`
|
||||
- Updated doctor provider checks for both env vars.
|
||||
- Updated setup wizard provider list/help text for both providers.
|
||||
- Added tests:
|
||||
- `src/config/schema.test.ts`
|
||||
- `src/daemon/clientFactory.test.ts`
|
||||
- `src/cli/setup/providers.test.ts`
|
||||
- `src/cli/doctor.test.ts`
|
||||
- Updated README provider docs and runtime `/model` provider list.
|
||||
|
||||
## Verification
|
||||
|
||||
- `pnpm test:run src/config/schema.test.ts`
|
||||
- `pnpm test:run src/daemon/clientFactory.test.ts`
|
||||
- `pnpm test:run src/cli/setup/providers.test.ts src/cli/doctor.test.ts`
|
||||
- `pnpm typecheck`
|
||||
+24
-2
@@ -157,6 +157,28 @@
|
||||
],
|
||||
"test_status": "pnpm typecheck + pnpm test:run passing"
|
||||
},
|
||||
"minimax-moonshot-provider": {
|
||||
"file": "2026-02-16-minimax-moonshot-provider-checklist.md",
|
||||
"status": "completed",
|
||||
"date": "2026-02-16",
|
||||
"updated": "2026-02-16",
|
||||
"summary": "Added first-class MiniMax and Moonshot model providers (OpenAI-compatible) with config schema support, model factory wiring, setup wizard options, doctor key checks, tests, and README updates.",
|
||||
"files_created": [
|
||||
"docs/plans/2026-02-16-minimax-moonshot-provider-checklist.md"
|
||||
],
|
||||
"files_modified": [
|
||||
"src/config/schema.ts",
|
||||
"src/config/schema.test.ts",
|
||||
"src/daemon/models.ts",
|
||||
"src/daemon/clientFactory.test.ts",
|
||||
"src/cli/doctor.ts",
|
||||
"src/cli/doctor.test.ts",
|
||||
"src/cli/setup/providers.ts",
|
||||
"src/cli/setup/providers.test.ts",
|
||||
"README.md"
|
||||
],
|
||||
"test_status": "pnpm test:run src/config/schema.test.ts src/daemon/clientFactory.test.ts src/cli/setup/providers.test.ts src/cli/doctor.test.ts + pnpm typecheck passing"
|
||||
},
|
||||
"skill-safety-scanner": {
|
||||
"file": "2026-02-15-skill-safety-scanner-checklist.md",
|
||||
"status": "completed",
|
||||
@@ -2201,12 +2223,12 @@
|
||||
"tier2_completion": "4/4 (100%) — inbound webhooks, vector memory search, Dockerfile, heartbeat monitor",
|
||||
"tier3_completion": "5/5 (100%) — lane queue, credential redaction, web UI token dashboard, xAI (Grok) provider, Voyage AI embeddings",
|
||||
"tier4_completion": "4/4 (100%) — gateway lock, shell completion, Tailscale Serve/Funnel, DM pairing codes",
|
||||
"feature_gap_scorecard": "104/128 match (81%), 0 partial (0%), 24 missing (19%)",
|
||||
"feature_gap_scorecard": "105/128 match (82%), 0 partial (0%), 23 missing (18%)",
|
||||
"operator_dx_milestone": "Phase 3 (Live Ops Dashboard): 2/2 plans complete — milestone done",
|
||||
"gmail_auth_cli": "flynn gmail-auth command implemented with OAuth2 flow, doctor check, config routed to Telegram",
|
||||
"native_audio_support": "completed — smart routing for native audio (Gemini/OpenAI/GitHub) vs Whisper transcription fallback",
|
||||
"remaining_phases_completion": "Phase 1: 3/3 (100%) — context levels, command registry, memory structure. Phase 2: 3/3 (100%) — component registry, confidence routing, history index. Phase 3: 2/2 (100%) — adaptive memory/compaction, truthfulness/autonomy hardening",
|
||||
"next_up": "Pick the next OpenClaw gap milestone and create a scoped checklist (candidates: announce delivery mode, presence tracking, MiniMax/Moonshot provider)"
|
||||
"next_up": "Pick the next OpenClaw gap milestone and create a scoped checklist (candidates: announce delivery mode, presence tracking, QMD backend)"
|
||||
},
|
||||
"soul_md_and_cron_create": {
|
||||
"date": "2026-02-11",
|
||||
|
||||
@@ -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
@@ -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];
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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',
|
||||
};
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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];
|
||||
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user