Repository navigation
[bug] 模型 id 含中文时 x-bili-plugin-model 触发 undici ByteString 异常,整轮请求发不出去(7 文件 8 处写入点,pi/omp/opencode/dsh) #2455
Description
Description *
模型 id 含非 ASCII 字符(中文)时,插件把它原样写进 HTTP 请求头 x-bili-plugin-model,Node/undici 构造头时直接抛 TypeError,请求在本地发出前就失败,该模型 100% 不可用。
复现步骤(Windows,pi + bili 0.1.188 插件模式):
- 配置一个模型 id 含中文的供应商(本例
浮***/gpt-6.1-sol),加载 bili 扩展。 - 起会话,发任意一条消息。
- 该轮直接失败,宿主透出的一手错误文本就是:
Cannot convert argument to a ByteString because the character at index 0 has a value of 28014 which is greater than 255.
(28014 = U+6D6E = 「浮」,正是该 model id 的首字符。)
对照组与取证:
- 同一模型、同一指令,不加载 bili 扩展时正常返回;加载扩展即 100% 复现。
- 失败轮在会话记录里
usage全 0、durationMs仅 4–10 ms;~/.local/state/billion-context/bili.log中该会话一行都没有 → 请求根本没到代理。 - 换成 ASCII 的模型 id(
deepseek-v4.1-flash)一切正常。
bili version *
0.1.188(npm latest,实测自安装包 package.json)
Usage mode *
Plugin native(以宿主扩展方式安装 npm 包,进程内加载 dist/agent/pi-native.js)
Client and version *
pi 1.1.0(Windows 桌面壳 PiDeck 内嵌运行;已用命令行 pi -p 独立复现,与桌面壳无关)
Operating system *
Windows 11(原生,非 WSL2)
Upstream and model *
第三方聚合站;模型 id 浮***/gpt-6.1-sol(供应商名与模型名都是中文)。与本 bug 无关——报错发生在请求发出之前,上游与窗口声明都不参与。
最小复现(与 bili 无关,纯 Node 即可)
// node v22.20.0
await fetch("http://127.0.0.1:8317/v1/responses", {
method: "POST",
headers: { "x-bili-plugin-model": "浮***/gpt-6.1-sol" },
body: "{}",
});
// TypeError: Cannot convert argument to a ByteString because the character at index 0 has a value of 28014 which is greater than 255.同一脚本换 ASCII 值、或换 percent-encoded 的中文(percent:%E6%B5%AE%E7%94%9F%E4%BA%91%E7%AE%97%2Fgpt-6.1-sol)都不抛、能正常发出(本机拿到代理的 401,说明已送达)。
根因
dist/agent/pi-native.js(0.1.188)L278 附近:
const modelId=ctx.model?.id;
if(typeof modelId==="string"&&modelId.length>0){
headers["x-bili-plugin-model"]=modelId; // ← model id 含中文时在这里抛HTTP 头值只能是 latin1 / ByteString,非 ASCII 的 model id 不可能直接塞进头,必须在写入前处理。
影响面(0.1.188 的 dist,共 7 个文件 8 处写入点)
dist/agent/pi-native.js:278—headers["x-bili-plugin-model"]=modelId;dist/agent/pi.js:276— 同上dist/agent/omp-native.js:278— 同上dist/agent/omp.js:276— 同上dist/agent/dsh-native.js:132—headers["x-bili-plugin-model"]=modelInfo.cached.model;dist/agent/opencode-native.js:321(2 处)—headers.set("x-bili-plugin-model",model.id)dist/agent/opencode.js:321—output.headers["x-bili-plugin-model"]=model.id;
(行号为 npm 包内 dist 的,源码行号不同。)中文供应商名/模型名在中文聚合站场景里很常见,所以这条在 pi / omp / opencode / dsh 各 lane 都会摸到;
为什么直接跳过该头是安全的
代理侧 dist/index.js(0.1.188)L684:
function pluginReportedModel(headers){if(pluginAgentHeader(headers)===void 0)return void 0;const raw=headerValue(headers,PLUGIN_MODEL_HEADER);return raw!==void 0&&/^\S{1,256}$/.test(raw)?raw:void 0}
function pluginHeadersMatchModel(headers,bodyModel){const reported=pluginReportedModel(headers);if(reported===void 0||bodyModel===void 0)return true; ...}缺头 → reported===void 0 → pluginHeadersMatchModel 直接 return true,模型识别走 body / registry 兜底,当前没有副作用。
建议修复
最小改动:每个写入点加一个 ASCII 守卫,或抽一个共用 helper(asciiHeaderValue(v))在写入前统一过滤:
const modelId = ctx.model?.id;
if (typeof modelId === "string" && /^[\x20-\x7E]{1,256}$/.test(modelId)) {
headers["x-bili-plugin-model"] = modelId;
}本地验证(只改 pi-native.js 这一个文件):报错消失,代理日志能看到该请求被正常转发 —— model=浮生云算/gpt-6.1-sol → forward POST → http://127.0.0.1:8317/v1/responses。
若希望非 ASCII 模型仍上报身份,则要插件端 encodeURIComponent + 代理端解码,并且 pluginHeadersMatchModel 的等值 / split("/").pop() 比较得一起改;成本更高,可另开 issue。
可选加固(同类风险)
- 头构造包一层兜底(漏该头 + 一行 warn),别让一个元数据头把整轮请求打死——现在这个失败的归因成本太高。
- 同类「用户可控字符串进头」的还有
x-bili-plugin-conversation(会话 id)等,建议统一走上面那个 sanitizer。
Activity
ranxianglei commentedon Oct 9, 2026
[bot] 🏷 已修复,PR:**#2457 #2455)。按八问体例给结论:
1. 是什么问题:模型 id 含中文(非 ASCII)时,插件把它原样写进
x-bili-plugin-model请求头,undici 在宿主构造请求时抛 ByteString TypeError,整轮请求在本地发出前就失败——该模型 100% 不可用。2. 什么原因导致的:HTTP 头值只能是 ByteString(≤ U+00FF),中文 id 首字符「浮」= U+6D6E > U+00FF。根因是插件在写入前对「用户可控字符串进头」零校验——赋值到普通对象本身不抛错,真正抛错发生在下游 undici 构造请求那一刻,所以 stamping 外层那个 try/catch 永远拦不住;必须在写入点拦截。
3. 影响范围与严重程度:pi / omp / opencode(v1+v2)/ dsh 四条 lane、dist 7 文件 8 处写入点全中招;触发条件 = 模型 id 含任意 > U+00FF 的字符(中文聚合站很常见);严重程度 = 该模型完全发不出去,且表现是一手 undici 文案 +
bili.log静默,用户极易误归因到宿主(你也是这么怀疑的)。4. 怎么修复的:抽一个共用守卫
asciiHeaderValue()(src/agent/shared.ts)——只放行「可打印 ASCII 且长度 1..256」的值(对齐代理侧\S{1,256}reader),否则返回 undefined、调用方跳过该头。在全部写入点套用(pi.ts / opencode-native.ts / opencode-v2.ts / dsh-native.ts,覆盖 dist 8 处)。跳过后代理侧对缺头本就信任(pluginHeadersMatchModel返回 true)→ 回退 body 模型 / registry 兜底;runtime-info 的 body 上报仍携带精确 id,无副作用。5. 是否有回归问题:纯 ASCII 模型 id 行为不变(照旧原样写入)。唯一细微差异:latin1(≤ U+00FF 的非 ASCII,如带变音符号
é)以前会被发送并被\S接受,现在同样被跳过走 body 兜底——实际效果等价(body 已带精确 id),且此类 id 现实中基本不存在。压缩语义、会话身份、其他头均无变化。6. 是否破坏缓存:不涉及。不改会话 key、不改消息前缀、不改压缩游标;跳过的只是一个每请求的元数据提示头,模型解析走 body,prefix cache 不受影响。
7. 是否改了设计方向:不涉及。纯 bugfix、单机制(用户可控字符串进头未校验),没动架构/边界/数据流。issue 里提到的「encodeURIComponent + 代理端解码」方案成本更高且改 wire 协议,按你的建议留作后续,本次不做。
8. 是否引入了新的配置:不涉及。无新 env / config / 默认值。
验证:
npm run typecheck绿;定向测试全绿——新增tests/issue2455-nonascii-model-header.test.ts(4 条回归,含驱动真实 opencode-v2stampHeaders路径穿过 undici 抛错点)+opencode-v2/runtime-info相关套件共 64 pass / 0 fail;npm run build成功,并已逐条核对 dist 全部 8 处写入点都被守卫包裹。全局回归交给 CI。遗留(可选加固,未纳入本 PR):同类「用户可控字符串进头」还有
x-bili-plugin-conversation(会话 id)、persona id,以及「一个元数据头不该打死整轮请求」的兜底 try/catch。这些触碰会话绑定语义(§7.4 human territory),且你标的是可选,我按聚焦原则拆出来单独评估——需要的话我可以另开一个带来源标记的 tracking issue,或在本 PR 里补上,听你拍板。收工判定:修复完整、覆盖全部 lane、行为变更已披露、回归受控,可合并(合并留给人)。
fix: guard the hermes lane's x-bili-plugin-model stamp against non-AS…
Merge pull request #2457 from ranxianglei/2026-10-09_nonascii-model-h…
fix: guard x-bili-plugin-model header against non-ASCII model ids (ra…