🎁 福利专区全网大模型免费应用 + 新用户福利 + 注册活动入口,低成本玩转 AI

缓存保温器 JanEickholt/dsh-warm

DeepSeek Harness
JanEickholt10 天前有提交已提供中文文档扫描:含敏感能力

工具执行期间保持提示词缓存温热,省下重预填充费用

29.9/100
综合分
0
GitHub 星标
—
npm 周下载
—/5
用户评分

数据截至 2026/10/23 · 周下载取自 npm 全网(含 CI / 镜像),非本站安装数;综合分由星标、活跃度与兼容性检查合成。

安装命令 · 需先装 dsh CLI 引擎
dsh plugin --profile web add JanEickholt/dsh-warm
该包不可直接 npm 安装,改从 GitHub 源安装(明细见下方安装检查)

状态速览

判定依据:实装验证 · 静态安装检查 · 自动扫描 · 仓库维护状态
信任档位:仅索引
是什么
dsh 原生插件 · model
装得上吗
本站尚未做安装检查
安全吗
本站尚未对该插件做风险分级
暂未覆盖,不等同于无风险
还在维护吗
活跃:最近一次提交在 10 天前
档位由哪些信号合成

本站实装验证(真实安装 L4 覆盖全站;L5 为运行时冒烟或实机截图,覆盖范围小于 L4) · 验证所用 dsh 版本 · 静态安装检查 · 风险分级 · 仓库维护状态。 下方各区块是它的证据明细。

验证判据与等级说明 →
社区实装还没有人打卡 · 装过的第一个就是你
安全扫描自动 · 静态分析源码
v2.7.0
🔵 扫描:含敏感能力尚无人工评估
展开查看扫描说明与证据 ↓

静态扫描发现插件具备触达本地系统的能力:执行外部命令、读写/删除本地文件、读取剪贴板或密钥类环境变量(至少其一)。这是能力清单,不是危险判定——具体能力项与证据见下方,安装前请自行判断是否需要。

读写/删除本地文件发起外部网络请求
查看扫描证据(共 31 处,展示前 27 条)
  • 读取本地文件
    lib/index.js:92
    size = statSync(path).size
  • 写入本地文件
    lib/index.js:102
    renameSync(path, `${path}.1`)
  • 写入本地文件
    lib/index.js:108
    appendFileSync(path, line)
  • 写入本地文件
    lib/index.js:123
    mkdirSync(storageDir, { recursive: true })
  • 发起网络请求
    client/index.js:46
    const res = await fetch(STATS_URL)
  • 读取本地文件
    lib/config.js:171
    if (existsSync(file)) {
  • 读取本地文件
    lib/config.js:173
    raw = JSON.parse(readFileSync(file, 'utf8'))
  • 写入本地文件
    lib/config.js:180
    mkdirSync(storageDir, { recursive: true })
  • 写入本地文件
    lib/config.js:181
    writeFileSync(file, JSON.stringify(DEFAULT_CONFIG, null, 2) + '\n')
  • 读取本地文件
    lib/journal.js:62
    return statSync(path).size
  • 读取本地文件
    lib/journal.js:72
    names = readdirSync(storageDir)
  • 读取本地文件
    lib/journal.js:83
    text = readFileSync(join(storageDir, name), 'utf8')
  • 读取本地文件
    lib/journal.js:103
    if (!existsSync(first)) return first
  • 读取本地文件
    lib/journal.js:106
    if (!existsSync(next)) return next
  • 读取本地文件
    lib/journal.js:127
    return fstatSync(handle).ino !== statSync(path).ino
  • 写入本地文件
    lib/journal.js:137
    renameSync(path, rollTarget())
  • 写入本地文件
    lib/journal.js:180
    appendFileSync(path, line)
  • 读取本地文件
    lib/stats.js:222
    if (!existsSync(statsPath)) return null
  • 读取本地文件
    lib/stats.js:224
    const parsed = JSON.parse(readFileSync(statsPath, 'utf8'))
  • 读取本地文件
    lib/stats.js:259
    const age = Date.now() - statSync(lockPath).mtimeMs
  • 读取本地文件
    lib/stats.js:261
    const pid = Number.parseInt(readFileSync(lockPath, 'utf8'), 10)
  • 删除本地文件
    lib/stats.js:286
    unlinkSync(lockPath)
  • 删除本地文件
    lib/stats.js:306
    unlinkSync(lockPath)
  • 写入本地文件
    lib/stats.js:321
    writeFileSync(tmpPath, `${JSON.stringify(merged)}\n`)
  • 写入本地文件
    lib/stats.js:322
    renameSync(tmpPath, statsPath)
  • 读取本地文件
    lib/stats.js:419
    names = readdirSync(storageDir)
  • 读取本地文件
    lib/stats.js:427
    text = readFileSync(join(storageDir, name), 'utf8')
扫描于 2026/10/31(1 天前) · 范围 入口闭包 + 发布文件清单 · 来源 GitHub 仓库

静态扫描只反映源码中出现的调用,不构成安全背书;人工评估与自动扫描相互独立、互不覆盖。

给这个插件打分还没有人投票↓
用户评分
还没有人投票,来当第一个

DeepSeek Harness 的提示词缓存预热器:在长时间工具执行期间保持会话服务商缓存前缀处于预热状态。宿主插件,按服务商划分的 TTL 网格,日志加 UTC 日预算。

README

由 DeepSeek 最新模型翻译生成
dsh-warm DeepSeek Harness 的提示词缓存保温器,一个宿主插件。它让会话的服务商提示词缓存前缀保持温热,时机仅限会话的智能体循环阻塞在工具执行内部时(长时间 bash、job_output、等待队友的 wait_agent,以及类似的等待)。 它会在服务商缓存即将过期之前,通过宿主 llm/stream 瀑布重新派发会话的最后一次对话请求,带上 maxTokens: 1(在不知道 ttlMs 时按固定的 intervalMs 网格),然后丢弃这个 1 token 的响应。工具结果落在仍然温热的前缀上,于是下一次真实请求按缓存命中计费,而不必以未命中价格重新预填充约 90k token,也不用再付一次完整的 TTFT。 为什么只保温工具阻塞 在本环境实测(2026-10-17,对 api.mistral.ai、zai-glm-5-3 的实时探测;50 天账本,350 个会话): - TTL 因服务商而异。 Mistral 在这里是 5 到 7 分钟(302 s 命中,424 s 未命中,且命中会刷新 TTL),所以间隔表按服务商分别设置。 - 工具等待和队友等待一定会恢复,因为工具一结束循环就继续。50 天下来净赚 +$7.15,并挽回约 1 小时 TTFT,尾部风险为零。 - 空闲会话大多一去不回。
展开完整文档(README 剩余 1 节)
dsh-warm

DeepSeek Harness 的提示词缓存保温器,一个宿主插件。它让会话的服务商提示词缓存前缀保持温热,时机仅限会话的智能体循环阻塞在工具执行内部时(长时间 bash、job_output、等待队友的 wait_agent,以及类似的等待)。

它会在服务商缓存即将过期之前,通过宿主 llm/stream 瀑布重新派发会话的最后一次对话请求,带上 maxTokens: 1(在不知道 ttlMs 时按固定的 intervalMs 网格),然后丢弃这个 1 token 的响应。工具结果落在仍然温热的前缀上,于是下一次真实请求按缓存命中计费,而不必以未命中价格重新预填充约 90k token,也不用再付一次完整的 TTFT。

为什么只保温工具阻塞

在本环境实测(2026-10-17,对 api.mistral.ai、zai-glm-5-3 的实时探测;50 天账本,350 个会话):

- TTL 因服务商而异。 Mistral 在这里是 5 到 7 分钟(302 s 命中,424 s 未命中,且命中会刷新 TTL),所以间隔表按服务商分别设置。
- 工具等待和队友等待一定会恢复,因为工具一结束循环就继续。50 天下来净赚 +$7.15,并挽回约 1 小时 TTFT,尾部风险为零。
- 空闲会话大多一去不回。 350 个会话中有 344 个从未恢复,照搬 OpenCode 式的空闲保温净亏 −$6.91 / 50 天。所以空闲会话和用户输入类工具(ask_user_question)在这里绝不保温。

该服务商的缓存命中按九折优惠计费(未命中 $1.40/M,命中 $0.14/M)。每 100k token 前缀每次 ping 约花 $0.014;当阻塞时长超过 TTL 时,能省下约 $0.126 的重新预填充,外加 25 到 40 s 的 TTFT。

重放如何保持字节一致

循环构建的请求到达 llm/stream 瀑布时已深度冻结,是会话日志的纯函数。保温器捕获这个确切对象,稍后发送一份新的浅拷贝,共享这些冻结字段,只改动:

- maxTokens: 1,一个采样字段,不属于缓存输入前缀;
- signal,把捕获调用的 signal 与 ping 超时链接起来(AbortSignal.any),这样用户中断也会中止进行中的 ping。

服务真实调用的同一个适配器负责序列化 ping,因此输入前缀字节一致,服务商直接从缓存返回。这与 harness 自身用于压缩摘要的辅助调用模式相同,都复用缓存。ping 是手工构建的调用(循环的 markAgentLoopRequest WeakSet 身份不会带到拷贝上),并且走完整条瀑布,所以路由适配器的行为完全一致,dsh-cost-meter 会像对待其他调用一样为其用量计费。账本保持诚实。

安装

dsh plugin --profile web add dsh-warm # from npm
dsh plugin --profile web add github:JanEickholt/dsh-warm # from GitHub, no npm needed

或者作为本地插件(link 协议):
1. profiles/<profile>/package.json:在 dependencies 里加上 "dsh-warm": "link:/absolute/path/to/dsh-warm",在 dsh.profile.bundles 里加上 "dsh-warm"。
2. 在配置档目录里执行 pnpm install。
3. 从终端重启 harness(例如 systemctl --user restart <your-dsh-unit>)。重启会杀掉宿主进程;千万不要在会话内部触发重启。

插件的 dsh.bundle.patch(cordis.patch.yml)会自动插入 loader 行,不用改 cordis.patch.yml。

配置

$DSH_HOME/storages/dsh-warm/config.json(首次加载时按默认值写入;改完再重启)。预热规则是按服务商定的,因为每个服务商的提示词缓存 TTL 是各自的事实:有了实测或文档给出的 ttlMs,调度就以过期时刻为锚点——锚定调度在 302 s − 60 s = 每 242 s 触发一次,也就是最后一个安全时刻减去一分钟缓冲。设了 ttlMs 之后,intervalMs 不再驱动调度——但别删掉它:一旦 ttlMs 被去掉,它仍是兜底的固定间隔;否则它就是统计 TTL 的替代值:

{
"enabled": true,
"dryRun": false,
"maxPings": 5,
"providers": {
"mistral": { "intervalMs": 240000, "ttlMs": 302000, "marginMs": 60000 },
"openai": { "intervalMs": 240000, "ttlMs": 300000, "marginMs": 60000 },
"anthropic": { "intervalMs": 240000, "ttlMs": 300000, "marginMs": 60000 }
},
"excludeTools": ["ask_user_question"],
"minPrefixChars": 12000,
"maxConcurrentPings": 2,
"dailyPingBudget": 300,
"pingTimeoutMs": 90000,
"stats": { "enabled": true }
}

| 字段 | 默认值 | 含义 |
| enabled | true | 总开关 |
| dryRun | false | 只记录本应触发的 ping,不实际派发。首次部署时用来观察。 |
| providers | 见上 | 出现在表里 = 预热该服务商;每项可设 intervalMs、ttlMs(以过期时刻为锚点调度)、marginMs 和 maxPings。已知服务商留成 {} 会采用内置规则(控制台告警);只写 ttlMs 的项会保留,intervalMs 默认 240000(只有兜底固定间隔会读它);未知名字且两个字段都没写,告警并丢弃 |
| maxPings | 5 | 服务商项没覆盖时,每个工具块的 ping 上限(刷新 maxPings 次后前缀反正也过期了) |
| excludeTools | ["ask_user_question"] | 类似用户空闲的等待,恢复时机不确定 |
| minPrefixChars | 12000 | 跳过太短的前缀(≈字符数/4 个 token);为它们防未命中不值得发一次 ping |
| maxConcurrentPings | 2 | 全局在途上限;超限的 ping 会在 5 s 后重试,不占名额 |
| dailyPingBudget | 300 | 每个 UTC 日的硬性上限,按实际派发尝试计数(预期需求 ≈30/天) |
| pingTimeoutMs | 90000 | 中止卡住的 ping 流 |
| providers..ttlMs | 按已知服务商采用 | 服务商的缓存 TTL 下限;设置后,ping 以过期时间为锚点,落在最后一次刷新缓存的事件之后 ttlMs − marginMs 处。它同时仍是 stats 分区的阈值。当 ttlMs <= intervalMs 时告警(回退网格永远无法覆盖 TTL) |
| providers..marginMs | 60000 | 从 ttlMs 中减去的安全缓冲:锚定 ping 会在缓存失效前 marginMs 触发。必须 < ttlMs —— 违反此条件的组合会丢弃 ttlMs(控制台告警),服务商回退到 intervalMs 网格。ttlMs 缺失时忽略。 |
| stats.enabled | true | stats 计数 + stats.json + /api/dsh-warm/stats 路由的快照 |
| debug | false | 把每个观察到的事件追加到 storages/dsh-warm/debug.jsonl(诊断辅助;正常运行时关闭) |

为什么是这三个服务商

间隔默认值绑定各服务商自己的缓存行为。添加更多之前先测量:

- mistral 在本环境实测过(2026-10-17 实时探测,zai-glm-5-3):302 s 命中,424 s 未命中;锚定调度在 302 s − 60 s = 每 242 s ping 一次,即最后安全时刻减去一分钟缓冲。
- openai 文档称其缓存「通常在 5 分钟到 1 小时之间」;240 s 保持在短端之下。
- anthropic 文档称 TTL 为 5 分钟,使用时刷新;240 s 合适。
- deepseek 刻意缺席。它的自动磁盘缓存比任何合理的 ping 网格都活得久(报告从几分钟到几小时不等,且 DeepSeek 未公布 TTL),所以每 4 分钟 ping 一次纯属浪费。先在你的账号上实测出真实 TTL,再添加条目。
- opencode 缺席:免费模型位于 zen/v1,其下限已实测(见下文小节),但 zen 的免费层门禁会拦截 dsh-warm 自己的 ping;go 端点 —— opencode-go 服务商的接口面 —— 是另一个未测量的缓存。其他代理同理:先探测,再添加,例如 "opencode-go": { "intervalMs": 120000 }" —— {} 快捷写法只为上述三个服务商采用默认值;未知名称配空条目仍会丢弃(并告警)。

OpenCode 免费模型 —— 实测 zen 下限(2026-10-20)

通过真正的 opencode 客户端(v2.0.3)实时探测,因为 zen 的免费层门禁对原始 HTTP 客户端返回 403 FreeTierError "OpenCode's free tier can only be used from within OpenCode" —— dsh-warm 无法自己 ping 这些模型,所以在 DSH 经由 opencode 客户端或指纹准确的代理路由之前,这些数字只是文档。方法:每个模型用一个专用客户端会话,先灌入固定约 14k 字符的前缀(约 10k token);重发时追加一条极短的后续消息;命中 = 重发的 tokens.cache.read ≈ 前缀,碎片 = 一个恒定的小残留(123/128/191 token),未命中 = 0。

| 模型(zen/v1 免费) | 实测下限 | 保守 ttlMs |
| glm-5.3-flash-free | (270, 300] s —— 300 时未命中 | 270000 |
| deepseek-v4.1-flash-free | (420, 600) s — 600 s 抖动 | 420000 |
| minimax-m3.1-flash-free | ≥ 420 s — 600 s 完整命中一次,碎片命中一次 | 420000 |
| big-pickle | (600, 900) s — 900 s 碎片命中 | 600000 |

命中会刷新缓存:每级阶梯的后续档位在累计年龄远超模型单次驻留下限时命中(最高 1,349 s),所以低于下限的间隔能让缓存无限期保持;marginMs 60000 照常生效。免费 id 会轮换(space-bunny-free 已在上游下线)——依赖某个 id 前先重新检查 GET /zen/v1/models。go 端点(付费模型加上它自己的免费 id)还没有实测 TTL;添加任何 opencode-go 条目之前先探测它。

表格之前的配置(warmProviders + intervalMs)会自动迁移到按服务商的表格,并打印一条控制台警告。

统计

dsh-warm 自己记一本账,记录预热买到了什么、浪费了什么,不依赖任何其他插件。它出现在三个地方:stats.json 文件、一个有文档的 HTTP 路由,以及一个小型客户端 bundle。

计数器(按 UTC 日与全时段分别记录;failed 只记全时段):

| 计数器 | 含义 |
| warms | 发出的 ping 数 |
| needless | 浪费:后续请求在 t0 + TTL 之前到达,其中 t0 是打开该块的请求——仅靠真实请求留下的缓存仍能撑住,所以没有 ping 买到任何东西。按块判定,不按 ping 判定:在密集网格上,每个 ping 单独看都能被邻居接上,但整个块本来就是热的 |
| noFollowup | 浪费:工具块结束时完全没有后续请求(回合转入空闲,或会话被销毁) |
| expired | 浪费:后续请求需要热前缀,但到达时距最后一次成功 ping 已超过 TTL,缓存早已掉出 |
| failed | 自身请求出错或中止的 ping(单独计数,绝不计入浪费) |
| savedCacheTokens | 块内最后一次成功 ping 的缓存读取 token 数,前提是该块的后续请求确有必要(到达时已过 t0 + TTL:没有任何 ping 的话缓存本会掉出)且仍然命中(在该 ping 的 TTL 之内),也就是预热可验证买到的部分 |
| models. | 上述每个计数器的按模型切片,外加下面的 token 数字;扁平字段仍是跨模型总计 |
| pingCacheReadTokens | 按模型、按结果:ping 自身的缓存读取成本。held = 确有必要且链保持住了——买到计入的后续请求的成本;needless/noFollowup/expired = 什么也没买到的成本 |
| pingUncachedTokens | 按模型:ping 的未缓存输入 token——原价计费的基础(缓存未命中在这里付费) |
| failedPingTokens | 全时段:失败 ping 的观测用量,{ cacheRead, uncached }——扁平字段加上按模型的 models..failedPingTokens(schema 3);升级前的行仍以扁平字段为准。在混合了不同时期的文件里,扁平字段超出按模型之和的部分恰好是升级前的行;全时段总计按扁平字段计价,归因按模型计价 |
| sessions | 按会话划分的桶,结构与 allTime 完全一致(计数 + models,不含 day),以会话 id 为键;最多保留最近活跃的 50 个会话,每次写入时裁剪 —— 被裁剪的会话保留其 today/allTime/byDay 数字,只是自己的桶没了 |

结果语义 —— 计数统计的是 ping 次数,不是价格;expired 不等于 failed:

- 每个计数行统计的都是 ping 次数。三个浪费行按块判定 —— 一次 follow-up 会一次性归类该块所有非 failed 的 ping,所以一个丢了 cover 的块,可能给 expired 加上好几个 ping。
- expired 的 ping 作为请求是成功的。每个 ping 自己的账单单独计量,走 token 账本:它的缓存读 token 记在 models..pingCacheReadTokens.expired,未命中缓存的输入 —— 缓存命中后仍要付的尾部,或 ping 本身打在死缓存上时的整个前缀 —— 记在 models..pingUncachedTokens。expired: 5 不是五次全价重新预填充。
- follow-up 在死缓存上付的那次重新预填充,完全不在这个账本里;follow-up 是普通请求,和别的请求一样计费,不体现在这些字段中。
- failed 是 ping 自己的请求出错或中止(服务商超时、用户中断)—— 服务商机制问题,全时段统计,按 failedPingTokens 计价,永远不算浪费行。expired 是节奏信号:cover 在 follow-up 之前就失效了(达到上限、预算闸门,或某个 failed ping 打断了刷新链)。调 marginMs/maxPings/dailyPingBudget 看 expired,调服务商看 failed。
- warms 在派发时计数,所以包含 failed 的 ping 和仍未关闭的块的 ping;浪费行只在 follow-up(或 idle/disposal)关闭该块时才填。可验证带上了 follow-up 的 ping 没有单独计数器:全时段 warms − failed − needless − noFollowup − expired 统计的是它们,再加上仍未关闭或 teardown 时丢弃的 ping,所以消费方把这个残差标为「held or open」。

dsh-warm 只提供 token —— 它不持有价格。消费方自己给这套账定价:收益侧是 savedCacheTokens × 输入价格(毛值 —— follow-up 为这些 token 仍付了自己的命中价);成本侧是各 ping 的实测支出,按模型算:每个 pingCacheReadTokens 数字按缓存读价格,加上 pingUncachedTokens 按输入价格,还有 —— schema 3 —— 按模型的 failedPingTokens.cacheRead 按缓存读价格、failedPingTokens.uncached 按输入价格(全时段范围;升级前的行带的是扁平总计,对这些行扁平字段仍是权威)。

dsh-context 卡片的金额图例:Saved = savedCacheTokens × 未命中价格;Useful spend = pingCacheReadTokens.held × 命中价格;Wasted spend = 浪费桶的 pingCacheReadTokens + failed 的 failedPingTokens.cacheRead(按模型;旧版回退时用扁平值)× 命中价格;Full-price spend = pingUncachedTokens(+ 按模型的 failedPingTokens.uncached)× 未命中价格。
设计上就是近似:分类时拿请求/ ping 时间戳和配置的 ttlMs 比较,不看服务商侧的缓存状态。报了 miss 的 ping(cacheReadTokens 为 0)永远不计入。崩溃之后(没有干净 dispose),启动时会从 warm.jsonl 重建 warms/failed,但结果计数器保持为零,因为结果需要日志行之后发生的那次后续请求。token 数字里,按模型的 pingUncachedTokens 和 failedPingTokens 从日志记录的用量重建;只有按结果拆分的 pingCacheReadTokens 跟着计数器一起保持为零。流里从未露出的用量按零算(失败的 ping 也一样)。在按模型映射之前写下的 stats.json 原样启动:缺失的映射从空开始,历史的扁平计数原封不动带过来。按会话同理:桶只统计升级后的归因,全新启动会从日志的 sessionId 重建按会话的 warms,50 会话上限背后的最近时间戳是内部的——它从不出现在路由上。统计写到 storages/dsh-warm/stats.json(原子重命名,UTC 日分桶)。

诊断计数: 设 "debug": true 并重启 harness;之后每个观察到的 llm/stream、工具窗口、ping 和链分区都会追加到 storages/dsh-warm/debug.jsonl,能看清某个结果桶到底漏了哪个事件。每个 chain.close 都带着分区比较的两个数字——gap(后续请求减去该块的起始请求)和 ttl。用完关掉。

开关: "stats": { "enabled": true },默认开启。关掉意味着不计数、不写 stats.json,下面的路由返回 "statsEnabled": false, "stats": null,让消费方能渲染状态而不是靠猜。

GET /api/dsh-warm/stats 为第三方消费方提供稳定 schema(比如 dsh-context-insights 卡片;两个插件完全独立,这条路由是唯一的耦合点,dsh-warm 不在时它 fail open)。结构只在 schema 升版时变化:

{
"schema": 3,
"statsEnabled": true,
"stats": {
"today": {
"day": "2026-10-20", "warms": 3, "needless": 0, "noFollowup": 1, "expired": 0, "savedCacheTokens": 1152,
"models": {
"zai-glm-5-3": {
"warms": 3, "needless": 0, "noFollowup": 1, "expired": 0, "savedCacheTokens": 1152,
"pingCacheReadTokens": { "held": 180000, "needless": 0, "noFollowup": 90000, "expired": 0 },
"pingUncachedTokens": 1200,
"failedPingTokens": { "cacheRead": 0, "uncached": 0 }
}
}
},
"allTime": {
"warms": 12, "needless": 1, "noFollowup": 2, "expired": 1, "savedCacheTokens": 4608,
"models": {
"zai-glm-5-3": {
"warms": 12, "needless": 1, "noFollowup": 2, "expired": 1, "savedCacheTokens": 4608,
"pingCacheReadTokens": { "held": 720000, "needless": 90000, "noFollowup": 180000, "expired": 90000 },
"pingUncachedTokens": 130000,
"failedPingTokens": { "cacheRead": 24000, "uncached": 8000 }
}
}
},
"byDay": {
"2026-10-20": {
"warms": 3, "needless": 0, "noFollowup": 1, "expired": 0, "savedCacheTokens": 1152,
"models": {
"zai-glm-5-3": {
"warms": 3, "needless": 0, "noFollowup": 1, "expired": 0, "savedCacheTokens": 1152,
"pingCacheReadTokens": { "held": 180000, "needless": 0, "noFollowup": 90000, "expired": 0 },
"pingUncachedTokens": 1200,
"failedPingTokens": { "cacheRead": 0, "uncached": 0 }
}
}
}
},
"failed": 2,
"failedPingTokens": { "cacheRead": 24000, "uncached": 8000 },
"sessions": {
"34955360-85de-4149-9981-bc7f2904ab29": {
"warms": 3, "needless": 0, "noFollowup": 1, "expired": 0, "savedCacheTokens": 1152,
"models": {
"zai-glm-5-3": {
"warms": 3, "needless": 0, "noFollowup": 1, "expired": 0, "savedCacheTokens": 1152,
"pingCacheReadTokens": { "held": 180000, "needless": 0, "noFollowup": 90000, "expired": 0 },
"pingUncachedTokens": 1200,
"failedPingTokens": { "cacheRead": 0, "uncached": 0 }
}
}
}
}
},
"config": {
"providers": { "mistral": { "intervalMs": 240000, "ttlMs": 302000 } },
"budget": { "dailyPingBudget": 300, "maxPingsPerBlock": 5 },
"dryRun": false
}
}

在 config.providers 里,某个条目如果设置了 maxPings,就会把它作为第三个可选字段回显。

这条路由位于 harness 已鉴权的 /api 围栏之后(没有单独的鉴权;与 harness 自己的路由共用同一道围栏),只要插件启用就存在,统计关闭时也存在,所以开关状态始终可见。

客户端 bundle: 包内附带一个 web 客户端(dsh.client,纯 CJS 工厂,无需构建步骤)。侧边栏页脚的「Warm」一行点开后是详情面板,显示今日与累计计数、服务商网格和预算;面板打开期间会轮询。Plugins 页面上的 dsh-warm 卡片显示同样的数据。两个界面都只读这条路由;客户端不存任何东西。

可观测性

- storages/dsh-warm/warm.jsonl 每次 ping 一行:服务商用量(cacheReadTokens > 0 表示命中)、延迟、错误、dry-run 标记。文件到 5 MB 滚动,预算计数器从当天的行启动。
- storages/dsh-warm/stats.json 保存上面的统计计数器(见 Stats),原子写入,崩溃后仍可恢复。
- 宿主控制台:加载横幅显示解析后的配置,另有做了限流的失败告警。
- dsh-cost-meter 的账本记录每次 ping(通过 sessionId 归属到会话),所以经济账可以对着现实核对。

已知限制

- 服务商范围:只有 providers 表里有条目的服务商才会被预热,而且每个都需要实测或文档给出的 TTL 下限。deepseek 和 opencode-go 默认缺席,原因就在此(见配置一节)。
- 重新分发走配置档的主 llm 服务;只在隔离服务上注册的服务商适配器,序列化结果会不一样(目前这个配置档里没有这种适配器)。
- 302 s 的 TTL 下限是 Mistral 对 zai-glm-5-3 的实测值;其他服务商要加进 providers 表,得先各自做探测。
预览分享卡片长什么样 ↓JanEickholt/dsh-warm
分享 · DeepSeek Harness Hub
JanEickholt/dsh-warm

工具执行期间保持提示词缓存温热,省下重预填充费用

回到顶部