From 444c292f9c354a66630156ee29fee69439462913 Mon Sep 17 00:00:00 2001 From: Teknium <127238744+teknium1@users.noreply.github.com> Date: Mon, 10 Aug 2026 17:22:09 -0700 Subject: [PATCH] fix(pricing): treat negative catalog pricing sentinels as unknown Port from Kilo-Org/kilocode#13040: OpenRouter and OpenAI-compatible model catalogs publish negative pricing sentinels ("-1") for dynamically priced models such as openrouter/auto. The sentinel flowed straight through _pricing_entry_from_metadata into PricingEntry as -$1,000,000/M and produced large negative session costs in the usage ledger and cost displays. Negative pricing fields now degrade to None (unknown) via _to_nonnegative_decimal; an all-sentinel pricing block yields no pricing entry at all so downstream cost falls back to official-docs pricing or 'unknown'. Zero (free models) is unaffected. --- agent/usage_pricing.py | 29 ++++++++++--- tests/agent/test_usage_pricing.py | 67 +++++++++++++++++++++++++++++++ 2 files changed, 91 insertions(+), 5 deletions(-) diff --git a/agent/usage_pricing.py b/agent/usage_pricing.py index 04d34d13372c9..2c35079e3b147 100644 --- a/agent/usage_pricing.py +++ b/agent/usage_pricing.py @@ -980,6 +980,23 @@ def _to_decimal(value: Any) -> Optional[Decimal]: return None +def _to_nonnegative_decimal(value: Any) -> Optional[Decimal]: + """Parse a catalog pricing field, treating negative values as unknown. + + Dynamic model catalogs use negative sentinels for "pricing varies": + OpenRouter publishes ``"-1"`` for auto-routed models (``openrouter/auto``), + and OpenAI-compatible endpoints mirror that convention. Multiplying a + sentinel through the usage ledger produces large NEGATIVE session costs + (-$1,000,000/M input on the auto router), so an unpriceable field must + degrade to "unknown" (None), never a signed price. + (Port of Kilo-Org/kilocode#13040.) + """ + parsed = _to_decimal(value) + if parsed is None or parsed < 0: + return None + return parsed + + def _to_int(value: Any) -> int: try: return int(value or 0) @@ -1138,15 +1155,17 @@ def _pricing_entry_from_metadata( if model_id not in metadata: return None pricing = metadata[model_id].get("pricing") or {} - prompt = _to_decimal(pricing.get("prompt")) - completion = _to_decimal(pricing.get("completion")) - request = _to_decimal(pricing.get("request")) - cache_read = _to_decimal( + # Negative values are dynamic-pricing sentinels (e.g. OpenRouter's "-1" + # on openrouter/auto), not real prices — treat them as unknown. + prompt = _to_nonnegative_decimal(pricing.get("prompt")) + completion = _to_nonnegative_decimal(pricing.get("completion")) + request = _to_nonnegative_decimal(pricing.get("request")) + cache_read = _to_nonnegative_decimal( pricing.get("cache_read") or pricing.get("cached_prompt") or pricing.get("input_cache_read") ) - cache_write = _to_decimal( + cache_write = _to_nonnegative_decimal( pricing.get("cache_write") or pricing.get("cache_creation") or pricing.get("input_cache_write") diff --git a/tests/agent/test_usage_pricing.py b/tests/agent/test_usage_pricing.py index 3d5228d19ae6d..054e89dbbdb66 100644 --- a/tests/agent/test_usage_pricing.py +++ b/tests/agent/test_usage_pricing.py @@ -312,3 +312,70 @@ def test_vertex_default_model_estimates_cached_usage(monkeypatch): assert result.status == "estimated" assert result.amount_usd is not None and result.amount_usd > 0 + + +def test_negative_pricing_sentinels_are_treated_as_unknown(): + """OpenRouter publishes "-1" pricing sentinels for dynamically priced + models (e.g. openrouter/auto). Before this fix the sentinel flowed + straight into PricingEntry as -$1,000,000/M and produced large negative + session costs. Negative fields must degrade to None (unknown), and an + all-sentinel pricing block must yield no entry at all. + (Port of Kilo-Org/kilocode#13040.) + """ + from agent.usage_pricing import _pricing_entry_from_metadata + + all_sentinel = { + "openrouter/auto": {"pricing": {"prompt": "-1", "completion": "-1"}} + } + assert ( + _pricing_entry_from_metadata( + all_sentinel, + "openrouter/auto", + source_url="https://example.test/models", + pricing_version="test", + ) + is None + ) + + +def test_negative_pricing_fields_degrade_individually(): + """A mixed pricing block keeps valid non-negative fields and drops only + the negative ones (including cache fields).""" + from decimal import Decimal + + from agent.usage_pricing import _pricing_entry_from_metadata + + mixed = { + "m": { + "pricing": { + "prompt": "-1", + "completion": "0.000002", + "cache_read": "-0.5", + "request": "-1", + } + } + } + entry = _pricing_entry_from_metadata( + mixed, "m", source_url="https://example.test/models", pricing_version="test" + ) + assert entry is not None + assert entry.input_cost_per_million is None + assert entry.output_cost_per_million == Decimal("2.000000") + assert entry.cache_read_cost_per_million is None + assert entry.request_cost is None + + +def test_zero_pricing_still_produces_entry(): + """Zero is a legitimate price (free models) and must NOT be dropped by + the negative-sentinel guard.""" + from decimal import Decimal + + from agent.usage_pricing import _pricing_entry_from_metadata + + free = {"f": {"pricing": {"prompt": "0", "completion": "0"}}} + entry = _pricing_entry_from_metadata( + free, "f", source_url="https://example.test/models", pricing_version="test" + ) + assert entry is not None + assert entry.input_cost_per_million == Decimal("0") + assert entry.output_cost_per_million == Decimal("0")