fix(#728): proactive nudge for upstreams that never report usage - #731
Conversation
[bot] 🏷 Review complete — verified end-to-end on branch head Duplicate screenNo duplicate. Searched open+closed issues/PRs for the silent-backend/nudge-fallback topic — this is the only implementation of the refined option 1 agreed in #728 triage. Relation to neighbors: #726 is the incident, #727 fixed the downstream symptom (empty-summary diagnosis/retry/cooldown) — complementary layers, no overlap with this growth-source fix. Pre-flight (all run on the PR branch)
Code review — every claim checked against source
Minor observations (non-blocking, no changes requested)
Diff hygiene: single commit, 4 files, every line on-topic; no version bump, no lockfile drift, CHANGELOG entry follows the existing format. 中文摘要: 修复了上游从不报告 usage(ChatGPT 登录态后端)时 nudge 结构性永不触发、上下文无界增长直到硬限紧急折叠的问题——host 侧以上一轮本地 post-fold 外发载荷上界估算在 |
|
冲突 |
c899546 to
7eb92e8
Compare
📦 Built Package ArtifactBranch: Option A — Install from npm PR tag (recommended)npm install -g billion-context@pr-731Each push to this PR publishes a new version under the Option B — Download artifact
tar xzf billion-context-pr731.tgz
npm install -g packageInstall instructions are refreshed on each push. |
[bot] 🏷 Conflict resolved — rebased onto latest master, re-verified green. Cause. Master moved past the PR's base (0173105 → 6fff170). The conflict came from #787 (server-split phase 1), which relocated Fix. Rebased the single commit onto master: new head Re-verified on the new base:
One minor note from the rebase: master's #767 gave PR is now mergeable (state "blocked" = pending CI/approval only). 中文摘要:冲突来自 master 侧 #787 把 token 估算辅助函数挪进 |
|
解决冲突 另外看看是否还需要解决这个问题?是否已经有类似修复 |
…e upstreams never report usage Silent backends (ChatGPT-login style) omit usage.input_tokens, so lastInputTokens stays 0 and decideNudge can never fire: context grows until the preflight hard limit forces an emergency fold (#726 shape). Record each turn's outbound payload upper bound as session.stats.localInputEstimate (cleared at native-compaction boundaries) and let effectiveTokenCount fall back to min(estimate, this request's inbound upper bound) only while no real usage has ever been reported. Rebased onto master 7bf7cde. The rebase exposed an interaction with #817's per-body preflight dead-end cooldown key (sha256 of the rebuilt wire body): once the fallback arms the nudge on these silent backends, the nudge text injected into the system prompt changes the rebuilt body between two identical client retries, the cooldown key misses, and every retry re-burns the doomed summarization walk — defeating #726's quota protection exactly where it was built for. Key the cooldown on the client's raw inbound body instead of the proxy-rebuilt wire body so proxy-side injections (nudge/system/tools/tags) cannot defeat it.
7eb92e8 to
f879c46
Compare
已完成 rebase + 查重 + 一个 rebase 暴露的真实回归修复。分支已更新: 1. 冲突解决(2 处)
2. 是否还需要 / 是否已有类似修复**仍然需要,master 上没有类似修复。**证据:
3. Rebase 暴露的新回归(已在同一提交内修复)Rebase 后
这是 #817 的潜在缺陷(任何逐轮变化的注入都能破掉 body-hash 身份),此前被掩盖——沉默后端上 nudge 本来就不触发。修复:cooldown 身份改为哈希客户端原始入站请求体(解码后的 raw bytes),代理侧注入(nudge/system/tools/tags)不再能击穿它;键格式不变( 4. 验证
一句话中文摘要:已 rebase 到最新 master 并解决 CHANGELOG 与 session.ts 两处冲突;确认 master 无类似修复、问题仍然存在;同时修复了 rebase 暴露的与 #817 dead-end cooldown 键的交互回归(cooldown 改按客户端原始请求体哈希,nudge 注入不再使冷却失效、重试不再重复烧上游配额),typecheck/全量测试/build 全绿,等待人工合并。 |
Summary
Implements the refined option 1 agreed in #728 triage (host-side fallback, no kernel change; option 2 rejected, option 3 deferred per triage; codex source cross-check confirmed the server-observed-first + local-estimator-fallback design shape).
When an upstream NEVER reports usage (ChatGPT-login backends omit
usage.input_tokensentirely),lastInputTokensstays 0 all session anddecideNudgeis structurally unfireable at tokenCount == 0 — contexts grew unbounded until the hard-limit emergency preflight took over (#726: ~1.32M tokens → ~630K-token emergency fold → 502 loop).Changes
src/session.ts: new statlocalInputEstimate(persisted with stats); cleared byresetSessionCompressionon native-compaction boundary reset.src/server.ts(prepareAnthropic/prepareOpenai/prepareResponses): each records the char-count upper bound of that turn's outbound payload — post-fold processed messages + system/tools overhead + image tokens — before returning (title-gen and compaction-trigger requests skip; failed transform falls back to the unprocessed projection so the fallback isn't blinded).src/server.ts(effectiveTokenCount): precedence unchanged for real usage (always wins) and for anonymous-prefix-affinity forks (raw inbound upper bound, fork 会话 lastInputTokens=0 时 preflight 退化为字符估算,大载荷被低估后不压缩直接裸转上游 #553 branch untouched); explicit-identity zero-baseline now returnslocalInputEstimatecapped by this request's inbound upper bound (a stale-high reading can't outlive a client-side shrink), falling back to 0 as before when nothing was measured — turn 1 behavior byte-identical.Same three invariants as #604's
armFailureShrinkexception: upper-bound estimator only errs early (compress earlier, never later); active only whilelastInputTokens == 0; any real usage report takes precedence immediately. Self-corrects after every fold (post-fold outbound payload shrinks → next reading drops), so proxy clients re-sending full raw history don't inflate it.Out of scope per triage: one-shot handoff paths (
prepareCountTokens/prepareResponsesCompact) still pass tokenCount directly — those requests ARE the compaction mechanism.Tests
New
tests/silent-backend-nudge.test.ts(e2e, Anthropic wire, explicit identity):input_tokens: 2000every turn → NO nudge anywhere (real usage beats the ~90% payload estimate);Full suite green: 1428 tests, 1426 pass, 2 skipped, 0 fail (includes existing anonymous/explicit regime pins unchanged).
Closes #728.