|
|
@@ -1280,6 +1280,25 @@ function extractUsageMetrics(value: unknown): UsageMetrics | null {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ // 兼容部分 relay / 旧字段命名:claude_cache_creation_5_m_tokens / claude_cache_creation_1_h_tokens
|
|
|
+ // 仅在标准字段缺失时使用,避免重复统计
|
|
|
+ if (
|
|
|
+ result.cache_creation_5m_input_tokens === undefined &&
|
|
|
+ typeof usage.claude_cache_creation_5_m_tokens === "number"
|
|
|
+ ) {
|
|
|
+ result.cache_creation_5m_input_tokens = usage.claude_cache_creation_5_m_tokens;
|
|
|
+ cacheCreationDetailedTotal += usage.claude_cache_creation_5_m_tokens;
|
|
|
+ hasAny = true;
|
|
|
+ }
|
|
|
+ if (
|
|
|
+ result.cache_creation_1h_input_tokens === undefined &&
|
|
|
+ typeof usage.claude_cache_creation_1_h_tokens === "number"
|
|
|
+ ) {
|
|
|
+ result.cache_creation_1h_input_tokens = usage.claude_cache_creation_1_h_tokens;
|
|
|
+ cacheCreationDetailedTotal += usage.claude_cache_creation_1_h_tokens;
|
|
|
+ hasAny = true;
|
|
|
+ }
|
|
|
+
|
|
|
if (result.cache_creation_input_tokens === undefined && cacheCreationDetailedTotal > 0) {
|
|
|
result.cache_creation_input_tokens = cacheCreationDetailedTotal;
|
|
|
}
|
|
|
@@ -1316,7 +1335,7 @@ function extractUsageMetrics(value: unknown): UsageMetrics | null {
|
|
|
return hasAny ? result : null;
|
|
|
}
|
|
|
|
|
|
-function parseUsageFromResponseText(
|
|
|
+export function parseUsageFromResponseText(
|
|
|
responseText: string,
|
|
|
providerType: string | null | undefined
|
|
|
): {
|
|
|
@@ -1404,11 +1423,29 @@ function parseUsageFromResponseText(
|
|
|
const events = parseSSEData(responseText);
|
|
|
|
|
|
// Claude SSE 特殊处理:
|
|
|
- // - message_start 包含 input tokens 和缓存创建字段(5m/1h 区分计费)
|
|
|
- // - message_delta 包含最终的 output_tokens
|
|
|
- // 需要分别提取并合并
|
|
|
+ // - message_delta 通常包含更完整的 usage(应优先使用)
|
|
|
+ // - message_start 可能包含 cache_creation 的 TTL 细分字段(作为缺失字段的补充)
|
|
|
let messageStartUsage: UsageMetrics | null = null;
|
|
|
- let messageDeltaOutputTokens: number | null = null;
|
|
|
+ let messageDeltaUsage: UsageMetrics | null = null;
|
|
|
+
|
|
|
+ const mergeUsageMetrics = (base: UsageMetrics | null, patch: UsageMetrics): UsageMetrics => {
|
|
|
+ if (!base) {
|
|
|
+ return { ...patch };
|
|
|
+ }
|
|
|
+
|
|
|
+ return {
|
|
|
+ input_tokens: patch.input_tokens ?? base.input_tokens,
|
|
|
+ output_tokens: patch.output_tokens ?? base.output_tokens,
|
|
|
+ cache_creation_input_tokens:
|
|
|
+ patch.cache_creation_input_tokens ?? base.cache_creation_input_tokens,
|
|
|
+ cache_creation_5m_input_tokens:
|
|
|
+ patch.cache_creation_5m_input_tokens ?? base.cache_creation_5m_input_tokens,
|
|
|
+ cache_creation_1h_input_tokens:
|
|
|
+ patch.cache_creation_1h_input_tokens ?? base.cache_creation_1h_input_tokens,
|
|
|
+ cache_ttl: patch.cache_ttl ?? base.cache_ttl,
|
|
|
+ cache_read_input_tokens: patch.cache_read_input_tokens ?? base.cache_read_input_tokens,
|
|
|
+ };
|
|
|
+ };
|
|
|
|
|
|
for (const event of events) {
|
|
|
if (typeof event.data !== "object" || !event.data) {
|
|
|
@@ -1417,37 +1454,54 @@ function parseUsageFromResponseText(
|
|
|
|
|
|
const data = event.data as Record<string, unknown>;
|
|
|
|
|
|
- // Claude message_start format: data.message.usage
|
|
|
- // 提取 input tokens 和缓存字段
|
|
|
- if (event.event === "message_start" && data.message && typeof data.message === "object") {
|
|
|
- const messageObj = data.message as Record<string, unknown>;
|
|
|
- if (messageObj.usage && typeof messageObj.usage === "object") {
|
|
|
- const extracted = extractUsageMetrics(messageObj.usage);
|
|
|
+ if (event.event === "message_start") {
|
|
|
+ // Claude message_start format: data.message.usage
|
|
|
+ // 部分 relay 可能是 data.usage(无 message 包裹)
|
|
|
+ let usageValue: unknown = null;
|
|
|
+ if (data.message && typeof data.message === "object") {
|
|
|
+ const messageObj = data.message as Record<string, unknown>;
|
|
|
+ usageValue = messageObj.usage;
|
|
|
+ }
|
|
|
+ if (!usageValue) {
|
|
|
+ usageValue = data.usage;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (usageValue && typeof usageValue === "object") {
|
|
|
+ const extracted = extractUsageMetrics(usageValue);
|
|
|
if (extracted) {
|
|
|
- messageStartUsage = extracted;
|
|
|
+ messageStartUsage = mergeUsageMetrics(messageStartUsage, extracted);
|
|
|
logger.debug("[ResponseHandler] Extracted usage from message_start", {
|
|
|
- source: "sse.message_start.message.usage",
|
|
|
+ source:
|
|
|
+ usageValue === data.usage
|
|
|
+ ? "sse.message_start.usage"
|
|
|
+ : "sse.message_start.message.usage",
|
|
|
usage: extracted,
|
|
|
});
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- // Claude message_delta format: data.usage.output_tokens
|
|
|
- // 提取最终的 output_tokens(在流结束时)
|
|
|
- if (event.event === "message_delta" && data.usage && typeof data.usage === "object") {
|
|
|
- const deltaUsage = data.usage as Record<string, unknown>;
|
|
|
- if (typeof deltaUsage.output_tokens === "number") {
|
|
|
- messageDeltaOutputTokens = deltaUsage.output_tokens;
|
|
|
- logger.debug("[ResponseHandler] Extracted output_tokens from message_delta", {
|
|
|
- source: "sse.message_delta.usage.output_tokens",
|
|
|
- outputTokens: messageDeltaOutputTokens,
|
|
|
- });
|
|
|
+ if (event.event === "message_delta") {
|
|
|
+ // Claude message_delta format: data.usage
|
|
|
+ let usageValue: unknown = data.usage;
|
|
|
+ if (!usageValue && data.delta && typeof data.delta === "object") {
|
|
|
+ usageValue = (data.delta as Record<string, unknown>).usage;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (usageValue && typeof usageValue === "object") {
|
|
|
+ const extracted = extractUsageMetrics(usageValue);
|
|
|
+ if (extracted) {
|
|
|
+ messageDeltaUsage = mergeUsageMetrics(messageDeltaUsage, extracted);
|
|
|
+ logger.debug("[ResponseHandler] Extracted usage from message_delta", {
|
|
|
+ source: "sse.message_delta.usage",
|
|
|
+ usage: extracted,
|
|
|
+ });
|
|
|
+ }
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// 非 Claude 格式的 SSE 处理(Gemini 等)
|
|
|
- if (!messageStartUsage && !messageDeltaOutputTokens) {
|
|
|
+ if (!messageStartUsage && !messageDeltaUsage) {
|
|
|
// Standard usage fields (data.usage)
|
|
|
applyUsageValue(data.usage, `sse.${event.event}.usage`);
|
|
|
|
|
|
@@ -1463,20 +1517,17 @@ function parseUsageFromResponseText(
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- // 合并 Claude SSE 的 message_start 和 message_delta 数据
|
|
|
- if (messageStartUsage) {
|
|
|
- // 使用 message_delta 中的 output_tokens 覆盖 message_start 中的值
|
|
|
- if (messageDeltaOutputTokens !== null) {
|
|
|
- messageStartUsage.output_tokens = messageDeltaOutputTokens;
|
|
|
- logger.debug(
|
|
|
- "[ResponseHandler] Merged output_tokens from message_delta into message_start usage",
|
|
|
- {
|
|
|
- finalOutputTokens: messageDeltaOutputTokens,
|
|
|
- }
|
|
|
- );
|
|
|
+ // Claude SSE 合并规则:优先使用 message_delta,缺失字段再回退到 message_start
|
|
|
+ const mergedClaudeUsage = (() => {
|
|
|
+ if (messageDeltaUsage && messageStartUsage) {
|
|
|
+ return mergeUsageMetrics(messageStartUsage, messageDeltaUsage);
|
|
|
}
|
|
|
- usageMetrics = adjustUsageForProviderType(messageStartUsage, providerType);
|
|
|
- usageRecord = messageStartUsage as unknown as Record<string, unknown>;
|
|
|
+ return messageDeltaUsage ?? messageStartUsage;
|
|
|
+ })();
|
|
|
+
|
|
|
+ if (mergedClaudeUsage) {
|
|
|
+ usageMetrics = adjustUsageForProviderType(mergedClaudeUsage, providerType);
|
|
|
+ usageRecord = mergedClaudeUsage as unknown as Record<string, unknown>;
|
|
|
logger.debug("[ResponseHandler] Final merged usage from Claude SSE", {
|
|
|
providerType,
|
|
|
usage: usageMetrics,
|