feat: Groq fallback writer 추가 — Gemini rate limit 시 자동 전환
- GroqWriter 클래스 추가 (llama-3.3-70b-versatile) - FallbackWriter 래퍼: primary 실패/빈응답 → fallback chain 자동 시도 - engine.json에 groq 설정 + fallback_chain: ["groq"] 추가 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -194,6 +194,41 @@ class GeminiWriter(BaseWriter):
|
||||
return ''
|
||||
|
||||
|
||||
class GroqWriter(BaseWriter):
|
||||
"""Groq API를 사용하는 글쓰기 엔진 (Gemini fallback용)"""
|
||||
|
||||
def __init__(self, cfg: dict):
|
||||
self.api_key = os.getenv(cfg.get('api_key_env', 'GROQ_API_KEY'), '')
|
||||
self.model = cfg.get('model', 'llama-3.3-70b-versatile')
|
||||
self.max_tokens = cfg.get('max_tokens', 4096)
|
||||
self.temperature = cfg.get('temperature', 0.7)
|
||||
|
||||
def write(self, prompt: str, system: str = '') -> str:
|
||||
if not self.api_key:
|
||||
logger.warning("GROQ_API_KEY 없음 — GroqWriter 비활성화")
|
||||
return ''
|
||||
try:
|
||||
from groq import Groq
|
||||
client = Groq(api_key=self.api_key)
|
||||
messages = []
|
||||
if system:
|
||||
messages.append({'role': 'system', 'content': system})
|
||||
messages.append({'role': 'user', 'content': prompt})
|
||||
response = client.chat.completions.create(
|
||||
model=self.model,
|
||||
messages=messages,
|
||||
max_tokens=self.max_tokens,
|
||||
temperature=self.temperature,
|
||||
)
|
||||
return response.choices[0].message.content or ''
|
||||
except ImportError:
|
||||
logger.warning("groq 미설치 — GroqWriter 비활성화")
|
||||
return ''
|
||||
except Exception as e:
|
||||
logger.error(f"GroqWriter 오류: {e}")
|
||||
return ''
|
||||
|
||||
|
||||
class ClaudeWebWriter(BaseWriter):
|
||||
"""Playwright Chromium에 세션 쿠키를 주입해 claude.ai를 자동화하는 Writer
|
||||
|
||||
@@ -540,6 +575,43 @@ class ExternalGenerator(BaseImageGenerator):
|
||||
return False
|
||||
|
||||
|
||||
# ─── Fallback Writer ──────────────────────────────────────
|
||||
|
||||
class FallbackWriter(BaseWriter):
|
||||
"""Primary writer 실패 시 fallback chain으로 자동 재시도"""
|
||||
|
||||
def __init__(self, primary_name: str, primary: BaseWriter,
|
||||
fallbacks: list[tuple[str, BaseWriter]]):
|
||||
self._primary_name = primary_name
|
||||
self._primary = primary
|
||||
self._fallbacks = fallbacks # [(provider_name, writer), ...]
|
||||
|
||||
def write(self, prompt: str, system: str = '') -> str:
|
||||
# 1차: primary
|
||||
try:
|
||||
result = self._primary.write(prompt, system)
|
||||
if result and result.strip():
|
||||
return result
|
||||
logger.warning(f"{self._primary_name} 빈 응답 — fallback 시도")
|
||||
except Exception as e:
|
||||
logger.warning(f"{self._primary_name} 실패: {e} — fallback 시도")
|
||||
|
||||
# 2차: fallback chain
|
||||
for fb_name, fb_writer in self._fallbacks:
|
||||
try:
|
||||
logger.info(f"Fallback writer 시도: {fb_name}")
|
||||
result = fb_writer.write(prompt, system)
|
||||
if result and result.strip():
|
||||
logger.info(f"Fallback 성공: {fb_name}")
|
||||
return result
|
||||
logger.warning(f"{fb_name} 빈 응답")
|
||||
except Exception as e:
|
||||
logger.warning(f"{fb_name} fallback 실패: {e}")
|
||||
|
||||
logger.error("모든 writer (primary + fallback) 실패")
|
||||
return ''
|
||||
|
||||
|
||||
# ─── EngineLoader ───────────────────────────────────────
|
||||
|
||||
class EngineLoader:
|
||||
@@ -601,22 +673,43 @@ class EngineLoader:
|
||||
else:
|
||||
logger.warning(f"update_provider: 알 수 없는 카테고리 '{category}'")
|
||||
|
||||
def get_writer(self) -> BaseWriter:
|
||||
"""현재 설정된 writing provider에 맞는 BaseWriter 구현체 반환"""
|
||||
def get_writer(self, with_fallback: bool = True) -> BaseWriter:
|
||||
"""현재 설정된 writing provider에 맞는 BaseWriter 구현체 반환.
|
||||
with_fallback=True이면 FallbackWriter로 감싸서 자동 재시도."""
|
||||
writing_cfg = self._config.get('writing', {})
|
||||
provider = writing_cfg.get('provider', 'claude')
|
||||
options = writing_cfg.get('options', {}).get(provider, {})
|
||||
fallback_chain = writing_cfg.get('fallback_chain', [])
|
||||
|
||||
writers = {
|
||||
'claude': ClaudeWriter,
|
||||
'openclaw': OpenClawWriter,
|
||||
'gemini': GeminiWriter,
|
||||
'groq': GroqWriter,
|
||||
'claude_web': ClaudeWebWriter,
|
||||
'gemini_web': GeminiWebWriter,
|
||||
}
|
||||
cls = writers.get(provider, ClaudeWriter)
|
||||
logger.info(f"Writer 로드: {provider} ({cls.__name__})")
|
||||
return cls(options)
|
||||
primary = cls(options)
|
||||
|
||||
if not with_fallback or not fallback_chain:
|
||||
return primary
|
||||
|
||||
# fallback 체인 구성
|
||||
fallback_writers = []
|
||||
for fb_provider in fallback_chain:
|
||||
if fb_provider == provider:
|
||||
continue
|
||||
fb_cls = writers.get(fb_provider)
|
||||
fb_opts = writing_cfg.get('options', {}).get(fb_provider, {})
|
||||
if fb_cls:
|
||||
fallback_writers.append((fb_provider, fb_cls(fb_opts)))
|
||||
|
||||
if not fallback_writers:
|
||||
return primary
|
||||
|
||||
return FallbackWriter(provider, primary, fallback_writers)
|
||||
|
||||
def get_tts(self) -> BaseTTS:
|
||||
"""현재 설정된 tts provider에 맞는 BaseTTS 구현체 반환"""
|
||||
|
||||
Reference in New Issue
Block a user