Files
claude-code/src/services/api/openai/__tests__/thinking.test.ts
cepvor 833181e025 feat: 添加 MiMo 模型 thinking mode 自动检测与兼容
isOpenAIThinkingEnabled() 现在自动检测模型名包含 "mimo" 的模型
(与 DeepSeek 并列),因为 MiMo 同样使用 reasoning_content 字段
且支持 thinking mode。

buildOpenAIRequestBody() 在 chat_template_kwargs 中同时发送
thinking: true 和 enable_thinking: true,兼容 DeepSeek 自托管和
MiMo 的 thinking 启用格式。

已有 reasoning_content 回传逻辑(openaiConvertMessages.ts)和流
解析逻辑(openaiStreamAdapter.ts)无需修改,MiMo 与 DeepSeek 共用
相同的 reasoning_content 字段协议。

Co-Authored-By: deepseek-v4-pro[1m] <deepseek-ai@claude-code-best.win>
2026-05-14 20:17:48 +08:00

294 lines
10 KiB
TypeScript

import { describe, expect, test, beforeEach, afterEach, mock } from 'bun:test'
import {
isOpenAIThinkingEnabled,
buildOpenAIRequestBody,
} from '../requestBody.js'
// Re-register envUtils.js with correct isEnvDefinedFalsy and isEnvTruthy to
// override pollution from other test files (debug-tool-call, issue,
// break-cache, MagicDocs/prompts, SessionMemory/prompts, cacheStats) that
// mock this module without exporting isEnvDefinedFalsy.
mock.module('src/utils/envUtils.js', () => ({
isEnvTruthy: (v: string | boolean | undefined): boolean => {
if (!v) return false
if (typeof v === 'boolean') return v
return ['1', 'true', 'yes', 'on'].includes(v.toLowerCase().trim())
},
isEnvDefinedFalsy: (v: string | boolean | undefined): boolean => {
if (v === undefined) return false
if (typeof v === 'boolean') return !v
if (!v) return false
return ['0', 'false', 'no', 'off'].includes(v.toLowerCase().trim())
},
}))
describe('isOpenAIThinkingEnabled', () => {
const originalEnv = {
OPENAI_ENABLE_THINKING: process.env.OPENAI_ENABLE_THINKING,
}
beforeEach(() => {
// Clear env var before each test
delete process.env.OPENAI_ENABLE_THINKING
})
afterEach(() => {
// Restore original env var — delete key if it was originally undefined
// to avoid leaking the env key into subsequent tests
if (originalEnv.OPENAI_ENABLE_THINKING === undefined) {
delete process.env.OPENAI_ENABLE_THINKING
} else {
process.env.OPENAI_ENABLE_THINKING = originalEnv.OPENAI_ENABLE_THINKING
}
})
describe('OPENAI_ENABLE_THINKING env var', () => {
test('returns true when OPENAI_ENABLE_THINKING=1', () => {
process.env.OPENAI_ENABLE_THINKING = '1'
expect(isOpenAIThinkingEnabled('gpt-4o')).toBe(true)
})
test('returns true when OPENAI_ENABLE_THINKING=true', () => {
process.env.OPENAI_ENABLE_THINKING = 'true'
expect(isOpenAIThinkingEnabled('gpt-4o')).toBe(true)
})
test('returns true when OPENAI_ENABLE_THINKING=yes', () => {
process.env.OPENAI_ENABLE_THINKING = 'yes'
expect(isOpenAIThinkingEnabled('gpt-4o')).toBe(true)
})
test('returns true when OPENAI_ENABLE_THINKING=on', () => {
process.env.OPENAI_ENABLE_THINKING = 'on'
expect(isOpenAIThinkingEnabled('gpt-4o')).toBe(true)
})
test('returns true when OPENAI_ENABLE_THINKING=TRUE (case insensitive)', () => {
process.env.OPENAI_ENABLE_THINKING = 'TRUE'
expect(isOpenAIThinkingEnabled('gpt-4o')).toBe(true)
})
test('returns false when OPENAI_ENABLE_THINKING=0', () => {
process.env.OPENAI_ENABLE_THINKING = '0'
expect(isOpenAIThinkingEnabled('deepseek-reasoner')).toBe(false)
})
test('returns false when OPENAI_ENABLE_THINKING=false', () => {
process.env.OPENAI_ENABLE_THINKING = 'false'
expect(isOpenAIThinkingEnabled('deepseek-reasoner')).toBe(false)
})
test('returns false when OPENAI_ENABLE_THINKING is empty', () => {
process.env.OPENAI_ENABLE_THINKING = ''
expect(isOpenAIThinkingEnabled('gpt-4o')).toBe(false)
})
test('returns false when OPENAI_ENABLE_THINKING is not set', () => {
expect(isOpenAIThinkingEnabled('gpt-4o')).toBe(false)
})
})
describe('model name auto-detect', () => {
test('returns true when model name is "deepseek-reasoner"', () => {
expect(isOpenAIThinkingEnabled('deepseek-reasoner')).toBe(true)
})
test('returns true when model name contains "deepseek-reasoner" (case insensitive)', () => {
expect(isOpenAIThinkingEnabled('DeepSeek-Reasoner')).toBe(true)
})
test('returns true when model name has prefix/suffix for deepseek-reasoner', () => {
expect(isOpenAIThinkingEnabled('my-deepseek-reasoner-v1')).toBe(true)
})
test('returns true when model name is namespaced for deepseek-reasoner', () => {
expect(isOpenAIThinkingEnabled('TokenService/deepseek-reasoner')).toBe(
true,
)
})
test('returns true when model name is "deepseek-v3.2"', () => {
expect(isOpenAIThinkingEnabled('deepseek-v3.2')).toBe(true)
})
test('returns true when model name contains "deepseek-v3.2" (case insensitive)', () => {
expect(isOpenAIThinkingEnabled('DeepSeek-V3.2')).toBe(true)
})
test('returns true when model name has prefix/suffix for deepseek-v3.2', () => {
expect(isOpenAIThinkingEnabled('my-deepseek-v3.2-v1')).toBe(true)
})
test('returns true when model name is namespaced for deepseek-v3.2', () => {
expect(isOpenAIThinkingEnabled('TokenService/deepseek-v3.2')).toBe(true)
})
test('returns true when model name is "deepseek-chat"', () => {
expect(isOpenAIThinkingEnabled('deepseek-chat')).toBe(true)
})
test('returns true when model name is "deepseek-v3"', () => {
expect(isOpenAIThinkingEnabled('deepseek-v3')).toBe(true)
})
test('returns true when model name is "deepseek-v4"', () => {
expect(isOpenAIThinkingEnabled('deepseek-v4')).toBe(true)
})
test('returns true when model name is "deepseek-v4-pro"', () => {
expect(isOpenAIThinkingEnabled('deepseek-v4-pro')).toBe(true)
})
test('returns true when model name is "deepseek-r1"', () => {
expect(isOpenAIThinkingEnabled('deepseek-r1')).toBe(true)
})
test('returns true when model name contains "deepseek"', () => {
expect(isOpenAIThinkingEnabled('deepseek-coder')).toBe(true)
})
test('returns true when model name is "mimo-v2-flash"', () => {
expect(isOpenAIThinkingEnabled('mimo-v2-flash')).toBe(true)
})
test('returns true when model name is "mimo-v2-pro"', () => {
expect(isOpenAIThinkingEnabled('mimo-v2-pro')).toBe(true)
})
test('returns true when model name is "mimo-v2.5-pro"', () => {
expect(isOpenAIThinkingEnabled('mimo-v2.5-pro')).toBe(true)
})
test('returns true when model name contains "mimo"', () => {
expect(isOpenAIThinkingEnabled('MiMo-V2-Omni')).toBe(true)
})
test('returns false when model name is "gpt-4o"', () => {
expect(isOpenAIThinkingEnabled('gpt-4o')).toBe(false)
})
test('returns false when model name is empty', () => {
expect(isOpenAIThinkingEnabled('')).toBe(false)
})
})
describe('priority and combined detection', () => {
test('OPENAI_ENABLE_THINKING=1 enables thinking for any model', () => {
process.env.OPENAI_ENABLE_THINKING = '1'
expect(isOpenAIThinkingEnabled('gpt-4o')).toBe(true)
expect(isOpenAIThinkingEnabled('deepseek-v3')).toBe(true)
expect(isOpenAIThinkingEnabled('qwen-3')).toBe(true)
})
test('OPENAI_ENABLE_THINKING=false disables thinking even for deepseek-reasoner', () => {
process.env.OPENAI_ENABLE_THINKING = 'false'
expect(isOpenAIThinkingEnabled('deepseek-reasoner')).toBe(false)
})
test('OPENAI_ENABLE_THINKING=0 disables thinking even for deepseek-reasoner', () => {
process.env.OPENAI_ENABLE_THINKING = '0'
expect(isOpenAIThinkingEnabled('deepseek-reasoner')).toBe(false)
})
test('both conditions can enable thinking', () => {
process.env.OPENAI_ENABLE_THINKING = '1'
expect(isOpenAIThinkingEnabled('deepseek-reasoner')).toBe(true)
})
})
})
describe('buildOpenAIRequestBody — thinking params', () => {
const baseParams = {
model: 'deepseek-reasoner',
messages: [{ role: 'user', content: 'hello' }],
tools: [] as any[],
toolChoice: undefined as any,
} as any
test('includes official DeepSeek API thinking format when enabled', () => {
const body = buildOpenAIRequestBody({ ...baseParams, enableThinking: true })
expect(body.thinking).toEqual({ type: 'enabled' })
})
test('includes vLLM/self-hosted thinking format when enabled', () => {
const body = buildOpenAIRequestBody({ ...baseParams, enableThinking: true })
expect(body.enable_thinking).toBe(true)
expect(body.chat_template_kwargs).toEqual({
thinking: true,
enable_thinking: true,
})
})
test('includes both formats simultaneously when enabled', () => {
const body = buildOpenAIRequestBody({ ...baseParams, enableThinking: true })
expect(body.thinking).toEqual({ type: 'enabled' })
expect(body.enable_thinking).toBe(true)
expect(body.chat_template_kwargs!.thinking).toBe(true)
})
test('does NOT include thinking params when disabled', () => {
const body = buildOpenAIRequestBody({
...baseParams,
enableThinking: false,
})
expect(body.thinking).toBeUndefined()
expect(body.enable_thinking).toBeUndefined()
expect(body.chat_template_kwargs).toBeUndefined()
})
test('always includes stream and stream_options', () => {
const body = buildOpenAIRequestBody({
...baseParams,
enableThinking: false,
})
expect(body.stream).toBe(true)
expect(body.stream_options).toEqual({ include_usage: true })
})
test('includes temperature when thinking is off and override is set', () => {
const body = buildOpenAIRequestBody({
...baseParams,
enableThinking: false,
temperatureOverride: 0.7,
})
expect(body.temperature).toBe(0.7)
})
test('excludes temperature when thinking is on even if override is set', () => {
const body = buildOpenAIRequestBody({
...baseParams,
enableThinking: true,
temperatureOverride: 0.7,
})
expect(body.temperature).toBeUndefined()
})
test('excludes temperature when thinking is off and no override', () => {
const body = buildOpenAIRequestBody({
...baseParams,
enableThinking: false,
})
expect(body.temperature).toBeUndefined()
})
test('includes tools and tool_choice when tools are provided', () => {
const body = buildOpenAIRequestBody({
...baseParams,
tools: [{ type: 'function', function: { name: 'test' } }],
toolChoice: 'auto',
enableThinking: false,
})
expect(body.tools).toHaveLength(1)
expect(body.tool_choice).toBe('auto')
})
test('excludes tools when empty', () => {
const body = buildOpenAIRequestBody({
...baseParams,
enableThinking: false,
})
expect(body.tools).toBeUndefined()
expect(body.tool_choice).toBeUndefined()
})
})