mirror of
https://github.com/BerriAI/litellm.git
synced 2026-11-06 07:48:06 +00:00
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:
parent
5242a8e2f3
commit
6b271d2290
7 changed files with 66 additions and 145 deletions
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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."""
|
||||
|
|
|
|||
|
|
@ -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$")
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue