|
15 | 15 | import json |
16 | 16 | import os |
17 | 17 | import sys |
| 18 | +import urllib.error |
18 | 19 | import urllib.request |
19 | 20 |
|
20 | 21 | # 소스 코드로 볼 확장자(로직 취약점 리뷰 대상). 문서/락파일/바이너리는 제외. |
|
28 | 29 | ".next", ".nuxt", "target", "bin", "obj", ".terraform", "migrations", "backups", |
29 | 30 | } |
30 | 31 | PER_FILE_MAX = 60_000 # 파일당 상한(대형 생성물 제외) |
31 | | -DEFAULT_TOTAL_MAX = 200_000 # 1회 전송 총량 상한(토큰·비용 통제) |
| 32 | +DEFAULT_TOTAL_MAX = 120_000 # 1회 전송 총량 상한(토큰·비용·400 방지) |
32 | 33 |
|
33 | 34 | _SCHEMA_HINT = ( |
34 | 35 | 'Return ONLY JSON: {"findings":[{"file":"path","line":N,"severity":"HIGH|MEDIUM|LOW",' |
@@ -128,8 +129,16 @@ def call_claude(api_key: str, model: str, prompt: str, *, max_tokens: int = 4096 |
128 | 129 | headers={"x-api-key": api_key, "anthropic-version": "2023-06-01", |
129 | 130 | "content-type": "application/json"}, |
130 | 131 | ) |
131 | | - with urllib.request.urlopen(req, timeout=180) as resp: # noqa: S310 (fixed host) |
132 | | - data = json.loads(resp.read()) |
| 132 | + try: |
| 133 | + with urllib.request.urlopen(req, timeout=180) as resp: # noqa: S310 (fixed host) |
| 134 | + data = json.loads(resp.read()) |
| 135 | + except urllib.error.HTTPError as exc: # 400 등 — 실제 사유(모델/길이)를 노출 |
| 136 | + detail = "" |
| 137 | + try: |
| 138 | + detail = exc.read().decode("utf-8", "replace")[:600] |
| 139 | + except Exception: |
| 140 | + detail = getattr(exc, "reason", "") |
| 141 | + raise RuntimeError(f"Anthropic API {exc.code}: {detail}") from exc |
133 | 142 | chunks = [c.get("text", "") for c in data.get("content", []) if c.get("type") == "text"] |
134 | 143 | return "".join(chunks) |
135 | 144 |
|
@@ -164,10 +173,18 @@ def main() -> int: |
164 | 173 | print("스캔할 소스 파일 없음 — 0건으로 기록") |
165 | 174 | findings: list[dict] = [] |
166 | 175 | else: |
| 176 | + prompt = build_prompt(files) |
| 177 | + print(f"Claude 호출: 모델={model}, 프롬프트≈{len(prompt):,}자") |
167 | 178 | try: |
168 | | - findings = parse_findings(call_claude(api_key, model, build_prompt(files))) |
| 179 | + findings = parse_findings(call_claude(api_key, model, prompt)) |
169 | 180 | except Exception as exc: |
170 | 181 | print(f"Claude 리뷰 실패: {exc}", file=sys.stderr) |
| 182 | + msg = str(exc).lower() |
| 183 | + if "model" in msg: |
| 184 | + print("힌트: CLAUDE_MODEL 을 계정에서 지원하는 모델 id로 지정하세요 " |
| 185 | + "(예: claude-sonnet-4-5, 워크플로 env CLAUDE_MODEL).", file=sys.stderr) |
| 186 | + elif "long" in msg or "max" in msg or "token" in msg: |
| 187 | + print("힌트: 프롬프트가 큽니다 — DEFAULT_TOTAL_MAX 를 더 낮추세요.", file=sys.stderr) |
171 | 188 | return 1 |
172 | 189 | print(f"findings: {len(findings)}건 (모델 {model})") |
173 | 190 | try: |
|
0 commit comments