mirror of
https://github.com/qingchencloud/clawpanel.git
synced 2026-08-06 05:33:45 +08:00
feat(assistant): 识别本地 LLM 服务端常见错误并给出修复指引
用户反馈:切本地 vLLM(Qwen/Qwen3-30B-A3B)后在助手里调用工具报错:
"auto" tool choice requires --enable-auto-tool-choice and
--tool-call-parser to be set
这是 vLLM 0.6+ 的默认安全策略 —— 必须在启动参数显式开启工具调用
才允许客户端在 body 里带 tools 字段。ClawPanel 发的请求符合 OpenAI
规范,不是我们的 bug,但用户面对这个原始报错字面上看不出是 vLLM
配置问题也不知道怎么修。
## 解决
新增 src/lib/model-error-diagnosis.js,提供 enhanceModelCallError:
保留原始错误文本 + 附加中文修复指引。目前覆盖 5 类常见本地部署错误:
1. **vLLM tool choice 限制**(本次用户实际踩的)
- 给出 --enable-auto-tool-choice + --tool-call-parser 启动命令
- Qwen / Mistral / Llama 各系列推荐的 parser 值
- 建议临时切到"聊天"模式规避
2. **llama.cpp / LM Studio 旧版本不支持 tools**
3. **Ollama 模型不支持 tools**
4. **模型 ID 不存在 / 404**
5. **上下文超长 / token limit**
在 assistant.js 的 5 个错误抛出点统一接入:
- callChatCompletions(OpenAI 聊天模式)
- callResponsesAPI(新 /v1/responses 接口)
- callAnthropicMessages(Claude)
- callGeminiGenerate(Gemini)
- callAIWithTools(工具模式,就是用户踩坑的那条路径)
## 验证
- npm run build 通过
- assistant chunk 从 153.98KB → 156.24KB(gzip +0.86KB),合理
- 所有增强都走 try { parseJSON } 之后,不会影响原有错误处理路径
## 相关
- #Compat-5 系列的一部分(运行时错误诊断)
- 用户场景:vLLM + Qwen3 MoE,切换到本地模型后调工具
- 用户侧实际修复命令:
vllm serve <model> --enable-auto-tool-choice --tool-call-parser hermes
This commit is contained in:
@@ -12,6 +12,7 @@ import { icon, statusIcon } from '../lib/icons.js'
|
||||
import { QTCOOL, PROVIDER_PRESETS, API_TYPES as SHARED_API_TYPES, fetchQtcoolModels } from '../lib/model-presets.js'
|
||||
import { t } from '../lib/i18n.js'
|
||||
import { getActiveEngineId } from '../lib/engine-manager.js'
|
||||
import { enhanceModelCallError } from '../lib/model-error-diagnosis.js'
|
||||
|
||||
// ── 常量 ──
|
||||
const STORAGE_KEY = 'clawpanel-assistant'
|
||||
@@ -1910,7 +1911,8 @@ async function callChatCompletions(base, messages, onChunk) {
|
||||
} catch {
|
||||
if (errText) errMsg += `: ${errText.slice(0, 200)}`
|
||||
}
|
||||
throw new Error(errMsg)
|
||||
// #Compat-5: 识别 vLLM/Ollama 等本地服务端的常见拒绝消息,附加修复指引
|
||||
throw new Error(enhanceModelCallError(errMsg))
|
||||
}
|
||||
|
||||
// 检测响应是否为 SSE 流式
|
||||
@@ -2004,7 +2006,8 @@ async function callResponsesAPI(base, messages, onChunk) {
|
||||
} catch {
|
||||
if (errText) errMsg += `: ${errText.slice(0, 200)}`
|
||||
}
|
||||
throw new Error(errMsg)
|
||||
// #Compat-5: 识别本地服务端拒绝消息,附加修复指引
|
||||
throw new Error(enhanceModelCallError(errMsg))
|
||||
}
|
||||
|
||||
await readSSEStream(resp, (json) => {
|
||||
@@ -2063,7 +2066,8 @@ async function callAnthropicMessages(base, messages, onChunk) {
|
||||
} catch {
|
||||
if (errText) errMsg += `: ${errText.slice(0, 200)}`
|
||||
}
|
||||
throw new Error(errMsg)
|
||||
// #Compat-5: 识别本地服务端拒绝消息,附加修复指引
|
||||
throw new Error(enhanceModelCallError(errMsg))
|
||||
}
|
||||
|
||||
_lastDebugInfo.streaming = true
|
||||
@@ -2131,7 +2135,8 @@ async function callGeminiGenerate(base, messages, onChunk) {
|
||||
const errText = await resp.text().catch(() => '')
|
||||
let errMsg = `API 错误 ${resp.status}`
|
||||
try { errMsg = JSON.parse(errText).error?.message || errMsg } catch {}
|
||||
throw new Error(errMsg)
|
||||
// #Compat-5: 识别本地服务端拒绝消息,附加修复指引
|
||||
throw new Error(enhanceModelCallError(errMsg))
|
||||
}
|
||||
|
||||
_lastDebugInfo.streaming = true
|
||||
@@ -2580,7 +2585,9 @@ async function callAIWithTools(messages, onStatus, onToolProgress, onChunk) {
|
||||
const errText = await resp.text().catch(() => '')
|
||||
let errMsg = `API 错误 ${resp.status}`
|
||||
try { errMsg = JSON.parse(errText).error?.message || errMsg } catch {}
|
||||
throw new Error(errMsg)
|
||||
// #Compat-5: callAIWithTools 场景下 tools 带进 body 最容易踩 vLLM tool choice 限制,
|
||||
// 识别并给出启动参数指引,避免用户一脸懵
|
||||
throw new Error(enhanceModelCallError(errMsg))
|
||||
}
|
||||
|
||||
// 流式累积状态
|
||||
|
||||
Reference in New Issue
Block a user