1. 现象
在 pi-web 的界面里,给自定义模型(自建 vLLM 部署的 Qwen 系 27B 推理模型,OpenAI Responses 兼容协议)选择思考级别(reasoning level)时:
- 下拉框里最高只有
medium,看不到xhigh; - 在设置里把级别手动改成
xhigh之后,实际跑的还是medium; - 进程环境变量里也能证实:
PI_REASONING_LEVEL=medium。
直观感受是"设置不生效",很像 pi-web 设置页的一个 bug。
2. 排查过程
第一步:确认实际生效值
bash复制
env | grep -iE 'PI_|REASON'
# PI_REASONING_LEVEL=medium
会话进程拿到的就是 medium,说明问题出在"设置值 → 会话环境变量"这条链路上,而不是显示问题。
第二步:排除 pi-web 设置项
检查 pi-web 自身的配置与进程环境:
~/.pi/agent/settings.json里只有默认 provider/model,没有思考级别相关字段;- pi-web 服务进程(pid 1)的环境里没有
PI_REASONING_LEVEL,说明这个值是 pi-web 启动 agent 会话时动态算出来注入的,计算依据在别处。
第三步:找到计算逻辑
顺着 PI_REASONING_LEVEL 在 pi 源码里搜索,定位到 pi-ai 包的 dist/models.js:
javascript复制
const EXTENDED_THINKING_LEVELS = ["off", "minimal", "low", "medium", "high", "xhigh", "max"];
export function getSupportedThinkingLevels(model) {
if (!model.reasoning) return ["off"];
return EXTENDED_THINKING_LEVELS.filter((level) => {
const mapped = model.thinkingLevelMap?.[level];
if (mapped === null) return false; // 显式置 null = 禁用该级别
if (level === "xhigh" || level === "max")
return mapped !== undefined; // xhigh/max 必须显式列出键才算支持
return true;
});
}
export function clampThinkingLevel(model, level) {
// 请求的级别不受支持时:先向上找最近的可用级别,找不到再向下找
...
}
两个关键规则:
thinkingLevelMap里值为null的级别 = 禁用;xhigh/max是特例:不在 map 里出现就默认为不支持(其他级别不出现则默认为支持)。
第四步:找到罪魁祸首
打开 ~/.pi/agent/models.json,自定义模型的规格:
json复制
{
"id": "Qwen3.8-27B",
"reasoning": true,
"thinkingLevelMap": {
"minimal": null,
"low": null,
"high": null,
"max": null
}
}
对照规则推导该模型实际支持的级别:
| 级别 | map 中的值 | 判定 |
|---|---|---|
| off | — | 支持 |
| minimal | null | 禁用 |
| low | null | 禁用 |
| medium | (未出现) | 支持(默认) |
| high | null | 禁用 |
| xhigh | (未出现) | 不支持(xhigh 必须显式列出) |
| max | null | 禁用 |
结果:只剩 off 和 medium。UI 选择器自然最高只给到 medium;而设置里选的 xhigh 会被 clampThinkingLevel 逐级钳制:
text复制
xhigh(不支持)→ max(null)→ high(null)→ medium(支持)停在 medium
这就是"设置 xhigh 实际跑 medium"的完整解释。
3. 修复
3.1 先验证后端是否真的接受 xhigh
改配置前,直接对 vLLM 后端发探测请求,确认 reasoning_effort: "xhigh" 不会 400:
bash复制
for e in xhigh medium none; do
curl -s $GATEWAY/v1/chat/completions \
-H "Authorization: Bearer $API_KEY" \
-H 'Content-Type: application/json' \
-d "{\"model\":\"<model>\",\"messages\":[{\"role\":\"user\",\"content\":\"say hi\"}],
\"max_tokens\":16,\"reasoning_effort\":\"$e\"}"
done
三个值均正常返回,xhigh 可用。
3.2 改模型规格
在 thinkingLevelMap 里补一个显式的 xhigh 键(值就是发给后端的 wire value):
json复制
"thinkingLevelMap": {
"minimal": null,
"low": null,
"high": null,
"xhigh": "xhigh",
"max": null
}
新开一个会话即生效(模型规格在会话启动时加载;当前会话不会中途变更)。如果 UI 仍未刷新,重启 pi-web 进程。
4. 教训
- "设置不生效"先怀疑生效链路,别急着怀疑设置项本身。 本例里 pi-web 的设置完全没问题,问题出在它下游"按模型规格钳制"这一步。环境变量(
PI_REASONING_LEVEL)是把设置值、钳制结果、实际值串起来的最佳观测点。 - 特例规则是坑的温床。
xhigh/max"不出现 = 不支持",与其他级别 "不出现 = 支持" 的默认语义恰好相反。这类不对称的隐式默认,读源码前几乎不可能猜对。 - 改配置前先探测后端。 先 curl 验证 vLLM 接受
xhigh,再改 map——避免"UI 显示了 xhigh,选上之后整条会话 400"的新 bug。 - 自定义 provider 的模型规格是"能力声明",不是"偏好设置"。
thinkingLevelMap同时决定:UI 显示哪些档位、请求值如何映射到 wire value、不受支持时如何钳制。三件事一个字段管,改的时候要整体想清楚。
5. 速查表
| 想让某级别出现在选择器里 | thinkingLevelMap 怎么配 |
|---|---|
| off / minimal / low / medium / high | 键值为非 null 字符串,或不写该键 |
| xhigh / max | 必须显式写键,值用非 null 字符串 |
| 禁用某个级别 | "级别": null |
| 映射到后端不同的 wire value | "medium": "low"(UI 显示 medium,实际发 low) |
附:排查用到的关键文件
text复制
~/.pi/agent/models.json # 自定义模型规格(本案根因所在)
~/.pi/agent/settings.json # pi 全局设置(本案无关,用于排除)
node_modules/.../pi-ai/dist/models.js # getSupportedThinkingLevels / clampThinkingLevel
node_modules/.../pi-ai/dist/api/openai-responses.js # reasoningEffort → 请求体映射