From b7fe75cb605c8476b8ad3d057bc10a74ae016e58 Mon Sep 17 00:00:00 2001 From: DokiDoki1103 <1666888816@qq.com> Date: Sat, 11 Oct 2025 20:16:44 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8DClaude=20Console?= =?UTF-8?q?=E6=B5=81=E5=BC=8F=E5=93=8D=E5=BA=94usage=E7=BB=9F=E8=AE=A1?= =?UTF-8?q?=E4=B8=8D=E5=AE=8C=E6=95=B4=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 完善message_delta中usage数据提取逻辑,支持提取input_tokens、cache_read_input_tokens等所有字段 - 添加兜底保护机制,确保流结束时不会丢失未保存的usage数据 - 提升关键日志级别从debug到info,便于问题排查 - 修复流式请求中input_tokens和cache_read_input_tokens为0的统计bug 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- src/services/claudeConsoleRelayService.js | 90 +++++++++++++++++++++-- 1 file changed, 83 insertions(+), 7 deletions(-) diff --git a/src/services/claudeConsoleRelayService.js b/src/services/claudeConsoleRelayService.js index 6b97c881..8130417d 100644 --- a/src/services/claudeConsoleRelayService.js +++ b/src/services/claudeConsoleRelayService.js @@ -517,14 +517,55 @@ class ClaudeConsoleRelayService { } } - if ( - data.type === 'message_delta' && - data.usage && - data.usage.output_tokens !== undefined - ) { - collectedUsageData.output_tokens = data.usage.output_tokens || 0 + if (data.type === 'message_delta' && data.usage) { + // 提取所有usage字段,message_delta可能包含完整的usage信息 + if (data.usage.output_tokens !== undefined) { + collectedUsageData.output_tokens = data.usage.output_tokens || 0 + } - if (collectedUsageData.input_tokens !== undefined && !finalUsageReported) { + // 提取input_tokens(如果存在) + if (data.usage.input_tokens !== undefined) { + collectedUsageData.input_tokens = data.usage.input_tokens || 0 + } + + // 提取cache相关的tokens + if (data.usage.cache_creation_input_tokens !== undefined) { + collectedUsageData.cache_creation_input_tokens = + data.usage.cache_creation_input_tokens || 0 + } + if (data.usage.cache_read_input_tokens !== undefined) { + collectedUsageData.cache_read_input_tokens = + data.usage.cache_read_input_tokens || 0 + } + + // 检查是否有详细的 cache_creation 对象 + if ( + data.usage.cache_creation && + typeof data.usage.cache_creation === 'object' + ) { + collectedUsageData.cache_creation = { + ephemeral_5m_input_tokens: + data.usage.cache_creation.ephemeral_5m_input_tokens || 0, + ephemeral_1h_input_tokens: + data.usage.cache_creation.ephemeral_1h_input_tokens || 0 + } + } + + logger.info( + '📊 [Console] Collected usage data from message_delta:', + JSON.stringify(collectedUsageData) + ) + + // 如果已经收集到了完整数据,触发回调 + if ( + collectedUsageData.input_tokens !== undefined && + collectedUsageData.output_tokens !== undefined && + !finalUsageReported + ) { + logger.info( + '🎯 [Console] Complete usage data collected:', + JSON.stringify(collectedUsageData) + ) usageCallback({ ...collectedUsageData, accountId }) finalUsageReported = true } @@ -569,6 +610,41 @@ class ClaudeConsoleRelayService { } } + // 🔧 兜底逻辑:确保所有未保存的usage数据都不会丢失 + if (!finalUsageReported) { + if ( + collectedUsageData.input_tokens !== undefined || + collectedUsageData.output_tokens !== undefined + ) { + // 补全缺失的字段 + if (collectedUsageData.input_tokens === undefined) { + collectedUsageData.input_tokens = 0 + logger.warn( + '⚠️ [Console] message_delta missing input_tokens, setting to 0. This may indicate incomplete usage data.' + ) + } + if (collectedUsageData.output_tokens === undefined) { + collectedUsageData.output_tokens = 0 + logger.warn( + '⚠️ [Console] message_delta missing output_tokens, setting to 0. This may indicate incomplete usage data.' + ) + } + // 确保有 model 字段 + if (!collectedUsageData.model) { + collectedUsageData.model = body.model + } + logger.info( + `📊 [Console] Saving incomplete usage data via fallback: ${JSON.stringify(collectedUsageData)}` + ) + usageCallback({ ...collectedUsageData, accountId }) + finalUsageReported = true + } else { + logger.warn( + '⚠️ [Console] Stream completed but no usage data was captured! This indicates a problem with SSE parsing or API response format.' + ) + } + } + // 确保流正确结束 if (!responseStream.destroyed) { responseStream.end()