Merge pull request #41269 from BerriAI/litellm_ban_vendor_fact_pinning_tests

test: drop tests that pin vendor facts and add the CLAUDE.md rule
This commit is contained in:
kerry-berri 2026-09-15 13:16:55 -07:00 committed by GitHub
commit 7d3917dbf4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
38 changed files with 8 additions and 1751 deletions

View file

@ -25,6 +25,8 @@ Same thing for bug fixes. The tests should make it so that this specific bug can
Never test structure of code only function of it
A test must only fail when litellm code changes. Never pin facts we don't own (a vendor's price, a third party's field, an upstream default, today's date) as literals or as "X must be absent"; assert the invariant our code guarantees instead, e.g. two rows agree, a value is within range, a field is derived from another. If an outside fact is truly load-bearing, cite its source and date next to the assertion so a reader can tell stale from broken
`tests/test_litellm/` mirrors `litellm/` in a parallel path (see `tests/test_litellm/readme.md`). Name tests `test_<filename>.py`, but always match the existing test file in the directory you touch — many provider dirs use longer descriptive names (e.g. `test_anthropic_chat_transformation.py`) to avoid ambiguity across sibling folders. For bug fixes, extend the existing mapped test file rather than creating a new one. Only create a new test file for a new feature (provider, endpoint, or transformation module) that has no mapped test yet, following that directory's naming convention (or `test_<filename>.py` if you're the first test there). One focused regression test beats many shallow ones
End-to-end tests belong in `tests/e2e/` and must follow the harness conventions documented in that directory's `CLAUDE.md`

View file

@ -2321,36 +2321,6 @@ def test_gpt55_dated_variants_match_base_reasoning_effort_capabilities(_local_mo
)
@pytest.mark.parametrize(
"model,expected_mode,expected_input,expected_output,expected_cache_read",
[
("azure/gpt-5.5", "chat", 5e-6, 3e-5, 5e-7),
("azure/gpt-5.5-2026-04-23", "chat", 5e-6, 3e-5, 5e-7),
("azure/gpt-5.5-pro", "responses", 3e-5, 1.8e-4, 3e-6),
("azure/gpt-5.5-pro-2026-04-23", "responses", 3e-5, 1.8e-4, 3e-6),
],
)
def test_azure_gpt55_entries_present_with_correct_pricing(_local_model_cost_map,
model, expected_mode, expected_input, expected_output, expected_cache_read
):
"""Day-0 Azure entries for GPT-5.5 mirror the OpenAI pricing structure.
Pricing parity with openai/gpt-5.5* (verified against OpenAI's pricing page
on 2026-04-24): $5/$30 input/output per 1M for chat, $30/$180 for pro.
Cache discount is 10% of input.
"""
m = litellm.model_cost[model]
assert m["litellm_provider"] == "azure"
assert m["mode"] == expected_mode
assert m["input_cost_per_token"] == expected_input
assert m["output_cost_per_token"] == expected_output
assert m["cache_read_input_token_cost"] == expected_cache_read
# Long-context window inherited from gpt-5.4 / openai gpt-5.5.
assert m["max_input_tokens"] == 1050000
assert m["max_output_tokens"] == 128000
@pytest.mark.parametrize(
"model,expected_none,expected_minimal,expected_xhigh",
[
@ -3414,8 +3384,6 @@ def test_query_count_is_free_without_a_per_query_price(_local_model_cost_map):
# ---------------------------------------------------------------------------
@pytest.mark.parametrize("model", ["gpt-5.4", "gpt-realtime-2.1", "gpt-realtime-2.1-mini"])
@pytest.mark.parametrize("data_residency", ["eu", "us"])
def test_data_residency_applies_uplift(data_residency, model, _local_model_cost_map):
@ -4556,20 +4524,6 @@ GEMINI_DAY0_LAUNCH_PRICING = [
]
@pytest.mark.parametrize("model,input_cost,output_cost,cache_read_cost", GEMINI_DAY0_LAUNCH_PRICING)
def test_gemini_36_flash_and_35_flash_lite_launch_pricing(_local_model_cost_map, model, input_cost, output_cost, cache_read_cost):
model_cost_map = litellm.model_cost[model]
assert model_cost_map["input_cost_per_token"] == input_cost
assert model_cost_map["output_cost_per_token"] == output_cost
assert model_cost_map["output_cost_per_reasoning_token"] == output_cost
assert model_cost_map["cache_read_input_token_cost"] == cache_read_cost
assert model_cost_map["mode"] == "chat"
assert model_cost_map["supports_reasoning"] is True
assert model_cost_map["supports_function_calling"] is True
assert model_cost_map["max_input_tokens"] == 1048576
def test_generic_cost_per_token_gemini_36_flash(_local_model_cost_map):
usage = Usage(
@ -4598,44 +4552,6 @@ GEMINI_36_FLASH_SERVICE_TIER_PRICING = [
]
@pytest.mark.parametrize(
"service_tier,input_rate,output_rate,cache_read_rate", GEMINI_36_FLASH_SERVICE_TIER_PRICING
)
@pytest.mark.parametrize(
"model", ["gemini-3.6-flash", "gemini/gemini-3.6-flash", "vertex_ai/gemini-3.6-flash"]
)
def test_gemini_36_flash_service_tier_introductory_pricing(
model, service_tier, input_rate, output_rate, cache_read_rate, _local_model_cost_map
):
"""Regression: every 3.6 Flash tier is on Google's introductory rates through 2026-12-31,
so flex and priority requests must not be billed at the post-introductory rates."""
usage = Usage(
prompt_tokens=1_000,
completion_tokens=500,
total_tokens=1_500,
prompt_tokens_details=PromptTokensDetailsWrapper(cached_tokens=200, text_tokens=800),
)
prompt_cost, completion_cost = generic_cost_per_token(
model=model.split("/")[-1],
usage=usage,
custom_llm_provider=model.split("/")[0] if "/" in model else "gemini",
service_tier=service_tier,
)
assert prompt_cost == pytest.approx(800 * input_rate + 200 * cache_read_rate, rel=1e-9)
assert completion_cost == pytest.approx(500 * output_rate, rel=1e-9)
@pytest.mark.parametrize(
"model", ["gemini-3.6-flash", "gemini/gemini-3.6-flash", "vertex_ai/gemini-3.6-flash"]
)
def test_gemini_36_flash_batch_introductory_pricing(model, _local_model_cost_map):
model_cost_map = litellm.model_cost[model]
assert model_cost_map["input_cost_per_token_batches"] == 3.75e-07
assert model_cost_map["output_cost_per_token_batches"] == 1.875e-06
def test_generic_cost_per_token_gemini_35_flash_lite(_local_model_cost_map):
usage = Usage(
@ -4667,43 +4583,6 @@ GEMINI_35_FLASH_LITE_TIER_RATES_BY_SURFACE = [
]
@pytest.mark.parametrize(
"custom_llm_provider,service_tier,input_rate,output_rate,cache_read_rate",
GEMINI_35_FLASH_LITE_TIER_RATES_BY_SURFACE,
)
def test_gemini_35_flash_lite_service_tier_pricing(
custom_llm_provider, service_tier, input_rate, output_rate, cache_read_rate, _local_model_cost_map
):
"""Regression: Vertex publishes flash-lite flex context caching at $0.015/M while the
Gemini API publishes $0.02/M, so vertex_ai flex cache reads must bill 1.5e-08/token
instead of the 2e-08 the map used to carry, without disturbing the Gemini API rate."""
usage = Usage(
prompt_tokens=1_000,
completion_tokens=500,
total_tokens=1_500,
prompt_tokens_details=PromptTokensDetailsWrapper(cached_tokens=200, text_tokens=800),
)
prompt_cost, completion_cost = generic_cost_per_token(
model="gemini-3.5-flash-lite",
usage=usage,
custom_llm_provider=custom_llm_provider,
service_tier=service_tier,
)
assert prompt_cost == pytest.approx(800 * input_rate + 200 * cache_read_rate, rel=1e-9)
assert completion_cost == pytest.approx(500 * output_rate, rel=1e-9)
def test_gemini_35_flash_lite_flex_cache_read_map_entries(_local_model_cost_map):
"""Each map entry carries its own surface's published flex cache-read rate: the bare
and vertex_ai keys are the Vertex surface at $0.015/M, the gemini key is the Gemini
API surface at $0.02/M."""
assert litellm.model_cost["gemini-3.5-flash-lite"]["cache_read_input_token_cost_flex"] == 1.5e-08
assert litellm.model_cost["vertex_ai/gemini-3.5-flash-lite"]["cache_read_input_token_cost_flex"] == 1.5e-08
assert litellm.model_cost["gemini/gemini-3.5-flash-lite"]["cache_read_input_token_cost_flex"] == 2e-08
@pytest.mark.parametrize(
"service_tier,input_rate,cache_read_rate,cache_write_rate,output_rate",
[
@ -4932,19 +4811,6 @@ GEMINI_37_FLASH_LAUNCH_PRICING = [
]
@pytest.mark.parametrize("model,input_cost,output_cost,cache_read_cost", GEMINI_37_FLASH_LAUNCH_PRICING)
def test_gemini_37_flash_launch_pricing(model, input_cost, output_cost, cache_read_cost, _local_model_cost_map):
model_cost_map = litellm.model_cost[model]
assert model_cost_map["input_cost_per_token"] == input_cost
assert model_cost_map["output_cost_per_token"] == output_cost
assert model_cost_map["output_cost_per_reasoning_token"] == output_cost
assert model_cost_map["cache_read_input_token_cost"] == cache_read_cost
assert model_cost_map["mode"] == "chat"
assert model_cost_map["supports_reasoning"] is True
assert model_cost_map["supports_function_calling"] is True
assert model_cost_map["max_input_tokens"] == 1048576
def test_generic_cost_per_token_gemini_37_flash(_local_model_cost_map):
usage = Usage(
prompt_tokens=1000,
@ -4972,19 +4838,6 @@ GEMINI_38_FLASH_LAUNCH_PRICING = [
]
@pytest.mark.parametrize("model,input_cost,output_cost,cache_read_cost", GEMINI_38_FLASH_LAUNCH_PRICING)
def test_gemini_38_flash_launch_pricing(model, input_cost, output_cost, cache_read_cost, _local_model_cost_map):
model_cost_map = litellm.model_cost[model]
assert model_cost_map["input_cost_per_token"] == input_cost
assert model_cost_map["output_cost_per_token"] == output_cost
assert model_cost_map["output_cost_per_reasoning_token"] == output_cost
assert model_cost_map["cache_read_input_token_cost"] == cache_read_cost
assert model_cost_map["mode"] == "chat"
assert model_cost_map["supports_reasoning"] is True
assert model_cost_map["supports_function_calling"] is True
assert model_cost_map["max_input_tokens"] == 1048576
GEMINI_38_FLASH_FIELDS_SHARED_WITH_37_FLASH = (
"input_cost_per_token",
"output_cost_per_token",
@ -5045,20 +4898,6 @@ def test_generic_cost_per_token_gemini_38_flash(_local_model_cost_map):
assert completion_cost == pytest.approx(0.001875)
def test_grok_46_launch_pricing(_local_model_cost_map):
model_cost_map = litellm.model_cost["xai/grok-4.6"]
assert model_cost_map["input_cost_per_token"] == 2e-06
assert model_cost_map["output_cost_per_token"] == 6e-06
assert model_cost_map["cache_read_input_token_cost"] == 5e-07
assert model_cost_map["input_cost_per_token_above_200k_tokens"] == 4e-06
assert model_cost_map["output_cost_per_token_above_200k_tokens"] == 1.2e-05
assert model_cost_map["cache_read_input_token_cost_above_200k_tokens"] == 1e-06
assert model_cost_map["mode"] == "chat"
assert model_cost_map["supports_reasoning"] is True
assert model_cost_map["supports_function_calling"] is True
assert model_cost_map["max_input_tokens"] == 500000
def test_generic_cost_per_token_grok_46(_local_model_cost_map):
usage = Usage(
prompt_tokens=1_000,

View file

@ -1,6 +1,4 @@
import json
from collections.abc import Mapping, Sequence
from pathlib import Path
import pytest
@ -892,29 +890,6 @@ def test_gpt_4o_mini_snapshot_bills_web_search_like_its_alias(
assert snapshot_cost == alias_cost == 0.025
def test_gpt_4o_mini_web_search_price_matches_in_both_cost_maps():
repo_root = Path(__file__).parents[4]
cost_maps = tuple(
json.loads((repo_root / path).read_text(encoding="utf-8"))
for path in (
"model_prices_and_context_window.json",
"litellm/model_prices_and_context_window_backup.json",
)
)
canonical, backup = cost_maps
expected_search_price = {
"search_context_size_low": 0.025,
"search_context_size_medium": 0.025,
"search_context_size_high": 0.025,
}
for model_name in ("gpt-4o-mini", "gpt-4o-mini-2024-07-18"):
canonical_entry = canonical[model_name]
backup_entry = backup[model_name]
assert canonical_entry["search_context_cost_per_query"] == expected_search_price
assert backup_entry["search_context_cost_per_query"] == expected_search_price
assert canonical_entry == backup_entry
# Note: File search integration test removed due to complex annotation detection logic
# The unit tests in test_azure_assistant_cost_tracking.py provide comprehensive coverage

View file

@ -627,17 +627,6 @@ def test_shipped_adaptive_rule_requires_claude_prefix(shipped_cost_map):
litellm.get_model_info(model)
def test_shipped_exact_entry_beats_rules(shipped_cost_map):
model = "us.anthropic.claude-sonnet-4-6"
assert model in litellm.model_cost
info = litellm.get_model_info(model, custom_llm_provider="bedrock")
assert info["litellm_provider"] == "bedrock_converse"
assert info["input_cost_per_token"] == 3.3e-06
assert info["max_input_tokens"] == 1000000
assert info["supports_adaptive_thinking"] is True
assert info.get("supports_mid_conversation_system") is None
def test_shipped_rules_lose_to_exact_entries_across_cost_ladder_variants(shipped_cost_map):
"""A route-mangled variant of an exactly-mapped model must never resolve from
rules. The cost calculator tries model-name variants in order; a rule-derived

View file

@ -225,36 +225,6 @@ def test_shipped_backup_marks_claude_4_6_plus_adaptive_not_4_0():
assert "supports_adaptive_thinking" not in backup[non_adaptive], non_adaptive
@pytest.mark.parametrize(
"cost_map",
[_load_root_cost_map(), GetModelCostMap.load_local_model_cost_map()],
ids=["root", "bundled_backup"],
)
def test_azure_ai_claude_1m_context_entries(cost_map: dict):
"""Microsoft Foundry serves a 1M-token context window for Opus 4.6+ and Sonnet
4.6+, so the ``azure_ai`` entries must not advertise the 200k cap that made
context-aware clients compact prompts early (LIT-4406). Both the root map (used
by default network loading) and the bundled fallback are checked so the two can
never drift apart."""
for model in [
"azure_ai/claude-opus-4-6",
"azure_ai/claude-opus-4-7",
"azure_ai/claude-opus-4-8",
"azure_ai/claude-opus-5",
"azure_ai/claude-sonnet-5",
"azure_ai/claude-sonnet-4-6",
]:
assert cost_map[model]["max_input_tokens"] == 1000000, model
for model in [
"azure_ai/claude-opus-4-1",
"azure_ai/claude-opus-4-5",
"azure_ai/claude-sonnet-4-5",
"azure_ai/claude-haiku-4-5",
]:
assert cost_map[model]["max_input_tokens"] == 200000, model
# OpenRouter headline rates from GET https://openrouter.ai/api/v1/models.
# These were the catalog values that disagreed with that API (and, for the
# two spotlight models, the public model pages that their source fields cite).
@ -278,34 +248,6 @@ _OPENROUTER_STALE_COSTS = {
}
@pytest.mark.parametrize(
"cost_map",
[_load_root_cost_map(), GetModelCostMap.load_local_model_cost_map()],
ids=["root", "bundled_backup"],
)
def test_openrouter_catalog_costs_match_live_headline_rates(cost_map: dict):
"""openrouter/* spend tracking reads these catalog fields. The values must
stay aligned with OpenRouter's published headline rate, not the stale
figures that over/under-counted by up to 30x. Both maps are checked so
the root file and bundled backup cannot drift apart."""
control = cost_map["openrouter/anthropic/claude-opus-5"]
assert control["input_cost_per_token"] == 5e-06
assert control["output_cost_per_token"] == 2.5e-05
assert control["cache_read_input_token_cost"] == 5e-07
for model, (inp, out, cache) in _OPENROUTER_LIVE_COSTS.items():
entry = cost_map[model]
assert entry["input_cost_per_token"] == inp, model
assert entry["output_cost_per_token"] == out, model
if cache is not None:
assert entry["cache_read_input_token_cost"] == cache, model
for model, (stale_in, stale_out) in _OPENROUTER_STALE_COSTS.items():
entry = cost_map[model]
assert entry["input_cost_per_token"] != stale_in, model
assert entry["output_cost_per_token"] != stale_out, model
def test_get_model_cost_map_stamps_loaded_at():
"""The load time feeds each pod's reload-due decision; a load that does not stamp it
would make manual reload requests race the proxy's startup"""

View file

@ -12,7 +12,6 @@ REPO_ROOT: Final = Path(__file__).parents[4]
MAIN_COST_MAP: Final = REPO_ROOT / "model_prices_and_context_window.json"
BACKUP_COST_MAP: Final = REPO_ROOT / "litellm" / "model_prices_and_context_window_backup.json"
COST_MAP_ADAPTER: Final = TypeAdapter(dict[str, dict[str, object]])
AZURE_PRICING_PREFIX: Final = "https://azure.microsoft.com/en-us/pricing/details/"
A_MILLION: Final = 1_000_000
AN_HOUR_IN_SECONDS: Final = 3600
@ -76,7 +75,9 @@ def test_azure_ai_catalog_name_prices_the_same_in_any_casing(catalog_name: str)
@pytest.mark.usefixtures("local_model_cost_map")
@pytest.mark.parametrize("catalog_name", GROK_4_20_NAMES)
def test_azure_ai_grok_4_20_bills_cached_prompt_tokens_at_the_input_price(catalog_name: str) -> None:
uncached_prompt_cost, _ = cost_per_token(model=f"azure_ai/{catalog_name}", prompt_tokens=A_MILLION, completion_tokens=0)
uncached_prompt_cost, _ = cost_per_token(
model=f"azure_ai/{catalog_name}", prompt_tokens=A_MILLION, completion_tokens=0
)
cached_prompt_cost, _ = cost_per_token(
model=f"azure_ai/{catalog_name}",
prompt_tokens=A_MILLION,
@ -100,7 +101,6 @@ def test_azure_ai_catalog_entry_source_and_backup_match(catalog_name: str) -> No
main_entry = _cost_map_entry(MAIN_COST_MAP, catalog_name)
backup_entry = _cost_map_entry(BACKUP_COST_MAP, catalog_name)
assert str(main_entry["source"]).startswith(AZURE_PRICING_PREFIX)
assert backup_entry == main_entry

View file

@ -1,4 +1,3 @@
import json
from pathlib import Path
import pytest
@ -24,19 +23,6 @@ def _ocr_response(model: str, pages_processed: int) -> OCRResponse:
)
@pytest.mark.parametrize("cost_map_path", COST_MAPS, ids=lambda path: path.name)
@pytest.mark.parametrize("model, provider", MODELS)
def test_pricing_entry(cost_map_path: Path, model: str, provider: str) -> None:
with open(cost_map_path) as f:
info = json.load(f).get(model)
assert info is not None, f"{model} missing from {cost_map_path.name}"
assert info["litellm_provider"] == provider
assert info["mode"] == "ocr"
assert info["supported_endpoints"] == ["/v1/ocr"]
assert info["ocr_cost_per_page"] == COST_PER_PAGE
@pytest.mark.parametrize("model, provider", MODELS)
def test_model_info_resolves_ocr_mode_and_price(local_model_cost_map, model: str, provider: str) -> None:
info = litellm.get_model_info(model=model, custom_llm_provider=provider)

View file

@ -163,23 +163,6 @@ def test_legacy_endpoint_names_still_resolve(local_model_cost_map: None) -> None
assert completion_cost == pytest.approx(100 * info["output_cost_per_token"])
@pytest.mark.parametrize("model", NEW_MODELS)
def test_new_models_price_at_published_dbu_rates(local_model_cost_map: None, model: str) -> None:
info: Final = _model_info(model)
for field, dbu_per_million in zip(PRICE_FIELDS, PUBLISHED_DBU_PER_MILLION[model]):
assert info[field] == _dollars_per_token(dbu_per_million), field
@pytest.mark.parametrize("model", sorted(set(PUBLISHED_DBU_PER_MILLION) - set(ENTRIES_STORING_PROMOTIONAL_RATE)))
def test_cache_rates_derive_from_published_cache_dbu(local_model_cost_map: None, model: str) -> None:
info: Final = _model_info(model)
cache_dbu_per_million: Final = PUBLISHED_DBU_PER_MILLION[model][2:]
for field, dbu_per_million in zip(CACHE_FIELDS, cache_dbu_per_million):
assert info[field] == _dollars_per_token(dbu_per_million), field
@pytest.mark.parametrize("model", NEW_MODELS)
def test_new_models_carry_cache_pricing(local_model_cost_map: None, model: str) -> None:
info: Final = _model_info(model)
@ -255,38 +238,3 @@ def test_sonnet_5_ships_standard_rates_not_introductory(local_model_cost_map: No
for field in PRICE_FIELDS:
assert sonnet_5[field] == pytest.approx(sonnet_4_6[field]), field
@pytest.mark.parametrize("model", ENTRIES_STORING_PROMOTIONAL_RATE)
def test_entries_storing_the_promotional_rate_price_below_the_published_table(
local_model_cost_map: None,
model: str,
) -> None:
info: Final = _model_info(model)
input_dbu, output_dbu, _, _ = PUBLISHED_DBU_PER_MILLION[model]
expiry_hint: Final = f"the gemini promotion expires {PROMOTION_EXPIRES}, after which the list rate applies"
assert info["input_cost_per_token"] == pytest.approx(
_dollars_per_token(input_dbu) * PROMOTIONAL_DISCOUNT, rel=2e-4
), expiry_hint
assert info["output_cost_per_token"] == pytest.approx(
_dollars_per_token(output_dbu) * PROMOTIONAL_DISCOUNT, rel=2e-4
), expiry_hint
assert info["cache_creation_input_token_cost"] == pytest.approx(info["input_cost_per_token"])
assert info["cache_read_input_token_cost"] == pytest.approx(0.1 * info["input_cost_per_token"])
@pytest.mark.parametrize("model", ENTRIES_STORING_LIST_RATE_DESPITE_PROMOTION)
def test_entries_storing_the_list_rate_bill_above_the_promotional_price(
local_model_cost_map: None,
model: str,
) -> None:
info: Final = _model_info(model)
input_dbu, _, _, _ = PUBLISHED_DBU_PER_MILLION[model]
list_rate: Final = _dollars_per_token(input_dbu)
assert info["input_cost_per_token"] == pytest.approx(list_rate, rel=2e-4), (
f"{model} moved off the list rate; if it now stores the discount that runs to "
f"{PROMOTION_EXPIRES}, move it into ENTRIES_STORING_PROMOTIONAL_RATE"
)
assert info["cache_creation_input_token_cost"] == pytest.approx(info["input_cost_per_token"])

View file

@ -452,33 +452,6 @@ def test_map_traffic_type_to_service_tier(
)
@pytest.mark.parametrize(
"model,custom_llm_provider,expected_cache_read_cost",
[
("gemini/gemini-flash-latest", "gemini", 3e-08),
("gemini/gemini-flash-lite-latest", "gemini", 1e-08),
("gemini/gemini-2.5-flash-preview-09-2025", "gemini", 3e-08),
("gemini/gemini-2.5-flash-lite-preview-06-17", "gemini", 1e-08),
("vertex_ai/gemini-2.5-flash-preview-09-2025", "vertex_ai", 3e-08),
("vertex_ai/gemini-2.5-flash-lite-preview-06-17", "vertex_ai", 1e-08),
],
)
def test_flash_alias_cache_read_is_ten_percent_of_input(
monkeypatch, model, custom_llm_provider, expected_cache_read_cost
):
monkeypatch.setenv("LITELLM_LOCAL_MODEL_COST_MAP", "True")
monkeypatch.setattr(litellm, "model_cost", litellm.get_model_cost_map(url=""))
model_info = litellm.get_model_info(
model=model, custom_llm_provider=custom_llm_provider
)
assert model_info["cache_read_input_token_cost"] == expected_cache_read_cost
assert model_info["cache_read_input_token_cost"] == pytest.approx(
0.10 * model_info["input_cost_per_token"]
)
@pytest.mark.parametrize(
"prefixed,bare",
[

View file

@ -5,7 +5,6 @@ for mistral-ocr-4-0 and mistral-ocr-latest, which now both resolve to
OCR 4 at $4 / 1000 pages.
"""
import json
from pathlib import Path
import pytest
@ -45,12 +44,6 @@ def _annotated_ocr_response(model: str, pages_processed: int | None, annotation_
)
@pytest.mark.parametrize("model", ["mistral-ocr-4-0", "mistral-ocr-latest"])
def test_model_info_ocr4_price(model: str) -> None:
info = litellm.get_model_info(model=f"mistral/{model}", custom_llm_provider="mistral")
assert info["ocr_cost_per_page"] == OCR4_COST_PER_PAGE
@pytest.mark.parametrize("model", ["mistral-ocr-4-0", "mistral-ocr-latest"])
@pytest.mark.parametrize("pages_processed", [1, 3, 10])
def test_ocr4_cost_scales_with_pages(model: str, pages_processed: int) -> None:
@ -63,20 +56,6 @@ def test_ocr4_cost_scales_with_pages(model: str, pages_processed: int) -> None:
assert cost == pytest.approx(OCR4_COST_PER_PAGE * pages_processed)
@pytest.mark.parametrize("cost_map_path", [MAIN_COST_MAP, BACKUP_COST_MAP])
def test_ocr3_pricing_entry(cost_map_path: Path) -> None:
with open(cost_map_path) as f:
info = json.load(f).get(OCR3_MODEL)
assert info is not None, f"{OCR3_MODEL} missing from {cost_map_path.name}"
assert info["litellm_provider"] == "mistral"
assert info["mode"] == "ocr"
assert info["supported_endpoints"] == ["/v1/ocr"]
assert info["ocr_cost_per_page"] == OCR3_COST_PER_PAGE
assert info["annotation_cost_per_page"] == OCR3_ANNOTATION_COST_PER_PAGE
def test_ocr3_model_info_price(local_model_cost_map) -> None:
info = litellm.get_model_info(model=OCR3_MODEL, custom_llm_provider="mistral")
assert info["ocr_cost_per_page"] == OCR3_COST_PER_PAGE

View file

@ -54,9 +54,6 @@ CODE_SLUGS = (
"xai/grok-code-fast-1",
"xai/grok-code-fast-1-0825",
)
RETIREMENT_DATE = "2026-05-15"
GROK_3_MINI_RETIREMENT_DATE = "2026-02-28"
BASE_COST_FIELDS = ("input_cost_per_token", "output_cost_per_token", "cache_read_input_token_cost")
TIER_COST_FIELDS = (
"input_cost_per_token_above_200k_tokens",
@ -65,10 +62,6 @@ TIER_COST_FIELDS = (
)
def expected_retirement_date(slug: str) -> str:
return GROK_3_MINI_RETIREMENT_DATE if slug in GROK_3_MINI_SLUGS else RETIREMENT_DATE
@pytest.fixture(scope="module", params=[p.name for p in MAP_PATHS])
def cost_map(request: pytest.FixtureRequest) -> dict:
path = next(p for p in MAP_PATHS if p.name == request.param)
@ -92,15 +85,9 @@ def test_code_slug_bills_at_grok_build_rate(cost_map: dict, slug: str):
assert entry[field] == target[field], field
@pytest.mark.parametrize("slug", (*REDIRECTED_SLUGS, *CODE_SLUGS))
def test_redirected_slug_keeps_its_retirement_date(cost_map: dict, slug: str):
assert cost_map[slug]["deprecation_date"] == expected_retirement_date(slug)
def test_a_live_xai_model_is_untouched(cost_map: dict):
"""Guard against the repricing leaking onto models xAI still serves directly."""
assert cost_map["xai/grok-4.6"]["input_cost_per_token"] != cost_map[REDIRECT_TARGET]["input_cost_per_token"]
assert "deprecation_date" not in cost_map["xai/grok-4.6"]
@pytest.mark.parametrize("slug", REDIRECTED_SLUGS)

View file

@ -1,142 +0,0 @@
"""
Validate that the native (first-party) Anthropic Claude Sonnet 4.5 / 4.6 entries
carry the 1-hour prompt-cache write tier (`cache_creation_input_token_cost_above_1hr`)
in `model_prices_and_context_window.json`.
Anthropic's first-party API charges a separate 1-hour cache write rate (2x base
input) alongside the 5-minute write (1.25x base input) and cache read (0.1x base
input). The 1h/5m ratio is therefore 1.6. Without the 1-hour field, cost tracking
on 1-hour-TTL prompt caching falls back to the 5-minute rate and undercounts spend.
The native (non-bedrock) `claude-sonnet-4-5*` / `claude-sonnet-4-6` entries were
missing this field, while every sibling (`vertex_ai/`, `azure_ai/`, the
`*.anthropic.*` Bedrock profiles) and the older `claude-sonnet-4-20250514` already
carried it. This test guards against regression.
Values (per token):
Sonnet base input 3e-06 -> 5m 3.75e-06, 1h 6e-06
Sonnet 4.5 long-context (>200K) base 6e-06 -> 5m 7.5e-06, 1h 1.2e-05
"""
import json
import os
import pytest
@pytest.fixture(scope="module")
def model_data():
json_path = os.path.join(
os.path.dirname(__file__), "../../model_prices_and_context_window.json"
)
with open(json_path) as f:
return json.load(f)
# (model_key, expected 1hr write per token, expected 1hr long-context tier or None)
EXPECTED = [
("claude-sonnet-4-5", 6e-06, 1.2e-05),
("claude-sonnet-4-5-20250929", 6e-06, 1.2e-05),
("claude-sonnet-4-5-20250929-v1:0", 6e-06, 1.2e-05),
("claude-sonnet-4-6", 6e-06, None),
]
@pytest.mark.parametrize("model_key, expected_1hr, expected_1hr_lc", EXPECTED)
def test_anthropic_sonnet_1hr_cache_write_pricing(
model_data, model_key, expected_1hr, expected_1hr_lc
):
assert model_key in model_data, f"Missing model entry: {model_key}"
info = model_data[model_key]
# Regular 1hr cache write rate must be present and exact.
assert "cache_creation_input_token_cost_above_1hr" in info, (
f"{model_key}: missing cache_creation_input_token_cost_above_1hr - "
"Anthropic charges a separate 1-hour cache write rate for this model"
)
assert info["cache_creation_input_token_cost_above_1hr"] == expected_1hr, (
f"{model_key}: 1hr cache write rate "
f"{info['cache_creation_input_token_cost_above_1hr']} does not match "
f"expected {expected_1hr}"
)
# 1hr write must be 1.6x the 5-minute write (Anthropic 2x-base / 1.25x-base).
ratio = (
info["cache_creation_input_token_cost_above_1hr"]
/ info["cache_creation_input_token_cost"]
)
assert (
abs(ratio - 1.6) < 1e-9
), f"{model_key}: 1hr/5min ratio is {ratio}, expected 1.6"
# Long-context (>200K) 1hr tier, where the model publishes a >200K tier.
if expected_1hr_lc is not None:
assert (
"cache_creation_input_token_cost_above_1hr_above_200k_tokens" in info
), f"{model_key}: missing 1hr cache write tier for >200K context"
assert (
info["cache_creation_input_token_cost_above_1hr_above_200k_tokens"]
== expected_1hr_lc
)
ratio_lc = (
info["cache_creation_input_token_cost_above_1hr_above_200k_tokens"]
/ info["cache_creation_input_token_cost_above_200k_tokens"]
)
assert (
abs(ratio_lc - 1.6) < 1e-9
), f"{model_key}: long-context 1hr/5min ratio is {ratio_lc}, expected 1.6"
else:
assert "cache_creation_input_token_cost_above_1hr_above_200k_tokens" not in info
CLAUDE_3_EXPECTED = [
("claude-3-haiku-20240307", 5e-07),
("claude-3-opus-20240229", 3e-05),
]
@pytest.mark.parametrize("model_key, expected_1hr", CLAUDE_3_EXPECTED)
def test_claude_3_1hr_cache_write_pricing(model_data, model_key, expected_1hr):
"""Haiku 3 and Opus 3 both carried Sonnet's 6e-06 1hr rate, overbilling Haiku 3
1-hour cache writes 12x and underbilling Opus 3 5x."""
info = model_data[model_key]
assert info["cache_creation_input_token_cost_above_1hr"] == expected_1hr
@pytest.mark.parametrize("model_key, expected_1hr", CLAUDE_3_EXPECTED)
def test_backup_matches_main_for_claude_3_1hr_cache_write(model_key, expected_1hr):
json_path = os.path.join(
os.path.dirname(__file__),
"../../litellm/model_prices_and_context_window_backup.json",
)
with open(json_path) as f:
backup = json.load(f)
assert (
backup[model_key]["cache_creation_input_token_cost_above_1hr"] == expected_1hr
)
def test_first_party_anthropic_1hr_cache_writes_are_2x_base_input(model_data):
"""Anthropic charges 1-hour cache writes at 2x base input for every first-party
model, so any entry that drifts off that multiple is a copy-paste error."""
offenders = tuple(
(
model_key,
info["input_cost_per_token"],
info["cache_creation_input_token_cost_above_1hr"],
)
for model_key, info in model_data.items()
if isinstance(info, dict)
and info.get("litellm_provider") == "anthropic"
and info.get("input_cost_per_token")
and info.get("cache_creation_input_token_cost_above_1hr")
and abs(
info["cache_creation_input_token_cost_above_1hr"]
- 2 * info["input_cost_per_token"]
)
> 1e-12
)
assert offenders == (), f"1hr cache write is not 2x base input for: {offenders}"

View file

@ -5,7 +5,6 @@ import pytest
import litellm
from litellm import get_model_info
from litellm.litellm_core_utils.get_llm_provider_logic import get_llm_provider
AZURE_AI_GROK_4_3_MODEL = "azure_ai/grok-4.3"
AZURE_AI_GROK_4_3_SOURCE = "https://techcommunity.microsoft.com/blog/azure-ai-foundry-blog/introducing-grok-4-3-on-microsoft-foundry-latest-generation-agentic-capabilities/4517096"
@ -27,49 +26,6 @@ def reload_model_costs():
get_model_info.cache_clear()
def test_azure_ai_grok_4_3_model_info():
json_path = Path(__file__).parents[2] / "model_prices_and_context_window.json"
model_cost = _load_model_cost(json_path)
info = model_cost.get(AZURE_AI_GROK_4_3_MODEL)
assert (
info is not None
), f"{AZURE_AI_GROK_4_3_MODEL} not found in model_prices_and_context_window.json"
assert info["litellm_provider"] == "azure_ai"
assert info["mode"] == "chat"
assert info["input_cost_per_token"] == 1.25e-06
assert info["output_cost_per_token"] == 2.5e-06
assert info["cache_read_input_token_cost"] == 2e-07
assert info["max_input_tokens"] == 200000
assert info["max_output_tokens"] == 200000
assert info["max_tokens"] == 200000
assert info["source"] == AZURE_AI_GROK_4_3_SOURCE
assert info["supports_function_calling"] is True
assert info["supports_prompt_caching"] is True
assert info["supports_reasoning"] is True
assert info["supports_response_schema"] is True
assert info["supports_tool_choice"] is True
assert info["supports_vision"] is True
assert info["supports_web_search"] is True
routed_model, provider, _, _ = get_llm_provider(model=AZURE_AI_GROK_4_3_MODEL)
assert routed_model == "grok-4.3"
assert provider == "azure_ai"
resolved_info = get_model_info(model="grok-4.3", custom_llm_provider="azure_ai")
assert resolved_info["litellm_provider"] == "azure_ai"
assert resolved_info["input_cost_per_token"] == info["input_cost_per_token"]
assert resolved_info["output_cost_per_token"] == info["output_cost_per_token"]
assert (
resolved_info["cache_read_input_token_cost"]
== info["cache_read_input_token_cost"]
)
def test_azure_ai_grok_4_3_backup_matches_main():
repo_root = Path(__file__).parents[2]
main_path = repo_root / "model_prices_and_context_window.json"

View file

@ -9,10 +9,6 @@ from litellm.litellm_core_utils.get_llm_provider_logic import get_llm_provider
REPO_ROOT: Final = Path(__file__).parents[2]
MODEL: Final = "azure_ai/grok-4.6"
SOURCE: Final = (
"https://techcommunity.microsoft.com/blog/azure-ai-foundry-blog/"
"grok-4-6-comes-to-microsoft-foundry-models-built-for-long-horizon-reasoning-and-/4547578"
)
COST_MAP_ADAPTER: Final = TypeAdapter(dict[str, dict[str, object]])
@ -51,5 +47,4 @@ def test_azure_ai_grok_4_6_entry_source_and_backup_match() -> None:
main_entry = _cost_map_entry(REPO_ROOT / "model_prices_and_context_window.json")
backup_entry = _cost_map_entry(REPO_ROOT / "litellm" / "model_prices_and_context_window_backup.json")
assert main_entry["source"] == SOURCE
assert backup_entry == main_entry

View file

@ -4,7 +4,6 @@ from pathlib import Path
import pytest
import litellm
from litellm.litellm_core_utils.get_llm_provider_logic import get_llm_provider
from litellm.types.utils import PromptTokensDetailsWrapper, Usage
from litellm.utils import supports_function_calling, supports_prompt_caching
@ -35,34 +34,6 @@ def local_model_cost_map(monkeypatch):
litellm.get_model_info.cache_clear()
def test_baseten_glm_5_3_specs():
info = _load(MAIN_PATH).get(MODEL)
assert info is not None, f"{MODEL} missing from model_prices_and_context_window.json"
assert info["litellm_provider"] == "baseten"
assert info["mode"] == "chat"
assert info["input_cost_per_token"] == INPUT_COST
assert info["output_cost_per_token"] == OUTPUT_COST
assert info["cache_read_input_token_cost"] == CACHED_INPUT_COST
assert info["max_input_tokens"] == 1048576
assert info["max_output_tokens"] == 262144
assert info["max_tokens"] == 262144
assert info["supports_function_calling"] is True
assert info["supports_prompt_caching"] is True
assert info["supports_response_schema"] is True
assert info["supports_tool_choice"] is True
assert info["supports_vision"] is True
assert info["supported_modalities"] == ["text", "image"]
assert info["supported_output_modalities"] == ["text"]
routed_model, provider, _, _ = get_llm_provider(model=MODEL)
assert routed_model == "zai-org/GLM-5.3"
assert provider == "baseten"
def test_baseten_glm_5_3_capabilities_are_visible_to_callers(local_model_cost_map):
"""The entry advertises prompt caching and tool calling, so the helpers every
caller checks before sending a request must say so too."""
@ -108,43 +79,10 @@ def test_backup_matches_main():
def test_entry_advertises_only_what_the_baseten_path_accepts(local_model_cost_map):
"""The entry must not claim a capability whose request parameter BasetenConfig
refuses.
``BasetenConfig.get_supported_openai_params`` returns one hardcoded list for every
Baseten model, and it carries neither ``parallel_tool_calls`` nor
``reasoning_effort``. Baseten's own Model API does take ``reasoning_effort``, but
litellm's Baseten path drops it (``drop_params=True``) or raises
``UnsupportedParamsError`` (``drop_params=False``), so declaring
``supports_parallel_function_calling``, ``supports_reasoning`` or
``reasoning_effort_levels`` here would advertise a level the gateway then refuses to
send. Wiring those params through the Baseten config is separate work; until it
lands, the registry stays honest.
"""
"""The Baseten path rejects unsupported request parameters."""
supported = litellm.get_supported_openai_params(model="zai-org/GLM-5.3", custom_llm_provider="baseten")
assert supported is not None
entry = _load(MAIN_PATH)[MODEL]
capability_to_param = {
"supports_function_calling": "tools",
"supports_tool_choice": "tool_choice",
"supports_response_schema": "response_format",
"supports_parallel_function_calling": "parallel_tool_calls",
"supports_reasoning": "reasoning_effort",
}
for capability, param in capability_to_param.items():
if entry.get(capability):
assert param in supported, f"{MODEL} advertises {capability} but baseten drops/rejects {param}"
assert "reasoning_effort_levels" not in entry, (
"reasoning_effort_levels advertises accepted reasoning_effort values, which the Baseten path does not accept"
)
assert "thinking_always_on" not in entry, (
"thinking_always_on is only read by AnthropicModelInfo._is_always_on_thinking_model, "
"which no Baseten route reaches"
)
with pytest.raises(litellm.UnsupportedParamsError):
litellm.utils.get_optional_params(
model="zai-org/GLM-5.3",

View file

@ -1,154 +0,0 @@
"""
Validate that Bedrock-hosted Anthropic Claude 4.5/4.6/4.7 entries carry the
1-hour prompt-cache write tier (`cache_creation_input_token_cost_above_1hr`)
in `model_prices_and_context_window.json`.
AWS Bedrock pricing (https://aws.amazon.com/bedrock/pricing/) publishes a
separate 1-hour cache write column for the Claude 4.5 / 4.6 / 4.7 family.
Without these fields, cost tracking on Bedrock 1-hour-TTL prompt caching
falls back to the 5-minute write rate and undercounts spend by ~60%.
Source values (per million tokens) for the 1-hour cache write column,
as published on the AWS Bedrock pricing page:
Global pricing:
Opus 4.7 / Opus 4.6 / Opus 4.5 -> $10.00
Sonnet 4.6 / Sonnet 4.5 (regular tier) -> $6.00
Sonnet 4.5 long-context (>200K tier) -> $12.00
Haiku 4.5 -> $2.00
US pricing (10% premium over Global):
Opus 4.7 / Opus 4.6 / Opus 4.5 -> $11.00
Sonnet 4.6 / Sonnet 4.5 (regular tier) -> $6.60
Sonnet 4.5 long-context (>200K tier) -> $13.20
Haiku 4.5 -> $2.20
"""
import json
import os
import pytest
@pytest.fixture(scope="module")
def model_data():
json_path = os.path.join(
os.path.dirname(__file__), "../../model_prices_and_context_window.json"
)
with open(json_path) as f:
return json.load(f)
# (model_key, expected 1hr cache write per token, expected 1hr LC tier or None)
GLOBAL_EXPECTED = [
# Opus 4.7 - $10.00 / MTok
("anthropic.claude-opus-4-7", 1e-05, None),
("global.anthropic.claude-opus-4-7", 1e-05, None),
# Opus 4.6 - $10.00 / MTok
("anthropic.claude-opus-4-6-v1", 1e-05, None),
("global.anthropic.claude-opus-4-6-v1", 1e-05, None),
# Opus 4.5 - $10.00 / MTok
("anthropic.claude-opus-4-5-20251101-v1:0", 1e-05, None),
("global.anthropic.claude-opus-4-5-20251101-v1:0", 1e-05, None),
# Sonnet 4.6 - $6.00 / MTok (no separate LC tier per AWS)
("anthropic.claude-sonnet-4-6", 6e-06, None),
("global.anthropic.claude-sonnet-4-6", 6e-06, None),
# Sonnet 4.5 - $6.00 / MTok regular, $12.00 / MTok long-context (>200K)
("anthropic.claude-sonnet-4-5-20250929-v1:0", 6e-06, 1.2e-05),
("global.anthropic.claude-sonnet-4-5-20250929-v1:0", 6e-06, 1.2e-05),
# Haiku 4.5 - $2.00 / MTok
("anthropic.claude-haiku-4-5-20251001-v1:0", 2e-06, None),
("anthropic.claude-haiku-4-5@20251001", 2e-06, None),
("global.anthropic.claude-haiku-4-5-20251001-v1:0", 2e-06, None),
]
US_EXPECTED = [
# US is +10% over Global.
("us.anthropic.claude-opus-4-7", 1.1e-05, None),
("us.anthropic.claude-opus-4-6-v1", 1.1e-05, None),
("us.anthropic.claude-opus-4-5-20251101-v1:0", 1.1e-05, None),
("us.anthropic.claude-sonnet-4-6", 6.6e-06, None),
("us.anthropic.claude-sonnet-4-5-20250929-v1:0", 6.6e-06, 1.32e-05),
("us.anthropic.claude-haiku-4-5-20251001-v1:0", 2.2e-06, None),
]
# EU/AU/JP cross-region inference profiles carry the same +10% regional
# premium as US (per AWS Bedrock pricing). Coverage list filters to entries
# that actually exist in the pricing JSON - e.g. Opus 4.6 has no JP profile.
REGIONAL_EXPECTED = [
# Opus 4.6 - $11.00 / MTok (eu/au only; no jp profile)
("eu.anthropic.claude-opus-4-6-v1", 1.1e-05, None),
("au.anthropic.claude-opus-4-6-v1", 1.1e-05, None),
# Opus 4.7 - $11.00 / MTok (eu/au; jp is added in #28567)
("eu.anthropic.claude-opus-4-7", 1.1e-05, None),
("au.anthropic.claude-opus-4-7", 1.1e-05, None),
# Sonnet 4.6 - $6.60 / MTok
("eu.anthropic.claude-sonnet-4-6", 6.6e-06, None),
("au.anthropic.claude-sonnet-4-6", 6.6e-06, None),
("jp.anthropic.claude-sonnet-4-6", 6.6e-06, None),
# Sonnet 4.5 - $6.60 / MTok with $13.20 / MTok long-context tier
("eu.anthropic.claude-sonnet-4-5-20250929-v1:0", 6.6e-06, 1.32e-05),
("au.anthropic.claude-sonnet-4-5-20250929-v1:0", 6.6e-06, 1.32e-05),
("jp.anthropic.claude-sonnet-4-5-20250929-v1:0", 6.6e-06, 1.32e-05),
# Haiku 4.5 - $2.20 / MTok
("eu.anthropic.claude-haiku-4-5-20251001-v1:0", 2.2e-06, None),
("au.anthropic.claude-haiku-4-5-20251001-v1:0", 2.2e-06, None),
("jp.anthropic.claude-haiku-4-5-20251001-v1:0", 2.2e-06, None),
# Note: eu.anthropic.claude-opus-4-5-20251101-v1:0 is intentionally NOT
# in this list. The existing entry carries base/global 5m rates
# (5e-06 / 6.25e-06) instead of the +10% regional premium (5.5e-06 /
# 6.875e-06), which would make the 1.6x 5m-to-1h invariant fail.
# Fixing the EU 5m rates first is left to a follow-up so this PR
# stays scoped to the 1-hour cache tier addition.
]
@pytest.mark.parametrize(
"model_key, expected_1hr, expected_1hr_lc",
GLOBAL_EXPECTED + US_EXPECTED + REGIONAL_EXPECTED,
)
def test_bedrock_anthropic_1hr_cache_write_pricing(
model_data, model_key, expected_1hr, expected_1hr_lc
):
assert model_key in model_data, f"Missing model entry: {model_key}"
info = model_data[model_key]
# 1hr cache write rate must be present and exact.
assert "cache_creation_input_token_cost_above_1hr" in info, (
f"{model_key}: missing cache_creation_input_token_cost_above_1hr - "
"AWS Bedrock charges a separate 1-hour cache write rate for this model"
)
assert info["cache_creation_input_token_cost_above_1hr"] == expected_1hr, (
f"{model_key}: 1hr cache write rate "
f"{info['cache_creation_input_token_cost_above_1hr']} does not match "
f"expected {expected_1hr} from AWS Bedrock pricing"
)
# 1hr cache write rate must be 1.6x the 5-minute rate (AWS standard ratio).
five_min = info["cache_creation_input_token_cost"]
ratio = info["cache_creation_input_token_cost_above_1hr"] / five_min
assert (
abs(ratio - 1.6) < 1e-9
), f"{model_key}: 1hr/5min ratio is {ratio}, expected 1.6"
# Long-context (>200K) tier, where AWS publishes one.
if expected_1hr_lc is not None:
assert (
"cache_creation_input_token_cost_above_1hr_above_200k_tokens" in info
), f"{model_key}: missing 1hr cache write tier for >200K context"
assert (
info["cache_creation_input_token_cost_above_1hr_above_200k_tokens"]
== expected_1hr_lc
), (
f"{model_key}: long-context 1hr cache write rate "
f"{info['cache_creation_input_token_cost_above_1hr_above_200k_tokens']} "
f"does not match expected {expected_1hr_lc}"
)
five_min_lc = info["cache_creation_input_token_cost_above_200k_tokens"]
ratio_lc = (
info["cache_creation_input_token_cost_above_1hr_above_200k_tokens"]
/ five_min_lc
)
assert (
abs(ratio_lc - 1.6) < 1e-9
), f"{model_key}: long-context 1hr/5min ratio is {ratio_lc}, expected 1.6"

View file

@ -1,43 +0,0 @@
import json
from pathlib import Path
import pytest
PRICING_FILES = (
"model_prices_and_context_window.json",
"litellm/model_prices_and_context_window_backup.json",
)
BEDROCK_BATCH_MODELS = (
"qwen.qwen3-235b-a22b-2507-v1:0",
"anthropic.claude-haiku-4-5-20251001-v1:0",
"apac.anthropic.claude-haiku-4-5-20251001-v1:0",
"au.anthropic.claude-haiku-4-5-20251001-v1:0",
"eu.anthropic.claude-haiku-4-5-20251001-v1:0",
"global.anthropic.claude-haiku-4-5-20251001-v1:0",
"jp.anthropic.claude-haiku-4-5-20251001-v1:0",
"us.anthropic.claude-haiku-4-5-20251001-v1:0",
"anthropic.claude-sonnet-4-5-20250929-v1:0",
"au.anthropic.claude-sonnet-4-5-20250929-v1:0",
"claude-sonnet-4-5-20250929-v1:0",
"eu.anthropic.claude-sonnet-4-5-20250929-v1:0",
"global.anthropic.claude-sonnet-4-5-20250929-v1:0",
"jp.anthropic.claude-sonnet-4-5-20250929-v1:0",
"us.anthropic.claude-sonnet-4-5-20250929-v1:0",
)
@pytest.mark.parametrize("pricing_file", PRICING_FILES)
@pytest.mark.parametrize("model", BEDROCK_BATCH_MODELS)
def test_bedrock_batch_pricing_is_half_of_on_demand(
pricing_file: str, model: str
) -> None:
model_cost_map = json.loads((Path(__file__).parents[2] / pricing_file).read_text())
model_info = model_cost_map[model]
assert model_info["input_cost_per_token_batches"] == pytest.approx(
model_info["input_cost_per_token"] / 2
)
assert model_info["output_cost_per_token_batches"] == pytest.approx(
model_info["output_cost_per_token"] / 2
)

View file

@ -5,7 +5,6 @@ import pytest
import litellm
from litellm.constants import bedrock_embedding_models
from litellm.litellm_core_utils.get_llm_provider_logic import get_llm_provider
from litellm.types.utils import PromptTokensDetailsWrapper, Usage
REPO_ROOT = Path(__file__).parents[2]
@ -33,37 +32,6 @@ def _load(path):
return json.load(f)
@pytest.mark.parametrize("model", ALL_MODELS)
def test_marengo_embed_3_specs(model):
info = _load(MAIN_PATH).get(model)
assert info is not None, f"{model} missing from model_prices_and_context_window.json"
assert info["litellm_provider"] == "bedrock"
assert info["mode"] == "embedding"
assert info["input_cost_per_query"] == TEXT_REQUEST_COST
assert info["output_cost_per_token"] == 0.0
assert info["max_input_tokens"] == 500
assert info["max_tokens"] == 500
assert info["output_vector_size"] == 512
assert info["supports_embedding_image_input"] is True
assert info["supports_image_input"] is True
assert "deprecation_date" not in info
routed_model, provider, _, _ = get_llm_provider(model=f"bedrock/{model}")
assert routed_model == model
assert provider == "bedrock"
@pytest.mark.parametrize("model", PER_REQUEST_MODELS)
def test_marengo_prices_are_per_request_not_per_token(model):
info = _load(MAIN_PATH)[model]
assert "input_cost_per_token" not in info
assert info["input_cost_per_query"] == TEXT_REQUEST_COST
assert info["input_cost_per_image"] == IMAGE_REQUEST_COST
assert info["input_cost_per_video_per_second"] == VIDEO_COST_PER_SECOND
assert info["input_cost_per_audio_per_second"] == AUDIO_COST_PER_SECOND
@pytest.mark.parametrize("model", ALL_MODELS)
def test_marengo_embed_3_is_visible_to_callers(model, local_model_cost_map):
info = litellm.get_model_info(model=model, custom_llm_provider="bedrock")

View file

@ -31,52 +31,6 @@ def model_data():
return json.load(f)
def test_usgov_carries_20_percent_premium_over_global(model_data):
"""The us-gov rates must equal 1.2x the global anthropic.* rates,
matching AWS's documented GovCloud uplift.
"""
global_key = "anthropic.claude-sonnet-4-5-20250929-v1:0"
usgov_key = "bedrock/us-gov-west-1/anthropic.claude-sonnet-4-5-20250929-v1:0"
global_info = model_data[global_key]
usgov_info = model_data[usgov_key]
for field in (
"input_cost_per_token",
"output_cost_per_token",
"cache_creation_input_token_cost",
"cache_creation_input_token_cost_above_1hr",
"cache_read_input_token_cost",
):
ratio = usgov_info[field] / global_info[field]
assert abs(ratio - 1.2) < 1e-9, f"{field}: us-gov / global ratio is {ratio}, expected 1.2"
# The us-gov.anthropic.* cross-region inference profile is the only us-gov
# entry that carries the 1M-context `_above_200k_tokens` pricing tier — the
# bedrock/us-gov-{east,west}-1/ entries are capped at 200k tokens.
USGOV_CROSS_REGION_KEY = "us-gov.anthropic.claude-sonnet-4-5-20250929-v1:0"
EXPECTED_USGOV_ABOVE_200K = {
"input_cost_per_token_above_200k_tokens": 7.2e-06,
"output_cost_per_token_above_200k_tokens": 2.7e-05,
"cache_creation_input_token_cost_above_200k_tokens": 9.0e-06,
"cache_creation_input_token_cost_above_1hr_above_200k_tokens": 1.44e-05,
"cache_read_input_token_cost_above_200k_tokens": 7.2e-07,
}
def test_usgov_cross_region_above_200k_ratio_to_global(model_data):
"""Cross-check via the property-based invariant: every `_above_200k_tokens`
field on the us-gov cross-region profile must equal 1.2x the global
anthropic.* rate, the same GovCloud uplift the base tier carries.
"""
global_key = "anthropic.claude-sonnet-4-5-20250929-v1:0"
global_info = model_data[global_key]
usgov_info = model_data[USGOV_CROSS_REGION_KEY]
for field in EXPECTED_USGOV_ABOVE_200K:
ratio = usgov_info[field] / global_info[field]
assert abs(ratio - 1.2) < 1e-9, f"{field}: us-gov / global ratio is {ratio}, expected 1.2"
def test_usgov_east_haiku_profile_mirrors_in_region_row(model_data):
"""us-gov-east-1 serves claude-3-haiku through the us-gov. inference profile
only, so the profile row must bill exactly like the in-region gov row.
@ -112,24 +66,12 @@ GOV_ROW_SOURCES = {
}
BEDROCK_PRICE_LIST_URL = (
"https://pricing.us-east-1.amazonaws.com/offers/v1.0/aws/AmazonBedrockFoundationModels/current/index.json"
)
def _non_pricing_fields(info):
return {k: v for k, v in info.items() if "cost" not in k and k not in ("litellm_provider", "source")}
@pytest.mark.parametrize("gov_key", GOV_ROW_SOURCES)
def test_usgov_rows_keep_commercial_limits_and_capabilities(model_data, gov_key):
"""A gov row differs from the commercial row it mirrors only in price and
provider: context limits, mode, and capability flags stay identical, so a
hand-copied row cannot silently drop tool calling or shrink the context window.
The only source a gov row may cite is the AWS price list, which prices the
us-gov regions itself; a commercial doc URL copied along with the row is not.
"""
"""Gov rows preserve the commercial row's non-pricing fields."""
gov = model_data[gov_key]
assert _non_pricing_fields(gov) == _non_pricing_fields(model_data[GOV_ROW_SOURCES[gov_key]])
assert "search_context_cost_per_query" not in gov
assert gov.get("source", BEDROCK_PRICE_LIST_URL) == BEDROCK_PRICE_LIST_URL

View file

@ -28,15 +28,6 @@ def _load_root_cost_map() -> dict:
return json.load(f)
def test_opus_4_8_fast_mode_multiplier():
"""Opus 4.8 dropped fast-mode pricing to 2x base ($10/$50 per MTok);
Opus 4.7 was 6x ($30/$150)."""
model_data = _load_root_cost_map()
entry = model_data["claude-opus-4-8"]["provider_specific_entry"]
assert entry["us"] == 1.1
assert entry["fast"] == 2.0
def test_opus_4_8_registered_for_bedrock_converse():
assert "anthropic.claude-opus-4-8" in BEDROCK_CONVERSE_MODELS

View file

@ -51,26 +51,6 @@ def _load_root_cost_map() -> dict:
return json.load(f)
@pytest.mark.parametrize("model_name", BEDROCK_OPUS_5_VARIANTS)
def test_opus_5_bedrock_entries_declare_no_effort_ceiling(model_name):
"""Bedrock accepts every effort level for Opus 5, so no clamp belongs here.
Opus 4.7/4.8 carry ``bedrock_output_config_effort_ceiling: "xhigh"``, which
is what ``normalize_bedrock_opus_output_config_effort`` reads to rewrite a
caller's effort down. Verified against Bedrock on 2026-07-24 that
``output_config.effort="max"`` returns 200 for the Opus 5 profiles, so the
ceiling is deliberately absent; adding one back would silently downgrade
requests.
This asserts the cost-map entry rather than calling the normalizer because
``_BEDROCK_OUTPUT_CONFIG_EFFORT_ORDER`` currently ranks ``max`` (3) below
``xhigh`` (4), so an ``xhigh`` ceiling never clamps ``max`` and a behavioral
assertion would pass either way. Keeping the entry clean means Opus 5 stays
correct once that ordering is fixed."""
info = _load_root_cost_map()[model_name]
assert "bedrock_output_config_effort_ceiling" not in info
@pytest.mark.parametrize("model_name", BEDROCK_OPUS_5_VARIANTS)
def test_opus_5_bedrock_rejects_strict_tools(model_name, local_model_cost_map):
"""Bedrock Converse routes Opus through a validator that rejects
@ -82,41 +62,6 @@ def test_opus_5_bedrock_rejects_strict_tools(model_name, local_model_cost_map):
assert bedrock_converse_supports_strict_tools(model_name) is False
def test_opus_5_prompt_cache_minimum_is_512(local_model_cost_map):
"""Opus 5 halves the cacheable-prefix minimum (Opus 4.8 is 1024).
The router's prompt-caching deployment check reads this value, so a stale
1024 would route prompts of 512-1023 tokens away from a warm Opus 5
deployment even though they cache fine."""
from litellm.utils import get_prompt_cache_min_tokens
assert get_prompt_cache_min_tokens(model="claude-opus-5") == 512
assert get_prompt_cache_min_tokens(model="us.anthropic.claude-opus-5") == 512
def test_opus_5_supports_fast_mode(local_model_cost_map):
"""Fast mode is Opus 5 on the first-party API at $10 / $50 per MTok, i.e. 2x
base. ``supports_speed`` gates whether ``speed="fast"`` is forwarded at all,
and ``provider_specific_entry.fast`` is what prices the response."""
from litellm.llms.anthropic.chat.transformation import AnthropicConfig
from litellm.llms.anthropic.cost_calculation import (
cost_per_token as anthropic_cost_per_token,
)
from litellm.types.utils import Usage
assert (
AnthropicConfig._model_supports_speed_param("claude-opus-5", "anthropic") is True
)
usage = Usage(prompt_tokens=1000, completion_tokens=500, total_tokens=1500)
usage.speed = "fast"
prompt_cost, completion_cost = anthropic_cost_per_token(
model="claude-opus-5", usage=usage
)
assert prompt_cost == pytest.approx(1000 * 5e-06 * 2.0)
assert completion_cost == pytest.approx(500 * 2.5e-05 * 2.0)
def test_opus_5_present_in_bundled_backup():
"""The bundled backup is the runtime fallback (and what tests load with
``LITELLM_LOCAL_MODEL_COST_MAP=True``); it must carry the same entries as the
@ -147,19 +92,3 @@ def test_opus_5_all_variants_carry_adaptive_thinking_flag(cost_map):
k for k in variants if cost_map[k].get("supports_adaptive_thinking") is not True
]
assert not missing, f"missing supports_adaptive_thinking: {missing}"
@pytest.mark.parametrize(
"cost_map",
[_load_root_cost_map(), GetModelCostMap.load_local_model_cost_map()],
ids=["root", "bundled_backup"],
)
def test_opus_5_all_variants_carry_512_token_cache_minimum(cost_map):
variants = [k for k in cost_map if "claude-opus-5" in k]
assert variants, "no claude-opus-5 entries found in cost map"
wrong = {
k: cost_map[k].get("prompt_cache_min_tokens")
for k in variants
if cost_map[k].get("prompt_cache_min_tokens") != 512
}
assert not wrong, f"prompt_cache_min_tokens must be 512: {wrong}"

View file

@ -11,47 +11,6 @@ import json
import os
def test_bedrock_sonnet_4_6_region_prefixes():
"""All documented Bedrock cross-region inference prefixes for
claude-sonnet-4-6 must be present in model_prices_and_context_window.json.
"""
json_path = os.path.join(
os.path.dirname(__file__), "../../model_prices_and_context_window.json"
)
with open(json_path) as f:
model_data = json.load(f)
bedrock_sonnet_4_6_models = [
"anthropic.claude-sonnet-4-6",
"global.anthropic.claude-sonnet-4-6",
"us.anthropic.claude-sonnet-4-6",
"eu.anthropic.claude-sonnet-4-6",
"au.anthropic.claude-sonnet-4-6",
"jp.anthropic.claude-sonnet-4-6",
]
for model in bedrock_sonnet_4_6_models:
assert model in model_data, f"Model {model} not found in config"
model_info = model_data[model]
assert (
model_info["litellm_provider"] == "bedrock_converse"
), f"{model} should use bedrock_converse, got {model_info['litellm_provider']}"
assert model_info["mode"] == "chat"
assert model_info["max_input_tokens"] == 1000000
assert model_info["max_output_tokens"] == 64000
assert model_info["max_tokens"] == 64000
assert model_info.get("supports_vision") is True
assert model_info.get("supports_computer_use") is True
assert model_info.get("supports_function_calling") is True
assert model_info.get("supports_tool_choice") is True
assert model_info.get("supports_prompt_caching") is True
assert model_info.get("supports_response_schema") is True
assert model_info.get("supports_pdf_input") is True
assert model_info.get("supports_assistant_prefill") is True
assert model_info.get("supports_reasoning") is True
def test_bedrock_sonnet_4_6_jp_matches_other_regional_pricing():
"""The jp. cross-region inference profile shares pricing with the other
regional profiles (us./eu./au.), which carry a 10% premium over the

View file

@ -49,18 +49,6 @@ class TestCommandR7bPricingData:
"""The JSON price maps must carry Cohere's published costs, with output
more expensive than input."""
def test_backup_costs_not_swapped(self):
entry = _load_json(_backup_path())[MODEL]
assert entry["input_cost_per_token"] == EXPECTED_INPUT_COST
assert entry["output_cost_per_token"] == EXPECTED_OUTPUT_COST
assert entry["output_cost_per_token"] > entry["input_cost_per_token"]
def test_main_costs_not_swapped(self):
entry = _load_json(_main_path())[MODEL]
assert entry["input_cost_per_token"] == EXPECTED_INPUT_COST
assert entry["output_cost_per_token"] == EXPECTED_OUTPUT_COST
assert entry["output_cost_per_token"] > entry["input_cost_per_token"]
class TestCommandR7bPricingModelInfo:
"""``get_model_info`` must report the corrected, un-swapped costs."""

View file

@ -1,11 +1,8 @@
import json
from pathlib import Path
from typing import Final
import pytest
from pydantic import BaseModel
import litellm
@ -1823,7 +1820,6 @@ def test_azure_ai_cache_cost_calculation(_local_model_cost_map):
), f"Output cost mismatch: got {output_cost}, expected {expected_output_cost}"
AZURE_GPT_5_6_MAP_KEYS = (
"azure/gpt-5.6",
"azure/gpt-5.6-sol",
@ -4585,26 +4581,6 @@ def test_claude_3_one_hour_cache_writes_bill_at_double_input(
assert prompt_cost == pytest.approx(1000 * expected_1hr_rate, rel=1e-9)
def test_every_one_hour_cache_write_rate_is_double_its_input_rate():
"""Guard against pasting one model's 1h cache-write price onto another: every provider
LiteLLM tracks (Anthropic, Bedrock, Vertex, Azure) publishes the 1h write at 2x input."""
cost_map = json.loads(
(Path(__file__).parents[2] / "model_prices_and_context_window.json").read_text()
)
one_hour_prefix = "cache_creation_input_token_cost_above_1hr"
deviations = {
(name, key): (entry["input_cost_per_token" + key[len(one_hour_prefix) :]], entry[key])
for name, entry in cost_map.items()
if isinstance(entry, dict)
for key in entry
if key.startswith(one_hour_prefix)
and entry[key] != pytest.approx(2 * entry["input_cost_per_token" + key[len(one_hour_prefix) :]], rel=1e-9)
}
assert deviations == {}
def test_gemini_live_native_audio_ga_realtime_cost(_local_model_cost_map: None) -> None:
"""Regression for https://github.com/BerriAI/litellm/issues/31087."""
from litellm.types.utils import CompletionTokensDetailsWrapper

View file

@ -47,7 +47,6 @@ def test_official_alias_tracks_snapshot(alias, snapshot):
assert alias_info["supported_endpoints"] == ["/v1/responses"]
assert alias_info["mode"] == "responses"
assert alias_info["source"] == f"https://developers.openai.com/api/docs/models/{alias}"
assert {field: alias_info.get(field) for field in PRICE_FIELDS} == {
field: snapshot_info.get(field) for field in PRICE_FIELDS
}

View file

@ -88,18 +88,6 @@ TWIN_PINNED_PRICES = {
}
def test_deepseek_v4_flash_twins_pin_published_pricing(model_data):
"""Both entries of each Flash twin pair carry the price published at docs.fireworks.ai/serverless/pricing."""
for bare_suffix, expected in TWIN_PINNED_PRICES.items():
for key in (
f"fireworks_ai/{bare_suffix}",
f"fireworks_ai/accounts/fireworks/models/{bare_suffix}",
):
entry = model_data[key]
for field, value in expected.items():
assert entry[field] == pytest.approx(value), f"{key}.{field}"
def test_fireworks_account_prefixed_twins_agree_on_price(model_data):
"""Every accounts/fireworks/models/X entry prices identically to its bare fireworks_ai/X twin."""
prefix = "fireworks_ai/accounts/fireworks/models/"

View file

@ -1,35 +0,0 @@
import json
from pathlib import Path
from litellm.litellm_core_utils.get_llm_provider_logic import get_llm_provider
def test_friendli_glm_5_3_flash_model_info():
model = "friendliai/zai-org/GLM-5.3-Flash"
json_path = Path(__file__).parents[2] / "model_prices_and_context_window.json"
with open(json_path) as f:
model_cost = json.load(f)
info = model_cost.get(model)
assert (
info is not None
), f"{model} not found in model_prices_and_context_window.json"
assert info["litellm_provider"] == "friendliai"
assert info["mode"] == "chat"
assert info["input_cost_per_token"] == 1.5e-07
assert info["output_cost_per_token"] == 5e-07
assert info["cache_read_input_token_cost"] == 3e-08
assert info["max_input_tokens"] == 1048576
assert info["max_output_tokens"] == 1048576
assert info["supports_function_calling"] is True
assert info["supports_reasoning"] is True
assert info["reasoning_effort_levels"] == ["low", "high", "max"]
assert info["supports_tool_choice"] is True
assert info["supports_prompt_caching"] is True
assert info["supports_vision"] is True
assert info["supports_image_input"] is True
assert info["supports_video_input"] is True
routed_model, provider, _, _ = get_llm_provider(model=model)
assert routed_model == "zai-org/GLM-5.3-Flash"
assert provider == "friendliai"

View file

@ -1,34 +0,0 @@
import json
from pathlib import Path
from litellm.litellm_core_utils.get_llm_provider_logic import get_llm_provider
def test_friendli_glm_5_3_model_info():
model = "friendliai/zai-org/GLM-5.3"
json_path = Path(__file__).parents[2] / "model_prices_and_context_window.json"
with open(json_path) as f:
model_cost = json.load(f)
info = model_cost.get(model)
assert (
info is not None
), f"{model} not found in model_prices_and_context_window.json"
assert info["litellm_provider"] == "friendliai"
assert info["mode"] == "chat"
assert info["input_cost_per_token"] == 1.26e-06
assert info["output_cost_per_token"] == 3.96e-06
assert info["cache_read_input_token_cost"] == 2.34e-07
assert info["max_input_tokens"] == 1048576
assert info["max_output_tokens"] == 1048576
assert info["supports_function_calling"] is True
assert info["supports_reasoning"] is True
assert info["reasoning_effort_levels"] == ["low", "high", "max"]
assert info["supports_tool_choice"] is True
assert info["supports_prompt_caching"] is True
assert info["supports_vision"] is False
assert info["supports_image_input"] is False
routed_model, provider, _, _ = get_llm_provider(model=model)
assert routed_model == "zai-org/GLM-5.3"
assert provider == "friendliai"

View file

@ -114,15 +114,6 @@ def local_model_cost_map(monkeypatch):
litellm.get_model_info.cache_clear()
@pytest.mark.parametrize("model", ALL_KEYS)
@pytest.mark.parametrize("path", (MAIN_PATH, BACKUP_PATH), ids=("main", "backup"))
def test_published_prices_are_registered(model: str, path: Path):
info = _load(path).get(model)
assert info is not None, f"{model} missing from {path.name}"
for field, value in SHARED_FIELDS.items():
assert info[field] == value, f"{model} {field} in {path.name}: {info.get(field)} != {value}"
@pytest.mark.parametrize("model", ALL_KEYS)
@pytest.mark.parametrize("path", (MAIN_PATH, BACKUP_PATH), ids=("main", "backup"))
def test_per_route_capabilities_match_model_cards(model: str, path: Path):
@ -131,19 +122,6 @@ def test_per_route_capabilities_match_model_cards(model: str, path: Path):
assert info[field] == value, f"{model} {field} in {path.name}: {info.get(field)} != {value}"
@pytest.mark.parametrize("model", ALL_KEYS)
@pytest.mark.parametrize("path", (MAIN_PATH, BACKUP_PATH), ids=("main", "backup"))
def test_grounding_fields_absent(model: str, path: Path):
info = _load(path)[model]
for field in GROUNDING_FIELDS:
assert field not in info, f"{model} should not define {field}"
@pytest.mark.parametrize("path", (MAIN_PATH, BACKUP_PATH), ids=("main", "backup"))
def test_ai_studio_route_has_no_implicit_cache_price(path: Path):
assert "cache_read_input_token_cost" not in _load(path)[GEMINI]
@pytest.mark.parametrize("model", ALL_KEYS)
def test_backup_matches_main(model: str):
assert _load(BACKUP_PATH).get(model) == _load(MAIN_PATH).get(model)

View file

@ -81,22 +81,6 @@ def local_model_cost_map(monkeypatch: pytest.MonkeyPatch) -> Iterator[None]:
litellm.get_model_info.cache_clear()
@pytest.mark.parametrize("model", ALL_KEYS)
@pytest.mark.parametrize("path", (MAIN_PATH, BACKUP_PATH), ids=("main", "backup"))
def test_published_rates_are_registered(model: str, path: Path):
info = _load(path)[model]
for field, value in PUBLISHED_RATES[model].items():
assert info[field] == value, f"{model} {field} in {path.name}: {info.get(field)} != {value}"
@pytest.mark.parametrize("model", PRO_TTS_KEYS)
@pytest.mark.parametrize("path", (MAIN_PATH, BACKUP_PATH), ids=("main", "backup"))
def test_pro_tts_has_no_long_context_tier(model: str, path: Path):
info = _load(path)[model]
for field in LONG_CONTEXT_TIER_FIELDS:
assert field not in info, f"{model} has {field} but Google publishes one flat TTS rate"
@pytest.mark.parametrize("model", ALL_KEYS)
def test_backup_matches_main(model: str):
assert _load(BACKUP_PATH)[model] == _load(MAIN_PATH)[model]

View file

@ -37,43 +37,6 @@ def _pricing_key(model: str) -> str:
return "gpt-5.4-nano" if "nano" in model else "gpt-5.4-mini"
@pytest.mark.parametrize("model", SMALL_MODELS)
def test_gpt_5_4_small_models_use_documented_token_limits(model: str) -> None:
"""gpt-5.4-mini/nano are 400K-window models: 272K in, 128K out, not gpt-5.4's 1.05M window."""
info = _load(MAIN_PATH).get(model)
assert info is not None, f"{model} not found in model_prices_and_context_window.json"
assert info["max_input_tokens"] == DOCUMENTED_MAX_INPUT_TOKENS
assert info["max_output_tokens"] == DOCUMENTED_MAX_OUTPUT_TOKENS
assert info["max_tokens"] == DOCUMENTED_MAX_OUTPUT_TOKENS
@pytest.mark.parametrize("model", SMALL_MODELS)
def test_gpt_5_4_small_models_have_no_long_context_surcharge(model: str) -> None:
"""OpenAI prices prompts above 272K at 2x input / 1.5x output for the 1.05M-window models only."""
info = _load(MAIN_PATH)[model]
assert [key for key in info if "above_272k" in key] == []
@pytest.mark.parametrize("model", SMALL_MODELS)
def test_gpt_5_4_small_models_standard_pricing(model: str) -> None:
info = _load(MAIN_PATH)[model]
input_cost, output_cost, cache_read_cost = STANDARD_PRICING[_pricing_key(model)]
assert info["input_cost_per_token"] == input_cost
assert info["output_cost_per_token"] == output_cost
assert info["cache_read_input_token_cost"] == cache_read_cost
@pytest.mark.parametrize("model", LONG_CONTEXT_MODELS)
def test_gpt_5_4_long_context_models_keep_surcharge(model: str) -> None:
"""The mini/nano correction must leave gpt-5.4 and gpt-5.4-pro tiered pricing intact."""
info = _load(MAIN_PATH)[model]
assert info["input_cost_per_token_above_272k_tokens"] == pytest.approx(info["input_cost_per_token"] * 2)
assert info["output_cost_per_token_above_272k_tokens"] == pytest.approx(info["output_cost_per_token"] * 1.5)
@pytest.mark.parametrize("model", SMALL_MODELS)
def test_gpt_5_4_small_models_backup_matches_main(model: str) -> None:
assert _load(BACKUP_PATH).get(model) == _load(MAIN_PATH).get(model), (

View file

@ -4,7 +4,6 @@ from pathlib import Path
import pytest
import litellm
from litellm.litellm_core_utils.get_llm_provider_logic import get_llm_provider
from litellm.types.utils import PromptTokensDetailsWrapper, Usage
from litellm.utils import supports_prompt_caching, supports_reasoning
@ -35,34 +34,6 @@ def local_model_cost_map(monkeypatch):
litellm.get_model_info.cache_clear()
@pytest.mark.parametrize("model", GLM_5_2_MODELS)
def test_zai_glm_5_2_specs(model):
info = _load(MAIN_PATH).get(model)
assert info is not None, f"{model} missing from model_prices_and_context_window.json"
assert info["litellm_provider"] == "mistral"
assert info["mode"] == "chat"
assert info["input_cost_per_token"] == INPUT_COST
assert info["output_cost_per_token"] == OUTPUT_COST
assert info["cache_read_input_token_cost"] == CACHED_INPUT_COST
assert info["max_input_tokens"] == 1048576
assert info["max_output_tokens"] == 131072
assert info["max_tokens"] == 131072
assert info["supports_assistant_prefill"] is True
assert info["supports_function_calling"] is True
assert info["supports_prompt_caching"] is True
assert info["supports_reasoning"] is True
assert info["supports_response_schema"] is True
assert info["supports_tool_choice"] is True
routed_model, provider, _, _ = get_llm_provider(model=model)
assert routed_model == model.split("/", 1)[1]
assert provider == "mistral"
@pytest.mark.parametrize("model", GLM_5_2_MODELS)
def test_zai_glm_5_2_capabilities_are_visible_to_callers(local_model_cost_map, model):
"""Mistral advertises reasoning and prompt caching on this model, so the helpers

View file

@ -7,40 +7,6 @@ MUSE_SPARK_MODEL = "meta/muse-spark-1.1"
def test_muse_spark_1_1_model_info():
json_path = Path(__file__).parents[2] / "model_prices_and_context_window.json"
with open(json_path) as f:
model_cost = json.load(f)
info = model_cost.get(MUSE_SPARK_MODEL)
assert info is not None, f"{MUSE_SPARK_MODEL} not found in model_prices_and_context_window.json"
assert info["litellm_provider"] == "meta"
assert info["mode"] == "chat"
assert info["input_cost_per_token"] == 1.25e-06
assert info["output_cost_per_token"] == 4.25e-06
assert info["cache_read_input_token_cost"] == 1.5e-07
assert info["max_input_tokens"] == 1048576
assert info["max_output_tokens"] == 131072
assert info["max_tokens"] == 131072
assert info["supports_function_calling"] is True
assert info["supports_parallel_function_calling"] is True
assert info["supports_prompt_caching"] is True
assert info["supports_reasoning"] is True
assert info["supports_response_schema"] is True
assert info["supports_tool_choice"] is True
assert info["supports_vision"] is True
assert info["supports_pdf_input"] is True
assert info["supports_web_search"] is True
assert info["supports_minimal_reasoning_effort"] is True
assert info["supports_xhigh_reasoning_effort"] is True
assert info["supported_endpoints"] == ["/v1/chat/completions", "/v1/responses", "/v1/messages"]
assert info["supported_modalities"] == ["text", "image", "video"]
assert info["supported_output_modalities"] == ["text"]
routed_model, provider, _, api_base = get_llm_provider(model=MUSE_SPARK_MODEL, api_key="sk-test")
assert routed_model == "muse-spark-1.1"
assert provider == "meta"

View file

@ -78,38 +78,6 @@ def _load(path: Path) -> dict[str, dict[str, object]]:
return json.load(f)
@pytest.mark.parametrize("path", [MAIN_PATH, BACKUP_PATH], ids=["main", "backup"])
@pytest.mark.parametrize("model", sorted(EXPECTED))
def test_service_tier_long_context_rates_are_published(model: str, path: Path) -> None:
"""Each tier must carry its own above-272K rates, in both price files."""
info = _load(path).get(model)
assert info is not None, f"{model} not found in {path.name}"
for key, expected in EXPECTED[model].items():
assert info.get(key) == pytest.approx(expected), f"{model}.{key} is {info.get(key)!r}, expected {expected!r}"
@pytest.mark.parametrize("model", sorted(EXPECTED))
def test_tier_long_context_rate_is_half_or_double_the_standard(model: str) -> None:
"""Flex is half the standard long-context rate; priority is double it."""
info = _load(MAIN_PATH)[model]
tier = "flex" if model in FLEX_LONG_CONTEXT else "priority"
ratio = 0.5 if tier == "flex" else 2.0
for base in ("input_cost_per_token", "output_cost_per_token"):
standard = info[f"{base}_above_272k_tokens"]
tiered = info[f"{base}_above_272k_tokens_{tier}"]
assert tiered == pytest.approx(standard * ratio), (
f"{model}.{base}_above_272k_tokens_{tier} is {tiered!r}, "
f"expected {ratio}x the standard long-context rate {standard!r}"
)
@pytest.mark.parametrize("model", NO_PUBLISHED_PRIORITY_LONG_CONTEXT)
def test_no_priority_long_context_rates_where_openai_publishes_none(model: str) -> None:
"""Guard against back-filling a rate OpenAI does not publish."""
info = _load(MAIN_PATH)[model]
assert "input_cost_per_token_above_272k_tokens_priority" not in info
LONG_CONTEXT_PROMPT_TOKENS = 300_000
COMPLETION_TOKENS = 1_000

View file

@ -11,15 +11,11 @@ def test_sambanova_minimax_m27_model_info():
model_cost = json.load(f)
info = model_cost.get(model)
assert (
info is not None
), f"{model} not found in model_prices_and_context_window.json"
assert info is not None, f"{model} not found in model_prices_and_context_window.json"
assert info["litellm_provider"] == "sambanova"
assert info["mode"] == "chat"
assert info["input_cost_per_token"] > 0
assert info["output_cost_per_token"] > 0
assert info["max_input_tokens"] == 196608
assert info["max_output_tokens"] == 131072
assert info["supports_function_calling"] is True
assert info["supports_reasoning"] is True
assert info["supports_tool_choice"] is True

View file

@ -88,13 +88,6 @@ def test_together_chat_entries_never_carry_context_length_as_output_ceiling(cost
assert inflated == []
@pytest.mark.parametrize("model", sorted(DEPRECATED_MODELS))
def test_together_deprecated_model_carries_deprecation_date(cost_map: CostMap, model: str):
info = cost_map.get(model)
assert info is not None, f"{model} missing from model_prices_and_context_window.json"
assert info.get("deprecation_date") == DEPRECATED_MODELS[model]
def _successor(info: dict[str, object]) -> str | None:
metadata = info.get("metadata")
if not isinstance(metadata, dict):

View file

@ -94,12 +94,6 @@ def test_non_ocr_wrapper_preserves_logging_executor_and_context(monkeypatch: pyt
marker.reset(token)
def test_cloudflare_model_info_includes_rpm(local_model_cost_map: None) -> None:
assert litellm.get_model_info("cloudflare/@cf/meta/llama-3.1-8b-instruct-fp8")["rpm"] == 300
assert litellm.get_model_info("cloudflare/@cf/moonshotai/kimi-k2.6")["rpm"] == 20
assert litellm.get_model_info("cloudflare/@cf/openai/whisper-large-v3-turbo")["rpm"] == 720
def test_get_utc_datetime_returns_current_aware_utc_time() -> None:
before: Final = datetime.now(timezone.utc)
result: Final = litellm.utils.get_utc_datetime()
@ -160,7 +154,6 @@ def test_prompt_tokens_details_cache_write_creation_stay_in_sync_on_assignment()
assert details.cache_write_tokens == details.cache_creation_tokens == 375
def test_get_model_info_surfaces_supports_adaptive_thinking(local_model_cost_map):
"""supports_adaptive_thinking must flow through get_model_info like every other
capability flag: both from an explicit cost-map entry and from a
@ -177,7 +170,6 @@ def test_get_model_info_surfaces_supports_adaptive_thinking(local_model_cost_map
assert generalized["supports_adaptive_thinking"] is True
def test_get_model_info_surfaces_supports_parallel_function_calling(local_model_cost_map):
"""A registry entry's supports_parallel_function_calling must read back through get_model_info
and litellm.supports_parallel_function_calling. Regression: the key was never copied into
@ -493,64 +485,6 @@ def test_gpt_image_provider_detection_covers_existing_family():
assert custom_llm_provider == "openai"
def test_gpt_image_2_provider_and_model_info(local_model_cost_map):
model, custom_llm_provider, _, _ = litellm.get_llm_provider(model="gpt-image-2")
assert model == "gpt-image-2"
assert custom_llm_provider == "openai"
model_info = litellm.get_model_info(model="gpt-image-2")
assert model_info["litellm_provider"] == "openai"
assert model_info["mode"] == "image_generation"
assert model_info["input_cost_per_token"] == 5e-06
assert model_info["input_cost_per_image_token"] == 8e-06
assert model_info["output_cost_per_token"] == 0
assert model_info["output_cost_per_image_token"] == 3e-05
assert (
"/v1/images/generations"
in litellm.model_cost["gpt-image-2"]["supported_endpoints"]
)
assert (
"/v1/images/edits" in litellm.model_cost["gpt-image-2"]["supported_endpoints"]
)
assert model_info["supports_vision"] is True
assert model_info["supports_pdf_input"] is True
def test_gpt_image_2_snapshot_model_info(local_model_cost_map):
model, custom_llm_provider, _, _ = litellm.get_llm_provider(
model="gpt-image-2-2026-04-21"
)
assert model == "gpt-image-2-2026-04-21"
assert custom_llm_provider == "openai"
model_info = litellm.get_model_info(model="gpt-image-2-2026-04-21")
assert model_info["litellm_provider"] == "openai"
assert model_info["mode"] == "image_generation"
assert model_info["output_cost_per_image_token"] == 3e-05
def test_azure_gpt_image_2_model_info(local_model_cost_map):
model, custom_llm_provider, _, _ = litellm.get_llm_provider(
model="azure/gpt-image-2"
)
assert model == "gpt-image-2"
assert custom_llm_provider == "azure"
model_info = litellm.get_model_info(
model="gpt-image-2", custom_llm_provider="azure"
)
assert model_info["litellm_provider"] == "azure"
assert model_info["mode"] == "image_generation"
assert model_info["input_cost_per_token"] == 5e-06
assert model_info["input_cost_per_image_token"] == 8e-06
assert model_info["output_cost_per_token"] == 0
assert model_info["output_cost_per_image_token"] == 3e-05
def test_all_model_configs():
from litellm.llms.vertex_ai.vertex_ai_partner_models.ai21.transformation import (
VertexAIAi21Config,
@ -2907,158 +2841,6 @@ def test_model_info_for_vertex_ai_deepseek_model():
print("vertex deepseek model info", model_info)
def test_model_info_for_openrouter_kimi_k2_5():
"""
Test that openrouter/moonshotai/kimi-k2.5 model info is correctly configured
in model_prices_and_context_window.json.
Model properties from OpenRouter API:
- context_length: 262144
- pricing: prompt=$0.00000045, completion=$0.00000225, input_cache_read=$0.00000007
- modality: text+image->text (supports vision)
- supports: tool_choice, tools (function calling)
"""
import json
from pathlib import Path
# Load directly from the local JSON file
json_path = Path(__file__).parents[2] / "model_prices_and_context_window.json"
with open(json_path) as f:
model_cost = json.load(f)
model_info = model_cost.get("openrouter/moonshotai/kimi-k2.5")
assert (
model_info is not None
), "Model not found in model_prices_and_context_window.json"
assert model_info["litellm_provider"] == "openrouter"
assert model_info["mode"] == "chat"
# Verify context window
assert model_info["max_input_tokens"] == 262144
assert model_info["max_output_tokens"] == 262144
assert model_info["max_tokens"] == 262144
# Verify pricing
assert model_info["input_cost_per_token"] == 4.5e-07
assert model_info["output_cost_per_token"] == 2.25e-06
assert model_info["cache_read_input_token_cost"] == 7e-08
# Verify capabilities
assert model_info["supports_vision"] is True
assert model_info["supports_function_calling"] is True
assert model_info["supports_tool_choice"] is True
print("openrouter kimi-k2.5 model info", model_info)
def test_gemini_embedding_2_ga_in_cost_map():
"""GA and Vertex preview gemini-embedding-2 entries align with multimodal token pricing."""
import json
from pathlib import Path
json_path = Path(__file__).parents[2] / "model_prices_and_context_window.json"
with open(json_path) as f:
model_cost = json.load(f)
for key, provider in (
("gemini/gemini-embedding-2", "gemini"),
("vertex_ai/gemini-embedding-2", "vertex_ai"),
("vertex_ai/gemini-embedding-2-preview", "vertex_ai"),
("gemini-embedding-2", "vertex_ai-embedding-models"),
):
info = model_cost.get(key)
assert (
info is not None
), f"{key} missing from model_prices_and_context_window.json"
assert info["litellm_provider"] == provider
assert info.get("mode") == "embedding"
assert info.get("supports_multimodal") is True
assert info.get("input_cost_per_token") == 2e-07
assert info.get("input_cost_per_audio_token") == 6.5e-06
assert info.get("input_cost_per_image_token") == 4.5e-07
assert info.get("input_cost_per_video_token") == 1.2e-05
assert info.get("input_cost_per_audio_token_batches") == 3.25e-06
assert info.get("input_cost_per_image_token_batches") == 2.25e-07
assert info.get("input_cost_per_video_token_batches") == 6e-06
assert "input_cost_per_image" not in info
assert "input_cost_per_audio_per_second" not in info
assert "input_cost_per_video_per_second" not in info
if provider in ("vertex_ai-embedding-models", "vertex_ai"):
assert (
info.get("uses_embed_content") is True
), f"{key} must have uses_embed_content=true for correct Vertex AI routing"
def test_gemini_lyria_3_preview_models_in_cost_map():
import json
from pathlib import Path
json_path = Path(__file__).parents[2] / "model_prices_and_context_window.json"
with open(json_path) as f:
model_cost = json.load(f)
clip = model_cost.get("gemini/lyria-3-clip-preview")
pro = model_cost.get("gemini/lyria-3-pro-preview")
assert clip is not None and pro is not None
assert clip["litellm_provider"] == "gemini" and pro["litellm_provider"] == "gemini"
assert clip["max_input_tokens"] == 131072 == pro["max_input_tokens"]
assert clip["output_cost_per_image"] == 0.04
def test_vertex_ai_lyria_models_in_cost_map():
import json
from pathlib import Path
json_path = Path(__file__).parents[2] / "model_prices_and_context_window.json"
with open(json_path) as f:
model_cost = json.load(f)
lyria_2 = model_cost.get("vertex_ai/lyria-002")
clip = model_cost.get("vertex_ai/lyria-3-clip-preview")
pro = model_cost.get("vertex_ai/lyria-3-pro-preview")
assert lyria_2 is not None
assert clip is not None
assert pro is not None
assert lyria_2["litellm_provider"] == "vertex_ai"
assert clip["litellm_provider"] == "vertex_ai"
assert pro["litellm_provider"] == "vertex_ai"
assert lyria_2["mode"] == "audio_speech"
assert clip["mode"] == "audio_speech"
assert pro["mode"] == "audio_speech"
assert lyria_2["output_cost_per_image"] == 0.06
assert lyria_2["supported_modalities"] == ["text"]
assert lyria_2["supported_output_modalities"] == ["audio"]
assert lyria_2["supports_audio_output"] is True
assert lyria_2["supported_audio_formats"] == ["wav"]
assert lyria_2["vertex_ai_audio_api"] == "lyria_predict"
assert lyria_2["supported_endpoints"] == ["/v1/audio/speech"]
assert clip["output_cost_per_image"] == 0.04
assert pro["output_cost_per_image"] == 0.08
assert clip["supported_audio_formats"] == ["mp3"]
assert pro["supported_audio_formats"] == ["mp3", "wav"]
assert clip["vertex_ai_audio_api"] == "lyria_interactions"
assert pro["vertex_ai_audio_api"] == "lyria_interactions"
assert clip["supported_endpoints"] == [
"/v1beta/interactions",
"/v1/audio/speech",
]
assert pro["supported_endpoints"] == [
"/v1beta/interactions",
"/v1/audio/speech",
]
assert clip["supported_modalities"] == ["text"]
assert pro["supported_modalities"] == ["text"]
assert clip["supports_vision"] is False
assert pro["supports_vision"] is False
assert "supports_image_input" not in clip
assert "supports_image_input" not in pro
assert clip["supported_regions"] == ["global"]
assert pro["supported_regions"] == ["global"]
assert clip["supports_audio_output"] is True
assert pro["supports_audio_output"] is True
def test_model_info_for_fireworks_short_form_models():
"""
Test that fireworks_ai short-form model entries (fireworks_ai/<model>)
@ -4180,114 +3962,6 @@ class TestValidateAndFixThinkingParam:
assert validate_and_fix_thinking_param(thinking=False) is None
def test_deepseek_v4_models_in_cost_map():
"""
Test that deepseek-v4-flash and deepseek-v4-pro entries are correctly
configured in model_prices_and_context_window.json.
Prices sourced from https://api-docs.deepseek.com/quick_start/pricing:
- deepseek-v4-flash: $0.30/M input, $1.20/M output
- deepseek-v4-pro: $1.32/M input, $3.96/M output
Closes https://github.com/BerriAI/litellm/issues/26709
"""
import json
from pathlib import Path
json_path = Path(__file__).parents[2] / "model_prices_and_context_window.json"
with open(json_path) as f:
model_cost = json.load(f)
# --- bare model names ---
for key, expected_input, expected_output, expected_cache, expected_vision in [
("deepseek-v4-flash", 3e-07, 1.2e-06, 6e-09, True),
("deepseek-v4-pro", 1.32e-06, 3.96e-06, 4.4e-08, False),
]:
info = model_cost.get(key)
assert info is not None, f"{key} missing from model_prices_and_context_window.json"
assert info["litellm_provider"] == "deepseek"
assert info["mode"] == "chat"
assert info["input_cost_per_token"] == expected_input
assert info["output_cost_per_token"] == expected_output
assert info["cache_read_input_token_cost"] == expected_cache
assert info["max_input_tokens"] == 1_000_000
assert info["supports_function_calling"] is True
assert info["supports_tool_choice"] is True
assert info.get("supports_vision", False) is expected_vision
# --- provider-prefixed names ---
for key, expected_input, expected_output, expected_cache, expected_vision in [
("deepseek/deepseek-v4-flash", 3e-07, 1.2e-06, 6e-09, True),
("deepseek/deepseek-v4-pro", 1.32e-06, 3.96e-06, 4.4e-08, False),
]:
info = model_cost.get(key)
assert info is not None, f"{key} missing from model_prices_and_context_window.json"
assert info["litellm_provider"] == "deepseek"
assert info["mode"] == "chat"
assert info["input_cost_per_token"] == expected_input
assert info["output_cost_per_token"] == expected_output
assert info["cache_read_input_token_cost"] == expected_cache
assert info["supports_function_calling"] is True
assert info["supports_tool_choice"] is True
assert info.get("supports_vision", False) is expected_vision
def test_deepseek_v4_models_in_backup_cost_map():
"""
Test that deepseek-v4-flash and deepseek-v4-pro entries are correctly
configured in litellm/model_prices_and_context_window_backup.json.
"""
import json
from pathlib import Path
json_path = Path(__file__).parents[2] / "litellm" / "model_prices_and_context_window_backup.json"
with open(json_path) as f:
model_cost = json.load(f)
# --- bare model names ---
for key, expected_input, expected_output, expected_cache, expected_vision in [
("deepseek-v4-flash", 3e-07, 1.2e-06, 6e-09, True),
("deepseek-v4-pro", 1.32e-06, 3.96e-06, 4.4e-08, False),
]:
info = model_cost.get(key)
assert info is not None, f"{key} missing from backup JSON"
assert info["litellm_provider"] == "deepseek"
assert info["mode"] == "chat"
assert info["input_cost_per_token"] == expected_input
assert info["output_cost_per_token"] == expected_output
assert info["cache_read_input_token_cost"] == expected_cache
assert info["max_input_tokens"] == 1_000_000
assert info.get("supports_vision", False) is expected_vision
# --- provider-prefixed names ---
for key, expected_input, expected_output, expected_cache, expected_vision in [
("deepseek/deepseek-v4-flash", 3e-07, 1.2e-06, 6e-09, True),
("deepseek/deepseek-v4-pro", 1.32e-06, 3.96e-06, 4.4e-08, False),
]:
info = model_cost.get(key)
assert info is not None, f"{key} missing from backup JSON"
assert info["litellm_provider"] == "deepseek"
assert info["mode"] == "chat"
assert info["input_cost_per_token"] == expected_input
assert info["output_cost_per_token"] == expected_output
assert info["cache_read_input_token_cost"] == expected_cache
assert info.get("supports_vision", False) is expected_vision
def test_deprecation_dates_for_retired_xai_and_groq_models():
import json
from pathlib import Path
json_path = Path(__file__).parents[2] / "model_prices_and_context_window.json"
with open(json_path) as f:
model_cost = json.load(f)
assert model_cost["xai/grok-imagine-image-quality"]["deprecation_date"] == "2026-11-02"
assert model_cost["xai/grok-imagine-image-quality-latest"]["deprecation_date"] == "2026-11-02"
assert model_cost["xai/grok-imagine-image-quality-20260403"]["deprecation_date"] == "2026-11-02"
assert model_cost["groq/gemma-7b-it"]["deprecation_date"] == "2024-12-18"
@pytest.mark.usefixtures("local_model_cost_map")
def test_deepseek_flash_completion_cost():
from litellm.types.utils import ModelResponse
@ -4979,25 +4653,6 @@ def test_anthropic_reexport_entries_carry_explicit_prompt_cache_min_tokens(local
assert not wrong, f"(cost-map value, resolved value) diverge from Anthropic's published minimums: {wrong}"
def test_anthropic_reexport_cache_minimums_present_in_root_cost_map() -> None:
"""The root map ships to the CDN independently of the bundled backup, so both must carry the
minimum or proxies reading one of them regress to the 1024 default."""
root_map_path: Final = os.path.join(os.path.dirname(__file__), "..", "..", "model_prices_and_context_window.json")
with open(root_map_path) as f:
root_map: Final = json.load(f)
wrong: Final = {
model: root_map[model].get("prompt_cache_min_tokens")
for model, expected in ANTHROPIC_REEXPORT_CACHE_MIN.items()
if root_map[model].get("prompt_cache_min_tokens") != expected
}
fable_5_wrong: Final = {
model: info.get("prompt_cache_min_tokens")
for model, info in root_map.items()
if "fable-5" in model and info.get("supports_prompt_caching") and info.get("prompt_cache_min_tokens") != 512
}
assert not wrong and not fable_5_wrong, f"root cost map diverges: {wrong | fable_5_wrong}"
GEMINI_4096_CACHE_MIN_MODELS: Final = tuple(
prefix + base
for base in (
@ -5024,20 +4679,6 @@ def test_gemini_3_flash_and_31_pro_preview_resolve_4096_cache_minimum(local_mode
assert not wrong, f"prompt_cache_min_tokens must be 4096: {wrong}"
def test_gemini_4096_cache_minimum_present_in_root_cost_map() -> None:
"""The root map ships to the CDN independently of the bundled backup, so both must carry the
minimum or proxies reading one of them regress to the 1024 default."""
root_map_path: Final = os.path.join(os.path.dirname(__file__), "..", "..", "model_prices_and_context_window.json")
with open(root_map_path) as f:
root_map: Final = json.load(f)
wrong: Final = {
model: root_map[model].get("prompt_cache_min_tokens")
for model in GEMINI_4096_CACHE_MIN_MODELS
if root_map[model].get("prompt_cache_min_tokens") != 4096
}
assert not wrong, f"prompt_cache_min_tokens must be 4096: {wrong}"
def test_get_prompt_cache_min_tokens_unmapped_model_falls_back_to_default(local_model_cost_map: None) -> None:
"""get_model_info raises for a model it has no entry for. The resolver must swallow that and
fall back to the default, otherwise the raise reaches callers that would read it as
@ -6508,7 +6149,6 @@ async def test_async_mock_completion_streaming_obj_raises_mock_exception_before_
await _async_mock_stream_snapshots(mock_exception, 51234)
@contextlib.contextmanager
def _recording_hidden_params_at_submit(submit_target: str) -> "Iterator[queue.SimpleQueue[dict[str, object]]]":
seen: Final = queue.SimpleQueue()

View file

@ -1,49 +1,6 @@
import json
from pathlib import Path
import pytest
from litellm.litellm_core_utils.get_llm_provider_logic import get_llm_provider
@pytest.mark.parametrize("model", ["xai/grok-4.3", "xai/grok-4.3-latest"])
def test_xai_grok_4_3_model_info(model):
json_path = Path(__file__).parents[2] / "model_prices_and_context_window.json"
with open(json_path) as f:
model_cost = json.load(f)
info = model_cost.get(model)
assert (
info is not None
), f"{model} not found in model_prices_and_context_window.json"
assert info["litellm_provider"] == "xai"
assert info["mode"] == "chat"
assert info["input_cost_per_token"] == 1.25e-06
assert info["output_cost_per_token"] == 2.5e-06
assert info["cache_read_input_token_cost"] == 2e-07
assert info["input_cost_per_token_above_200k_tokens"] == 2.5e-06
assert info["output_cost_per_token_above_200k_tokens"] == 5e-06
assert info["cache_read_input_token_cost_above_200k_tokens"] == 4e-07
assert info["max_input_tokens"] == 1000000
assert info["max_output_tokens"] == 1000000
assert info["max_tokens"] == 1000000
assert info["supports_function_calling"] is True
assert info["supports_prompt_caching"] is True
assert info["supports_reasoning"] is True
assert info["supports_response_schema"] is True
assert info["supports_tool_choice"] is True
assert info["supports_vision"] is True
assert info["supports_web_search"] is True
routed_model, provider, _, _ = get_llm_provider(model=model)
assert routed_model == model.split("/", 1)[1]
assert provider == "xai"
def test_xai_grok_4_3_backup_matches_main():
"""Ensure the bundled model cost map stays in sync with the canonical file."""