Skip to content

Commit 4fd7b38

Browse files
Enforce workflow model policy (#64)
1 parent 8bf9854 commit 4fd7b38

2 files changed

Lines changed: 31 additions & 1 deletion

File tree

.github/workflows/behavioral.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,8 @@ jobs:
139139
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
140140
# Lets the harness default to this skill if a test relies on the env.
141141
BEHAVIORAL_SKILL: ${{ matrix.skill }}
142+
# Cost cap: sonnet only. The harness also enforces this under CI.
143+
BEHAVIORAL_MODEL: sonnet
142144
run: |
143145
set -euo pipefail
144146
test_file="tests/test_$(echo '${{ matrix.skill }}' | tr '-' '_').py"

eval/behavioral/harness.py

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,32 @@ def test_image_generation():
4444
DEFAULT_MODEL = os.environ.get("BEHAVIORAL_MODEL", "sonnet")
4545
DEFAULT_EFFORT = os.environ.get("BEHAVIORAL_EFFORT", "high")
4646

47+
# Automated runs are capped at sonnet: a behavioral run makes real cloud calls
48+
# (agent run + LLM judge), so a workflow picking an expensive model can quietly
49+
# run up a large bill. No override -- the cap is non-negotiable in CI.
50+
AUTOMATED_MODEL = "sonnet"
51+
_TRUTHY = {"1", "true", "yes", "on"}
52+
53+
54+
def _is_automated_env() -> bool:
55+
"""True under CI / an automated workflow (GitHub Actions sets both)."""
56+
return any(
57+
os.environ.get(var, "").strip().lower() in _TRUTHY
58+
for var in ("CI", "GITHUB_ACTIONS")
59+
)
60+
61+
62+
def _enforce_model_policy(model: str | None) -> str | None:
63+
"""Coerce non-sonnet models to sonnet in CI; pass through otherwise."""
64+
if model is None or not _is_automated_env() or "sonnet" in model.lower():
65+
return model
66+
print(
67+
f"[behavioral] automated run: coercing model '{model}' -> "
68+
f"'{AUTOMATED_MODEL}' to cap token usage.",
69+
flush=True,
70+
)
71+
return AUTOMATED_MODEL
72+
4773

4874
def _claude_env() -> dict[str, str]:
4975
"""Environment for `claude` subprocesses.
@@ -70,6 +96,7 @@ def check_api_reachable(model: str | None = DEFAULT_MODEL, timeout: int = 60) ->
7096
if not claude_bin:
7197
return False, "'claude' CLI not found on PATH"
7298

99+
model = _enforce_model_policy(model)
73100
cmd = [claude_bin, "-p", "Reply with the single word: ok", "--output-format", "json"]
74101
if model:
75102
cmd += ["--model", model]
@@ -314,7 +341,8 @@ def __init__(
314341
skill: str = DEFAULT_SKILL,
315342
effort: str | None = DEFAULT_EFFORT,
316343
) -> None:
317-
self.model = model
344+
# Coerce here so the agent run and the LLM judge share the capped model.
345+
self.model = _enforce_model_policy(model)
318346
self.skill = skill
319347
self.effort = effort
320348
self.workspace: Path | None = None

0 commit comments

Comments
 (0)