From 29d36bdf1464e3a63190a3ab8438bad1c31a86b9 Mon Sep 17 00:00:00 2001 From: Anonymous Contributor Date: Fri, 13 Feb 2026 15:11:10 +0800 Subject: [PATCH 1/3] fix(gemini): handle split chunks correctly in stream usage capture --- src/handlers/geminiHandlers.js | 33 +++++++++++++++++++++++++-------- 1 file changed, 25 insertions(+), 8 deletions(-) diff --git a/src/handlers/geminiHandlers.js b/src/handlers/geminiHandlers.js index fc9f0103..085a56bb 100644 --- a/src/handlers/geminiHandlers.js +++ b/src/handlers/geminiHandlers.js @@ -1945,7 +1945,7 @@ async function handleStreamGenerateContent(req, res) { res.setHeader('X-Accel-Buffering', 'no') // 处理流式响应并捕获usage数据 - let streamBuffer = '' + let streamBuffer = '' // 移动到 data 事件处理器外部,保持状态 let totalUsage = { promptTokenCount: 0, candidatesTokenCount: 0, @@ -1981,27 +1981,44 @@ async function handleStreamGenerateContent(req, res) { setImmediate(() => { try { const chunkStr = chunk.toString() - if (!chunkStr.trim() || !chunkStr.includes('usageMetadata')) { - return + streamBuffer += chunkStr + + // 如果 buffer 过大,进行保护性清理(防止内存泄漏) + if (streamBuffer.length > 1024 * 1024) { // 1MB + streamBuffer = streamBuffer.slice(-1024 * 64) // 只保留最后 64KB } - streamBuffer += chunkStr const lines = streamBuffer.split('\n') + // 保留最后一行(可能不完整) streamBuffer = lines.pop() || '' for (const line of lines) { - if (!line.trim() || !line.includes('usageMetadata')) { + // 只处理可能包含数据的行 + if (!line.trim() || !line.startsWith('data:')) { continue } try { + // ��试解析 SSE 行 const parsed = parseSSELine(line) - if (parsed.type === 'data' && parsed.data.response?.usageMetadata) { - totalUsage = parsed.data.response.usageMetadata + + // 检查各种可能的 usage 位置 + let extractedUsage = null + + if (parsed.type === 'data') { + if (parsed.data.response?.usageMetadata) { + extractedUsage = parsed.data.response.usageMetadata + } else if (parsed.data.usageMetadata) { + extractedUsage = parsed.data.usageMetadata + } + } + + if (extractedUsage) { + totalUsage = extractedUsage logger.debug('📊 Captured Gemini usage data:', totalUsage) } } catch (parseError) { - logger.warn('⚠️ Failed to parse usage line:', parseError.message) + // 解析失败忽略,可能是非 JSON 数据 } } } catch (error) { From 3f0dabc5fad3a284c43b047077ee668eb981f988 Mon Sep 17 00:00:00 2001 From: Anonymous Contributor Date: Sat, 14 Feb 2026 15:44:25 +0800 Subject: [PATCH 2/3] fix(gemini): resolve incomplete fix and race conditions in usage capture --- src/handlers/geminiHandlers.js | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/src/handlers/geminiHandlers.js b/src/handlers/geminiHandlers.js index 085a56bb..ca96989b 100644 --- a/src/handlers/geminiHandlers.js +++ b/src/handlers/geminiHandlers.js @@ -642,6 +642,7 @@ async function handleMessages(req, res) { candidatesTokenCount: 0, totalTokenCount: 0 } + let streamBuffer = '' geminiResponse.on('data', (chunk) => { try { @@ -649,7 +650,17 @@ async function handleMessages(req, res) { res.write(chunkStr) // 尝试从 SSE 流中提取 usage 数据 - const lines = chunkStr.split('\n') + streamBuffer += chunkStr + + // 如果 buffer 过大,进行保护性清理(防止内存泄漏) + if (streamBuffer.length > 1024 * 1024) { // 1MB + streamBuffer = streamBuffer.slice(-1024 * 64) // 只保留最后 64KB + } + + const lines = streamBuffer.split('\n') + // 保留最后一行(可能不完整) + streamBuffer = lines.pop() || '' + for (const line of lines) { if (line.startsWith('data:')) { const data = line.substring(5).trim() @@ -1977,9 +1988,8 @@ async function handleStreamGenerateContent(req, res) { res.write(chunk) } - // 异步提取 usage 数据 - setImmediate(() => { - try { + // 提取 usage 数据 + try { const chunkStr = chunk.toString() streamBuffer += chunkStr @@ -2024,7 +2034,6 @@ async function handleStreamGenerateContent(req, res) { } catch (error) { logger.warn('⚠️ Error extracting usage data:', error.message) } - }) } catch (error) { logger.error('Error processing stream chunk:', error) } @@ -2780,7 +2789,6 @@ async function handleStandardStreamGenerateContent(req, res) { res.write(outputChunk) } - setImmediate(() => { try { const usageSource = processedPayload && processedPayload !== '[DONE]' ? processedPayload : dataPayload @@ -2799,7 +2807,6 @@ async function handleStandardStreamGenerateContent(req, res) { } catch (error) { // 提取用量失败时忽略 } - }) } streamResponse.on('data', (chunk) => { From 3f81afabced7e109117eac5fcf79d77a99159920 Mon Sep 17 00:00:00 2001 From: Anonymous Contributor Date: Sat, 14 Feb 2026 15:54:27 +0800 Subject: [PATCH 3/3] style(gemini): apply linter fixes for code quality check --- src/handlers/geminiHandlers.js | 124 +++++++++++++++++---------------- 1 file changed, 63 insertions(+), 61 deletions(-) diff --git a/src/handlers/geminiHandlers.js b/src/handlers/geminiHandlers.js index ca96989b..40e4a703 100644 --- a/src/handlers/geminiHandlers.js +++ b/src/handlers/geminiHandlers.js @@ -651,10 +651,11 @@ async function handleMessages(req, res) { // 尝试从 SSE 流中提取 usage 数据 streamBuffer += chunkStr - + // 如果 buffer 过大,进行保护性清理(防止内存泄漏) - if (streamBuffer.length > 1024 * 1024) { // 1MB - streamBuffer = streamBuffer.slice(-1024 * 64) // 只保留最后 64KB + if (streamBuffer.length > 1024 * 1024) { + // 1MB + streamBuffer = streamBuffer.slice(-1024 * 64) // 只保留最后 64KB } const lines = streamBuffer.split('\n') @@ -1990,50 +1991,51 @@ async function handleStreamGenerateContent(req, res) { // 提取 usage 数据 try { - const chunkStr = chunk.toString() - streamBuffer += chunkStr - - // 如果 buffer 过大,进行保护性清理(防止内存泄漏) - if (streamBuffer.length > 1024 * 1024) { // 1MB - streamBuffer = streamBuffer.slice(-1024 * 64) // 只保留最后 64KB - } + const chunkStr = chunk.toString() + streamBuffer += chunkStr - const lines = streamBuffer.split('\n') - // 保留最后一行(可能不完整) - streamBuffer = lines.pop() || '' - - for (const line of lines) { - // 只处理可能包含数据的行 - if (!line.trim() || !line.startsWith('data:')) { - continue - } - - try { - // ��试解析 SSE 行 - const parsed = parseSSELine(line) - - // 检查各种可能的 usage 位置 - let extractedUsage = null - - if (parsed.type === 'data') { - if (parsed.data.response?.usageMetadata) { - extractedUsage = parsed.data.response.usageMetadata - } else if (parsed.data.usageMetadata) { - extractedUsage = parsed.data.usageMetadata - } - } - - if (extractedUsage) { - totalUsage = extractedUsage - logger.debug('📊 Captured Gemini usage data:', totalUsage) - } - } catch (parseError) { - // 解析失败忽略,可能是非 JSON 数据 - } - } - } catch (error) { - logger.warn('⚠️ Error extracting usage data:', error.message) + // 如果 buffer 过大,进行保护性清理(防止内存泄漏) + if (streamBuffer.length > 1024 * 1024) { + // 1MB + streamBuffer = streamBuffer.slice(-1024 * 64) // 只保留最后 64KB } + + const lines = streamBuffer.split('\n') + // 保留最后一行(可能不完整) + streamBuffer = lines.pop() || '' + + for (const line of lines) { + // 只处理可能包含数据的行 + if (!line.trim() || !line.startsWith('data:')) { + continue + } + + try { + // ��试解析 SSE 行 + const parsed = parseSSELine(line) + + // 检查各种可能的 usage 位置 + let extractedUsage = null + + if (parsed.type === 'data') { + if (parsed.data.response?.usageMetadata) { + extractedUsage = parsed.data.response.usageMetadata + } else if (parsed.data.usageMetadata) { + extractedUsage = parsed.data.usageMetadata + } + } + + if (extractedUsage) { + totalUsage = extractedUsage + logger.debug('📊 Captured Gemini usage data:', totalUsage) + } + } catch (parseError) { + // 解析失败忽略,可能是非 JSON 数据 + } + } + } catch (error) { + logger.warn('⚠️ Error extracting usage data:', error.message) + } } catch (error) { logger.error('Error processing stream chunk:', error) } @@ -2789,24 +2791,24 @@ async function handleStandardStreamGenerateContent(req, res) { res.write(outputChunk) } - try { - const usageSource = - processedPayload && processedPayload !== '[DONE]' ? processedPayload : dataPayload + try { + const usageSource = + processedPayload && processedPayload !== '[DONE]' ? processedPayload : dataPayload - if (!usageSource || !usageSource.includes('usageMetadata')) { - return - } - - const usageObj = JSON.parse(usageSource) - const usage = usageObj.usageMetadata || usageObj.response?.usageMetadata || usageObj.usage - - if (usage && typeof usage === 'object') { - totalUsage = usage - logger.debug('📊 Captured Gemini usage data (async):', totalUsage) - } - } catch (error) { - // 提取用量失败时忽略 + if (!usageSource || !usageSource.includes('usageMetadata')) { + return } + + const usageObj = JSON.parse(usageSource) + const usage = usageObj.usageMetadata || usageObj.response?.usageMetadata || usageObj.usage + + if (usage && typeof usage === 'object') { + totalUsage = usage + logger.debug('📊 Captured Gemini usage data (async):', totalUsage) + } + } catch (error) { + // 提取用量失败时忽略 + } } streamResponse.on('data', (chunk) => {