mirror of
https://github.com/NanmiCoder/cc-haha
synced 2026-07-15 12:53:31 +08:00
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:
parent
8320a36195
commit
060a134ca1
@ -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')
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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())
|
||||
|
||||
132
src/services/api/claudeRequiredThinking.test.ts
Normal file
132
src/services/api/claudeRequiredThinking.test.ts
Normal 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)
|
||||
@ -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', () => {
|
||||
|
||||
@ -5,6 +5,7 @@ export type ModelCapabilityOverride =
|
||||
| 'effort'
|
||||
| 'max_effort'
|
||||
| 'thinking'
|
||||
| 'required_thinking'
|
||||
| 'adaptive_thinking'
|
||||
| 'interleaved_thinking'
|
||||
|
||||
|
||||
@ -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')
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user