soyailabs / app /gemini_client.py
GitHub Actions
Auto-deploy from GitHub Actions - 2025-12-12 06:47:16
0996863
"""
Google Gemini API ν΄λΌμ΄μ–ΈνŠΈ
"""
import os
import time
import requests
import google.generativeai as genai
from google.api_core import retry
from typing import Optional, List, Dict
import functools
import json
# Gemini API ν‚€ (ν™˜κ²½ λ³€μˆ˜ λ˜λŠ” λ°μ΄ν„°λ² μ΄μŠ€μ—μ„œ κ°€μ Έμ˜€κΈ°)
def get_gemini_api_key():
"""Gemini API ν‚€ κ°€μ Έμ˜€κΈ° (ν™˜κ²½ λ³€μˆ˜ μš°μ„ , μ—†μœΌλ©΄ DBμ—μ„œ)"""
# ν™˜κ²½ λ³€μˆ˜μ—μ„œ λ¨Όμ € 확인
api_key = os.getenv('GEMINI_API_KEY', '').strip()
if api_key:
print(f"[Gemini] ν™˜κ²½ λ³€μˆ˜μ—μ„œ API ν‚€ κ°€μ Έμ˜΄ (길이: {len(api_key)}자)")
return api_key
# DBμ—μ„œ κ°€μ Έμ˜€κΈ° (μˆœν™˜ μ°Έμ‘° λ°©μ§€λ₯Ό μœ„ν•΄ μ—¬κΈ°μ„œ μž„ν¬νŠΈ)
try:
from app.database import SystemConfig
api_key = SystemConfig.get_config('gemini_api_key', '').strip()
if api_key:
print(f"[Gemini] DBμ—μ„œ API ν‚€ κ°€μ Έμ˜΄ (길이: {len(api_key)}자)")
else:
print(f"[Gemini] DB에 API ν‚€κ°€ μ—†κ±°λ‚˜ λΉ„μ–΄μžˆμŒ")
return api_key
except Exception as e:
print(f"[Gemini] DBμ—μ„œ API ν‚€ 쑰회 μ‹€νŒ¨: {e}")
return ''
GEMINI_API_KEY = get_gemini_api_key()
# μ‚¬μš© κ°€λŠ₯ν•œ Gemini λͺ¨λΈ λͺ©λ‘ (μ΅œμ‹  버전 μš°μ„ )
AVAILABLE_GEMINI_MODELS = [
'gemini-2.0-flash-exp',
'gemini-1.5-pro-002',
'gemini-1.5-flash',
'gemini-1.5-pro-latest',
'gemini-1.5-flash-latest',
'gemini-pro',
'gemini-pro-vision'
]
class GeminiClient:
"""Google Gemini API ν΄λΌμ΄μ–ΈνŠΈ 클래슀"""
def __init__(self, api_key: Optional[str] = None):
"""Gemini ν΄λΌμ΄μ–ΈνŠΈ μ΄ˆκΈ°ν™”"""
if api_key:
self.api_key = api_key
else:
# μ΅œμ‹  API ν‚€ κ°€μ Έμ˜€κΈ° (DBμ—μ„œ λ™μ μœΌλ‘œ)
self.api_key = get_gemini_api_key()
if not self.api_key:
print("[Gemini] κ²½κ³ : GEMINI_API_KEYκ°€ μ„€μ •λ˜μ§€ μ•Šμ•˜μŠ΅λ‹ˆλ‹€. ν™˜κ²½ λ³€μˆ˜λ‚˜ 관리 νŽ˜μ΄μ§€μ—μ„œ μ„€μ •ν•˜μ„Έμš”.")
return
try:
# API ν‚€ μ„€μ • 및 νƒ€μž„μ•„μ›ƒ μ„€μ • (κΈ°λ³Έ 60초 -> 300초(5λΆ„)둜 증가)
# μ „μ—­ νƒ€μž„μ•„μ›ƒ μ„€μ •
self.request_timeout = 300 # 5λΆ„(300초) νƒ€μž„μ•„μ›ƒ
# μž¬μ‹œλ„ μ •μ±… μ„€μ •
self.retry_policy = retry.Retry(
initial=10.0, # 초기 λŒ€κΈ° μ‹œκ°„ (10초)
maximum=60.0, # μ΅œλŒ€ λŒ€κΈ° μ‹œκ°„ (60초)
multiplier=2.0, # λŒ€κΈ° μ‹œκ°„ 배수
deadline=600.0 # 전체 μž¬μ‹œλ„ κΈ°κ°„ (600초 = 10λΆ„)
)
# REST API μ‚¬μš©μ„ μœ„ν•œ μ„€μ •
# ν™˜κ²½ λ³€μˆ˜λ₯Ό 톡해 HTTP ν΄λΌμ΄μ–ΈνŠΈ νƒ€μž„μ•„μ›ƒ μ„€μ •
os.environ.setdefault('HTTPX_TIMEOUT', str(self.request_timeout))
os.environ.setdefault('GOOGLE_API_TIMEOUT', str(self.request_timeout))
# REST API μ—”λ“œν¬μΈνŠΈ μ„€μ • (v1 μ‚¬μš©)
# 2025λ…„ 4μ›”λΆ€ν„° v1betaκ°€ 지원 쀑단될 수 μžˆμœΌλ―€λ‘œ v1을 기본으둜 μ‚¬μš©
# ν•˜μ§€λ§Œ μ΅œμ‹  λͺ¨λΈ(gemini-1.5-pro, flash λ“±)은 v1betaμ—μ„œ λ¨Όμ € 지원될 수 있음
# v1betaλ₯Ό 기본으둜 μ‚¬μš©ν•˜κ³ , μ‹€νŒ¨ μ‹œ v1으둜 ν΄λ°±ν•˜λŠ” 것이 더 μ•ˆμ •μ μΌ 수 있음
self.rest_base_url = 'https://generativelanguage.googleapis.com/v1beta'
self.use_rest_api = True # REST API κ°•μ œ μ‚¬μš©
# API ν‚€ μ„€μ • (fallback용, REST APIκ°€ μ‹€νŒ¨ν•  경우)
try:
genai.configure(api_key=self.api_key)
print(f"[Gemini] API ν‚€ μ„€μ • μ™„λ£Œ (REST API λͺ¨λ“œ, νƒ€μž„μ•„μ›ƒ: {self.request_timeout}초)")
except Exception as e:
print(f"[Gemini] API ν‚€ μ„€μ • 였λ₯˜: {e}")
# API ν‚€κ°€ μ‹€μ œλ‘œ μ„€μ •λ˜μ—ˆλŠ”μ§€ 확인
try:
# genai λͺ¨λ“ˆμ˜ μ „μ—­ API ν‚€ 확인
configured_key = getattr(genai, '_api_key', None) or getattr(genai, 'api_key', None)
if configured_key:
print(f"[Gemini] μ „μ—­ API ν‚€ 확인: 섀정됨 (길이: {len(str(configured_key))}자)")
else:
print(f"[Gemini] κ²½κ³ : μ „μ—­ API ν‚€κ°€ ν™•μΈλ˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€. API 호좜이 μ‹€νŒ¨ν•  수 μžˆμŠ΅λ‹ˆλ‹€.")
except Exception as e:
print(f"[Gemini] API ν‚€ 확인 였λ₯˜: {e}")
print(f"[Gemini] μž¬μ‹œλ„ μ •μ±… 적용됨")
except Exception as e:
print(f"[Gemini] API ν‚€ μ„€μ • 였λ₯˜: {e}")
def reload_api_key(self):
"""API ν‚€λ₯Ό λ‹€μ‹œ λ‘œλ“œ (DBμ—μ„œ μ΅œμ‹  κ°’ κ°€μ Έμ˜€κΈ°)"""
self.api_key = get_gemini_api_key()
if self.api_key:
try:
genai.configure(api_key=self.api_key)
print(f"[Gemini] API ν‚€ μž¬λ‘œλ“œ μ™„λ£Œ")
return True
except Exception as e:
print(f"[Gemini] API ν‚€ μž¬λ‘œλ“œ 였λ₯˜: {e}")
return False
return False
def is_configured(self) -> bool:
"""Gemini APIκ°€ μ œλŒ€λ‘œ μ„€μ •λ˜μ—ˆλŠ”μ§€ 확인"""
return bool(self.api_key)
def get_available_models(self) -> List[str]:
"""μ‚¬μš© κ°€λŠ₯ν•œ Gemini λͺ¨λΈ λͺ©λ‘ λ°˜ν™˜"""
if not self.is_configured():
return []
try:
# μ‹€μ œ μ‚¬μš© κ°€λŠ₯ν•œ λͺ¨λΈ 확인
available_models = []
for model_name in AVAILABLE_GEMINI_MODELS:
try:
model = genai.GenerativeModel(model_name)
# λͺ¨λΈ μ ‘κ·Ό κ°€λŠ₯ μ—¬λΆ€ 확인
available_models.append(model_name)
except Exception as e:
# λͺ¨λΈμ„ 찾을 수 μ—†μœΌλ©΄ κ±΄λ„ˆλ›°κΈ°
continue
# λͺ¨λΈμ„ μ°Ύμ§€ λͺ»ν•œ 경우 κΈ°λ³Έ λͺ¨λΈ μ‹œλ„
if not available_models:
try:
# 기본적으둜 gemini-1.5-flash μ‹œλ„
model = genai.GenerativeModel('gemini-1.5-flash')
available_models.append('gemini-1.5-flash')
except:
pass
print(f"[Gemini] μ‚¬μš© κ°€λŠ₯ν•œ λͺ¨λΈ: {available_models}")
return available_models
except Exception as e:
print(f"[Gemini] λͺ¨λΈ λͺ©λ‘ 쑰회 였λ₯˜: {e}")
return []
def generate_response(self, prompt: str, model_name: str = 'gemini-1.5-flash', **kwargs) -> Dict:
"""
Gemini APIλ₯Ό μ‚¬μš©ν•˜μ—¬ 응닡 생성
Args:
prompt: μž…λ ₯ ν”„λ‘¬ν”„νŠΈ
model_name: μ‚¬μš©ν•  λͺ¨λΈ 이름
**kwargs: μΆ”κ°€ νŒŒλΌλ―Έν„° (temperature, max_tokens λ“±)
Returns:
Dict: {'response': str, 'error': str or None}
"""
if not self.is_configured():
return {
'response': None,
'error': 'Gemini API ν‚€κ°€ μ„€μ •λ˜μ§€ μ•Šμ•˜μŠ΅λ‹ˆλ‹€. GEMINI_API_KEY ν™˜κ²½ λ³€μˆ˜λ₯Ό μ„€μ •ν•˜μ„Έμš”.'
}
try:
# API ν‚€λ₯Ό 항상 μ΅œμ‹ μœΌλ‘œ λ‹€μ‹œ κ°€μ Έμ™€μ„œ μ„€μ • (DBμ—μ„œ λ³€κ²½λ˜μ—ˆμ„ 수 있음)
current_api_key = get_gemini_api_key()
if not current_api_key:
return {
'response': None,
'error': 'Gemini API ν‚€κ°€ μ„€μ •λ˜μ§€ μ•Šμ•˜μŠ΅λ‹ˆλ‹€. 관리 νŽ˜μ΄μ§€μ—μ„œ API ν‚€λ₯Ό μ„€μ •ν•˜μ„Έμš”.'
}
# API ν‚€κ°€ λ³€κ²½λ˜μ—ˆκ±°λ‚˜ μ—†λŠ” 경우 μž¬μ„€μ •
if not self.api_key or self.api_key != current_api_key:
self.api_key = current_api_key
genai.configure(api_key=self.api_key)
print(f"[Gemini] API ν‚€ μž¬μ„€μ • μ™„λ£Œ (길이: {len(self.api_key)}자)")
else:
# API ν‚€κ°€ 이미 μ„€μ •λ˜μ–΄ μžˆμ–΄λ„ 맀번 μž¬μ„€μ •ν•˜μ—¬ ν™•μ‹€νžˆ 함
genai.configure(api_key=self.api_key)
# λͺ¨λΈ 생성
model = genai.GenerativeModel(model_name)
print(f"[Gemini] λͺ¨λΈ 생성 μ™„λ£Œ: {model_name}")
# 생성 μ„€μ •
generation_config = {
'temperature': kwargs.get('temperature', 0.7),
'top_p': kwargs.get('top_p', 0.95),
'top_k': kwargs.get('top_k', 40),
'max_output_tokens': kwargs.get('max_output_tokens', 8192),
}
# 응닡 생성 (νƒ€μž„μ•„μ›ƒ μ„€μ •)
timeout_seconds = getattr(self, 'request_timeout', 300) # 5λΆ„ νƒ€μž„μ•„μ›ƒ
print(f"[Gemini] λͺ¨λΈ {model_name}둜 응닡 생성 쀑... (νƒ€μž„μ•„μ›ƒ: {timeout_seconds}초)")
print(f"[Gemini] API ν‚€ 확인: 섀정됨 (길이: {len(self.api_key)}자)")
print(f"[Gemini] ν”„λ‘¬ν”„νŠΈ 길이: {len(prompt)}자")
# νƒ€μž„μ•„μ›ƒμ„ μœ„ν•œ μ‹œμž‘ μ‹œκ°„ 기둝
start_time = time.time()
# google-generativeaiλŠ” retry νŒŒλΌλ―Έν„°λ₯Ό μ§€μ›ν•˜μ§€ μ•ŠμœΌλ―€λ‘œ
# μž¬μ‹œλ„ 정책을 μˆ˜λ™μœΌλ‘œ κ΅¬ν˜„ν•˜μ—¬ 적용
retry_policy = getattr(self, 'retry_policy', None)
print(f"[Gemini] API 호좜 μ‹œμž‘ (νƒ€μž„μ•„μ›ƒ: {timeout_seconds}초, μž¬μ‹œλ„ μ •μ±… 적용)")
# μž¬μ‹œλ„ 둜직 κ΅¬ν˜„ (μž¬μ‹œλ„ μ •μ±… 객체의 μ„€μ • μ‚¬μš©)
# retry.Retry 객체λ₯Ό μƒμ„±ν–ˆμ§€λ§Œ, μ‹€μ œλ‘œλŠ” κ·Έ 섀정값듀을 직접 μ‚¬μš©
initial_wait = 10.0 # 초기 λŒ€κΈ° μ‹œκ°„
max_wait = 60.0 # μ΅œλŒ€ λŒ€κΈ° μ‹œκ°„
multiplier = 2.0 # λŒ€κΈ° μ‹œκ°„ 배수
deadline = 600.0 # 전체 μž¬μ‹œλ„ κΈ°κ°„ (10λΆ„)
wait_time = initial_wait
deadline_time = time.time() + deadline
retry_count = 0
last_error = None
while True:
try:
# API 호좜 μ „ API ν‚€ μž¬ν™•μΈ 및 μž¬μ„€μ •
current_key = get_gemini_api_key()
if not current_key:
raise Exception("API ν‚€κ°€ μ„€μ •λ˜μ§€ μ•Šμ•˜μŠ΅λ‹ˆλ‹€. 관리 νŽ˜μ΄μ§€μ—μ„œ API ν‚€λ₯Ό μ„€μ •ν•˜μ„Έμš”.")
# API ν‚€κ°€ λ³€κ²½λ˜μ—ˆκ±°λ‚˜ μ„€μ •λ˜μ§€ μ•Šμ€ 경우 μž¬μ„€μ •
if not self.api_key or self.api_key != current_key:
self.api_key = current_key.strip() if current_key else None # 곡백 제거
print(f"[Gemini] API ν‚€ μž¬μ„€μ • μ™„λ£Œ (길이: {len(self.api_key) if self.api_key else 0}자)")
# API ν‚€ μœ νš¨μ„± 검사
if not self.api_key or not self.api_key.strip():
raise Exception("API ν‚€κ°€ λΉ„μ–΄μžˆμŠ΅λ‹ˆλ‹€. 관리 νŽ˜μ΄μ§€μ—μ„œ API ν‚€λ₯Ό μ„€μ •ν•˜μ„Έμš”.")
# API ν‚€ μ•žλ’€ 곡백 제거
api_key_clean = self.api_key.strip()
if not api_key_clean:
raise Exception("API ν‚€κ°€ μœ νš¨ν•˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€ (곡백만 포함).")
# API ν‚€ ν˜•μ‹ 확인 (Google API ν‚€λŠ” 보톡 AIza둜 μ‹œμž‘)
if not api_key_clean.startswith('AIza'):
print(f"[Gemini] κ²½κ³ : API ν‚€κ°€ 일반적인 Google API ν‚€ ν˜•μ‹μ΄ μ•„λ‹™λ‹ˆλ‹€ (AIza둜 μ‹œμž‘ν•˜μ§€ μ•ŠμŒ)")
# API ν‚€ 길이 확인 (일반적으둜 39자 이상)
if len(api_key_clean) < 20:
raise Exception(f"API ν‚€ 길이가 λ„ˆλ¬΄ μ§§μŠ΅λ‹ˆλ‹€ ({len(api_key_clean)}자). μ˜¬λ°”λ₯Έ API 킀인지 ν™•μΈν•˜μ„Έμš”.")
print(f"[Gemini] API ν‚€ 검증 μ™„λ£Œ (길이: {len(api_key_clean)}자, μ‹œμž‘: {api_key_clean[:10]}..., 끝: ...{api_key_clean[-5:]})")
# REST APIλ₯Ό 직접 μ‚¬μš©ν•˜μ—¬ 호좜
use_rest = getattr(self, 'use_rest_api', True)
if use_rest:
print(f"[Gemini] REST API 직접 호좜 λͺ¨λ“œ")
# API 버전 및 λͺ¨λΈ 이름 μ •κ·œν™”
# λͺ¨λΈ μ΄λ¦„μ—μ„œ 'gemini:' 접두사 제거 및 μ •κ·œν™”
model_name_clean = model_name.strip()
if ':' in model_name_clean:
# "gemini:gemini-1.5-flash" ν˜•μ‹μΈ 경우
model_name_clean = model_name_clean.split(':', 1)[1].strip()
elif model_name_clean.startswith('gemini-'):
# "gemini-1.5-flash" ν˜•μ‹μΈ 경우 κ·ΈλŒ€λ‘œ μ‚¬μš©
pass
# REST API 베이슀 URL (v1beta μš°μ„  μ‚¬μš©)
rest_base_url = 'https://generativelanguage.googleapis.com/v1beta'
url = f"{rest_base_url}/models/{model_name_clean}:generateContent"
print(f"[Gemini] - API 버전: v1beta (κΈ°λ³Έ)")
print(f"[Gemini] - 원본 λͺ¨λΈ 이름: {model_name}")
print(f"[Gemini] - μ •κ·œν™”λœ λͺ¨λΈ 이름: {model_name_clean}")
print(f"[Gemini] - 전체 URL: {url}")
# REST API μš”μ²­ λ³Έλ¬Έ ꡬ성
request_body = {
"contents": [{
"parts": [{
"text": prompt
}]
}],
"generationConfig": generation_config
}
# REST API 헀더 (API ν‚€λ₯Ό ν—€λ”λ‘œ 전달 μ‹œλ„)
headers = {
"Content-Type": "application/json",
"x-goog-api-key": api_key_clean
}
print(f"[Gemini] REST API 호좜 전솑 쀑...")
print(f"[Gemini] - URL: {url}")
print(f"[Gemini] - λͺ¨λΈ: {model_name}")
print(f"[Gemini] - API ν‚€: 섀정됨 (길이: {len(api_key_clean)}자)")
print(f"[Gemini] - API ν‚€ μ‹œμž‘: {api_key_clean[:15]}...")
print(f"[Gemini] - API ν‚€ 끝: ...{api_key_clean[-10:]}")
print(f"[Gemini] - ν”„λ‘¬ν”„νŠΈ 길이: {len(prompt)}자")
# REST API 호좜 (API ν‚€λ₯Ό 헀더와 params μ–‘μͺ½μœΌλ‘œ 전달 μ‹œλ„)
# Google Gemini APIλŠ” 헀더 λ˜λŠ” params 쀑 ν•˜λ‚˜λ§Œ ν•„μš”ν•˜μ§€λ§Œ, μ–‘μͺ½ λͺ¨λ‘ μ‹œλ„
api_params = {"key": api_key_clean}
print(f"[Gemini] - API ν‚€ 전달 방식: 헀더 (x-goog-api-key) 및 νŒŒλΌλ―Έν„° (key)")
rest_response = requests.post(
url,
headers=headers,
json=request_body,
params=api_params,
timeout=timeout_seconds
)
# μš”μ²­ URLμ—μ„œ API ν‚€ λΆ€λΆ„λ§Œ μ œκ±°ν•˜μ—¬ λ‘œκΉ… (λ³΄μ•ˆ)
request_url = rest_response.request.url
if 'key=' in request_url:
# API ν‚€ 뢀뢄을 λ§ˆμŠ€ν‚Ή
from urllib.parse import urlparse, parse_qs, urlencode, urlunparse
parsed = urlparse(request_url)
params = parse_qs(parsed.query)
if 'key' in params:
masked_params = params.copy()
masked_key = masked_params['key'][0][:10] + '...' + masked_params['key'][0][-5:]
masked_params['key'] = [masked_key]
masked_query = urlencode(masked_params, doseq=True)
masked_url = urlunparse((parsed.scheme, parsed.netloc, parsed.path, parsed.params, masked_query, parsed.fragment))
print(f"[Gemini] - μš”μ²­ URL (λ§ˆμŠ€ν‚Ήλ¨): {masked_url[:150]}...")
else:
print(f"[Gemini] - μš”μ²­ URL: {request_url[:150]}...")
print(f"[Gemini] REST API 응닡 μƒνƒœ μ½”λ“œ: {rest_response.status_code}")
# 응닡 λ³Έλ¬Έ 확인 (μƒνƒœ μ½”λ“œκ°€ 200이어도 μ—λŸ¬κ°€ μžˆμ„ 수 있음)
response_has_error = False
try:
response_data_check = rest_response.json()
# 응닡에 error ν•„λ“œκ°€ μžˆλŠ”μ§€ 확인
if 'error' in response_data_check:
response_has_error = True
error_info = response_data_check['error']
error_code = error_info.get('code', rest_response.status_code)
error_message = error_info.get('message', 'μ•Œ 수 μ—†λŠ” 였λ₯˜')
print(f"[Gemini] 응닡 본문에 μ—λŸ¬ 감지: code={error_code}, message={error_message}")
# μ—λŸ¬ μ½”λ“œμ— 따라 처리
if error_code == 404:
# 404 였λ₯˜μΈ 경우 v1으둜 μž¬μ‹œλ„ (v1betaμ—μ„œ μ‹€νŒ¨ν•œ 경우)
print(f"[Gemini] v1betaμ—μ„œ λͺ¨λΈμ„ 찾을 수 μ—†μŒ, v1으둜 μž¬μ‹œλ„...")
rest_base_url_v1 = 'https://generativelanguage.googleapis.com/v1'
url_v1 = f"{rest_base_url_v1}/models/{model_name_clean}:generateContent"
rest_response = requests.post(
url_v1,
headers=headers,
json=request_body,
params=api_params,
timeout=timeout_seconds
)
print(f"[Gemini] v1 REST API 응닡 μƒνƒœ μ½”λ“œ: {rest_response.status_code}")
# v1 응닡도 확인 (성곡 λ˜λŠ” μ‹€νŒ¨ λͺ¨λ‘ 처리)
v1_success = False
if rest_response.status_code == 200:
response_data_check_v1 = rest_response.json()
if 'error' not in response_data_check_v1:
# v1μ—μ„œ 성곡
v1_success = True
response_has_error = False
print(f"[Gemini] v1μ—μ„œ 정상 응닡 λ°›μŒ")
if not v1_success:
# v1μ—μ„œλ„ μ‹€νŒ¨ (μ—λŸ¬ λ˜λŠ” 404 λ“±)
if rest_response.status_code == 200:
# 200 OKμ§€λ§Œ μ—λŸ¬ ν•„λ“œκ°€ μžˆλŠ” 경우
response_data_check_v1 = rest_response.json()
error_info_v1 = response_data_check_v1.get('error', {})
error_code_v1 = error_info_v1.get('code', 404)
else:
# HTTP μ—λŸ¬ (404 λ“±)
error_code_v1 = rest_response.status_code
error_info_v1 = {'message': rest_response.text[:200]}
print(f"[Gemini] v1μ—μ„œλ„ μ‹€νŒ¨ (μ½”λ“œ: {error_code_v1}), λŒ€μ²΄ λͺ¨λΈ 검색 μ‹œμž‘...")
# λͺ¨λΈ λͺ©λ‘ 쑰회 μ‹œλ„ (v1beta)
available_models_str = "확인 λΆˆκ°€"
available_models_list = []
try:
list_models_url = f"{rest_base_url}/models"
list_response = requests.get(
list_models_url,
headers={"x-goog-api-key": api_key_clean},
params={"key": api_key_clean},
timeout=10
)
if list_response.status_code == 200:
models_data = list_response.json()
available_models_list = []
for m in models_data.get('models', []):
model_name_full = m.get('name', '')
if '/' in model_name_full:
model_name_short = model_name_full.split('/')[-1]
else:
model_name_short = model_name_full
# generateContentλ₯Ό μ§€μ›ν•˜λŠ” λͺ¨λΈλ§Œ 필터링
supported_methods = m.get('supportedGenerationMethods', [])
if 'generateContent' in supported_methods:
available_models_list.append(model_name_short)
available_models_str = ', '.join(available_models_list[:10]) if available_models_list else 'μ—†μŒ'
print(f"[Gemini] μ‚¬μš© κ°€λŠ₯ν•œ λͺ¨λΈ λͺ©λ‘ (v1beta): {available_models_list[:10]}")
except Exception as list_error:
print(f"[Gemini] λͺ¨λΈ λͺ©λ‘ 쑰회 μ‹€νŒ¨: {list_error}")
# λŒ€μ²΄ λͺ¨λΈ μ°ΎκΈ° 및 μž¬μ‹œλ„
fallback_model = None
# 1. 동일 계열 μ΅œμ‹  λͺ¨λΈ μ°ΎκΈ°
if 'gemini-1.5-pro' in model_name_clean:
for m in available_models_list:
if 'gemini-1.5-pro' in m and m != model_name_clean:
fallback_model = m
break
# 2. Flash λͺ¨λΈλ‘œ λŒ€μ²΄
if not fallback_model and 'gemini-1.5-flash' in available_models_list:
fallback_model = 'gemini-1.5-flash'
# 3. 아무 λͺ¨λΈμ΄λ‚˜ 선택
if not fallback_model and available_models_list:
fallback_model = available_models_list[0]
if fallback_model:
print(f"[Gemini] λͺ¨λΈ {model_name_clean}을(λ₯Ό) 찾을 수 μ—†μ–΄ {fallback_model}둜 λŒ€μ²΄ν•˜μ—¬ μž¬μ‹œλ„ν•©λ‹ˆλ‹€.")
url_fallback = f"{rest_base_url}/models/{fallback_model}:generateContent"
rest_response = requests.post(
url_fallback,
headers=headers,
json=request_body,
params=api_params,
timeout=timeout_seconds
)
# Fallback 성곡 μ—¬λΆ€ 확인
if rest_response.status_code == 200:
# μ„±κ³΅μ μœΌλ‘œ 응닡 λ°›μŒ -> 루프 νƒˆμΆœμ„ μœ„ν•΄ response_has_error=False μœ μ§€
print(f"[Gemini] λŒ€μ²΄ λͺ¨λΈ({fallback_model})둜 μž¬μ‹œλ„ 성곡")
# λͺ¨λΈλͺ…을 λŒ€μ²΄λœ κ²ƒμœΌλ‘œ μ—…λ°μ΄νŠΈ (토큰 μ €μž₯ 등을 μœ„ν•΄)
model_name = fallback_model
response_has_error = False
else:
# Fallback도 μ‹€νŒ¨ν•˜λ©΄ μ—λŸ¬ λ°œμƒ
error_text_v1 = json.dumps(error_info_v1)
raise Exception(f"REST API 였λ₯˜ {error_code_v1}: {error_text_v1}\nμ‚¬μš© κ°€λŠ₯ν•œ λͺ¨λΈ: {available_models_str}\nλŒ€μ²΄ μ‹œλ„ μ‹€νŒ¨: {fallback_model}")
else:
error_text_v1 = json.dumps(error_info_v1)
raise Exception(f"REST API 였λ₯˜ {error_code_v1}: {error_text_v1}\nμ‚¬μš© κ°€λŠ₯ν•œ λͺ¨λΈ: {available_models_str}")
elif error_code == 429:
# 429 였λ₯˜: ν• λ‹ΉλŸ‰ 초과 (μž¬μ‹œλ„ λΆˆκ°€λŠ₯)
print(f"[Gemini] ❌ ν• λ‹ΉλŸ‰ 초과 였λ₯˜ (429) 감지")
print(f"[Gemini] μž¬μ‹œλ„ λΆˆκ°€λŠ₯ν•œ 였λ₯˜: REST API 였λ₯˜ {error_code}: {json.dumps(error_info)}")
# μ—λŸ¬ λ©”μ‹œμ§€μ—μ„œ ꢌμž₯ λͺ¨λΈ μΆ”μΆœ
recommended_model = None
if 'gemini-2.0-flash' in error_message.lower() or 'gemini 2.0' in error_message.lower():
recommended_model = "gemini-2.0-flash-exp"
# μ‚¬μš©μž μΉœν™”μ μΈ μ—λŸ¬ λ©”μ‹œμ§€ 생성
quota_error_msg = f"""Gemini API ν• λ‹ΉλŸ‰ 초과 (429)
ν˜„μž¬ μ‚¬μš© 쀑인 λͺ¨λΈ '{model_name_clean}'의 일일 μš”μ²­ ν•œλ„λ₯Ό μ΄ˆκ³Όν–ˆμŠ΅λ‹ˆλ‹€.
ν•΄κ²° 방법:
1. λ‚΄μΌκΉŒμ§€ λŒ€κΈ° (ν• λ‹ΉλŸ‰μ΄ μžλ™μœΌλ‘œ μž¬μ„€μ •λ©λ‹ˆλ‹€)
2. λ‹€λ₯Έ Gemini λͺ¨λΈλ‘œ λ³€κ²½:
- gemini-2.0-flash-exp (더 높은 ν• λ‹ΉλŸ‰ 제곡)
- gemini-1.5-pro-002
- gemini-1.5-flash
3. Google AI Studioμ—μ„œ ν• λ‹ΉλŸ‰ 확인:
https://aistudio.google.com/app/apikey
상세 였λ₯˜: {error_message[:200]}"""
if recommended_model:
quota_error_msg += f"\n\nꢌμž₯: {recommended_model} λͺ¨λΈλ‘œ 변경을 κ³ λ €ν•΄λ³΄μ„Έμš”."
raise Exception(quota_error_msg)
else:
# 404, 429κ°€ μ•„λ‹Œ λ‹€λ₯Έ μ—λŸ¬
error_text = json.dumps(error_info)
raise Exception(f"REST API 였λ₯˜ {error_code}: {error_text}")
except ValueError:
# JSON νŒŒμ‹± μ‹€νŒ¨
pass
# 이미 μ—λŸ¬κ°€ μ²˜λ¦¬λ˜μ§€ μ•Šμ€ κ²½μš°μ—λ§Œ μΆ”κ°€ μ—λŸ¬ 처리
# (response_has_errorκ°€ Trueλ©΄ 이미 μœ„μ—μ„œ μ²˜λ¦¬λ˜μ—ˆκ±°λ‚˜ Exception이 λ°œμƒν–ˆμ„ 것)
if rest_response.status_code != 200 and not response_has_error:
error_text = rest_response.text[:1000] if rest_response.text else '상세 정보 μ—†μŒ'
# API ν‚€ 였λ₯˜μΈ 경우 더 μƒμ„Έν•œ μ•ˆλ‚΄ 제곡
if rest_response.status_code == 400 and ('API key' in error_text or 'API_KEY' in error_text):
print(f"[Gemini] ❌ API ν‚€ 였λ₯˜ 감지")
print(f"[Gemini] ν˜„μž¬ API ν‚€ 정보:")
print(f"[Gemini] - 길이: {len(api_key_clean)}자")
print(f"[Gemini] - μ‹œμž‘: {api_key_clean[:15]}...")
print(f"[Gemini] - 끝: ...{api_key_clean[-10:]}")
print(f"[Gemini] - ν˜•μ‹ 확인: {'AIza둜 μ‹œμž‘' if api_key_clean.startswith('AIza') else 'AIza둜 μ‹œμž‘ν•˜μ§€ μ•ŠμŒ (비정상)'}")
raise Exception(f"""REST API 였λ₯˜ 400: API ν‚€κ°€ μœ νš¨ν•˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€.
확인 사항:
1. 관리 νŽ˜μ΄μ§€μ—μ„œ API ν‚€κ°€ μ˜¬λ°”λ₯΄κ²Œ μ„€μ •λ˜μ—ˆλŠ”μ§€ ν™•μΈν•˜μ„Έμš”.
2. Google AI Studio (https://aistudio.google.com/app/apikey)μ—μ„œ API ν‚€κ°€ ν™œμ„±ν™”λ˜μ–΄ μžˆλŠ”μ§€ ν™•μΈν•˜μ„Έμš”.
3. API ν‚€ ν˜•μ‹μ΄ μ˜¬λ°”λ₯Έμ§€ ν™•μΈν•˜μ„Έμš” (일반적으둜 'AIza'둜 μ‹œμž‘).
4. API 킀에 λΆˆν•„μš”ν•œ κ³΅λ°±μ΄λ‚˜ μ€„λ°”κΏˆμ΄ ν¬ν•¨λ˜μ§€ μ•Šμ•˜λŠ”μ§€ ν™•μΈν•˜μ„Έμš”.
였λ₯˜ 상세: {error_text[:300]}""")
raise Exception(f"REST API 였λ₯˜ {rest_response.status_code}: {error_text}")
# μ—λŸ¬κ°€ μžˆμ—ˆμ§€λ§Œ μ²˜λ¦¬λ˜μ§€ μ•Šμ€ 경우 (정상 응닡이 μ•„λ‹˜)
if response_has_error:
# 이미 μœ„μ—μ„œ Exception이 λ°œμƒν–ˆμ–΄μ•Ό ν•˜μ§€λ§Œ, ν˜Ήμ‹œ λͺ¨λ₯΄λ‹ˆ 확인
error_text = rest_response.text[:1000] if rest_response.text else '상세 정보 μ—†μŒ'
raise Exception(f"REST API 였λ₯˜: 응닡에 μ—λŸ¬κ°€ ν¬ν•¨λ˜μ–΄ μžˆμŠ΅λ‹ˆλ‹€. {error_text}")
# REST API 응닡 νŒŒμ‹±
response_data = rest_response.json()
# 토큰 μ‚¬μš©λŸ‰ 정보 μΆ”μΆœ
input_tokens = None
output_tokens = None
if 'usageMetadata' in response_data:
usage = response_data['usageMetadata']
input_tokens = usage.get('promptTokenCount')
output_tokens = usage.get('candidatesTokenCount')
total_tokens = usage.get('totalTokenCount')
print(f"[Gemini] 토큰 μ‚¬μš©λŸ‰: μž…λ ₯={input_tokens}, 좜λ ₯={output_tokens}, 총={total_tokens}")
# μ‘λ‹΅μ—μ„œ ν…μŠ€νŠΈ μΆ”μΆœ
if 'candidates' in response_data and len(response_data['candidates']) > 0:
candidate = response_data['candidates'][0]
if 'content' in candidate and 'parts' in candidate['content']:
parts = candidate['content']['parts']
if len(parts) > 0 and 'text' in parts[0]:
response_text = parts[0]['text']
print(f"[Gemini] REST API 응닡 μˆ˜μ‹  성곡 (길이: {len(response_text)}자)")
# genai 라이브러리 ν˜•μ‹μœΌλ‘œ λ³€ν™˜ (ν˜Έν™˜μ„±μ„ μœ„ν•΄)
class MockResponse:
def __init__(self, text, input_tokens=None, output_tokens=None):
self.text = text
self.input_tokens = input_tokens
self.output_tokens = output_tokens
response = MockResponse(response_text, input_tokens, output_tokens)
break
else:
raise Exception("REST API 응닡에 ν…μŠ€νŠΈκ°€ μ—†μŠ΅λ‹ˆλ‹€.")
else:
raise Exception("REST API 응닡 ν˜•μ‹μ΄ μ˜¬λ°”λ₯΄μ§€ μ•ŠμŠ΅λ‹ˆλ‹€.")
else:
raise Exception("REST API 응닡에 candidatesκ°€ μ—†μŠ΅λ‹ˆλ‹€.")
else:
# κΈ°μ‘΄ genai 라이브러리 μ‚¬μš© (fallback)
genai.configure(api_key=self.api_key)
print(f"[Gemini] genai 라이브러리 μ‚¬μš© (fallback)")
response = model.generate_content(
prompt,
generation_config=generation_config
)
print(f"[Gemini] Gemini API 응닡 μˆ˜μ‹  성곡")
break
# 성곡 μ‹œ 루프 μ’…λ£Œ
if retry_count > 0:
print(f"[Gemini] μž¬μ‹œλ„ 성곡 (총 {retry_count}회 μž¬μ‹œλ„)")
break
except Exception as e:
last_error = e
error_str = str(e).lower()
# μž¬μ‹œλ„ κ°€λŠ₯ν•œ 였λ₯˜μΈμ§€ 확인 (νƒ€μž„μ•„μ›ƒ, λ„€νŠΈμ›Œν¬ 였λ₯˜ λ“±)
# 429(ν• λ‹ΉλŸ‰ 초과), 400(잘λͺ»λœ μš”μ²­), 401(인증 μ‹€νŒ¨), 403(κΆŒν•œ μ—†μŒ)은 μž¬μ‹œλ„ λΆˆκ°€
non_retryable_errors = ['429', 'quota', 'exceeded', '400', '401', '403', 'api key', 'invalid', 'unauthorized', 'forbidden']
is_non_retryable = any(err in error_str for err in non_retryable_errors)
retryable_errors = ['timeout', '503', '502', '504', 'connection', 'network', 'illegal metadata']
is_retryable = any(err in error_str for err in retryable_errors) and not is_non_retryable
# deadline 확인
if time.time() >= deadline_time:
print(f"[Gemini] μž¬μ‹œλ„ deadline 초과 ({deadline}초), λ§ˆμ§€λ§‰ 였λ₯˜ λ°˜ν™˜")
raise
if is_retryable:
retry_count += 1
print(f"[Gemini] μž¬μ‹œλ„ {retry_count} - {wait_time:.1f}초 ν›„ μž¬μ‹œλ„ (였λ₯˜: {str(e)[:100]})")
time.sleep(wait_time)
wait_time = min(wait_time * multiplier, max_wait) # 배수둜 증가, μ΅œλŒ€κ°’ μ œν•œ
else:
# μž¬μ‹œλ„ λΆˆκ°€λŠ₯ν•œ 였λ₯˜
if is_non_retryable:
print(f"[Gemini] μž¬μ‹œλ„ λΆˆκ°€λŠ₯ν•œ 였λ₯˜ (ν• λ‹ΉλŸ‰/인증 였λ₯˜): {str(e)[:200]}")
else:
print(f"[Gemini] μž¬μ‹œλ„ λΆˆκ°€λŠ₯ν•œ 였λ₯˜: {str(e)[:100]}")
raise
# 응닡 μ‹œκ°„ 확인
elapsed_time = time.time() - start_time
print(f"[Gemini] 응닡 μˆ˜μ‹  μ™„λ£Œ (κ²½κ³Ό μ‹œκ°„: {elapsed_time:.2f}초)")
# 응닡 ν…μŠ€νŠΈ μΆ”μΆœ
response_text = response.text if hasattr(response, 'text') else str(response)
# 토큰 정보 μΆ”μΆœ
input_tokens = getattr(response, 'input_tokens', None)
output_tokens = getattr(response, 'output_tokens', None)
print(f"[Gemini] 응닡 생성 μ™„λ£Œ: {len(response_text)}자, μž…λ ₯ 토큰: {input_tokens}, 좜λ ₯ 토큰: {output_tokens}")
return {
'response': response_text,
'error': None,
'input_tokens': input_tokens,
'output_tokens': output_tokens
}
except Exception as e:
error_msg = f'Gemini API 였λ₯˜: {str(e)}'
print(f"[Gemini] {error_msg}")
return {
'response': None,
'error': error_msg
}
def generate_chat_response(self, messages: List[Dict], model_name: str = 'gemini-1.5-flash', **kwargs) -> Dict:
"""
Gemini APIλ₯Ό μ‚¬μš©ν•˜μ—¬ μ±„νŒ… 응닡 생성
Args:
messages: λ©”μ‹œμ§€ 리슀트 [{'role': 'user', 'content': '...'}, ...]
model_name: μ‚¬μš©ν•  λͺ¨λΈ 이름
**kwargs: μΆ”κ°€ νŒŒλΌλ―Έν„°
Returns:
Dict: {'response': str, 'error': str or None}
"""
if not self.is_configured():
return {
'response': None,
'error': 'Gemini API ν‚€κ°€ μ„€μ •λ˜μ§€ μ•Šμ•˜μŠ΅λ‹ˆλ‹€. GEMINI_API_KEY ν™˜κ²½ λ³€μˆ˜λ₯Ό μ„€μ •ν•˜μ„Έμš”.'
}
try:
# λͺ¨λΈ 생성
model = genai.GenerativeModel(model_name)
# μ±„νŒ… μ„Έμ…˜ μ‹œμž‘
chat = model.start_chat(history=[])
# 이전 λŒ€ν™” λ‚΄μ—­ μΆ”κ°€ (user와 assistant λ©”μ‹œμ§€)
for msg in messages[:-1]: # λ§ˆμ§€λ§‰ λ©”μ‹œμ§€ μ œμ™Έ
if msg['role'] == 'user':
chat.send_message(msg['content'])
elif msg['role'] == 'assistant' or msg['role'] == 'ai':
# GeminiλŠ” μ‚¬μš©μž λ©”μ‹œμ§€λ§Œ 직접 λ³΄λ‚΄λ―€λ‘œ, assistant λ©”μ‹œμ§€λŠ” νžˆμŠ€ν† λ¦¬λ‘œ μ²˜λ¦¬ν•˜μ§€ μ•ŠμŒ
pass
# λ§ˆμ§€λ§‰ μ‚¬μš©μž λ©”μ‹œμ§€λ‘œ 응닡 생성 (νƒ€μž„μ•„μ›ƒ μ„€μ •)
last_message = messages[-1] if messages else {'content': ''}
timeout_seconds = getattr(self, 'timeout', 600) # 5λΆ„ νƒ€μž„μ•„μ›ƒ
print(f"[Gemini] μ±„νŒ… 응닡 생성 쀑... (νƒ€μž„μ•„μ›ƒ: {timeout_seconds}초)")
# νƒ€μž„μ•„μ›ƒμ„ μœ„ν•œ μ‹œμž‘ μ‹œκ°„ 기둝
start_time = time.time()
# google-generativeaiλŠ” retry νŒŒλΌλ―Έν„°λ₯Ό μ§€μ›ν•˜μ§€ μ•ŠμœΌλ―€λ‘œ
# μž¬μ‹œλ„ 정책을 μˆ˜λ™μœΌλ‘œ κ΅¬ν˜„ν•˜μ—¬ 적용
print(f"[Gemini] μ±„νŒ… API 호좜 μ‹œμž‘ (νƒ€μž„μ•„μ›ƒ: {timeout_seconds}초, μž¬μ‹œλ„ μ •μ±… 적용)")
# μž¬μ‹œλ„ 둜직 κ΅¬ν˜„ (μž¬μ‹œλ„ μ •μ±… 객체의 μ„€μ • μ‚¬μš©)
initial_wait = 10.0 # 초기 λŒ€κΈ° μ‹œκ°„
max_wait = 60.0 # μ΅œλŒ€ λŒ€κΈ° μ‹œκ°„
multiplier = 2.0 # λŒ€κΈ° μ‹œκ°„ 배수
deadline = 600.0 # 전체 μž¬μ‹œλ„ κΈ°κ°„ (10λΆ„)
wait_time = initial_wait
deadline_time = time.time() + deadline
retry_count = 0
last_error = None
while True:
try:
response = chat.send_message(last_message['content'])
# 성곡 μ‹œ 루프 μ’…λ£Œ
if retry_count > 0:
print(f"[Gemini] μ±„νŒ… μž¬μ‹œλ„ 성곡 (총 {retry_count}회 μž¬μ‹œλ„)")
break
except Exception as e:
last_error = e
error_str = str(e).lower()
# μž¬μ‹œλ„ κ°€λŠ₯ν•œ 였λ₯˜μΈμ§€ 확인 (νƒ€μž„μ•„μ›ƒ, λ„€νŠΈμ›Œν¬ 였λ₯˜ λ“±)
# 429(ν• λ‹ΉλŸ‰ 초과), 400(잘λͺ»λœ μš”μ²­), 401(인증 μ‹€νŒ¨), 403(κΆŒν•œ μ—†μŒ)은 μž¬μ‹œλ„ λΆˆκ°€
non_retryable_errors = ['429', 'quota', 'exceeded', '400', '401', '403', 'api key', 'invalid', 'unauthorized', 'forbidden']
is_non_retryable = any(err in error_str for err in non_retryable_errors)
retryable_errors = ['timeout', '503', '502', '504', 'connection', 'network', 'illegal metadata']
is_retryable = any(err in error_str for err in retryable_errors) and not is_non_retryable
# deadline 확인
if time.time() >= deadline_time:
print(f"[Gemini] μ±„νŒ… μž¬μ‹œλ„ deadline 초과 ({deadline}초), λ§ˆμ§€λ§‰ 였λ₯˜ λ°˜ν™˜")
raise
if is_retryable:
retry_count += 1
print(f"[Gemini] μ±„νŒ… μž¬μ‹œλ„ {retry_count} - {wait_time:.1f}초 ν›„ μž¬μ‹œλ„ (였λ₯˜: {str(e)[:100]})")
time.sleep(wait_time)
wait_time = min(wait_time * multiplier, max_wait) # 배수둜 증가, μ΅œλŒ€κ°’ μ œν•œ
else:
# μž¬μ‹œλ„ λΆˆκ°€λŠ₯ν•œ 였λ₯˜
if is_non_retryable:
print(f"[Gemini] μ±„νŒ… μž¬μ‹œλ„ λΆˆκ°€λŠ₯ν•œ 였λ₯˜ (ν• λ‹ΉλŸ‰/인증 였λ₯˜): {str(e)[:200]}")
else:
print(f"[Gemini] μ±„νŒ… μž¬μ‹œλ„ λΆˆκ°€λŠ₯ν•œ 였λ₯˜: {str(e)[:100]}")
raise
elapsed_time = time.time() - start_time
print(f"[Gemini] μ±„νŒ… 응닡 μˆ˜μ‹  μ™„λ£Œ (κ²½κ³Ό μ‹œκ°„: {elapsed_time:.2f}초)")
response_text = response.text if hasattr(response, 'text') else str(response)
print(f"[Gemini] μ±„νŒ… 응닡 생성 μ™„λ£Œ: {len(response_text)}자")
return {
'response': response_text,
'error': None
}
except Exception as e:
error_msg = f'Gemini API 였λ₯˜: {str(e)}'
print(f"[Gemini] {error_msg}")
return {
'response': None,
'error': error_msg
}
# μ „μ—­ Gemini ν΄λΌμ΄μ–ΈνŠΈ μΈμŠ€ν„΄μŠ€
_gemini_client = None
def get_gemini_client() -> GeminiClient:
"""Gemini ν΄λΌμ΄μ–ΈνŠΈ 싱글톀 μΈμŠ€ν„΄μŠ€ λ°˜ν™˜"""
global _gemini_client
if _gemini_client is None:
_gemini_client = GeminiClient()
else:
# API ν‚€κ°€ λ³€κ²½λ˜μ—ˆμ„ 수 μžˆμœΌλ―€λ‘œ μž¬λ‘œλ“œ μ‹œλ„
_gemini_client.reload_api_key()
return _gemini_client
def reset_gemini_client():
"""Gemini ν΄λΌμ΄μ–ΈνŠΈ 리셋 (API ν‚€ λ³€κ²½ ν›„ 호좜)"""
global _gemini_client
_gemini_client = None
return get_gemini_client()