feat: add adaptive review effort budgets #19
@@ -163,6 +163,8 @@ def equivalent_cost(usage: dict, model: str, price_target: str = "") -> float:
|
|||||||
"""Estimate comparison cost for one completed iteration."""
|
"""Estimate comparison cost for one completed iteration."""
|
||||||
try:
|
try:
|
||||||
from cost_model import PRICES, Usage, cost
|
from cost_model import PRICES, Usage, cost
|
||||||
|
except (ImportError, ModuleNotFoundError):
|
||||||
|
return 0.0
|
||||||
target = price_target or os.environ.get("PRAGENT_PRICE_TARGET", "claude-sonnet-5")
|
target = price_target or os.environ.get("PRAGENT_PRICE_TARGET", "claude-sonnet-5")
|
||||||
price = PRICES.get(target)
|
price = PRICES.get(target)
|
||||||
if price is None:
|
if price is None:
|
||||||
@@ -173,5 +175,3 @@ def equivalent_cost(usage: dict, model: str, price_target: str = "") -> float:
|
|||||||
cache_writes=int(usage.get("cache_write") or 0),
|
cache_writes=int(usage.get("cache_write") or 0),
|
||||||
output=int(usage.get("output") or 0),
|
output=int(usage.get("output") or 0),
|
||||||
|
masi marked this conversation as resolved
Outdated
|
|||||||
), price)
|
), price)
|
||||||
except Exception:
|
|
||||||
return 0.0
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
"""Isolated opencode process runtime."""
|
"""Isolated opencode process runtime."""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
import selectors
|
||||||
import subprocess
|
import subprocess
|
||||||
import time
|
import time
|
||||||
|
|
||||||
@@ -130,6 +131,8 @@ def _run_process(
|
|||||||
cmd, cwd=cwd, env=env, capture_output=True, text=True,
|
cmd, cwd=cwd, env=env, capture_output=True, text=True,
|
||||||
stdin=subprocess.DEVNULL, timeout=timeout,
|
stdin=subprocess.DEVNULL, timeout=timeout,
|
||||||
)
|
)
|
||||||
|
if runner not in (None, subprocess.run):
|
||||||
|
raise ValueError("custom runners are unsupported for budgeted streaming")
|
||||||
existing_reason = budget_state.reason()
|
existing_reason = budget_state.reason()
|
||||||
if existing_reason:
|
if existing_reason:
|
||||||
budget_state.cap_reason = existing_reason
|
budget_state.cap_reason = existing_reason
|
||||||
@@ -142,9 +145,27 @@ def _run_process(
|
|||||||
previous = {"steps": 0, "total": 0, "output": 0, "cost": 0.0}
|
previous = {"steps": 0, "total": 0, "output": 0, "cost": 0.0}
|
||||||
cap_reason = ""
|
cap_reason = ""
|
||||||
started = time.monotonic()
|
started = time.monotonic()
|
||||||
|
selector = selectors.DefaultSelector()
|
||||||
try:
|
try:
|
||||||
assert proc.stdout is not None
|
assert proc.stdout is not None
|
||||||
for line in proc.stdout:
|
selector.register(proc.stdout, selectors.EVENT_READ)
|
||||||
|
while selector.get_map():
|
||||||
|
remaining = budget.max_duration_seconds - (time.monotonic() - started)
|
||||||
|
if remaining <= 0:
|
||||||
|
cap_reason = "max_duration_seconds"
|
||||||
|
budget_state.cap_reason = cap_reason
|
||||||
|
proc.terminate()
|
||||||
|
break
|
||||||
|
events = selector.select(timeout=remaining)
|
||||||
|
if not events:
|
||||||
|
cap_reason = "max_duration_seconds"
|
||||||
|
budget_state.cap_reason = cap_reason
|
||||||
|
proc.terminate()
|
||||||
|
break
|
||||||
|
line = proc.stdout.readline()
|
||||||
|
if not line:
|
||||||
|
selector.unregister(proc.stdout)
|
||||||
|
break
|
||||||
output.append(line)
|
output.append(line)
|
||||||
_, usage = parse_events("".join(output))
|
_, usage = parse_events("".join(output))
|
||||||
if usage:
|
if usage:
|
||||||
@@ -180,6 +201,7 @@ def _run_process(
|
|||||||
proc.kill()
|
proc.kill()
|
||||||
proc.wait()
|
proc.wait()
|
||||||
finally:
|
finally:
|
||||||
|
selector.close()
|
||||||
if proc.stdout:
|
if proc.stdout:
|
||||||
proc.stdout.close()
|
proc.stdout.close()
|
||||||
stderr = proc.stderr.read() if proc.stderr else ""
|
stderr = proc.stderr.read() if proc.stderr else ""
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
import json
|
import json
|
||||||
|
import subprocess
|
||||||
|
|
||||||
ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", ".."))
|
ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", ".."))
|
||||||
sys.path.insert(0, os.path.join(ROOT, "pilot"))
|
sys.path.insert(0, os.path.join(ROOT, "pilot"))
|
||||||
@@ -56,7 +57,20 @@ def test_process_terminates_after_step_budget():
|
|||||||
proc = opencode_runtime._run_process(
|
proc = opencode_runtime._run_process(
|
||||||
[sys.executable, "-u", "-c", code], cwd=".", env=os.environ.copy(),
|
[sys.executable, "-u", "-c", code], cwd=".", env=os.environ.copy(),
|
||||||
timeout=10, parse_events=parse_opencode_events, budget=budget,
|
timeout=10, parse_events=parse_opencode_events, budget=budget,
|
||||||
budget_state=state, model="glm-5.2:cloud", runner=None,
|
budget_state=state, model="glm-5.2:cloud", runner=subprocess.run,
|
||||||
)
|
)
|
||||||
assert state.snapshot()["cap_reason"] == "max_steps"
|
assert state.snapshot()["cap_reason"] == "max_steps"
|
||||||
assert proc.stdout.count("step_finish") == 1
|
assert proc.stdout.count("step_finish") == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_process_terminates_silent_child_at_duration_budget():
|
||||||
|
code = "import time; time.sleep(30)"
|
||||||
|
budget = Budget(max_steps=20, max_duration_seconds=1)
|
||||||
|
state = BudgetState(budget)
|
||||||
|
proc = opencode_runtime._run_process(
|
||||||
|
[sys.executable, "-u", "-c", code], cwd=".", env=os.environ.copy(),
|
||||||
|
timeout=10, parse_events=parse_opencode_events, budget=budget,
|
||||||
|
budget_state=state, model="glm-5.2:cloud", runner=subprocess.run,
|
||||||
|
)
|
||||||
|
assert state.snapshot()["cap_reason"] == "max_duration_seconds"
|
||||||
|
assert proc.stdout == ""
|
||||||
|
|||||||
Reference in New Issue
Block a user
🔴 [HIGH] equivalent_cost wraps the entire cost_model import and cost() call in 'except Exception', which silently catches ImportError/ModuleNotFoundError. If cost_model is not on the Python path at runtime (any deployment where pilot/ is not in sys.path), equivalent_cost returns 0.0 and max_equivalent_cost_usd budget enforcement is completely non-functional — the cost cap can never be reached.
Fix: Catch only ImportError/ModuleNotFoundError and let other exceptions propagate; or validate at startup that cost_model is importable when max_equivalent_cost_usd is configured.
🪙 ~2,951 (3.0K) tok (19% · attributed output)