rltgjqm
Version:
Google Gemini API를 사용하여 자연어로 Git 명령어를 생성하는 CLI 도구 - 인터랙티브 메뉴와 자동 API 키 설정 지원
264 lines (235 loc) • 8.78 kB
JavaScript
const axios = require('axios');
const chalk = require('chalk');
/**
* Gemini API 관련 함수들
*/
class GeminiService {
constructor() {
// 최신 Gemini 1.5 Flash 모델 사용
this.modelName = 'gemini-1.5-flash';
this.baseUrl = 'https://generativelanguage.googleapis.com/v1beta/models/' + this.modelName + ':generateContent';
}
/**
* API 키 가져오기 (config.js에서 관리)
*/
getApiKey() {
try {
const config = require('./config');
return config.getApiKey();
} catch (error) {
// config.js를 불러올 수 없으면 환경변수에서 가져오기
return process.env.GEMINI_API_KEY;
}
}
/**
* Gemini API를 호출하여 Git 명령어 생성
* @param {string} prompt - 완성된 프롬프트 (promptTemplate.buildPrompt 결과)
* @returns {Promise<{text: string, usage: object}>} 생성된 응답 텍스트와 정확한 사용량 정보
*/
async generateCommand(prompt) {
try {
console.log(chalk.white('🤖 Gemini API 호출 중...'));
console.log(chalk.white(`모델: ${this.modelName}`));
const apiKey = this.getApiKey();
if (!apiKey) {
throw new Error('GEMINI_API_KEY가 설정되지 않았습니다.');
}
const requestData = {
contents: [
{
parts: [
{
text: prompt
}
]
}
],
generationConfig: {
temperature: 0.1,
topK: 40,
topP: 0.95,
maxOutputTokens: 2048,
stopSequences: []
},
safetySettings: [
{
category: "HARM_CATEGORY_HARASSMENT",
threshold: "BLOCK_NONE"
},
{
category: "HARM_CATEGORY_HATE_SPEECH",
threshold: "BLOCK_NONE"
},
{
category: "HARM_CATEGORY_SEXUALLY_EXPLICIT",
threshold: "BLOCK_NONE"
},
{
category: "HARM_CATEGORY_DANGEROUS_CONTENT",
threshold: "BLOCK_NONE"
}
]
};
const response = await axios.post(
`${this.baseUrl}?key=${apiKey}`,
requestData,
{
headers: {
'Content-Type': 'application/json',
},
timeout: 30000 // 30초 타임아웃
}
);
// 응답 처리
if (response.data && response.data.candidates && response.data.candidates.length > 0) {
const candidate = response.data.candidates[0];
// 안전성 등급 확인
if (candidate.finishReason === 'SAFETY') {
throw new Error('안전성 필터로 인해 응답이 차단되었습니다. 다른 방식으로 질문해보세요.');
}
if (candidate.finishReason === 'RECITATION') {
throw new Error('저작권 문제로 응답이 차단되었습니다. 다른 방식으로 질문해보세요.');
}
if (!candidate.content || !candidate.content.parts || candidate.content.parts.length === 0) {
throw new Error('빈 응답을 받았습니다. 다시 시도해보세요.');
}
const generatedText = candidate.content.parts[0].text;
// 정확한 사용량 정보 추출 (Gemini API는 usageMetadata 제공)
const usageMetadata = response.data.usageMetadata || {};
const usage = {
prompt_tokens: usageMetadata.promptTokenCount || 0,
completion_tokens: usageMetadata.candidatesTokenCount || 0,
total_tokens: usageMetadata.totalTokenCount || 0
};
console.log(chalk.green('✅ API 응답 받음'));
console.log(chalk.white(`📊 실제 사용량: ${usage.prompt_tokens} → ${usage.completion_tokens} (총 ${usage.total_tokens} 토큰)`));
// 텍스트와 사용량 정보를 함께 반환
return {
text: generatedText,
usage: usage
};
} else {
throw new Error('API 응답 형식이 올바르지 않습니다.');
}
} catch (error) {
console.error(chalk.red('❌ Gemini API 호출 실패:'));
if (error.response) {
// HTTP 응답 오류
const status = error.response.status;
const data = error.response.data;
if (status === 400) {
console.error(chalk.red('요청 형식이 올바르지 않습니다.'));
if (data.error && data.error.message) {
console.error(chalk.red(`상세: ${data.error.message}`));
}
} else if (status === 401) {
console.error(chalk.red('API 키가 유효하지 않습니다. rltgjqm config로 API 키를 확인하세요.'));
} else if (status === 403) {
console.error(chalk.red('API 접근 권한이 없습니다. API 키 권한을 확인하세요.'));
} else if (status === 404) {
console.error(chalk.red(`모델을 찾을 수 없습니다. 현재 모델: ${this.modelName}`));
console.error(chalk.yellow('💡 최신 API 키를 사용하고 있는지 확인하세요.'));
} else if (status === 429) {
console.error(chalk.red('API 호출 한도를 초과했습니다. 잠시 후 다시 시도하세요.'));
} else if (status === 500) {
console.error(chalk.red('Gemini API 서버 오류입니다. 잠시 후 다시 시도하세요.'));
} else {
console.error(chalk.red(`HTTP ${status}: ${error.message}`));
}
} else if (error.code === 'ECONNABORTED') {
console.error(chalk.red('요청 시간 초과. 인터넷 연결을 확인하세요.'));
} else if (error.code === 'ENOTFOUND') {
console.error(chalk.red('인터넷 연결을 확인하세요.'));
} else {
console.error(chalk.red(error.message));
}
throw error;
}
}
/**
* 사용 가능한 모델 목록 가져오기
*/
async getAvailableModels() {
try {
const apiKey = this.getApiKey();
if (!apiKey) {
throw new Error('API 키가 필요합니다.');
}
const response = await axios.get(
`https://generativelanguage.googleapis.com/v1beta/models?key=${apiKey}`,
{
timeout: 10000
}
);
if (response.data && response.data.models) {
return response.data.models
.filter(model => model.name.includes('gemini'))
.map(model => ({
name: model.name.replace('models/', ''),
displayName: model.displayName,
description: model.description
}));
}
return [];
} catch (error) {
console.error(chalk.red('모델 목록을 가져올 수 없습니다:'), error.message);
return [];
}
}
/**
* API 키 유효성 검사
* @returns {Promise<boolean>} API 키 유효성 여부
*/
async validateApiKey() {
try {
console.log(chalk.white('🔑 API 키 유효성 검사 중...'));
const testPrompt = `
간단한 테스트입니다.
"git status" 명령어만 출력해주세요.
`;
await this.generateCommand(testPrompt);
console.log(chalk.green('✅ API 키가 유효합니다.'));
return true;
} catch (error) {
console.log(chalk.red('❌ API 키가 유효하지 않습니다.'));
return false;
}
}
/**
* API 연결 상태 확인
* @returns {Promise<Object>} 연결 상태 정보
*/
async checkConnection() {
try {
const startTime = Date.now();
const response = await axios.get('https://generativelanguage.googleapis.com', {
timeout: 5000
});
const endTime = Date.now();
return {
connected: true,
responseTime: endTime - startTime,
status: response.status
};
} catch (error) {
return {
connected: false,
error: error.message
};
}
}
/**
* 요청 제한 확인 (일일 한도 등)
* @returns {Object} 사용량 정보
*/
getUsageInfo() {
// 실제 Gemini API에서는 사용량 정보를 제공하지 않으므로
// 클라이언트 측에서 추정하는 정보만 제공
return {
note: 'Gemini API는 사용량 정보를 직접 제공하지 않습니다.',
recommendation: 'API 키 사용량은 Google Cloud Console에서 확인하세요.',
currentModel: this.modelName
};
}
}
module.exports = new GeminiService();