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:
JOUNGWOOK KWON
2026-03-30 18:40:01 +09:00
parent 8a15148b7b
commit 8eb6b7a7f9
2 changed files with 104 additions and 4 deletions

View File

@@ -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 구현체 반환"""

View File

@@ -3,7 +3,8 @@
"_updated": "2026-03-29",
"writing": {
"provider": "gemini",
"_comment_provider": "openclaw=ChatGPT Pro(OAuth), claude_web=Claude Max(웹쿠키), gemini_web=Gemini Pro(웹쿠키), claude=Anthropic API키, gemini=Google AI API키",
"fallback_chain": ["groq"],
"_comment_provider": "openclaw=ChatGPT Pro(OAuth), claude_web=Claude Max(웹쿠키), gemini_web=Gemini Pro(웹쿠키), claude=Anthropic API키, gemini=Google AI API키, groq=Groq API키",
"options": {
"openclaw": {
"agent_name": "blog-writer",
@@ -29,6 +30,12 @@
"model": "gemini-2.5-flash",
"max_tokens": 4096,
"temperature": 0.7
},
"groq": {
"api_key_env": "GROQ_API_KEY",
"model": "llama-3.3-70b-versatile",
"max_tokens": 4096,
"temperature": 0.7
}
}
},