fix(provider): require thinking for Kimi K2.7 #986

Model Kimi K2.7 as required-thinking so a global disabled preference cannot generate an unsupported request. Keep K2.6 and other providers free to disable thinking.

Tested: request-shape loopback through the query pipeline
Tested: bun run check:provider-contract
Tested: bun run check:server (1608 tests)
Tested: bun run check:coverage
Not-tested: live Kimi completion blocked by insufficient provider balance
Constraint: CLI core changes require allow-cli-core-change approval for PR
Confidence: high
Scope-risk: moderate
This commit is contained in:
程序员阿江(Relakkes) 2026-07-11 01:07:22 +08:00
parent 8320a36195
commit 060a134ca1
8 changed files with 164 additions and 8 deletions

View File

@ -98,7 +98,9 @@ describe('provider presets API', () => {
expect(kimi?.authStrategy).toBe('auth_token')
expect(kimi?.defaultModels.main).toBe('kimi-k2.7-code')
expect(kimi?.defaultEnv?.CC_HAHA_SEND_DISABLED_THINKING).toBeUndefined()
expect(kimi?.defaultEnv?.ANTHROPIC_DEFAULT_SONNET_MODEL_SUPPORTED_CAPABILITIES).toBe('thinking')
expect(kimi?.defaultEnv?.ANTHROPIC_DEFAULT_SONNET_MODEL_SUPPORTED_CAPABILITIES).toBe(
'thinking,required_thinking',
)
expect(minimax?.authStrategy).toBe('auth_token')
expect(minimax?.defaultModels.main).toBe('MiniMax-M3[1m]')
expect(minimax?.defaultEnv?.CLAUDE_CODE_AUTO_COMPACT_WINDOW).toBe('1000000')

View File

@ -247,7 +247,7 @@ describe('providerRuntimeEnv', () => {
expect(kimiEnv).toMatchObject({
ANTHROPIC_BASE_URL: 'https://api.moonshot.cn/anthropic',
ANTHROPIC_MODEL: 'kimi-k2.7-code',
ANTHROPIC_DEFAULT_SONNET_MODEL_SUPPORTED_CAPABILITIES: 'thinking',
ANTHROPIC_DEFAULT_SONNET_MODEL_SUPPORTED_CAPABILITIES: 'thinking,required_thinking',
})
expect(JSON.parse(kimiEnv!.CLAUDE_CODE_MODEL_CONTEXT_WINDOWS)).toMatchObject({
'kimi-k2.7-code': 262144,

View File

@ -86,9 +86,9 @@
"apiKeyUrl": "https://platform.kimi.com/console/api-keys",
"authStrategy": "auth_token",
"defaultEnv": {
"ANTHROPIC_DEFAULT_HAIKU_MODEL_SUPPORTED_CAPABILITIES": "thinking",
"ANTHROPIC_DEFAULT_SONNET_MODEL_SUPPORTED_CAPABILITIES": "thinking",
"ANTHROPIC_DEFAULT_OPUS_MODEL_SUPPORTED_CAPABILITIES": "thinking"
"ANTHROPIC_DEFAULT_HAIKU_MODEL_SUPPORTED_CAPABILITIES": "thinking,required_thinking",
"ANTHROPIC_DEFAULT_SONNET_MODEL_SUPPORTED_CAPABILITIES": "thinking,required_thinking",
"ANTHROPIC_DEFAULT_OPUS_MODEL_SUPPORTED_CAPABILITIES": "thinking,required_thinking"
},
"modelContextWindows": {
"kimi-k2.7-code": 262144,

View File

@ -183,6 +183,7 @@ import { endQueryProfile, queryCheckpoint } from "src/utils/queryProfiler.js";
import {
modelSupportsAdaptiveThinking,
modelSupportsThinking,
resolveModelThinkingEnabled,
shouldSendExplicitDisabledThinking,
type ThinkingConfig,
} from "src/utils/thinking.js";
@ -1655,9 +1656,11 @@ async function* queryModel(
: [];
const extraBodyParams = getExtraBodyParams(bedrockBetas);
const hasThinking =
const hasThinking = resolveModelThinkingEnabled(
options.model,
thinkingConfig.type !== 'disabled' &&
!isEnvTruthy(process.env.CLAUDE_CODE_DISABLE_THINKING)
!isEnvTruthy(process.env.CLAUDE_CODE_DISABLE_THINKING),
)
const modelCanThink = modelSupportsThinking(options.model)
const sendsExplicitDisabledThinking =
!hasThinking && (modelCanThink || shouldSendExplicitDisabledThinking())

View File

@ -0,0 +1,132 @@
import { expect, test } from 'bun:test'
import { mkdtemp, rm } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { enableConfigs } from '../../utils/config.js'
import { queryWithModel } from './claude.js'
function sseEvent(name: string, data: unknown): string {
return `event: ${name}\ndata: ${JSON.stringify(data)}\n\n`
}
function successfulResponse(): string {
return [
sseEvent('message_start', {
type: 'message_start',
message: {
id: 'msg_required_thinking',
type: 'message',
role: 'assistant',
model: 'kimi-k2.7-code',
content: [],
stop_reason: null,
stop_sequence: null,
usage: { input_tokens: 1, output_tokens: 0 },
},
}),
sseEvent('content_block_start', {
type: 'content_block_start',
index: 0,
content_block: { type: 'text', text: '' },
}),
sseEvent('content_block_delta', {
type: 'content_block_delta',
index: 0,
delta: { type: 'text_delta', text: 'OK' },
}),
sseEvent('content_block_stop', { type: 'content_block_stop', index: 0 }),
sseEvent('message_delta', {
type: 'message_delta',
delta: { stop_reason: 'end_turn', stop_sequence: null },
usage: { output_tokens: 1 },
}),
sseEvent('message_stop', { type: 'message_stop' }),
].join('')
}
const ENV_KEYS = [
'NODE_ENV',
'CLAUDE_CONFIG_DIR',
'CLAUDE_CODE_USE_BEDROCK',
'CLAUDE_CODE_USE_VERTEX',
'CLAUDE_CODE_USE_FOUNDRY',
'ANTHROPIC_BASE_URL',
'ANTHROPIC_AUTH_TOKEN',
'ANTHROPIC_API_KEY',
'ANTHROPIC_MODEL',
'ANTHROPIC_DEFAULT_HAIKU_MODEL',
'ANTHROPIC_DEFAULT_SONNET_MODEL',
'ANTHROPIC_DEFAULT_OPUS_MODEL',
'ANTHROPIC_DEFAULT_HAIKU_MODEL_SUPPORTED_CAPABILITIES',
'ANTHROPIC_DEFAULT_SONNET_MODEL_SUPPORTED_CAPABILITIES',
'ANTHROPIC_DEFAULT_OPUS_MODEL_SUPPORTED_CAPABILITIES',
] as const
test('keeps required-thinking models enabled when the caller requests disabled thinking', async () => {
const requests: Array<Record<string, unknown>> = []
const server = Bun.serve({
hostname: '127.0.0.1',
port: 0,
async fetch(request) {
requests.push(await request.json() as Record<string, unknown>)
return new Response(successfulResponse(), {
headers: { 'content-type': 'text/event-stream' },
})
},
})
const configDir = await mkdtemp(join(tmpdir(), 'cc-haha-required-thinking-'))
const originalEnv = Object.fromEntries(ENV_KEYS.map((key) => [key, process.env[key]]))
const globals = globalThis as typeof globalThis & { MACRO?: { BUILD_TIME: string } }
const originalMacro = globals.MACRO
try {
globals.MACRO = { BUILD_TIME: '' }
process.env.NODE_ENV = 'production'
process.env.CLAUDE_CONFIG_DIR = configDir
delete process.env.CLAUDE_CODE_USE_BEDROCK
delete process.env.CLAUDE_CODE_USE_VERTEX
delete process.env.CLAUDE_CODE_USE_FOUNDRY
process.env.ANTHROPIC_BASE_URL = `http://127.0.0.1:${server.port}`
process.env.ANTHROPIC_AUTH_TOKEN = 'loopback-test-key'
process.env.ANTHROPIC_API_KEY = ''
process.env.ANTHROPIC_MODEL = 'kimi-k2.7-code'
process.env.ANTHROPIC_DEFAULT_HAIKU_MODEL = 'kimi-k2.7-code'
process.env.ANTHROPIC_DEFAULT_SONNET_MODEL = 'kimi-k2.7-code'
process.env.ANTHROPIC_DEFAULT_OPUS_MODEL = 'kimi-k2.7-code'
process.env.ANTHROPIC_DEFAULT_HAIKU_MODEL_SUPPORTED_CAPABILITIES =
'thinking,required_thinking'
process.env.ANTHROPIC_DEFAULT_SONNET_MODEL_SUPPORTED_CAPABILITIES =
'thinking,required_thinking'
process.env.ANTHROPIC_DEFAULT_OPUS_MODEL_SUPPORTED_CAPABILITIES =
'thinking,required_thinking'
enableConfigs()
const result = await queryWithModel({
userPrompt: 'Reply exactly OK',
signal: new AbortController().signal,
options: {
model: 'kimi-k2.7-code',
querySource: 'insights',
agents: [],
isNonInteractiveSession: true,
hasAppendSystemPrompt: false,
mcpTools: [],
},
})
expect(result.message.content).toEqual([{ type: 'text', text: 'OK' }])
expect(requests).toHaveLength(1)
expect(requests[0]?.model).toBe('kimi-k2.7-code')
expect(requests[0]?.thinking).toMatchObject({ type: 'enabled' })
} finally {
for (const key of ENV_KEYS) {
const value = originalEnv[key]
if (value === undefined) delete process.env[key]
else process.env[key] = value
}
if (originalMacro === undefined) delete globals.MACRO
else globals.MACRO = originalMacro
server.stop(true)
await rm(configDir, { recursive: true, force: true })
}
}, 10_000)

View File

@ -4,7 +4,9 @@ import { resolveSideQueryThinkingConfig } from '../sideQuery.js'
import { modelSupportsEffort, modelSupportsMaxEffort } from '../effort.js'
import {
modelSupportsAdaptiveThinking,
modelRequiresThinking,
modelSupportsThinking,
resolveModelThinkingEnabled,
shouldSendExplicitDisabledThinking,
} from '../thinking.js'
@ -109,13 +111,18 @@ describe('provider-aware thinking support', () => {
test('Kimi preset models declare thinking without effort passthrough', () => {
process.env.ANTHROPIC_BASE_URL = 'https://api.moonshot.cn/anthropic'
process.env.ANTHROPIC_DEFAULT_SONNET_MODEL = 'kimi-k2.7-code'
process.env.ANTHROPIC_DEFAULT_SONNET_MODEL_SUPPORTED_CAPABILITIES = 'thinking'
process.env.ANTHROPIC_DEFAULT_SONNET_MODEL_SUPPORTED_CAPABILITIES =
'thinking,required_thinking'
clearCapabilityCache()
expect(modelSupportsThinking('kimi-k2.7-code')).toBe(true)
expect(modelSupportsAdaptiveThinking('kimi-k2.7-code')).toBe(false)
expect(modelRequiresThinking('kimi-k2.7-code')).toBe(true)
expect(resolveModelThinkingEnabled('kimi-k2.7-code', false)).toBe(true)
expect(modelSupportsEffort('kimi-k2.7-code')).toBe(false)
expect(modelSupportsMaxEffort('kimi-k2.7-code')).toBe(false)
expect(modelRequiresThinking('kimi-k2.6')).toBe(false)
expect(resolveModelThinkingEnabled('kimi-k2.6', false)).toBe(false)
})
test('third-party base URLs do not default unknown model names to effort support', () => {

View File

@ -5,6 +5,7 @@ export type ModelCapabilityOverride =
| 'effort'
| 'max_effort'
| 'thinking'
| 'required_thinking'
| 'adaptive_thinking'
| 'interleaved_thinking'

View File

@ -116,6 +116,17 @@ export function modelSupportsThinking(model: string): boolean {
return canonical.includes('sonnet-4') || canonical.includes('opus-4')
}
export function modelRequiresThinking(model: string): boolean {
return get3PModelCapabilityOverride(model, 'required_thinking') === true
}
export function resolveModelThinkingEnabled(
model: string,
requestedEnabled: boolean,
): boolean {
return modelRequiresThinking(model) || requestedEnabled
}
function isMiniMaxAnthropicEndpoint(): boolean {
const baseUrl = process.env.ANTHROPIC_BASE_URL?.toLowerCase() ?? ''
return baseUrl.includes('minimax') || baseUrl.includes('minimaxi')