Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions packages/internal/src/env-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export const serverEnvSchema = clientEnvSchema.extend({
OPENAI_API_KEY: z.string().min(1),
ANTHROPIC_API_KEY: z.string().min(1),
FIREWORKS_API_KEY: z.string().min(1),
MOONSHOT_API_KEY: z.string().min(1).optional(),
CANOPYWAVE_API_KEY: z.string().min(1).optional(),
DEEPSEEK_API_KEY: z.string().min(1).optional(),
SILICONFLOW_API_KEY: z.string().min(1).optional(),
Expand Down Expand Up @@ -88,6 +89,7 @@ export const serverProcessEnv: ServerInput = {
OPENAI_API_KEY: process.env.OPENAI_API_KEY,
ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY,
FIREWORKS_API_KEY: process.env.FIREWORKS_API_KEY,
MOONSHOT_API_KEY: process.env.MOONSHOT_API_KEY,
CANOPYWAVE_API_KEY: process.env.CANOPYWAVE_API_KEY,
DEEPSEEK_API_KEY: process.env.DEEPSEEK_API_KEY,
SILICONFLOW_API_KEY: process.env.SILICONFLOW_API_KEY,
Expand Down
1 change: 1 addition & 0 deletions packages/internal/src/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ if (isCI) {
ensureEnvDefault('OPENAI_API_KEY', 'test')
ensureEnvDefault('ANTHROPIC_API_KEY', 'test')
ensureEnvDefault('FIREWORKS_API_KEY', 'test')
ensureEnvDefault('MOONSHOT_API_KEY', 'test')
ensureEnvDefault('CANOPYWAVE_API_KEY', 'test')
ensureEnvDefault('DEEPSEEK_API_KEY', 'test')
ensureEnvDefault('OPENCODE_API_KEY', 'test')
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -869,9 +869,13 @@ describe('/api/v1/chat/completions POST endpoint', () => {
)

it(
'routes OpenCode Zen-prefixed and Kimi models to the direct OpenCode Zen provider',
'routes OpenCode Zen models and existing Kimi alias to the direct OpenCode Zen provider',
async () => {
const testCases = [
{
codebuffModel: 'moonshotai/kimi-k2.6',
upstreamModel: 'kimi-k2.6',
},
{
codebuffModel: openCodeZenModels.opencode_kimi_k2_6,
upstreamModel: 'kimi-k2.6',
Expand All @@ -880,10 +884,6 @@ describe('/api/v1/chat/completions POST endpoint', () => {
codebuffModel: openCodeZenModels.opencode_minimax_m2_7,
upstreamModel: 'minimax-m2.7',
},
{
codebuffModel: 'moonshotai/kimi-k2.6',
upstreamModel: 'kimi-k2.6',
},
]

for (const { codebuffModel, upstreamModel } of testCases) {
Expand Down
112 changes: 72 additions & 40 deletions web/src/app/api/v1/chat/completions/_post.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,12 @@ import {
handleDeepSeekStream,
isDeepSeekModel,
} from '@/llm-api/deepseek'
import {
handleMoonshotNonStream,
handleMoonshotStream,
isMoonshotModel,
MoonshotError,
} from '@/llm-api/moonshot'
import {
OpenCodeZenError,
handleOpenCodeZenNonStream,
Expand Down Expand Up @@ -616,18 +622,22 @@ export async function postChatCompletions(params: {
// Streaming request — route supported models to direct providers.
const useSiliconFlow = false // isSiliconFlowModel(typedBody.model)
const useOpenCodeZen = isOpenCodeZenModel(typedBody.model)
const useMoonshot = !useOpenCodeZen && isMoonshotModel(typedBody.model)
const useCanopyWave =
!useOpenCodeZen && isCanopyWaveModel(typedBody.model)
!useMoonshot && !useOpenCodeZen && isCanopyWaveModel(typedBody.model)
const useDeepSeek =
!useMoonshot &&
!useOpenCodeZen &&
!useCanopyWave &&
isDeepSeekModel(typedBody.model)
const useFireworks =
!useMoonshot &&
!useOpenCodeZen &&
!useCanopyWave &&
!useDeepSeek &&
isFireworksModel(typedBody.model)
const useOpenAIDirect =
!useMoonshot &&
!useOpenCodeZen &&
!useCanopyWave &&
!useDeepSeek &&
Expand All @@ -644,20 +654,22 @@ export async function postChatCompletions(params: {
}
const stream = useSiliconFlow
? await handleSiliconFlowStream(baseArgs)
: useOpenCodeZen
? await handleOpenCodeZenStream(baseArgs)
: useCanopyWave
? await handleCanopyWaveStream(baseArgs)
: useDeepSeek
? await handleDeepSeekStream(baseArgs)
: useFireworks
? await handleFireworksStream(baseArgs)
: useOpenAIDirect
? await handleOpenAIStream(baseArgs)
: await handleOpenRouterStream({
...baseArgs,
openrouterApiKey,
})
: useMoonshot
? await handleMoonshotStream(baseArgs)
: useOpenCodeZen
? await handleOpenCodeZenStream(baseArgs)
: useCanopyWave
? await handleCanopyWaveStream(baseArgs)
: useDeepSeek
? await handleDeepSeekStream(baseArgs)
: useFireworks
? await handleFireworksStream(baseArgs)
: useOpenAIDirect
? await handleOpenAIStream(baseArgs)
: await handleOpenRouterStream({
...baseArgs,
openrouterApiKey,
})

trackEvent({
event: AnalyticsEvent.CHAT_COMPLETIONS_STREAM_STARTED,
Expand All @@ -682,15 +694,22 @@ export async function postChatCompletions(params: {
const model = typedBody.model
const useSiliconFlow = false // isSiliconFlowModel(model)
const useOpenCodeZen = isOpenCodeZenModel(model)
const useCanopyWave = !useOpenCodeZen && isCanopyWaveModel(model)
const useMoonshot = !useOpenCodeZen && isMoonshotModel(model)
const useCanopyWave =
!useMoonshot && !useOpenCodeZen && isCanopyWaveModel(model)
const useDeepSeek =
!useOpenCodeZen && !useCanopyWave && isDeepSeekModel(model)
!useMoonshot &&
!useOpenCodeZen &&
!useCanopyWave &&
isDeepSeekModel(model)
const useFireworks =
!useMoonshot &&
!useOpenCodeZen &&
!useCanopyWave &&
!useDeepSeek &&
isFireworksModel(model)
const shouldUseOpenAIEndpoint =
!useMoonshot &&
!useOpenCodeZen &&
!useCanopyWave &&
!useDeepSeek &&
Expand All @@ -708,20 +727,22 @@ export async function postChatCompletions(params: {
}
const nonStreamRequest = useSiliconFlow
? handleSiliconFlowNonStream(baseArgs)
: useOpenCodeZen
? handleOpenCodeZenNonStream(baseArgs)
: useCanopyWave
? handleCanopyWaveNonStream(baseArgs)
: useDeepSeek
? handleDeepSeekNonStream(baseArgs)
: useFireworks
? handleFireworksNonStream(baseArgs)
: shouldUseOpenAIEndpoint
? handleOpenAINonStream(baseArgs)
: handleOpenRouterNonStream({
...baseArgs,
openrouterApiKey,
})
: useMoonshot
? handleMoonshotNonStream(baseArgs)
: useOpenCodeZen
? handleOpenCodeZenNonStream(baseArgs)
: useCanopyWave
? handleCanopyWaveNonStream(baseArgs)
: useDeepSeek
? handleDeepSeekNonStream(baseArgs)
: useFireworks
? handleFireworksNonStream(baseArgs)
: shouldUseOpenAIEndpoint
? handleOpenAINonStream(baseArgs)
: handleOpenRouterNonStream({
...baseArgs,
openrouterApiKey,
})
const result = await nonStreamRequest

trackEvent({
Expand Down Expand Up @@ -754,6 +775,10 @@ export async function postChatCompletions(params: {
if (error instanceof DeepSeekError) {
deepseekError = error
}
let moonshotError: MoonshotError | undefined
if (error instanceof MoonshotError) {
moonshotError = error
}
let siliconflowError: SiliconFlowError | undefined
if (error instanceof SiliconFlowError) {
siliconflowError = error
Expand All @@ -773,15 +798,17 @@ export async function postChatCompletions(params: {
? 'SiliconFlow'
: opencodeZenError
? 'OpenCode Zen'
: canopywaveError
? 'CanopyWave'
: deepseekError
? 'DeepSeek'
: fireworksError
? 'Fireworks'
: openaiError
? 'OpenAI'
: 'OpenRouter'
: moonshotError
? 'Moonshot'
: canopywaveError
? 'CanopyWave'
: deepseekError
? 'DeepSeek'
: fireworksError
? 'Fireworks'
: openaiError
? 'OpenAI'
: 'OpenRouter'
logger.error(
{
error: getErrorObject(error),
Expand All @@ -798,6 +825,7 @@ export async function postChatCompletions(params: {
providerStatusCode: (
openrouterError ??
fireworksError ??
moonshotError ??
canopywaveError ??
deepseekError ??
siliconflowError ??
Expand All @@ -807,6 +835,7 @@ export async function postChatCompletions(params: {
providerStatusText: (
openrouterError ??
fireworksError ??
moonshotError ??
canopywaveError ??
deepseekError ??
siliconflowError ??
Expand Down Expand Up @@ -840,6 +869,9 @@ export async function postChatCompletions(params: {
if (error instanceof FireworksError) {
return NextResponse.json(error.toJSON(), { status: error.statusCode })
}
if (error instanceof MoonshotError) {
return NextResponse.json(error.toJSON(), { status: error.statusCode })
}
if (error instanceof CanopyWaveError) {
return NextResponse.json(error.toJSON(), { status: error.statusCode })
}
Expand Down
82 changes: 82 additions & 0 deletions web/src/llm-api/__tests__/moonshot.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { describe, expect, it } from 'bun:test'

import { buildMoonshotRequestBody } from '../moonshot'

import type { ChatCompletionRequestBody } from '../types'

type MoonshotRequestBody = Omit<ChatCompletionRequestBody, 'messages'> & {
messages: Array<
ChatCompletionRequestBody['messages'][number] & {
reasoning_content?: string | null
}
>
}

function buildBody(body: MoonshotRequestBody) {
return buildMoonshotRequestBody(
body as ChatCompletionRequestBody,
'moonshotai/kimi-k2.6',
)
}

describe('buildMoonshotRequestBody', () => {
it('enables preserved thinking by default for Kimi K2.6', () => {
const body = buildBody({
model: 'moonshotai/kimi-k2.6',
messages: [
{
role: 'assistant',
content: 'I will inspect the files.',
reasoning_content: 'Need to understand the repo first.',
},
{
role: 'user',
content: 'Continue.',
},
],
})

expect(body.model).toBe('kimi-k2.6')
expect(body.thinking).toEqual({ type: 'enabled', keep: 'all' })
expect(body.messages).toEqual([
{
role: 'assistant',
content: 'I will inspect the files.',
reasoning_content: 'Need to understand the repo first.',
},
{
role: 'user',
content: 'Continue.',
},
])
})

it('keeps historical reasoning when thinking is explicitly enabled', () => {
const body = buildBody({
model: 'moonshotai/kimi-k2.6',
messages: [{ role: 'user', content: 'hello' }],
reasoning: { enabled: true },
})

expect(body.thinking).toEqual({ type: 'enabled', keep: 'all' })
expect(body.reasoning).toBeUndefined()
})

it('does not preserve thinking when reasoning is explicitly disabled', () => {
const body = buildBody({
model: 'moonshotai/kimi-k2.6',
messages: [
{
role: 'assistant',
content: 'Done.',
reasoning_content: 'Used the tool result.',
},
{ role: 'user', content: 'next' },
],
reasoning: { enabled: false },
})

expect(body.thinking).toEqual({ type: 'disabled' })
expect(body.reasoning).toBeUndefined()
})
})
Loading
Loading