@@ -7,7 +7,7 @@ const path = require('path');
77const MAX_TOKENS_PER_REQUEST = 80000 ; // Conservative limit for Gemini 2.5 Flash
88const CHARS_PER_TOKEN = 4 ; // Rough estimation
99//const MAX_CHARS_PER_CHUNK = MAX_TOKENS_PER_REQUEST * CHARS_PER_TOKEN;
10- const MAX_CHUNKS = 3 ; // Limit to prevent excessive API calls
10+ const MAX_CHUNKS = 5 ; // Limit to prevent excessive API calls
1111
1212/**
1313 * Estimate token count for text (rough approximation)
@@ -52,46 +52,69 @@ function chunkDiffByFiles(diffContent) {
5252 const lines = diffContent . split ( '\n' ) ;
5353 let currentChunk = '' ;
5454 let currentFile = '' ;
55- let tokenCount = 0 ;
55+ let currentChunkTokenCount = 0 ;
56+ const PROMPT_OVERHEAD = 2000 ; // Reserve tokens for prompt overhead
57+ const MAX_CHUNK_TOKENS = MAX_TOKENS_PER_REQUEST - PROMPT_OVERHEAD ;
5658
57- for ( const line of lines ) {
58- // Check if this is a new file header
59- //console.error(`Line is estimated at ${estimateTokens(line)} tokens`);
60- tokenCount += estimateTokens ( line ) ;
61- //console.error(`Total tokens for this chunk is ${tokenCount}`);
62- if ( line . startsWith ( 'diff --git' ) || line . startsWith ( '+++' ) || line . startsWith ( '---' ) ) {
63- // If we have content and it's getting large, save current chunk
64- if ( currentChunk && tokenCount > MAX_TOKENS_PER_REQUEST ) {
65- fileChunks . push ( {
66- content : currentChunk . trim ( ) ,
67- file : currentFile ,
68- type : 'file-chunk'
69- } ) ;
70- currentChunk = '' ;
71- tokenCount = 0 ;
59+ for ( let i = 0 ; i < lines . length ; i ++ ) {
60+ const line = lines [ i ] ;
61+ const lineTokens = estimateTokens ( line ) ;
62+ const isNewFile = line . startsWith ( 'diff --git' ) ;
63+ const isFileHeader = line . startsWith ( '+++' ) || line . startsWith ( '---' ) ;
64+
65+ // Check if we need to split current chunk before adding this line
66+ if ( currentChunk && ( currentChunkTokenCount + lineTokens ) > MAX_CHUNK_TOKENS ) {
67+ // Current chunk is getting too large, save it
68+ fileChunks . push ( {
69+ content : currentChunk . trim ( ) ,
70+ file : currentFile ,
71+ type : 'file-chunk'
72+ } ) ;
73+ console . error ( `Chunk ${ fileChunks . length } saved: ${ currentChunkTokenCount } tokens for ${ currentFile || 'unknown' } ` ) ;
74+ currentChunk = '' ;
75+ currentChunkTokenCount = 0 ;
76+ }
77+
78+ // Handle new file boundaries
79+ if ( isNewFile ) {
80+ // Extract filename from next lines
81+ // Look ahead for +++ line
82+ for ( let j = i + 1 ; j < Math . min ( i + 10 , lines . length ) ; j ++ ) {
83+ if ( lines [ j ] . startsWith ( '+++' ) ) {
84+ currentFile = lines [ j ] . replace ( '+++ b/' , '' ) . replace ( '+++ a/' , '' ) ;
85+ break ;
86+ }
7287 }
88+ }
89+
90+ // Add line to current chunk
91+ currentChunk += line + '\n' ;
92+ currentChunkTokenCount += lineTokens ;
93+
94+ // If a single line is too large, split it
95+ if ( lineTokens > MAX_CHUNK_TOKENS && currentChunk . length > 100 ) {
96+ // Remove the line from current chunk
97+ currentChunk = currentChunk . substring ( 0 , currentChunk . length - line . length - 1 ) ;
98+ currentChunkTokenCount -= lineTokens ;
7399
74- // Start new chunk
75- currentChunk = line + '\n' ;
76-
77-
78- // Extract filename for reference
79- if ( line . startsWith ( '+++' ) ) {
80- currentFile = line . replace ( '+++ b/' , '' ) . replace ( '+++ a/' , '' ) ;
81- }
82- if ( tokenCount > MAX_TOKENS_PER_REQUEST ) {
83- const split_chunk = splitStringByTokens ( currentChunk , MAX_TOKENS_PER_REQUEST ) ;
84- currentChunk = split_chunk [ split_chunk . length - 1 ] ;
85- for ( let i = 0 ; i < split_chunk . length - 1 ; i ++ ) {
86- fileChunks . push ( {
87- content : split_chunk [ i ] . trim ( ) ,
88- file : currentFile ,
89- type : 'file-chunk'
90- } ) ;
100+ // Split the large line
101+ const splitChunks = splitStringByTokens ( line , MAX_CHUNK_TOKENS ) ;
102+ for ( let j = 0 ; j < splitChunks . length ; j ++ ) {
103+ if ( j > 0 ) {
104+ // Save previous chunk if it has content
105+ if ( currentChunk . trim ( ) ) {
106+ fileChunks . push ( {
107+ content : currentChunk . trim ( ) ,
108+ file : currentFile ,
109+ type : 'file-chunk'
110+ } ) ;
111+ currentChunk = '' ;
112+ currentChunkTokenCount = 0 ;
113+ }
91114 }
115+ currentChunk = splitChunks [ j ] + '\n' ;
116+ currentChunkTokenCount = estimateTokens ( currentChunk ) ;
92117 }
93- } else {
94- currentChunk += line + '\n' ;
95118 }
96119 }
97120
@@ -102,6 +125,7 @@ function chunkDiffByFiles(diffContent) {
102125 file : currentFile ,
103126 type : 'file-chunk'
104127 } ) ;
128+ console . error ( `Final chunk ${ fileChunks . length } saved: ${ currentChunkTokenCount } tokens for ${ currentFile || 'unknown' } ` ) ;
105129 }
106130
107131 return fileChunks ;
@@ -149,44 +173,67 @@ ${diffContent}`;
149173}
150174
151175/**
152- * Call Gemini API with the given prompt
176+ * Call Gemini API with the given prompt (with retry logic for rate limits)
153177 */
154- async function callGeminiAPI ( prompt , apiKey ) {
178+ async function callGeminiAPI ( prompt , apiKey , retryCount = 0 ) {
179+ const maxRetries = 3 ;
180+ const baseDelay = 1000 ; // 1 second base delay
181+
155182 console . error ( `Sending prompt with an estimated ${ estimateTokens ( prompt ) } tokens` ) ;
156- const response = await fetch ( `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent?key=${ apiKey } ` , {
157- method : 'POST' ,
158- headers : { 'Content-Type' : 'application/json' } ,
159- body : JSON . stringify ( {
160- contents : [ {
161- parts : [ {
162- text : prompt
163- } ]
164- } ] ,
165- generationConfig : {
166- temperature : 0.7 ,
167- topK : 40 ,
168- topP : 0.95 ,
169- maxOutputTokens : 8192 ,
170- }
171- } )
172- } ) ;
183+
184+ try {
185+ const response = await fetch ( `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent?key=${ apiKey } ` , {
186+ method : 'POST' ,
187+ headers : { 'Content-Type' : 'application/json' } ,
188+ body : JSON . stringify ( {
189+ contents : [ {
190+ parts : [ {
191+ text : prompt
192+ } ]
193+ } ] ,
194+ generationConfig : {
195+ temperature : 0.7 ,
196+ topK : 40 ,
197+ topP : 0.95 ,
198+ maxOutputTokens : 8192 ,
199+ }
200+ } )
201+ } ) ;
173202
174- if ( ! response . ok ) {
175- const errorText = await response . text ( ) ;
176- throw new Error ( `Gemini API request failed with status ${ response . status } : ${ errorText } ` ) ;
177- }
203+ // Handle rate limiting (429) with exponential backoff
204+ if ( response . status === 429 && retryCount < maxRetries ) {
205+ const delay = baseDelay * Math . pow ( 2 , retryCount ) ; // Exponential backoff: 1s, 2s, 4s
206+ console . error ( `Rate limit hit, retrying in ${ delay } ms (attempt ${ retryCount + 1 } /${ maxRetries } )` ) ;
207+ await sleep ( delay ) ;
208+ return await callGeminiAPI ( prompt , apiKey , retryCount + 1 ) ;
209+ }
178210
179- const json = await response . json ( ) ;
180-
181- if ( ! json . candidates || ! json . candidates [ 0 ] ) {
182- throw new Error ( 'Invalid response from Gemini API' ) ;
183- }
211+ if ( ! response . ok ) {
212+ const errorText = await response . text ( ) ;
213+ throw new Error ( `Gemini API request failed with status ${ response . status } : ${ errorText } ` ) ;
214+ }
184215
185- if ( ! json . candidates [ 0 ] . content || ! json . candidates [ 0 ] . content . parts || ! json . candidates [ 0 ] . content . parts [ 0 ] || ! json . candidates [ 0 ] . content . parts [ 0 ] . text ) {
186- throw new Error ( 'Invalid response structure from Gemini API - missing content' ) ;
187- }
216+ const json = await response . json ( ) ;
217+
218+ if ( ! json . candidates || ! json . candidates [ 0 ] ) {
219+ throw new Error ( 'Invalid response from Gemini API' ) ;
220+ }
221+
222+ if ( ! json . candidates [ 0 ] . content || ! json . candidates [ 0 ] . content . parts || ! json . candidates [ 0 ] . content . parts [ 0 ] || ! json . candidates [ 0 ] . content . parts [ 0 ] . text ) {
223+ throw new Error ( 'Invalid response structure from Gemini API - missing content' ) ;
224+ }
188225
189- return json . candidates [ 0 ] . content . parts [ 0 ] . text ;
226+ return json . candidates [ 0 ] . content . parts [ 0 ] . text ;
227+ } catch ( error ) {
228+ // If it's a network error and we have retries left, retry with exponential backoff
229+ if ( retryCount < maxRetries && ( error . message . includes ( 'fetch' ) || error . message . includes ( 'network' ) ) ) {
230+ const delay = baseDelay * Math . pow ( 2 , retryCount ) ;
231+ console . error ( `Network error, retrying in ${ delay } ms (attempt ${ retryCount + 1 } /${ maxRetries } ): ${ error . message } ` ) ;
232+ await sleep ( delay ) ;
233+ return await callGeminiAPI ( prompt , apiKey , retryCount + 1 ) ;
234+ }
235+ throw error ;
236+ }
190237}
191238
192239/**
@@ -201,12 +248,13 @@ async function processChunks(chunks, apiKey) {
201248
202249 // Multiple chunks - process each and combine
203250 const chunkResults = [ ] ;
251+ const CHUNK_DELAY = 500 ; // 500ms delay between chunks (reduced from 5s)
204252
205253 for ( let i = 0 ; i < Math . min ( chunks . length , MAX_CHUNKS ) ; i ++ ) {
206254 const chunk = chunks [ i ] ;
207255 if ( i > 0 ) {
208- // sleep for 3 seconds
209- sleep ( 5 * 1000 ) ;
256+ // Small delay between chunks to avoid rate limits (reduced from 5s to 500ms)
257+ await sleep ( CHUNK_DELAY ) ;
210258 }
211259 console . error ( `Processing chunk ${ i + 1 } /${ Math . min ( chunks . length , MAX_CHUNKS ) } (${ chunk . file || 'unknown file' } )` ) ;
212260
@@ -225,7 +273,10 @@ async function processChunks(chunks, apiKey) {
225273 if ( chunkResults . length === 0 ) {
226274 throw new Error ( 'Failed to process any chunks' ) ;
227275 }
228- sleep ( 5 * 1000 ) ;
276+
277+ // Small delay before combining (reduced from 5s to 500ms)
278+ await sleep ( CHUNK_DELAY ) ;
279+
229280 // Combine results from multiple chunks
230281 const combinedPrompt = `Combine these pull request descriptions into a single, coherent PR description. Use the same format:
231282
0 commit comments