UNPKG

rltgjqm

Version:

Google Gemini API를 사용하여 자연어로 Git 명령어를 생성하는 CLI 도구 - 인터랙티브 메뉴와 자동 API 키 설정 지원

369 lines (311 loc) 12.5 kB
const fs = require('fs'); const path = require('path'); const os = require('os'); const chalk = require('chalk'); /** * API 사용량 추적 및 관리 클래스 */ class UsageTracker { constructor() { this.configDir = path.join(os.homedir(), '.rltgjqm'); this.usageFile = path.join(this.configDir, 'usage.json'); // 일일 한도 설정 this.limits = { 'chatgpt': { name: 'ChatGPT (OpenAI)', tier1: 2500000, // 2.5M 토큰/일 tier2: 2500000, // 2.5M 토큰/일 tier3: 10000000, // 10M 토큰/일 tier4: 10000000, // 10M 토큰/일 tier5: 10000000, // 10M 토큰/일 default: 2500000 // 기본값은 Tier 1-2 }, 'gemini': { name: 'Gemini (Google)', estimated: 1000000, // 추정 1M 토큰/일 (실제 한도 불명) default: 1000000 } }; this.ensureUsageFile(); } /** * 사용량 파일 확인 및 생성 */ ensureUsageFile() { if (!fs.existsSync(this.configDir)) { fs.mkdirSync(this.configDir, { recursive: true }); } if (!fs.existsSync(this.usageFile)) { this.resetDailyUsage(); } } /** * 현재 날짜 (YYYY-MM-DD 형식) */ getCurrentDate() { return new Date().toISOString().split('T')[0]; } /** * 사용량 데이터 읽기 */ readUsageData() { try { const data = fs.readFileSync(this.usageFile, 'utf-8'); return JSON.parse(data); } catch (error) { return this.getDefaultUsageData(); } } /** * 기본 사용량 데이터 */ getDefaultUsageData() { return { date: this.getCurrentDate(), chatgpt: { totalTokens: 0, promptTokens: 0, completionTokens: 0, requests: 0 }, gemini: { totalTokens: 0, // 정확한 총 토큰 수 (API 응답에서) promptTokens: 0, // 정확한 입력 토큰 수 completionTokens: 0, // 정확한 출력 토큰 수 estimatedTokens: 0, // 추정 토큰 수 (하위 호환성) requests: 0 } }; } /** * 사용량 데이터 저장 */ saveUsageData(data) { try { fs.writeFileSync(this.usageFile, JSON.stringify(data, null, 2), 'utf-8'); } catch (error) { console.error(chalk.red(`사용량 데이터 저장 실패: ${error.message}`)); } } /** * 일일 사용량 초기화 (날짜가 바뀐 경우) */ resetDailyUsage() { const data = this.getDefaultUsageData(); this.saveUsageData(data); return data; } /** * ChatGPT 사용량 기록 */ recordChatGPTUsage(usageInfo) { let data = this.readUsageData(); // 날짜가 바뀌면 초기화 if (data.date !== this.getCurrentDate()) { data = this.resetDailyUsage(); } if (usageInfo) { data.chatgpt.totalTokens += usageInfo.total_tokens || 0; data.chatgpt.promptTokens += usageInfo.prompt_tokens || 0; data.chatgpt.completionTokens += usageInfo.completion_tokens || 0; } data.chatgpt.requests += 1; this.saveUsageData(data); return data.chatgpt; } /** * Gemini 사용량 기록 (추정) - 하위 호환성용 */ recordGeminiUsage(promptText, responseText) { let data = this.readUsageData(); // 날짜가 바뀌면 초기화 if (data.date !== this.getCurrentDate()) { data = this.resetDailyUsage(); } // 대략적인 토큰 계산 (1토큰 ≈ 4글자) const estimatedPromptTokens = Math.ceil((promptText || '').length / 4); const estimatedResponseTokens = Math.ceil((responseText || '').length / 4); const estimatedTotal = estimatedPromptTokens + estimatedResponseTokens; data.gemini.estimatedTokens += estimatedTotal; data.gemini.requests += 1; this.saveUsageData(data); return { estimatedTokens: estimatedTotal, totalEstimatedTokens: data.gemini.estimatedTokens }; } /** * Gemini 사용량 기록 (정확한 API 데이터 사용) */ recordGeminiUsageWithActualData(actualUsage) { let data = this.readUsageData(); // 날짜가 바뀌면 초기화 if (data.date !== this.getCurrentDate()) { data = this.resetDailyUsage(); } // ChatGPT와 동일한 형식으로 정확한 토큰 수 저장 if (!data.gemini.totalTokens) data.gemini.totalTokens = 0; if (!data.gemini.promptTokens) data.gemini.promptTokens = 0; if (!data.gemini.completionTokens) data.gemini.completionTokens = 0; data.gemini.totalTokens += actualUsage.total_tokens || 0; data.gemini.promptTokens += actualUsage.prompt_tokens || 0; data.gemini.completionTokens += actualUsage.completion_tokens || 0; data.gemini.requests += 1; // 기존 추정값도 업데이트 (하위 호환성) data.gemini.estimatedTokens = data.gemini.totalTokens; this.saveUsageData(data); return { actualTokens: actualUsage.total_tokens, totalActualTokens: data.gemini.totalTokens, promptTokens: actualUsage.prompt_tokens, completionTokens: actualUsage.completion_tokens }; } /** * 현재 사용량 정보 가져오기 */ getCurrentUsage() { let data = this.readUsageData(); // 날짜가 바뀌면 초기화 if (data.date !== this.getCurrentDate()) { data = this.resetDailyUsage(); } return data; } /** * 이모티콘 게이지 생성 */ createUsageGauge(used, limit, length = 10) { const percentage = Math.min(used / limit, 1); const filled = Math.floor(percentage * length); const empty = length - filled; let gauge = ''; let color = chalk.green; // 색상 결정 if (percentage >= 0.9) color = chalk.red; else if (percentage >= 0.7) color = chalk.yellow; else if (percentage >= 0.5) color = chalk.white; // 게이지 생성 gauge += color('█'.repeat(filled)); gauge += chalk.white('░'.repeat(empty)); return gauge; } /** * 사용량 표시 (상세) */ displayUsageInfo(provider) { const usage = this.getCurrentUsage(); const limits = this.limits[provider]; if (!limits) return; console.log(chalk.cyan(`\n📊 ${limits.name} 일일 사용량:`)); if (provider === 'chatgpt') { const used = usage.chatgpt.totalTokens; const limit = limits.default; const remaining = Math.max(0, limit - used); const percentage = ((used / limit) * 100).toFixed(1); console.log(chalk.white(` 사용됨: ${used.toLocaleString()} 토큰`)); console.log(chalk.white(` 남은량: ${remaining.toLocaleString()} 토큰`)); console.log(chalk.white(` 요청수: ${usage.chatgpt.requests}회`)); console.log(chalk.white(` 사용률: ${percentage}%`)); console.log(` 게이지: ${this.createUsageGauge(used, limit)} ${percentage}%`); if (percentage >= 90) { console.log(chalk.red(' ⚠️ 일일 한도에 거의 도달했습니다!')); } else if (percentage >= 70) { console.log(chalk.yellow(' ⚠️ 일일 한도의 70%를 사용했습니다.')); } } else if (provider === 'gemini') { // 정확한 토큰 수가 있으면 사용, 없으면 추정값 사용 const hasActualTokens = usage.gemini.totalTokens !== undefined && usage.gemini.totalTokens > 0; const used = hasActualTokens ? usage.gemini.totalTokens : usage.gemini.estimatedTokens; const limit = limits.default; const remaining = Math.max(0, limit - used); const percentage = ((used / limit) * 100).toFixed(1); if (hasActualTokens) { console.log(chalk.white(` 실제 사용: ${used.toLocaleString()} 토큰`)); console.log(chalk.white(` 입력: ${usage.gemini.promptTokens?.toLocaleString() || 0} / 출력: ${usage.gemini.completionTokens?.toLocaleString() || 0}`)); console.log(chalk.white(` 남은량: ${remaining.toLocaleString()} 토큰`)); console.log(chalk.white(` 요청수: ${usage.gemini.requests}회`)); console.log(chalk.white(` 사용률: ${percentage}%`)); console.log(` 게이지: ${this.createUsageGauge(used, limit)} ${percentage}%`); console.log(chalk.green(' ✅ 정확한 사용량 데이터')); } else { console.log(chalk.white(` 추정 사용: ${used.toLocaleString()} 토큰`)); console.log(chalk.white(` 추정 남은량: ${remaining.toLocaleString()} 토큰`)); console.log(chalk.white(` 요청수: ${usage.gemini.requests}회`)); console.log(chalk.white(` 추정 사용률: ${percentage}%`)); console.log(` 게이지: ${this.createUsageGauge(used, limit)} ${percentage}%`); console.log(chalk.yellow(' 💡 추정값입니다 (정확한 사용량은 Google AI Studio에서 확인)')); } } } /** * 간단한 사용량 표시 (한 줄) */ displayUsageBrief(provider) { const usage = this.getCurrentUsage(); const limits = this.limits[provider]; if (!limits) return ''; if (provider === 'chatgpt') { const used = usage.chatgpt.totalTokens; const limit = limits.default; const percentage = ((used / limit) * 100).toFixed(1); const gauge = this.createUsageGauge(used, limit, 8); return `📊 ${gauge} ${percentage}% (${usage.chatgpt.requests}회)`; } else if (provider === 'gemini') { // 정확한 토큰 수가 있으면 사용, 없으면 추정값 사용 const hasActualTokens = usage.gemini.totalTokens !== undefined && usage.gemini.totalTokens > 0; const used = hasActualTokens ? usage.gemini.totalTokens : usage.gemini.estimatedTokens; const limit = limits.default; const percentage = ((used / limit) * 100).toFixed(1); const gauge = this.createUsageGauge(used, limit, 8); const accuracy = hasActualTokens ? '정확' : '추정'; return `📊 ${gauge} ${percentage}% (${usage.gemini.requests}회, ${accuracy})`; } return ''; } /** * 명령어 실행 후 사용량 요약 */ displayPostCommandUsage(provider, usageInfo) { console.log(chalk.white('\n─────────────────────────────────────────────')); console.log(chalk.white('📊 API 사용량 정보')); console.log(chalk.white('─────────────────────────────────────────────')); if (provider === 'chatgpt' && usageInfo) { console.log(chalk.green(`✅ 이번 요청: ${(usageInfo.total_tokens || 0).toLocaleString()} 토큰`)); console.log(chalk.white(` 프롬프트: ${(usageInfo.prompt_tokens || 0).toLocaleString()} 토큰`)); console.log(chalk.white(` 응답: ${(usageInfo.completion_tokens || 0).toLocaleString()} 토큰`)); } else if (provider === 'gemini' && usageInfo) { console.log(chalk.green(`✅ 이번 요청: 약 ${usageInfo.estimatedTokens.toLocaleString()} 토큰 (추정)`)); } this.displayUsageInfo(provider); } /** * 사용량 초기화 (디버깅용) */ resetUsage() { this.resetDailyUsage(); console.log(chalk.green('✅ 사용량이 초기화되었습니다.')); } /** * 사용량 통계 (디버깅용) */ getUsageStats() { const usage = this.getCurrentUsage(); return { date: usage.date, chatgpt: { totalTokens: usage.chatgpt.totalTokens, requests: usage.chatgpt.requests, averageTokensPerRequest: usage.chatgpt.requests > 0 ? Math.round(usage.chatgpt.totalTokens / usage.chatgpt.requests) : 0 }, gemini: { estimatedTokens: usage.gemini.estimatedTokens, requests: usage.gemini.requests, averageTokensPerRequest: usage.gemini.requests > 0 ? Math.round(usage.gemini.estimatedTokens / usage.gemini.requests) : 0 } }; } } module.exports = new UsageTracker();