Enforce the token cap, and treat a spent budget as an interruption
CI / checks (push) Skipped
CI / checks (pull_request) Successful in 51s
CI / checks (push) Skipped
CI / checks (pull_request) Successful in 51s
Exceeding a LiteLLM spend limit takes external models away for the rest of the day, which makes three existing gaps consequential rather than untidy. - A13 calls the delegation caps hard on "turns, tokens, and wall clock", and the module docstring repeated it, but the token cap was decoration: nothing counted, and result.tokens was always zero. It is now metered from the usage the provider reports, so a confused delegate cannot spend a day's allowance. A5 keeps the *money* cap on the LiteLLM key, which is a different thing; this is a scope cap - The normalized route sent no ceiling at all, so truncation detection there had nothing to detect against. Both routes now send one - A budget refusal is recognised distinctly. It is not a rate limit: retrying cannot help, so the planner no longer burns a second call to fail identically And a delegation cut off by an exhausted budget is now "interrupted" rather than "failed", which preserves its branch and leaves its intent pending. The work on that branch may be perfectly good; discarding it because the budget ran out would throw away work for a reason that has nothing to do with the work. A3's resume rule already knows how to pick such a delegation back up. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
249bcb10fc
commit
8cfb45aed3
5 files changed
+232
-6
No files matched your search
@@ -20,7 +20,7 @@ from nisse_dev.control import (
|
||||
PlanningError,
|
||||
SubmitPlan,
|
||||
)
|
||||
from nisse_dev.llm import RoleMap
|
||||
from nisse_dev.llm import BudgetExhausted, RoleMap
|
||||
from nisse_dev.oplog import OpLog
|
||||
from nisse_dev.planner import PLANNER_TOOLS, LLMPlanningAgent
|
||||
from nisse_dev.state import StateStore
|
||||
@@ -279,3 +279,21 @@ def test_rejection_feedback_reaches_the_planner_via_the_log(
|
||||
isinstance(e, UserMessage) and "too vague" in e.text for e in replan_context
|
||||
)
|
||||
assert "v2" in transport.outbound[-1].text
|
||||
|
||||
|
||||
def test_budget_exhaustion_is_not_retried(store: StateStore) -> None:
|
||||
"""A budget will not reappear within a turn, so a retry buys nothing and
|
||||
delays telling the operator."""
|
||||
|
||||
class BrokeClient:
|
||||
def __init__(self) -> None:
|
||||
self.calls = 0
|
||||
|
||||
def complete(self, *args: Any, **kwargs: Any):
|
||||
self.calls += 1
|
||||
raise BudgetExhausted("daily budget exhausted")
|
||||
|
||||
client = BrokeClient()
|
||||
with pytest.raises(BudgetExhausted):
|
||||
LLMPlanningAgent(client, store, ROLES).plan("arch", "demo")
|
||||
assert client.calls == 1 # not two
|
||||
Reference in new issue
Block a user