"""Claude client abstraction. SDK preferred, `claude -p` CLI fallback.""" from __future__ import annotations import os import shutil import subprocess from typing import Callable LLMCaller = Callable[..., str] # (prompt, *, model, max_tokens) -> response text def _have_anthropic_sdk() -> bool: try: import anthropic # noqa: F401 return True except ImportError: return False def _have_claude_cli() -> bool: return shutil.which("claude") is not None def _call_via_sdk(prompt: str, model: str, max_tokens: int) -> str: import anthropic client = anthropic.Anthropic() response = client.messages.create( model=model, max_tokens=max_tokens, messages=[{"role": "user", "content": prompt}], ) return response.content[0].text def _call_via_cli(prompt: str, model: str, max_tokens: int) -> str: """Shell out to Claude Code CLI. max_tokens is ignored (CLI handles defaults).""" result = subprocess.run( ["claude", "-p", prompt, "--model", model], capture_output=True, text=True, check=True, timeout=60, ) return result.stdout.strip() def call_claude( prompt: str, *, model: str = "claude-haiku-4-5", max_tokens: int = 1000, ) -> str: """Send a prompt to Claude. Tries the anthropic SDK first (requires ANTHROPIC_API_KEY), falls back to the `claude -p` CLI if available. Raises RuntimeError if neither works. """ if _have_anthropic_sdk() and os.getenv("ANTHROPIC_API_KEY"): return _call_via_sdk(prompt, model, max_tokens) if _have_claude_cli(): return _call_via_cli(prompt, model, max_tokens) raise RuntimeError( "No Claude client available. Either:\n" " - Install the `anthropic` SDK and set ANTHROPIC_API_KEY, or\n" " - Install Claude Code CLI (`claude` on PATH)" )