From 1716cf392f6d4a90cfc4908f0f76f5504a21f823 Mon Sep 17 00:00:00 2001 From: digua Date: Sat, 30 May 2026 20:09:58 +0800 Subject: [PATCH] refactor(ai): replace manual reasoning switches with auto-inference MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove the "Reasoning Model" and "Disable Thinking Mode" toggles from the model config dialog. These switches were injecting `enable_thinking` (a Qwen/DashScope-specific request parameter) into all providers, causing 400 errors on OpenAI, Anthropic, Gemini, and other official services. Replace with automatic inference in buildPiModel: - reasoning: true when the model's catalog capabilities include 'reasoning'; falls back to a model-name heuristic for unlisted custom/self-hosted models (qwen3, qwq, deepseek-r, o1/o3, etc.). - compat.thinkingFormat 'qwen': only set when reasoning is true AND the model is from the qwen provider or the model id contains "qwen"/"qwq". All other providers get compat: undefined and will never receive enable_thinking. This means official reasoning models (o3, deepseek-reasoner, etc.) now automatically get thinkingLevel:'medium', and self-hosted Qwen3 models continue to work as before — without any manual toggle. Behavior change: models with 'reasoning' in their catalog capabilities now default to thinkingLevel:'medium' (previously always 'off' unless the manual switch was enabled). Co-Authored-By: Claude Opus 4.8 --- apps/cli/src/ai/llm-config.ts | 2 - apps/cli/src/http/routes/ai.ts | 4 -- apps/desktop/main/ai/llm/model-types.ts | 2 - apps/desktop/main/ai/llm/types.ts | 2 - apps/desktop/main/ai/rag/index.ts | 2 - apps/desktop/preload/apis/ai.ts | 6 --- apps/desktop/preload/index.d.ts | 6 --- packages/core/src/ai/model-types.ts | 2 - .../src/ai/__tests__/llm-builder.test.ts | 52 +++++++++++++++---- packages/node-runtime/src/ai/llm-builder.ts | 39 ++++++++++++-- .../node-runtime/src/ai/llm-config-store.ts | 2 - .../common/Settings/AI/AIModelEditModal.vue | 29 ----------- .../common/Settings/AI/useAIConfigForm.ts | 13 ----- src/i18n/locales/en-US/settings.json | 4 -- src/i18n/locales/ja-JP/settings.json | 4 -- src/i18n/locales/zh-CN/settings.json | 4 -- src/i18n/locales/zh-TW/settings.json | 4 -- 17 files changed, 77 insertions(+), 100 deletions(-) diff --git a/apps/cli/src/ai/llm-config.ts b/apps/cli/src/ai/llm-config.ts index af18cb3d..47d72bbc 100644 --- a/apps/cli/src/ai/llm-config.ts +++ b/apps/cli/src/ai/llm-config.ts @@ -25,8 +25,6 @@ export interface AIServiceConfig { baseUrl?: string maxTokens?: number apiFormat?: string - disableThinking?: boolean - isReasoningModel?: boolean customModels?: Array<{ id: string; name: string }> authProfile?: string } diff --git a/apps/cli/src/http/routes/ai.ts b/apps/cli/src/http/routes/ai.ts index 57057e52..f847fe19 100644 --- a/apps/cli/src/http/routes/ai.ts +++ b/apps/cli/src/http/routes/ai.ts @@ -267,8 +267,6 @@ export function registerAiRoutes( baseUrl?: string maxTokens?: number apiFormat?: string - disableThinking?: boolean - isReasoningModel?: boolean customModels?: Array<{ id: string; name: string }> } }>('/_web/ai/llm/configs', async (request) => { @@ -286,8 +284,6 @@ export function registerAiRoutes( baseUrl?: string maxTokens?: number apiFormat?: string - disableThinking?: boolean - isReasoningModel?: boolean customModels?: Array<{ id: string; name: string }> } }>('/_web/ai/llm/configs/:id', async (request) => { diff --git a/apps/desktop/main/ai/llm/model-types.ts b/apps/desktop/main/ai/llm/model-types.ts index 0b3281fc..98d8cbd4 100644 --- a/apps/desktop/main/ai/llm/model-types.ts +++ b/apps/desktop/main/ai/llm/model-types.ts @@ -33,8 +33,6 @@ export interface LLMConnectionConfig { } export interface LLMConnectionConfigCompat extends LLMConnectionConfig { - disableThinking?: boolean - isReasoningModel?: boolean customModels?: Array<{ id: string; name: string }> } diff --git a/apps/desktop/main/ai/llm/types.ts b/apps/desktop/main/ai/llm/types.ts index ca37ed37..8d4ba12d 100644 --- a/apps/desktop/main/ai/llm/types.ts +++ b/apps/desktop/main/ai/llm/types.ts @@ -26,8 +26,6 @@ export interface AIServiceConfig { baseUrl?: string maxTokens?: number apiFormat?: string - disableThinking?: boolean - isReasoningModel?: boolean customModels?: Array<{ id: string; name: string }> createdAt: number updatedAt: number diff --git a/apps/desktop/main/ai/rag/index.ts b/apps/desktop/main/ai/rag/index.ts index 60c8ecb3..9c03378b 100644 --- a/apps/desktop/main/ai/rag/index.ts +++ b/apps/desktop/main/ai/rag/index.ts @@ -43,8 +43,6 @@ function ensureInit(): void { baseUrl: cfg.baseUrl, maxTokens: cfg.maxTokens, apiFormat: cfg.apiFormat, - disableThinking: cfg.disableThinking, - isReasoningModel: cfg.isReasoningModel, apiKey: cfg.apiKey || undefined, } }, diff --git a/apps/desktop/preload/apis/ai.ts b/apps/desktop/preload/apis/ai.ts index 03689352..04adb130 100644 --- a/apps/desktop/preload/apis/ai.ts +++ b/apps/desktop/preload/apis/ai.ts @@ -202,8 +202,6 @@ export interface AIServiceConfigDisplay { baseUrl?: string maxTokens?: number apiFormat?: string - disableThinking?: boolean - isReasoningModel?: boolean customModels?: Array<{ id: string; name: string }> createdAt: number updatedAt: number @@ -721,8 +719,6 @@ export const llmApi = { baseUrl?: string maxTokens?: number apiFormat?: string - disableThinking?: boolean - isReasoningModel?: boolean customModels?: Array<{ id: string; name: string }> }): Promise<{ success: boolean; config?: AIServiceConfigDisplay; error?: string }> => { return ipcRenderer.invoke('llm:addConfig', config) @@ -741,8 +737,6 @@ export const llmApi = { baseUrl?: string maxTokens?: number apiFormat?: string - disableThinking?: boolean - isReasoningModel?: boolean customModels?: Array<{ id: string; name: string }> } ): Promise<{ success: boolean; error?: string }> => { diff --git a/apps/desktop/preload/index.d.ts b/apps/desktop/preload/index.d.ts index 6d020a00..6cad4b0d 100644 --- a/apps/desktop/preload/index.d.ts +++ b/apps/desktop/preload/index.d.ts @@ -559,8 +559,6 @@ interface AIServiceConfigDisplay { baseUrl?: string maxTokens?: number apiFormat?: string - disableThinking?: boolean - isReasoningModel?: boolean createdAt: number updatedAt: number } @@ -620,8 +618,6 @@ interface LlmApi { baseUrl?: string maxTokens?: number apiFormat?: string - disableThinking?: boolean - isReasoningModel?: boolean customModels?: Array<{ id: string; name: string }> }) => Promise<{ success: boolean; config?: AIServiceConfigDisplay; error?: string }> updateConfig: ( @@ -634,8 +630,6 @@ interface LlmApi { baseUrl?: string maxTokens?: number apiFormat?: string - disableThinking?: boolean - isReasoningModel?: boolean customModels?: Array<{ id: string; name: string }> } ) => Promise<{ success: boolean; error?: string }> diff --git a/packages/core/src/ai/model-types.ts b/packages/core/src/ai/model-types.ts index 8173e40b..c97ea1ef 100644 --- a/packages/core/src/ai/model-types.ts +++ b/packages/core/src/ai/model-types.ts @@ -57,8 +57,6 @@ export interface LLMConnectionConfig { } export interface LLMConnectionConfigCompat extends LLMConnectionConfig { - disableThinking?: boolean - isReasoningModel?: boolean customModels?: Array<{ id: string; name: string }> } diff --git a/packages/node-runtime/src/ai/__tests__/llm-builder.test.ts b/packages/node-runtime/src/ai/__tests__/llm-builder.test.ts index b64d29fe..c4c7c02c 100644 --- a/packages/node-runtime/src/ai/__tests__/llm-builder.test.ts +++ b/packages/node-runtime/src/ai/__tests__/llm-builder.test.ts @@ -87,24 +87,56 @@ describe('buildPiModel', () => { assert.deepEqual(model.headers, { 'X-Custom': 'value' }) }) - it('sets compat.thinkingFormat when disableThinking is true', () => { + it('auto-infers reasoning=true for catalog models with reasoning capability', () => { + // o3 is in the ChatLab catalog with capabilities: ['chat', 'reasoning', ...] const model = buildPiModel({ provider: 'openai', - model: 'test', + model: 'o3', baseUrl: 'https://api.openai.com/v1', - disableThinking: true, }) + assert.equal(model.reasoning, true) + }) + + it('auto-infers reasoning=false and compat=undefined for non-reasoning OpenAI model', () => { + // gpt-4o is not a reasoning model; no enable_thinking should ever be injected + const model = buildPiModel({ + provider: 'openai', + model: 'gpt-4o', + baseUrl: 'https://api.openai.com/v1', + }) + assert.equal(model.reasoning, false) + assert.equal(model.compat, undefined) + }) + + it('auto-infers reasoning=true and compat.thinkingFormat=qwen for official qwen reasoning model', () => { + // qwen3-max is in the qwen provider catalog with reasoning capability + const model = buildPiModel({ + provider: 'qwen', + model: 'qwen3-max', + baseUrl: 'https://dashscope.aliyuncs.com/compatible-mode/v1', + }) + assert.equal(model.reasoning, true) assert.deepEqual(model.compat, { thinkingFormat: 'qwen' }) }) - it('sets reasoning flag from isReasoningModel', () => { - const model = buildPiModel({ - provider: 'openai', - model: 'o1', - baseUrl: 'https://api.openai.com/v1', - isReasoningModel: true, - }) + it('auto-infers reasoning=true and compat.thinkingFormat=qwen for custom self-hosted qwen3 model', () => { + // Custom model not in catalog — falls back to name heuristic + const model = buildPiModel( + { provider: 'openai-compatible', model: 'qwen3:8b', baseUrl: 'http://localhost:11434/v1' }, + { findModelFn: () => null } + ) assert.equal(model.reasoning, true) + assert.deepEqual(model.compat, { thinkingFormat: 'qwen' }) + }) + + it('auto-infers reasoning=false and compat=undefined for non-reasoning Anthropic model', () => { + const model = buildPiModel({ + provider: 'openai-compatible', + model: 'claude-3-5-sonnet', + baseUrl: 'https://api.anthropic.com/v1', + }) + assert.equal(model.reasoning, false) + assert.equal(model.compat, undefined) }) it('uses custom findModelFn for context window', () => { diff --git a/packages/node-runtime/src/ai/llm-builder.ts b/packages/node-runtime/src/ai/llm-builder.ts index 95c8135a..bee43b24 100644 --- a/packages/node-runtime/src/ai/llm-builder.ts +++ b/packages/node-runtime/src/ai/llm-builder.ts @@ -15,8 +15,6 @@ export interface PiModelConfig { baseUrl?: string maxTokens?: number apiFormat?: string - disableThinking?: boolean - isReasoningModel?: boolean } export interface BuildPiModelOptions { @@ -65,6 +63,37 @@ const BUILTIN_PROVIDER_API: Record = { anthropic: 'anthropic-messages', } +/** + * Infer whether a model should have reasoning enabled and which + * thinking format to use, based on the model catalog and provider. + * + * - reasoning: true when the catalog marks the model as 'reasoning' capable, + * or (for custom/unlisted models) when the model id matches a known pattern. + * - compat.thinkingFormat 'qwen': only for Qwen-family endpoints (official + * DashScope provider or model id containing "qwen"/"qwq"). Other providers + * must never receive `enable_thinking` in the request body. + */ +function inferReasoning( + provider: string, + modelId: string, + modelDef: ModelDefinition | null, +): { reasoning: boolean; compat: PiModel['compat'] } { + // Catalog is the authoritative source; fall back to heuristic for unlisted + // custom/self-hosted models where modelDef is null. + const reasoning = modelDef + ? modelDef.capabilities.includes('reasoning') + : /qwen3|qwq|deepseek-r|r1\b|o1\b|o3\b|thinking|reasoning/i.test(modelId) + + if (!reasoning) return { reasoning: false, compat: undefined } + + // Qwen-family: official DashScope provider id OR model name containing qwen/qwq. + const isQwen = provider === 'qwen' || /qwen|qwq/i.test(modelId) + return { + reasoning: true, + compat: isQwen ? { thinkingFormat: 'qwen' } : undefined, + } +} + export function buildPiModel(config: PiModelConfig, options?: BuildPiModelOptions): PiModel { const providerDef = BUILTIN_PROVIDERS.find((p) => p.id === config.provider) const baseUrl = config.baseUrl || providerDef?.defaultBaseUrl || '' @@ -111,6 +140,8 @@ export function buildPiModel(config: PiModelConfig, options?: BuildPiModelOption ? normalizeOpenAICompatibleBaseUrl(baseUrl) : baseUrl + const { reasoning, compat } = inferReasoning(config.provider, modelId, modelDef) + return { id: modelId, name: modelId, @@ -118,11 +149,11 @@ export function buildPiModel(config: PiModelConfig, options?: BuildPiModelOption provider: config.provider, baseUrl: resolvedBaseUrl, headers: options?.headers, - reasoning: config.isReasoningModel ?? false, + reasoning, input: ['text'], cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, contextWindow, maxTokens: config.maxTokens ?? 4096, - compat: config.disableThinking ? { thinkingFormat: 'qwen' } : undefined, + compat, } } diff --git a/packages/node-runtime/src/ai/llm-config-store.ts b/packages/node-runtime/src/ai/llm-config-store.ts index 8116785b..d48d722e 100644 --- a/packages/node-runtime/src/ai/llm-config-store.ts +++ b/packages/node-runtime/src/ai/llm-config-store.ts @@ -17,8 +17,6 @@ export interface AIServiceConfig { baseUrl?: string maxTokens?: number apiFormat?: string - disableThinking?: boolean - isReasoningModel?: boolean customModels?: Array<{ id: string; name: string }> createdAt: number updatedAt: number diff --git a/src/components/common/Settings/AI/AIModelEditModal.vue b/src/components/common/Settings/AI/AIModelEditModal.vue index ad7a2571..71266dde 100644 --- a/src/components/common/Settings/AI/AIModelEditModal.vue +++ b/src/components/common/Settings/AI/AIModelEditModal.vue @@ -526,35 +526,6 @@ function closeModal() { - - diff --git a/src/components/common/Settings/AI/useAIConfigForm.ts b/src/components/common/Settings/AI/useAIConfigForm.ts index ed8c3897..246b4780 100644 --- a/src/components/common/Settings/AI/useAIConfigForm.ts +++ b/src/components/common/Settings/AI/useAIConfigForm.ts @@ -15,8 +15,6 @@ export interface AIServiceConfig { model?: string baseUrl?: string apiFormat?: string - disableThinking?: boolean - isReasoningModel?: boolean customModels?: Array<{ id: string; name: string }> createdAt: number updatedAt: number @@ -96,8 +94,6 @@ export function useAIConfigForm(props: { baseUrl: '', apiFormat: API_FORMAT_DEFAULT, customName: '', - disableThinking: true, - isReasoningModel: false, }) const validationResult = ref<'idle' | 'valid' | 'invalid'>('idle') @@ -261,8 +257,6 @@ export function useAIConfigForm(props: { baseUrl: defaultProviderDef?.defaultBaseUrl || '', apiFormat: API_FORMAT_DEFAULT, customName: '', - disableThinking: true, - isReasoningModel: false, } validationResult.value = 'idle' validationMessage.value = '' @@ -292,8 +286,6 @@ export function useAIConfigForm(props: { baseUrl: config.baseUrl || providerDef?.defaultBaseUrl || '', apiFormat: config.apiFormat || API_FORMAT_DEFAULT, customName: config.name || '', - disableThinking: config.disableThinking ?? true, - isReasoningModel: config.isReasoningModel ?? false, } validationResult.value = 'idle' validationMessage.value = '' @@ -637,7 +629,6 @@ export function useAIConfigForm(props: { } const finalName = formData.value.customName.trim() || generateName() - const isReasoning = formData.value.isReasoningModel const persistCustomModels = isCompatMode.value && compatModels.value.length > 0 ? compatModels.value.map((m) => ({ id: m.id, name: m.name })) @@ -651,8 +642,6 @@ export function useAIConfigForm(props: { model: formData.value.model.trim() || undefined, baseUrl: formData.value.baseUrl.trim() || undefined, apiFormat: savedApiFormat, - disableThinking: isReasoning ? formData.value.disableThinking : undefined, - isReasoningModel: isReasoning || undefined, customModels: persistCustomModels, }) @@ -670,8 +659,6 @@ export function useAIConfigForm(props: { model: formData.value.model.trim() || undefined, baseUrl: formData.value.baseUrl.trim() || undefined, apiFormat: savedApiFormat, - disableThinking: isReasoning ? formData.value.disableThinking : undefined, - isReasoningModel: isReasoning || undefined, customModels: persistCustomModels, } diff --git a/src/i18n/locales/en-US/settings.json b/src/i18n/locales/en-US/settings.json index 81ebf0ca..b66df0e8 100644 --- a/src/i18n/locales/en-US/settings.json +++ b/src/i18n/locales/en-US/settings.json @@ -154,10 +154,6 @@ "addAllModels": "Add all", "searchModels": "Search models...", "apiEndpointOverrideHint": "Default URL is prefilled. Replace if needed", - "disableThinking": "Disable Thinking Mode", - "disableThinkingDesc": "For models like Qwen3, DeepSeek-R1. Uses standard tool calling format when disabled.", - "isReasoningModel": "Reasoning Model", - "isReasoningModelDesc": "Extracts thinking process and disables tool calling (e.g. DeepSeek-R1, QwQ)", "advancedOptions": "Advanced Options", "customService": "Custom Service", "unnamedConfig": "Unnamed Configuration", diff --git a/src/i18n/locales/ja-JP/settings.json b/src/i18n/locales/ja-JP/settings.json index 846a7b76..edd2484b 100644 --- a/src/i18n/locales/ja-JP/settings.json +++ b/src/i18n/locales/ja-JP/settings.json @@ -154,10 +154,6 @@ "addAllModels": "すべて追加", "searchModels": "モデルを検索...", "apiEndpointOverrideHint": "デフォルトURLは入力済みです。必要に応じて変更してください", - "disableThinking": "思考モードをオフ", - "disableThinkingDesc": "Qwen3 や DeepSeek-R1 などのモデル向け。オフにすると標準のツール実行形式を使います", - "isReasoningModel": "推論モデル", - "isReasoningModelDesc": "有効にすると推論過程を抽出し、ツール実行を無効にします(DeepSeek-R1、QwQ など)", "advancedOptions": "詳細設定", "customService": "カスタムサービス", "unnamedConfig": "名称未設定", diff --git a/src/i18n/locales/zh-CN/settings.json b/src/i18n/locales/zh-CN/settings.json index 7404e6d6..c8163497 100644 --- a/src/i18n/locales/zh-CN/settings.json +++ b/src/i18n/locales/zh-CN/settings.json @@ -154,10 +154,6 @@ "addAllModels": "全部添加", "searchModels": "搜索模型...", "apiEndpointOverrideHint": "默认地址已填入,可按需改为完整 URL", - "disableThinking": "禁用思考模式", - "disableThinkingDesc": "针对 Qwen3、DeepSeek-R1 等模型,禁用后使用标准工具调用格式", - "isReasoningModel": "推理模型", - "isReasoningModelDesc": "启用后将提取思考过程并禁用工具调用(如 DeepSeek-R1、QwQ 等)", "advancedOptions": "高级选项", "customService": "自定义服务", "unnamedConfig": "未命名配置", diff --git a/src/i18n/locales/zh-TW/settings.json b/src/i18n/locales/zh-TW/settings.json index ae6baac2..8fdb85d4 100644 --- a/src/i18n/locales/zh-TW/settings.json +++ b/src/i18n/locales/zh-TW/settings.json @@ -154,10 +154,6 @@ "addAllModels": "全部添加", "searchModels": "搜尋模型...", "apiEndpointOverrideHint": "預設地址已填入,可按需改為完整 URL", - "disableThinking": "關閉思考模式", - "disableThinkingDesc": "針對 Qwen3、DeepSeek-R1 等模型,停用後使用標準工具呼叫格式", - "isReasoningModel": "推理模型", - "isReasoningModelDesc": "啟用後將提取思考過程並停用工具呼叫(如 DeepSeek-R1、QwQ 等)", "advancedOptions": "進階設定", "customService": "自訂服務", "unnamedConfig": "未命名設定",