Add Kimi Code provider preset and rename Moonshot API preset (#862)

* Add Kimi Code provider preset

* fix desc.

Co-authored-by: Copilot <copilot@github.com>

* more desc. fixes.

* Fix release validation tests

---------

Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
JATMN
2026-04-24 21:36:54 -07:00
committed by GitHub
parent 26413f6d30
commit 9070220292
18 changed files with 296 additions and 56 deletions

View File

@@ -3563,6 +3563,107 @@ test('Moonshot: cn host is also detected', async () => {
expect(requestBody?.store).toBeUndefined()
})
test('Kimi Code endpoint inherits Moonshot max_tokens/store compatibility', async () => {
process.env.OPENAI_BASE_URL = 'https://api.kimi.com/coding/v1'
process.env.OPENAI_API_KEY = 'sk-kimi-test'
let requestBody: Record<string, unknown> | undefined
globalThis.fetch = (async (_input, init) => {
requestBody = JSON.parse(String(init?.body))
return new Response(
JSON.stringify({
id: 'chatcmpl-1',
model: 'kimi-for-coding',
choices: [
{ message: { role: 'assistant', content: 'ok' }, finish_reason: 'stop' },
],
usage: { prompt_tokens: 3, completion_tokens: 1, total_tokens: 4 },
}),
{ headers: { 'Content-Type': 'application/json' } },
)
}) as FetchType
const client = createOpenAIShimClient({}) as OpenAIShimClient
await client.beta.messages.create({
model: 'kimi-for-coding',
system: 'you are kimi code',
messages: [{ role: 'user', content: 'hi' }],
max_tokens: 256,
stream: false,
})
expect(requestBody?.max_tokens).toBe(256)
expect(requestBody?.max_completion_tokens).toBeUndefined()
expect(requestBody?.store).toBeUndefined()
})
test('Kimi Code endpoint echoes reasoning_content on assistant tool-call messages', async () => {
process.env.OPENAI_BASE_URL = 'https://api.kimi.com/coding/v1'
process.env.OPENAI_API_KEY = 'sk-kimi-test'
let requestBody: Record<string, unknown> | undefined
globalThis.fetch = (async (_input, init) => {
requestBody = JSON.parse(String(init?.body))
return new Response(
JSON.stringify({
id: 'chatcmpl-1',
model: 'kimi-for-coding',
choices: [
{ message: { role: 'assistant', content: 'ok' }, finish_reason: 'stop' },
],
usage: { prompt_tokens: 3, completion_tokens: 1, total_tokens: 4 },
}),
{ headers: { 'Content-Type': 'application/json' } },
)
}) as FetchType
const client = createOpenAIShimClient({}) as OpenAIShimClient
await client.beta.messages.create({
model: 'kimi-for-coding',
system: 'you are kimi code',
messages: [
{ role: 'user', content: 'check the logs' },
{
role: 'assistant',
content: [
{
type: 'thinking',
thinking: 'Need to inspect logs via Bash; running a cat.',
},
{ type: 'text', text: "I'll inspect the logs." },
{
type: 'tool_use',
id: 'call_bash_1',
name: 'Bash',
input: { command: 'cat /tmp/app.log' },
},
],
},
{
role: 'user',
content: [
{
type: 'tool_result',
tool_use_id: 'call_bash_1',
content: 'log line 1\nlog line 2',
},
],
},
],
max_tokens: 256,
stream: false,
})
const messages = requestBody?.messages as Array<Record<string, unknown>>
const assistantWithToolCall = messages.find(
m => m.role === 'assistant' && Array.isArray(m.tool_calls),
)
expect(assistantWithToolCall).toBeDefined()
expect(assistantWithToolCall?.reasoning_content).toBe(
'Need to inspect logs via Bash; running a cat.',
)
})
test('DeepSeek sends thinking toggle and normalized reasoning effort', async () => {
process.env.OPENAI_BASE_URL = 'https://api.deepseek.com/v1'
process.env.OPENAI_API_KEY = 'sk-deepseek'

View File

@@ -88,6 +88,7 @@ const MOONSHOT_API_HOSTS = new Set([
'api.moonshot.ai',
'api.moonshot.cn',
])
const KIMI_CODE_API_HOST = 'api.kimi.com'
const DEEPSEEK_API_HOSTS = new Set([
'api.deepseek.com',
])
@@ -156,10 +157,16 @@ function hasGeminiApiHost(baseUrl: string | undefined): boolean {
}
}
function isMoonshotBaseUrl(baseUrl: string | undefined): boolean {
function isMoonshotCompatibleBaseUrl(baseUrl: string | undefined): boolean {
if (!baseUrl) return false
try {
return MOONSHOT_API_HOSTS.has(new URL(baseUrl).hostname.toLowerCase())
const parsed = new URL(baseUrl)
const hostname = parsed.hostname.toLowerCase()
return (
MOONSHOT_API_HOSTS.has(hostname) ||
(hostname === KIMI_CODE_API_HOST &&
parsed.pathname.toLowerCase().startsWith('/coding'))
)
} catch {
return false
}
@@ -516,7 +523,7 @@ function convertMessages(
})(),
}
// Providers that validate reasoning continuity (Moonshot: "thinking
// Providers that validate reasoning continuity (Moonshot/Kimi Code: "thinking
// is enabled but reasoning_content is missing in assistant tool call
// message at index N" 400) need the original chain-of-thought echoed
// back on each assistant message that carries a tool_call. We kept
@@ -1504,12 +1511,13 @@ class OpenAIShimMessages {
request.resolvedModel,
)
const openaiMessages = convertMessages(compressedMessages, params.system, {
// Moonshot requires every assistant tool-call message to carry
// Moonshot/Kimi Code requires every assistant tool-call message to carry
// reasoning_content when its thinking feature is active. DeepSeek does
// the same for tool-call turns in thinking mode. Echo it back from the
// thinking block we captured on the inbound response.
preserveReasoningContent:
isMoonshotBaseUrl(request.baseUrl) || isDeepSeekBaseUrl(request.baseUrl),
isMoonshotCompatibleBaseUrl(request.baseUrl) ||
isDeepSeekBaseUrl(request.baseUrl),
})
const body: Record<string, unknown> = {
@@ -1546,7 +1554,7 @@ class OpenAIShimMessages {
const isGithubCopilot = isGithub && githubEndpointType === 'copilot'
const isGithubModels = isGithub && (githubEndpointType === 'models' || githubEndpointType === 'custom')
const isMoonshot = isMoonshotBaseUrl(request.baseUrl)
const isMoonshot = isMoonshotCompatibleBaseUrl(request.baseUrl)
const isDeepSeek = isDeepSeekBaseUrl(request.baseUrl)
if ((isGithub || isMistral || isLocal || isMoonshot || isDeepSeek) && body.max_completion_tokens !== undefined) {
@@ -1556,9 +1564,10 @@ class OpenAIShimMessages {
// mistral and gemini don't recognize body.store — Gemini returns 400
// "Invalid JSON payload received. Unknown name 'store': Cannot find field."
// Moonshot (api.moonshot.ai/.cn) has not published support for the
// parameter either; strip it preemptively to avoid the same class of
// error on strict-parse providers.
// Moonshot direct API, Kimi Code's OpenAI-compatible coding endpoint,
// and DeepSeek have not published support for the parameter either;
// strip it preemptively to avoid the same class of error on strict-parse
// providers.
if (isMistral || isGeminiMode() || isMoonshot || isDeepSeek) {
delete body.store
}

View File

@@ -3,6 +3,8 @@ import { getAutoFixConfig } from './autoFixConfig.js'
import { shouldRunAutoFix, buildAutoFixContext } from './autoFixHook.js'
import { runAutoFixCheck } from './autoFixRunner.js'
const TEST_CWD = process.cwd()
describe('autoFix end-to-end flow', () => {
test('full flow: config → shouldRun → check → context', async () => {
const config = getAutoFixConfig({
@@ -19,7 +21,7 @@ describe('autoFix end-to-end flow', () => {
test: config!.test,
timeout: config!.timeout,
cwd: '/tmp',
cwd: TEST_CWD,
})
expect(result.hasErrors).toBe(true)
@@ -39,7 +41,7 @@ describe('autoFix end-to-end flow', () => {
lint: config!.lint,
timeout: config!.timeout,
cwd: '/tmp',
cwd: TEST_CWD,
})
expect(result.hasErrors).toBe(false)
const context = buildAutoFixContext(result)

View File

@@ -5,13 +5,15 @@ import {
type AutoFixCheckOptions,
} from './autoFixRunner.js'
const TEST_CWD = process.cwd()
describe('runAutoFixCheck', () => {
test('returns success when lint command exits 0', async () => {
const result = await runAutoFixCheck({
lint: 'echo "all clean"',
timeout: 5000,
cwd: '/tmp',
cwd: TEST_CWD,
})
expect(result.hasErrors).toBe(false)
expect(result.lintOutput).toContain('all clean')
@@ -23,7 +25,7 @@ describe('runAutoFixCheck', () => {
lint: 'echo "error: unused var" && exit 1',
timeout: 5000,
cwd: '/tmp',
cwd: TEST_CWD,
})
expect(result.hasErrors).toBe(true)
expect(result.lintOutput).toContain('unused var')
@@ -35,7 +37,7 @@ describe('runAutoFixCheck', () => {
test: 'echo "FAIL test_foo" && exit 1',
timeout: 5000,
cwd: '/tmp',
cwd: TEST_CWD,
})
expect(result.hasErrors).toBe(true)
expect(result.testOutput).toContain('FAIL test_foo')
@@ -48,7 +50,7 @@ describe('runAutoFixCheck', () => {
test: 'echo "test ok"',
timeout: 5000,
cwd: '/tmp',
cwd: TEST_CWD,
})
expect(result.hasErrors).toBe(false)
expect(result.lintOutput).toContain('lint ok')
@@ -61,7 +63,7 @@ describe('runAutoFixCheck', () => {
test: 'echo "should not run"',
timeout: 5000,
cwd: '/tmp',
cwd: TEST_CWD,
})
expect(result.hasErrors).toBe(true)
expect(result.lintOutput).toContain('lint error')
@@ -73,7 +75,7 @@ describe('runAutoFixCheck', () => {
lint: 'node -e "setTimeout(() => {}, 10000)"',
timeout: 100,
cwd: '/tmp',
cwd: TEST_CWD,
})
expect(result.hasErrors).toBe(true)
expect(result.timedOut).toBe(true)
@@ -83,7 +85,7 @@ describe('runAutoFixCheck', () => {
const result = await runAutoFixCheck({
timeout: 5000,
cwd: '/tmp',
cwd: TEST_CWD,
})
expect(result.hasErrors).toBe(false)
})
@@ -93,7 +95,7 @@ describe('runAutoFixCheck', () => {
lint: 'echo "src/foo.ts:10:5 error no-unused-vars" && exit 1',
timeout: 5000,
cwd: '/tmp',
cwd: TEST_CWD,
})
expect(result.hasErrors).toBe(true)
const summary = result.errorSummary