Reproduction steps:
- Extract a clean station-v0.71 tarball, sys.path.insert(0, "workbench").
- import cost_router as CR
- vault = {"settings": {"monthly_budget_usd": 0}}
- remaining, cap = CR.budget_remaining(ws, vault)
- result = CR.pick(messages, vault, ws, available_providers=["openai"])
Expected: a monthly budget explicitly set to 0 should be reported as "$0
remaining, cap $0" (or equivalent zero-spend state), and pick()'s
90%-used tier-demotion logic should treat it as already over budget.
Actual:
budget_remaining(): (None, None)
pick(): {..., 'budget_remaining': None, ...}
A 0 budget is indistinguishable from no budget configured at all -- therailcall cost display would show no cap, and pick() never demotes tier
regardless of how much has actually been spent, because the 90%-used check
is itself gated on the same falsy value.
Root cause: workbench/cost_router.py, budget_remaining() (~line 201-215):
def budget_remaining(ws, vault):
budget = vault.get("settings", {})
if isinstance(budget, dict):
cap = budget.get("monthly_budget_usd")
else:
cap = None
if not cap: # <-- 0 is falsy, same branch as None/missing
return None, None
...
and pick() (~line 275): if cap and remaining is not None: -- the same
falsy-zero conflation gates whether the tier-demotion logic runs at all.
Both call sites treat "cap is unset" and "cap is explicitly zero" as the
same case, even though they mean opposite things (no restriction vs.
maximum restriction).
Suggested fix: check cap is not None instead of the truthiness of cap
in both places, so a 0 is honored as a real (zero) cap rather than folded
into the unconfigured case -- mirroring how a declared 0 is already handled
correctly elsewhere in this station's spend-cap logic (a declared 0 means
a zero-spend workflow, not "no cap declared").