mirror of
https://github.com/NanmiCoder/cc-haha
synced 2026-07-19 13:33:35 +08:00
fix: prevent third-party thinking mode mismatches
Desktop users can now disable thinking for new sessions, and Anthropic-compatible providers that opt in receive explicit disabled-thinking requests across main turns, side queries, and AI title generation. Constraint: DeepSeek/Kimi/GLM Anthropic-compatible endpoints need a non-thinking path without scattering provider-specific logic through the CLI. Rejected: Per-model if/else branches | centralized provider preset env keeps the native CLI surface smaller and easier to audit. Confidence: high Scope-risk: moderate Tested: bun test src/utils/__tests__/thinking.test.ts src/server/__tests__/title-service.test.ts src/server/__tests__/provider-presets.test.ts src/server/__tests__/conversations.test.ts Tested: cd desktop && bun run test -- generalSettings.test.tsx Tested: cd desktop && bun run lint Tested: cd desktop && bun run build Tested: agent-browser DeepSeek desktop E2E with transparent proxy captured main and title requests with thinking.type=disabled
This commit is contained in:
parent
dcf844b7e7
commit
9ecc178c7a
@ -106,7 +106,11 @@ describe('Settings > General tab', () => {
|
|||||||
|
|
||||||
useSettingsStore.setState({
|
useSettingsStore.setState({
|
||||||
locale: 'en',
|
locale: 'en',
|
||||||
|
thinkingEnabled: true,
|
||||||
skipWebFetchPreflight: true,
|
skipWebFetchPreflight: true,
|
||||||
|
setThinkingEnabled: vi.fn().mockImplementation(async (enabled: boolean) => {
|
||||||
|
useSettingsStore.setState({ thinkingEnabled: enabled })
|
||||||
|
}),
|
||||||
setSkipWebFetchPreflight: vi.fn().mockImplementation(async (enabled: boolean) => {
|
setSkipWebFetchPreflight: vi.fn().mockImplementation(async (enabled: boolean) => {
|
||||||
useSettingsStore.setState({ skipWebFetchPreflight: enabled })
|
useSettingsStore.setState({ skipWebFetchPreflight: enabled })
|
||||||
}),
|
}),
|
||||||
@ -150,6 +154,18 @@ describe('Settings > General tab', () => {
|
|||||||
expect(useSettingsStore.getState().setSkipWebFetchPreflight).toHaveBeenCalledWith(false)
|
expect(useSettingsStore.getState().setSkipWebFetchPreflight).toHaveBeenCalledWith(false)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('lets the user disable thinking mode for new sessions', () => {
|
||||||
|
render(<Settings />)
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByText('General'))
|
||||||
|
|
||||||
|
const toggle = screen.getByLabelText('Enable thinking mode')
|
||||||
|
expect(toggle).toBeChecked()
|
||||||
|
fireEvent.click(toggle)
|
||||||
|
|
||||||
|
expect(useSettingsStore.getState().setThinkingEnabled).toHaveBeenCalledWith(false)
|
||||||
|
})
|
||||||
|
|
||||||
it('keeps extension tabs available alongside the terminal tab', () => {
|
it('keeps extension tabs available alongside the terminal tab', () => {
|
||||||
render(<Settings />)
|
render(<Settings />)
|
||||||
|
|
||||||
|
|||||||
@ -482,6 +482,10 @@ export const en = {
|
|||||||
'settings.general.effort.medium': 'Medium',
|
'settings.general.effort.medium': 'Medium',
|
||||||
'settings.general.effort.high': 'High',
|
'settings.general.effort.high': 'High',
|
||||||
'settings.general.effort.max': 'Max',
|
'settings.general.effort.max': 'Max',
|
||||||
|
'settings.general.thinkingTitle': 'Thinking Mode',
|
||||||
|
'settings.general.thinkingDescription': 'Controls whether new sessions start with model thinking enabled. When off, compatible providers such as DeepSeek receive an explicit non-thinking parameter.',
|
||||||
|
'settings.general.thinkingEnabled': 'Enable thinking mode',
|
||||||
|
'settings.general.thinkingHint': 'Turn this off to start new sessions with --thinking disabled; useful for DeepSeek V4 Flash/Pro and other non-thinking workflows.',
|
||||||
'settings.general.webFetchPreflightTitle': 'WebFetch Preflight',
|
'settings.general.webFetchPreflightTitle': 'WebFetch Preflight',
|
||||||
'settings.general.webFetchPreflightDescription': 'Desktop sessions skip Claude\'s domain preflight by default to avoid false failures on third-party providers and restricted networks.',
|
'settings.general.webFetchPreflightDescription': 'Desktop sessions skip Claude\'s domain preflight by default to avoid false failures on third-party providers and restricted networks.',
|
||||||
'settings.general.webFetchPreflightEnabled': 'Skip WebFetch domain preflight',
|
'settings.general.webFetchPreflightEnabled': 'Skip WebFetch domain preflight',
|
||||||
|
|||||||
@ -484,6 +484,10 @@ export const zh: Record<TranslationKey, string> = {
|
|||||||
'settings.general.effort.medium': '中',
|
'settings.general.effort.medium': '中',
|
||||||
'settings.general.effort.high': '高',
|
'settings.general.effort.high': '高',
|
||||||
'settings.general.effort.max': '最大',
|
'settings.general.effort.max': '最大',
|
||||||
|
'settings.general.thinkingTitle': '思考模式',
|
||||||
|
'settings.general.thinkingDescription': '控制新会话是否启用模型思考。关闭后,DeepSeek 等兼容供应商会收到显式非思考模式参数。',
|
||||||
|
'settings.general.thinkingEnabled': '启用思考模式',
|
||||||
|
'settings.general.thinkingHint': '关闭后会以 --thinking disabled 启动新会话;适合 DeepSeek V4 Flash/Pro 等需要非思考模式的模型。',
|
||||||
'settings.general.webFetchPreflightTitle': 'WebFetch 预检',
|
'settings.general.webFetchPreflightTitle': 'WebFetch 预检',
|
||||||
'settings.general.webFetchPreflightDescription': '桌面端默认跳过 Claude 的域名预检,避免第三方服务商或受限网络下出现误报失败。',
|
'settings.general.webFetchPreflightDescription': '桌面端默认跳过 Claude 的域名预检,避免第三方服务商或受限网络下出现误报失败。',
|
||||||
'settings.general.webFetchPreflightEnabled': '跳过 WebFetch 域名预检',
|
'settings.general.webFetchPreflightEnabled': '跳过 WebFetch 域名预检',
|
||||||
|
|||||||
@ -872,6 +872,8 @@ function GeneralSettings() {
|
|||||||
const {
|
const {
|
||||||
effortLevel,
|
effortLevel,
|
||||||
setEffort,
|
setEffort,
|
||||||
|
thinkingEnabled,
|
||||||
|
setThinkingEnabled,
|
||||||
locale,
|
locale,
|
||||||
setLocale,
|
setLocale,
|
||||||
theme,
|
theme,
|
||||||
@ -957,6 +959,28 @@ function GeneralSettings() {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-8">
|
||||||
|
<h2 className="text-base font-semibold text-[var(--color-text-primary)] mb-1">{t('settings.general.thinkingTitle')}</h2>
|
||||||
|
<p className="text-sm text-[var(--color-text-tertiary)] mb-3">{t('settings.general.thinkingDescription')}</p>
|
||||||
|
<label className="flex items-start gap-3 rounded-xl border border-[var(--color-border)] bg-[var(--color-surface-container-low)] px-4 py-3 cursor-pointer hover:border-[var(--color-border-focus)] transition-colors">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
aria-label={t('settings.general.thinkingEnabled')}
|
||||||
|
checked={thinkingEnabled}
|
||||||
|
onChange={(e) => void setThinkingEnabled(e.target.checked)}
|
||||||
|
className="mt-0.5 h-4 w-4 rounded border-[var(--color-border)] text-[var(--color-brand)] focus:ring-[var(--color-brand)]"
|
||||||
|
/>
|
||||||
|
<div className="min-w-0">
|
||||||
|
<div className="text-sm font-medium text-[var(--color-text-primary)]">
|
||||||
|
{t('settings.general.thinkingEnabled')}
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-[var(--color-text-tertiary)] mt-1 leading-5">
|
||||||
|
{t('settings.general.thinkingHint')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="mt-8">
|
<div className="mt-8">
|
||||||
<h2 className="text-base font-semibold text-[var(--color-text-primary)] mb-1">{t('settings.general.webFetchPreflightTitle')}</h2>
|
<h2 className="text-base font-semibold text-[var(--color-text-primary)] mb-1">{t('settings.general.webFetchPreflightTitle')}</h2>
|
||||||
<p className="text-sm text-[var(--color-text-tertiary)] mb-3">{t('settings.general.webFetchPreflightDescription')}</p>
|
<p className="text-sm text-[var(--color-text-tertiary)] mb-3">{t('settings.general.webFetchPreflightDescription')}</p>
|
||||||
|
|||||||
@ -19,6 +19,7 @@ type SettingsStore = {
|
|||||||
permissionMode: PermissionMode
|
permissionMode: PermissionMode
|
||||||
currentModel: ModelInfo | null
|
currentModel: ModelInfo | null
|
||||||
effortLevel: EffortLevel
|
effortLevel: EffortLevel
|
||||||
|
thinkingEnabled: boolean
|
||||||
availableModels: ModelInfo[]
|
availableModels: ModelInfo[]
|
||||||
activeProviderName: string | null
|
activeProviderName: string | null
|
||||||
locale: Locale
|
locale: Locale
|
||||||
@ -31,6 +32,7 @@ type SettingsStore = {
|
|||||||
setPermissionMode: (mode: PermissionMode) => Promise<void>
|
setPermissionMode: (mode: PermissionMode) => Promise<void>
|
||||||
setModel: (modelId: string) => Promise<void>
|
setModel: (modelId: string) => Promise<void>
|
||||||
setEffort: (level: EffortLevel) => Promise<void>
|
setEffort: (level: EffortLevel) => Promise<void>
|
||||||
|
setThinkingEnabled: (enabled: boolean) => Promise<void>
|
||||||
setLocale: (locale: Locale) => void
|
setLocale: (locale: Locale) => void
|
||||||
setTheme: (theme: ThemeMode) => Promise<void>
|
setTheme: (theme: ThemeMode) => Promise<void>
|
||||||
setSkipWebFetchPreflight: (enabled: boolean) => Promise<void>
|
setSkipWebFetchPreflight: (enabled: boolean) => Promise<void>
|
||||||
@ -40,6 +42,7 @@ export const useSettingsStore = create<SettingsStore>((set, get) => ({
|
|||||||
permissionMode: 'default',
|
permissionMode: 'default',
|
||||||
currentModel: null,
|
currentModel: null,
|
||||||
effortLevel: 'medium',
|
effortLevel: 'medium',
|
||||||
|
thinkingEnabled: true,
|
||||||
availableModels: [],
|
availableModels: [],
|
||||||
activeProviderName: null,
|
activeProviderName: null,
|
||||||
locale: getStoredLocale(),
|
locale: getStoredLocale(),
|
||||||
@ -66,6 +69,7 @@ export const useSettingsStore = create<SettingsStore>((set, get) => ({
|
|||||||
activeProviderName: modelsRes.provider?.name ?? null,
|
activeProviderName: modelsRes.provider?.name ?? null,
|
||||||
currentModel: model,
|
currentModel: model,
|
||||||
effortLevel: level,
|
effortLevel: level,
|
||||||
|
thinkingEnabled: userSettings.alwaysThinkingEnabled !== false,
|
||||||
theme,
|
theme,
|
||||||
skipWebFetchPreflight: userSettings.skipWebFetchPreflight !== false,
|
skipWebFetchPreflight: userSettings.skipWebFetchPreflight !== false,
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
@ -105,6 +109,16 @@ export const useSettingsStore = create<SettingsStore>((set, get) => ({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
setThinkingEnabled: async (enabled) => {
|
||||||
|
const prev = get().thinkingEnabled
|
||||||
|
set({ thinkingEnabled: enabled })
|
||||||
|
try {
|
||||||
|
await settingsApi.updateUser({ alwaysThinkingEnabled: enabled ? undefined : false })
|
||||||
|
} catch {
|
||||||
|
set({ thinkingEnabled: prev })
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
setLocale: (locale) => {
|
setLocale: (locale) => {
|
||||||
set({ locale })
|
set({ locale })
|
||||||
try { localStorage.setItem(LOCALE_STORAGE_KEY, locale) } catch { /* noop */ }
|
try { localStorage.setItem(LOCALE_STORAGE_KEY, locale) } catch { /* noop */ }
|
||||||
|
|||||||
@ -16,6 +16,7 @@ export type UserSettings = {
|
|||||||
model?: string
|
model?: string
|
||||||
modelContext?: string
|
modelContext?: string
|
||||||
effort?: EffortLevel
|
effort?: EffortLevel
|
||||||
|
alwaysThinkingEnabled?: boolean
|
||||||
permissionMode?: PermissionMode
|
permissionMode?: PermissionMode
|
||||||
theme?: ThemeMode
|
theme?: ThemeMode
|
||||||
skipWebFetchPreflight?: boolean
|
skipWebFetchPreflight?: boolean
|
||||||
|
|||||||
@ -138,6 +138,22 @@ describe('ConversationService', () => {
|
|||||||
])
|
])
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should pass disabled thinking to the CLI runtime args', () => {
|
||||||
|
const svc = new ConversationService()
|
||||||
|
expect((svc as any).getRuntimeArgs({
|
||||||
|
model: 'deepseek-v4-pro',
|
||||||
|
effort: 'medium',
|
||||||
|
thinking: 'disabled',
|
||||||
|
})).toEqual([
|
||||||
|
'--model',
|
||||||
|
'deepseek-v4-pro',
|
||||||
|
'--effort',
|
||||||
|
'medium',
|
||||||
|
'--thinking',
|
||||||
|
'disabled',
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
it('should return false when sending interrupt to non-existent session', () => {
|
it('should return false when sending interrupt to non-existent session', () => {
|
||||||
const svc = new ConversationService()
|
const svc = new ConversationService()
|
||||||
const result = svc.sendInterrupt('no-such-session')
|
const result = svc.sendInterrupt('no-such-session')
|
||||||
@ -632,6 +648,41 @@ describe('WebSocket Chat Integration', () => {
|
|||||||
expect(statusMsgs[0].state).toBe('thinking')
|
expect(statusMsgs[0].state).toBe('thinking')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should start desktop sessions with disabled thinking when configured', async () => {
|
||||||
|
const sessionId = `chat-thinking-disabled-${crypto.randomUUID()}`
|
||||||
|
const originalStartSession = conversationService.startSession.bind(conversationService)
|
||||||
|
const startOptions: Array<{ thinking?: string; model?: string }> = []
|
||||||
|
|
||||||
|
conversationService.startSession = (async function patchedStartSession(
|
||||||
|
sid: string,
|
||||||
|
workDir: string,
|
||||||
|
sdkUrl: string,
|
||||||
|
options?: { permissionMode?: string; model?: string; effort?: string; thinking?: 'enabled' | 'adaptive' | 'disabled'; providerId?: string | null },
|
||||||
|
) {
|
||||||
|
if (sid === sessionId) {
|
||||||
|
startOptions.push({ thinking: options?.thinking, model: options?.model })
|
||||||
|
}
|
||||||
|
return originalStartSession(sid, workDir, sdkUrl, options)
|
||||||
|
}) as typeof conversationService.startSession
|
||||||
|
|
||||||
|
try {
|
||||||
|
await fs.writeFile(
|
||||||
|
path.join(tmpDir, 'settings.json'),
|
||||||
|
JSON.stringify({ alwaysThinkingEnabled: false }, null, 2),
|
||||||
|
'utf-8',
|
||||||
|
)
|
||||||
|
|
||||||
|
const messages = await runTurn(sessionId, 'Hello without thinking')
|
||||||
|
|
||||||
|
expect(messages.some((m) => m.type === 'message_complete')).toBe(true)
|
||||||
|
expect(startOptions).toEqual([{ thinking: 'disabled', model: undefined }])
|
||||||
|
} finally {
|
||||||
|
conversationService.startSession = originalStartSession as typeof conversationService.startSession
|
||||||
|
conversationService.stopSession(sessionId)
|
||||||
|
await fs.writeFile(path.join(tmpDir, 'settings.json'), '{}\n', 'utf-8')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
it('should continue chat when SDK init arrives only after the first user turn', async () => {
|
it('should continue chat when SDK init arrives only after the first user turn', async () => {
|
||||||
const messages = await withMockInitMode('on_first_user', () =>
|
const messages = await withMockInitMode('on_first_user', () =>
|
||||||
runTurn('chat-test-lazy-init', 'Hello after lazy init'),
|
runTurn('chat-test-lazy-init', 'Hello after lazy init'),
|
||||||
@ -901,7 +952,7 @@ describe('WebSocket Chat Integration', () => {
|
|||||||
sid: string,
|
sid: string,
|
||||||
workDir: string,
|
workDir: string,
|
||||||
sdkUrl: string,
|
sdkUrl: string,
|
||||||
options?: { permissionMode?: string; model?: string; effort?: string; providerId?: string | null },
|
options?: { permissionMode?: string; model?: string; effort?: string; thinking?: 'enabled' | 'adaptive' | 'disabled'; providerId?: string | null },
|
||||||
) {
|
) {
|
||||||
startCalls.push({ sessionId: sid })
|
startCalls.push({ sessionId: sid })
|
||||||
return originalStartSession(sid, workDir, sdkUrl, options)
|
return originalStartSession(sid, workDir, sdkUrl, options)
|
||||||
@ -1037,7 +1088,7 @@ describe('WebSocket Chat Integration', () => {
|
|||||||
sid: string,
|
sid: string,
|
||||||
workDir: string,
|
workDir: string,
|
||||||
sdkUrl: string,
|
sdkUrl: string,
|
||||||
options?: { permissionMode?: string; model?: string; effort?: string; providerId?: string | null },
|
options?: { permissionMode?: string; model?: string; effort?: string; thinking?: 'enabled' | 'adaptive' | 'disabled'; providerId?: string | null },
|
||||||
) {
|
) {
|
||||||
startCalls.push({ sessionId: sid, options })
|
startCalls.push({ sessionId: sid, options })
|
||||||
return originalStartSession(sid, workDir, sdkUrl, options)
|
return originalStartSession(sid, workDir, sdkUrl, options)
|
||||||
@ -1102,7 +1153,7 @@ describe('WebSocket Chat Integration', () => {
|
|||||||
sid: string,
|
sid: string,
|
||||||
workDir: string,
|
workDir: string,
|
||||||
sdkUrl: string,
|
sdkUrl: string,
|
||||||
options?: { permissionMode?: string; model?: string; effort?: string; providerId?: string | null },
|
options?: { permissionMode?: string; model?: string; effort?: string; thinking?: 'enabled' | 'adaptive' | 'disabled'; providerId?: string | null },
|
||||||
) {
|
) {
|
||||||
startCalls.push({ sessionId: sid, options })
|
startCalls.push({ sessionId: sid, options })
|
||||||
if (startCalls.length === 1) {
|
if (startCalls.length === 1) {
|
||||||
@ -1205,7 +1256,7 @@ describe('WebSocket Chat Integration', () => {
|
|||||||
sid: string,
|
sid: string,
|
||||||
workDir: string,
|
workDir: string,
|
||||||
sdkUrl: string,
|
sdkUrl: string,
|
||||||
options?: { permissionMode?: string; model?: string; effort?: string; providerId?: string | null },
|
options?: { permissionMode?: string; model?: string; effort?: string; thinking?: 'enabled' | 'adaptive' | 'disabled'; providerId?: string | null },
|
||||||
) {
|
) {
|
||||||
startCalls.push({ sessionId: sid, options })
|
startCalls.push({ sessionId: sid, options })
|
||||||
return originalStartSession(sid, workDir, sdkUrl, options)
|
return originalStartSession(sid, workDir, sdkUrl, options)
|
||||||
@ -1386,7 +1437,7 @@ describe('WebSocket Chat Integration', () => {
|
|||||||
sid: string,
|
sid: string,
|
||||||
workDir: string,
|
workDir: string,
|
||||||
sdkUrl: string,
|
sdkUrl: string,
|
||||||
options?: { permissionMode?: string; model?: string; effort?: string; providerId?: string | null },
|
options?: { permissionMode?: string; model?: string; effort?: string; thinking?: 'enabled' | 'adaptive' | 'disabled'; providerId?: string | null },
|
||||||
) {
|
) {
|
||||||
startCalls.push({ sessionId: sid, options })
|
startCalls.push({ sessionId: sid, options })
|
||||||
return originalStartSession(sid, workDir, sdkUrl, options)
|
return originalStartSession(sid, workDir, sdkUrl, options)
|
||||||
@ -1532,7 +1583,7 @@ describe('WebSocket Chat Integration', () => {
|
|||||||
sid: string,
|
sid: string,
|
||||||
workDir: string,
|
workDir: string,
|
||||||
sdkUrl: string,
|
sdkUrl: string,
|
||||||
options?: { permissionMode?: string; model?: string; effort?: string; providerId?: string | null },
|
options?: { permissionMode?: string; model?: string; effort?: string; thinking?: 'enabled' | 'adaptive' | 'disabled'; providerId?: string | null },
|
||||||
) {
|
) {
|
||||||
startCalls.push({ sessionId: sid, options })
|
startCalls.push({ sessionId: sid, options })
|
||||||
return originalStartSession(sid, workDir, sdkUrl, options)
|
return originalStartSession(sid, workDir, sdkUrl, options)
|
||||||
|
|||||||
@ -80,12 +80,14 @@ describe('provider presets API', () => {
|
|||||||
expect(deepseek?.defaultModels.haiku).toBe('deepseek-v4-flash')
|
expect(deepseek?.defaultModels.haiku).toBe('deepseek-v4-flash')
|
||||||
expect(deepseek?.defaultModels.sonnet).toBe('deepseek-v4-pro')
|
expect(deepseek?.defaultModels.sonnet).toBe('deepseek-v4-pro')
|
||||||
expect(deepseek?.defaultModels.opus).toBe('deepseek-v4-pro')
|
expect(deepseek?.defaultModels.opus).toBe('deepseek-v4-pro')
|
||||||
|
expect(deepseek?.defaultEnv?.CC_HAHA_SEND_DISABLED_THINKING).toBe('1')
|
||||||
expect(zhipu?.defaultModels.main).toBe('glm-5.1')
|
expect(zhipu?.defaultModels.main).toBe('glm-5.1')
|
||||||
expect(zhipu?.defaultModels.haiku).toBe('glm-4.5-air')
|
expect(zhipu?.defaultModels.haiku).toBe('glm-4.5-air')
|
||||||
expect(zhipu?.defaultModels.sonnet).toBe('glm-5-turbo')
|
expect(zhipu?.defaultModels.sonnet).toBe('glm-5-turbo')
|
||||||
expect(zhipu?.defaultModels.opus).toBe('glm-5.1')
|
expect(zhipu?.defaultModels.opus).toBe('glm-5.1')
|
||||||
expect(kimi?.baseUrl).toBe('https://api.kimi.com/coding')
|
expect(kimi?.baseUrl).toBe('https://api.kimi.com/coding')
|
||||||
expect(kimi?.defaultModels.main).toBe('kimi-k2.6')
|
expect(kimi?.defaultModels.main).toBe('kimi-k2.6')
|
||||||
|
expect(kimi?.defaultEnv?.CC_HAHA_SEND_DISABLED_THINKING).toBe('1')
|
||||||
expect(minimax?.defaultModels.main).toBe('MiniMax-M2.7')
|
expect(minimax?.defaultModels.main).toBe('MiniMax-M2.7')
|
||||||
expect(jiekouai?.baseUrl).toBe('https://api.jiekou.ai/anthropic')
|
expect(jiekouai?.baseUrl).toBe('https://api.jiekou.ai/anthropic')
|
||||||
expect(jiekouai?.defaultModels.main).toBe('claude-sonnet-4-6')
|
expect(jiekouai?.defaultModels.main).toBe('claude-sonnet-4-6')
|
||||||
@ -118,6 +120,7 @@ describe('provider presets API', () => {
|
|||||||
expect(deepseek?.apiKeyUrl).toBe('https://platform.deepseek.com/api_keys')
|
expect(deepseek?.apiKeyUrl).toBe('https://platform.deepseek.com/api_keys')
|
||||||
expect(zhipu?.apiKeyUrl).toBe('https://www.bigmodel.cn/invite?icode=d41B2qi8Z5xNwTGLNPPF3OZLO2QH3C0EBTSr%2BArzMw4%3D')
|
expect(zhipu?.apiKeyUrl).toBe('https://www.bigmodel.cn/invite?icode=d41B2qi8Z5xNwTGLNPPF3OZLO2QH3C0EBTSr%2BArzMw4%3D')
|
||||||
expect(zhipu?.promoText).toContain('cc-haha')
|
expect(zhipu?.promoText).toContain('cc-haha')
|
||||||
|
expect(zhipu?.defaultEnv?.CC_HAHA_SEND_DISABLED_THINKING).toBe('1')
|
||||||
expect(kimi?.apiKeyUrl).toBe('https://platform.kimi.com/console/api-keys')
|
expect(kimi?.apiKeyUrl).toBe('https://platform.kimi.com/console/api-keys')
|
||||||
expect(minimax?.apiKeyUrl).toBe('https://platform.minimaxi.com/subscribe/token-plan?code=1TG2Cseab2&source=link')
|
expect(minimax?.apiKeyUrl).toBe('https://platform.minimaxi.com/subscribe/token-plan?code=1TG2Cseab2&source=link')
|
||||||
expect(jiekouai?.apiKeyUrl).toBe('https://jiekou.ai/referral?invited_code=OBNU3K')
|
expect(jiekouai?.apiKeyUrl).toBe('https://jiekou.ai/referral?invited_code=OBNU3K')
|
||||||
|
|||||||
79
src/server/__tests__/title-service.test.ts
Normal file
79
src/server/__tests__/title-service.test.ts
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
import { afterEach, beforeEach, describe, expect, test } from 'bun:test'
|
||||||
|
import * as fs from 'fs/promises'
|
||||||
|
import * as os from 'os'
|
||||||
|
import * as path from 'path'
|
||||||
|
import { generateTitle } from '../services/titleService.js'
|
||||||
|
|
||||||
|
describe('titleService', () => {
|
||||||
|
let tmpDir: string
|
||||||
|
let originalConfigDir: string | undefined
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
originalConfigDir = process.env.CLAUDE_CONFIG_DIR
|
||||||
|
tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'title-service-test-'))
|
||||||
|
process.env.CLAUDE_CONFIG_DIR = tmpDir
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
restoreEnv('CLAUDE_CONFIG_DIR', originalConfigDir)
|
||||||
|
await fs.rm(tmpDir, { recursive: true, force: true })
|
||||||
|
})
|
||||||
|
|
||||||
|
test('sends disabled thinking for opted-in providers when desktop thinking is off', async () => {
|
||||||
|
let requestBody: Record<string, unknown> | null = null
|
||||||
|
const server = Bun.serve({
|
||||||
|
hostname: '127.0.0.1',
|
||||||
|
port: 0,
|
||||||
|
async fetch(req) {
|
||||||
|
requestBody = await req.json() as Record<string, unknown>
|
||||||
|
return Response.json({
|
||||||
|
content: [{ type: 'text', text: '{"title":"Trace ok"}' }],
|
||||||
|
})
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
try {
|
||||||
|
const providerId = 'deepseek-test'
|
||||||
|
await fs.mkdir(path.join(tmpDir, 'cc-haha'), { recursive: true })
|
||||||
|
await fs.writeFile(
|
||||||
|
path.join(tmpDir, 'settings.json'),
|
||||||
|
JSON.stringify({ alwaysThinkingEnabled: false }, null, 2),
|
||||||
|
)
|
||||||
|
await fs.writeFile(
|
||||||
|
path.join(tmpDir, 'cc-haha', 'providers.json'),
|
||||||
|
JSON.stringify({
|
||||||
|
activeId: providerId,
|
||||||
|
providers: [
|
||||||
|
{
|
||||||
|
id: providerId,
|
||||||
|
presetId: 'deepseek',
|
||||||
|
name: 'DeepSeek',
|
||||||
|
apiKey: 'test-key',
|
||||||
|
baseUrl: `http://127.0.0.1:${server.port}/anthropic`,
|
||||||
|
apiFormat: 'anthropic',
|
||||||
|
models: {
|
||||||
|
main: 'deepseek-v4-pro',
|
||||||
|
haiku: 'deepseek-v4-pro',
|
||||||
|
sonnet: 'deepseek-v4-pro',
|
||||||
|
opus: 'deepseek-v4-pro',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}, null, 2),
|
||||||
|
)
|
||||||
|
|
||||||
|
await expect(generateTitle('请只回复 trace-ok')).resolves.toBe('Trace ok')
|
||||||
|
expect(requestBody?.thinking).toEqual({ type: 'disabled' })
|
||||||
|
} finally {
|
||||||
|
server.stop(true)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
function restoreEnv(key: string, value: string | undefined) {
|
||||||
|
if (value === undefined) {
|
||||||
|
delete process.env[key]
|
||||||
|
} else {
|
||||||
|
process.env[key] = value
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -26,7 +26,10 @@
|
|||||||
},
|
},
|
||||||
"needsApiKey": true,
|
"needsApiKey": true,
|
||||||
"websiteUrl": "https://platform.deepseek.com",
|
"websiteUrl": "https://platform.deepseek.com",
|
||||||
"apiKeyUrl": "https://platform.deepseek.com/api_keys"
|
"apiKeyUrl": "https://platform.deepseek.com/api_keys",
|
||||||
|
"defaultEnv": {
|
||||||
|
"CC_HAHA_SEND_DISABLED_THINKING": "1"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "zhipuglm",
|
"id": "zhipuglm",
|
||||||
@ -42,7 +45,10 @@
|
|||||||
"needsApiKey": true,
|
"needsApiKey": true,
|
||||||
"websiteUrl": "https://open.bigmodel.cn",
|
"websiteUrl": "https://open.bigmodel.cn",
|
||||||
"apiKeyUrl": "https://www.bigmodel.cn/invite?icode=d41B2qi8Z5xNwTGLNPPF3OZLO2QH3C0EBTSr%2BArzMw4%3D",
|
"apiKeyUrl": "https://www.bigmodel.cn/invite?icode=d41B2qi8Z5xNwTGLNPPF3OZLO2QH3C0EBTSr%2BArzMw4%3D",
|
||||||
"promoText": "智谱 GLM 为 cc-haha 用户准备了专属邀请福利,使用此链接注册后可领取新用户权益。"
|
"promoText": "智谱 GLM 为 cc-haha 用户准备了专属邀请福利,使用此链接注册后可领取新用户权益。",
|
||||||
|
"defaultEnv": {
|
||||||
|
"CC_HAHA_SEND_DISABLED_THINKING": "1"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "kimi",
|
"id": "kimi",
|
||||||
@ -57,7 +63,10 @@
|
|||||||
},
|
},
|
||||||
"needsApiKey": true,
|
"needsApiKey": true,
|
||||||
"websiteUrl": "https://platform.moonshot.cn",
|
"websiteUrl": "https://platform.moonshot.cn",
|
||||||
"apiKeyUrl": "https://platform.kimi.com/console/api-keys"
|
"apiKeyUrl": "https://platform.kimi.com/console/api-keys",
|
||||||
|
"defaultEnv": {
|
||||||
|
"CC_HAHA_SEND_DISABLED_THINKING": "1"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "minimax",
|
"id": "minimax",
|
||||||
|
|||||||
@ -49,6 +49,7 @@ type SessionStartOptions = {
|
|||||||
permissionMode?: string
|
permissionMode?: string
|
||||||
model?: string
|
model?: string
|
||||||
effort?: string
|
effort?: string
|
||||||
|
thinking?: 'enabled' | 'adaptive' | 'disabled'
|
||||||
providerId?: string | null
|
providerId?: string | null
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -589,6 +590,10 @@ export class ConversationService {
|
|||||||
args.push('--effort', options.effort)
|
args.push('--effort', options.effort)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (options?.thinking) {
|
||||||
|
args.push('--thinking', options.thinking)
|
||||||
|
}
|
||||||
|
|
||||||
return args
|
return args
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -615,6 +620,7 @@ export class ConversationService {
|
|||||||
'ANTHROPIC_DEFAULT_SONNET_MODEL_SUPPORTED_CAPABILITIES',
|
'ANTHROPIC_DEFAULT_SONNET_MODEL_SUPPORTED_CAPABILITIES',
|
||||||
'ANTHROPIC_DEFAULT_OPUS_MODEL',
|
'ANTHROPIC_DEFAULT_OPUS_MODEL',
|
||||||
'ANTHROPIC_DEFAULT_OPUS_MODEL_SUPPORTED_CAPABILITIES',
|
'ANTHROPIC_DEFAULT_OPUS_MODEL_SUPPORTED_CAPABILITIES',
|
||||||
|
'CC_HAHA_SEND_DISABLED_THINKING',
|
||||||
] as const
|
] as const
|
||||||
|
|
||||||
const cleanEnv = { ...process.env }
|
const cleanEnv = { ...process.env }
|
||||||
@ -738,6 +744,7 @@ export class ConversationService {
|
|||||||
'ANTHROPIC_DEFAULT_SONNET_MODEL_SUPPORTED_CAPABILITIES',
|
'ANTHROPIC_DEFAULT_SONNET_MODEL_SUPPORTED_CAPABILITIES',
|
||||||
'ANTHROPIC_DEFAULT_OPUS_MODEL',
|
'ANTHROPIC_DEFAULT_OPUS_MODEL',
|
||||||
'ANTHROPIC_DEFAULT_OPUS_MODEL_SUPPORTED_CAPABILITIES',
|
'ANTHROPIC_DEFAULT_OPUS_MODEL_SUPPORTED_CAPABILITIES',
|
||||||
|
'CC_HAHA_SEND_DISABLED_THINKING',
|
||||||
].some((key) => typeof env[key] === 'string' && env[key]!.trim().length > 0)
|
].some((key) => typeof env[key] === 'string' && env[key]!.trim().length > 0)
|
||||||
} catch {
|
} catch {
|
||||||
return false
|
return false
|
||||||
|
|||||||
@ -7,7 +7,10 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { ProviderService } from './providerService.js'
|
import { ProviderService } from './providerService.js'
|
||||||
|
import { SettingsService } from './settingsService.js'
|
||||||
import { sessionService } from './sessionService.js'
|
import { sessionService } from './sessionService.js'
|
||||||
|
import { PROVIDER_PRESETS } from '../config/providerPresets.js'
|
||||||
|
import { isEnvTruthy } from '../../utils/envUtils.js'
|
||||||
|
|
||||||
const TITLE_MAX_LEN = 50
|
const TITLE_MAX_LEN = 50
|
||||||
|
|
||||||
@ -69,6 +72,7 @@ export async function generateTitle(
|
|||||||
|
|
||||||
const model = resolvedProvider.models.haiku || resolvedProvider.models.main
|
const model = resolvedProvider.models.haiku || resolvedProvider.models.main
|
||||||
const url = `${resolvedProvider.baseUrl.replace(/\/+$/, '')}/v1/messages`
|
const url = `${resolvedProvider.baseUrl.replace(/\/+$/, '')}/v1/messages`
|
||||||
|
const shouldDisableThinking = await shouldDisableThinkingForTitle(resolvedProvider.presetId)
|
||||||
|
|
||||||
const response = await fetch(url, {
|
const response = await fetch(url, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@ -82,6 +86,7 @@ export async function generateTitle(
|
|||||||
max_tokens: 100,
|
max_tokens: 100,
|
||||||
system: TITLE_SYSTEM_PROMPT,
|
system: TITLE_SYSTEM_PROMPT,
|
||||||
messages: [{ role: 'user', content: trimmed.slice(0, 2000) }],
|
messages: [{ role: 'user', content: trimmed.slice(0, 2000) }],
|
||||||
|
...(shouldDisableThinking && { thinking: { type: 'disabled' } }),
|
||||||
}),
|
}),
|
||||||
signal: AbortSignal.timeout(15_000),
|
signal: AbortSignal.timeout(15_000),
|
||||||
})
|
})
|
||||||
@ -108,6 +113,14 @@ export async function generateTitle(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function shouldDisableThinkingForTitle(presetId: string): Promise<boolean> {
|
||||||
|
const settings = await new SettingsService().getUserSettings()
|
||||||
|
if (settings.alwaysThinkingEnabled !== false) return false
|
||||||
|
|
||||||
|
const presetEnv = PROVIDER_PRESETS.find((preset) => preset.id === presetId)?.defaultEnv
|
||||||
|
return isEnvTruthy(presetEnv?.CC_HAHA_SEND_DISABLED_THINKING)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Persist an AI-generated title to the session's JSONL file.
|
* Persist an AI-generated title to the session's JSONL file.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@ -1236,6 +1236,7 @@ type RuntimeSettings = {
|
|||||||
permissionMode?: string
|
permissionMode?: string
|
||||||
model?: string
|
model?: string
|
||||||
effort?: string
|
effort?: string
|
||||||
|
thinking?: 'disabled'
|
||||||
providerId?: string | null
|
providerId?: string | null
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1259,11 +1260,13 @@ async function getRuntimeSettings(sessionId?: string): Promise<RuntimeSettings>
|
|||||||
typeof userSettings.effort === 'string' && userSettings.effort.trim()
|
typeof userSettings.effort === 'string' && userSettings.effort.trim()
|
||||||
? userSettings.effort
|
? userSettings.effort
|
||||||
: undefined
|
: undefined
|
||||||
|
const thinking = resolveDesktopThinkingMode(userSettings)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
permissionMode: await settingsService.getPermissionMode().catch(() => undefined),
|
permissionMode: await settingsService.getPermissionMode().catch(() => undefined),
|
||||||
model: runtimeOverride.modelId,
|
model: runtimeOverride.modelId,
|
||||||
effort,
|
effort,
|
||||||
|
thinking,
|
||||||
providerId: runtimeOverride.providerId,
|
providerId: runtimeOverride.providerId,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1294,6 +1297,7 @@ async function getDefaultRuntimeSettings(): Promise<RuntimeSettings> {
|
|||||||
typeof userSettings.effort === 'string' && userSettings.effort.trim()
|
typeof userSettings.effort === 'string' && userSettings.effort.trim()
|
||||||
? userSettings.effort
|
? userSettings.effort
|
||||||
: undefined
|
: undefined
|
||||||
|
const thinking = resolveDesktopThinkingMode(userSettings)
|
||||||
|
|
||||||
let model: string | undefined
|
let model: string | undefined
|
||||||
if (resolvedActiveId) {
|
if (resolvedActiveId) {
|
||||||
@ -1320,10 +1324,17 @@ async function getDefaultRuntimeSettings(): Promise<RuntimeSettings> {
|
|||||||
permissionMode: await settingsService.getPermissionMode().catch(() => undefined),
|
permissionMode: await settingsService.getPermissionMode().catch(() => undefined),
|
||||||
model,
|
model,
|
||||||
effort,
|
effort,
|
||||||
|
thinking,
|
||||||
providerId: resolvedActiveId,
|
providerId: resolvedActiveId,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function resolveDesktopThinkingMode(
|
||||||
|
settings: Record<string, unknown>,
|
||||||
|
): 'disabled' | undefined {
|
||||||
|
return settings.alwaysThinkingEnabled === false ? 'disabled' : undefined
|
||||||
|
}
|
||||||
|
|
||||||
async function buildSessionStartupDiagnosticMessage(
|
async function buildSessionStartupDiagnosticMessage(
|
||||||
sessionId: string,
|
sessionId: string,
|
||||||
cause: string,
|
cause: string,
|
||||||
|
|||||||
@ -181,6 +181,7 @@ import { endQueryProfile, queryCheckpoint } from 'src/utils/queryProfiler.js'
|
|||||||
import {
|
import {
|
||||||
modelSupportsAdaptiveThinking,
|
modelSupportsAdaptiveThinking,
|
||||||
modelSupportsThinking,
|
modelSupportsThinking,
|
||||||
|
shouldSendExplicitDisabledThinking,
|
||||||
type ThinkingConfig,
|
type ThinkingConfig,
|
||||||
} from 'src/utils/thinking.js'
|
} from 'src/utils/thinking.js'
|
||||||
import {
|
import {
|
||||||
@ -1627,6 +1628,10 @@ async function* queryModel(
|
|||||||
type: 'enabled',
|
type: 'enabled',
|
||||||
} satisfies BetaMessageStreamParams['thinking']
|
} satisfies BetaMessageStreamParams['thinking']
|
||||||
}
|
}
|
||||||
|
} else if (shouldSendExplicitDisabledThinking()) {
|
||||||
|
thinking = {
|
||||||
|
type: 'disabled',
|
||||||
|
} as unknown as BetaMessageStreamParams['thinking']
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get API context management strategies if enabled
|
// Get API context management strategies if enabled
|
||||||
|
|||||||
@ -1,6 +1,11 @@
|
|||||||
import { afterEach, beforeEach, describe, expect, test } from 'bun:test'
|
import { afterEach, beforeEach, describe, expect, test } from 'bun:test'
|
||||||
import { get3PModelCapabilityOverride } from '../model/modelSupportOverrides.js'
|
import { get3PModelCapabilityOverride } from '../model/modelSupportOverrides.js'
|
||||||
import { modelSupportsAdaptiveThinking, modelSupportsThinking } from '../thinking.js'
|
import { resolveSideQueryThinkingConfig } from '../sideQuery.js'
|
||||||
|
import {
|
||||||
|
modelSupportsAdaptiveThinking,
|
||||||
|
modelSupportsThinking,
|
||||||
|
shouldSendExplicitDisabledThinking,
|
||||||
|
} from '../thinking.js'
|
||||||
|
|
||||||
describe('provider-aware thinking support', () => {
|
describe('provider-aware thinking support', () => {
|
||||||
let originalBaseUrl: string | undefined
|
let originalBaseUrl: string | undefined
|
||||||
@ -9,6 +14,7 @@ describe('provider-aware thinking support', () => {
|
|||||||
let originalBedrock: string | undefined
|
let originalBedrock: string | undefined
|
||||||
let originalVertex: string | undefined
|
let originalVertex: string | undefined
|
||||||
let originalFoundry: string | undefined
|
let originalFoundry: string | undefined
|
||||||
|
let originalExplicitDisabledThinking: string | undefined
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
originalBaseUrl = process.env.ANTHROPIC_BASE_URL
|
originalBaseUrl = process.env.ANTHROPIC_BASE_URL
|
||||||
@ -17,6 +23,7 @@ describe('provider-aware thinking support', () => {
|
|||||||
originalBedrock = process.env.CLAUDE_CODE_USE_BEDROCK
|
originalBedrock = process.env.CLAUDE_CODE_USE_BEDROCK
|
||||||
originalVertex = process.env.CLAUDE_CODE_USE_VERTEX
|
originalVertex = process.env.CLAUDE_CODE_USE_VERTEX
|
||||||
originalFoundry = process.env.CLAUDE_CODE_USE_FOUNDRY
|
originalFoundry = process.env.CLAUDE_CODE_USE_FOUNDRY
|
||||||
|
originalExplicitDisabledThinking = process.env.CC_HAHA_SEND_DISABLED_THINKING
|
||||||
|
|
||||||
delete process.env.CLAUDE_CODE_USE_BEDROCK
|
delete process.env.CLAUDE_CODE_USE_BEDROCK
|
||||||
delete process.env.CLAUDE_CODE_USE_VERTEX
|
delete process.env.CLAUDE_CODE_USE_VERTEX
|
||||||
@ -30,6 +37,7 @@ describe('provider-aware thinking support', () => {
|
|||||||
restoreEnv('CLAUDE_CODE_USE_BEDROCK', originalBedrock)
|
restoreEnv('CLAUDE_CODE_USE_BEDROCK', originalBedrock)
|
||||||
restoreEnv('CLAUDE_CODE_USE_VERTEX', originalVertex)
|
restoreEnv('CLAUDE_CODE_USE_VERTEX', originalVertex)
|
||||||
restoreEnv('CLAUDE_CODE_USE_FOUNDRY', originalFoundry)
|
restoreEnv('CLAUDE_CODE_USE_FOUNDRY', originalFoundry)
|
||||||
|
restoreEnv('CC_HAHA_SEND_DISABLED_THINKING', originalExplicitDisabledThinking)
|
||||||
clearCapabilityCache()
|
clearCapabilityCache()
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -60,6 +68,24 @@ describe('provider-aware thinking support', () => {
|
|||||||
expect(modelSupportsThinking('claude-sonnet-4-6')).toBe(true)
|
expect(modelSupportsThinking('claude-sonnet-4-6')).toBe(true)
|
||||||
expect(modelSupportsAdaptiveThinking('claude-sonnet-4-6')).toBe(true)
|
expect(modelSupportsAdaptiveThinking('claude-sonnet-4-6')).toBe(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('only sends explicit disabled thinking when the provider opts in', () => {
|
||||||
|
delete process.env.CC_HAHA_SEND_DISABLED_THINKING
|
||||||
|
expect(shouldSendExplicitDisabledThinking()).toBe(false)
|
||||||
|
|
||||||
|
process.env.CC_HAHA_SEND_DISABLED_THINKING = '1'
|
||||||
|
expect(shouldSendExplicitDisabledThinking()).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('side queries inherit explicit disabled thinking for opted-in providers', () => {
|
||||||
|
delete process.env.CC_HAHA_SEND_DISABLED_THINKING
|
||||||
|
expect(resolveSideQueryThinkingConfig(undefined, 1024)).toBeUndefined()
|
||||||
|
|
||||||
|
process.env.CC_HAHA_SEND_DISABLED_THINKING = '1'
|
||||||
|
expect(resolveSideQueryThinkingConfig(undefined, 1024)).toEqual({ type: 'disabled' })
|
||||||
|
expect(resolveSideQueryThinkingConfig(false, 1024)).toEqual({ type: 'disabled' })
|
||||||
|
expect(resolveSideQueryThinkingConfig(256, 1024)).toEqual({ type: 'enabled', budget_tokens: 256 })
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
function restoreEnv(key: string, value: string | undefined) {
|
function restoreEnv(key: string, value: string | undefined) {
|
||||||
|
|||||||
@ -17,6 +17,7 @@ import { getAnthropicClient } from '../services/api/client.js'
|
|||||||
import { getModelBetas, modelSupportsStructuredOutputs } from './betas.js'
|
import { getModelBetas, modelSupportsStructuredOutputs } from './betas.js'
|
||||||
import { computeFingerprint } from './fingerprint.js'
|
import { computeFingerprint } from './fingerprint.js'
|
||||||
import { normalizeModelStringForAPI } from './model/model.js'
|
import { normalizeModelStringForAPI } from './model/model.js'
|
||||||
|
import { shouldSendExplicitDisabledThinking } from './thinking.js'
|
||||||
|
|
||||||
type MessageParam = Anthropic.MessageParam
|
type MessageParam = Anthropic.MessageParam
|
||||||
type TextBlockParam = Anthropic.TextBlockParam
|
type TextBlockParam = Anthropic.TextBlockParam
|
||||||
@ -166,15 +167,7 @@ export async function sideQuery(opts: SideQueryOptions): Promise<BetaMessage> {
|
|||||||
: []),
|
: []),
|
||||||
].filter((block): block is TextBlockParam => block !== null)
|
].filter((block): block is TextBlockParam => block !== null)
|
||||||
|
|
||||||
let thinkingConfig: BetaThinkingConfigParam | undefined
|
const thinkingConfig = resolveSideQueryThinkingConfig(thinking, max_tokens)
|
||||||
if (thinking === false) {
|
|
||||||
thinkingConfig = { type: 'disabled' }
|
|
||||||
} else if (thinking !== undefined) {
|
|
||||||
thinkingConfig = {
|
|
||||||
type: 'enabled',
|
|
||||||
budget_tokens: Math.min(thinking, max_tokens - 1),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const normalizedModel = normalizeModelStringForAPI(model)
|
const normalizedModel = normalizeModelStringForAPI(model)
|
||||||
const start = Date.now()
|
const start = Date.now()
|
||||||
@ -220,3 +213,22 @@ export async function sideQuery(opts: SideQueryOptions): Promise<BetaMessage> {
|
|||||||
|
|
||||||
return response
|
return response
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function resolveSideQueryThinkingConfig(
|
||||||
|
thinking: SideQueryOptions['thinking'],
|
||||||
|
maxTokens: number,
|
||||||
|
): BetaThinkingConfigParam | undefined {
|
||||||
|
if (
|
||||||
|
thinking === false ||
|
||||||
|
(thinking === undefined && shouldSendExplicitDisabledThinking())
|
||||||
|
) {
|
||||||
|
return { type: 'disabled' }
|
||||||
|
}
|
||||||
|
if (thinking !== undefined) {
|
||||||
|
return {
|
||||||
|
type: 'enabled',
|
||||||
|
budget_tokens: Math.min(thinking, maxTokens - 1),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|||||||
@ -6,6 +6,7 @@ import { getCanonicalName } from './model/model.js'
|
|||||||
import { get3PModelCapabilityOverride } from './model/modelSupportOverrides.js'
|
import { get3PModelCapabilityOverride } from './model/modelSupportOverrides.js'
|
||||||
import { getAPIProvider, isFirstPartyAnthropicBaseUrl } from './model/providers.js'
|
import { getAPIProvider, isFirstPartyAnthropicBaseUrl } from './model/providers.js'
|
||||||
import { getSettingsWithErrors } from './settings/settings.js'
|
import { getSettingsWithErrors } from './settings/settings.js'
|
||||||
|
import { isEnvTruthy } from './envUtils.js'
|
||||||
|
|
||||||
export type ThinkingConfig =
|
export type ThinkingConfig =
|
||||||
| { type: 'adaptive' }
|
| { type: 'adaptive' }
|
||||||
@ -170,3 +171,7 @@ export function shouldEnableThinkingByDefault(): boolean {
|
|||||||
// Enable thinking by default unless explicitly disabled.
|
// Enable thinking by default unless explicitly disabled.
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function shouldSendExplicitDisabledThinking(): boolean {
|
||||||
|
return isEnvTruthy(process.env.CC_HAHA_SEND_DISABLED_THINKING)
|
||||||
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user