Repository navigation
Replies: 1 comment
补充复现:膨胀只发生在 tool_result 里的图片,换协议或换模型都正常环境:DSH Desktop( 直接请求网关,同一张 1440×960 PNG(106,973 B)
所以这次的触发条件比「中转站按 base64 计价」更窄:只有网关把 Anthropic 格式转成 OpenAI 格式、并且图片在 会话日志里的后果
恢复方法(结合 #9888)把 GPT 改到 |
0 replies
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
中转站把 base64 当文本计价时,图片成本比计量器高约 3,500 倍,且 pi-ai 的静默溢出判定会丢弃已完成回答
Summary
在
api: anthropic-messages的中转路由上,图片被上游按 base64 文本计价(实测 ≈1 provider token / 原始图片字节),而dsh-token-meter把每个 image 块按附件引用 JSON 的长度计价(55–59 token/张)。两者相差约 3,500 倍。后果是:图片累积到"像素本身吃掉 95% 上下文额度"时,harness 侧的仪表仍显示只用了 6 万 token,压缩永远选不中图片,最后撞上上游硬 400:另外在同一份日志里发现第二个独立缺陷:
mapStopReason的静默溢出判定会把已经完成的回答整段丢弃并判CONTEXT_WINDOW_EXCEEDED(与 #9230 同类,但发生在llm-pi-ai+anthropic-messages路径上)。Environment
0.2.1-rc.3(安装包44.7.0),Windows x64llm-pi-ai自声明路由blow,api: anthropic-messages,baseURL: https://boclaw.cn,模型deepseek-v4.1-flashid/name/input: [text, image],未声明contextWindow、未声明任何图片字节预算(resolveProfiles因此取defaultContextWindow = 262144)$DSH_HOME/sessions/*/session.v4.jsonl.zstd),2769 个成功步骤,4 份会话复现同一个 400复现
会话里连续
read_image几张全分辨率截图即可。最小复现是一份只跑了 19 步的会话:两次read_image读入 1920×1080、751,223 B + 758,081 B 两张 PNG,inputTokens才涨到 102,276 就 400 死掉——因为这两张图的真实成本约 1,373,466 token,已经超过上游的 1,048,568。主要会话(
session-66e87b4e,38 个 assistant 步)的触发点是确定的:assistant/messageinputTokens = 735,304(其中 4 张图 724,814 B ≈ 659,580)tool/call read_image {"file_path":"p0026_root.png"}tool/result,+375,255 Bassistant/attemptfinish400 ... Input exceeds the context limit (1048568 tokens),code: CONTEXT_WINDOW_EXCEEDEDturn/endkind: error预测请求 ≈ 1,076,786 vs 上限 1,048,568(超出 ≈28,218)。
证据 1:该路由按原始图片字节计费,≈1 token/字节
用同一路由下"图片到达、期间无压缩"的相邻两步做差分(8 个独立样本):
即上游是把 base64 当普通文本分词(≈1.4 字符/token),而不是按视觉 token 计价。作为对照,同样 5 张图按 Anthropic 公开公式
w*h/750只应收 6,860 token——该路由收了 1,001,062,146 倍。主会话 surface 上 5 张图共 1,100,069 B ⇒ ≈ 1,001,062 token = 上限的 95.5%。像素之外留给文本/工具/系统提示的只有约 4.5%。
证据 2:
dsh-token-meter对图片的计价与之相差约 3,500 倍estimateStructuralBlock():主会话这 5 张图的实际计价:
合计 284 token,真实 ≈ 1,001,062。
priceSurface(nodes, pricing, fileText)只在pricing !== undefined时重定价;而TokenMeter._routeImagePricing(header)取ctx.get("llm")?.imageRequestPricing(provider, model)。全仓库只有dsh-llm-deepseek与dsh-llm接口声明了它,dsh-llm-pi-ai没有实现(只实现imageRequestTarget)。所以在这条路由上图片永远按上面的引用长度计价——并且这个错误价同时进入三处决策:compactIfNeeded(trigger="pressure")的阈值比较;selectCompactableRange的保留尾部与范围选择;summarizeCompaction的收缩闸门framedSummaryTokenCount >= prepared.shadowedRouteTokenCount。本会话记录的仪表读数:
request/context.contextWindow = 262144、contextPressure.surfaceTokens = 60928、contextBreakdown三项和 68,293。证据 3:
mapStopReason丢弃已完成的回答同一份安装的另一个会话(
session-077662a9,同一路由)有两次这样的assistant/attempt:{"kind":"error","failure":{"message":"pi-ai detected context overflow for model \"deepseek-v4.1-flash\"","code":"CONTEXT_WINDOW_EXCEEDED"}}但它的
replayState明确写着上游是正常结束的,且回答内容已经生成:(两个样本:seq 648 带完整 reasoning 块 + usage
input=1007885, cacheRead=1007616, output=21515;seq 700 带完整 text 块 + usageinput=1026422, cacheRead=1026176, output=1265。第二个样本的正文是一份完整的四组识图实测结论表。)mapStopReason(message, contextWindow)的静默溢出分支:于是:上游已经收下这次请求(stopReason=stop)并生成了完整回答,harness 却因为自己声明的窗口比上游小(262,144 vs 上游实际接受的水平)把这整段回答判成溢出并丢弃,转而去压缩。这个判定还在压缩请求上复用,因此"已完成的摘要"也可能被同一个本地检查丢掉(同 #9230 的机制)。
补充一点可疑之处:这里
input + cacheRead是 2,015,501,而上游自己的上限是 1,048,568,并且上游明确接受了这个请求。也就是说input + cacheRead并不是上游真正设限的那个量(在 9 份日志 / 2,769 个成功步骤里,被接受的最大inputTokens是 1,048,084,而input + cacheRead曾达到 2,094,996 并被正常接受)。用input + cacheRead去比窗口,会在这类路由上系统性地误判。证据 4:唯一能移除图片的机制在这类路由上不可能触发
toPiContextWithImages只在累计 base64 载荷超过maxRequestImageBytes(默认 20 MiB)时抛出:主会话峰值 base64 载荷约 1.47 MB,远低于 20 MiB。结果是 9 份会话日志里
image/offload事件总数为 0。也就是说:字节预算这道闸是按"字节"设的,而真正被烧掉的是"token"——在一个按字节计 token 的路由上,19 步就能烧掉一个满额上下文,但字节数离任何闸门都差一个数量级。这道闸正是本该救回本会话的那一道。(相关:#6521 提出按数量而非字节设预算,方向一致。)证据 5:压缩确实够不到像素
对该会话 24 条
compaction/summary/compaction/prune记录的shadowedSeqs逐一核对:seq 195的图片一直活到seq 463触发致命 400。summary is not smaller than the shadowed content (1824 estimated framed tokens >= 1768)等——闸门两侧用的都是上面那个瞎眼的计量器。pi-ai detected context overflow for model "deepseek-v4.1-flash"失败——按 262,144 判的。compaction/prune各只删掉一个纯文本节点(12,491 / 3,171 / 8,100 / 3,890 / 9,130 / 5,296 / 3,507 / 2,065 / 3,470 token)。按其文档,pruner 只裁文本,"images and structured blocks keep their order"。原因是结构性的:
selectCompactableRange(session, measurement, retainTokens)从尾部往前走,把最后若干节点整段保留;而 checkpoint 消息被插到 surface 头部(validateSurfaceRegion用nodes.slice(startIdx, endIdx+1)就地替换,替换后 surface 的第一个节点就是 checkpoint,后续indexOf因重复而落到它上面)。掌握压缩顺序的人可能是对的,但结果是:图片永远落在保留尾部的"后面",永远进不了 shadowed 范围。这与 #9635 / #7212 描述的"压缩选中自己的 checkpoint / 永不收敛"是同一族现象,但从"图片"这个入口看,它意味着在这类路由上压缩永远无法降低真实上下文占用。次要发现:
defaultContextWindow262,144 与上游实际 1,048,568 相差 4 倍(与 #7213 同一问题,这里补一条"按路由声明即可修复、且无需重启"的实测。)
该路由未声明窗口 ⇒
request/context全程contextWindow: 262144,而 400 里的真实上限是 1,048,568。这直接决定了上面第 5 条里那 10 次失败的判定基准。在 profile 里给该路由加
defaultContextWindow: 1048576后,正在运行的会话在下一次请求就切换了(request/context从262144变为1048576,无需重启):需要说明:这只是把仪表修正到真实值,并没有修掉根因。 像素仍然撤不掉、压缩仍然选不中图片、计量器仍然只看到 57 token/张。它能减少"按错误窗口反复触发/误判"的无效压缩,但不能阻止那个 400。
Suggested direction
按"该在哪一层做决定就在哪一层修"的顺序:
llm-pi-ai实现imageRequestPricing;或者更通用地,让TokenMeter在路由未声明图片定价时,退化为按传输字节估价(例如 base64 长度 / 4),而不是按附件引用 JSON 的长度。当前的三处决策(压力阈值、范围选择、收缩闸门)都建立在"图片几乎不要钱"这个假设上,而api默认投影并不能覆盖"上游把 base64 当 prompt 文本"的中转形态。maxRequestImageBytes默认 20 MiB,在按字节计 token 的路由上等于永不触发(本安装 9 份会话、0 次image/offload)。可考虑按"估算 provider token"设预算,或提供maxRequestImages。mapStopReason不要丢弃已完成的回答(同 [Bug] Completed Codex responses are misclassified as context overflow above the catalog window (tested patch) #9230 的修法):stopReason === "stop"且content.length > 0时应以内容为准,把超窗 usage 作为"下一轮该压缩"的信号,而不是把回答变成CONTEXT_WINDOW_EXCEEDED。当前行为在最坏情况下把用户拿到的唯一一份答案丢掉。input + cacheRead比窗口:本安装里被上游接受的最大inputTokens是 1,048,084,而input + cacheRead到过 2,094,996 仍被接受;用它比窗口会在这类路由上系统性误判。resolveCompactSpec/ 路由默认窗口:无声明时按 262,144 兜底,而这类自定义中转通常另有真实上限(本例 1,048,568)。窗口属于"提供方事实",不该由适配器猜;至少应在无法确知时给出可见告警,而不是静默采用一个 4 倍偏小的值。([Bug] llm-pi-ai silently resolves an unconfigured route to a 262144 context window while the provider declares 1048576 #7213 / [llm-pi-ai] renamed custom provider routes silently lose installed-catalog metadata (contextWindow/maxTokens) — catalog matched by provider name only #4409 已讨论。)Honest caveats
1,048,568是上游 400 报文里给出的数字,不是文档承诺;"≈1 token / 原始字节"是从 8 个样本差分测出来的,不是规范。anthropic-messages路由都这样计价;本条结论限定在"按 base64 文本计价的中转"这一类,这也是它值得单列的原因——api: anthropic-messages这个声明本身给出不了任何关于计价的保证。1048576)是本安装的配置层规避,不是本报告主张的修复。All reactions