mirror of
https://github.com/hellodigua/ChatLab.git
synced 2026-10-25 17:54:34 +08:00
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:
1 parent
886642c1a4
commit
1716cf392f
17 files changed
+77
-100
No files matched your search
@@ -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
|
||||
}
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -33,8 +33,6 @@ export interface LLMConnectionConfig {
|
||||
}
|
||||
|
||||
export interface LLMConnectionConfigCompat extends LLMConnectionConfig {
|
||||
disableThinking?: boolean
|
||||
isReasoningModel?: boolean
|
||||
customModels?: Array<{ id: string; name: string }>
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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 }> => {
|
||||
|
||||
Vendored
-6
@@ -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 }>
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -154,10 +154,6 @@
|
||||
"addAllModels": "すべて追加",
|
||||
"searchModels": "モデルを検索...",
|
||||
"apiEndpointOverrideHint": "デフォルトURLは入力済みです。必要に応じて変更してください",
|
||||
"disableThinking": "思考モードをオフ",
|
||||
"disableThinkingDesc": "Qwen3 や DeepSeek-R1 などのモデル向け。オフにすると標準のツール実行形式を使います",
|
||||
"isReasoningModel": "推論モデル",
|
||||
"isReasoningModelDesc": "有効にすると推論過程を抽出し、ツール実行を無効にします(DeepSeek-R1、QwQ など)",
|
||||
"advancedOptions": "詳細設定",
|
||||
"customService": "カスタムサービス",
|
||||
"unnamedConfig": "名称未設定",
|
||||
|
||||
@@ -154,10 +154,6 @@
|
||||
"addAllModels": "全部添加",
|
||||
"searchModels": "搜索模型...",
|
||||
"apiEndpointOverrideHint": "默认地址已填入,可按需改为完整 URL",
|
||||
"disableThinking": "禁用思考模式",
|
||||
"disableThinkingDesc": "针对 Qwen3、DeepSeek-R1 等模型,禁用后使用标准工具调用格式",
|
||||
"isReasoningModel": "推理模型",
|
||||
"isReasoningModelDesc": "启用后将提取思考过程并禁用工具调用(如 DeepSeek-R1、QwQ 等)",
|
||||
"advancedOptions": "高级选项",
|
||||
"customService": "自定义服务",
|
||||
"unnamedConfig": "未命名配置",
|
||||
|
||||
@@ -154,10 +154,6 @@
|
||||
"addAllModels": "全部添加",
|
||||
"searchModels": "搜尋模型...",
|
||||
"apiEndpointOverrideHint": "預設地址已填入,可按需改為完整 URL",
|
||||
"disableThinking": "關閉思考模式",
|
||||
"disableThinkingDesc": "針對 Qwen3、DeepSeek-R1 等模型,停用後使用標準工具呼叫格式",
|
||||
"isReasoningModel": "推理模型",
|
||||
"isReasoningModelDesc": "啟用後將提取思考過程並停用工具呼叫(如 DeepSeek-R1、QwQ 等)",
|
||||
"advancedOptions": "進階設定",
|
||||
"customService": "自訂服務",
|
||||
"unnamedConfig": "未命名設定",
|
||||
|
||||
Reference in new issue
Block a user