mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-06 02:48:13 +00:00
fix(sync-cost-map): reject non-finite catalog prices and rename the guard in the reasoning-effort docstring
This commit is contained in:
parent
d9e7448938
commit
5cc9690cb5
3 changed files with 63 additions and 49 deletions
|
|
@ -77,7 +77,7 @@ def declared_reasoning_efforts(model_info: Mapping[str, object]) -> tuple[str, .
|
|||
"""The entry's own answer, read through the same bare twin as the flags so both spellings of one
|
||||
model agree. Present-and-a-list IS the answer, so a declared [] correctly empties the group and
|
||||
an unknown level is dropped rather than raised: the bundled map is enum-validated by
|
||||
validate-model-prices-json, but an operator can put this key on a config.yaml model_info block
|
||||
cost-map-guard, but an operator can put this key on a config.yaml model_info block
|
||||
where that schema never runs, and one mistyped level must not fail every sibling on the proxy."""
|
||||
own: Final = model_info.get(_DECLARED_EFFORTS_KEY)
|
||||
raw: Final = own if own is not None else _bare_model_entry(model_info).get(_DECLARED_EFFORTS_KEY)
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ Policy:
|
|||
|
||||
import argparse
|
||||
import json
|
||||
import math
|
||||
import sys
|
||||
import time
|
||||
from collections.abc import Mapping, Sequence
|
||||
|
|
@ -120,7 +121,7 @@ def _token_price(raw: str | None) -> float | None:
|
|||
if raw is None:
|
||||
return None
|
||||
value: Final = float(raw)
|
||||
return per_token(value) if value >= 0 else None
|
||||
return per_token(value) if math.isfinite(value) and value >= 0 else None
|
||||
|
||||
|
||||
def _extra_price(raw: str | None) -> float | None:
|
||||
|
|
|
|||
|
|
@ -64,9 +64,9 @@ def _base_map() -> dict[str, object]:
|
|||
|
||||
@pytest.fixture(scope="module")
|
||||
def sync() -> ModuleType:
|
||||
spec = importlib.util.spec_from_file_location("sync_cost_map", SCRIPT_PATH)
|
||||
spec: Final = importlib.util.spec_from_file_location("sync_cost_map", SCRIPT_PATH)
|
||||
assert spec is not None and spec.loader is not None
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
module: Final = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
return module
|
||||
|
||||
|
|
@ -78,7 +78,7 @@ def _run(sync: ModuleType, cost_map: dict[str, object]):
|
|||
|
||||
|
||||
def test_new_openrouter_entry_carries_catalog_prices_limits_and_capabilities(sync: ModuleType) -> None:
|
||||
outcome = _run(sync, _base_map())
|
||||
outcome: Final = _run(sync, _base_map())
|
||||
|
||||
assert outcome.cost_map["openrouter/inception/mercury-2.5-preview"] == {
|
||||
"cache_read_input_token_cost": 4e-9,
|
||||
|
|
@ -98,11 +98,11 @@ def test_new_openrouter_entry_carries_catalog_prices_limits_and_capabilities(syn
|
|||
|
||||
|
||||
def test_input_modalities_become_capability_flags(sync: ModuleType) -> None:
|
||||
outcome = _run(sync, _base_map())
|
||||
outcome: Final = _run(sync, _base_map())
|
||||
|
||||
gpt5 = outcome.cost_map["openrouter/openai/gpt-5-mini"]
|
||||
gemma = outcome.cost_map["openrouter/google/gemma-4-26b-a4b-it:free"]
|
||||
vercel_gpt5 = outcome.cost_map["vercel_ai_gateway/openai/gpt-5-mini"]
|
||||
gpt5: Final = outcome.cost_map["openrouter/openai/gpt-5-mini"]
|
||||
gemma: Final = outcome.cost_map["openrouter/google/gemma-4-26b-a4b-it:free"]
|
||||
vercel_gpt5: Final = outcome.cost_map["vercel_ai_gateway/openai/gpt-5-mini"]
|
||||
assert (gpt5["supports_vision"], gpt5["supports_pdf_input"]) == (True, True)
|
||||
assert "supports_video_input" not in gpt5 and "supports_audio_input" not in gpt5
|
||||
assert "input_cost_per_image" not in gpt5 and "supports_prompt_caching" not in gpt5
|
||||
|
|
@ -113,16 +113,16 @@ def test_input_modalities_become_capability_flags(sync: ModuleType) -> None:
|
|||
|
||||
|
||||
def test_free_models_are_added_with_zero_prices(sync: ModuleType) -> None:
|
||||
outcome = _run(sync, _base_map())
|
||||
outcome: Final = _run(sync, _base_map())
|
||||
|
||||
free = outcome.cost_map["openrouter/cohere/north-mini-code:free"]
|
||||
free: Final = outcome.cost_map["openrouter/cohere/north-mini-code:free"]
|
||||
assert (free["input_cost_per_token"], free["output_cost_per_token"]) == (0.0, 0.0)
|
||||
assert (free["max_input_tokens"], free["max_output_tokens"], free["max_tokens"]) == (256000, 64000, 64000)
|
||||
assert "cache_read_input_token_cost" not in free
|
||||
|
||||
|
||||
def test_router_rows_and_unpriced_rows_are_skipped(sync: ModuleType) -> None:
|
||||
outcome = _run(sync, _base_map())
|
||||
outcome: Final = _run(sync, _base_map())
|
||||
|
||||
openrouter, vercel = outcome.providers
|
||||
assert "openrouter/openrouter/auto" not in outcome.cost_map
|
||||
|
|
@ -132,9 +132,9 @@ def test_router_rows_and_unpriced_rows_are_skipped(sync: ModuleType) -> None:
|
|||
|
||||
|
||||
def test_vercel_rows_map_type_to_mode_and_drop_non_token_types(sync: ModuleType) -> None:
|
||||
outcome = _run(sync, _base_map())
|
||||
outcome: Final = _run(sync, _base_map())
|
||||
|
||||
embedding = outcome.cost_map["vercel_ai_gateway/alibaba/qwen3-embedding-0.6b"]
|
||||
embedding: Final = outcome.cost_map["vercel_ai_gateway/alibaba/qwen3-embedding-0.6b"]
|
||||
assert embedding == {
|
||||
"input_cost_per_token": 1e-8,
|
||||
"litellm_provider": "vercel_ai_gateway",
|
||||
|
|
@ -150,15 +150,15 @@ def test_vercel_rows_map_type_to_mode_and_drop_non_token_types(sync: ModuleType)
|
|||
|
||||
|
||||
def test_existing_entry_is_repriced_without_losing_curated_fields(sync: ModuleType) -> None:
|
||||
outcome = _run(sync, _base_map())
|
||||
outcome: Final = _run(sync, _base_map())
|
||||
|
||||
deepseek = outcome.cost_map["openrouter/deepseek/deepseek-v4-pro-0813"]
|
||||
deepseek: Final = outcome.cost_map["openrouter/deepseek/deepseek-v4-pro-0813"]
|
||||
assert deepseek["input_cost_per_token"] == 5.7948e-7
|
||||
assert deepseek["output_cost_per_token"] == 1.73844e-6
|
||||
assert deepseek["cache_read_input_token_cost"] == 1.9316e-8
|
||||
assert deepseek["input_cost_per_token_cache_hit"] == 4.4e-8
|
||||
assert (deepseek["max_output_tokens"], deepseek["max_tokens"]) == (300000, 300000)
|
||||
glm = outcome.cost_map["vercel_ai_gateway/zai/glm-4.6"]
|
||||
glm: Final = outcome.cost_map["vercel_ai_gateway/zai/glm-4.6"]
|
||||
assert (glm["input_cost_per_token"], glm["output_cost_per_token"]) == (6e-7, 2.2e-6)
|
||||
assert glm["supports_parallel_function_calling"] is True
|
||||
assert glm["supports_reasoning"] is True
|
||||
|
|
@ -170,23 +170,23 @@ def test_existing_entry_is_repriced_without_losing_curated_fields(sync: ModuleTy
|
|||
|
||||
|
||||
def test_legacy_max_tokens_is_never_paired_with_a_different_max_output_tokens(sync: ModuleType) -> None:
|
||||
legacy = {
|
||||
legacy: Final = {
|
||||
"input_cost_per_token": 4e-8,
|
||||
"litellm_provider": "openrouter",
|
||||
"max_tokens": 8192,
|
||||
"mode": "chat",
|
||||
"output_cost_per_token": 1.5e-7,
|
||||
}
|
||||
outcome = _run(sync, {**_base_map(), "openrouter/inception/mercury-2.5-preview": legacy})
|
||||
outcome: Final = _run(sync, {**_base_map(), "openrouter/inception/mercury-2.5-preview": legacy})
|
||||
|
||||
mercury = outcome.cost_map["openrouter/inception/mercury-2.5-preview"]
|
||||
mercury: Final = outcome.cost_map["openrouter/inception/mercury-2.5-preview"]
|
||||
assert mercury["max_tokens"] == 8192
|
||||
assert "max_output_tokens" not in mercury
|
||||
assert mercury["max_input_tokens"] == 260000
|
||||
|
||||
|
||||
def test_untouched_entries_survive_byte_for_byte(sync: ModuleType) -> None:
|
||||
outcome = _run(sync, _base_map())
|
||||
outcome: Final = _run(sync, _base_map())
|
||||
|
||||
assert outcome.cost_map["gpt-4o"] == {"litellm_provider": "openai", "mode": "chat"}
|
||||
assert outcome.cost_map["openrouter/openai/gpt-3.5-turbo-instruct"] == BLOCK_END_OPENROUTER
|
||||
|
|
@ -194,9 +194,9 @@ def test_untouched_entries_survive_byte_for_byte(sync: ModuleType) -> None:
|
|||
|
||||
|
||||
def test_second_sync_is_a_no_op(sync: ModuleType) -> None:
|
||||
first = _run(sync, _base_map())
|
||||
first: Final = _run(sync, _base_map())
|
||||
|
||||
second = _run(sync, dict(first.cost_map))
|
||||
second: Final = _run(sync, dict(first.cost_map))
|
||||
|
||||
assert second.has_changes is False
|
||||
assert all(not provider.added and not provider.updated for provider in second.providers)
|
||||
|
|
@ -204,18 +204,21 @@ def test_second_sync_is_a_no_op(sync: ModuleType) -> None:
|
|||
|
||||
|
||||
def test_mode_mismatch_warns_and_leaves_the_entry_alone(sync: ModuleType) -> None:
|
||||
cost_map = _base_map()
|
||||
cost_map["vercel_ai_gateway/openai/gpt-5-mini"] = {
|
||||
"litellm_provider": "vercel_ai_gateway",
|
||||
"mode": "responses",
|
||||
"input_cost_per_token": 1.0,
|
||||
}
|
||||
|
||||
outcome = _run(sync, cost_map)
|
||||
outcome: Final = _run(
|
||||
sync,
|
||||
{
|
||||
**_base_map(),
|
||||
"vercel_ai_gateway/openai/gpt-5-mini": {
|
||||
"litellm_provider": "vercel_ai_gateway",
|
||||
"mode": "responses",
|
||||
"input_cost_per_token": 1.0,
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
assert outcome.cost_map["vercel_ai_gateway/openai/gpt-5-mini"]["input_cost_per_token"] == 1.0
|
||||
vercel = outcome.providers[1]
|
||||
assert "vercel_ai_gateway/openai/gpt-5-mini" not in outcome.providers[1].added
|
||||
vercel: Final = outcome.providers[1]
|
||||
assert "vercel_ai_gateway/openai/gpt-5-mini" not in vercel.added
|
||||
assert all("gpt-5-mini" not in line for line in vercel.updated)
|
||||
assert len(vercel.warnings) == 1
|
||||
assert "vercel_ai_gateway/openai/gpt-5-mini" in vercel.warnings[0]
|
||||
|
|
@ -223,7 +226,7 @@ def test_mode_mismatch_warns_and_leaves_the_entry_alone(sync: ModuleType) -> Non
|
|||
|
||||
|
||||
def test_new_keys_land_at_the_end_of_their_provider_block(sync: ModuleType) -> None:
|
||||
outcome = _run(sync, _base_map())
|
||||
outcome: Final = _run(sync, _base_map())
|
||||
|
||||
assert list(outcome.cost_map) == [
|
||||
"sample_spec",
|
||||
|
|
@ -242,9 +245,9 @@ def test_new_keys_land_at_the_end_of_their_provider_block(sync: ModuleType) -> N
|
|||
|
||||
|
||||
def test_provider_without_a_block_is_appended_at_the_end(sync: ModuleType) -> None:
|
||||
outcome = _run(sync, {"gpt-4o": {"litellm_provider": "openai", "mode": "chat"}})
|
||||
outcome: Final = _run(sync, {"gpt-4o": {"litellm_provider": "openai", "mode": "chat"}})
|
||||
|
||||
keys = list(outcome.cost_map)
|
||||
keys: Final = list(outcome.cost_map)
|
||||
assert keys[0] == "gpt-4o"
|
||||
assert keys[1:6] == sorted(keys[1:6]) and all(key.startswith("openrouter/") for key in keys[1:6])
|
||||
assert keys[6:] == sorted(keys[6:]) and all(key.startswith("vercel_ai_gateway/") for key in keys[6:])
|
||||
|
|
@ -252,7 +255,7 @@ def test_provider_without_a_block_is_appended_at_the_end(sync: ModuleType) -> No
|
|||
|
||||
|
||||
def test_pr_body_lists_changes_per_provider(sync: ModuleType) -> None:
|
||||
body = sync.render_pr_body(_run(sync, _base_map()))
|
||||
body: Final = sync.render_pr_body(_run(sync, _base_map()))
|
||||
|
||||
assert "## openrouter" in body and "## vercel_ai_gateway" in body
|
||||
assert "### Added (4)" in body and "- `openrouter/inception/mercury-2.5-preview`" in body
|
||||
|
|
@ -271,13 +274,23 @@ def test_pr_body_lists_changes_per_provider(sync: ModuleType) -> None:
|
|||
],
|
||||
)
|
||||
def test_malformed_catalogs_fail_the_run(sync: ModuleType, loader: str, raw: bytes) -> None:
|
||||
kwargs = {"now_ms": NOW_MS} if loader == "load_vercel" else {}
|
||||
kwargs: Final = {"now_ms": NOW_MS} if loader == "load_vercel" else {}
|
||||
with pytest.raises(sync.SyncError):
|
||||
getattr(sync, loader)(raw, **kwargs)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("price", ["Infinity", "-Infinity", "NaN"])
|
||||
def test_non_finite_catalog_prices_count_as_unpriced(sync: ModuleType, price: str) -> None:
|
||||
raw: Final = json.dumps({"data": [{"id": "acme/x", "pricing": {"prompt": price, "completion": "0"}}]}).encode()
|
||||
|
||||
catalog: Final = sync.load_openrouter(raw)
|
||||
|
||||
assert catalog.entries == ()
|
||||
assert dict(catalog.skipped) == {"unpriced or router": 1}
|
||||
|
||||
|
||||
def _vercel_language_row(deprecated_at: int | None) -> bytes:
|
||||
row = {
|
||||
row: Final = {
|
||||
"id": "acme/chat-1",
|
||||
"type": "language",
|
||||
"context_window": 1000,
|
||||
|
|
@ -289,8 +302,8 @@ def _vercel_language_row(deprecated_at: int | None) -> bytes:
|
|||
|
||||
|
||||
def test_a_scheduled_deprecation_keeps_syncing_until_the_date(sync: ModuleType) -> None:
|
||||
scheduled = sync.load_vercel(_vercel_language_row(NOW_MS + 1), now_ms=NOW_MS)
|
||||
passed = sync.load_vercel(_vercel_language_row(NOW_MS), now_ms=NOW_MS)
|
||||
scheduled: Final = sync.load_vercel(_vercel_language_row(NOW_MS + 1), now_ms=NOW_MS)
|
||||
passed: Final = sync.load_vercel(_vercel_language_row(NOW_MS), now_ms=NOW_MS)
|
||||
|
||||
assert [entry.key for entry in scheduled.entries] == ["vercel_ai_gateway/acme/chat-1"]
|
||||
assert dict(scheduled.skipped)["deprecated"] == 0
|
||||
|
|
@ -307,10 +320,10 @@ def _repo(tmp_path: Path) -> Path:
|
|||
|
||||
|
||||
def test_write_updates_both_cost_map_files_identically(sync: ModuleType, tmp_path: Path, capsys) -> None:
|
||||
repo = _repo(tmp_path)
|
||||
body_file = tmp_path / "body.md"
|
||||
repo: Final = _repo(tmp_path)
|
||||
body_file: Final = tmp_path / "body.md"
|
||||
|
||||
code = sync.main(
|
||||
code: Final = sync.main(
|
||||
[
|
||||
"--write",
|
||||
"--openrouter-json",
|
||||
|
|
@ -324,8 +337,8 @@ def test_write_updates_both_cost_map_files_identically(sync: ModuleType, tmp_pat
|
|||
]
|
||||
)
|
||||
|
||||
root = (repo / "model_prices_and_context_window.json").read_text()
|
||||
backup = (repo / "litellm" / "model_prices_and_context_window_backup.json").read_text()
|
||||
root: Final = (repo / "model_prices_and_context_window.json").read_text()
|
||||
backup: Final = (repo / "litellm" / "model_prices_and_context_window_backup.json").read_text()
|
||||
assert code == 0
|
||||
assert root == backup
|
||||
assert root.endswith("}\n")
|
||||
|
|
@ -335,10 +348,10 @@ def test_write_updates_both_cost_map_files_identically(sync: ModuleType, tmp_pat
|
|||
|
||||
|
||||
def test_dry_run_touches_nothing(sync: ModuleType, tmp_path: Path, capsys) -> None:
|
||||
repo = _repo(tmp_path)
|
||||
before = (repo / "model_prices_and_context_window.json").read_bytes()
|
||||
repo: Final = _repo(tmp_path)
|
||||
before: Final = (repo / "model_prices_and_context_window.json").read_bytes()
|
||||
|
||||
code = sync.main(
|
||||
code: Final = sync.main(
|
||||
[
|
||||
"--openrouter-json",
|
||||
str(FIXTURES / "openrouter_models.json"),
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue