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 구현체 반환"""
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user