Skip to content

The default modelContextLimit of 200000 underestimates the context window for custom provider routing, and over-window | modelContextLimit 缺省 200000 会把自定义 provider 路由的窗口算小,且超窗的「无归因」请求会被照发而不是本地拒绝 #5907

Closed

Description

@Lumid0uce

摘要

在一条自定义的 OpenAI 兼容路由上(模型名带 provider 前缀,例如 <路由>/cn:deepseek-v4.1-flash,走本机网关),
ACP 会把**匿名(无归因)**链按 167232 token 估算窗口,而宿主其实声明了 contextWindow: 1000000。
于是一个已经被压缩到约 32 万 token 的会话,仍然被判为超窗并被"照样转发",结果要么上游回
400 prompt is too long,要么干等 15 秒拿不到首字节。

下面两个相互独立的缺陷共同造成了这个结果。

环境

  • billion-context 0.1.212(代理以 bili start --host 127.0.0.1 --port <port> 启动)
  • 宿主:DeepSeek Harness 桌面端 0.2.2-rc.1,插件 lane dsh,Windows
  • provider:magpie,baseURL http://127.0.0.1:<port>/v1,模型 <路由>/cn:deepseek-v4.1-flash,
    contextWindow: 1000000,maxTokens: 128000
  • billion-context.json 里只有 providers: {} 与 billing.prices(即没有配置 modelContextLimit)

观察到的现象

有归因的会话完全正常——宿主会带 x-bili-plugin-context-window,ACP 记下的是真实余量:

[window]   model=… source=plugin native=1000000 base=1000000 fallback=false
[headroom] effective window 1000000 -> 872000 (reserved 128000 for max output)
request: 480 msgs, inbound=2.4MiB, outbound=...  [acp-usage] input=112724

而一条匿名链(sess=pfa-…,在 prefix-affinity.json 里 identified:false)拿到的是回退值:

nudge INJECT T2: usage=2524% (4220212/167232), growth=3400373/22500 ... reason="EMERGENCY T2 distill"
preflight compressed 146 range(s), ~3350200 tokens saved (2692756 -> 323977) in 12758ms
payload still over window 167232 by local estimate (~323977 tok, 146 range(s) folded) with no upstream
evidence - forwarding once for the upstream to arbitrate size before failing fast (#3146/#4185)
stream silent 15000ms - holding client with SSE keep-alive comments past its undici body timeout (#1647)

上游随后在未经压缩的那条路径上回了 {"code":11115,"msg":"prompt is too long: 1449728 tokens > 1048576 maximum"};
而"压缩了但仍判超窗"的那条路径,每轮白烧 15 秒。

问题一:默认窗口纯靠猜,可能小 5 倍

modelContextLimit 缺省是 2e5,再减去默认输出预留 32768,得到 167232。
CONTEXT_LIMIT_TABLE 里确实有 ^deepseek -> 1e6,但
modelRoots('<路由>/cn:deepseek-v4.1-flash') 产出的是
['cn:deepseek-v4.1-flash', '<路由>/cn:deepseek-v4.1-flash'],
两者都不以 deepseek 开头,所以这条规则永远命中不了,模型一路落到 2e5。

建议(任意一条都能避免这次事故):

  1. 匹配表时也试一下 : 之后的最后一段(即 deepseek-v4.1-flash),别只拿完整模型名去匹配,
    这样厂商前缀(<路由>/cn:)就不会把家族识别打废。
  2. 只要任何可及处声明了真实窗口(宿主头、providers[url].models[m].context、runtime-info),
    就优先用它而不是常量缺省;另外当模型名看起来能分类却落到 fallback 时,把
    [window] ... fallback=true 这行从 info 提到 warn——现在这行太容易被忽略。
  3. 输出预留可以考虑从请求体里的 max_tokens 推导,而不是固定 32768;
    本例配置的是 maxTokens: 128000,200000 − 32768 两头都错。

问题二:恰恰在最需要拦的时候,"拒绝"退化成了"照发"

bodyProvablyOverWindow(bodyBytes, rawBody, declaredWindow) 开头是:

if (bodyBytes === void 0 || declaredWindow === void 0) return void 0

于是当归因失败、并且 effectiveContextWindow(ctx) 也解析不出窗口时(即上面那个匿名场景),
这道本地保险从构造上就是死的,超窗请求被原样转发。refuseUnattributed / settleThrewAttribution
两处调用点拿到的都是 undefined,所以什么也没拒。

建议:这两处调用点回退到 MIN_ASSUMED_WINDOW_TOKENS:

const unattributedDoom = bodyProvablyOverWindow2(body, init, effectiveContextWindow(ctx) ?? MIN_ASSUMED_WINDOW_TOKENS)
const doom = bodyProvablyOverWindow2(body, init, effectiveContextWindow(ctx) ?? MIN_ASSUMED_WINDOW_TOKENS)

这样"静悄悄发 145 万 token 让上游回 400"就变成一次本地 422
(x-bili-gate: oversize-refusal-unattributed),而且带可操作的提示。
我在本地就是靠这两处字符串替换在跑,行为符合预期。

其他较小的问题

  • [split-brain] 守卫是有效的,但两个实例抢端口时,输家会打 EADDRINUSE + split-brain,赢家继续跑;
    两段告警相差不到 40 ms,日志里看着像致命错误,其实输家是干净退出的。
    建议加一行"重复实例已退出,属预期"。
  • [slow-session] slow-session: 3086 msgs / inbound=24.9MiB ... starving the event loop
    在长会话的每一轮都会刷;加个硬上限,或者提示一下 bili sessions rm 的存在,
    会帮到那些不知道会话已经长到 270 万 token 的用户。

补充

我这边的复现、以及"通道侧如何绕过这个问题(改成注入 GUI 输入框以天然获得归因)",写在
#5907 (comment) 这条评论里;
另外同一份说明也同步到了移动端通道的两个仓库(dshgo / ds-harness-remote)。

归因与代理这套设计本身很好用,只有"无归因时的回退"这一条会静悄悄地赔上 400 和 15 秒。

Activity

  1. Lumid0uce commented on Nov 7, 2026

    @Lumid0uce
    Author

    我们这边的实测复现 + 一份可直接用的补丁(供参考)

    同一台机器上(dsh-desktop 0.2.2-rc.1 / billion-context 0.1.212 / Windows),同一个会话:

    入口 归属 代理看到的窗口 结果
    桌面 GUI 输入框(进程内 controller,每次带 resolvedClientTimeZone()) sess=session-… 1,000,000 − 128,000 = 872000 inbound=2.4MiB → outbound=712KiB,cache hit 99%,正常
    插件直投 api.sessions.prompt / session/prompt sess=pfa-…(identified:false) 200,000 − 32,768 = 167232 payload still over window 167232 → forwarding once for the upstream to arbitrate → 上游 400 或 stream silent 15000ms

    关键日志(匿名链):

    nudge INJECT T2: usage=2524% (4220212/167232), growth=3400373/22500
    preflight compressed 146 range(s), ~3350200 tokens saved (2692756 → 323977) in 12758ms
    payload still over window 167232 by local estimate (~323977 tok, 146 range(s) folded)
      with no upstream evidence — forwarding once for the upstream to arbitrate size before failing fast
    request: 3086 msgs, inbound=24.9MiB, outbound=1.1MiB, local=13599ms
    stream silent 15000ms — holding client with SSE keep-alive comments past its undici body timeout
    

    压缩器其实工作正常(269 万 → 32.4 万),是目标窗口错了,所以它以为还是超窗。

    补丁(两处调用点,一行业务表达式)

    - const unattributedDoom = bodyProvablyOverWindow2(body, init, effectiveContextWindow(ctx));
    + const unattributedDoom = bodyProvablyOverWindow2(body, init, effectiveContextWindow(ctx) ?? MIN_ASSUMED_WINDOW_TOKENS);
    
    - const doom = bodyProvablyOverWindow2(body, init, effectiveContextWindow(ctx));
    + const doom = bodyProvablyOverWindow2(body, init, effectiveContextWindow(ctx) ?? MIN_ASSUMED_WINDOW_TOKENS);

    因为 bodyProvablyOverWindow() 开头是 if (bodyBytes === void 0 || declaredWindow === void 0) return void 0;,
    无归因时 declaredWindow 为 undefined,本地超窗拒绝这条保险整条哑火,1.45M token 的 body 会裸发。
    兜底到 MIN_ASSUMED_WINDOW_TOKENS(1e6) 之后,这类请求会被本地 422 拒掉(x-bili-gate: oversize-refusal-unattributed),
    而不是打到上游变成 400。

    另外配置层(无热加载,改完重启代理):

    { "modelContextLimit": 1000000, "providers": {} }

    一个额外观察

    CONTEXT_LIMIT_TABLE 对 cn:deepseek-v4.1-flash 不命中:modelRoots() 产出
    cn:deepseek-v4.1-flash 与模型全名,都不以 deepseek 开头,所以 ^deepseek → 1e6 那条规则走不到,最终落到 2e5 缺省。
    如果 root 匹配改成"按 : 取最后一段"或"用 includes",很多自定义 provider 前缀的模型就不用改配置了。

    通道侧的兜底(不依赖上游修)

    把插件直投改成"写进 GUI composer 再回车",宿主自己的 controller 会盖章,天然拿到 sess= 归因——
    实测与人工输入完全一致(见上表第一行)。Windows 上的最小实现是 CDP:
    Input.dispatchMouseEvent 真点 → Ctrl+A+Delete 清空 → Input.insertText → Input.dispatchKeyEvent 真 Enter,
    并要求渲染进程以 --remote-debugging-port=9222 启动。合成事件 / execCommand('insertText') / UIA ValuePattern.SetValue
    在 Chromium 的 React composer 上都不落地,只有真 CDP 输入事件能生效。

    如果需要,我可以把注入器与幂等补丁脚本整理成独立仓库发上来。

  2. ranxianglei commented on Nov 7, 2026

    @ranxianglei
    Owner

    🤖 Powered by ework · qwen3.8-27b

    (ai: vllm-qwen/qwen3.8-27b)

    查重: 不重复。 相关但不同: #2074/#2081(已关)是同机制家族——family 表对带前缀 id 的匹配——修了 prefix/name 斜杠形;本单是它剩下的残差形状(vendor/region:model 冒号前缀)。#5502(已关决策单)的方向对齐选项 A(保留 2e5 缺省、随现场证据扩展 family 匹配);本修复不动缺省值与 miss 语义。#5032/#5037 跟踪 omp/opencode 直发守卫的分车道启用,本单只动 dsh 车道两个调用点,不碰那个设计空间。

    标签: bug / dsh / scope/wiring / p1(窗口误判导致该入口每轮必败,沿用 #5500/#5502 同类 p1 评定)。

    代码级核实完成(v0.1.212 = 最新 release,代码与 master 一致),两个问题都成立:

    1. Issue 1 的根因就是你第二条观察: modelRoots('liuyue/cn:deepseek-v4.1-flash') 只产出 cn:deepseek-v4.1-flash 与全名两个根,都不以 deepseek 开头 → /^deepseek/i → 1M 规则走不到 → 落 2e5 缺省 − 32768 输出预留 = 167232。压缩器一直工作正常(269 万 → 32.4 万),是目标窗口错了。
    2. Issue 2 成立: bodyProvablyOverWindow 开头就是 if (bodyBytes === undefined || declaredWindow === undefined) return undefined(src/agent/native-intercept.ts:710)。归因失败 + 宿主解析不出自定义 provider 的窗口(modelInfo 缓存 miss)时,effectiveContextWindow(ctx) 返回 undefined,dsh 两个 gate arm(refuseUnattributed / settleThrewAttribution)整条哑火,落到 refuse() = "sent DIRECT (uncompressed)"——1.45M token 的 body 就这样裸发上游。你的两行补丁方向正确,我按此实现(对齐 Context Window Overflow Issue Summary: Raw Requests Sent Directly to Upstream Rejected + Local Intercept Cooldown Lockin | 上下文超窗问题汇总:请求裸冲上游被拒 + 本地拦截冷却锁死会话(统一分析入口) #4079 "never assumes below 1M" 教义)。

    修复方案(一个 PR,闭环):

    范围说明: 服务端 "forward once for the upstream to arbitrate"(#3146/#4185)是有意的,不改——窗口对了 payload 就装得下,不会再走到那条路;"proxy not ready" 的 directSendOrRefuse 路径(src/agent/native-intercept.ts:1436)有同款洞,但它属于 #5032 的分车道启用决策(启用会改变每次降级直发都缓冲 body 的性能姿态),本 PR 不碰。你报的两个小项(split-brain 日志措辞、slow-session 告警上限)属实,按一单一范围不混进本 PR,结论里给评估,是否单独跟踪等你裁决。CDP 通道侧兜底属于你的工具链,bili 代码范围外。

    开工,PR 出来回贴。

  3. changed the title [-][windows] `modelContextLimit` default (200000) mis-sizes custom provider routes, and oversized *unattributed* requests get forwarded instead of refused[/-] [+]`modelContextLimit` 缺省 200000 会把自定义 provider 路由的窗口算小,且超窗的「无归因」请求会被照发而不是本地拒绝[/+] on Nov 7, 2026
  4. changed the title [-]`modelContextLimit` 缺省 200000 会把自定义 provider 路由的窗口算小,且超窗的「无归因」请求会被照发而不是本地拒绝[/-] [+]The default `modelContextLimit` of 200000 underestimates the context window for custom provider routing, and over-window | `modelContextLimit` 缺省 200000 会把自定义 provider 路由的窗口算小,且超窗的「无归因」请求会被照发而不是本地拒绝[/+] on Nov 7, 2026
  5. ranxianglei commented on Nov 7, 2026

    @ranxianglei
    Owner

    🤖 Powered by ework · qwen3.8-27b

    (ai: vllm-qwen/qwen3.8-27b)

    **PR: #5910 #5907,标签 bug / p1 / dsh,按本 PR 自身 diff 判定)。本地定向验证:rebase 到当前 master(cda6e73)后 routing 26/26、dsh-native 75/75、apig-resign+issue5686 49/49、typecheck、build 全绿;全量回归以远程 CI 为准(PR 刚开,check 尚未出结果,不轮询,红了我会定向排查)。

    审核过程中发现 master 当天合入的 #5875/#5613(dsh gate 校准估算层)重写了本 PR 触碰的两个 gate arm——已 rebase 并适配:它们的校准层对"窗口未知"仍按构造 fail-open,本 PR 修的正是它留下的那个洞,两者互补不冲突。PR 描述里已写明与它的关系。

    结构化八问

    1. 是什么问题:两个独立缺陷造成同一个现场症状——① vendor/region:model 形模型 id(如 liuyue/cn:deepseek-v4.1-flash)匹配不到 family 表任何规则,窗口落 2e5 缺省(−32768 预留 = 167232),而真实模型是 1M;② 归因失败且宿主解析不出自定义 provider 窗口时,dsh gate 的本地超窗拒绝整条哑火,超大未归因请求被裸发上游(每轮必 400 或挂 15s)。
    2. 什么原因导致的:① modelRoots 只剥斜杠前的 provider 段,不剥冒号前缀,产出的根都以命名空间开头,^-anchored 规则全部走不到;② 共享判定函数在 declaredWindow === undefined 时按构造返回 undefined(契约如此),而两个 gate arm 传入的 effectiveContextWindow(ctx) 在宿主从未 resolveModelInfo 成功时就是 undefined——保险丝两头都没接上。
    3. 影响范围与严重程度:所有"宿主解析不出窗口 + 归因失败"的 dsh 插件车道会话(自定义 provider + 未归因/threw 链,即你实测的 api.sessions.prompt 直投形态);该入口每轮必败 = p1(沿用 localInputEstimate persists the char-count upper bound (~4x char/token) and poisons every later request; with a small window preflight then reports "exceeds the model window" on a 2.4x-overestimate #5500/decision request: lookup-miss window fallback — treat unknown as unknown instead of the 2e5 default #5502 同类评定);有归属会话与其它车道不受影响;附带危害是 nudge/preflight 对着永远装不下的目标窗口空转压缩。
    4. 怎么修复的:① modelRoots 渐进剥前缀——每个既有根再派生"最后一个冒号后的尾段"作候选根,严格排在未剥形式之后测(防 o3-preview 这类后缀误撞别的 family 规则),无冒号 id 的根列表逐字节不变;② 两个 gate arm 传 declared ?? MIN_ASSUMED_WINDOW_TOKENS(1M floor,Context Window Overflow Issue Summary: Raw Requests Sent Directly to Upstream Rejected + Local Intercept Cooldown Lockin | 上下文超窗问题汇总:请求裸冲上游被拒 + 本地拦截冷却锁死会话(统一分析入口) #4079 教义),拒绝日志区分 "no window declared; assuming the 1M floor" 与真实声明。
    5. 是否有回归问题:审过的既有行为——无冒号 id 的查表结果(控制行钉死不变)、声明窗口下的 gate 语义([dsh-native] takeover-gate refusal (threw state) fails open with the FULL uncompressed transcript; a >window local history dies upstream with 400 (maximum context length) #2924/The input (6165048 tokens) is longer than the model's context length (1048580 tokens(todo fix)). #4068/[Bug][dsh] Model requests silently bypass the compression proxy (fail-open gate) — 927K-token prompt sent raw, 1-token reply, session bricked with 400 #5613 原有断言原样通过)、floor 之下 body 保持 direct(钉死)、共享判定函数契约不变(undefined-on-missing-data,故 omp/opencode 车道与 omp/opencode lanes: oversized history egresses uncompressed when the proxy is down — direct-send window guard still unarmed (#5025 family residual) #5032 的分车道决策不受触碰)、服务端 forward-once 仲裁语义不动、tierGatedStandardWindow 随同一根扩展一致受益。有意变更(即修复本身):窗口未知 + 可证超 floor 的 body 从"裸发上游"变为"本地 422 拒"。
    6. 是否破坏缓存:不涉及——未动任何缓存/重试/游标/去重键。
    7. 是否改了设计方向:不涉及架构转向;是在既有 fallback 教义(Context Window Overflow Issue Summary: Raw Requests Sent Directly to Upstream Rejected + Local Intercept Cooldown Lockin | 上下文超窗问题汇总:请求裸冲上游被拒 + 本地拦截冷却锁死会话(统一分析入口) #4079 "never assumes below 1M")内补齐一个失接点。
    8. 是否引入了新的配置:无。modelContextLimit 逃生门照旧有效(你配置层的临时方案继续可用,修复后不再必需)。

    遗留小项(不在本 PR,见下一条 🔶 评论):split-brain 端口竞争日志措辞、slow-session 告警每轮刷屏——均属实,按一单一范围未混入,处置等你裁决。

  6. ranxianglei commented on Nov 7, 2026

    @ranxianglei
    Owner

    🤖 Powered by ework · qwen3.8-27b

    (ai: vllm-qwen/qwen3.8-27b)

    🔶 需要决策

    背景一句话:主修复(PR #5910)已交付;issue 里顺带报的两个小项我核实属实,但按一单一范围没有混进该 PR,现在悬在这里等你定是否跟踪。

    待决问题:这两个小项修不修、怎么修——

    • a) split-brain 端口竞争日志:落败实例的 EADDRINUSE 与胜出实例的告警相隔 ~40ms 成对出现,读起来像致命错误。修法已想清:在该竞争路径给双方日志各补一句"duplicate instance exited, this is expected"级别的说明(几行,纯措辞)。
    • b) [slow-session] 告警每轮刷屏(3086 msgs / 24.9MiB 会话每轮都记一条):修法二选一——硬上限(超过后静默)或每会话只记一次并附 bili sessions rm 提示。这动的是告警节奏,属行为变更,需要你点头方向。

    选项各带代价:

    • A. 暂不跟踪(默认):零动作;日志噪音与刷屏保留,均不影响功能与主修复。
    • B. 批准跟踪:我按上面想清的修法开修(a 几行;b 按你选的方向),独立小 PR;代价是一次额外调度。
    • C. 并入下次发版批次:若后续有触碰同路径的 PR 可顺手带上,否则等同 A。

    我的推荐:B——两项都是修法已明确的小精修,不值得为它们单等一个发版窗口;不决定的默认行为是 A(原样保留),主线不受任何影响。

  7. Lumid0uce commented on Nov 8, 2026

    @Lumid0uce
    Author

    选 B —— 批准跟踪。

    • a) split-brain 端口竞争日志:按你写的来,落败实例那句 "duplicate instance exited, this is expected" 加上即可,纯措辞,我没别的意见。
    • b) [slow-session] 刷屏:选"每会话只记一次 + 附 bili sessions rm 提示"这条,不要用硬上限静默——一次足够止噪,但静默会让人彻底失去"这个会话已经长到多大了"的线索。另外如果方便,那一条里顺带把当前 msgs 与估算 token 数写全(现在只有 msgs 和 inbound MiB),用户看到 270 万 token 才会真去清会话。

    两条都独立小 PR 就行,不用等我再确认。

    另外确认 PR #5910 的方向与我这边实测一致:两处 gate arm 的 ?? MIN_ASSUMED_WINDOW_TOKENS 我在本地跑了几个小时,行为符合预期(未知窗口 + 可证超 floor 的 body 变成本地 422,不再裸发)。modelRoots 那处"冒号尾段作候选根、排在未剥形式之后"的顺序也和我想的一致——先把 o3-preview 这类后缀误撞的风险压住,是对的做法。

    修复合入后我这边两样东西就都可以撤掉:配置层的 modelContextLimit: 1000000 逃生门,以及我自己打在 bundle 上的两行字符串补丁(我会在升级后确认 bundle 不再需要它,避免和上游版本打架)。

  8. ranxianglei commented on Nov 8, 2026

    @ranxianglei
    Owner

    🤖 Powered by ework · qwen3.8-27b

    (ai: vllm-qwen/qwen3.8-27b)

    谢谢确认——#5910 的方向与你本地几小时实测一致,这正好把"未知窗口 + 可证超 floor → 本地 422"这条行为钉住了。

    两个小项已修完,各一个独立标注 commit,推到了 #5910 分支(它还没合并,合入前小修按仓库惯例直接上原分支;若 #5910 先被合并,我再切 follow-up PR):

    • a) split-brain 日志对(6c4c9c49b9):只动了 holder 诊断的 serving 分支——追加一句"宿主重启与旧实例竞态时,重复实例退出是预期结果、无需处理;若附近时间戳有 [split-brain] 通告点名本退出 pid,就是同一次启动竞态在收尾,不是数据损坏进行中"。wedged/dead 诊断分支一字未动(那个是真故障告警,不能一起软化)。
    • b) slow-session 刷屏(a3f01bc6bf):按你选的方向——每会话每进程只记一次,不用硬上限静默;那一条写全 msgs / inbound / ~N est tok(bytes/4 乐观底数,标 est)+ bili sessions rm <id> 提示(命令已核实存在)。行为变更披露:旧 = 每个超 1s 墙钟预算的回合都记一条;新 = 同一会话只记首次(有界 map,超 512 会话淘汰最早告警者;"passthrough" 伪会话同样适用一次语义)。

    验证:定向测试 32/32 绿(port-owner + issue4528,含双向钉:同会话第二次静默 / 另一会话照常首告)、typecheck、build;CI 对新 head 重跑中(不轮询,红了定向排查)。PR 描述已补这两个 commit 的说明与行为变更段。

    权限说明(照实):你的"选 B"是参考意见(你不是 owner);我动手的依据是仓库既有的小问题直接修规则(日志措辞/噪音类可直接修),具体方案采用了你作为受影响方选定的方向。owner 若有异议,合并时 reject 这两个 commit 即可,主修复不受影响。needs-decision 标签已摘(决策点已落地)。

    你撤 workaround 的计划没问题:合入并升级后,配置层的 modelContextLimit: 1000000 和你打在 bundle 上的两行补丁都可以撤——bundle 补丁与上游修复重叠,升级后保留反而会打架;配置逃生门任何时候都还在,留不留随你。

  9. 3 remaining items

  10. ranxianglei commented on Nov 8, 2026

    @ranxianglei
    Owner

    🤖 Powered by ework · qwen3.8-27b

    [bot] 🏷

    补充的「整轮没进 ACP」这个面很有价值。我逐条对着代码核实并本地实测了。结论先行:这不是独立新问题,是 #5907 的另一个缺陷面(同一失败域 = dsh 车道超大载荷撞上游窗口;机制不同 = 接管覆盖面缺失),按平台规则不另立项——能修的部分已实现并推到 PR #5910 分支(已 rebase 到最新 master,第 5 个 commit 7b09b2fefa;本地定向测试 216/216 + typecheck + build 全绿;CI 当前 0 失败 / 12 项运行中)。

    你的观察逐条核实:

    1. ✅ 接管机制描述正确:patch globalThis.fetch(每进程一次)+ 改写到 {proxy}/bili/{原始URL}(src/agent/native-intercept.ts 的 installNativeFetchIntercept)。
    2. ⚠️ 「完全静默」只对部分情况成立。第三方赋值式顶替自 方式一安装的dsh 原生插件:llm-pi-ai 传输(OpenAI SDK + options.fetch 注入)绕过 globalThis.fetch 拦截,模型请求不进代理、压缩完全不生效 #1158 起并不静默:受保护的 accessor 会记录每一次外部安装并把它重新接成下游(third-party globalThis.fetch install detected (#1158) — re-chaining as downstream),且我用 git show v0.1.212 确认这段逻辑已在你的构建里。所以如果某轮是被别的组件赋值顶掉的,bili.log 里应有一行。零行说明属于以下向量之一:
      • 客户端在插件装上前就捕获了 fetch 引用(你的 guard 仓库正好兜这一类);
      • 非 fetch 传输(undici dispatcher / 裸 http / WebSocket);
      • 该轮跑在另一个 JS realm / 进程(手机端轮次的间歇性很指向这里);
      • 宿主属性非可配置 → 退化为经典直装,该姿态在装钩时原本完全静默(见下)。
        前三类是宿主内部的事,bili 侧看不到——请求没到代理,按构造就零痕迹,不做宿主侧配合无法逐事件留痕。bili 能做的是把「本进程处于哪种退化姿态」变成可见的。
    3. ⚠️ 你贴的 403 body("Request not allowed")在 bili 生产代码路径里不存在(全仓 grep,只有测试 mock)。我在干净实例上实测:裸路径 POST /v1/chat/completions → 400 Missing stable conversation identity…(bili 把裸模型路径当合法请求、要求会话身份头);POST /bili/http://… → 通道被接受(我的假上游返回 502 upstream failure,即请求进了 bili 并到达内嵌上游)。你那台机器的 403 可能来自同端口的其它组件(如 dsh-http-proxy)——但结论不变:/bili/ 通道确实可以直连使用,不依赖宿主钩子。
    4. ✅ 967232 = 1e6 − 32768 输出预留,正确。

    本次做了什么(commit 7b09b2fefa,挂在 #5910):

    你的 guard 仓库:未做任何动作(跨仓动作需 owner 批准),对 captured-reference 一类它仍是有效的双保险,可以继续用。

    🔶 需要决策(跨仓库投递需 owner 批准)

    • 背景:手机 App 轮次绕过的头号嫌疑在 dsh-desktop 侧——该 lane 走了不同进程/realm,或捕获引用/非 fetch 传输;这是宿主代码,bili 不可见。
    • 待决:是否向 dsh-desktop 项目投一个排查 issue(附本次核实结论 + 复现特征:同一手机同一会话间歇性整轮漏掉、探针 vs bili.log 时间线对齐法)?
    • 选项:A) 批准投递 —— 宿主侧查移动端 lane 的进程/传输路径,可根治;代价是一次外部沟通。B) 不投递 —— 用户继续用自己的 guard 仓库兜底(对 captured-reference 有效,对非 fetch 传输无效);代价是真向量若是传输类型则兜不住。
    • 推荐:A。不决定的默认行为:B,维持现状(bili 侧可见性 + 文档已就位,无阻塞项)。

    八问:

    1. 是什么问题:部分轮次的模型请求根本没进压缩代理(bili.log 零行),原始载荷直接撞上游窗口上限;根因是宿主侧 fetch 接管钩子没覆盖那些轮次,且无守卫姿态在 bili 侧零痕迹。
    2. 什么原因导致的:接管靠 patch globalThis.fetch,只覆盖「走当前全局 fetch 的调用」;捕获引用/非 fetch 传输/不同 realm 的绕过按构造到不了代理;叠加无守卫姿态(不可配置属性或 env 逃生舱)装钩时不告警,整体不可诊断。
    3. 影响范围与严重程度:影响「钩子覆盖不全」的原生车道(dsh 等);症状 = 偶发上游 400 prompt-too-long 且本地无任何记录;始终走当前全局 fetch 的用户不受影响。触发条件窄但命中时体验毁灭(不能用且无线索)。
    4. 怎么修复的:让无守卫姿态可见(一次性 warn + 后果 + 兜底指引),把 /bili/ 直连文档化为受支持的兜底形态;宿主侧向量识别(疑似主因)超出 bili 可见范围,是否转 dsh-desktop 待 owner 拍板。
    5. 是否有回归问题:审过既有行为——方式一安装的dsh 原生插件:llm-pi-ai 传输(OpenAI SDK + options.fetch 注入)绕过 globalThis.fetch 拦截,模型请求不进代理、压缩完全不生效 #1158 自愈路径未动(既有 22 个 case 全绿);guard 可用的宿主不会多出新警告(只在 else 分支触发);路由/wire/配置无变化;rebase 冲突按双方意图并集解决(locale 数字 + windowNote),四旧 commit 新基线重验通过。
    6. 是否破坏缓存:不涉及(无缓存/重试/游标相关改动)。
    7. 是否改了设计方向:不涉及(无架构变化,只补观测与文档)。
    8. 是否引入了新的配置:不涉及(无新 env/config;既有 BILI_RECLAIM_FETCH_PATCH 语义不变,只是其禁用态现在会告警)。

    判定:bili 侧能修的部分已完成并挂在 #5910(同线程同 PR,一 issue 一 PR),等 CI 绿 + owner 合并;跨仓部分按上面待决。合并升级后你可以移除 modelContextLimit: 1000000 配置和 bundle 补丁(colon 窗口问题已在上游修好);guard 仓库可留作双保险。

  11. ranxianglei commented on Nov 8, 2026

    @ranxianglei
    Owner

    🤖 Powered by ework · qwen3.8-27b

    [bot] 🏷

    这条转发与你 2026-11-08 14:43 (UTC) 发的「补充另一个缺陷面」评论逐字相同,我核对过线程:该评论只出现过一次,且我已针对它发过长回复(以「补充的『整轮没进 ACP』这个面很有价值…」开头那条,含逐条核实、commit 7b09b2fefa、🔶 跨仓决策请求和八问)。本次没有检测到新信息,不重复全文。

    刚做的实时状态核查(2026-11-09):

    • PR [PR] fix: #5907 colon-prefixed gateway model ids resolve their window; unknown-window dsh oversize arm falls back to the 1M floor #5910:open、未合并,head 7b09b2fefa(含全部 5 个 commit:colon 窗口匹配、未知窗口 1M floor 兜底 ×2 车道、split-brain 日志措辞、slow-session 去重 + /bili/ 直连文档与无守卫告警)。
    • CI:该 head 上 30 项检查运行中,0 失败 / 9 项 pending(尚未跑完)。
    • master 又前进了(base 已到 848f59b996),分支显示 behind——无冲突、不阻塞合并;如你希望合并前完全对齐最新 master,说一声我随时 rebase + 重验。
    • 待你拍板:上一条回复里的 🔶 跨仓项——是否向 dsh-desktop 投排查 issue(选项 A 批准投递 / B 不投继续用你自己的 guard 仓库兜底,推荐 A;不决定的默认行为是 B)。

    如果这条其实是你想补的新内容(而不是重复投递),告诉我具体要补充什么即可。

  12. ranxianglei commented on Nov 8, 2026

    @ranxianglei
    Owner

    🤖 Powered by ework · qwen3.8-27b

    [bot] 🏷

    这条转发与你 2026-11-08 14:43 (UTC) 发的「补充另一个缺陷面」评论逐字相同,我核对过线程:该评论只出现过一次,且我已针对它发过长回复(以「补充的『整轮没进 ACP』这个面很有价值…」开头那条,含逐条核实、commit 7b09b2fefa、🔶 跨仓决策请求和八问)。本次没有检测到新信息,不重复全文。

    刚做的实时状态核查(2026-11-09):

    • PR [PR] fix: #5907 colon-prefixed gateway model ids resolve their window; unknown-window dsh oversize arm falls back to the 1M floor #5910:open、未合并,head 7b09b2fefa(含全部 5 个 commit:colon 窗口匹配、未知窗口 1M floor 兜底 ×2 车道、split-brain 日志措辞、slow-session 去重 + /bili/ 直连文档与无守卫告警)。
    • CI:该 head 上 30 项检查运行中,0 失败 / 9 项 pending(尚未跑完)。
    • master 又前进了(base 已到 848f59b996),分支显示 behind——无冲突、不阻塞合并;如你希望合并前完全对齐最新 master,说一声我随时 rebase + 重验。
    • 待你拍板:上一条回复里的 🔶 跨仓项——是否向 dsh-desktop 投排查 issue(选项 A 批准投递 / B 不投继续用你自己的 guard 仓库兜底,推荐 A;不决定的默认行为是 B)。

    如果这条其实是你想补的新内容(而不是重复投递),告诉我具体要补充什么即可。

  13. Lumid0uce commented on Nov 9, 2026

    @Lumid0uce
    Author

    决定:批准(A),但官方 dsh-desktop 没有公开投递口;已就近投到两个下游仓库

    按 A 执行,先探了宿主侧的入口,结果是:

    • 本机装的是官方桌面版(DeepSeek Harness.exe 0.2.2-rc.1,签名 = Hangzhou DeepSeek Artificial Intelligence Co., Ltd.),内部包 @deepseek-ai/dsh-desktop / @deepseek-ai/dsh-desktop-host 都是 "private": true,无公开仓库。
    • deepseek-ai/deepseek-harness 存在但 issues 关闭(has_issues: false),无法投递。

    所以把同一条线索就近投到了这条链路的两个可操作仓库(手机侧客户端 / 手机投递插件层):

    • xiazhi88/dshgo#6:手机端直投 prompt 会让长会话发炸(拿不到 host 归因 → ACP 按 200k sizing → 400 / 15s 无首字节)
    • liguobao/ds-harness-remote#348:session/prompt 直投拿不到 host 归因(同一条 BUG 的下游视角)

    如果你知道桌面宿主侧真实的收件仓库(或愿意我投到某个社区桌面端仓库),说一下地址我立刻转投;否则这两条就作为 DSHGO / harness-remote 维护者能自己动手的部分。

    补两条本轮新证据

    1)ACP 自动升到 0.1.213,窗口判断函数改名了——?? MIN_ASSUMED_WINDOW_TOKENS 需要对名字重打

    本机 2026-11-09 11:23 自动升级 billion-context 0.1.212 → 0.1.213,我 0.2.0 插件的字符串补丁当场静默失效(pattern not found)。核对 0.1.213 的 dist/agent/dsh-native.js:

    • 0.1.212:bodyProvablyOverWindow2(body,init,effectiveContextWindow(ctx))
    • 0.1.213:bodyOverWindowDetail2(body,init,effectiveContextWindow(ctx)),内部新增 CALIBRATED_TEXT_TOKEN_RATIO 校准;bodyProvablyOverWindow 这个名字在 0.1.213 里已完全不存在。

    而那两处调用点(settleThrewAttribution 的 doom=、refuseUnattributed 的 unattributedDoom=)仍然是裸 effectiveContextWindow(ctx),if(bodyBytes===void 0||declaredWindow===void 0)return void 0 也原样保留 → 洞还在。提醒一句:如果 #5910 的补丁与测试是按 0.1.212 的函数名写的,rebase 到含 0.1.213 的 master 时请确认没被改名绕过(我这边已出 0.2.1,两套函数名都覆盖)。

    2)我那个 403 是错的,你的 400 才对

    复测时把响应头抓全了:那条 403 {"error":{"type":"forbidden","message":"Request not allowed"}} 带着 server: cloudflare、cf-ray: …-SJC、x-robots-tag: none —— 不是 bili 的门,是本机链路上别的东西返回的(本机上游是 Cloudflare 前置的 API,中间还有一层系统代理)。

    /bili/ 的结论仍然成立,而且用更干净的观测重验了:POST {proxy}/bili/http://127.0.0.1:<upstream>/v1/chat/completions 拿到的是内嵌上游自己的错误体(magpie knows no model "x" … 加完整模型清单),说明请求确实穿过代理到了上游。给你一条错误线索,抱歉。

    关于「完全静默」四类向量,我能确认到什么

    手机轮次漏网时:同一进程里其它会话(桌面发起)在同一秒正常 forward POST;探针记录的 options 与命中轮次完全同形(sessionId / purpose / tools / maxTokens 都一样),失败轮在 bili.log 里是 0 行。所以我倾向你列的第 3 类(另一个 JS realm / 进程)或第 1 类(捕获引用);「非 fetch 传输」用探针无法区分——探针的 globalThis.fetch 包装是改写链的内层,只看得见已经到代理的请求。

    这条已经写进我投给下游的两个 issue 的复现特征里。另外你新加的「无守卫姿态一次性 warn」很有用——如果后续现场里出现那条 warn,就能直接把向量钉在「属性不可配置 / 环境变量逃生舱」这一支上。

  14. ranxianglei commented on Nov 9, 2026

    @ranxianglei
    Owner

    🤖 Powered by ework · qwen3.8-27b

    [bot] 🏷

    收到。三条新信息都核实过了,逐条回:

    1) 改名核对——你的观察完全正确,#5910 没有被改名绕过

    对着 v0.1.213 tag(你自动升级到的版本)核过:

    • src/agent/dsh-native.ts:2294(settleThrewAttribution)与 :2318(refuseUnattributed)确实仍是裸 bodyOverWindowDetail(body, init, effectiveContextWindow(ctx)),bodyProvablyOverWindow 这个名字在 0.1.213 里已完全消失 → 发布版里洞还在,与你 dist 侧观察一致。
    • 缺失原因:[PR] fix: #5907 colon-prefixed gateway model ids resolve their window; unknown-window dsh oversize arm falls back to the 1M floor #5910 尚未合并(刚 live 查:open、unmerged、head 7b09b2fefa)。v0.1.213 是从不含本 PR 的 master 切的。
    • 你担心的「改名绕过」没有发生:rebase 冲突正是在这两行解的——我的分支 head 上两处已是 bodyOverWindowDetail(body, init, declared ?? MIN_ASSUMED_WINDOW_TOKENS)(:2317/:2347,两臂都带),配 locale 数字措辞,dsh-native 75/75 测试钉的就是新措辞。也就是说:[PR] fix: #5907 colon-prefixed gateway model ids resolve their window; unknown-window dsh oversize arm falls back to the 1M floor #5910 合并 + 你升级到含修复的版本后,guard 里针对这个洞的字符串补丁可以退役;fetch 改写兜底部分建议保留,继续兜绕过类。顺带一提:对 dist 做字符串补丁天然脆(这次 bodyProvablyOverWindow→bodyOverWindowDetail 就让它静默失效了),修复落地后建议改成行为观测(本地 422 / bili.log 有无该行)而不是匹配函数名。

    2) 403 更正——谢谢

    server: cloudflare + cf-ray 说明那条响应来自 Cloudflare 前置的上游(中间还隔一层系统代理),不是 bili 的门——我此前猜「同端口的其它组件」方向一致,你用 magpie 错误体 + 模型清单的重验证据更干净。/bili/ 直连结论不变。

    3) 向量收窄——同意你的推理

    同进程同秒其它会话正常 forward + 探针 options 跨轮完全同形 → 排除配置漂移与代理故障;剩下「不同 realm/进程」或「捕获引用」两类,都在宿主侧、bili 不可见;非 fetch 传输用内层探针包装确实区分不了——对。#5910 新加的无守卫姿态一次性 warn 如果日后在现场出现,就能把向量钉到「属性不可配置 / env 逃生舱」那一支。

    关于「批准 A」

    你的两个下游投递已确认存在且标题相符(xiazhi88/dshgo#6、liguobao/ds-harness-remote#348,只读核对过)。桌面宿主侧的公开收件渠道我也不掌握(包 private:true、deepseek-harness issues 关闭——与你探测一致)。目前我没有可执行的外部投递动作;如果 owner(@ranxianglei)知道官方桌面宿主的渠道,请他直接说一声——按平台规则我的跨仓投递需要他的明确批准,他点头我就投。

    实时状态(刚查)

    本轮无新代码(纯核实回复)。等待中:CI 绿 + owner 合并(+ 可选:owner 提供官方桌面渠道)。

  15. Lumid0uce commented on Nov 9, 2026

    @Lumid0uce
    Author

    给个仓库地址,也补一条这两天新抓到的证据——它跟「完全没进 ACP」是两个不同形状。

    项目地址:https://github.com/Lumid0uce/dsh-acp-window-guard (MIT,零依赖,README 里有装法 / 体检 / 回滚)

    它现在做三件事,按你上一条的建议已经分层了:

    1. 配置兜底:把 modelContextLimit 抬到 1000000(你核过的 967232 = 1e6 − 32768 就是这么来的)。
    2. 超窗保险:给两处 ...(body, init, effectiveContextWindow(ctx)) 补齐 ?? MIN_ASSUMED_WINDOW_TOKENS。字符串补丁现在两版函数名都认(bodyProvablyOverWindow2 / bodyOverWindowDetail2)——这次 0.1.213 自动升级确实把旧补丁静默打没了,我才改成多模式匹配 + shape 字段回报命中哪一版。你说的「[PR] fix: #5907 colon-prefixed gateway model ids resolve their window; unknown-window dsh oversize arm falls back to the 1M floor #5910 合并后可以退役」我认,届时只保留行为观测(本地 422 / bili.log 有没有那一行),不再匹配函数名。
    3. fetch 改写兜底(这条按你建议保留):把没进 ACP 的模型端点请求改写成 {proxy}/bili/{原始URL},并且补上 x-bili-plugin / x-bili-plugin-conversation / -model / -context-window / -max-output 身份头——这一条是下面新证据逼出来的。

    新形状:进了 ACP,但没身份 → 被当匿名链

    原来的假设是「整轮绕过代理」。实测抓到第三种:请求确实被我的桥送进了代理,但没带身份头,于是 bili 新建了 pfa-* 链,压缩几乎没起作用:

    [prefix-affinity] new anonymous session pfa-… (depth=1035, lineage=truncated of session-…; ua="deepseek-harness/0.2.2-rc.1 …"; plugin=-)
    [headroom] model=…: effective window 900000 -> 772000 (reserved 128000 …)
    nudge idle: usage=340% (2622166/772000)
    [preflight] payload ~2676821 tok vs window 772000 (~3.5x) — raising summarization budget 16 -> 32
    [preflight] fit-forward on estimate only: ~722482 tok vs window 772000 …
    preflight made no progress but the payload fits; forwarding as-is
    request: 1036 msgs, inbound=4.3MiB, outbound=3.5MiB        ← 基本没压下来
    stream silent 15000ms — holding client with SSE keep-alive comments past its undici body timeout (#1647)
    client disconnected mid-stream after 22006ms
    

    补上身份头之后,同一个入口立刻正常(request: 1104 msgs, inbound=4.4MiB, outbound=1001.3KiB,[acp-usage] 缓存命中恢复正常,bili.log 里不再出现新的 pfa-)。

    对我们两边的意义:

    不需要你为我对 master rebase,等 CI 绿 + owner 合并即可;合并后我把第 2 项退役,只留第 3 项。

  16. ranxianglei commented on Nov 9, 2026

    @ranxianglei
    Owner

    🤖 Powered by ework · qwen3.8-27b

    收到,三件事都有下落了:

    1. 新形状(进了 ACP、没身份 → pfa-* 匿名链)已确认并修好。 你说的判据与代码完全吻合:请求不带任何会话身份时走前缀亲和新建 pfa-*,窗口只能来自静态阶梯;而那条 preflight made no progress but the payload fits; forwarding as-is 正是这种链「静默烧掉一轮」的落点——之后上游 400 / 掉线,日志里没有任何一行点名这个类别。建议 (a) 今天已经作为独立提交 cee765f 进 PR #5910 分支并推送:该 warn 在匿名请求时追加一段说明(无身份 → 窗口来自静态阶梯而非宿主上报、可能配错模型、若本轮上游 400/卡住请把身份头带上让轮次归回真实会话),已识别会话的原文一字不动;纯日志面变更,不碰控制流/线上格式。新测试文件 tests/issue5907-anon-preflight-warn.test.ts 双向钉死(#3617):匿名=false 原文不变、匿名=true 带新说明,且两个用例都断言该分支仍然转发而非 fail-fast。合并后你直接 grep unattributed chain 就能分流,不用再靠外部探针对齐时间线。

    2. 建议 (b) 其实已经成立。 窗口阶梯对每个带 body model 的请求都跑(匿名链也包括):插件头 → 运行时信息 → 路由/模型级配置 → 注册表 → CONTEXT_LIMIT_TABLE 按模型名查表 → 注册表抓取 → 缺省。查表那一档用的就是 body 里的模型名——所以 #5910 的冒号修复合入后,liuyue/cn:deepseek-v4.1-flash 会命中 ^deepseek → 1M,不再掉到 2e5 缺省(最初的 167232 是「冒号形状漏表 + 其余各档都没解析出」双重 miss)。至于你新日志里的 900000:每个模型的解析结果会打一条一次性 [window] 行并记录来源档(source=),grep 那个会话首轮附近的 [window] 就能看到是哪一档给的数;如果合并后仍不对,把那几行发我。

    3. guard 仓库地址已记录(Lumid0uce/dsh-acp-window-guard,MIT)——那是你的项目,我这边不需要也不应该动它。退役顺序建议:合并 + 升级后第 2 项(字符串补丁)可以立即退休(两处闸门都已在上游修好,双函数名兜底不再需要);第 1 项(modelContextLimit: 1e6)建议留作双保险,直到你用 [window] 行确认过该路由上跑的所有模型都能正确解析窗口再撤;第 3 项(fetch 改写 + 身份头)继续保留——它修的是宿主侧漏轮,属于你自己已立项的那条线,bili 管不到。

    当前状态(刚核过):PR #5910 仍 open/unmerged,head cee765f(含今日这个 facet-3 提交共 6 个提交),CI 检查点 0 failed / 9 pending(运行中,已完成项全绿);base 848f59b。按你说的不 rebase;合并前 master 若大幅漂移我再处理。合并权在 owner,你这边等绿即可。标签:needs-decision 已撤(你自行向 xiazhi88/dshgo#6、liguobao/ds-harness-remote#348 立项,挂起的那个决策已自然消解)→ 现为 bug, dsh, p1, scope/wiring。

  17. added 3 commits that reference this issue on Nov 9, 2026
    8626aeb
    d271b14
    1e53ad3
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions