diff --git a/.github/scripts/auto_update_price_and_context_window_file.py b/.github/scripts/auto_update_price_and_context_window_file.py index 1b46aa17454..34e63308716 100644 --- a/.github/scripts/auto_update_price_and_context_window_file.py +++ b/.github/scripts/auto_update_price_and_context_window_file.py @@ -69,6 +69,10 @@ def write_to_file(file_path: str, data: dict[str, object]) -> None: def transform_openrouter_data(data): transformed = {} for row in data: + if float(row["pricing"]["prompt"]) < 0 or float(row["pricing"]["completion"]) < 0: + print(f"Skipping openrouter/{row['id']}: the catalog lists no usable per-token price for it.") + continue + # Add the fields 'max_tokens' and 'input_cost_per_token' obj = { "max_tokens": row["context_length"], @@ -106,8 +110,10 @@ def transform_openrouter_data(data): def _vercel_entry(model: VercelModel) -> dict[str, object] | None: mode: Final = VERCEL_TYPE_TO_MODE[model.type] pricing: Final = model.pricing if model.pricing is not None else VercelPricing() - if pricing.input is None or (mode == "chat" and pricing.output is None): - print(f"Skipping vercel_ai_gateway/{model.id}: the catalog lists no per-token price for it.") + input_price_unusable: Final = pricing.input is None or pricing.input < 0 + output_price_unusable: Final = mode == "chat" and (pricing.output is None or pricing.output < 0) + if input_price_unusable or output_price_unusable: + print(f"Skipping vercel_ai_gateway/{model.id}: the catalog lists no usable per-token price for it.") return None candidate: Final = { "max_tokens": model.max_tokens, diff --git a/tests/test_litellm/test_auto_update_price_and_context_window_file.py b/tests/test_litellm/test_auto_update_price_and_context_window_file.py index b97f6954b91..f98edb8b836 100644 --- a/tests/test_litellm/test_auto_update_price_and_context_window_file.py +++ b/tests/test_litellm/test_auto_update_price_and_context_window_file.py @@ -40,11 +40,31 @@ VIDEO_ROW = { "pricing": {"video_duration_pricing": [{"resolution": "720p", "price_per_second": "0.4"}]}, } IMAGE_ROW = {"id": "openai/gpt-image-1", "type": "image"} +NEGATIVE_PRICE_LANGUAGE_ROW = { + "id": "openrouter/auto", + "type": "language", + "context_window": 2000000, + "max_tokens": 2000000, + "pricing": {"input": "-1", "output": "-1"}, +} +OPENROUTER_ROW = { + "id": "anthropic/claude-sonnet-4.5", + "context_length": 1000000, + "pricing": {"prompt": "0.000003", "completion": "0.000015", "image": "0"}, + "top_provider": {"max_completion_tokens": 64000}, + "architecture": {"modality": "text+image->text"}, +} +OPENROUTER_VARIABLE_PRICE_ROW = { + "id": "openrouter/auto", + "context_length": 2000000, + "pricing": {"prompt": "-1", "completion": "-1"}, + "top_provider": {"max_completion_tokens": None}, +} def test_vercel_rows_without_per_token_pricing_are_skipped_instead_of_crashing() -> None: result = price_sync.transform_vercel_ai_gateway_data( - [LANGUAGE_ROW, EMBEDDING_ROW, UNPRICED_LANGUAGE_ROW, VIDEO_ROW, IMAGE_ROW] + [LANGUAGE_ROW, EMBEDDING_ROW, UNPRICED_LANGUAGE_ROW, VIDEO_ROW, IMAGE_ROW, NEGATIVE_PRICE_LANGUAGE_ROW] ) assert result == { @@ -71,6 +91,21 @@ def test_vercel_rows_without_per_token_pricing_are_skipped_instead_of_crashing() } +def test_openrouter_variable_priced_rows_are_skipped_so_the_schema_gate_stays_green() -> None: + result = price_sync.transform_openrouter_data([OPENROUTER_ROW, OPENROUTER_VARIABLE_PRICE_ROW]) + + assert result == { + "openrouter/anthropic/claude-sonnet-4.5": { + "max_tokens": 1000000, + "max_output_tokens": 64000, + "input_cost_per_token": 3e-06, + "output_cost_per_token": 1.5e-05, + "litellm_provider": "openrouter", + "mode": "chat", + } + } + + def test_write_to_file_matches_the_checked_in_cost_map_format(tmp_path: Path) -> None: target = tmp_path / "model_prices_and_context_window.json"