diff --git a/src/commands/providers.ts b/src/commands/providers.ts index 22acd5662..7accb9bbc 100644 --- a/src/commands/providers.ts +++ b/src/commands/providers.ts @@ -242,6 +242,7 @@ async function runExplain(args: string[]): Promise { DEEPSEEK_API_KEY: !!process.env.DEEPSEEK_API_KEY, GROQ_API_KEY: !!process.env.GROQ_API_KEY, TOGETHER_API_KEY: !!process.env.TOGETHER_API_KEY, + MOONSHOT_API_KEY: !!process.env.MOONSHOT_API_KEY, }; // Parallel probes for local providers (1s timeout each) diff --git a/src/core/ai/recipes/index.ts b/src/core/ai/recipes/index.ts index 5915c9a92..afbc4b652 100644 --- a/src/core/ai/recipes/index.ts +++ b/src/core/ai/recipes/index.ts @@ -21,6 +21,7 @@ import { dashscope } from './dashscope.ts'; import { zhipu } from './zhipu.ts'; import { azureOpenAI } from './azure-openai.ts'; import { zeroentropyai } from './zeroentropyai.ts'; +import { kimi } from './kimi.ts'; const ALL: Recipe[] = [ openai, @@ -38,6 +39,7 @@ const ALL: Recipe[] = [ zhipu, azureOpenAI, zeroentropyai, + kimi, ]; /** Map from `provider:id` key to recipe. */ diff --git a/src/core/ai/recipes/kimi.ts b/src/core/ai/recipes/kimi.ts new file mode 100644 index 000000000..f465f1e57 --- /dev/null +++ b/src/core/ai/recipes/kimi.ts @@ -0,0 +1,37 @@ +import type { Recipe } from '../types.ts'; + +/** + * Kimi / Moonshot AI. OpenAI-compatible chat completions endpoint. + * + * Docs: https://platform.moonshot.ai/docs/overview + * API base: https://api.moonshot.ai/v1 + */ +export const kimi: Recipe = { + id: 'kimi', + name: 'Kimi (Moonshot AI)', + tier: 'openai-compat', + implementation: 'openai-compatible', + base_url_default: 'https://api.moonshot.ai/v1', + auth_env: { + required: ['MOONSHOT_API_KEY'], + setup_url: 'https://platform.moonshot.ai/', + }, + touchpoints: { + expansion: { + models: ['kimi-k2.6', 'kimi-k2.5', 'kimi-k2-turbo-preview', 'kimi-k2-thinking', 'kimi-k2-thinking-turbo'], + cost_per_1m_tokens_usd: undefined, + price_last_verified: '2026-05-20', + }, + chat: { + models: ['kimi-k2.6', 'kimi-k2.5', 'kimi-k2-turbo-preview', 'kimi-k2-thinking', 'kimi-k2-thinking-turbo'], + supports_tools: true, + supports_subagent_loop: false, + supports_prompt_cache: false, + max_context_tokens: 256000, + cost_per_1m_input_usd: undefined, + cost_per_1m_output_usd: undefined, + price_last_verified: '2026-05-20', + }, + }, + setup_hint: 'Get an API key at https://platform.moonshot.ai/, then `export MOONSHOT_API_KEY=...`', +}; diff --git a/test/ai/recipe-kimi.test.ts b/test/ai/recipe-kimi.test.ts new file mode 100644 index 000000000..c7b4f117a --- /dev/null +++ b/test/ai/recipe-kimi.test.ts @@ -0,0 +1,58 @@ +/** + * Kimi / Moonshot AI recipe smoke. + * + * Coverage: + * - Recipe registered with expected OpenAI-compatible shape + * - Chat + expansion touchpoints expose current Kimi model IDs + * - default auth: MOONSHOT_API_KEY -> Authorization Bearer + */ + +import { describe, expect, test } from 'bun:test'; +import { getRecipe } from '../../src/core/ai/recipes/index.ts'; +import { defaultResolveAuth } from '../../src/core/ai/gateway.ts'; +import { AIConfigError } from '../../src/core/ai/errors.ts'; + +describe('recipe: kimi', () => { + test('registered with expected provider shape', () => { + const r = getRecipe('kimi'); + expect(r).toBeDefined(); + expect(r!.id).toBe('kimi'); + expect(r!.name).toBe('Kimi (Moonshot AI)'); + expect(r!.tier).toBe('openai-compat'); + expect(r!.implementation).toBe('openai-compatible'); + expect(r!.base_url_default).toBe('https://api.moonshot.ai/v1'); + expect(r!.auth_env?.required).toEqual(['MOONSHOT_API_KEY']); + }); + + test('expansion touchpoint declares Kimi models', () => { + const r = getRecipe('kimi')!; + expect(r.touchpoints.expansion).toBeDefined(); + expect(r.touchpoints.expansion!.models[0]).toBe('kimi-k2.6'); + expect(r.touchpoints.expansion!.models).toContain('kimi-k2.5'); + expect(r.touchpoints.expansion!.models).toContain('kimi-k2-turbo-preview'); + expect(r.touchpoints.expansion!.models).toContain('kimi-k2-thinking'); + }); + + test('chat touchpoint declares Kimi models and conservative capabilities', () => { + const r = getRecipe('kimi')!; + expect(r.touchpoints.chat).toBeDefined(); + expect(r.touchpoints.chat!.models[0]).toBe('kimi-k2.6'); + expect(r.touchpoints.chat!.models).toContain('kimi-k2.5'); + expect(r.touchpoints.chat!.supports_tools).toBe(true); + expect(r.touchpoints.chat!.supports_subagent_loop).toBe(false); + expect(r.touchpoints.chat!.supports_prompt_cache).toBe(false); + expect(r.touchpoints.chat!.max_context_tokens).toBe(256000); + }); + + test('default auth: MOONSHOT_API_KEY set -> Bearer token', () => { + const r = getRecipe('kimi')!; + const auth = defaultResolveAuth(r, { MOONSHOT_API_KEY: 'fake-moonshot-key' }, 'chat'); + expect(auth.headerName).toBe('Authorization'); + expect(auth.token).toBe('Bearer fake-moonshot-key'); + }); + + test('default auth: missing MOONSHOT_API_KEY -> AIConfigError', () => { + const r = getRecipe('kimi')!; + expect(() => defaultResolveAuth(r, {}, 'chat')).toThrow(AIConfigError); + }); +});