fix(cost): price a bare response model under the requested provider and drop the unlisted Fireworks bucket change

The logging payload picks the pricing model without a provider, so a bare echo
like mistral-large-latest failed its price check and the requested alias took
over model_map_key. The served name is now priced under the requested model's
provider, and an echo that is the requested name minus its provider prefix
skips the checks.

Treating the Router's empty price row as missing let unlisted Fireworks
deployments bill from a name guess, which misprices dedicated deployment ids
and routes some short names to another provider's rates. That change and its
tests go back to the merge base, so unlisted Fireworks models keep billing $0
behind the proxy as on main.
This commit is contained in:
mateo-berri 2026-10-28 21:45:40 -07:00
parent 5242a8e2f3
commit 6b271d2290
7 changed files with 66 additions and 145 deletions

View file

@ -39,8 +39,6 @@ from litellm.litellm_core_utils.llm_cost_calc.utils import (
CostCalculatorUtils,
calculate_cache_writing_cost,
calculate_cost_component,
cost_map_entry_prices_anything,
cost_map_key_prices_anything,
generic_cost_per_character,
generic_cost_per_token,
get_batch_cost_rates,
@ -50,7 +48,6 @@ from litellm.litellm_core_utils.llm_cost_calc.utils import (
get_token_type_cost_breakdown,
get_vertex_regional_endpoint_uplift,
parse_prompt_tokens_details,
raw_cost_map_entry,
select_cost_metric_for_model,
)
from litellm.llms.anthropic.cost_calculation import (
@ -78,9 +75,6 @@ from litellm.llms.deepseek.cost_calculator import (
from litellm.llms.fireworks_ai.cost_calculator import (
cost_per_token as fireworks_ai_cost_per_token,
)
from litellm.llms.fireworks_ai.cost_calculator import (
has_size_bucket as fireworks_ai_has_size_bucket,
)
from litellm.llms.gemini.cost_calculator import cost_per_token as gemini_cost_per_token
from litellm.llms.lemonade.cost_calculator import (
cost_per_token as lemonade_cost_per_token,
@ -864,8 +858,27 @@ def _get_hidden_str_for_cost_calc(hidden_params: object, key: str) -> str | None
return value if isinstance(value, str) and value else None
_NON_TOKEN_RATE_FIELDS: Final = frozenset(
{
"cost_per_second",
"input_cost_per_second",
"output_cost_per_second",
"input_cost_per_query",
"input_cost_per_text_message",
"tiered_pricing",
}
)
def _cost_map_entry_prices_anything(entry: Mapping[str, object]) -> bool:
return any(
value is not None and (field in _NON_TOKEN_RATE_FIELDS or ("cost_per" in field and "token" in field))
for field, value in entry.items()
)
def _bedrock_registration_has_prices(entry: Mapping[str, object]) -> bool:
return cost_map_entry_prices_anything(entry) or any(
return _cost_map_entry_prices_anything(entry) or any(
value is not None and field.startswith("cache_") and is_server_derived_pricing_key(field)
for field, value in entry.items()
)
@ -895,6 +908,11 @@ def _bedrock_pricing_info(
return model_info_with_pricing_overrides(profile, registration[1])
def cost_map_key_prices_anything(key: str) -> bool:
entry: Final = _raw_cost_map_entry(key)
return entry is not None and _cost_map_entry_prices_anything(entry)
def _declares_rates(model_info: ModelInfo | None) -> bool:
return model_info is not None and cost_map_key_prices_anything(model_info["key"])
@ -972,7 +990,7 @@ def select_model_name_for_cost_calc(
if custom_pricing is True:
if router_model_id is not None and router_model_id in litellm.model_cost:
entry: Final = litellm.model_cost[router_model_id]
if cost_map_entry_prices_anything(entry):
if _cost_map_entry_prices_anything(entry):
return_model = router_model_id
else:
return_model = model
@ -1020,15 +1038,11 @@ def _row_declares_rates(model: str, custom_llm_provider: str | None) -> bool:
def _rule_declares_rates(model: str, custom_llm_provider: str | None) -> bool:
rule_info: Final = unmapped_model_generalization(model, custom_llm_provider)
return rule_info is not None and cost_map_entry_prices_anything(rule_info)
return rule_info is not None and _cost_map_entry_prices_anything(rule_info)
def _resolves_to_rates(model: str, custom_llm_provider: str | None) -> bool:
return (
_row_declares_rates(model, custom_llm_provider)
or _rule_declares_rates(model, custom_llm_provider)
or (custom_llm_provider == "fireworks_ai" and fireworks_ai_has_size_bucket(model))
)
return _row_declares_rates(model, custom_llm_provider) or _rule_declares_rates(model, custom_llm_provider)
def _served_or_priced_requested_model(
@ -1038,11 +1052,12 @@ def _served_or_priced_requested_model(
if requested_model is None:
return served
requested: Final = _cost_map_model_name(requested_model, custom_llm_provider, None)
if requested == served or _row_declares_rates(served, custom_llm_provider):
if served in (requested, requested.partition("/")[2]):
return served
if not _resolves_to_rates(requested, _get_provider_for_cost_calc(requested, custom_llm_provider)):
pricing_provider: Final = _get_provider_for_cost_calc(requested, custom_llm_provider)
if not _resolves_to_rates(requested, pricing_provider):
return served
return served if _rule_declares_rates(served, custom_llm_provider) else requested
return served if _resolves_to_rates(served, pricing_provider) else requested
def _strip_unregistered_leading_segments(model: str, region_name: str | None) -> str:
@ -2373,7 +2388,7 @@ def _deployment_model_info(
) -> ModelInfo | None:
if not custom_pricing:
return None
registered_deployment_info: Final = raw_cost_map_entry(router_model_id) if router_model_id is not None else None
registered_deployment_info: Final = _raw_cost_map_entry(router_model_id) if router_model_id is not None else None
if registered_deployment_info is not None:
return cast(ModelInfo, registered_deployment_info) # cast-ok: router registers deployment prices under its id
if litellm_logging_obj is None:
@ -2435,7 +2450,7 @@ def _is_unpriced_generalized_candidate(candidate: _CostCandidate) -> bool:
if model is None:
return False
rule_info: Final = unmapped_model_generalization(model, pricing_provider)
return rule_info is not None and not cost_map_entry_prices_anything(rule_info)
return rule_info is not None and not _cost_map_entry_prices_anything(rule_info)
def _priced_candidates_before_unpriced_rules(candidates: tuple[_CostCandidate, ...]) -> tuple[_CostCandidate, ...]:
@ -2446,6 +2461,11 @@ def _priced_candidates_before_unpriced_rules(candidates: tuple[_CostCandidate, .
)
def _raw_cost_map_entry(key: str) -> Mapping[str, object] | None:
raw_entry: Final = litellm.model_cost.get(key)
return raw_entry if isinstance(raw_entry, Mapping) else None
def pricing_entry_for_cost_calc(
model: str | None,
completion_response: object | None,
@ -2485,7 +2505,7 @@ def pricing_entry_for_cost_calc(
)
if resolved is None:
return None
return resolved["key"], raw_cost_map_entry(resolved["key"]) or resolved
return resolved["key"], _raw_cost_map_entry(resolved["key"]) or resolved
def ocr_cost(
@ -3477,7 +3497,7 @@ def _cost_map_entry_declares_pricing(model_name: str, custom_llm_provider: str)
litellm.model_cost.get(model_name),
litellm.model_cost.get(f"{custom_llm_provider}/{model_name}"),
)
return any(entry is not None and cost_map_entry_prices_anything(entry) for entry in entries)
return any(entry is not None and _cost_map_entry_prices_anything(entry) for entry in entries)
def _first_priced_realtime_token_costs(

View file

@ -11,10 +11,10 @@ from pydantic_core import to_jsonable_python
from typing_extensions import assert_never
import litellm
from litellm.cost_calculator import cost_map_key_prices_anything
from litellm.litellm_core_utils.core_helpers import RESPONSE_COST_HEADER, normalize_drop_params
from litellm.litellm_core_utils.get_provider_specific_headers import ProviderSpecificHeaderUtils
from litellm.litellm_core_utils.litellm_logging import Logging as LiteLLMLoggingObj
from litellm.litellm_core_utils.llm_cost_calc.utils import cost_map_key_prices_anything
from litellm.litellm_core_utils.prompt_templates.image_handling import (
async_convert_urls_to_base64,
convert_url_to_base64,

View file

@ -52,35 +52,6 @@ _VALID_DATA_RESIDENCIES: Final = frozenset(r.value for r in DataResidency)
_DEPLOYMENT_PRICING_KEYS: Final[frozenset[str]] = frozenset(CustomPricingLiteLLMParams.model_fields)
_NON_TOKEN_RATE_FIELDS: Final = frozenset(
{
"cost_per_second",
"input_cost_per_second",
"output_cost_per_second",
"input_cost_per_query",
"input_cost_per_text_message",
"tiered_pricing",
}
)
def cost_map_entry_prices_anything(entry: Mapping[str, object]) -> bool:
return any(
value is not None and (field in _NON_TOKEN_RATE_FIELDS or ("cost_per" in field and "token" in field))
for field, value in entry.items()
)
def raw_cost_map_entry(key: str) -> Mapping[str, object] | None:
raw_entry: Final = litellm.model_cost.get(key)
return raw_entry if isinstance(raw_entry, Mapping) else None
def cost_map_key_prices_anything(key: str) -> bool:
entry: Final = raw_cost_map_entry(key)
return entry is not None and cost_map_entry_prices_anything(entry)
_BASE_CACHE_WRITE_RATE_KEY: Final = "cache_creation_input_token_cost"
_CACHE_WRITE_RATE_KEYS: Final[Mapping[str, str]] = MappingProxyType(

View file

@ -11,13 +11,9 @@ from litellm.constants import (
FIREWORKS_AI_56_B_MOE,
FIREWORKS_AI_176_B_MOE,
)
from litellm.litellm_core_utils.llm_cost_calc.utils import (
cost_map_entry_prices_anything,
cost_map_key_prices_anything,
generic_cost_per_token,
)
from litellm.litellm_core_utils.llm_cost_calc.utils import generic_cost_per_token
from litellm.types.utils import ModelInfo, Usage
from litellm.utils import get_model_info, unmapped_model_generalization
from litellm.utils import get_model_info
# Extract the number of billion parameters from the model name
@ -60,22 +56,11 @@ def get_base_model_for_pricing(model_name: str) -> str:
return "fireworks-ai-default"
def has_size_bucket(model_name: str) -> bool:
return get_base_model_for_pricing(model_name) != "fireworks-ai-default"
def _resolve_model_info(model: str) -> ModelInfo:
try:
model_info: Final = get_model_info(model=model, custom_llm_provider="fireworks_ai")
return get_model_info(model=model, custom_llm_provider="fireworks_ai")
except Exception:
return _resolve_routed_model_info(model)
declares_rates: Final = cost_map_key_prices_anything(model_info["key"]) or _rule_declares_rates(model)
return model_info if declares_rates else _resolve_routed_model_info(model)
def _rule_declares_rates(model: str) -> bool:
rule_info: Final = unmapped_model_generalization(model, "fireworks_ai")
return rule_info is not None and cost_map_entry_prices_anything(rule_info)
def _resolve_routed_model_info(model: str) -> ModelInfo:

View file

@ -12127,6 +12127,26 @@ def test_get_model_cost_information_maps_requested_model_when_response_names_an_
assert result["model_map_value"]["input_cost_per_token"] == litellm.model_cost[requested_model]["input_cost_per_token"]
@pytest.mark.usefixtures("_vcr_outcome_gate", "drain_logging_worker", "isolate_litellm_state", "setup_and_teardown")
def test_get_model_cost_information_keeps_a_priced_response_model_that_echoes_without_its_provider(
monkeypatch: pytest.MonkeyPatch,
) -> None:
served_row: Final = {"litellm_provider": "mistral", "mode": "chat", "input_cost_per_token": 2e-6, "output_cost_per_token": 5e-6}
requested_row: Final = {"litellm_provider": "mistral", "mode": "chat", "input_cost_per_token": 9e-6, "output_cost_per_token": 9e-6}
monkeypatch.setitem(litellm.model_cost, "mistral/sol-snapshot-test", served_row)
monkeypatch.setitem(litellm.model_cost, "mistral/sol-alias-test", requested_row)
response: Final = ModelResponse(model="sol-snapshot-test", choices=[])
response._hidden_params["litellm_model_name"] = "mistral/sol-alias-test"
result: Final = StandardLoggingPayloadSetup.get_model_cost_information(
base_model=None, custom_pricing=False, custom_llm_provider="mistral", init_response_obj=response
)
assert result["model_map_key"] == "sol-snapshot-test"
assert result["model_map_value"] is not None
assert result["model_map_value"]["input_cost_per_token"] == served_row["input_cost_per_token"]
@pytest.mark.usefixtures("_vcr_outcome_gate", "drain_logging_worker", "isolate_litellm_state", "setup_and_teardown")
def test_standard_logging_payload_uses_deployment_when_no_base_model():
"""metadata["deployment"] is used for cost-map lookup when base_model is not set."""

View file

@ -7,10 +7,6 @@ from typing import Final
import pytest
import litellm
from litellm.litellm_core_utils.fallback_generalizations import (
get_fallback_generalization_rules,
set_fallback_generalizations,
)
from litellm.litellm_core_utils.llm_cost_calc.utils import (
calculate_prompt_caching_savings,
generic_cost_per_token,
@ -369,36 +365,6 @@ def test_an_unknown_fireworks_model_still_falls_back_to_the_parameter_size_bucke
assert prompt_cost > 0
@pytest.fixture
def priced_rule_for_prefixed_fireworks_names() -> Generator[None, None, None]:
previous_rules: Final = list(get_fallback_generalization_rules())
try:
set_fallback_generalizations(
[
{
"name": "fireworks-sol-ruled-priced",
"pattern": r"fireworks_ai/accounts/fireworks/models/sol-ruled-[a-z]+",
"model_info": {"mode": "chat", "input_cost_per_token": 3.21e-05, "output_cost_per_token": 7.65e-05},
}
]
)
yield
finally:
set_fallback_generalizations(previous_rules)
def test_a_model_priced_only_by_a_fallback_rule_bills_at_the_rule_rates(
priced_rule_for_prefixed_fireworks_names: None,
):
prompt_cost, completion_cost = cost_per_token(
model="accounts/fireworks/models/sol-ruled-max",
usage=_usage(prompt_tokens=100, cached_tokens=0, completion_tokens=10),
)
assert prompt_cost == pytest.approx(100 * 3.21e-05)
assert completion_cost == pytest.approx(10 * 7.65e-05)
_TIERED_INPUT_PATTERN: Final = re.compile(r"^input_cost_per_token_above_(\d+)k_tokens$")

View file

@ -13,7 +13,7 @@ from openai.types.completion_usage import CompletionUsage
from pydantic import BaseModel
import litellm
from litellm import Router, TranscriptionResponse, model_cost
from litellm import TranscriptionResponse, model_cost
from litellm.constants import REALTIME_METERED_USAGE_KEY
from litellm.cost_calculator import BaseTokenUsageProcessor, RealtimeAPITokenUsageProcessor, ResponsesWebSocketTokenUsageProcessor, batch_cost_calculator, completion_cost, cost_per_token, handle_realtime_stream_cost_calculation, response_cost_calculator
from litellm.litellm_core_utils.llm_cost_calc.utils import parse_prompt_tokens_details
@ -544,47 +544,6 @@ def test_completion_cost_keeps_response_pricing_when_requested_alias_has_no_rate
assert cost == pytest.approx(_token_rates_cost(litellm.model_cost[size_bucket], usage))
@pytest.mark.parametrize("model_passed", [True, False])
def test_completion_cost_bills_requested_size_bucket_when_response_names_an_unmapped_display_name(
_local_model_cost_map: None, monkeypatch: pytest.MonkeyPatch, model_passed: bool
) -> None:
requested_model: Final = "fireworks_ai/accounts/fireworks/models/sol-display-70b-test"
size_bucket: Final = get_base_model_for_pricing(requested_model)
monkeypatch.setitem(
litellm.model_cost,
size_bucket,
{"litellm_provider": "fireworks_ai", "mode": "chat", "input_cost_per_token": 3e-6, "output_cost_per_token": 7e-6},
)
usage: Final = Usage(prompt_tokens=1000, completion_tokens=500, total_tokens=1500)
response: Final = ModelResponse(model="Sol Display Max", choices=[], usage=usage)
response._hidden_params["litellm_model_name"] = requested_model
cost: Final = (
completion_cost(completion_response=response, model=requested_model, custom_llm_provider="fireworks_ai")
if model_passed
else completion_cost(completion_response=response)
)
assert cost == pytest.approx(_token_rates_cost(litellm.model_cost[size_bucket], usage))
@pytest.mark.parametrize("served_model", ["Sol Display Max", "accounts/fireworks/models/sol-unmapped-70b-test"])
def test_completion_cost_bills_size_bucket_for_a_router_deployment_of_an_unmapped_fireworks_model(
_local_model_cost_map: None, served_model: str
) -> None:
requested_model: Final = "fireworks_ai/accounts/fireworks/models/sol-unmapped-70b-test"
Router(model_list=[{"model_name": "fw-sol", "litellm_params": {"model": requested_model, "api_key": "sk-test"}}])
usage: Final = Usage(prompt_tokens=1000, completion_tokens=500, total_tokens=1500)
response: Final = ModelResponse(model=served_model, choices=[], usage=usage)
response._hidden_params["litellm_model_name"] = requested_model
cost: Final = completion_cost(completion_response=response)
size_bucket_row: Final = litellm.model_cost[get_base_model_for_pricing(requested_model)]
assert cost == pytest.approx(_token_rates_cost(size_bucket_row, usage))
assert cost > 0
def test_completion_cost_bills_requested_model_priced_only_by_a_rule_when_response_row_has_no_rates(
_local_model_cost_map: None, _priced_claude_family_generalization: None, monkeypatch: pytest.MonkeyPatch
) -> None: