Skip to content

[PR] fix(wiring): credential fingerprint covers all forwarded auth headers, order-independent (#6148) - #6154

Open
ranxianglei wants to merge 1 commit into
masterfrom
2026-11-11_multi-cred-fingerprint-order
Open

ranxianglei wants to merge 1 commit into
masterfrom
2026-11-11_multi-cred-fingerprint-order

Conversation

@ranxianglei

Copy link
Copy Markdown
Owner

Closes #6148

Problem

credentialFingerprint (src/cache-ledger.ts) broke on the first recognized auth header in insertion order, so a lane forwarding two distinct credentials (e.g. a shared gateway authorization plus a per-account x-api-key) got an identity that was both:

The pre-ingest fast path (src/server/handle.ts, #5795) collects all four header names in fixed name-order, which is exactly how it could disagree with the real governor's key for the same outbound request.

Fix

Collect every recognized header, normalize each (trim / strip Bearer envelope / fold SDK-HMAC-SHA256 lines to their Access key id), dedupe by normalized value, and hash the sorted set (\u0000-joined). Order-independent by construction, complete by construction.

Behavior change (disclosed)

before after why
≥2 distinct credentials: fingerprint = first header in insertion order fingerprint = sorted set of all distinct normalized credentials the old identity was unstable (flipped with header order) and incomplete — that IS the bug
single-credential requests byte-identical a single-distinct-value set hashes exactly as before — bearer/x-api-key aliasing (#2131) and SDK-HMAC folding (#1884) preserved

Affected surfaces: the throttle hold key (upstreamThrottleGate / noteUpstreamThrottle429 / noteUpstreamThrottleResolved) and cache-ledger kp attribution. Multi-credential lanes had no stable identity before this fix (it flipped with insertion order), so nothing consistent is invalidated.

Tests

New tests/issue6148-multi-cred-fingerprint.test.ts (10 rows):

  • unit: order independence, completeness (changing the second credential flips the digest), multi-set ≠ either single member, alias preservation, SDK-HMAC inside a multi-set, header-name case-insensitivity, empty/undefined regression, join-separator collision guard
  • governor: hold matches across header reorder; an account sharing the gateway credential neither inherits nor clears another account's hold

Verified: 6 of the 10 rows fail against unchanged source (the rest are regression guards). Local targeted run: 148 tests across the cache-ledger / fingerprint / governor / fast-path suites pass; typecheck + build clean. Full suite rides CI.

Scope

Single module (src/cache-ledger.ts) + test file. No config surface, no wire/message shape, no kernel/, no persistence-format change. Pure fix:.

…, order-independent (#6148)

credentialFingerprint broke on the FIRST recognized credential header in insertion order, so a lane forwarding two distinct credentials (shared gateway 'authorization' + per-account 'x-api-key') got an identity that was both order-dependent (reorder flipped the digest -> 429 governor holds missed) and incomplete (second credential invisible -> another account behind the same gateway inherited, and could clear on success, the first account's hold).

Collect every recognized header, dedupe by normalized value (Bearer-envelope strip + SDK-HMAC Access-key folding preserved), hash the SORTED set. Single-distinct-value sets hash byte-identically to before; only lanes with >=2 distinct credentials change identity - which had no stable identity before.

Behavior change: for >=2-distinct-credential requests the throttle hold key and cache-ledger kp move from 'first header in insertion order' to 'sorted full set'. Single-credential requests are byte-identical.
@ranxianglei ranxianglei added bug Something isn't working p2 scope/wiring labels Nov 10, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📦 Built Package Artifact

Branch: 2026-11-11_multi-cred-fingerprint-order (3c0c507)

Latest build — install this exact version

npm install -g billion-context@0.0.8996-pr.6154.5191816

This exact version is immutable and reproducible. The rolling pr tag tracks the newest test build across all PRs.

Recent builds (commit → version)

commit version build
3c0c507 billion-context@0.0.8996-pr.6154.5191816 build

Download artifact

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

Install instructions are refreshed on each push.

@ranxianglei ranxianglei changed the title fix(wiring): credential fingerprint covers all forwarded auth headers, order-independent (#6148) [PR] fix(wiring): credential fingerprint covers all forwarded auth headers, order-independent (#6148) Nov 10, 2026
@ranxianglei

Copy link
Copy Markdown
Owner Author

🤖 Powered by ework · qwen3.8-27b

🔶 需要决策

背景:本 PR(GitHub #6154)与外部 PR #6149(Afloat16 fork,早开约 4.5 小时)修的是同一个缺陷 #6148、同一机制、同一修法家族(归一化凭证集合排序后哈希,单凭证保持字节一致),两者都 open 且都声明 Closes #6148。
待决问题:保留哪一个?(agent 无权合并/关闭 PR,只能由 owner 拍板)
选项:

(ai: vllm-qwen/qwen3.8-27b)

查重结论:重复 —— PR #6149(Afloat16/billion-context,opened 2026-11-10T15:35Z)与本 PR(opened 20:01Z)目标同为 #6148、同根因同机制,属同一修复的两个在途实现;按先来后到,本 PR 是后到者。如实说明:此重复先于本会话存在——#6154 开出时未查到 #6149。

标签:bug · p2 · scope/wiring(+ needs-decision,对应上方待决项)。理由:仅多凭证车道受影响、特定条件下限流/归属判定出错,无数据丢失/可用性毁灭/缓存命中崩坏 → p2;无具体客户端车道,不打客户端标签。GitHub PR 侧已存在同样三个标签(live 核验)。

Live state(今日核验):PR open、mergeable=true;head 3c0c507 CI 绿——最新 CI run success(2026-11-10T20:10Z)、全部 e2e 车道 success、check-runs 零失败(cancelled/skipped 条目均为并发策略下被取代的重复 run 与 skip-scoped labeled run;三层 CI 对 PR 以 CORE 矩阵把关)。

Review of #6154(independent pass)

① Direction — verified. Root cause is real and correctly located: credentialFingerprint (src/cache-ledger.ts) breaks on the FIRST recognized non-empty auth header, so the identity could represent exactly one credential. I reproduced both failure modes against master's source with a standalone script running the verbatim pre-fix function: reordering {authorization, x-api-key} flips the digest; changing the second credential while keeping the first does NOT flip it. Fixing the identity function (the producer) is the right layer — no consumer-level interception involved.

② Regression audit — existing behaviors on the touched paths:

existing behavior verdict
single-credential fingerprint (all four header names + SDK-HMAC line) byte-identical — verified by direct hash comparison old vs new
empty / null / whitespace-only input → undefined preserved (verified)
header-name case-insensitivity preserved (verified)
#2131 aliasing (Bearer X + bare X → one credential) preserved — folds to the exact old single fp (verified)
#1884/#4204 SDK-HMAC folding (resign-lane stability) preserved — folded per-header BEFORE set construction, so re-signed lines stay stable inside a multi-set (verified)
governor hold-key shape (host\u0000cred, src/upstream-throttle.ts:372-381) unchanged; ALL call sites (relay.ts:1256/2107/2109/2157, loop/core.ts:667/683/688, degenerate-retry.ts:151/215/223, fast path handle.ts:104→601) key through the same function — fast-path/governor agreement becomes structural (set semantics make collection order irrelevant everywhere)
cache-ledger kp attribution (relay.ts:1165, loop/core.ts:616) + keepalive snapshot id (cache-keepalive.ts:287) value changes ONLY for multi-credential lanes, which had no stable value before (it flipped with insertion order) — nothing consistent invalidated

No fail-fast→swallow, no threshold/timing/output-format drift, persistence format untouched. No unintended regressions found.

③ Code. Clean rewrite: collect-all → normalize → Set-dedupe by normalized value → hash the sorted set (\u0000-joined, collision-guarded by a dedicated test row). One semantic note: the set is value-only, so the same two credentials carried under different header names share one identity — that is the correct quota-pool semantics (same credentials = same upstream pool regardless of carrier header). Provenance comments match house style; no dead code, no as any.

④ Tests. 10 rows against REAL governor state (no mocks): env knobs explicitly pinned so daemon config can't leak into rows, deterministic integer timestamps, _resetUpstreamThrottleForTest isolation. Teeth confirmed independently (core failure modes fail on unmodified source). One gap: the new file does not pin the POSITIVE clear path (hold released when the original set succeeds) — that path IS pinned in #6149's suite and by the standing upstream-throttle suite, and CI's unit shards are green on this head.

⑤ Diff. Two files, every line on-purpose, single commit, clean message, house-style imports (.ts specifiers, matching the repo convention). Base moved after the cut (only compressAtFoldPoint removal landed since — zero contention on touched files); a rebase would be mechanical if option B is chosen.

Side-by-side: #6149 vs #6154

#6149 (Afloat16, external fork) #6154 (this thread)
opened 2026-11-10T15:35Z (earlier) 2026-11-10T20:01Z
base current master tip 33104b1f95 (fresh) 58a8796224 (one step behind; mechanical rebase, no contention)
multi-set encoding credential-set: + sorted JSON of [name, credential] pairs (header names participate) sorted normalized VALUES joined \u0000 (names don't participate)
single-credential compat byte-identical byte-identical (verified directly)
tests 5 rows incl. positive clear path, alias + case edges 10 rows incl. pair≠member, join-collision guard, null/undefined regression, pool-count asserts; lacks positive clear path
state mergeable=true mergeable=true, CI green

Both are correct implementations of the same fix; neither warrants rework. The differences are test-matrix granularity vs base freshness + the critical clear-path row.

八问

  1. 是什么问题:多凭证车道(如共享网关 authorization + 每账号 x-api-key)的限流/缓存归属身份只取了插入顺序里的第一个认证头——换序即变身份(429 hold 失配)、第二个凭证不可见(同网关下另一账号会继承、甚至成功时清掉前者的 hold)。
  2. 什么原因导致的:credentialFingerprint(src/cache-ledger.ts)遇到第一个非空认证头就 break,身份在构造上只能表达一个凭证——根因在身份函数本身(生产者),不在任何消费者。
  3. 影响范围与严重程度:仅转发 ≥2 个不同凭证的车道(特定条件);后果是 429 governor 双向判错(hold 漏配 / 跨账号继承与清除)+ ledger kp 归属碎片化。单凭证车道字节级不受影响。p2:可用,特定条件下明显问题,可绕行。
  4. 怎么修复的:收集全部识别到的认证头,逐个归一化(trim / 去 Bearer 信封 / SDK-HMAC 折成 Access key id),按归一化值去重,对排序后的集合哈希——构造上同时满足顺序无关与完整;单一凭证集合与旧实现逐字节一致。
  5. 是否有回归问题:逐项审过既有行为(见上表)——单凭证指纹字节一致(四种头名 + SDK-HMAC 直接比对验证)、空输入 undefined、大小写不敏感、大量缓存接缝事件导致缓存命中率大幅度降低 #2131 别名折叠、bili 改写带 body 签名的模型请求,CodeArts(华为 APIG SDK-HMAC-SHA256)全部 401 #1884 SDK-HMAC 折叠、governor key 形状、快路径/governor 一致性(现为结构性保证)、kp 归属与 keepalive 身份——均保持或按披露有意变更;无 fail-fast 变吞错、无阈值/时序/格式漂移。CI CORE 矩阵 + 全 e2e 车道在推送 head 上绿。
  6. 是否破坏缓存:与 prompt-cache 无关。throttle hold 表与 ledger kp 桶:单凭证键不变;多凭证车道此前没有稳定键(随插入顺序翻转),不存在被失效的一致状态——此类车道的在途 hold 至多重置一次(进程内内存态,有界)。
  7. 是否改了设计方向:没有。同函数、同消费者、同 key 形状,只有身份内容规则从「第一个头」变为「排序集合」。
  8. 是否引入了新的配置:没有。无新 env/config/knob。

收工判定:代码本身达到可合并质量;但同一 issue 不应留两个 open PR——按先来后到推荐选项 A(合 #6149、关 #6154),等 owner 拍板;若选 B,我立即 rebase 并补正向清除测试行。

This branch has not been deployed

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

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug] Multiple authentication headers make throttle identity order-dependent and incomplete

1 participant