From 4643e471ee5c0ef8e99381c885a58193d8b163c1 Mon Sep 17 00:00:00 2001 From: shaw Date: Sun, 17 Aug 2025 00:03:17 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8Dclaude=E8=BD=AC?= =?UTF-8?q?=E5=8F=91usage=E7=BB=9F=E8=AE=A1=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/services/claudeRelayService.js | 132 ++++++++++++++++++++++------- 1 file changed, 102 insertions(+), 30 deletions(-) diff --git a/src/services/claudeRelayService.js b/src/services/claudeRelayService.js index 71ed496b..50fb739c 100644 --- a/src/services/claudeRelayService.js +++ b/src/services/claudeRelayService.js @@ -269,17 +269,39 @@ class ClaudeRelayService { } } - // 记录成功的API调用 - const inputTokens = requestBody.messages - ? requestBody.messages.reduce((sum, msg) => sum + (msg.content?.length || 0), 0) / 4 - : 0 // 粗略估算 - const outputTokens = response.content - ? response.content.reduce((sum, content) => sum + (content.text?.length || 0), 0) / 4 - : 0 + // 记录成功的API调用并打印详细的usage数据 + let responseBody = null + try { + responseBody = typeof response.body === 'string' ? JSON.parse(response.body) : response.body + } catch (e) { + logger.debug('Failed to parse response body for usage logging') + } - logger.info( - `✅ API request completed - Key: ${apiKeyData.name}, Account: ${accountId}, Model: ${requestBody.model}, Input: ~${Math.round(inputTokens)} tokens, Output: ~${Math.round(outputTokens)} tokens` - ) + if (responseBody && responseBody.usage) { + const { usage } = responseBody + logger.info('📊 === Non-Stream Request Usage Summary ===', { + request_model: requestBody.model, + response_model: responseBody.model || requestBody.model, + input_tokens: usage.input_tokens || 0, + output_tokens: usage.output_tokens || 0, + cache_creation_tokens: usage.cache_creation_input_tokens || 0, + cache_read_tokens: usage.cache_read_input_tokens || 0, + api_key: apiKeyData.name, + account_id: accountId + }) + } else { + // 如果没有usage数据,使用估算值 + const inputTokens = requestBody.messages + ? requestBody.messages.reduce((sum, msg) => sum + (msg.content?.length || 0), 0) / 4 + : 0 + const outputTokens = response.content + ? response.content.reduce((sum, content) => sum + (content.text?.length || 0), 0) / 4 + : 0 + + logger.info( + `✅ API request completed - Key: ${apiKeyData.name}, Account: ${accountId}, Model: ${requestBody.model}, Input: ~${Math.round(inputTokens)} tokens (estimated), Output: ~${Math.round(outputTokens)} tokens (estimated)` + ) + } // 在响应中添加accountId,以便调用方记录账户级别统计 response.accountId = accountId @@ -893,8 +915,8 @@ class ClaudeRelayService { } let buffer = '' - let finalUsageReported = false // 防止重复统计的标志 - const collectedUsageData = {} // 收集来自不同事件的usage数据 + const allUsageData = [] // 收集所有的usage事件 + let currentUsageData = {} // 当前正在收集的usage数据 let rateLimitDetected = false // 限流检测标志 // 监听数据块,解析SSE并寻找usage信息 @@ -931,34 +953,43 @@ class ClaudeRelayService { // 收集来自不同事件的usage数据 if (data.type === 'message_start' && data.message && data.message.usage) { + // 新的消息开始,如果之前有数据,先保存 + if ( + currentUsageData.input_tokens !== undefined && + currentUsageData.output_tokens !== undefined + ) { + allUsageData.push({ ...currentUsageData }) + currentUsageData = {} + } + // message_start包含input tokens、cache tokens和模型信息 - collectedUsageData.input_tokens = data.message.usage.input_tokens || 0 - collectedUsageData.cache_creation_input_tokens = + currentUsageData.input_tokens = data.message.usage.input_tokens || 0 + currentUsageData.cache_creation_input_tokens = data.message.usage.cache_creation_input_tokens || 0 - collectedUsageData.cache_read_input_tokens = + currentUsageData.cache_read_input_tokens = data.message.usage.cache_read_input_tokens || 0 - collectedUsageData.model = data.message.model + currentUsageData.model = data.message.model // 检查是否有详细的 cache_creation 对象 if ( data.message.usage.cache_creation && typeof data.message.usage.cache_creation === 'object' ) { - collectedUsageData.cache_creation = { + currentUsageData.cache_creation = { ephemeral_5m_input_tokens: data.message.usage.cache_creation.ephemeral_5m_input_tokens || 0, ephemeral_1h_input_tokens: data.message.usage.cache_creation.ephemeral_1h_input_tokens || 0 } - logger.info( + logger.debug( '📊 Collected detailed cache creation data:', - JSON.stringify(collectedUsageData.cache_creation) + JSON.stringify(currentUsageData.cache_creation) ) } - logger.info( + logger.debug( '📊 Collected input/cache data from message_start:', - JSON.stringify(collectedUsageData) + JSON.stringify(currentUsageData) ) } @@ -968,18 +999,25 @@ class ClaudeRelayService { data.usage && data.usage.output_tokens !== undefined ) { - collectedUsageData.output_tokens = data.usage.output_tokens || 0 + currentUsageData.output_tokens = data.usage.output_tokens || 0 - logger.info( + logger.debug( '📊 Collected output data from message_delta:', - JSON.stringify(collectedUsageData) + JSON.stringify(currentUsageData) ) - // 如果已经收集到了input数据,现在有了output数据,可以统计了 - if (collectedUsageData.input_tokens !== undefined && !finalUsageReported) { - logger.info('🎯 Complete usage data collected, triggering callback') - usageCallback(collectedUsageData) - finalUsageReported = true + // 如果已经收集到了input数据和output数据,这是一个完整的usage + if (currentUsageData.input_tokens !== undefined) { + logger.info( + '🎯 Complete usage data collected for model:', + currentUsageData.model + ) + // 触发回调记录这个usage + usageCallback(currentUsageData) + // 保存到列表中 + allUsageData.push({ ...currentUsageData }) + // 重置当前数据,准备接收下一个 + currentUsageData = {} } } @@ -1037,11 +1075,45 @@ class ClaudeRelayService { logger.error('❌ Error processing stream end:', error) } + // 如果还有未完成的usage数据,尝试保存 + if (currentUsageData.input_tokens !== undefined) { + if (currentUsageData.output_tokens === undefined) { + currentUsageData.output_tokens = 0 // 如果没有output,设为0 + } + usageCallback(currentUsageData) + allUsageData.push(currentUsageData) + } + // 检查是否捕获到usage数据 - if (!finalUsageReported) { + if (allUsageData.length === 0) { logger.warn( '⚠️ Stream completed but no usage data was captured! This indicates a problem with SSE parsing or Claude API response format.' ) + } else { + // 打印此次请求的所有usage数据汇总 + const totalUsage = allUsageData.reduce( + (acc, usage) => ({ + input_tokens: (acc.input_tokens || 0) + (usage.input_tokens || 0), + output_tokens: (acc.output_tokens || 0) + (usage.output_tokens || 0), + cache_creation_input_tokens: + (acc.cache_creation_input_tokens || 0) + (usage.cache_creation_input_tokens || 0), + cache_read_input_tokens: + (acc.cache_read_input_tokens || 0) + (usage.cache_read_input_tokens || 0), + models: [...(acc.models || []), usage.model].filter(Boolean) + }), + {} + ) + + logger.info('📊 === Stream Request Usage Summary ===', { + request_body_model: body.model, + total_input_tokens: totalUsage.input_tokens, + total_output_tokens: totalUsage.output_tokens, + total_cache_creation: totalUsage.cache_creation_input_tokens, + total_cache_read: totalUsage.cache_read_input_tokens, + models_used: [...new Set(totalUsage.models)], + usage_events_count: allUsageData.length, + detailed_usage: allUsageData + }) } // 处理限流状态