fix(register_model)!: keep bedrock geo deployments priced across price data reloads (#41523)

* fix(register_model): keep bedrock geo deployments priced across price data reloads

A request-time pricing registration inserting into litellm.model_cost while a
price data reload walked it could abort the reload (dictionary changed size
during iteration in add_known_models), or trip the lookup map rebuild inside
get_model_info so the deployment replay fell through to the unmapped path and
wrote a raw bedrock/au.anthropic.<model> entry with no token pricing, a $0
shadow re-rolled on every later reload. Walk snapshots of the map, bind the
case-insensitive lookup map locally, and let the unmapped path inherit the
whole built-in entry of a provider get_model_info would accept.

* fix(register_model): keep synthesized zero costs out of inherited built-in pricing

An unmapped alias that inherits its built-in entry now takes only the token
prices the catalogue really carries, so a synthesized zero never lands next to
the caller's custom price. The router regression test reports the writer's
failures instead of swallowing them, and the local names carry Final.

* fix(register_model): match inherited built-ins by the key provider prefix when the value names none

* fix(utils): resolve a tripped lookup from the key's own catalog row before its region sibling

* fix(register_model): keep custom flat prices above inherited tier rates

* fix(register_model): keep the other direction's tier rates when a custom flat price covers one

* fix(register_model): keep a catalog row's tiers when its lookup trips, and snapshot request-path cost map walks

A tripped get_model_info on a key that is itself a catalog row now inherits that row whole, so a custom flat price bills a long prompt at the row's above-threshold rate on both lookup paths instead of flipping until the next reload. The fal_ai, fireworks, ollama and gpt-5 request-path walks over litellm.model_cost iterate a snapshot so a concurrent reload cannot raise mid-request.

* fix(register_model): price route-prefixed geo keys on a tripped lookup and replace cost map entries atomically

* fix(register_model): move every alias of a cost map entry to its registered price

* perf(register_model): read alias names from the shared entry instead of scanning the cost map

Each registration scanned every cost map entry to find the names sharing it,
about 1 ms on the 4,189-entry map, and a proxy request to a custom-priced
deployment registers twice. Aliased entries now carry the names they are
served under, so a registration reads them directly, and an alias the caller
pointed at its own entry keeps that entry. The tests this PR added lose their
docstrings

* fix(utils): keep capability flags and write through on a tripped lookup in register_model

A key the lookup cannot map now seeds its registered entry from the capability
rules its unregistered lookup resolves to, so registering a region alias no
longer turns off supports_system_messages and similar flags. A lookup that
trips mid-walk on a key naming a catalog row writes into that row instead of
leaving a shadow key that goes stale. Adds regression tests for both and for
the request-path snapshot walks.

* fix(utils): retry a tripped registration lookup and rebuild a registration a reload races

A lookup a concurrent write trips is retried instead of writing into the
catalog row its key names, so a per-request price never reprices a shared
row. The inherited entry leaves the matched row's provider behind, so a
provider-less azure_ai key stays priced, and a registration whose map a
reload swapped mid-build is rebuilt from the new map.

* fix(utils): write each registration into the map it was built from and rebuild it when a reload replaced that map

* test(integration): price data reload keeps every registered deployment priced

---------

Co-authored-by: mateo-berri <277851410+mateo-berri@users.noreply.github.com>
This commit is contained in:
devin-ai-integration[bot] 2026-10-17 19:10:04 +00:00 • committed by GitHub
parent b5037f28d1
commit 9e9dccb9d4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 1546 additions and 133 deletions

View file

@ -742,7 +742,7 @@ def is_openai_finetune_model(key: str) -> bool:
def _populate_provider_model_sets(model_cost_map: Mapping[str, Mapping[str, object]]) -> None:
for key, value in model_cost_map.items():
for key, value in tuple(model_cost_map.items()):
if value.get("litellm_provider") == "openai" and not is_openai_finetune_model(key):
open_ai_chat_completion_models.add(key)
elif value.get("litellm_provider") == "text-completion-openai":

View file

@ -557,22 +557,39 @@ def get_model_cost_map_loaded_at() -> "datetime | None":
return _cost_map_source_info.loaded_at
def _expand_model_aliases(model_cost: dict) -> dict:
class AliasedCostEntry(dict[str, object]):
__slots__ = ("names",)
def __init__(self, entry: Mapping[str, object], names: tuple[str, ...]) -> None:
super().__init__(entry)
self.names: tuple[str, ...] = names
def _alias_groups(alias_owners: Mapping[str, str]) -> Mapping[str, tuple[str, ...]]:
return MappingProxyType(
{
owner: (owner, *(alias for alias, alias_owner in alias_owners.items() if alias_owner == owner))
for owner in dict.fromkeys(alias_owners.values())
}
)
def _expand_model_aliases(model_cost: dict[str, dict[str, object]]) -> dict[str, dict[str, object]]:
"""
Expand ``aliases`` lists in model cost entries into top-level entries.
Each alias gets a reference to the **same** dict object as the canonical
entry (zero memory overhead). The ``aliases`` key is removed from the
entry so downstream code never sees it.
Each alias gets a reference to the **same** ``AliasedCostEntry`` as the
canonical entry, which carries every name it is served under. The
``aliases`` key is removed from the entry so downstream code never sees it.
If an alias collides with an existing canonical entry the alias is
skipped and a warning is logged.
"""
aliases_to_add: Final[dict[str, dict]] = {}
alias_owners: Final[dict[str, str]] = {}
keys_with_aliases: Final[list[str]] = []
for model_name, model_info in model_cost.items():
aliases: list | None = model_info.get("aliases")
aliases: object = model_info.get("aliases")
if aliases is None:
continue
keys_with_aliases.append(model_name)
@ -594,7 +611,7 @@ def _expand_model_aliases(model_cost: dict) -> dict:
model_name,
)
continue
if alias in aliases_to_add:
if alias in alias_owners:
verbose_logger.warning(
"LiteLLM model alias conflict: alias '%s' (from '%s') "
"was already claimed by another entry — skipping.",
@ -602,13 +619,14 @@ def _expand_model_aliases(model_cost: dict) -> dict:
model_name,
)
continue
aliases_to_add[alias] = model_info # same dict reference
alias_owners[alias] = model_name
# Remove the ``aliases`` key from entries so it doesn't pollute model info
for key in keys_with_aliases:
model_cost[key].pop("aliases", None)
model_cost.update(aliases_to_add)
for owner, names in _alias_groups(alias_owners).items():
model_cost.update(dict.fromkeys(names, AliasedCostEntry(model_cost[owner], names)))
return model_cost

View file

@ -87,7 +87,7 @@ def _keyed_rows(model: str, quality: str) -> tuple[tuple[int, int, float], ...]:
suffix: Final = f"/{model}"
return tuple(
(width, height, float(raw_cost))
for key in litellm.model_cost
for key in (*litellm.model_cost,)
if isinstance(key, str) and key.startswith(prefix) and key.endswith(suffix)
for size in (key[len(prefix) : -len(suffix)],)
for dimensions in (_parse_keyed_dimensions(size),)

View file

@ -544,7 +544,7 @@ class FireworksAIConfig(FireworksAIMixin, OpenAIGPTConfig):
return cached[2]
index: Final[list[tuple[str, dict]]] = []
for key, model_info in model_cost.items():
for key, model_info in (*model_cost.items(),):
if not key.startswith("fireworks_ai/"):
continue
if not isinstance(model_info, dict):

View file

@ -197,7 +197,7 @@ class OllamaModelInfo(BaseLLMModelInfo):
"ollama/" + stripped_model,
"ollama_chat/" + stripped_model,
}
model_cost_keys: Final = {key.lower() for key in model_cost}
model_cost_keys: Final = {key.lower() for key in (*model_cost,)}
return any(name.lower() in model_cost_keys for name in potential_model_names)
@staticmethod

View file

@ -28,7 +28,9 @@ def _catalogue_declares_default_effort() -> bool:
the one litellm gave before it existed. Scanning costs ~80us on the largest published map and
only on the fallback path, which is noise beside the request it precedes.
"""
return any(isinstance(entry, dict) and "default_reasoning_effort" in entry for entry in litellm.model_cost.values())
return any(
isinstance(entry, dict) and "default_reasoning_effort" in entry for entry in (*litellm.model_cost.values(),)
)
def _normalize_reasoning_effort_for_chat_completion(

View file

@ -44,7 +44,7 @@ from httpx import Proxy
from httpx._utils import get_environment_proxies
from openai.lib import _parsing, _pydantic # pyright: ignore[reportPrivateUsage] # OpenAI parser module is private
from openai.types.chat.completion_create_params import ResponseFormat
from pydantic import BaseModel, ConfigDict, JsonValue, TypeAdapter, with_config
from pydantic import BaseModel, ConfigDict, JsonValue, TypeAdapter, ValidationError, with_config
import litellm
import litellm.litellm_core_utils
@ -89,6 +89,7 @@ from litellm.litellm_core_utils.fallback_generalizations import (
match_capability_generalizations,
match_fill_missing_generalizations,
)
from litellm.litellm_core_utils.get_model_cost_map import AliasedCostEntry
from litellm.litellm_core_utils.model_param_helper import peek_reasoning_summary_aliases
from litellm.litellm_core_utils.sensitive_data_masker import redact_credentials_in_payload
from litellm.litellm_core_utils.tokenizer import strip_special_tokens
@ -3303,46 +3304,176 @@ _BEDROCK_REGION_PREFIXES: Final = (
"ap-northeast-1.",
)
_CACHE_PRICING_FIELDS: Final = (
"cache_creation_input_token_cost",
"cache_creation_input_token_cost_above_1hr",
"cache_creation_input_token_cost_above_200k_tokens",
"cache_read_input_token_cost",
"cache_read_input_token_cost_above_200k_tokens",
_TOKEN_COST_FIELDS: Final = ("input_cost_per_token", "output_cost_per_token")
def _builtin_cost_map() -> dict[str, Mapping[str, object]]:
return cast(dict[str, Mapping[str, object]], litellm.model_cost)
def _replace_cost_map_entry(cost_map: dict[str, Mapping[str, object]], key: str, entry: Mapping[str, object]) -> None:
current: Final = cost_map.get(key)
merged: Final = {**(current or {}), **entry}
if not isinstance(current, AliasedCostEntry):
cost_map.update({key: merged})
return
sharing_names: Final = tuple(name for name in current.names if cost_map.get(name) is current)
cost_map.update(dict.fromkeys((key, *sharing_names), AliasedCostEntry(merged, current.names)))
def _without_synthesized_costs(
model_info: Mapping[str, object], key: str, value: Mapping[str, object]
) -> Mapping[str, object]:
"""``_get_model_info_helper`` synthesizes ``input_cost_per_token`` / ``output_cost_per_token`` = 0 when the raw
entry has neither (#30198). Persisting those zeros would turn a name-priced entry into a free one and make
``_is_cost_explicitly_configured`` disable budget enforcement on the next re-registration."""
builtin_cost_map: Final = _builtin_cost_map()
raw_fields: Final = frozenset(builtin_cost_map.get(str(model_info["key"]), builtin_cost_map.get(key, {})))
return MappingProxyType(
{
name: field_value
for name, field_value in model_info.items()
if name not in _TOKEN_COST_FIELDS or name in raw_fields or name in value
}
)
_DIRECTION_RATE_FIELDS: Final = MappingProxyType(
{
"input_cost_per_token": ("input_cost_per_token",),
"output_cost_per_token": ("output_cost_per_token", "output_cost_per_reasoning_token"),
}
)
_RATE_TABLE: Final = TypeAdapter(Mapping[str, object])
_RATE_TIERS: Final = TypeAdapter(tuple[Mapping[str, object], ...])
def _resolve_builtin_model_cost_entry(key: str, provider: str) -> dict[str, object] | None:
def _is_overridden_rate(name: str, overridden_fields: tuple[str, ...]) -> bool:
is_variant: Final = name.startswith(tuple(f"{rate_field}_" for rate_field in overridden_fields))
return name in overridden_fields or (is_variant and "_cache_" not in name)
def _without_overridden_rates(rates: Mapping[str, object], overridden_fields: tuple[str, ...]) -> Mapping[str, object]:
return MappingProxyType(
{name: rate for name, rate in rates.items() if not _is_overridden_rate(name, overridden_fields)}
)
def _rate_table_with_flat_prices(
name: str, table: object, overridden_fields: tuple[str, ...], flat_prices: Mapping[str, object]
) -> object:
try:
if name == "tiered_pricing":
return [
{**_without_overridden_rates(tier, overridden_fields), **flat_prices}
for tier in _RATE_TIERS.validate_python(table)
]
if name == "off_peak_pricing":
return dict(_without_overridden_rates(_RATE_TABLE.validate_python(table), overridden_fields))
except ValidationError:
return table
return table
def _without_overridden_price_variants(
entry: Mapping[str, object], value: Mapping[str, object]
) -> Mapping[str, object]:
"""A caller's flat ``input_cost_per_token`` / ``output_cost_per_token`` is that direction's price above every
token threshold, for every service tier and batch, in every ``tiered_pricing`` tier and off-peak window, so each
inherited rate that would stand in for it there, the reasoning-token rate included for output, is dropped, and
each tier carries the flat price instead."""
flat_prices: Final = MappingProxyType(
{cost_field: value[cost_field] for cost_field in _TOKEN_COST_FIELDS if value.get(cost_field) is not None}
)
overridden_fields: Final = tuple(
itertools.chain.from_iterable(_DIRECTION_RATE_FIELDS[cost_field] for cost_field in flat_prices)
)
if not overridden_fields:
return entry
return MappingProxyType(
{
name: _rate_table_with_flat_prices(name, field_value, overridden_fields, flat_prices)
for name, field_value in entry.items()
if not _is_overridden_rate(name, overridden_fields)
}
)
@dataclass(frozen=True, slots=True)
class _BuiltinCostMatch:
key: str
entry: Mapping[str, object]
names_the_row: bool
@dataclass(frozen=True, slots=True)
class _LookupMiss:
tripped: bool
@dataclass(frozen=True, slots=True)
class _FallbackRegistration:
entry: Mapping[str, object]
matched_builtin: bool
def _fallback_registration(key: str, provider: str, value: Mapping[str, object]) -> _FallbackRegistration:
"""The key keeps its own entry, seeded from the capability rules an unregistered lookup resolves it to, with the
matched built-in entry on top. The matched row's provider stays behind, since the key's own lookup can reject it
(an ``azure_ai/`` key never resolves to an ``openai`` entry), and the key keeps the provider its caller gave."""
match: Final = _resolve_builtin_model_cost_entry(key=key, provider=provider)
generalized: Final[Mapping[str, object]] = match_capability_generalizations(key) or {}
if match is None:
return _FallbackRegistration(entry=generalized, matched_builtin=False)
inherited: Final = match.entry if match.names_the_row else _without_overridden_price_variants(match.entry, value)
own_fields: Final = {name: field_value for name, field_value in inherited.items() if name != "litellm_provider"}
return _FallbackRegistration(entry={**generalized, **own_fields}, matched_builtin=True)
def _resolve_builtin_model_cost_entry(key: str, provider: str) -> _BuiltinCostMatch | None:
"""Best-effort lookup of a built-in ``model_cost`` entry for a custom key
whose shape ``get_model_info`` cannot resolve (repeated provider prefixes
like ``bedrock/bedrock/bedrock/us.anthropic.claude-sonnet-4-6`` or region
aliases).
aliases), or whose lookup tripped.
Returns a copy of the matching entry so the caller can inherit its defaults
(most importantly cache pricing) without mutating the shared built-in.
Returns ``None`` when no safe match exists.
The key itself is tried first, then the key behind its provider prefixes
and bedrock route, so a catalog row whose lookup tripped inherits from
itself rather than from a region-stripped sibling priced at the base rate.
``names_the_row`` is set when the match is the row the key names, which
``get_model_info`` resolves it to, so the caller keeps that row's rates as
an untripped registration does. Returns ``None`` when no candidate exists
under a provider ``get_model_info`` would accept for ``provider``, or, when
the value names none, for the provider prefix the key itself carries,
which is what ``get_model_info`` derives when it reads the key back.
"""
candidates: Final[list[str]] = []
from litellm.llms.bedrock.common_utils import strip_bedrock_routing_prefix
segments: Final = key.split("/")
idx = 0
while idx < len(segments) - 1 and segments[idx] in LlmProvidersSet:
idx += 1
candidates.append("/".join(segments[idx:]))
base: Final = candidates[-1] if candidates else key
for region_prefix in _BEDROCK_REGION_PREFIXES:
if base.startswith(region_prefix):
candidates.append(base[len(region_prefix) :])
if provider:
stripped: Final = _strip_model_name(model=base, custom_llm_provider=provider)
if stripped != base:
candidates.append(stripped)
match_provider: Final = provider or (segments[0] if len(segments) > 1 and segments[0] in LlmProvidersSet else "")
provider_prefix_count: Final = next(
(idx for idx, segment in enumerate(segments[:-1]) if segment not in LlmProvidersSet), len(segments) - 1
)
prefix_stripped: Final = tuple("/".join(segments[idx:]) for idx in range(1, provider_prefix_count + 1))
prefix_free: Final = prefix_stripped[-1] if prefix_stripped else key
base: Final = (
strip_bedrock_routing_prefix(prefix_free) if match_provider in ("bedrock", "bedrock_converse") else prefix_free
)
region_stripped: Final = tuple(
base[len(region_prefix) :] for region_prefix in _BEDROCK_REGION_PREFIXES if base.startswith(region_prefix)
)
stripped: Final = _strip_model_name(model=base, custom_llm_provider=match_provider) if match_provider else base
row_names: Final = (key, *prefix_stripped, base, stripped)
candidates: Final = (key, *prefix_stripped, base, *region_stripped, stripped)
builtin_cost_map: Final = _builtin_cost_map()
for candidate in candidates:
entry = litellm.model_cost.get(candidate)
if entry is not None and entry.get("litellm_provider") is not None:
return dict(entry)
entry = builtin_cost_map.get(candidate)
if (
entry is not None
and entry.get("litellm_provider") is not None
and _check_provider_match(dict(entry), match_provider)
):
return _BuiltinCostMatch(key=candidate, entry=dict(entry), names_the_row=candidate in row_names)
return None
@ -3360,19 +3491,115 @@ def is_generalized_model_info(model_info: ModelInfo) -> bool:
return key not in litellm.model_cost and match_capability_generalizations(key) is not None
def _get_builtin_model_info_for_registration(model: str, custom_llm_provider: str | None) -> ModelInfo | None:
"""Resolve ``model`` to its built-in cost-map entry for registration merging.
_REGISTRATION_ATTEMPTS: Final = 3
Returns ``None`` when the lookup raises or when it resolved via a
fallback-generalization capability rule. A rule-derived entry carries no
pricing, so treating it as a hit would skip the built-in cache-pricing
inheritance for prefix-mangled keys.
"""
def _lookup_for_registration(model: str, custom_llm_provider: str | None) -> ModelInfo | _LookupMiss:
try:
info: Final = get_model_info(model=model, custom_llm_provider=custom_llm_provider)
except ModelNotMappedError:
return _LookupMiss(tripped=False)
except Exception:
return None
return None if is_generalized_model_info(info) else info
return _LookupMiss(tripped=True)
return _LookupMiss(tripped=False) if is_generalized_model_info(info) else info
def _get_builtin_model_info_for_registration(model: str, custom_llm_provider: str | None) -> ModelInfo | _LookupMiss:
"""Resolve ``model`` to its built-in cost-map entry for registration merging.
Misses when the lookup cannot map the model or resolved it via a
fallback-generalization capability rule, since a rule-derived entry carries no
pricing. A lookup that raised anything else, such as a concurrent registration
changing the map mid-walk, is retried, so a transient failure registers exactly
where the untripped lookup would.
"""
lookups: Final = (_lookup_for_registration(model, custom_llm_provider) for _ in range(_REGISTRATION_ATTEMPTS))
return next(
(lookup for lookup in lookups if not (isinstance(lookup, _LookupMiss) and lookup.tripped)),
_LookupMiss(tripped=True),
)
@dataclass(frozen=True, slots=True)
class _RegistrationWrite:
key: str
entry: Mapping[str, object]
def _registration_base(
key: str, value: Mapping[str, object], custom_llm_provider: str | None, warning_display_name: str | None
) -> _RegistrationWrite:
provider: Final = str(value.get("litellm_provider") or "")
if provider in PROVIDERS_THAT_AUTHENTICATE_ON_PROVIDER_INFO or any(
key.startswith(f"{p}/") for p in PROVIDERS_THAT_AUTHENTICATE_ON_PROVIDER_INFO
):
return _RegistrationWrite(key=key, entry=dict(_builtin_cost_map().get(key, {})))
builtin_model_info: Final = _get_builtin_model_info_for_registration(
model=key, custom_llm_provider=custom_llm_provider
)
if not isinstance(builtin_model_info, _LookupMiss):
return _RegistrationWrite(
key=builtin_model_info["key"],
entry=dict(_without_synthesized_costs(builtin_model_info, key=key, value=value)),
)
fallback: Final = _fallback_registration(key=key, provider=provider, value=value)
if not fallback.matched_builtin and (
value.get("cache_creation_input_token_cost") is None
and value.get("cache_read_input_token_cost") is None
and value.get("tiered_pricing") is None
and (value.get("input_cost_per_token") is not None or value.get("output_cost_per_token") is not None)
):
verbose_logger.warning(
"register_model: model=%s has custom pricing but not in built-in cost map and no prefix/region variant matched; cache_creation_input_token_cost and cache_read_input_token_cost will default to 0 for this model (input/output cost tracking is unaffected). To track cache cost, add them to model_info",
warning_display_name or key,
)
return _RegistrationWrite(key=key, entry=dict(fallback.entry))
def _registration_write(
key: str, value: Mapping[str, object], custom_llm_provider: str | None, warning_display_name: str | None
) -> _RegistrationWrite:
base: Final = _registration_base(key, value, custom_llm_provider, warning_display_name)
# ``get_model_info`` returns ``litellm_provider: None`` when the
# provider is unknown (e.g. custom deployments registered via
# ``Router.add_deployment``). Persisting that None into
# ``litellm.model_cost`` causes ``_check_provider_match`` to drop
# custom pricing on subsequent cost lookups.
existing_model: Final = {
name: field_value
for name, field_value in base.entry.items()
if name != "litellm_provider" or field_value is not None
}
return _RegistrationWrite(key=base.key, entry=update_dictionary(existing_model, value))
def _registration_attempt(
key: str,
value: Mapping[str, object],
custom_llm_provider: str | None,
warning_display_name: str | None,
final_attempt: bool,
) -> _RegistrationWrite | None:
source: Final = _builtin_cost_map()
write: Final = _registration_write(key, value, custom_llm_provider, warning_display_name)
_replace_cost_map_entry(_builtin_cost_map() if final_attempt else source, write.key, write.entry)
return write if final_attempt or litellm.model_cost is source else None
def _register_on_current_map(
key: str, value: Mapping[str, object], custom_llm_provider: str | None, warning_display_name: str | None
) -> _RegistrationWrite:
"""A price data reload that swaps the map while the entry is being built or written would otherwise get the old
map's prices written over its own, or drop the entry with the old map, so each attempt writes into the map it was
built from and the entry is rebuilt when a reload replaced that map before the write finished. The last attempt
writes into the current map, so the entry is never lost."""
attempts: Final = (
_registration_attempt(
key, value, custom_llm_provider, warning_display_name, final_attempt=attempt == _REGISTRATION_ATTEMPTS - 1
)
for attempt in range(_REGISTRATION_ATTEMPTS)
)
return next(write for write in attempts if write is not None)
_runtime_registered_model_cost: Final[dict[str, dict[str, object]]] = {} # mutable-ok: replayed on reload
@ -3492,79 +3719,15 @@ def register_model(
for _registered_key, _registered_value in _registrations.items():
_runtime_registered_model_cost[_registered_key] = dict(_registered_value)
_skip_get_model_info_providers: Final = PROVIDERS_THAT_AUTHENTICATE_ON_PROVIDER_INFO
for key, value in loaded_model_cost.items():
## get model info ##
provider = value.get("litellm_provider", "")
_key_str = str(key)
if provider in _skip_get_model_info_providers or any(
_key_str.startswith(f"{p}/") for p in _skip_get_model_info_providers
):
existing_model = litellm.model_cost.get(key, {})
model_cost_key = key
else:
builtin_model_info = _get_builtin_model_info_for_registration(
model=_key_str, custom_llm_provider=custom_llm_provider
)
if builtin_model_info is not None:
existing_model = cast(dict, builtin_model_info)
model_cost_key = existing_model["key"]
else:
# An exact entry ends the lookup ladder before the capability rules are
# consulted, so seed from them: otherwise registering an unmapped model
# shadows the very defaults it would have resolved to unregistered.
existing_model = dict(match_capability_generalizations(_key_str) or {})
model_cost_key = key
builtin_entry = _resolve_builtin_model_cost_entry(key=_key_str, provider=provider)
if builtin_entry is not None:
for field in _CACHE_PRICING_FIELDS:
if value.get(field) is None and builtin_entry.get(field) is not None:
existing_model[field] = builtin_entry[field]
elif (
value.get("cache_creation_input_token_cost") is None
and value.get("cache_read_input_token_cost") is None
and value.get("tiered_pricing") is None
and (
value.get("input_cost_per_token") is not None or value.get("output_cost_per_token") is not None
)
):
verbose_logger.warning(
"register_model: model=%s has custom pricing but not in built-in cost map and no prefix/region variant matched; cache_creation_input_token_cost and cache_read_input_token_cost will default to 0 for this model (input/output cost tracking is unaffected). To track cache cost, add them to model_info",
warning_display_name or key,
)
# ``get_model_info`` returns ``litellm_provider: None`` when the
# provider is unknown (e.g. custom deployments registered via
# ``Router.add_deployment``). Persisting that None into
# ``litellm.model_cost`` causes ``_check_provider_match`` to drop
# custom pricing on subsequent cost lookups.
if existing_model.get("litellm_provider") is None:
existing_model.pop("litellm_provider", None)
# Same pattern for cost fields (#30198): ``_get_model_info_helper``
# synthesizes ``input_cost_per_token`` / ``output_cost_per_token``
# = 0 when they are absent from the raw entry. Writing those zeros
# back flips a sparse entry from "no cost keys" (priced via name)
# to "cost keys = 0" (free), which makes
# ``_is_cost_explicitly_configured`` return True and silently
# disables budget enforcement on the next re-registration.
_raw_entry = litellm.model_cost.get(model_cost_key)
if _raw_entry is None:
_raw_entry = litellm.model_cost.get(key)
if _raw_entry is None:
_raw_entry = {}
for _cost_field in ("input_cost_per_token", "output_cost_per_token"):
if _cost_field not in _raw_entry and _cost_field not in value:
existing_model.pop(_cost_field, None)
## override / add new keys to the existing model cost dictionary
updated_dictionary = update_dictionary(existing_model, value)
litellm.model_cost.setdefault(model_cost_key, {}).update(updated_dictionary)
write = _register_on_current_map(str(key), value, custom_llm_provider, warning_display_name)
# Invalidate case-insensitive lookup map since model_cost was modified
_invalidate_model_cost_lowercase_map()
verbose_logger.debug("added/updated model=%s in litellm.model_cost: %s", model_cost_key, model_cost_key)
verbose_logger.debug("added/updated model=%s in litellm.model_cost: %s", write.key, write.key)
if add_to_provider_model_sets and value.get("litellm_provider") == "openrouter":
litellm.openrouter_models.add(_key_str.split("/", 1)[-1])
litellm.openrouter_models.add(str(key).split("/", 1)[-1])
if add_to_provider_model_sets:
litellm.add_known_models(loaded_model_cost)
return model_cost
@ -5732,7 +5895,7 @@ def _rebuild_model_cost_lowercase_map() -> dict[str, str]:
The rebuilt map (guaranteed to be not None).
"""
global _model_cost_lowercase_map
_model_cost_lowercase_map = {k.lower(): k for k in litellm.model_cost}
_model_cost_lowercase_map = {k.lower(): k for k in tuple(litellm.model_cost)}
return _model_cost_lowercase_map
@ -5747,9 +5910,7 @@ def _handle_stale_map_entry_rebuild(
Returns:
The matched key if found after rebuild, None otherwise.
"""
global _model_cost_lowercase_map
_model_cost_lowercase_map = _rebuild_model_cost_lowercase_map()
matched_key: Final = _model_cost_lowercase_map.get(potential_key_lower)
matched_key: Final = _rebuild_model_cost_lowercase_map().get(potential_key_lower)
if matched_key is not None and matched_key in litellm.model_cost:
return matched_key
return None
@ -5767,7 +5928,7 @@ def _handle_new_key_with_scan(
The matched key if found, None otherwise.
"""
global _model_cost_lowercase_map
for key in litellm.model_cost:
for key in tuple(_builtin_cost_map()):
if key.lower() == potential_key_lower:
_model_cost_lowercase_map = _rebuild_model_cost_lowercase_map()
return key
@ -5789,18 +5950,19 @@ def get_model_cost_key(potential_key: str) -> str | None:
called and confirmed not to cause performance issues, add it to the allowed_helpers
list in: tests/code_coverage_tests/check_get_model_cost_key_performance.py
"""
global _model_cost_lowercase_map
# Exact match (O(1))
if potential_key in litellm.model_cost:
return potential_key
# Case-insensitive lookup via map (O(1))
if _model_cost_lowercase_map is None:
_model_cost_lowercase_map = _rebuild_model_cost_lowercase_map()
# Case-insensitive lookup via map (O(1)); bound locally because another thread's
# registration can invalidate the module-level map at any point
cached_lowercase_map: Final = _model_cost_lowercase_map
lowercase_map: Final = (
cached_lowercase_map if cached_lowercase_map is not None else _rebuild_model_cost_lowercase_map()
)
potential_key_lower: Final = potential_key.lower()
matched_key = _model_cost_lowercase_map.get(potential_key_lower)
matched_key = lowercase_map.get(potential_key_lower)
# Verify key exists (O(1) - handles model_cost.pop() case)
if matched_key is not None and matched_key in litellm.model_cost:

View file

@ -0,0 +1,433 @@
import itertools
import json
import re
import threading
import uuid
from collections.abc import Callable, Mapping
from concurrent.futures import ThreadPoolExecutor
from dataclasses import dataclass
from pathlib import Path
from types import MappingProxyType
from typing import Final
import httpx
import pytest
from integration._support.client import Gateway, eventually, object_value, string_value
from integration._support.database import read_rows
from integration._support.process import graceful_stop_seconds, owned_proxy
from integration._support.wire import Reply, Request, wire_server
from pydantic import JsonValue
_ROW: Final = "anthropic.claude-audit-reload-v1:0"
_GEO: Final = f"au.{_ROW}"
_FINETUNE: Final = "ft:gpt-audit-reload-2025-01-01"
_CLAUDE: Final = "claude-audit-reload-sonnet"
_MINI: Final = "gpt-audit-reload-mini"
_USAGE: Final = re.compile(r"bill (\d+) (\d+) (\d+)")
_CUSTOM: Final = MappingProxyType({"input_cost_per_token": 1.1e-06, "output_cost_per_token": 2.3e-06})
_COST_MAP_LIMITS: Final = MappingProxyType({"MODEL_COST_MAP_MIN_MODEL_COUNT": "1", "MODEL_COST_MAP_MAX_SHRINK_RATIO": "0"})
_PUBLISHED_SCALE: Final = 2.0
_CATALOG: Final[Mapping[str, Mapping[str, JsonValue]]] = MappingProxyType(
{
_ROW: {
"litellm_provider": "bedrock_converse",
"mode": "chat",
"input_cost_per_token": 3e-06,
"output_cost_per_token": 1.5e-05,
"cache_read_input_token_cost": 3e-07,
"input_cost_per_token_above_200k_tokens": 6e-06,
"output_cost_per_token_above_200k_tokens": 2.25e-05,
},
_GEO: {
"litellm_provider": "bedrock_converse",
"mode": "chat",
"input_cost_per_token": 3.3e-06,
"output_cost_per_token": 1.65e-05,
},
_FINETUNE: {
"litellm_provider": "openai",
"mode": "chat",
"input_cost_per_token": 3.75e-06,
"output_cost_per_token": 1.5e-05,
"cache_read_input_token_cost": 1.875e-06,
},
_CLAUDE: {
"litellm_provider": "anthropic",
"mode": "chat",
"input_cost_per_token": 3e-06,
"output_cost_per_token": 1.5e-05,
"cache_read_input_token_cost": 3e-07,
},
_MINI: {
"litellm_provider": "openai",
"mode": "chat",
"input_cost_per_token": 5e-07,
"output_cost_per_token": 2e-06,
},
}
)
@dataclass(frozen=True, slots=True)
class _Usage:
prompt: int
cached: int
completion: int
def text(self) -> str:
return f"bill {self.prompt} {self.cached} {self.completion}"
@dataclass(frozen=True, slots=True)
class _Rates:
input: float
output: float
cache_read: float
def cost(self, usage: _Usage) -> float:
return (
(usage.prompt - usage.cached) * self.input + usage.cached * self.cache_read + usage.completion * self.output
)
@dataclass(frozen=True, slots=True)
class _Deployment:
name: str
model: str
prices: Mapping[str, float]
usage: _Usage
rates: Callable[[float], _Rates | None]
@dataclass(frozen=True, slots=True)
class _Charge:
request_id: str
expected: float
def _catalog(scale: float) -> dict[str, dict[str, JsonValue]]:
return {
model: {name: value * scale if isinstance(value, float) else value for name, value in row.items()}
for model, row in _CATALOG.items()
}
def _rate(model: str, field: str, scale: float) -> float:
rate: Final = _CATALOG[model][field]
assert isinstance(rate, float), rate
return rate * scale
def _catalog_rates(model: str) -> Callable[[float], _Rates]:
return lambda scale: _Rates(
input=_rate(model, "input_cost_per_token", scale),
output=_rate(model, "output_cost_per_token", scale),
cache_read=_rate(model, "cache_read_input_token_cost", scale)
if "cache_read_input_token_cost" in _CATALOG[model]
else _rate(model, "input_cost_per_token", scale),
)
def _custom_rates(_scale: float) -> _Rates:
return _Rates(
input=_CUSTOM["input_cost_per_token"],
output=_CUSTOM["output_cost_per_token"],
cache_read=_CUSTOM["input_cost_per_token"],
)
def _unpriced(_scale: float) -> None:
return None
def _expected_cost(rates: _Rates | None, usage: _Usage) -> float:
return 0.0 if rates is None else rates.cost(usage)
def _expected_listing(rates: _Rates | None) -> tuple[object, object]:
if rates is None:
return None, None
return pytest.approx(rates.input), pytest.approx(rates.output)
def _connection(model: str, wire_url: str) -> dict[str, JsonValue]:
if model.startswith("bedrock/"):
return {"api_base": wire_url, "api_key": "synthetic-bedrock-bearer", "aws_region_name": "ap-northeast-1"}
if model.startswith("azure/"):
return {"api_base": wire_url, "api_key": "synthetic-azure-key", "api_version": "2025-01-01-preview"}
return {"api_base": f"{wire_url}/v1", "api_key": "synthetic-openai-key"}
def _config(directory: Path, deployments: tuple[_Deployment, ...], wire_url: str) -> Path:
config: Final = directory / "price_data_reload_config.yaml"
config.write_text(
json.dumps(
{
"model_list": [
{
"model_name": deployment.name,
"litellm_params": {
"model": deployment.model,
**_connection(deployment.model, wire_url),
**deployment.prices,
},
}
for deployment in deployments
],
"general_settings": {
"master_key": "os.environ/LITELLM_MASTER_KEY",
"database_url": "os.environ/DATABASE_URL",
},
"router_settings": {"disable_cooldowns": True},
}
)
)
return config
def _usage_in(request: Request) -> _Usage:
found: Final = _USAGE.search(request.body.decode())
assert found is not None, request
return _Usage(prompt=int(found.group(1)), cached=int(found.group(2)), completion=int(found.group(3)))
def _reply(request: Request) -> Reply:
usage: Final = _usage_in(request)
if "/converse" in request.target:
return Reply(
body=json.dumps(
{
"output": {"message": {"role": "assistant", "content": [{"text": "priced"}]}},
"stopReason": "end_turn",
"usage": {
"inputTokens": usage.prompt,
"outputTokens": usage.completion,
"totalTokens": usage.prompt + usage.completion,
},
"metrics": {"latencyMs": 1},
}
).encode()
)
if "/invoke" in request.target:
return Reply(
body=json.dumps(
{
"id": f"msg_{uuid.uuid4().hex}",
"type": "message",
"role": "assistant",
"model": "claude-scripted",
"content": [{"type": "text", "text": "priced"}],
"stop_reason": "end_turn",
"stop_sequence": None,
"usage": {"input_tokens": usage.prompt, "output_tokens": usage.completion},
}
).encode()
)
return Reply(
body=json.dumps(
{
"id": f"chatcmpl-{uuid.uuid4().hex}",
"object": "chat.completion",
"created": 1,
"model": "scripted",
"choices": [
{"index": 0, "message": {"role": "assistant", "content": "priced"}, "finish_reason": "stop"}
],
"usage": {
"prompt_tokens": usage.prompt,
"completion_tokens": usage.completion,
"total_tokens": usage.prompt + usage.completion,
"prompt_tokens_details": {"cached_tokens": usage.cached},
},
}
).encode()
)
def _responder(published: threading.Event) -> Callable[[Request], Reply]:
def respond(request: Request) -> Reply:
if request.target == "/model_prices.json":
return Reply(body=json.dumps(_catalog(_PUBLISHED_SCALE if published.is_set() else 1.0)).encode())
if request.method == "GET":
return Reply(body=json.dumps({"object": "list", "data": []}).encode())
return _reply(request)
return respond
def _listed_rates(proxy: Gateway, name: str) -> tuple[JsonValue, JsonValue]:
entries: Final = proxy.get("/model/info")["data"]
assert isinstance(entries, list), entries
info: Final = next(
object_value(object_value(entry)["model_info"])
for entry in entries
if object_value(entry)["model_name"] == name
)
return info.get("input_cost_per_token"), info.get("output_cost_per_token")
def _billed(client: httpx.Client, key: str, deployment: _Deployment) -> tuple[str, float | None]:
response: Final = client.post(
"/v1/chat/completions",
json={"model": deployment.name, "messages": [{"role": "user", "content": deployment.usage.text()}]},
headers={"Authorization": f"Bearer {key}"},
)
assert response.status_code == 200, response.text
reported: Final = response.headers.get("x-litellm-response-cost")
return string_value(object_value(response.json())["id"]), None if reported is None else float(reported)
def _charged_as_listed(proxy: Gateway, deployment: _Deployment, scale: float) -> _Charge:
rates: Final = deployment.rates(scale)
assert _listed_rates(proxy, deployment.name) == _expected_listing(rates), deployment.name
request_id, charged = _billed(proxy.client, proxy.key, deployment)
expected: Final = _expected_cost(rates, deployment.usage)
assert charged == (None if rates is None else pytest.approx(expected, rel=1e-6)), deployment.name
return _Charge(request_id=request_id, expected=expected)
def _spend(request_id: str) -> float:
rows: Final = eventually(
lambda: read_rows('SELECT spend FROM "LiteLLM_SpendLogs" WHERE request_id = %s', (request_id,)),
lambda values: len(values) == 1,
seconds=70,
)
spend: Final = rows[0]["spend"]
assert isinstance(spend, (int, float)), rows
return float(spend)
def _proxy_environment(wire_url: str) -> dict[str, str]:
return {**_COST_MAP_LIMITS, "LITELLM_MODEL_COST_MAP_URL": f"{wire_url}/model_prices.json"}
def _assert_priced_across_a_reload(gateway: Gateway, tmp_path: Path, deployments: tuple[_Deployment, ...]) -> None:
published: Final = threading.Event()
with (
wire_server(_responder(published)) as wire,
owned_proxy(
gateway,
tmp_path,
_proxy_environment(wire.url),
config=_config(tmp_path, deployments, wire.url),
remove_environment=("LITELLM_LOCAL_MODEL_COST_MAP",),
) as proxy,
):
before: Final = tuple(_charged_as_listed(proxy, deployment, 1.0) for deployment in deployments)
published.set()
reload: Final = proxy.request("POST", "/reload/model_cost_map")
assert reload.status_code == 200, reload.text
after: Final = tuple(_charged_as_listed(proxy, deployment, _PUBLISHED_SCALE) for deployment in deployments)
for charge in (*before, *after):
assert _spend(charge.request_id) == pytest.approx(charge.expected, rel=1e-6)
@pytest.mark.timeout(2 * graceful_stop_seconds() + 120)
def test_price_data_reload_reprices_catalog_deployments_and_keeps_custom_prices(
gateway: Gateway, tmp_path: Path
) -> None:
_assert_priced_across_a_reload(
gateway,
tmp_path,
(
_Deployment("geo-unpriced", f"bedrock/{_GEO}", {}, _Usage(1000, 0, 500), _catalog_rates(_GEO)),
_Deployment("mini-custom", f"openai/{_MINI}", _CUSTOM, _Usage(1000, 0, 500), _custom_rates),
),
)
@pytest.mark.timeout(2 * graceful_stop_seconds() + 120)
@pytest.mark.parametrize(
"deployment",
(
_Deployment(
"region-unpriced", f"bedrock/ap-northeast-1.{_ROW}", {}, _Usage(1000, 0, 500), _catalog_rates(_ROW)
),
_Deployment(
"finetune-unpriced",
f"openai/{_FINETUNE}:org:suffix:y",
{},
_Usage(2000, 1000, 1000),
_catalog_rates(_FINETUNE),
),
_Deployment("finetune-custom", f"openai/{_FINETUNE}:org:suffix:z", _CUSTOM, _Usage(2000, 0, 1000), _custom_rates),
_Deployment("cross-provider-unpriced", f"azure/{_CLAUDE}", {}, _Usage(2000, 1000, 1000), _unpriced),
_Deployment(
"region-custom-above-threshold",
f"bedrock/ap-northeast-1.{_ROW}",
_CUSTOM,
_Usage(250_000, 0, 1000),
_custom_rates,
),
),
ids=lambda deployment: deployment.name,
)
def test_deployment_bills_the_catalog_row_its_registration_resolves_to_across_a_reload(
gateway: Gateway, tmp_path: Path, deployment: _Deployment
) -> None:
_assert_priced_across_a_reload(gateway, tmp_path, (deployment,))
def _storm_deployment(index: int) -> _Deployment:
prices: Final = MappingProxyType(
{"input_cost_per_token": (index + 1) * 1e-07, "output_cost_per_token": (index + 1) * 3e-07}
)
return _Deployment(
f"storm-{index}",
f"openai/gpt-audit-reload-storm-{index}",
prices,
_Usage(100, 0, 50),
lambda _scale: _Rates(
input=prices["input_cost_per_token"],
output=prices["output_cost_per_token"],
cache_read=prices["input_cost_per_token"],
),
)
def _storm_traffic(
base_url: str, key: str, deployments: tuple[_Deployment, ...], offset: int, finished: threading.Event
) -> tuple[tuple[float, float], ...]:
rotation: Final = (
deployments[(offset + turn) % len(deployments)] for turn, _ in enumerate(iter(finished.is_set, True))
)
with httpx.Client(base_url=base_url, timeout=60, trust_env=False) as client:
return tuple(
(_billed(client, key, deployment)[1], _expected_cost(deployment.rates(1.0), deployment.usage))
for deployment in rotation
)
@pytest.mark.timeout(2 * graceful_stop_seconds() + 120)
def test_price_data_reloads_under_custom_priced_traffic_keep_every_deployment_priced(
gateway: Gateway, tmp_path: Path
) -> None:
storm: Final = tuple(_storm_deployment(index) for index in range(24))
geo: Final = _Deployment("geo-unpriced", f"bedrock/{_GEO}", {}, _Usage(1000, 0, 500), _catalog_rates(_GEO))
finished: Final = threading.Event()
with (
wire_server(_responder(threading.Event())) as wire,
owned_proxy(
gateway,
tmp_path,
_proxy_environment(wire.url),
config=_config(tmp_path, (geo, *storm), wire.url),
remove_environment=("LITELLM_LOCAL_MODEL_COST_MAP",),
) as proxy,
ThreadPoolExecutor(max_workers=8) as pool,
):
traffic: Final = tuple(
pool.submit(_storm_traffic, str(proxy.client.base_url), proxy.key, storm, offset, finished)
for offset in range(8)
)
reloads: Final = tuple(proxy.request("POST", "/reload/model_cost_map") for _ in range(40))
finished.set()
charges: Final = tuple(itertools.chain.from_iterable(calls.result() for calls in traffic))
assert tuple(reload.status_code for reload in reloads) == (200,) * 40, tuple(
reload.text for reload in reloads if reload.status_code != 200
)
assert len(charges) >= 8
assert tuple(charged for charged, _ in charges) == pytest.approx(tuple(expected for _, expected in charges))
settled: Final = tuple(_charged_as_listed(proxy, deployment, 1.0) for deployment in (geo, *storm))
for charge in settled:
assert _spend(charge.request_id) == pytest.approx(charge.expected, rel=1e-6)

View file

@ -10,13 +10,16 @@ calculations for DB-sourced models with prompt caching pricing.
"""
import copy
import itertools
import os
from typing import Final
from collections.abc import Callable, Mapping
from typing import Final, NoReturn
import pytest
import litellm
from litellm.litellm_core_utils.get_model_cost_map import _expand_model_aliases, adopt_model_cost_map
from litellm.main import _build_custom_pricing_entry
from litellm.utils import _invalidate_model_cost_lowercase_map
@ -796,6 +799,609 @@ def test_embedding_direct_sdk_custom_pricing_still_registers_shared_key():
_invalidate_model_cost_lowercase_map()
@pytest.mark.parametrize("route", ["", "converse/", "invoke/"])
def test_register_model_prices_a_geo_alias_from_its_builtin_when_the_lookup_fails(
monkeypatch: pytest.MonkeyPatch, route: str
) -> None:
from litellm.types.utils import ModelResponse, Usage
monkeypatch.setenv("LITELLM_LOCAL_MODEL_COST_MAP", "True")
monkeypatch.setattr(litellm, "model_cost", litellm.get_model_cost_map(url=""))
_invalidate_model_cost_lowercase_map()
canonical_key: Final = "au.anthropic.claude-sonnet-5"
alias: Final = f"bedrock/{route}{canonical_key}"
canonical: Final = litellm.model_cost[canonical_key]
def lookup_tripped_by_a_concurrent_registration(model: str, custom_llm_provider: str | None = None) -> NoReturn:
raise RuntimeError("dictionary changed size during iteration")
with pytest.MonkeyPatch.context() as lookup_patch:
lookup_patch.setattr(litellm.utils, "get_model_info", lookup_tripped_by_a_concurrent_registration)
litellm.register_model(
{alias: {"litellm_provider": "bedrock", "mode": "chat", "supports_vision": True}},
persist_across_reloads=False,
)
usage: Final = Usage(prompt_tokens=1000, completion_tokens=100, total_tokens=1100)
alias_cost: Final = litellm.completion_cost(
completion_response=ModelResponse(model=alias, usage=usage), model=alias, custom_llm_provider="bedrock"
)
canonical_cost: Final = litellm.completion_cost(
completion_response=ModelResponse(model=canonical_key, usage=usage),
model=canonical_key,
custom_llm_provider="bedrock",
)
try:
assert alias_cost == canonical_cost > 0
assert litellm.get_model_info(alias)["input_cost_per_token"] == canonical["input_cost_per_token"]
assert litellm.get_model_info(alias)["supports_vision"] is True
finally:
_invalidate_model_cost_lowercase_map()
def test_register_model_keeps_a_catalog_rows_own_prices_when_the_lookup_fails(monkeypatch: pytest.MonkeyPatch) -> None:
geo_key: Final = "au.anthropic.lit-5853-geo-model"
geo_row: Final = {
"litellm_provider": "bedrock_converse",
"mode": "chat",
"input_cost_per_token": 3.3e-06,
"output_cost_per_token": 1.65e-05,
"cache_read_input_token_cost": 3.3e-07,
}
sibling_row: Final = {
"litellm_provider": "bedrock_converse",
"mode": "chat",
"input_cost_per_token": 3e-06,
"output_cost_per_token": 1.5e-05,
"cache_read_input_token_cost": 3e-07,
}
monkeypatch.setenv("LITELLM_LOCAL_MODEL_COST_MAP", "True")
monkeypatch.setattr(
litellm,
"model_cost",
{
**litellm.get_model_cost_map(url=""),
geo_key: dict(geo_row),
"anthropic.lit-5853-geo-model": sibling_row,
},
)
_invalidate_model_cost_lowercase_map()
def lookup_tripped_by_a_concurrent_registration(model: str, custom_llm_provider: str | None = None) -> NoReturn:
raise RuntimeError("dictionary changed size during iteration")
with pytest.MonkeyPatch.context() as lookup_patch:
lookup_patch.setattr(litellm.utils, "get_model_info", lookup_tripped_by_a_concurrent_registration)
litellm.register_model({geo_key: {"mode": "chat"}}, persist_across_reloads=False)
try:
for field in ("input_cost_per_token", "output_cost_per_token", "cache_read_input_token_cost"):
assert litellm.model_cost[geo_key][field] == geo_row[field]
assert litellm.get_model_info(geo_key)[field] == geo_row[field]
assert litellm.model_cost[geo_key]["litellm_provider"] == geo_row["litellm_provider"]
finally:
_invalidate_model_cost_lowercase_map()
@pytest.mark.parametrize("route", ["", "converse/", "invoke/"])
def test_register_model_leaves_no_shadow_key_when_the_lookup_trips_once(
monkeypatch: pytest.MonkeyPatch, route: str
) -> None:
catalog_key: Final = "au.anthropic.lit-5853-geo-model"
alias: Final = f"bedrock/{route}{catalog_key}"
repriced_input_cost: Final = 9e-06
monkeypatch.setenv("LITELLM_LOCAL_MODEL_COST_MAP", "True")
monkeypatch.setattr(
litellm,
"model_cost",
{
**litellm.get_model_cost_map(url=""),
catalog_key: {
"litellm_provider": "bedrock_converse",
"mode": "chat",
"input_cost_per_token": 3.3e-06,
"output_cost_per_token": 1.65e-05,
},
},
)
_invalidate_model_cost_lowercase_map()
real_lookup: Final = litellm.utils.get_model_info
lookups: Final = itertools.count()
def lookup_tripped_once_by_a_concurrent_registration(model: str, custom_llm_provider: str | None = None) -> object:
if next(lookups) == 0:
raise RuntimeError("dictionary changed size during iteration")
return real_lookup(model=model, custom_llm_provider=custom_llm_provider)
with pytest.MonkeyPatch.context() as lookup_patch:
lookup_patch.setattr(litellm.utils, "get_model_info", lookup_tripped_once_by_a_concurrent_registration)
litellm.register_model({alias: {"litellm_provider": "bedrock", "mode": "chat"}}, persist_across_reloads=False)
litellm.register_model({catalog_key: {"input_cost_per_token": repriced_input_cost}}, persist_across_reloads=False)
try:
assert litellm.get_model_info(alias)["key"] == catalog_key
assert litellm.get_model_info(alias)["input_cost_per_token"] == repriced_input_cost
finally:
_invalidate_model_cost_lowercase_map()
@pytest.mark.parametrize("key_prefix", ["", "bedrock/", "bedrock/converse/"])
def test_register_model_bills_a_catalog_rows_tier_rates_the_same_when_the_lookup_fails(
monkeypatch: pytest.MonkeyPatch, key_prefix: str
) -> None:
from litellm.types.utils import ModelResponse, Usage
catalog_key: Final = "au.anthropic.lit-5853-tiered-geo-model"
registered_key: Final = f"{key_prefix}{catalog_key}"
catalog_row: Final = {
"litellm_provider": "bedrock_converse",
"mode": "chat",
"input_cost_per_token": 3.3e-06,
"output_cost_per_token": 1.65e-05,
"input_cost_per_token_above_200k_tokens": 6.6e-06,
"output_cost_per_token_above_200k_tokens": 2.475e-05,
}
builtin_cost_map: Final = litellm.get_model_cost_map(url="")
monkeypatch.setenv("LITELLM_LOCAL_MODEL_COST_MAP", "True")
def lookup_tripped_by_a_concurrent_registration(model: str, custom_llm_provider: str | None = None) -> NoReturn:
raise RuntimeError("dictionary changed size during iteration")
def long_prompt_cost_after_registering_with(lookup: Callable[..., object]) -> float:
monkeypatch.setattr(litellm, "model_cost", {**builtin_cost_map, catalog_key: dict(catalog_row)})
_invalidate_model_cost_lowercase_map()
with pytest.MonkeyPatch.context() as lookup_patch:
lookup_patch.setattr(litellm.utils, "get_model_info", lookup)
litellm.register_model(
{
registered_key: {
"litellm_provider": "bedrock",
"input_cost_per_token": 1e-06,
"output_cost_per_token": 2e-06,
}
},
persist_across_reloads=False,
)
return litellm.completion_cost(
completion_response=ModelResponse(
model=registered_key,
usage=Usage(prompt_tokens=250_000, completion_tokens=1_000, total_tokens=251_000),
),
model=registered_key,
custom_llm_provider="bedrock",
)
try:
assert long_prompt_cost_after_registering_with(lookup_tripped_by_a_concurrent_registration) == pytest.approx(
long_prompt_cost_after_registering_with(litellm.utils.get_model_info)
)
finally:
_invalidate_model_cost_lowercase_map()
@pytest.mark.parametrize("lookup_trips", [False, True])
def test_register_model_prices_a_provider_less_azure_ai_key_without_repricing_the_openai_row(
monkeypatch: pytest.MonkeyPatch, lookup_trips: bool
) -> None:
openai_key: Final = "lit-5853-openai-model"
azure_ai_key: Final = f"azure_ai/{openai_key}"
openai_row: Final = {
"litellm_provider": "openai",
"mode": "chat",
"input_cost_per_token": 5e-06,
"output_cost_per_token": 1.5e-05,
}
monkeypatch.setenv("LITELLM_LOCAL_MODEL_COST_MAP", "True")
monkeypatch.setenv("AZURE_AI_API_BASE", "https://lit-5853.openai.azure.com")
monkeypatch.setattr(litellm, "model_cost", {**litellm.get_model_cost_map(url=""), openai_key: dict(openai_row)})
monkeypatch.setattr(
litellm, "open_ai_chat_completion_models", {*litellm.open_ai_chat_completion_models, openai_key}
)
_invalidate_model_cost_lowercase_map()
def lookup_tripped_by_a_concurrent_registration(model: str, custom_llm_provider: str | None = None) -> NoReturn:
raise RuntimeError("dictionary changed size during iteration")
with pytest.MonkeyPatch.context() as lookup_patch:
if lookup_trips:
lookup_patch.setattr(litellm.utils, "get_model_info", lookup_tripped_by_a_concurrent_registration)
litellm.register_model(
{azure_ai_key: {"input_cost_per_token": 1e-06, "output_cost_per_token": 2e-06}},
persist_across_reloads=False,
)
try:
assert litellm.model_cost[openai_key] == openai_row
assert litellm.cost_per_token(model=azure_ai_key, prompt_tokens=1000, completion_tokens=1000) == pytest.approx(
(1e-03, 2e-03)
)
finally:
_invalidate_model_cost_lowercase_map()
def test_register_model_keeps_the_prices_a_reload_adopts_while_it_registers(monkeypatch: pytest.MonkeyPatch) -> None:
catalog_key: Final = "au.anthropic.lit-5853-reloaded-model"
deployment_key: Final = f"bedrock/{catalog_key}"
catalog_row: Final = {
"litellm_provider": "bedrock_converse",
"mode": "chat",
"input_cost_per_token": 3.3e-06,
"output_cost_per_token": 1.65e-05,
}
reloaded_input_cost: Final = 6.6e-06
builtin_cost_map: Final = litellm.get_model_cost_map(url="")
monkeypatch.setenv("LITELLM_LOCAL_MODEL_COST_MAP", "True")
monkeypatch.setattr(litellm, "model_cost", {**builtin_cost_map, catalog_key: dict(catalog_row)})
_invalidate_model_cost_lowercase_map()
reloaded_cost_map: Final = {
**builtin_cost_map,
catalog_key: {**catalog_row, "input_cost_per_token": reloaded_input_cost},
}
real_lookup: Final = litellm.utils.get_model_info
def lookup_racing_a_price_data_reload(model: str, custom_llm_provider: str | None = None) -> object:
info: Final = real_lookup(model=model, custom_llm_provider=custom_llm_provider)
if litellm.model_cost is not reloaded_cost_map:
adopt_model_cost_map(reloaded_cost_map)
return info
with pytest.MonkeyPatch.context() as lookup_patch:
lookup_patch.setattr(litellm.utils, "get_model_info", lookup_racing_a_price_data_reload)
litellm.register_model({deployment_key: {"litellm_provider": "bedrock"}}, persist_across_reloads=False)
try:
assert litellm.model_cost[catalog_key]["input_cost_per_token"] == reloaded_input_cost
assert litellm.get_model_info(deployment_key)["input_cost_per_token"] == reloaded_input_cost
finally:
_invalidate_model_cost_lowercase_map()
def test_register_model_keeps_its_price_when_a_reload_adopts_a_map_as_it_writes(
monkeypatch: pytest.MonkeyPatch,
) -> None:
catalog_key: Final = "au.anthropic.lit-5853-reloaded-as-written-model"
deployment_key: Final = f"bedrock/{catalog_key}"
catalog_row: Final = {
"litellm_provider": "bedrock_converse",
"mode": "chat",
"input_cost_per_token": 3.3e-06,
"output_cost_per_token": 1.65e-05,
}
reloaded_input_cost: Final = 6.6e-06
registered_output_cost: Final = 2e-05
builtin_cost_map: Final = litellm.get_model_cost_map(url="")
reloaded_cost_map: Final = {
**builtin_cost_map,
catalog_key: {**catalog_row, "input_cost_per_token": reloaded_input_cost},
}
class CostMapAReloadReplacesAsItIsWritten(dict[str, dict[str, object]]):
def update(self, other: Mapping[str, dict[str, object]], /) -> None: # pyright: ignore[reportIncompatibleMethodOverride] # registration writes pass one mapping
if litellm.model_cost is self:
adopt_model_cost_map(reloaded_cost_map)
super().update(other)
monkeypatch.setenv("LITELLM_LOCAL_MODEL_COST_MAP", "True")
monkeypatch.setattr(
litellm,
"model_cost",
CostMapAReloadReplacesAsItIsWritten({**builtin_cost_map, catalog_key: dict(catalog_row)}),
)
_invalidate_model_cost_lowercase_map()
litellm.register_model(
{deployment_key: {"litellm_provider": "bedrock", "output_cost_per_token": registered_output_cost}},
persist_across_reloads=False,
)
try:
assert litellm.model_cost is reloaded_cost_map
assert litellm.get_model_info(deployment_key)["input_cost_per_token"] == reloaded_input_cost
assert litellm.get_model_info(deployment_key)["output_cost_per_token"] == registered_output_cost
finally:
_invalidate_model_cost_lowercase_map()
def test_register_model_leaves_the_entry_a_lookup_is_walking_intact(monkeypatch: pytest.MonkeyPatch) -> None:
catalog_key: Final = "lit-5853-live-entry-model"
monkeypatch.setenv("LITELLM_LOCAL_MODEL_COST_MAP", "True")
monkeypatch.setattr(
litellm,
"model_cost",
{
**litellm.get_model_cost_map(url=""),
catalog_key: {"litellm_provider": "openai", "mode": "chat", "input_cost_per_token": 2e-06},
},
)
_invalidate_model_cost_lowercase_map()
walk: Final = iter(litellm.model_cost[catalog_key].items())
first_field: Final = next(walk)
try:
litellm.register_model(
{catalog_key: {"litellm_provider": "openai", "input_cost_per_token": 1e-06, "supports_vision": True}},
persist_across_reloads=False,
)
assert dict((first_field, *walk)) == {
"litellm_provider": "openai",
"mode": "chat",
"input_cost_per_token": 2e-06,
}
assert litellm.get_model_info(catalog_key)["input_cost_per_token"] == 1e-06
assert litellm.get_model_info(catalog_key)["supports_vision"] is True
finally:
_invalidate_model_cost_lowercase_map()
_CANONICAL_KEY: Final = "lit-5853-canonical-model"
_ALIAS_KEY: Final = "lit-5853-alias-model"
def _install_aliased_cost_map(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setenv("LITELLM_LOCAL_MODEL_COST_MAP", "True")
monkeypatch.setattr(
litellm,
"model_cost",
_expand_model_aliases(
{
**litellm.get_model_cost_map(url=""),
_CANONICAL_KEY: {
"aliases": [_ALIAS_KEY],
"litellm_provider": "openai",
"mode": "chat",
"input_cost_per_token": 2e-06,
"output_cost_per_token": 8e-06,
},
}
),
)
_invalidate_model_cost_lowercase_map()
def _token_prices(name: str) -> tuple[float | None, float | None]:
info: Final = litellm.get_model_info(name)
return info["input_cost_per_token"], info["output_cost_per_token"]
@pytest.mark.parametrize("registered_key", [_CANONICAL_KEY, _ALIAS_KEY])
def test_register_model_prices_every_alias_of_the_registered_entry(
monkeypatch: pytest.MonkeyPatch, registered_key: str
) -> None:
_install_aliased_cost_map(monkeypatch)
try:
litellm.register_model(
{registered_key: {"litellm_provider": "openai", "input_cost_per_token": 1e-06}},
persist_across_reloads=False,
)
litellm.register_model(
{registered_key: {"litellm_provider": "openai", "output_cost_per_token": 4e-06}},
persist_across_reloads=False,
)
assert {name: _token_prices(name) for name in (_CANONICAL_KEY, _ALIAS_KEY)} == {
_CANONICAL_KEY: (1e-06, 4e-06),
_ALIAS_KEY: (1e-06, 4e-06),
}
finally:
_invalidate_model_cost_lowercase_map()
def test_register_model_leaves_an_alias_repointed_at_its_own_entry(monkeypatch: pytest.MonkeyPatch) -> None:
_install_aliased_cost_map(monkeypatch)
litellm.model_cost[_ALIAS_KEY] = {
"litellm_provider": "openai",
"mode": "chat",
"input_cost_per_token": 5e-06,
"output_cost_per_token": 6e-06,
}
try:
litellm.register_model(
{_CANONICAL_KEY: {"litellm_provider": "openai", "input_cost_per_token": 1e-06}},
persist_across_reloads=False,
)
assert {name: _token_prices(name) for name in (_CANONICAL_KEY, _ALIAS_KEY)} == {
_CANONICAL_KEY: (1e-06, 8e-06),
_ALIAS_KEY: (5e-06, 6e-06),
}
finally:
_invalidate_model_cost_lowercase_map()
def test_register_model_inherits_builtin_token_pricing_for_unmapped_key(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setenv("LITELLM_LOCAL_MODEL_COST_MAP", "True")
monkeypatch.setattr(litellm, "model_cost", litellm.get_model_cost_map(url=""))
_invalidate_model_cost_lowercase_map()
builtin_key: Final = "us.anthropic.claude-sonnet-4-6"
registered_key: Final = f"bedrock/bedrock/bedrock/{builtin_key}"
builtin: Final = litellm.model_cost[builtin_key]
try:
litellm.register_model(
{registered_key: {"litellm_provider": "bedrock", "mode": "chat"}}, persist_across_reloads=False
)
info: Final = litellm.get_model_info(registered_key)
assert info["input_cost_per_token"] == builtin["input_cost_per_token"] > 0
assert info["output_cost_per_token"] == builtin["output_cost_per_token"] > 0
assert info["cache_read_input_token_cost"] == builtin["cache_read_input_token_cost"]
finally:
_invalidate_model_cost_lowercase_map()
def test_register_model_keeps_the_capabilities_an_unregistered_region_alias_resolves_to(
monkeypatch: pytest.MonkeyPatch,
) -> None:
from litellm.litellm_core_utils.fallback_generalizations import (
get_fallback_generalization_rules,
set_fallback_generalizations,
)
sibling_key: Final = "anthropic.lit-5853-flagless-model"
region_alias: Final = f"bedrock/ap-northeast-1.{sibling_key}"
sibling_row: Final = {
"litellm_provider": "bedrock_converse",
"mode": "chat",
"input_cost_per_token": 3e-06,
"output_cost_per_token": 1.5e-05,
}
monkeypatch.setenv("LITELLM_LOCAL_MODEL_COST_MAP", "True")
monkeypatch.setattr(litellm, "model_cost", {**litellm.get_model_cost_map(url=""), sibling_key: sibling_row})
_invalidate_model_cost_lowercase_map()
previous_rules: Final = list(get_fallback_generalization_rules())
set_fallback_generalizations(
[
{
"name": "lit-5853-flagless",
"pattern": "lit-5853-flagless",
"model_info": {"supports_system_messages": True, "supports_parallel_function_calling": True},
}
]
)
try:
unregistered: Final = litellm.get_model_info(region_alias)
litellm.register_model(
{region_alias: {"litellm_provider": "bedrock", "mode": "chat"}}, persist_across_reloads=False
)
registered: Final = litellm.get_model_info(region_alias)
assert registered["supports_system_messages"] is unregistered["supports_system_messages"] is True
assert (
registered["supports_parallel_function_calling"]
is unregistered["supports_parallel_function_calling"]
is True
)
assert registered["input_cost_per_token"] == sibling_row["input_cost_per_token"]
finally:
set_fallback_generalizations(previous_rules)
_invalidate_model_cost_lowercase_map()
@pytest.mark.parametrize(
("builtin_prices", "custom_prices", "usage", "expected_cost"),
[
pytest.param(
{
"input_cost_per_token": 3e-06,
"output_cost_per_token": 1.5e-05,
"input_cost_per_token_above_200k_tokens": 6e-06,
"output_cost_per_token_above_200k_tokens": 2.25e-05,
},
{"input_cost_per_token": 1e-06, "output_cost_per_token": 2e-06},
{"prompt_tokens": 250_000, "completion_tokens": 1_000},
250_000 * 1e-06 + 1_000 * 2e-06,
id="above-200k-tier",
),
pytest.param(
{
"tiered_pricing": [
{"input_cost_per_token": 5e-08, "output_cost_per_token": 4e-07, "range": [0, 256_000]},
{"input_cost_per_token": 2.5e-07, "output_cost_per_token": 2e-06, "range": [256_000, 1_000_000]},
]
},
{"input_cost_per_token": 1e-06},
{"prompt_tokens": 300_000, "completion_tokens": 1_000},
300_000 * 1e-06 + 1_000 * 2e-06,
id="tiered-pricing-input-only",
),
pytest.param(
{"input_cost_per_token": 3e-06, "output_cost_per_token": 1.5e-05, "output_cost_per_reasoning_token": 3e-05},
{"output_cost_per_token": 2e-06},
{"prompt_tokens": 100, "completion_tokens": 1_000, "reasoning_tokens": 600},
100 * 3e-06 + 1_000 * 2e-06,
id="reasoning-rate-output-only",
),
pytest.param(
{
"input_cost_per_token": 3e-06,
"output_cost_per_token": 1.5e-05,
"off_peak_pricing": {
"input_cost_per_token": 1e-06,
"output_cost_per_token": 5e-06,
"hours_utc": "00:00-00:00",
},
},
{"output_cost_per_token": 2e-06},
{"prompt_tokens": 100, "completion_tokens": 1_000},
100 * 1e-06 + 1_000 * 2e-06,
id="off-peak-output-only",
),
],
)
def test_register_model_keeps_custom_flat_prices_over_inherited_rate_variants(
monkeypatch: pytest.MonkeyPatch,
builtin_prices: dict[str, object],
custom_prices: dict[str, float],
usage: dict[str, int],
expected_cost: float,
) -> None:
from litellm.types.utils import CompletionTokensDetailsWrapper, ModelResponse, Usage
builtin_key: Final = "anthropic.lit-5853-tiered-model-v1:0"
alias: Final = f"bedrock/ap-northeast-1.{builtin_key}"
monkeypatch.setenv("LITELLM_LOCAL_MODEL_COST_MAP", "True")
monkeypatch.setattr(
litellm,
"model_cost",
{
**litellm.get_model_cost_map(url=""),
builtin_key: {"litellm_provider": "bedrock", "mode": "chat", **builtin_prices},
},
)
_invalidate_model_cost_lowercase_map()
try:
litellm.register_model(
{alias: {"litellm_provider": "bedrock", **custom_prices}},
persist_across_reloads=False,
)
response_usage: Final = Usage(
prompt_tokens=usage["prompt_tokens"],
completion_tokens=usage["completion_tokens"],
total_tokens=usage["prompt_tokens"] + usage["completion_tokens"],
completion_tokens_details=CompletionTokensDetailsWrapper(reasoning_tokens=usage.get("reasoning_tokens", 0)),
)
cost: Final = litellm.completion_cost(
completion_response=ModelResponse(model=alias, usage=response_usage),
model=alias,
custom_llm_provider="bedrock",
)
assert cost == pytest.approx(expected_cost)
finally:
_invalidate_model_cost_lowercase_map()
def test_register_model_never_inherits_pricing_across_providers(monkeypatch: pytest.MonkeyPatch) -> None:
openai_key: Final = "lit-5853-openai-only-model"
monkeypatch.setenv("LITELLM_LOCAL_MODEL_COST_MAP", "True")
monkeypatch.setattr(
litellm,
"model_cost",
{
**litellm.get_model_cost_map(url=""),
openai_key: {
"litellm_provider": "openai",
"mode": "chat",
"input_cost_per_token": 2.5e-06,
"output_cost_per_token": 1e-05,
"cache_read_input_token_cost": 1.25e-06,
},
},
)
_invalidate_model_cost_lowercase_map()
registered_key: Final = f"bedrock/{openai_key}"
try:
litellm.register_model(
{registered_key: {"litellm_provider": "bedrock", "mode": "chat"}}, persist_across_reloads=False
)
registered: Final = litellm.model_cost[registered_key]
assert "cache_read_input_token_cost" not in registered
assert "input_cost_per_token" not in registered
assert "output_cost_per_token" not in registered
finally:
_invalidate_model_cost_lowercase_map()
def test_update_dictionary_merges_nested_dicts_without_aliasing():
"""A nested dict must be merged copy-on-write: the pre-existing nested dict
object stays untouched, and the caller's incoming nested dict is never

View file

@ -9,9 +9,14 @@ should still use the built-in pricing.
import asyncio
import copy
import itertools
import logging
import os
import queue
import re
import sys
import threading
from types import MappingProxyType
from typing import Final
from unittest.mock import Mock, patch
@ -3241,6 +3246,76 @@ def test_price_data_reload_refreshes_the_cached_model_group_and_deployment_info(
assert router.cached_deployment_model_info("dep-a", "openai/gpt-4o")["input_cost_per_token"] == new_price
def test_price_data_reload_keeps_geo_deployments_priced_while_requests_register_pricing(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setenv("LITELLM_LOCAL_MODEL_COST_MAP", "True")
catalog: Final = litellm.get_model_cost_map(url="")
monkeypatch.setattr(litellm, "model_cost", dict(catalog))
_invalidate_model_cost_lowercase_map()
backends: Final = tuple(
f"bedrock/au.anthropic.{model}"
for model in ("claude-sonnet-5", "claude-opus-5", "claude-haiku-4-5-20251001-v1:0", "claude-sonnet-4-6")
)
canonical_prices: Final = MappingProxyType(
{backend: catalog[backend.removeprefix("bedrock/")]["input_cost_per_token"] for backend in backends}
)
assert all(price > 0 for price in canonical_prices.values())
router: Final = Router(
model_list=[
{
"model_name": backend.rsplit(".", 1)[-1],
"litellm_params": {"model": backend, "aws_region_name": "ap-southeast-2"},
"model_info": {"id": f"lit-5853-dep-{index}"},
}
for index, backend in enumerate(backends)
]
)
stop: Final = threading.Event()
writer_outcome: Final[queue.SimpleQueue[int | Exception]] = queue.SimpleQueue()
def register_until_stopped() -> None:
try:
for i in itertools.count():
if stop.is_set():
writer_outcome.put(i)
return
litellm.register_model(
{
f"lit-5853-writer-{i}": {
"litellm_provider": "lit-5853-writer",
"mode": "chat",
"input_cost_per_token": 1e-07,
"output_cost_per_token": 4e-07,
"cache_read_input_token_cost": 1e-08,
}
},
persist_across_reloads=False,
)
except Exception as failure:
writer_outcome.put(failure)
writer: Final = threading.Thread(target=register_until_stopped, daemon=True)
saved_switch_interval: Final = sys.getswitchinterval()
sys.setswitchinterval(1e-4)
writer.start()
try:
for _ in range(10):
_simulate_price_data_reload_with_provider_sets(monkeypatch, dict(catalog))
finally:
stop.set()
writer.join()
sys.setswitchinterval(saved_switch_interval)
registrations_landed: Final = writer_outcome.get(timeout=5)
try:
assert isinstance(registrations_landed, int) and registrations_landed > 0, registrations_landed
assert tuple(backend for backend in backends if backend in litellm.model_cost) == ()
for backend in backends:
assert litellm.get_model_info(backend)["input_cost_per_token"] == canonical_prices[backend]
assert tuple(router.get_model_names()) == tuple(backend.rsplit(".", 1)[-1] for backend in backends)
finally:
_invalidate_model_cost_lowercase_map()
_COLLIDING_BUILTIN_KEY: Final = "baseten/zai-org/GLM-5.2"
_COLLIDING_SHARED_OPENAI_KEY: Final = "openai/zai-org/GLM-5.2"
_COLLIDING_MODEL_INFO: Final = {

View file

@ -14,7 +14,7 @@ from collections.abc import Callable, Iterator, Mapping
from concurrent.futures import Future, ThreadPoolExecutor
from datetime import datetime, timedelta, timezone
from pathlib import Path, PurePath
from typing import Final, cast
from typing import Final, SupportsIndex, cast
from unittest import mock
from unittest.mock import AsyncMock, MagicMock, Mock, patch
@ -4887,6 +4887,104 @@ def test_reapply_runtime_registrations_drops_request_scoped_registrations(monkey
_invalidate_model_cost_lowercase_map()
def test_model_cost_walks_survive_a_registration_landing_mid_walk(monkeypatch: pytest.MonkeyPatch) -> None:
from litellm.utils import _invalidate_model_cost_lowercase_map, _rebuild_model_cost_lowercase_map
class _KeyRegisteredMidWalk(str):
def lower(self) -> str:
litellm.model_cost.setdefault("lit-5853-registered-during-the-rebuild", {"mode": "chat"})
return super().lower()
class _EntryRegisteredMidWalk(dict):
def get(self, field: str, default: object = None) -> object:
litellm.model_cost.setdefault("lit-5853-registered-during-add-known-models", {"mode": "chat"})
return super().get(field, default)
monkeypatch.setattr(litellm, "model_cost", dict(litellm.model_cost))
litellm.model_cost[_KeyRegisteredMidWalk("LIT-5853-WALKED-LAST")] = _EntryRegisteredMidWalk(mode="chat")
try:
assert _rebuild_model_cost_lowercase_map()["lit-5853-walked-last"] == "LIT-5853-WALKED-LAST"
litellm.add_known_models(model_cost_map=litellm.model_cost)
assert "lit-5853-registered-during-the-rebuild" in litellm.model_cost
assert "lit-5853-registered-during-add-known-models" in litellm.model_cost
finally:
_invalidate_model_cost_lowercase_map()
class _KeyWhoseLoweringLosesTheRace(str):
"""Lowering the key is the one Python-level step between the lookup map's rebuild and
its read, so another thread's registration invalidating the map lands right there."""
def lower(self) -> str:
from litellm.utils import _invalidate_model_cost_lowercase_map
_invalidate_model_cost_lowercase_map()
return super().lower()
def test_case_insensitive_model_cost_lookup_survives_an_invalidation_mid_lookup() -> None:
from litellm.utils import _get_model_cost_key, _invalidate_model_cost_lowercase_map
_invalidate_model_cost_lowercase_map()
try:
assert _get_model_cost_key(_KeyWhoseLoweringLosesTheRace("GPT-4O")) == "gpt-4o"
finally:
_invalidate_model_cost_lowercase_map()
def _register_a_model_mid_walk() -> None:
litellm.model_cost.setdefault("lit-5853-registered-mid-walk", {"mode": "chat"})
class _KeyReadDuringARegistration(str):
def startswith(
self,
prefix: str | tuple[str, ...],
start: SupportsIndex | None = None,
end: SupportsIndex | None = None,
) -> bool:
_register_a_model_mid_walk()
return super().startswith(prefix, start, end)
def lower(self) -> str:
_register_a_model_mid_walk()
return super().lower()
class _EntryReadDuringARegistration(dict[str, object]):
def __contains__(self, key: object) -> bool:
_register_a_model_mid_walk()
return super().__contains__(key)
@pytest.mark.parametrize("walk", ["fal_ai", "fireworks_ai", "ollama", "gpt-5", "case-insensitive scan"])
def test_request_path_cost_map_walks_survive_a_registration_mid_walk(monkeypatch: pytest.MonkeyPatch, walk: str) -> None:
from litellm.llms.fal_ai.cost_calculator import _keyed_rows
from litellm.llms.fireworks_ai.chat.transformation import FireworksAIConfig
from litellm.llms.ollama.common_utils import OllamaModelInfo
from litellm.llms.openai.chat.gpt_5_transformation import _catalogue_declares_default_effort
from litellm.utils import _handle_new_key_with_scan, _invalidate_model_cost_lowercase_map
walks: Final[Mapping[str, Callable[[], object]]] = {
"fal_ai": lambda: _keyed_rows("lit-5853-model", "standard"),
"fireworks_ai": FireworksAIConfig._get_fireworks_index,
"ollama": lambda: OllamaModelInfo._is_static_ollama_model("ollama/lit-5853-model"),
"gpt-5": _catalogue_declares_default_effort,
"case-insensitive scan": lambda: _handle_new_key_with_scan("lit-5853-model"),
}
monkeypatch.setattr(
litellm,
"model_cost",
{_KeyReadDuringARegistration("lit-5853-catalog-model"): _EntryReadDuringARegistration(mode="chat")},
)
try:
walks[walk]()
assert "lit-5853-registered-mid-walk" in litellm.model_cost
finally:
_invalidate_model_cost_lowercase_map()
class _JsonCapture(logging.Handler):
def __init__(self):
super().__init__()
@ -7091,6 +7189,25 @@ def test_get_model_info_gemini(monkeypatch):
assert info.get("rpm") is not None, f"{model} does not have rpm"
def test_register_model_without_a_provider_never_inherits_another_providers_entry(monkeypatch: pytest.MonkeyPatch) -> None:
from litellm.utils import _invalidate_model_cost_lowercase_map
key: Final = "azure/claude-sonnet-4-6"
monkeypatch.setattr(litellm, "model_cost", dict(litellm.model_cost))
litellm.model_cost.pop(key, None)
try:
litellm.register_model(
{key: {"input_cost_per_token": 1e-07, "output_cost_per_token": 4e-07}}, persist_across_reloads=False
)
assert litellm.model_cost[key].get("litellm_provider") is None
info: Final = litellm.get_model_info(key)
assert info["litellm_provider"] == "azure"
assert info["input_cost_per_token"] == 1e-07
assert info["output_cost_per_token"] == 4e-07
finally:
_invalidate_model_cost_lowercase_map()
@pytest.mark.parametrize(
"model, litellm_params, expects_native_config",
[