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:
@@ -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'
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user