Skip to content

Commit 6217305

Browse files
committed
fix: Anthropic usage 的 input_tokens 改为只计非缓存部分(issue #25)
CC 上游的 inputTokens 是总数(含缓存命中),而 Anthropic 的 input_tokens 只计非缓存部分。 官方 SDK 注释:Total input tokens in a request is the summation of `input_tokens`, `cache_creation_input_tokens`, and `cache_read_input_tokens`。 此前直接把 CC 的 inputTokens 转发为 input_tokens,下游按 Anthropic 语义把两者视为 互不重叠,相加后约为真实输入的两倍(实测 7648 vs 实际 3936)。 CC 实际已经算好非缓存部分:inputTokenDetails.noCacheTokens(实测满足 noCacheTokens + cacheReadTokens === inputTokens),故优先采用该字段; 字段缺失时回退到 inputTokens - cacheRead - cacheWrite,保证老版本上游也正确。 覆盖两处输出点: - buildAnthropicResponse(非流式) - createAnthropicSseTranslator 的 message_delta(流式) 验证(真 key,deepseek/deepseek-v4.1-flash,每次用独立 system prompt 保证 cold 真冷) 修复前 cold warm input_tokens 3936 3936 cache_read 0 3712 sum(总输入) 3936 7648 ← 1.94 倍 修复后 input_tokens 3936 224 cache_read 0 3712 sum(总输入) 3936 3936 ← 恒定 ✔ 非流式与流式两条路径结果一致。 回归:既有端点全部通过;OpenAI 格式(chat/completions)的 prompt_tokens 仍为总数 (OpenAI 语义确实包含 cached,不应改动),本修复只作用于 Anthropic 端点。
1 parent 7488662 commit 6217305

1 file changed

Lines changed: 36 additions & 3 deletions

File tree

‎proxy.mjs‎

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -753,6 +753,24 @@ function normalizeUsage(u) {
753753
}
754754
}
755755

756+
// CC 的 inputTokens 是「总数」(含缓存命中部分),而 Anthropic 的 input_tokens 只计
757+
// 非缓存部分 —— 官方 SDK 注释:Total input tokens in a request is the summation of
758+
// `input_tokens`, `cache_creation_input_tokens`, and `cache_read_input_tokens`。
759+
// 直接把 CC 的 inputTokens 当 input_tokens 转发,会让下游把两者当成互不重叠的两部分,
760+
// 相加后约为真实输入的两倍(issue #25)。
761+
//
762+
// CC 实际已经算好:inputTokenDetails.noCacheTokens(实测 noCacheTokens + cacheReadTokens
763+
// === inputTokens)。优先采用该字段;缺失时回退到减法,保证老版本上游也能得到正确值。
764+
function anthropicInputTokens(usage, noCacheOverride) {
765+
const u = usage || {};
766+
if (typeof noCacheOverride === 'number' && noCacheOverride >= 0) return noCacheOverride;
767+
const noCache = u.inputTokenDetails && u.inputTokenDetails.noCacheTokens;
768+
if (typeof noCache === 'number' && noCache >= 0) return noCache;
769+
const cacheRead = u.cachedInputTokens || (u.inputTokenDetails && u.inputTokenDetails.cacheReadTokens) || 0;
770+
const cacheWrite = (u.inputTokenDetails && u.inputTokenDetails.cacheWriteTokens) || 0;
771+
return Math.max(0, (u.inputTokens || 0) - cacheRead - cacheWrite);
772+
}
773+
756774
function mapFinishReason(reason) {
757775
switch (reason) {
758776
case 'tool-calls': return 'tool_calls';
@@ -1384,7 +1402,8 @@ function buildAnthropicResponse(model, fullText, toolCalls, finishReason, usage,
13841402
const estOut = Math.max(1,
13851403
Math.ceil(((fullText || '').length + (thinkingText || '').length) / 4) + (toolCalls ? toolCalls.length * 20 : 0));
13861404
return {
1387-
input_tokens: usage?.inputTokens ?? 0,
1405+
// input_tokens 只计非缓存部分(Anthropic 语义),与 cache_* 相加才等于总输入
1406+
input_tokens: anthropicInputTokens(usage),
13881407
output_tokens: usage?.outputTokens || estOut,
13891408
cache_creation_input_tokens: usage?.inputTokenDetails?.cacheWriteTokens ?? 0,
13901409
cache_read_input_tokens: usage?.cachedInputTokens ?? 0,
@@ -1551,6 +1570,7 @@ async function* createAnthropicSseTranslator(response, model, messageId, ctx) {
15511570
let outputTokens = 0;
15521571
let cachedInputTokens = 0;
15531572
let cacheWriteTokens = 0;
1573+
let noCacheTokens = -1; // -1 = 上游未提供该字段,改用减法兜底
15541574
let stopReason = null;
15551575
let hasError = false;
15561576
let currentThinkingText = ''; // accumulated thinking text for the open block
@@ -1690,6 +1710,9 @@ async function* createAnthropicSseTranslator(response, model, messageId, ctx) {
16901710
outputTokens = u.outputTokens ?? outputTokens;
16911711
cachedInputTokens = u.cachedInputTokens ?? cachedInputTokens;
16921712
cacheWriteTokens = u.inputTokenDetails?.cacheWriteTokens ?? cacheWriteTokens;
1713+
if (typeof u.inputTokenDetails?.noCacheTokens === 'number') {
1714+
noCacheTokens = u.inputTokenDetails.noCacheTokens;
1715+
}
16931716
ctx.inputTokens = inputTokens;
16941717
ctx.outputTokens = outputTokens;
16951718
ctx.cachedInputTokens = cachedInputTokens;
@@ -1717,7 +1740,9 @@ async function* createAnthropicSseTranslator(response, model, messageId, ctx) {
17171740
}
17181741
}
17191742

1720-
// 无论上游是否回报 usage,都把本地计数同步进 ctx(零输出判定与 message_delta 账单依赖它)
1743+
// 无论上游是否回报 usage,都把本地计数同步进 ctx(零输出判定与超时日志依赖它)。
1744+
// 注意:ctx.inputTokens 保存的是上游原始总数,仅供日志排查;
1745+
// message_delta 的 input_tokens 走 anthropicInputTokens / noCacheTokens 换算,不读它。
17211746
ctx.inputTokens = inputTokens;
17221747
ctx.outputTokens = outputTokens;
17231748
ctx.cachedInputTokens = cachedInputTokens;
@@ -1735,7 +1760,15 @@ async function* createAnthropicSseTranslator(response, model, messageId, ctx) {
17351760
yield `event: message_delta\ndata: ${JSON.stringify({
17361761
type: 'message_delta',
17371762
delta: { stop_reason: stopReason || 'end_turn' },
1738-
usage: { output_tokens: outputTokens, cache_read_input_tokens: cachedInputTokens, cache_creation_input_tokens: cacheWriteTokens || 0, input_tokens: inputTokens },
1763+
usage: {
1764+
output_tokens: outputTokens,
1765+
cache_read_input_tokens: cachedInputTokens,
1766+
cache_creation_input_tokens: cacheWriteTokens || 0,
1767+
// 只计非缓存部分;否则下游把 input 与 cache_read 相加会得到约两倍(issue #25)
1768+
input_tokens: noCacheTokens >= 0
1769+
? noCacheTokens
1770+
: Math.max(0, inputTokens - cachedInputTokens - (cacheWriteTokens || 0)),
1771+
},
17391772
})}\n\n`;
17401773

17411774
yield `event: message_stop\ndata: ${JSON.stringify({ type: 'message_stop' })}\n\n`;

0 commit comments

Comments
 (0)