UNPKG

pushscript

Version:

AI-powered Git workflow automation with conventional commits, vulnerability scanning, and multi-provider LLM support

419 lines (364 loc) 12.7 kB
/** * Gemini Token Manager - Production-ready implementation for Gemini 2.0 Flash * Based on official rate limits from https://ai.google.dev/gemini-api/docs/rate-limits */ /** * Production Token Manager for Gemini API * Handles accurate token estimation and request validation for Gemini 2.0 Flash */ export class GeminiTokenManager { // Official rate limits from Gemini API documentation (2025) static RATE_LIMITS = { '2.0-flash': { free: { rpm: 15, tpm: 1_000_000, rpd: 1_500 }, tier1: { rpm: 2_000, tpm: 4_000_000, }, tier2: { rpm: 10_000, tpm: 10_000_000, }, tier3: { rpm: 30_000, tpm: 30_000_000, } } }; constructor(tier = 'free', safetyFactor = 0.95) { this.tier = tier; this.safetyFactor = safetyFactor; } /** * Accurate token estimation for Gemini 2.0 Flash * Based on observed tokenization patterns * @param {string} text - Text to estimate tokens for * @returns {number} Estimated token count */ estimateTokens(text) { if (!text) return 0; // Gemini tokenization patterns from testing const CHARS_PER_TOKEN = 3.2; // Average observed ratio const CODE_MULTIPLIER = 1.3; // Code requires more tokens const PUNCTUATION_PENALTY = 0.05; // Additional tokens for punctuation let tokenCount = text.length / CHARS_PER_TOKEN; // Code detection and adjustment if (this.detectCode(text)) { tokenCount *= CODE_MULTIPLIER; } // Account for special characters and punctuation const specialChars = (text.match(/[{}[\]()\\.,:;?!]/g) || []).length; tokenCount += specialChars * PUNCTUATION_PENALTY; return Math.ceil(tokenCount); } /** * Validates if a request is within rate limits * @param {string} content - Content to validate * @returns {Object} Validation result */ validateRequest(content) { const estimatedTokens = this.estimateTokens(content); const currentLimit = GeminiTokenManager.RATE_LIMITS['2.0-flash'][this.tier]; const safeTPMLimit = Math.floor(currentLimit.tpm * this.safetyFactor); return { valid: estimatedTokens <= safeTPMLimit, estimatedTokens: estimatedTokens, limit: safeTPMLimit, exceededBy: Math.max(0, estimatedTokens - safeTPMLimit), model: '2.0-flash', tier: this.tier }; } /** * Detects if content is primarily code * @param {string} text - Text to analyze * @returns {boolean} Whether text is primarily code */ detectCode(text) { const codeIndicators = [ /function\s+\w+\s*\(/, /class\s+\w+/, /import\s+(?:\{[^}]+\}|.*?)\s+from/, /export\s+(?:default\s+)?(?:class|function|const)/, /(?:const|let|var)\s+\w+\s*=/, /if\s*\([^)]+\)\s*\{/, /for\s*\([^)]+\)\s*\{/, /console\.\w+\(/, /^\s*[\{\}\[\]\(\)]$/m ]; const codeSignals = codeIndicators.filter(pattern => pattern.test(text)).length; const totalLines = text.split('\n').length; const codeRatio = codeSignals / Math.max(totalLines, 1); return codeRatio > 0.2; // If more than 20% of patterns match, consider it code } } /** * Advanced Diff Optimization for Gemini API */ export class GeminiDiffOptimizer { constructor(tier = 'free') { this.tokenManager = new GeminiTokenManager(tier); } /** * Optimizes git diff for Gemini API consumption * @param {string} diff - Git diff to optimize * @param {Object} options - Optimization options * @returns {Object} Optimization result */ optimizeDiff(diff, options = {}) { const validation = this.tokenManager.validateRequest(diff); if (validation.valid) { return { content: diff, optimized: false, valid: validation.valid, estimatedTokens: validation.estimatedTokens, limit: validation.limit, exceededBy: validation.exceededBy, model: validation.model, tier: validation.tier }; } // Progressive optimization strategy let optimizedDiff = diff; let iterations = 0; const maxIterations = 5; while (!this.tokenManager.validateRequest(optimizedDiff).valid && iterations < maxIterations) { optimizedDiff = this.applySingleOptimization(optimizedDiff, iterations); iterations++; } const finalValidation = this.tokenManager.validateRequest(optimizedDiff); return { content: optimizedDiff, optimized: true, iterations: iterations, valid: finalValidation.valid, estimatedTokens: finalValidation.estimatedTokens, limit: finalValidation.limit, exceededBy: finalValidation.exceededBy, model: finalValidation.model, tier: finalValidation.tier }; } /** * Applies optimization strategies progressively * @param {string} diff - Diff to optimize * @param {number} iteration - Iteration number * @returns {string} Optimized diff */ applySingleOptimization(diff, iteration) { switch (iteration) { case 0: // Remove context lines return diff.replace(/^ .*$/gm, ''); case 1: // Remove file mode information return diff.replace(/^old mode \d+\nnew mode \d+$/gm, ''); case 2: // Shorten file headers return diff.replace(/^diff --git a\/(.*?) b\/.*?$/gm, '--- $1 ---'); case 3: // Remove index lines and merge headers return diff.replace(/^index [0-9a-f]+\.\.[0-9a-f]+.*$/gm, '') .replace(/^(\+{3}|-{3}) [ab]\//gm, '$1 '); case 4: // Keep +/- lines, hunk headers, and some context for better understanding return diff.split('\n') .filter(line => line.startsWith('@') || line.startsWith('+') || line.startsWith('-') || line.startsWith(' ')) .join('\n'); default: // Last resort: truncate with indicator const maxLength = Math.floor(diff.length * 0.8); return diff.substring(0, maxLength) + '\n... [diff truncated due to size] ...'; } } } /** * Rate limit tracker for request management */ export class GeminiRateManager { constructor(tokenManager) { this.tokenManager = tokenManager; this.requestCounts = { minute: { count: 0, resetTime: Date.now() + 60000 }, day: { count: 0, resetTime: Date.now() + 86400000 } }; } /** * Checks if a request can be made within rate limits * @param {number} estimatedTokens - Number of tokens to check * @returns {Object} Rate check result */ canMakeRequest(estimatedTokens) { this.resetCounters(); const limits = GeminiTokenManager.RATE_LIMITS['2.0-flash'][this.tokenManager.tier]; // Check RPM if (this.requestCounts.minute.count >= limits.rpm) { return { allowed: false, reason: 'RPM_EXCEEDED', waitTime: this.requestCounts.minute.resetTime - Date.now() }; } // Check RPD if applicable if (limits.rpd && this.requestCounts.day.count >= limits.rpd) { return { allowed: false, reason: 'RPD_EXCEEDED', waitTime: this.requestCounts.day.resetTime - Date.now() }; } return { allowed: true }; } /** * Records a successful request */ recordRequest() { this.resetCounters(); this.requestCounts.minute.count++; this.requestCounts.day.count++; } /** * Resets counters if time window has passed */ resetCounters() { const now = Date.now(); if (now >= this.requestCounts.minute.resetTime) { this.requestCounts.minute.count = 0; this.requestCounts.minute.resetTime = now + 60000; } if (now >= this.requestCounts.day.resetTime) { this.requestCounts.day.count = 0; this.requestCounts.day.resetTime = now + 86400000; } } } /** * Creates a configured Gemini manager instance * @param {string} tier - The tier to use ('free', 'tier1', 'tier2', 'tier3') * @returns {Object} Manager object with methods */ export function createGeminiManager(tier = 'free') { const tokenManager = new GeminiTokenManager(tier); const diffOptimizer = new GeminiDiffOptimizer(tier); const rateManager = new GeminiRateManager(tokenManager); return { /** * Validates a request * @param {string} content - Content to validate * @returns {Object} Validation result */ validateRequest: (content) => tokenManager.validateRequest(content), /** * Optimizes a diff * @param {string} diff - Diff to optimize * @param {Object} options - Optimization options * @returns {Object} Optimization result */ optimizeDiff: (diff, options = {}) => diffOptimizer.optimizeDiff(diff, options), /** * Checks rate limits * @param {number} estimatedTokens - Tokens to check * @returns {Object} Rate check result */ checkRateLimit: (estimatedTokens) => rateManager.canMakeRequest(estimatedTokens), /** * Records a request */ recordRequest: () => rateManager.recordRequest() }; } /** * Provides backward compatibility with the previous token utility API * Used for the existing code that may still reference these functions * @param {string} text - Text to estimate token count for * @returns {number} Estimated token count */ export function estimateTokenCount(text) { const tokenManager = new GeminiTokenManager(); return tokenManager.estimateTokens(text); } /** * Backward compatibility function for optimizing diffs * @param {string} diff - Git diff to optimize * @param {number} maxTokens - Maximum tokens to allow * @returns {Object} Optimization result in the old format */ export function optimizeDiffForLLM(diff, maxTokens = 1500) { const optimizer = new GeminiDiffOptimizer(); const result = optimizer.optimizeDiff(diff); return { content: result.content, isOptimized: result.optimized, estimatedTokens: result.estimatedTokens }; } /** * Backward compatibility function for optimizing request parameters * @param {string} provider - LLM provider name * @param {Object} content - Content object with metadata * @returns {Object} Optimized request parameters */ export function optimizeRequestParams(provider, content) { const params = { temperature: 0.1, maxTokens: 150 }; if (provider === 'gemini') { params.temperature = 0; const tokenManager = new GeminiTokenManager(); const estimatedTokens = typeof content.estimatedTokens === 'number' ? content.estimatedTokens : tokenManager.estimateTokens(content.content || ''); if (estimatedTokens > 8000) { params.needsMoreOptimization = true; } } return params; } // Export a CLI if this file is run directly if (process.argv[1].endsWith('token-utils.js')) { const args = process.argv.slice(2); const command = args[0]; if (command === 'analyze') { const filePath = args[1]; if (!filePath) { console.error('Error: Please provide a file path to analyze'); process.exit(1); } try { const fs = require('fs'); const content = fs.readFileSync(filePath, 'utf8'); const tokenManager = new GeminiTokenManager(); const tokens = tokenManager.estimateTokens(content); console.log(`File: ${filePath}`); console.log(`Size: ${content.length} bytes`); console.log(`Estimated tokens: ${tokens}`); // Validate against limits const validation = tokenManager.validateRequest(content); console.log(`Within free tier limits: ${validation.valid ? 'Yes' : 'No'}`); if (!validation.valid) { console.log(`Exceeds limit by: ${validation.exceededBy} tokens`); // Demonstrate optimization const optimizer = new GeminiDiffOptimizer(); const optimized = optimizer.optimizeDiff(content); console.log(`\nOptimization results:`); console.log(`- Applied ${optimized.iterations} optimization steps`); console.log(`- Reduced to ${optimized.estimatedTokens} tokens (${Math.round(optimized.estimatedTokens / tokens * 100)}%)`); console.log(`- Now within limits: ${optimized.valid ? 'Yes' : 'No'}`); } } catch (error) { console.error(`Error: ${error.message}`); process.exit(1); } } else { console.log('Gemini Token Manager CLI'); console.log('------------------------'); console.log('Commands:'); console.log(' analyze <file> - Analyze token count for a file'); } }