feat: gemini-3-flash-preview + 역할별 thinkingBudget
- 모델: gemini-3-flash-preview (2.0 Flash → 3 Flash 업그레이드) - 역할별 thinkingBudget 동적 조절: unified/summarizer = 512 (가벼운 분류/요약) planner = 4096 (계획/검수) coder/reviewer = 8192 (구현/비평) - ~/.gemini/settings.json에 호출 직전 반영 - Docker에서도 settings.json 마운트로 동작
This commit is contained in:
@@ -1,12 +1,11 @@
|
||||
"""GeminiCaller — gemini CLI 호출.
|
||||
|
||||
두 가지 모드:
|
||||
1. call() — 텍스트 입출력 (분류/리뷰/총평)
|
||||
2. call_agent() — 프로젝트 디렉토리에서 에이전트 실행 (코딩)
|
||||
→ Gemini가 직접 파일 읽기/쓰기/명령 실행
|
||||
모든 역할이 gemini-3-flash-preview 모델을 사용하며,
|
||||
역할별 thinkingBudget을 동적으로 조절합니다.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import logging
|
||||
import time
|
||||
import sys
|
||||
@@ -15,6 +14,18 @@ from pathlib import Path
|
||||
logger = logging.getLogger("variet.gemini")
|
||||
|
||||
ROLE_PROMPTS_DIR = Path(__file__).parent.parent / "prompts"
|
||||
GEMINI_MODEL = "gemini-3-flash-preview"
|
||||
|
||||
# 역할별 thinkingBudget (토큰 단위)
|
||||
# 512=가벼운 분류/요약, 4096=계획/검수, 8192=구현/비평
|
||||
ROLE_THINKING: dict[str, int] = {
|
||||
"unified": 512,
|
||||
"summarizer": 512,
|
||||
"planner": 4096,
|
||||
"coder": 8192,
|
||||
"reviewer": 8192,
|
||||
}
|
||||
DEFAULT_THINKING = 4096
|
||||
|
||||
# 동시 호출 제한 (Gemini AI Ultra 120RPM 고려)
|
||||
_semaphore = asyncio.Semaphore(4)
|
||||
@@ -22,6 +33,9 @@ _semaphore = asyncio.Semaphore(4)
|
||||
# Windows에서 PS ExecutionPolicy 우회
|
||||
_IS_WIN = sys.platform == "win32"
|
||||
|
||||
# ~/.gemini/settings.json 경로
|
||||
_SETTINGS_PATH = Path.home() / ".gemini" / "settings.json"
|
||||
|
||||
|
||||
class GeminiCallError(Exception):
|
||||
"""Gemini CLI 호출 실패."""
|
||||
@@ -37,10 +51,34 @@ class GeminiCaller:
|
||||
self.last_call_time = 0.0
|
||||
|
||||
def _build_cmd(self):
|
||||
"""OS에 맞는 gemini 커맨드 빌드."""
|
||||
"""에 맞는 gemini 커맨드 빌드."""
|
||||
if _IS_WIN:
|
||||
return ["cmd", "/c", "gemini", "--approval-mode", "yolo"]
|
||||
return ["gemini", "--approval-mode", "yolo"]
|
||||
return ["cmd", "/c", "gemini", "--model", GEMINI_MODEL,
|
||||
"--approval-mode", "yolo"]
|
||||
return ["gemini", "--model", GEMINI_MODEL, "--approval-mode", "yolo"]
|
||||
|
||||
def _set_thinking_budget(self, role: str):
|
||||
"""역할별 thinkingBudget을 settings.json에 반영."""
|
||||
budget = ROLE_THINKING.get(role, DEFAULT_THINKING)
|
||||
try:
|
||||
if _SETTINGS_PATH.exists():
|
||||
settings = json.loads(_SETTINGS_PATH.read_text(encoding="utf-8"))
|
||||
else:
|
||||
settings = {}
|
||||
|
||||
# modelConfigs.default.thinkingConfig.thinkingBudget 설정
|
||||
configs = settings.setdefault("modelConfigs", {})
|
||||
default = configs.setdefault("default", {})
|
||||
thinking = default.setdefault("thinkingConfig", {})
|
||||
thinking["thinkingBudget"] = budget
|
||||
|
||||
_SETTINGS_PATH.write_text(
|
||||
json.dumps(settings, indent=2, ensure_ascii=False),
|
||||
encoding="utf-8",
|
||||
)
|
||||
logger.debug(f"thinkingBudget={budget} for role={role}")
|
||||
except Exception as e:
|
||||
logger.warning(f"thinkingBudget 설정 실패 (role={role}): {e}")
|
||||
|
||||
def _clean_output(self, raw: str) -> str:
|
||||
"""Gemini 출력에서 노이즈 라인 제거."""
|
||||
@@ -70,6 +108,7 @@ class GeminiCaller:
|
||||
|
||||
async def _call_text(self, role: str, context: str, timeout: int) -> str:
|
||||
"""텍스트 전용 호출."""
|
||||
self._set_thinking_budget(role)
|
||||
prompt_file = ROLE_PROMPTS_DIR / f"{role}.md"
|
||||
if prompt_file.exists():
|
||||
system_prompt = prompt_file.read_text(encoding="utf-8")
|
||||
@@ -143,6 +182,7 @@ class GeminiCaller:
|
||||
self, role: str, context: str, cwd: str, timeout: int,
|
||||
) -> str:
|
||||
"""에이전트 모드 구현."""
|
||||
self._set_thinking_budget(role)
|
||||
prompt_file = ROLE_PROMPTS_DIR / f"{role}.md"
|
||||
if prompt_file.exists():
|
||||
system_prompt = prompt_file.read_text(encoding="utf-8")
|
||||
|
||||
Reference in New Issue
Block a user