diff --git a/ouroboros/loop_nudges.py b/ouroboros/loop_nudges.py index 02e2c54cf..f00580a14 100644 --- a/ouroboros/loop_nudges.py +++ b/ouroboros/loop_nudges.py @@ -334,6 +334,37 @@ def _maybe_inject_cost_budget_milestone( return True +def _own_round_cost_sentence(ctx: Any, cost: float) -> tuple[str, str]: + """The reminder's money sentence from COST EVIDENCE first: a positive priced + delta (provider-reported or estimated cost of the nanny's own rounds) is + metered money; no price is an unknown cash cost, never zero. The latest round's + route is named only as context, and a fallback clause appears only when a + fallback chain is actually configured (a paid round is no proof the next is).""" + usage = getattr(ctx, "_accumulated_usage", None) + meta = usage.get("_last_llm_call_meta") if isinstance(usage, dict) else None + meta = meta if isinstance(meta, dict) else {} + model = str(meta.get("resolved_model") or meta.get("model") or "") + route = " / ".join(item for item in (str(meta.get("provider") or ""), model) if item) + if cost > 0: + cost_class, sentence = "priced", ( + "Your own rounds here reported a price, so that spend is metered money") + else: + cost_class, sentence = "unpriced", ( + "No provider price reached this task for your own rounds here: an unknown " + "cash cost, not a zero one") + sentence += f" (latest round: {route})." if route else "." + try: + from ouroboros.model_slots import get_fallback_models + + fallbacks = get_fallback_models(str(meta.get("model") or "")) + except Exception: + fallbacks = [] + if fallbacks: + sentence += (f" If this route stops serving, the configured fallback ({', '.join(fallbacks[:3])}) " + "may take over, and a fallback round may be metered.") + return cost_class, sentence + + def _maybe_inject_nanny_economics_reminder( round_idx: int, messages: List[Dict[str, Any]], @@ -378,13 +409,13 @@ def _maybe_inject_nanny_economics_reminder( # BR1-3: never an unconditional "$0" claim — the owner's wording law is # typed cost classes: known-zero only on a settled $0 spend, never "free" # unqualified (estimated/undisclosed spend is never zero). + cost_class, cost_sentence = _own_round_cost_sentence(ctx, cost) reminder = ( "[NANNY ECONOMICS REMINDER]\n" f"You are a harness-dispatched NANNY and you have spent {_nanny_burn_phrase(rounds, cost)} " f"{since_phrase}. A subscription-lane delegated run has known-zero " "marginal cost only when its settled spend reports $0 (estimated or " - "undisclosed spend is never zero); every round you think yourself is " - "metered API money.\n" + f"undisclosed spend is never zero). {cost_sentence}\n" "This is a reminder, not a stop. Consider: delegate the remaining work " "(delegate_start / delegate_wait — follow-up work and fixes are delegated too), " "and keep your own rounds for judgment: acceptance, integration, honest " @@ -400,6 +431,7 @@ def _maybe_inject_nanny_economics_reminder( "round": round_idx, "metered_rounds_since_delegate_activity": rounds, "metered_cost_since_delegate_activity_usd": round(cost, 4), + "own_round_cost_class": cost_class, }) return True diff --git a/ouroboros/nanny_pacing.py b/ouroboros/nanny_pacing.py index 42b2383be..1a362f421 100644 --- a/ouroboros/nanny_pacing.py +++ b/ouroboros/nanny_pacing.py @@ -131,7 +131,7 @@ def nanny_burn_phrase(rounds: int, cost: float) -> str: return f"~${cost:.2f} of your own metered spend" if cost > 0: return f"{rounds} of your own metered LLM rounds (~${cost:.2f})" - return f"{rounds} of your own metered LLM rounds" + return f"{rounds} of your own LLM rounds (no provider price reported: cash cost unknown, not zero)" # Compatibility spellings retained on ``ouroboros.loop`` through imports. diff --git a/tests/test_nanny_economics.py b/tests/test_nanny_economics.py index 9e166c226..b8d5398e0 100644 --- a/tests/test_nanny_economics.py +++ b/tests/test_nanny_economics.py @@ -904,3 +904,47 @@ def test_burn_phrase_never_claims_zero_rounds_with_real_dollars(): assert "0 of your own metered LLM rounds" not in _nanny_burn_phrase(0, 2.45) assert "$2.45" in _nanny_burn_phrase(0, 2.45) assert "3 of your own metered LLM rounds" in _nanny_burn_phrase(3, 2.45) + + +# -- the money sentence follows the round's own cost evidence ------------------ + + +def _fire_reminder(cost_per_round, meta, monkeypatch, fallbacks=""): + from ouroboros.loop import _maybe_inject_nanny_economics_reminder, _note_nanny_delegate_activity + + monkeypatch.setenv("OUROBOROS_MODEL_FALLBACKS", fallbacks) + monkeypatch.delenv("OUROBOROS_MODEL_FALLBACK", raising=False) + ctx = _nanny_ctx(_accumulated_usage={"_last_llm_call_meta": meta}) + tools = SimpleNamespace(_ctx=ctx) + _note_nanny_delegate_activity(ctx, 1, {"cost": 0.0}, [_delegate_call()]) + round_idx, msgs = 1, [] + while not msgs: + round_idx += 1 + _note_nanny_delegate_activity(ctx, round_idx, {"cost": cost_per_round * round_idx}, []) + _maybe_inject_nanny_economics_reminder(round_idx, msgs, tools, lambda *_: None) + return "\n".join(m.get("content", "") for m in msgs) + + +def test_an_unpriced_round_is_an_unknown_cash_cost_never_metered_or_zero(monkeypatch): + text = _fire_reminder(0.0, {"provider": "claudexor", "model": "codex=gpt"}, monkeypatch) + assert "metered API money" not in text and "metered LLM rounds" not in text + assert "cash cost unknown, not zero" in text # burn phrase, cost class 'unpriced' + assert "claudexor / codex=gpt" in text # the route, named as context only + assert "only when its settled spend reports $0" in text # the pinned conditional stays + assert "fallback" not in text # no chain configured, no claim + + +def test_a_priced_round_stays_metered_and_names_a_configured_fallback(monkeypatch): + text = _fire_reminder(0.05, {"provider": "openrouter", "model": "anthropic/claude"}, + monkeypatch, fallbacks="openai/gpt-x, google/gem") + assert "metered LLM rounds" in text and "that spend is metered money" in text + assert "cash cost unknown" not in text + assert "configured fallback (openai/gpt-x, google/gem)" in text + + +def test_the_unpriced_burn_phrase_never_calls_rounds_metered(): + from ouroboros.loop import _nanny_burn_phrase + + assert _nanny_burn_phrase(4, 0.0) == ( + "4 of your own LLM rounds (no provider price reported: cash cost unknown, not zero)") + assert "4 of your own metered LLM rounds" in _nanny_burn_phrase(4, 0.5)