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.
This commit is contained in:
parent
e5e2fb8b2d
commit
444c292f9c
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
Loading…
Reference in New Issue