diff --git a/headroom/proxy/cost.py b/headroom/proxy/cost.py index f520d09cb..e9fcdf63c 100644 --- a/headroom/proxy/cost.py +++ b/headroom/proxy/cost.py @@ -44,6 +44,22 @@ def _get_litellm_module() -> Any | None: logger = logging.getLogger("headroom.proxy") +# Pricing-lookup warnings are emitted on the per-request cost path, so an +# unresolvable model (a custom / OpenAI-compatible name LiteLLM can't price, +# e.g. glm-5.2) floods proxy.log with an identical WARNING every single request +# (#2504). Track which models have already been warned so each fires once per +# process; the set is tiny and bounded by the number of distinct models seen. +_warned_pricing_models: set[str] = set() + + +def _warn_pricing_once(model: str, message: str) -> None: + """Emit ``message`` at WARNING only the first time ``model`` fails pricing.""" + if model in _warned_pricing_models: + return + _warned_pricing_models.add(model) + logger.warning(message) + + # Provider-specific cache discount multipliers (what fraction of input price) # Used to calculate dollar savings from prefix caching _CACHE_ECONOMICS = { @@ -706,7 +722,10 @@ class CostTracker: """ litellm = _get_litellm_module() if litellm is None: - logger.warning("LiteLLM not available - cannot calculate costs") + _warn_pricing_once( + f"__litellm_unavailable__:{model}", + f"LiteLLM not available - cannot calculate costs for model {model}", + ) return None try: @@ -728,7 +747,7 @@ class CostTracker: return float(total_cost) if total_cost > 0 else None except Exception as e: - logger.warning(f"Failed to get pricing for model {model}: {e}") + _warn_pricing_once(model, f"Failed to get pricing for model {model}: {e}") return None def _prune_old_costs(self): diff --git a/tests/test_cost_pricing_warning_dedup.py b/tests/test_cost_pricing_warning_dedup.py new file mode 100644 index 000000000..520bef07a --- /dev/null +++ b/tests/test_cost_pricing_warning_dedup.py @@ -0,0 +1,65 @@ +"""Pricing-lookup warnings for an unresolvable model must fire once, not per request. + +#2504: a custom / OpenAI-compatible model LiteLLM can't price (e.g. glm-5.2) +logged an identical WARNING on every single request, flooding proxy.log. +""" + +from __future__ import annotations + +import logging + +import pytest + + +@pytest.fixture +def cost_tracker(monkeypatch: pytest.MonkeyPatch): + import headroom.proxy.cost as cost_mod + + # Reset the per-process dedup set so tests are order-independent. + cost_mod._warned_pricing_models.clear() + + class _FakeLiteLLM: + @staticmethod + def cost_per_token(**_kwargs): + raise RuntimeError("LLM Provider NOT provided.") + + monkeypatch.setattr(cost_mod, "_get_litellm_module", lambda: _FakeLiteLLM()) + return cost_mod.CostTracker() + + +def test_pricing_failure_warns_once_per_model(cost_tracker, caplog): + with caplog.at_level(logging.WARNING, logger="headroom.proxy"): + for _ in range(5): + assert cost_tracker.estimate_cost("glm-5.2", 100, 50) is None + + warnings = [ + r for r in caplog.records if "Failed to get pricing for model glm-5.2" in r.getMessage() + ] + assert len(warnings) == 1 + + +def test_distinct_models_each_warn_once(cost_tracker, caplog): + with caplog.at_level(logging.WARNING, logger="headroom.proxy"): + cost_tracker.estimate_cost("glm-5.2", 10, 5) + cost_tracker.estimate_cost("glm-5.2", 10, 5) + cost_tracker.estimate_cost("mystery-model", 10, 5) + cost_tracker.estimate_cost("mystery-model", 10, 5) + + msgs = [r.getMessage() for r in caplog.records if "Failed to get pricing" in r.getMessage()] + assert sum("for model glm-5.2:" in m for m in msgs) == 1 + assert sum("for model mystery-model:" in m for m in msgs) == 1 + + +def test_litellm_unavailable_warns_once_per_model(monkeypatch, caplog): + import headroom.proxy.cost as cost_mod + + cost_mod._warned_pricing_models.clear() + monkeypatch.setattr(cost_mod, "_get_litellm_module", lambda: None) + tracker = cost_mod.CostTracker() + + with caplog.at_level(logging.WARNING, logger="headroom.proxy"): + for _ in range(3): + assert tracker.estimate_cost("glm-5.2", 10, 5) is None + + unavailable = [r for r in caplog.records if "LiteLLM not available" in r.getMessage()] + assert len(unavailable) == 1