refactor(ai): replace manual reasoning switches with auto-inference

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 <noreply@anthropic.com>
This commit is contained in:
diguaandClaude Opus 4.8 committed 2026-05-31 10:28:29 +08:00
1 parent 886642c1a4
commit 1716cf392f
17 files changed
+77 -100

No files matched your search

-2
View File
@@ -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
}
-4
View File
@@ -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) => {
-2
View File
@@ -33,8 +33,6 @@ export interface LLMConnectionConfig {
}
export interface LLMConnectionConfigCompat extends LLMConnectionConfig {
disableThinking?: boolean
isReasoningModel?: boolean
customModels?: Array<{ id: string; name: string }>
}
-2
View File
@@ -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
-2
View File
@@ -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,
}
},
-6
View File
@@ -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 }> => {
-6
View File
@@ -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 }>
-2
View File
@@ -57,8 +57,6 @@ export interface LLMConnectionConfig {
}
export interface LLMConnectionConfigCompat extends LLMConnectionConfig {
disableThinking?: boolean
isReasoningModel?: boolean
customModels?: Array<{ id: string; name: string }>
}
@@ -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', () => {
+35 -4
View File
@@ -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<string, PiApi> = {
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<PiApi>['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<PiApi> {
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,
}
}
@@ -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
@@ -526,35 +526,6 @@ function closeModal() {
</div>
</template>
<!-- ===== 通用:推理模型选项 ===== -->
<template v-if="formData.provider || !isPresetMode">
<div class="flex items-center justify-between rounded-lg bg-gray-50 p-3 dark:bg-gray-800">
<div>
<p class="text-sm font-medium text-gray-700 dark:text-gray-300">
{{ t('settings.aiConfig.modal.isReasoningModel') }}
</p>
<p class="text-xs text-gray-500 dark:text-gray-400">
{{ t('settings.aiConfig.modal.isReasoningModelDesc') }}
</p>
</div>
<USwitch v-model="formData.isReasoningModel" />
</div>
<div
v-if="formData.isReasoningModel"
class="flex items-center justify-between rounded-lg bg-gray-50 p-3 dark:bg-gray-800"
>
<div>
<p class="text-sm font-medium text-gray-700 dark:text-gray-300">
{{ t('settings.aiConfig.modal.disableThinking') }}
</p>
<p class="text-xs text-gray-500 dark:text-gray-400">
{{ t('settings.aiConfig.modal.disableThinkingDesc') }}
</p>
</div>
<USwitch v-model="formData.disableThinking" />
</div>
</template>
</div>
</div>
@@ -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,
}
-4
View File
@@ -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",
-4
View File
@@ -154,10 +154,6 @@
"addAllModels": "すべて追加",
"searchModels": "モデルを検索...",
"apiEndpointOverrideHint": "デフォルトURLは入力済みです。必要に応じて変更してください",
"disableThinking": "思考モードをオフ",
"disableThinkingDesc": "Qwen3 や DeepSeek-R1 などのモデル向け。オフにすると標準のツール実行形式を使います",
"isReasoningModel": "推論モデル",
"isReasoningModelDesc": "有効にすると推論過程を抽出し、ツール実行を無効にします(DeepSeek-R1、QwQ など)",
"advancedOptions": "詳細設定",
"customService": "カスタムサービス",
"unnamedConfig": "名称未設定",
-4
View File
@@ -154,10 +154,6 @@
"addAllModels": "全部添加",
"searchModels": "搜索模型...",
"apiEndpointOverrideHint": "默认地址已填入,可按需改为完整 URL",
"disableThinking": "禁用思考模式",
"disableThinkingDesc": "针对 Qwen3、DeepSeek-R1 等模型,禁用后使用标准工具调用格式",
"isReasoningModel": "推理模型",
"isReasoningModelDesc": "启用后将提取思考过程并禁用工具调用(如 DeepSeek-R1、QwQ 等)",
"advancedOptions": "高级选项",
"customService": "自定义服务",
"unnamedConfig": "未命名配置",
-4
View File
@@ -154,10 +154,6 @@
"addAllModels": "全部添加",
"searchModels": "搜尋模型...",
"apiEndpointOverrideHint": "預設地址已填入,可按需改為完整 URL",
"disableThinking": "關閉思考模式",
"disableThinkingDesc": "針對 Qwen3、DeepSeek-R1 等模型,停用後使用標準工具呼叫格式",
"isReasoningModel": "推理模型",
"isReasoningModelDesc": "啟用後將提取思考過程並停用工具呼叫(如 DeepSeek-R1、QwQ 等)",
"advancedOptions": "進階設定",
"customService": "自訂服務",
"unnamedConfig": "未命名設定",