Skip to content

Commit fd0d34a

Browse files
feat(roles): add claude_cli provider — run kernel via Claude Code CLI (#38)
Adds a third LLM provider alongside anthropic and openai. Set `llm.provider: claude_cli` in evolution.yml to drive planner / evaluator / goal_evaluator / strategist through the local `claude` binary instead of an API key. Why: the bundled examples (oss_fix_demo) already use claude CLI for the executor, but the planning roles still required an Anthropic API key. With this, a Claude Pro subscription is enough to run the full kernel — lowers the bar for evaluators / readers who try the README quickstart. Implementation: - Each role gets a `_call_claude_cli(prompt, model)` that shells out to `claude -p --model <model> --output-format json`, reads `result` from the JSON envelope. Planner / evaluator also read `total_cost_usd` and total tokens so the kernel-level cost guards keep working. - All four roles route through the existing `provider` switch; default behavior unchanged (provider defaults stay anthropic / openai). - No new dependency — uses stdlib subprocess + json. Defaults: opt-in only. Existing configs continue to work bit-for-bit. Targets a v1.2.0 minor release.
1 parent 849e3a6 commit fd0d34a

4 files changed

Lines changed: 76 additions & 0 deletions

File tree

‎roles/evaluator.py‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,23 @@ def _call_llm(prompt: str, cfg: dict) -> tuple[str, int, float]:
4141
tokens = resp.usage.total_tokens
4242
return resp.choices[0].message.content, tokens, tokens * 3e-6
4343

44+
if provider == "claude_cli":
45+
import subprocess
46+
proc = subprocess.run(
47+
["claude", "-p", "--model", model, "--output-format", "json"],
48+
input=prompt,
49+
capture_output=True,
50+
text=True,
51+
)
52+
if proc.returncode != 0:
53+
raise RuntimeError(f"claude -p failed (exit {proc.returncode}): {proc.stderr[:200]}")
54+
data = json.loads(proc.stdout)
55+
text = data.get("result", "")
56+
usage = data.get("usage", {})
57+
tokens = usage.get("input_tokens", 0) + usage.get("output_tokens", 0)
58+
cost = float(data.get("total_cost_usd", 0.0))
59+
return text, tokens, cost
60+
4461
raise ValueError(f"unknown llm.provider: {provider!r}")
4562

4663

‎roles/goal_evaluator.py‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,21 @@ def _call_openai(prompt: str, model: str, api_key_env: str) -> str:
3838
return resp.choices[0].message.content
3939

4040

41+
def _call_claude_cli(prompt: str, model: str) -> str:
42+
"""Call Claude via the local Claude Code CLI (no API key required)."""
43+
import subprocess
44+
proc = subprocess.run(
45+
["claude", "-p", "--model", model, "--output-format", "json"],
46+
input=prompt,
47+
capture_output=True,
48+
text=True,
49+
)
50+
if proc.returncode != 0:
51+
raise RuntimeError(f"claude -p failed (exit {proc.returncode}): {proc.stderr[:200]}")
52+
data = json.loads(proc.stdout)
53+
return data.get("result", "")
54+
55+
4156
def main() -> None:
4257
parser = argparse.ArgumentParser()
4358
parser.add_argument("--input", required=True)
@@ -81,6 +96,8 @@ def main() -> None:
8196
text = _call_anthropic(prompt, model, api_key_env)
8297
elif provider == "openai":
8398
text = _call_openai(prompt, model, api_key_env)
99+
elif provider == "claude_cli":
100+
text = _call_claude_cli(prompt, model)
84101
else:
85102
print(f"error: unknown llm.provider: {provider!r}", file=sys.stderr)
86103
sys.exit(1)

‎roles/planner.py‎

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,29 @@ def _call_openai(prompt: str, model: str, api_key_env: str) -> tuple[str, int, f
4545
return resp.choices[0].message.content, tokens, cost
4646

4747

48+
def _call_claude_cli(prompt: str, model: str) -> tuple[str, int, float]:
49+
"""Call Claude via the local Claude Code CLI (no API key required).
50+
51+
Uses --output-format json to recover the response text plus real
52+
token / cost numbers, so kernel-level cost guards keep working.
53+
"""
54+
import subprocess
55+
proc = subprocess.run(
56+
["claude", "-p", "--model", model, "--output-format", "json"],
57+
input=prompt,
58+
capture_output=True,
59+
text=True,
60+
)
61+
if proc.returncode != 0:
62+
raise RuntimeError(f"claude -p failed (exit {proc.returncode}): {proc.stderr[:200]}")
63+
data = json.loads(proc.stdout)
64+
text = data.get("result", "")
65+
usage = data.get("usage", {})
66+
tokens = usage.get("input_tokens", 0) + usage.get("output_tokens", 0)
67+
cost = float(data.get("total_cost_usd", 0.0))
68+
return text, tokens, cost
69+
70+
4871
def main() -> None:
4972
parser = argparse.ArgumentParser()
5073
parser.add_argument("--input", required=True)
@@ -104,6 +127,8 @@ def main() -> None:
104127
text, tokens, cost = _call_anthropic(prompt, model, api_key_env)
105128
elif provider == "openai":
106129
text, tokens, cost = _call_openai(prompt, model, api_key_env)
130+
elif provider == "claude_cli":
131+
text, tokens, cost = _call_claude_cli(prompt, model)
107132
else:
108133
print(f"error: unknown llm.provider: {provider!r}", file=sys.stderr)
109134
sys.exit(1)

‎roles/strategist.py‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,21 @@ def _call_openai(prompt: str, model: str, api_key_env: str) -> str:
3838
return resp.choices[0].message.content
3939

4040

41+
def _call_claude_cli(prompt: str, model: str) -> str:
42+
"""Call Claude via the local Claude Code CLI (no API key required)."""
43+
import subprocess
44+
proc = subprocess.run(
45+
["claude", "-p", "--model", model, "--output-format", "json"],
46+
input=prompt,
47+
capture_output=True,
48+
text=True,
49+
)
50+
if proc.returncode != 0:
51+
raise RuntimeError(f"claude -p failed (exit {proc.returncode}): {proc.stderr[:200]}")
52+
data = json.loads(proc.stdout)
53+
return data.get("result", "")
54+
55+
4156
def main() -> None:
4257
parser = argparse.ArgumentParser()
4358
parser.add_argument("--input", required=True)
@@ -80,6 +95,8 @@ def main() -> None:
8095
text = _call_anthropic(prompt, model, api_key_env)
8196
elif provider == "openai":
8297
text = _call_openai(prompt, model, api_key_env)
98+
elif provider == "claude_cli":
99+
text = _call_claude_cli(prompt, model)
83100
else:
84101
print(f"error: unknown llm.provider: {provider!r}", file=sys.stderr)
85102
sys.exit(1)

0 commit comments

Comments
 (0)