Skip to content

Commit 3c626e9

Browse files
[codex] Add Moonshot Kimi backend provider (#639)
Co-authored-by: James Grugett <jahooma@gmail.com>
1 parent 0203b7c commit 3c626e9

7 files changed

Lines changed: 990 additions & 47 deletions

File tree

packages/internal/src/env-schema.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ export const serverEnvSchema = clientEnvSchema.extend({
77
OPENAI_API_KEY: z.string().min(1),
88
ANTHROPIC_API_KEY: z.string().min(1),
99
FIREWORKS_API_KEY: z.string().min(1),
10+
MOONSHOT_API_KEY: z.string().min(1).optional(),
1011
CANOPYWAVE_API_KEY: z.string().min(1).optional(),
1112
DEEPSEEK_API_KEY: z.string().min(1).optional(),
1213
SILICONFLOW_API_KEY: z.string().min(1).optional(),
@@ -88,6 +89,7 @@ export const serverProcessEnv: ServerInput = {
8889
OPENAI_API_KEY: process.env.OPENAI_API_KEY,
8990
ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY,
9091
FIREWORKS_API_KEY: process.env.FIREWORKS_API_KEY,
92+
MOONSHOT_API_KEY: process.env.MOONSHOT_API_KEY,
9193
CANOPYWAVE_API_KEY: process.env.CANOPYWAVE_API_KEY,
9294
DEEPSEEK_API_KEY: process.env.DEEPSEEK_API_KEY,
9395
SILICONFLOW_API_KEY: process.env.SILICONFLOW_API_KEY,

packages/internal/src/env.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ if (isCI) {
1717
ensureEnvDefault('OPENAI_API_KEY', 'test')
1818
ensureEnvDefault('ANTHROPIC_API_KEY', 'test')
1919
ensureEnvDefault('FIREWORKS_API_KEY', 'test')
20+
ensureEnvDefault('MOONSHOT_API_KEY', 'test')
2021
ensureEnvDefault('CANOPYWAVE_API_KEY', 'test')
2122
ensureEnvDefault('DEEPSEEK_API_KEY', 'test')
2223
ensureEnvDefault('OPENCODE_API_KEY', 'test')

web/src/app/api/v1/chat/completions/__tests__/completions.test.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -869,9 +869,13 @@ describe('/api/v1/chat/completions POST endpoint', () => {
869869
)
870870

871871
it(
872-
'routes OpenCode Zen-prefixed and Kimi models to the direct OpenCode Zen provider',
872+
'routes OpenCode Zen models and existing Kimi alias to the direct OpenCode Zen provider',
873873
async () => {
874874
const testCases = [
875+
{
876+
codebuffModel: 'moonshotai/kimi-k2.6',
877+
upstreamModel: 'kimi-k2.6',
878+
},
875879
{
876880
codebuffModel: openCodeZenModels.opencode_kimi_k2_6,
877881
upstreamModel: 'kimi-k2.6',
@@ -880,10 +884,6 @@ describe('/api/v1/chat/completions POST endpoint', () => {
880884
codebuffModel: openCodeZenModels.opencode_minimax_m2_7,
881885
upstreamModel: 'minimax-m2.7',
882886
},
883-
{
884-
codebuffModel: 'moonshotai/kimi-k2.6',
885-
upstreamModel: 'kimi-k2.6',
886-
},
887887
]
888888

889889
for (const { codebuffModel, upstreamModel } of testCases) {

web/src/app/api/v1/chat/completions/_post.ts

Lines changed: 72 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,12 @@ import {
5555
handleDeepSeekStream,
5656
isDeepSeekModel,
5757
} from '@/llm-api/deepseek'
58+
import {
59+
handleMoonshotNonStream,
60+
handleMoonshotStream,
61+
isMoonshotModel,
62+
MoonshotError,
63+
} from '@/llm-api/moonshot'
5864
import {
5965
OpenCodeZenError,
6066
handleOpenCodeZenNonStream,
@@ -616,18 +622,22 @@ export async function postChatCompletions(params: {
616622
// Streaming request — route supported models to direct providers.
617623
const useSiliconFlow = false // isSiliconFlowModel(typedBody.model)
618624
const useOpenCodeZen = isOpenCodeZenModel(typedBody.model)
625+
const useMoonshot = !useOpenCodeZen && isMoonshotModel(typedBody.model)
619626
const useCanopyWave =
620-
!useOpenCodeZen && isCanopyWaveModel(typedBody.model)
627+
!useMoonshot && !useOpenCodeZen && isCanopyWaveModel(typedBody.model)
621628
const useDeepSeek =
629+
!useMoonshot &&
622630
!useOpenCodeZen &&
623631
!useCanopyWave &&
624632
isDeepSeekModel(typedBody.model)
625633
const useFireworks =
634+
!useMoonshot &&
626635
!useOpenCodeZen &&
627636
!useCanopyWave &&
628637
!useDeepSeek &&
629638
isFireworksModel(typedBody.model)
630639
const useOpenAIDirect =
640+
!useMoonshot &&
631641
!useOpenCodeZen &&
632642
!useCanopyWave &&
633643
!useDeepSeek &&
@@ -644,20 +654,22 @@ export async function postChatCompletions(params: {
644654
}
645655
const stream = useSiliconFlow
646656
? await handleSiliconFlowStream(baseArgs)
647-
: useOpenCodeZen
648-
? await handleOpenCodeZenStream(baseArgs)
649-
: useCanopyWave
650-
? await handleCanopyWaveStream(baseArgs)
651-
: useDeepSeek
652-
? await handleDeepSeekStream(baseArgs)
653-
: useFireworks
654-
? await handleFireworksStream(baseArgs)
655-
: useOpenAIDirect
656-
? await handleOpenAIStream(baseArgs)
657-
: await handleOpenRouterStream({
658-
...baseArgs,
659-
openrouterApiKey,
660-
})
657+
: useMoonshot
658+
? await handleMoonshotStream(baseArgs)
659+
: useOpenCodeZen
660+
? await handleOpenCodeZenStream(baseArgs)
661+
: useCanopyWave
662+
? await handleCanopyWaveStream(baseArgs)
663+
: useDeepSeek
664+
? await handleDeepSeekStream(baseArgs)
665+
: useFireworks
666+
? await handleFireworksStream(baseArgs)
667+
: useOpenAIDirect
668+
? await handleOpenAIStream(baseArgs)
669+
: await handleOpenRouterStream({
670+
...baseArgs,
671+
openrouterApiKey,
672+
})
661673

662674
trackEvent({
663675
event: AnalyticsEvent.CHAT_COMPLETIONS_STREAM_STARTED,
@@ -682,15 +694,22 @@ export async function postChatCompletions(params: {
682694
const model = typedBody.model
683695
const useSiliconFlow = false // isSiliconFlowModel(model)
684696
const useOpenCodeZen = isOpenCodeZenModel(model)
685-
const useCanopyWave = !useOpenCodeZen && isCanopyWaveModel(model)
697+
const useMoonshot = !useOpenCodeZen && isMoonshotModel(model)
698+
const useCanopyWave =
699+
!useMoonshot && !useOpenCodeZen && isCanopyWaveModel(model)
686700
const useDeepSeek =
687-
!useOpenCodeZen && !useCanopyWave && isDeepSeekModel(model)
701+
!useMoonshot &&
702+
!useOpenCodeZen &&
703+
!useCanopyWave &&
704+
isDeepSeekModel(model)
688705
const useFireworks =
706+
!useMoonshot &&
689707
!useOpenCodeZen &&
690708
!useCanopyWave &&
691709
!useDeepSeek &&
692710
isFireworksModel(model)
693711
const shouldUseOpenAIEndpoint =
712+
!useMoonshot &&
694713
!useOpenCodeZen &&
695714
!useCanopyWave &&
696715
!useDeepSeek &&
@@ -708,20 +727,22 @@ export async function postChatCompletions(params: {
708727
}
709728
const nonStreamRequest = useSiliconFlow
710729
? handleSiliconFlowNonStream(baseArgs)
711-
: useOpenCodeZen
712-
? handleOpenCodeZenNonStream(baseArgs)
713-
: useCanopyWave
714-
? handleCanopyWaveNonStream(baseArgs)
715-
: useDeepSeek
716-
? handleDeepSeekNonStream(baseArgs)
717-
: useFireworks
718-
? handleFireworksNonStream(baseArgs)
719-
: shouldUseOpenAIEndpoint
720-
? handleOpenAINonStream(baseArgs)
721-
: handleOpenRouterNonStream({
722-
...baseArgs,
723-
openrouterApiKey,
724-
})
730+
: useMoonshot
731+
? handleMoonshotNonStream(baseArgs)
732+
: useOpenCodeZen
733+
? handleOpenCodeZenNonStream(baseArgs)
734+
: useCanopyWave
735+
? handleCanopyWaveNonStream(baseArgs)
736+
: useDeepSeek
737+
? handleDeepSeekNonStream(baseArgs)
738+
: useFireworks
739+
? handleFireworksNonStream(baseArgs)
740+
: shouldUseOpenAIEndpoint
741+
? handleOpenAINonStream(baseArgs)
742+
: handleOpenRouterNonStream({
743+
...baseArgs,
744+
openrouterApiKey,
745+
})
725746
const result = await nonStreamRequest
726747

727748
trackEvent({
@@ -754,6 +775,10 @@ export async function postChatCompletions(params: {
754775
if (error instanceof DeepSeekError) {
755776
deepseekError = error
756777
}
778+
let moonshotError: MoonshotError | undefined
779+
if (error instanceof MoonshotError) {
780+
moonshotError = error
781+
}
757782
let siliconflowError: SiliconFlowError | undefined
758783
if (error instanceof SiliconFlowError) {
759784
siliconflowError = error
@@ -773,15 +798,17 @@ export async function postChatCompletions(params: {
773798
? 'SiliconFlow'
774799
: opencodeZenError
775800
? 'OpenCode Zen'
776-
: canopywaveError
777-
? 'CanopyWave'
778-
: deepseekError
779-
? 'DeepSeek'
780-
: fireworksError
781-
? 'Fireworks'
782-
: openaiError
783-
? 'OpenAI'
784-
: 'OpenRouter'
801+
: moonshotError
802+
? 'Moonshot'
803+
: canopywaveError
804+
? 'CanopyWave'
805+
: deepseekError
806+
? 'DeepSeek'
807+
: fireworksError
808+
? 'Fireworks'
809+
: openaiError
810+
? 'OpenAI'
811+
: 'OpenRouter'
785812
logger.error(
786813
{
787814
error: getErrorObject(error),
@@ -798,6 +825,7 @@ export async function postChatCompletions(params: {
798825
providerStatusCode: (
799826
openrouterError ??
800827
fireworksError ??
828+
moonshotError ??
801829
canopywaveError ??
802830
deepseekError ??
803831
siliconflowError ??
@@ -807,6 +835,7 @@ export async function postChatCompletions(params: {
807835
providerStatusText: (
808836
openrouterError ??
809837
fireworksError ??
838+
moonshotError ??
810839
canopywaveError ??
811840
deepseekError ??
812841
siliconflowError ??
@@ -840,6 +869,9 @@ export async function postChatCompletions(params: {
840869
if (error instanceof FireworksError) {
841870
return NextResponse.json(error.toJSON(), { status: error.statusCode })
842871
}
872+
if (error instanceof MoonshotError) {
873+
return NextResponse.json(error.toJSON(), { status: error.statusCode })
874+
}
843875
if (error instanceof CanopyWaveError) {
844876
return NextResponse.json(error.toJSON(), { status: error.statusCode })
845877
}
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import { describe, expect, it } from 'bun:test'
2+
3+
import { buildMoonshotRequestBody } from '../moonshot'
4+
5+
import type { ChatCompletionRequestBody } from '../types'
6+
7+
type MoonshotRequestBody = Omit<ChatCompletionRequestBody, 'messages'> & {
8+
messages: Array<
9+
ChatCompletionRequestBody['messages'][number] & {
10+
reasoning_content?: string | null
11+
}
12+
>
13+
}
14+
15+
function buildBody(body: MoonshotRequestBody) {
16+
return buildMoonshotRequestBody(
17+
body as ChatCompletionRequestBody,
18+
'moonshotai/kimi-k2.6',
19+
)
20+
}
21+
22+
describe('buildMoonshotRequestBody', () => {
23+
it('enables preserved thinking by default for Kimi K2.6', () => {
24+
const body = buildBody({
25+
model: 'moonshotai/kimi-k2.6',
26+
messages: [
27+
{
28+
role: 'assistant',
29+
content: 'I will inspect the files.',
30+
reasoning_content: 'Need to understand the repo first.',
31+
},
32+
{
33+
role: 'user',
34+
content: 'Continue.',
35+
},
36+
],
37+
})
38+
39+
expect(body.model).toBe('kimi-k2.6')
40+
expect(body.thinking).toEqual({ type: 'enabled', keep: 'all' })
41+
expect(body.messages).toEqual([
42+
{
43+
role: 'assistant',
44+
content: 'I will inspect the files.',
45+
reasoning_content: 'Need to understand the repo first.',
46+
},
47+
{
48+
role: 'user',
49+
content: 'Continue.',
50+
},
51+
])
52+
})
53+
54+
it('keeps historical reasoning when thinking is explicitly enabled', () => {
55+
const body = buildBody({
56+
model: 'moonshotai/kimi-k2.6',
57+
messages: [{ role: 'user', content: 'hello' }],
58+
reasoning: { enabled: true },
59+
})
60+
61+
expect(body.thinking).toEqual({ type: 'enabled', keep: 'all' })
62+
expect(body.reasoning).toBeUndefined()
63+
})
64+
65+
it('does not preserve thinking when reasoning is explicitly disabled', () => {
66+
const body = buildBody({
67+
model: 'moonshotai/kimi-k2.6',
68+
messages: [
69+
{
70+
role: 'assistant',
71+
content: 'Done.',
72+
reasoning_content: 'Used the tool result.',
73+
},
74+
{ role: 'user', content: 'next' },
75+
],
76+
reasoning: { enabled: false },
77+
})
78+
79+
expect(body.thinking).toEqual({ type: 'disabled' })
80+
expect(body.reasoning).toBeUndefined()
81+
})
82+
})

0 commit comments

Comments
 (0)