From 0470647ee7423c53e1bc439225a8690b0d3040f4 Mon Sep 17 00:00:00 2001 From: William Valentin Date: Sun, 15 Feb 2026 19:18:48 -0800 Subject: [PATCH] feat(models): add minimax and moonshot providers --- README.md | 4 +- ...-16-minimax-moonshot-provider-checklist.md | 33 ++++++++++ docs/plans/state.json | 26 +++++++- src/cli/doctor.test.ts | 62 +++++++++++++++++++ src/cli/doctor.ts | 4 +- src/cli/setup/providers.test.ts | 14 +++++ src/cli/setup/providers.ts | 4 ++ src/config/schema.test.ts | 18 ++++++ src/config/schema.ts | 2 +- src/daemon/clientFactory.test.ts | 40 ++++++++++++ src/daemon/models.ts | 12 ++++ 11 files changed, 214 insertions(+), 5 deletions(-) create mode 100644 docs/plans/2026-02-16-minimax-moonshot-provider-checklist.md diff --git a/README.md b/README.md index b707736..0830833 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/docs/plans/2026-02-16-minimax-moonshot-provider-checklist.md b/docs/plans/2026-02-16-minimax-moonshot-provider-checklist.md new file mode 100644 index 0000000..c538bad --- /dev/null +++ b/docs/plans/2026-02-16-minimax-moonshot-provider-checklist.md @@ -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` diff --git a/docs/plans/state.json b/docs/plans/state.json index 816787b..e17c3dc 100644 --- a/docs/plans/state.json +++ b/docs/plans/state.json @@ -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", diff --git a/src/cli/doctor.test.ts b/src/cli/doctor.test.ts index 98757ad..db285ca 100644 --- a/src/cli/doctor.test.ts +++ b/src/cli/doctor.test.ts @@ -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; diff --git a/src/cli/doctor.ts b/src/cli/doctor.ts index 96fa3b8..1c2233c 100644 --- a/src/cli/doctor.ts +++ b/src/cli/doctor.ts @@ -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 = { 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]; diff --git a/src/cli/setup/providers.test.ts b/src/cli/setup/providers.test.ts index 9f0c433..b0e894c 100644 --- a/src/cli/setup/providers.test.ts +++ b/src/cli/setup/providers.test.ts @@ -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'); + }); }); diff --git a/src/cli/setup/providers.ts b/src/cli/setup/providers.ts index 371e350..7017912 100644 --- a/src/cli/setup/providers.ts +++ b/src/cli/setup/providers.ts @@ -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 = { 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', }; diff --git a/src/config/schema.test.ts b/src/config/schema.test.ts index 4b4e0c0..82324d3 100644 --- a/src/config/schema.test.ts +++ b/src/config/schema.test.ts @@ -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', () => { diff --git a/src/config/schema.ts b/src/config/schema.ts index b50d47c..18118bd 100644 --- a/src/config/schema.ts +++ b/src/config/schema.ts @@ -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]; diff --git a/src/daemon/clientFactory.test.ts b/src/daemon/clientFactory.test.ts index e4013b2..188ceac 100644 --- a/src/daemon/clientFactory.test.ts +++ b/src/daemon/clientFactory.test.ts @@ -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({ diff --git a/src/daemon/models.ts b/src/daemon/models.ts index a636761..fd1fc88 100644 --- a/src/daemon/models.ts +++ b/src/daemon/models.ts @@ -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,