fix(money): reject invalid Tavily rate tables

This commit is contained in:
shehjad-dev
2026-09-23 01:06:19 +06:00
parent 12d7d22790
commit 2613538eaa
7 changed files with 152 additions and 13 deletions
+3
View File
@@ -208,6 +208,9 @@ hold. Search reserves one or two credits and `settle._tavily_cost_micro` reads i
mapping and extraction modes; Tavily does not expose every successfully mapped page, so treg absorbs
any hidden mapping difference under the 20-page platform cap. Extract, Map and Crawl never use the
provider account's grouped `usage.credits` to decide which team pays. BYOK bypasses all metering.
Each endpoint's `tavily_rates` mapping has an exact mode-key contract. Catalog validation rejects an
incomplete, extra, non-finite or non-positive rate, and runtime repeats that check before reserve or
relay so catalog drift cannot silently turn a platform call into a free call.
A verification stamp proves the request shape, response shape, and paid behavior that the evidence
actually observed. A placeholder path value or a free miss does not prove a paid hit. Such rows keep
+1 -1
View File
@@ -503,7 +503,7 @@ Provider-specific calculation stays outside the faithful relay.
| Evidence | Settlement behavior |
|---|---|
| Generic catalog-reported charge | A paid synchronous cost may name `reported_charge.path` with unit `usd`. A finite nonnegative response value, including zero, settles exactly; invalid or absent evidence falls through to the normal estimate/miss behavior |
| Tavily Search | Reserve one credit for Basic, Fast and Ultra-fast or two for Advanced and an auto-selected depth; an explicit Basic depth overrides automatic selection. Platform Search requires caller-supplied `include_usage: true` and settles finite nonnegative per-request `usage.credits`. Empty results remain a paid routing miss. Missing or malformed usage keeps the frozen reserve. BYOK is unmetered and need not request usage |
| Tavily Search | Reserve one credit for Basic, Fast and Ultra-fast or two for Advanced and an auto-selected depth; an explicit Basic depth overrides automatic selection. Platform Search requires caller-supplied `include_usage: true` and settles finite nonnegative per-request `usage.credits`. Empty results remain a paid routing miss. Missing or malformed usage keeps the frozen reserve. BYOK is unmetered and need not request usage. The endpoint-specific rate table must be complete, positive and finite; catalog validation rejects bad declarations and runtime refuses the call before reserve or relay instead of pricing it at zero |
| Tavily Extract | Reserve the requested URL count (bounded by the documented 20-URL maximum) at 0.2 credit per Basic or 0.4 per Advanced extraction. Settle that fractional allocation for each valid entry in `results`; `failed_results` and grouped `usage.credits` do not charge the caller. A documented empty results list is free; malformed evidence keeps the frozen reserve |
| Tavily Map | Platform calls require an explicit integer `limit` from 1 to 20. Reserve that many pages at 0.1 credit each, or 0.2 when the caller supplied nonempty `instructions`; settle valid URL strings in `results` at the frozen per-page unit. Empty results are free, malformed evidence keeps the reserve, and grouped `usage.credits` is ignored |
| Tavily Crawl | Platform calls require the same 1-20 limit. Reserve per returned extraction at 0.3 credit (Basic), 0.4 (Basic + instructions), 0.5 (Advanced), or 0.6 (Advanced + instructions), then settle valid extracted entries in `results`. This is a conservative deterministic allocation, not the exact Tavily account charge: the response does not expose every page successfully mapped before extraction. treg absorbs any hidden mapping difference, bounded by the 20-page platform cap. Grouped `usage.credits` is ignored and BYOK remains unmetered |
+26
View File
@@ -257,6 +257,30 @@ def check_platform_request(rule: object, input_schema: object, where: str,
fail(errors, where, "platform_request value must match the field's singleton enum")
TAVILY_RATE_KEYS = {
"tavily.web.search": {"basic", "fast", "ultra_fast", "advanced"},
"tavily.web.extract": {"basic", "advanced"},
"tavily.web.map": {"regular", "instructions"},
"tavily.web.crawl": {
"basic", "basic_instructions", "advanced", "advanced_instructions",
},
}
def check_tavily_rates(endpoint_id: str, cost: dict, where: str,
errors: list[str]) -> None:
"""Tavily's provider-specific formulas require a complete positive finite mode table."""
expected = TAVILY_RATE_KEYS.get(endpoint_id)
if expected is None:
return
rates = cost.get("tavily_rates")
if not isinstance(rates, dict) or set(rates) != expected:
fail(errors, where, "cost.tavily_rates must contain exactly " + ", ".join(sorted(expected)))
return
if any(not _finite_number(value) or value <= 0 for value in rates.values()):
fail(errors, where, "cost.tavily_rates values must be positive finite numbers")
def check_platform_auth(ep: dict, where: str, errors: list[str]) -> None:
"""Anonymous platform fallback is intentionally narrow: proven public GETs that cost zero."""
mode = ep.get("platform_auth")
@@ -1045,6 +1069,8 @@ def main(argv: list[str]) -> int:
fail(errors, where, f"cost.type missing or not one of {sorted(COST_TYPES)}")
else:
check_cost(cost, where, errors, warnings, inp, provider)
if service == "tavily":
check_tavily_rates(eid, cost, where, errors)
effective_async = effective_async_descriptor(data.get("async"), ep.get("async"))
if effective_async is not None:
check_async_descriptor(effective_async, where, str(service), endpoint_index,
+48 -12
View File
@@ -4,6 +4,7 @@ from __future__ import annotations
import hashlib
import json
import math
import re
from email import policy
from email.parser import BytesParser
@@ -575,6 +576,14 @@ _TAVILY_ENDPOINTS = frozenset({
})
_TAVILY_BOUNDED_SITE_ENDPOINTS = frozenset({"tavily.web.map", "tavily.web.crawl"})
_TAVILY_PLATFORM_MAX_RESULTS = 20
_TAVILY_RATE_KEYS = {
"tavily.web.search": frozenset({"basic", "fast", "ultra_fast", "advanced"}),
"tavily.web.extract": frozenset({"basic", "advanced"}),
"tavily.web.map": frozenset({"regular", "instructions"}),
"tavily.web.crawl": frozenset({
"basic", "basic_instructions", "advanced", "advanced_instructions",
}),
}
def _openmart_credits(records: int) -> int:
@@ -602,31 +611,57 @@ def _openmart_requested_records(endpoint_id: str, body: bytes) -> int | None:
return value if type(value) is int else None
def _tavily_rate_credits(cost: dict, name: str) -> float:
"""Read Tavily's endpoint/mode credit allocation from catalog data."""
rates = cost.get("tavily_rates") or {}
value = rates.get(name)
return float(value) if isinstance(value, (int, float)) and not isinstance(value, bool) else 0.0
def _tavily_rate_credits(endpoint_id: str, cost: dict, name: str) -> float:
"""Read a complete positive Tavily rate set, or refuse the call before reserve/relay."""
rates = cost.get("tavily_rates")
expected = _TAVILY_RATE_KEYS[endpoint_id]
valid = (
isinstance(rates, dict)
and set(rates) == expected
and all(
isinstance(value, (int, float))
and not isinstance(value, bool)
and math.isfinite(float(value))
and value > 0
for value in rates.values()
)
)
if not valid:
raise ResolutionFailed(
"catalog_price_invalid", status_code=503, detail={
"error": "catalog_price_invalid",
"endpoint_id": endpoint_id,
"message": "Tavily pricing is unavailable because its catalog rates are invalid",
},
)
return float(rates[name])
def _tavily_pricing(endpoint_id: str, cost: dict, body: bytes) -> tuple[int, int]:
"""Return Tavily's bounded hold and frozen response unit without rewriting the request."""
rate = catalog_store.load().credit_rates.get("tavily")
if not rate:
return 0, 0
if (not isinstance(rate, (int, float)) or isinstance(rate, bool)
or not math.isfinite(float(rate)) or rate <= 0):
raise ResolutionFailed(
"catalog_price_invalid", status_code=503, detail={
"error": "catalog_price_invalid",
"endpoint_id": endpoint_id,
"message": "Tavily pricing is unavailable because its credit rate is invalid",
},
)
credit_micro = _usd_to_micro(float(rate))
document = _json_object(body)
if endpoint_id == "tavily.web.search":
depth = document.get("search_depth")
if depth == "advanced":
credits = _tavily_rate_credits(cost, "advanced")
credits = _tavily_rate_credits(endpoint_id, cost, "advanced")
elif depth in ("basic", "fast", "ultra-fast"):
# An explicit depth overrides auto_parameters, including explicit basic.
credits = _tavily_rate_credits(cost, str(depth).replace("-", "_"))
credits = _tavily_rate_credits(endpoint_id, cost, str(depth).replace("-", "_"))
elif document.get("auto_parameters") is True:
credits = _tavily_rate_credits(cost, "advanced")
credits = _tavily_rate_credits(endpoint_id, cost, "advanced")
else:
credits = _tavily_rate_credits(cost, "basic")
credits = _tavily_rate_credits(endpoint_id, cost, "basic")
return _usd_to_micro(float(rate) * credits), credit_micro
instructions = document.get("instructions")
@@ -645,7 +680,8 @@ def _tavily_pricing(endpoint_id: str, cost: dict, body: bytes) -> tuple[int, int
else:
depth = "advanced" if document.get("extract_depth") == "advanced" else "basic"
mode = f"{depth}_instructions" if instructed else depth
per_result_micro = _usd_to_micro(float(rate) * _tavily_rate_credits(cost, mode))
per_result_micro = _usd_to_micro(
float(rate) * _tavily_rate_credits(endpoint_id, cost, mode))
return count * per_result_micro, per_result_micro
+1
View File
@@ -34,6 +34,7 @@ _BLAME_BY_KIND: dict[str, Blame] = {
"trial_allowance_unavailable": "treg",
"trial_allowance_reached": "caller",
"platform_cap_unavailable": "treg",
"catalog_price_invalid": "treg",
"platform_daily_cap_reached": "caller",
"tag_budget_unavailable": "treg",
"tag_cardinality_exceeded": "caller",
+29
View File
@@ -560,6 +560,35 @@ def test_platform_request_requires_declared_fixed_body_value(rule, valid):
assert (not errors) is valid
def test_tavily_rates_require_complete_positive_finite_endpoint_tables():
cat = catalog_store.load()
for endpoint_id, expected in validator.TAVILY_RATE_KEYS.items():
cost = cat.by_id[endpoint_id]["cost"]
errors = []
validator.check_tavily_rates(endpoint_id, cost, endpoint_id, errors)
assert errors == []
assert set(cost["tavily_rates"]) == expected
valid = cat.by_id["tavily.web.search"]["cost"]["tavily_rates"]
broken = [
None,
{},
{key: value for key, value in valid.items() if key != "advanced"},
valid | {"typo": 1},
valid | {"basic": 0},
valid | {"basic": -1},
valid | {"basic": True},
valid | {"basic": "1"},
valid | {"basic": float("nan")},
valid | {"basic": float("inf")},
]
for rates in broken:
errors = []
validator.check_tavily_rates(
"tavily.web.search", {"tavily_rates": rates}, "test", errors)
assert errors
# ---- ContactOut ----
def _contactout_cost(eid):
+44
View File
@@ -1526,6 +1526,29 @@ def test_tavily_search_reserve_honors_depth_and_explicit_basic_override(body, ex
assert (reserve, unit) == (expected, 8_000)
@pytest.mark.parametrize("rates", [
None,
{},
{"basic": 1, "fast": 1, "ultra_fast": 1},
{"basic": 1, "fast": 1, "ultra_fast": 1, "advanced": 2, "typo": 1},
{"basic": 0, "fast": 1, "ultra_fast": 1, "advanced": 2},
{"basic": -1, "fast": 1, "ultra_fast": 1, "advanced": 2},
{"basic": True, "fast": 1, "ultra_fast": 1, "advanced": 2},
{"basic": "1", "fast": 1, "ultra_fast": 1, "advanced": 2},
{"basic": float("nan"), "fast": 1, "ultra_fast": 1, "advanced": 2},
{"basic": float("inf"), "fast": 1, "ultra_fast": 1, "advanced": 2},
])
def test_tavily_pricing_fails_closed_on_incomplete_or_invalid_rates(rates):
cost = _tavily_cost("tavily.web.search") | {"tavily_rates": rates}
with pytest.raises(ResolutionFailed) as caught:
call_resolution._marketplace_pricing(
"tavily", "tavily.web.search", cost, {},
b'{"query":"x","search_depth":"basic"}',
)
assert caught.value.kind == "catalog_price_invalid"
assert caught.value.status_code == 503
@pytest.mark.parametrize("count", [1, 4, 5, 6, 20])
def test_tavily_extract_reserve_and_basic_settlement_are_fractional_per_success(count):
urls = [f"https://example.com/{i}" for i in range(count)]
@@ -1690,6 +1713,27 @@ async def test_tavily_platform_gates_search_usage_and_site_work_limits(
assert await _balance(clients) == before
async def test_tavily_invalid_runtime_rates_refuse_before_reserve_and_relay(
clients, monkeypatch, tavily_platform_on,
):
rates = catalog_store.load().by_id["tavily.web.search"]["cost"]["tavily_rates"]
monkeypatch.setitem(rates, "basic", 0)
async def must_not_relay(*args, **kwargs):
raise AssertionError("invalid Tavily pricing must fail before upstream relay")
monkeypatch.setattr(call_service, "relay", must_not_relay)
before_balance = await _balance(clients)
before_entries = await _entries(clients)
response = await clients.post("/call/tavily.web.search", json={
"query": "x", "search_depth": "basic", "include_usage": True,
})
assert response.status_code == 503
assert response.json()["detail"]["error"] == "catalog_price_invalid"
assert await _balance(clients) == before_balance
assert await _entries(clients) == before_entries
@pytest.mark.parametrize("status", [401, 422, 429, 432, 433])
async def test_tavily_failures_release_the_hold_without_hiding_upstream_status(
clients, monkeypatch, tavily_platform_on, status,