Skip to content

fix: report uncompressed baseline in host usage + clamp negative persisted token stats (#408) - #425

Merged
ranxianglei merged 1 commit into
masterfrom
2026-09-01_host-usage-backfill
Sep 6, 2026
Merged

ranxianglei merged 1 commit into
masterfrom
2026-09-01_host-usage-backfill

Conversation

@ranxianglei

Copy link
Copy Markdown
Owner

Fixes #408. Two defects, two fixes.

1. Host usage backfill (main defect — host context accounting collapses to ~0). When bili folds compressed blocks out of the forwarded view, the provider's usage reports the POST-fold prompt, and the host (omp) anchors its context accounting on that value — status line shows ~0% and the auto-compact safety nets sleep. The issue's proposed fix (calling omp's recordAnchoredHistoryRewrite) was withdrawn after line-by-line review: that API SUBTRACTS from the anchor (shake semantics — provider over-reports) and is not exposed on the extension surface; in the bili case the anchor is already post-fold-small, so calling it would make it smaller.

This PR instead backfills the usage bili returns to the host: each prepare* computes hostCreditTokens = est(originalMessages) − est(processedMessages) (same tokenizer both sides → systematic error cancels), and every client-facing usage path adds it back into the input-side field (openai prompt_tokens+total_tokens, anthropic/responses input_tokens): loop completions (synthesized), raw passthrough events (openai real-tool finish chunk, anthropic first-round message_start), plugin SSE/JSON pipes, and the non-stream JSON path. The internal ledger (lastInputTokens) stays post-fold (unchanged); a new in-memory hostContextTokens (last backfilled total) feeds the /acp panel tokenCount + contextTokens so panel == host footer. Trace: info log per armed request + /acp panel note line (host baseline: uncompressed (proxy backfilled +N tok)) — the degraded-host acceptance is covered without any host-side change: hosts without an add-back API simply receive the corrected usage directly.

2. Persist restore clamp + one-time migration. buildSession now wraps lastInputTokens/contextTokens in Math.max(0, …); loadAll/loadSync detect pre-clamp negative values on disk and rewrite the stale file once (log: one-time migration (#408): clamped … in N session(s) to 0). Old negative values (e.g. -54119) can no longer revive after upgrade; the /acp panel can no longer print negative percentages.

Pre-flight: typecheck ✓; new tests/host-usage-backfill.test.ts 15/15 ✓ (backfill unit, responses loop, openai/anthropic adapter raw events, plugin SSE/JSON pipes, persist clamp+migration incl. flat v1); full suite 308 pass / 3 fail — the 3 failures are in tests/proxy-codex-compact.test.ts and were verified pre-existing on clean master via git stash (same failures without this diff); build ✓. No version bump (content branch).

@github-actions

github-actions Bot commented Sep 1, 2026 •

Copy link
Copy Markdown
Contributor

📦 Built Package Artifact

Branch: 2026-09-01_host-usage-backfill (294f7c5)

Option A — Install from npm PR tag (recommended)

npm install -g billion-context@pr-425

Each push to this PR publishes a new version under the pr-425 npm tag.

Option B — Download artifact

  1. Download the artifact from the Actions run
  2. Extract the tarball and install:
tar xzf billion-context-pr425.tgz
npm install -g package

This comment is automatically updated on each push.

@ranxianglei

Copy link
Copy Markdown
Owner Author

🤖 ework agent · vllm-qwen/qwen3.8-27b

[bot] 🏷 Review complete — found and fixed one gap; PR updated with commit 80a9f89.

Verification (all on the PR branch):

  • npm run typecheck ✓
  • tests/host-usage-backfill.test.ts 16/16 ✓ (15 original + 1 new integration test)
  • Full suite: 907 pass / 0 fail — the 3 tests/proxy-codex-compact.test.ts failures mentioned in the description did not reproduce here
  • npm run build ✓; version untouched (0.1.67, content branch) ✓

Gap found in review: the description (and the #408 thread) state "each prepare* computes hostCreditTokens", but only prepareAnthropic (src/server.ts:1374) actually armed it. prepareOpenai and prepareResponses never set the field, so the credit stayed undefined → 0 on those wires and every backfill path was inert for openai/responses (loop completion synthesis, raw finish-chunk patching, plugin SSE/JSON pipes, non-stream JSON). The plumbing itself was correct and unit-tested (the tests inject the credit manually), but in production only the Anthropic wire — i.e. omp's case — was actually backfilling.

Fix pushed (80a9f89):

  • prepareOpenai + prepareResponses now mirror prepareAnthropic's arming verbatim: reset at request top, hostCreditTokens = max(0, est(original) − est(processed)) before return, info log when armed. prepareResponsesCompact / prepareCountTokens intentionally untouched (forge paths mint their own usage).
  • New full-proxy integration test: 6 client turns through a real proxy + mock OpenAI relay; turn 5 returns a compress tool call folding m00003..m00004; asserts the fold genuinely happened upstream (sentinel content absent from the forwarded body) and that turn 6's host-facing prompt_tokens is backfilled above the post-fold raw usage. One kernel quirk worth knowing: the kernel never prunes the first user message (rebuildMessages keeps firstUserIndex unconditionally), so a fold range that includes m00001 leaves the big content in the forwarded view and the credit stays ≈ 0 — the test's range starts at m00003 for that reason.

Before merge (human side):

  • Per AGENTS.md, server.ts + src/loop/ + adapters changed, so please run the e2e codex suite (ACP_TEST_E2E=1 node --import tsx --test tests/e2e/e2e-codex.test.ts) before merging — I could not run it in this environment.
  • Merge stays human-only per repo rules.

Minor (pre-existing on master, not introduced here): package-lock.json still records version: 0.1.65 while package.json is 0.1.67. Harmless in practice (CI's npm ci tolerates it and releases have been shipping), but worth syncing in the next release commit.

@ranxianglei

Copy link
Copy Markdown
Owner Author

Reviewed against master @8496194 (v0.1.83). The design is sound and I verified it works on current master — I performed the merge onto master locally to test it (branch base is 470c43b, Aug 31, so it needs a nontrivial rebase). Result: tsc clean, 1046/1048 tests pass (the 2 failures are the known plugin-agent env fails), build clean, all 15+1 tests in tests/host-usage-backfill.test.ts green after the API-drift fixes below.

Design — agree

  • Credit = estimateCoreMessages(original) − estimateCoreMessages(processed) armed per prepare*, reset each request: same estimator both sides so systematic error cancels — good property.
  • Backfill is host-agnostic (better than the withdrawn recordAnchoredHistoryRewrite idea, which subtracts and would shrink the anchor).
  • Internal ledger (lastInputTokens) staying post-fold while the host-facing report is backfilled is the right split — nudge decisions must read the forwarded view, not the host's view.
  • Persist clamp + one-time migration for negative tokens: reasonable hygiene.

Rebase conflicts — what master changed under this PR

  1. pipeThroughWithUsage no longer exists: master rewrote it as pipePluginChatWithStrip(stream, res, protocol, session?, log?) — note protocol/session order swapped — with the tag-echo filter state machine (偶尔重复输出 <acp tokens="177" type="text">m00155</acp> #206/plugin 模式 openai SSE:混合文本字段 chunk 因单字段纯标签 echo 被整体丢弃(丢失同 chunk 真实输出) #463), settleUsage (客户端取消 = 无主 AbortError ×260/天 + 已计费 usage 丢弃(lastInputTokens 冻结)+ 透传路径漏 10min timer(无 finally) #411), and sawTerminal/高 usage 下的非 400 失败(流截断/超时)无任何 overflow 兜底 —— 会话直接死 #498 truncation hooks. The backfill must be injected into that event loop: patch the parsed ev's usage before processOpenai/processAnthropic, and only rebuildEvent when the processors returned the chunk verbatim (out === rawEvent + "\n\n"); overwriting out unconditionally would drop render-tag stripping. Also keep the trailing-partial-event flush (if (buf.length > 0) await write(buf)) — master's loop only writes complete events.
  2. pipePluginJson: master extended the strip to anthropic+openai (stripAnthropicText/stripOpenaiChatText); merge the single-parse + mutated flag pattern with the protocol-specific strip so backfill and strip compose (json ?? JSON.parse(text) reuse).
  3. pickAdapter/adapters: signatures gained hostCredit = 0 — that part of the diff still applies cleanly, but master added stripFinishReasonChunk and the "authoritative completion" path in adapter-openai; the finish-chunk patch must keep finish_reason intact (the fix: report uncompressed baseline in host usage + clamp negative persisted token stats (#408) #425 code already does — chunk keeps it, done follows immediately).
  4. server.ts prepare*: master's returns now carry renderTags: "text-only" ([bug] ACP 压缩轮以 stop 终止任务:模型输出压缩摘要后零 toolCalls,需用户手动「继续」(#361 同族) #422) and the plugin mode: no hard backstop when the agent ignores the compress nudge — context grows past the window and upstream 400s #453 clamp constants live between the prepare functions; also pickAdapter call site now sits next to refreshFolded ([bug] ACP 压缩轮以 stop 终止任务:模型输出压缩摘要后零 toolCalls,需用户手动「继续」(#361 同族) #422). Credit-arming blocks in all prepare fns merge cleanly once those are kept.
  5. Test file: pipeThroughWithUsage → pipePluginChatWithStrip with the new arg order (this is what made 3 tests fail before I fixed them; with the rename+reorder they pass).

Minor notes

Fixes #408 once rebased. Given the conflict volume, recommend rebasing onto master rather than merging; the semantic points above are the only traps.

@ranxianglei

Copy link
Copy Markdown
Owner Author

解决冲突

@ranxianglei

Copy link
Copy Markdown
Owner Author

🤖 ework agent · qwen3.8-27b

[bot] 🏷 收到,正在按你给的冲突地图 rebase 到 master @8496194(adapters 已解决;plugin.ts 的 pipePluginChatWithStrip 事件循环注入和 pipePluginJson 合成进行中)。完成后会跑 typecheck + 全量测试 + build,force-push 分支并回帖验证结果。

@ranxianglei
ranxianglei force-pushed the 2026-09-01_host-usage-backfill branch from 80a9f89 to ac2b75e Compare September 4, 2026 05:05
@ranxianglei

Copy link
Copy Markdown
Owner Author

🤖 ework agent · qwen3.8-27b

[bot] 🏷 Rebase 完成,已 force-push 到 2026-09-01_host-usage-backfill(c4d94c6 + ac2b75e,base = master @8496194 / v0.1.83)。冲突全部按你给的地图解决:

冲突解决

  1. pipePluginChatWithStrip:backfill 注入在事件循环内、processOpenai/processAnthropic 之前 patch 解析后的 ev;仅当 processor 原样返回(out === rawEvent + "\n\n")才 rebuildEvent(rawEvent, ev),否则写 processor 的输出——render-tag stripping 不丢。trailing partial-event flush 保留(if (buf.length > 0 && !res.destroyed && !res.writableEnded) await write(buf),用了 master 的 destroyed guard 防止向已销毁的 res 挂 drain 等待)。usage 落账走 master 的 settleUsage()(客户端取消 = 无主 AbortError ×260/天 + 已计费 usage 丢弃(lastInputTokens 冻结)+ 透传路径漏 10min timer(无 finally) #411),它内部调用带 backfill 的 applyUsageSample,abort 路径也覆盖。
  2. pipePluginJson:合成成单次 parse + mutated flag——json && containsRenderTagText(text) 时按 protocol 选 stripResponsesText/stripAnthropicText/stripOpenaiChatText(均为原地修改,与 代理压缩不上报改写量,宿主上下文会计塌缩至 ~0(omps recordAnchoredHistoryRewrite 零调用);且持久化 lastInputTokens 恢复无钳制,旧版负值升级后复活 #408 的 usage backfill 天然组合),最后统一一次 reserialize。master 原来的二次 JSON.parse(text) 会丢掉已回填的 usage,已消除。
  3. adapters:openai 保留 master 的 stripFinishReasonChunk + authoritative-completion 路径,finish-chunk patch 只动 usage、finish_reason 原样;anthropic 的 messageStartForwarded(emitError 兜底用)与 backfill 并存。
  4. server.ts:三个 prepare* 的 credit arming 都在(anthropic/openai/responses);master 的 renderTags: "text-only" ([bug] ACP 压缩轮以 stop 终止任务:模型输出压缩摘要后零 toolCalls,需用户手动「继续」(#361 同族) #422) 和 plugin mode: no hard backstop when the agent ignores the compress nudge — context grows past the window and upstream 400s #453 clamp 常量(prepare 函数之间)、refreshFolded 全部保留;pickAdapter 调用点带 prepared.session.hostCreditTokens ?? 0。imports 合并时丢了 master 已无引用的 hoistMidSystemMessages(避免 dead import)。

rebase 后新发现并修掉的两处

  • 测试文件如你所料需要 pipeThroughWithUsage → pipePluginChatWithStrip 改名 + 参数重排(3 处调用 + import + 测试名)。
  • pipePluginResponsesWithStrip 里我们注入的 hunk 访问 session.hostCreditTokens 没加可选链——master 把该函数的 session 改成了 optional,TS18048。改为 session?.hostCreditTokens ?? 0。

验证(rebased 树)

  • npm run typecheck ✓
  • tests/host-usage-backfill.test.ts 16/16 ✓(含全代理集成测试)
  • 全量 1047/1048 ✓ —— 唯一失败是 launcher.test.ts: resolveClientCommand: codex/claude resolve to themselves,纯环境性:本环境装有 codex CLI,resolver 返回绝对路径而非裸命令,与本 diff 无关(和你看到的已知 env fails 同类)
  • npm run build ✓;version 未动(0.1.83,content branch)
  • diff vs master:11 files, +724/−27,全部在 PR 范围内

e2e:本环境有 codex CLI 但默认 upstream(本地 sglang :8199)不可达,e2e 套件跑不了——merge 前仍建议在有 upstream 的环境跑一遍 ACP_TEST_E2E=1 node --import tsx --test tests/e2e/e2e-codex.test.ts(server.ts/loop/adapters 都有改动)。

分支已就绪,可以 review/merge。

…isted token stats (#408)

- prepare* (anthropic/openai/responses) arm session.hostCreditTokens =
  est(original) - est(processed) each request; every host-facing usage
  outlet adds the credit back (adapters' raw SSE chunks, loop completion,
  plugin passthrough SSE/JSON, server JSON path) so the host anchors on
  the uncompressed baseline instead of the post-fold value
- /acp panel contextTokens switches to hostContextTokens; pi agent panel
  shows the backfill line
- persist: buildSession clamps negative lastInputTokens/contextTokens at
  restore; loaders rewrite stale files carrying negatives (one-time
  migration, logged)
- openai split-semantics upstreams (prompt_tokens < cached_tokens) now
  add the cached segment back in usageTotals/recordUsage/applyUsageSample
  via shared promptInputTotal — fixes the ~0% context reading on
  cache-heavy anthropic-family upstreams served over an openai wire
@ranxianglei
ranxianglei force-pushed the 2026-09-01_host-usage-backfill branch from 6a32faf to 294f7c5 Compare September 6, 2026 07:00
@ranxianglei

Copy link
Copy Markdown
Owner Author

已接手收尾:rebase 到当前 master(v0.1.87 6820e7d,cherry-pick 干净无冲突)+ 补齐上一条评论承诺但未完成的 openai 拆分口径判检。

本次新增(对应 @wosnxn123 09-04 的 cache 证据)

promptInputTotal(protocol, input, cached) 共享助手(src/util.ts):OpenAI/Responses 语义下 prompt_tokens >= cached_tokens 恒成立;一旦违反即证明该上游在 openai 线上报 Anthropic 拆分口径(prompt_tokens 仅新段),此时 cached 段不在 prompt_tokens 内,必须加回(实测 {"prompt_tokens":6, "cached_tokens":26278} → 真实 ~26284,而非 6)。

三处接入同一规则:

  • usageTotals(util.ts)openai 分支
  • recordUsage(loop/core.ts,替换原 includesCached 手写逻辑)
  • applyUsageSample(plugin.ts,同上)

内部账本 lastInputTokens / hostContextTokens / cache-hit% 从此按真实 prompt 规模记账 —— 这是比压缩塌缩更常触发的第二条塌缩路(anthropic 系上游轮轮命中缓存),正是 0% (6/1.0M) 的面值形态。

测试(tests/host-usage-backfill.test.ts,16→19)

  • promptInputTotal 协议矩阵(anthropic 加和 / openai 真语义不变 / 违反加回 / responses 同理 / undefined 输入→0)
  • usageTotals openai 拆分口径(26284 / 26278;真语义 30000 不变)
  • e2e:pipePluginChatWithStrip 上游发 {"prompt_tokens":6, "prompt_tokens_details":{"cached_tokens":26278}} → lastInputTokens=26284、cachedTokens=26278

Pre-flight

  • typecheck ✓
  • host-usage-backfill.test.ts 19/19 ✓
  • 全量 1082/1084(2 个已知 plugin-agent 环境失败,与 master 基线一致)✓
  • build ✓

分支 squash 为单提交 294f7c5,rebase 后显示 11 files +801/-45。

范围外(按前述讨论,另开票跟进)

  • anthropic cache_creation 进内部 total(usage 事件类型需加字段,波及面大)
  • "Sent to LLM (sent view)" 的 system+tools 漏计(硬编码 0)

Closes #408

@ranxianglei

Copy link
Copy Markdown
Owner Author

🤖 ework agent · qwen3.8-27b

[bot] 🏷 独立复核完成,全部属实:

分支状态:远端 tip = 294f7c5,merge-base = 6820e7d(master / v0.1.87)——确为单提交、干净基于当前 master;diff vs master = 11 files +801/−45,与你的描述完全一致;version 未动(0.1.87,content branch)。

promptInputTotal 逻辑审读:推理成立——真 OpenAI/Responses 语义下 prompt_tokens ⊇ cached_tokens 恒成立,所以 input < cached 的违反只能是 openai 线上报 Anthropic 拆分口径,加回正确。边界核对过:anthropic 恒加和;openai/responses 无违反时原样返回;input === cached(全命中缓存的合法真语义形态)不误判;input === undefined → 0。三处接入一致:recordUsage(core.ts)、applyUsageSample(plugin.ts)、usageTotals(util.ts openai 分支);hostContextTokens 和 warnCacheCollapse 都吃到拆分感知后的 total——cache-hit% 不再被假 6-token prompt 拉爆,这点正是 0% 面值形态的根因修复。

独立 pre-flight(本环境重跑):

  • npm run typecheck ✓
  • tests/host-usage-backfill.test.ts 19/19 ✓(与你一致)
  • 全量 1083/1084 —— 唯一失败 resolveClientCommand: codex/claude resolve to themselves,纯环境性(本环境装有 codex CLI,resolver 返回绝对路径而非裸命令名);diff 不含 launcher.ts/launcher.test.ts,与改动结构性无关。你那边是 1082/1084(2 个 plugin-agent 环境失败),同类 env 差异,非 diff 引入
  • npm run build ✓(dist/index.js 2.64MB)

范围外两项(anthropic cache_creation 进 internal total、sent-view system+tools 漏计)同意另开票,不阻塞本 PR。

分支已就绪。剩下就是 human merge —— merge 后 Closes #408 生效。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant