Skip to content

[bug] 模型 id 含中文时 x-bili-plugin-model 触发 undici ByteString 异常,整轮请求发不出去(7 文件 8 处写入点,pi/omp/opencode/dsh) #2455

Closed

Description

@EEEEEEEEdison

Description *

模型 id 含非 ASCII 字符(中文)时,插件把它原样写进 HTTP 请求头 x-bili-plugin-model,Node/undici 构造头时直接抛 TypeError,请求在本地发出前就失败,该模型 100% 不可用。

复现步骤(Windows,pi + bili 0.1.188 插件模式):

  1. 配置一个模型 id 含中文的供应商(本例 浮***/gpt-6.1-sol),加载 bili 扩展。
  2. 起会话,发任意一条消息。
  3. 该轮直接失败,宿主透出的一手错误文本就是:
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。

可选加固(同类风险)

  1. 头构造包一层兜底(漏该头 + 一行 warn),别让一个元数据头把整轮请求打死——现在这个失败的归因成本太高。
  2. 同类「用户可控字符串进头」的还有 x-bili-plugin-conversation(会话 id)等,建议统一走上面那个 sanitizer。

Activity

  1. ranxianglei commented on Oct 9, 2026

    @ranxianglei
    Owner

    🤖 Powered by ework · qwen3.8-27b

    [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-v2 stampHeaders 路径穿过 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、行为变更已披露、回归受控,可合并(合并留给人)。

  2. added 2 commits that reference this issue on Oct 9, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions