You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Counts are DISJOINT: inputTokens is uncached input only; cached input is reported separately as cacheReadTokens/cacheWriteTokens (billed input = sum of the three). Adapters whose providers fold cache hits into a total prompt count subtract them out.
reacted with thumbs up emoji reacted with thumbs down emoji reacted with laugh emoji reacted with hooray emoji reacted with confused emoji reacted with heart emoji reacted with rocket emoji reacted with eyes emoji
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
摘要
使用
dsh-llm-pi-ai的api: anthropic-messages连接第三方 Anthropic 兼容网关时,该网关返回的input_tokens已经包含cache_read_input_tokens(非标准行为;Anthropic 的约定是只报未缓存部分)。这个折叠在 DSH 中造成 4 个后果,其中第一个会直接杀掉整个 turn 并丢弃已经完整生成的模型回答。后果 1、2、3 同源,修一处即可全消;后果 4 是独立的既有缺陷,只是被这里放大暴露。
环境:DSH
0.2.1-rc.2,@deepseek-ai/dsh-llm-pi-ai,api: anthropic-messages。后果 1(最严重):误判上下文溢出,整轮失败并丢弃完整回答
机制
packages/llm/llm-pi-ai/src/stream.ts:81把 pi-ai 的原始 usage 交给上游的静默溢出检测:isContextOverflow的 case 2 按"互不重叠"假设求和:折叠后这个和接近真实 prompt 的 2 倍。真实 60 万的 prompt 被按 120 万判超 100 万窗口,
mapStopReason返回CONTEXT_WINDOW_EXCEEDED,agent loop 结束本轮。实测证据(本地会话日志)
inputcacheRead0114fc3e2b4d209ecd3a3292前两例的
input与cacheRead比值都是 99.9x%,正是本文开头复现的折叠特征,而且这两例的totalTokens字段也正好等于input + output + cacheRead(1,509,709 与 1,200,391),可直接验证 pi-ai 的求和口径。真实 prompt 分别为 75 万和 60 万,都低于网关实际接受的 1,048,568——这些请求本来能成功。
被丢弃的回答
判定发生时模型已经正常生成完毕(
stopReason: "stop"),内容仍被整段丢弃:stopReason0114fc3estop0114fc3estop2b4d209estop2b4d209estop2b4d209estop用户侧看到的是「本轮运行失败 / CONTEXT_WINDOW_EXCEEDED」,且重试无法脱困——下一轮请求会被同样判超。这会形成稳定的死锁,而不是偶发失败。
另外,
stopReason: "stop"且内容非空时判为溢出本身即自相矛盾:真正溢出的请求不会收到完整回答。这一条可以独立作为兜底。后果 2:缓存命中率显示约 50%,实际约 99.8%
packages/client/ui-chat/src/client/chat/StatsPills.tsx:118把三个桶相加作分母:而
uncachedInputTokens直接来自usage.inputTokens(packages/llm/token-meter/src/usage-projection.ts:22),折叠时它并不减少。复现(向网关发送三次完全相同的请求,大且稳定的 system prompt):
缓存命中时
input_tokens不减少,说明它是总量而非未命中量。DSH 按2432 / (2433 + 2432) ≈ 50%显示,真实值2432 / 2433 ≈ 99.96%。长会话逐步数据完全一致:
后果 3:上下文占用接近翻倍,压缩阈值被提前触发
packages/llm/token-meter/src/usage-projection.ts:78与packages/llm/token-meter/src/index.ts:71同样按互不重叠求和:前者驱动 ContextMeter 占用显示与
projectedTokens,后者是 token-meter 的用量锚点(baseline)。折叠时两者都接近翻倍。compaction-basic用measurement.totalTokens < spec.thresholdTokens判断是否压缩(packages/compaction/compaction-basic/src/index.ts:319、:327、:340),所以压缩会在真实占用约一半时触发,白白截断仍然可用的上下文。实测(
contextWindow = 1,000,000):inputTokenscacheReadTokenspressureTokens注意后果 1 与后果 3 方向相反:与
contextWindow直接比较的一侧会误判溢出,与阈值比较的一侧会提前压缩。两者必须一起修,否则只修一边会让另一边继续保持错误。后果 4:真溢出时恢复路径本身不可能成功(既有缺陷,被此折叠放大)
即使溢出是真的,
compaction-basic的恢复也救不回来。packages/compaction/compaction-basic/src/index.ts:190的agent/request-error收到CONTEXT_WINDOW_EXCEEDED后调用compactIfNeeded(agent, 'context-overflow', …)。摘要请求由buildSummarizationInput()(packages/compaction/compaction-basic/src/region.ts:544)构造:system + tools + 被压缩区域的完整消息,设计上刻意复用 KV cache(packages/compaction/compaction-basic/src/summarizer.ts:28-30)。当被压缩区域接近整段历史(正是溢出的情形),这个辅助请求与原请求几乎等大,于是摘要本身再次溢出,
compaction/end记录的错误与turn/end完全相同:maxOverflowRetries默认 1(packages/compaction/compaction-basic/src/config.ts:106),用完即next()并保留原始错误。而summarizationProvider/summarizationModel默认回落到被路由模型本身——也就是刚刚溢出的同一个窗口。本地会话里连续多个 turn 都停在这一步:
2b4d209e的 turn 3 / 4 / 5 各自产生一次compaction/start+ 同错compaction/end,期间没有任何一次成功的 pre-step 压缩。根因归属
packages/llm/llm/src/types.ts:170-173已声明该契约:两点澄清:
llm-deepseek并没有做减法,也不需要——它走 Anthropic 风格的input_tokens,本就只报未缓存部分(packages/llm/llm-deepseek/src/translate.ts:36直接映射)。该文档句子里提到的prompt_tokens折叠属于openai-completions协议,pi-ai 自己的该实现确实做了减法(dist/api/openai-completions.js:1177:Math.max(0, promptTokens - cacheReadTokens - cacheWriteTokens))。anthropic-messages不能假定折叠,因为标准 Anthropic 不折叠;折叠只出现在部分兼容网关上。因此归一化应发生在适配器摄入点(
mapUsage),而不是各个消费方分别求和;且必须是 opt-in 的兼容开关,不能改变标准网关的行为。建议修复
A. pi-ai 路由在摄入时归一化 usage(修后果 1、2、3)
在
packages/llm/llm-pi-ai/src/stream.ts:24的mapUsage()增加兼容参数:调用点两处(
stream.ts:209、:219)透传开关。开关位置:不能放进
PiAiCompatProfile——那个接口由EveryProfileFieldIsOffered/EveryProfileFieldMatchesUpstream编译期钉死在 pi-ai 上游的 compat 类型上,加非上游字段无法编译。应作为普通的配置字段加入:packages/llm/llm-pi-ai/src/config.ts的 route profile(紧邻defaultContextWindow),schemastery 声明inputTokensIncludeCacheRead: z.boolean()PiAiModelProfile(catalog.ts:603)+config.ts的modelFields,覆盖路由默认configuredMaxTokens的既有做法——在catalog.ts的resolveEntry旁记入一个Map<string, boolean>,由adapter.ts的modelInfo()/generate()读取后传给toStreamChunks默认关闭,标准 Anthropic 端点与
llm-deepseek完全不受影响。B. 不要用未归一化的 usage 判溢出(同源,需一并修)
仅靠 A 还不够:
mapStopReason目前把 pi-ai 的原始Usage交给isContextOverflow。归一化后仍应让 DSH 自己掌握这条判定,而不是依赖 pi-ai 的usage.input + usage.cacheRead。建议改
toStreamChunks的签名(packages/llm/llm-pi-ai/src/stream.ts:142,调用点adapter.ts:426),把contextWindow换成{ contextWindow, inputIncludesCacheRead },然后:isContextOverflow(message, undefined)—— 显式 provider 错误仍然可信;stopReason === 'stop'且content.length > 0时永不判为溢出。C. 摘要请求加大小上限(修后果 4,独立改动)
buildSummarizationInput()应受一个按 token 计价的预算约束(可复用tokenMeter的 node 价格)。超出时改为分段摘要,或退回更小的区域。同时,summarizationProvider/summarizationModel默认不该回落到刚刚溢出的同一路由;若确实回落,至少应在日志中明确提示「摘要使用了与溢出相同的窗口」。D. 文档修正
packages/llm/llm/src/types.ts:173提到 "DeepSeek'sprompt_tokens",但llm-deepseek走 Anthropic 协议、从不接收prompt_tokens。这句应改为指名openai-completions协议(pi-ai 实现),否则读者会以为减法已经发生在错误的包里。影响面
input_tokens计费,DSH 只是读数与阈值错了。compaction-basic的触发时机,以及后果 1 的整轮失败与回答丢弃。llm-deepseek与标准 Anthropic 端点;受影响的只有把缓存读折叠进input_tokens的兼容网关。附:复现用最小配置
判定网关是否折叠:连发两次相同请求,若
input_tokens不随命中下降、且input ≈ cacheRead,即属于本 issue。All reactions