diff --git a/litellm/litellm_core_utils/llm_cost_calc/utils.py b/litellm/litellm_core_utils/llm_cost_calc/utils.py index 1b836e935d7..31da260588c 100644 --- a/litellm/litellm_core_utils/llm_cost_calc/utils.py +++ b/litellm/litellm_core_utils/llm_cost_calc/utils.py @@ -356,7 +356,7 @@ def get_batch_cost_rates(model_info: ModelInfo, usage: Usage, custom_llm_provide ) -def _select_priced_tier(model_info: ModelInfo, usage: Usage) -> dict | None: +def _select_priced_tier(model_info: ModelInfo, usage: Usage) -> dict[str, object] | None: tiered_pricing: Final = model_info.get("tiered_pricing") if not isinstance(tiered_pricing, list) or not tiered_pricing: return None @@ -545,6 +545,71 @@ class TokenRates: return self.output_rate if self.reasoning_rate is None else self.reasoning_rate +def resolve_explicit_cache_read_cost_rate( + model_info: ModelInfo, + usage: Usage, + current_time: datetime | None, + service_tier: str | None = None, +) -> float | None: + cache_read_cost_key: Final = "explicit_cache_read_input_token_cost" + service_cache_read_cost_key: Final = _get_service_tier_cost_key(cache_read_cost_key, service_tier) + if ( + model_info.get(service_cache_read_cost_key) is None + and model_info.get(cache_read_cost_key) is None + and model_info.get("tiered_pricing") is None + and model_info.get("off_peak_pricing") is None + ): + return None + + prompt_tokens_details: Final = usage.prompt_tokens_details + cache_type: Final = ( + getattr(prompt_tokens_details, "cache_type", None) if prompt_tokens_details is not None else None + ) + if parse_prompt_tokens_details(usage)["cache_hit_tokens"] <= 0 or cache_type != "ephemeral": + return None + + tier: Final = _select_priced_tier(model_info=model_info, usage=usage) + off_peak: Final = _open_off_peak_block(model_info, current_time) + model_cache_read_cost_rate: Final = _get_cost_per_unit( + model_info, + service_cache_read_cost_key, + default_value=None, + ) + tier_has_explicit_rate: Final = tier is not None and cache_read_cost_key in tier + off_peak_has_explicit_rate: Final = ( + off_peak is not None and _parse_off_peak_rate(off_peak.get(cache_read_cost_key)) is not None + ) + if model_cache_read_cost_rate is None and not tier_has_explicit_rate and not off_peak_has_explicit_rate: + return None + + cache_read_cost_rate: Final = ( + tier_rate(tier, cache_read_cost_key) + if tier is not None and tier_has_explicit_rate + else tier_rate(tier, "cache_read_input_token_cost", "input_cost_per_token") + if tier is not None + else model_cache_read_cost_rate + if model_cache_read_cost_rate is not None + else _get_cost_per_unit( + model_info, + _get_service_tier_cost_key("cache_read_input_token_cost", service_tier), + default_value=_get_cost_per_unit(model_info, "input_cost_per_token") or 0.0, + ) + or 0.0 + ) + + if off_peak is None: + return cache_read_cost_rate + mode_specific_off_peak_rate: Final = _parse_off_peak_rate(off_peak.get(cache_read_cost_key)) + generic_off_peak_rate: Final = _parse_off_peak_rate(off_peak.get("cache_read_input_token_cost")) + return ( + mode_specific_off_peak_rate + if mode_specific_off_peak_rate is not None + else generic_off_peak_rate + if generic_off_peak_rate is not None + else cache_read_cost_rate + ) + + def _parse_off_peak_rate(value: object) -> float | None: if isinstance(value, bool): return None @@ -1458,12 +1523,19 @@ def generic_cost_per_token( current_time=billing_time, threshold_is_inclusive=_uses_inclusive_token_thresholds(custom_llm_provider), ) + resolved_cache_read_cost: Final = resolve_explicit_cache_read_cost_rate( + model_info=resolved_model_info, + usage=usage, + current_time=billing_time, + service_tier=service_tier, + ) + effective_cache_read_cost: Final = cache_read_cost if resolved_cache_read_cost is None else resolved_cache_read_cost prompt_cost = _calculate_input_cost( prompt_tokens_details=prompt_tokens_details, model_info=resolved_model_info, prompt_base_cost=prompt_base_cost, - cache_read_cost=cache_read_cost, + cache_read_cost=effective_cache_read_cost, cache_creation_cost=cache_creation_cost, cache_creation_cost_above_1hr=cache_creation_cost_above_1hr, service_tier=service_tier, @@ -1658,6 +1730,16 @@ def _cost_map_billed_rates( current_time=billing_time, threshold_is_inclusive=_uses_inclusive_token_thresholds(custom_llm_provider), ) + provider_cache_read_cost_rate: Final = resolve_explicit_cache_read_cost_rate( + model_info=model_info, + usage=usage, + current_time=billing_time, + service_tier=service_tier, + ) + effective_cache_read_cost_rate: Final = ( + cache_read_cost_rate if provider_cache_read_cost_rate is None else provider_cache_read_cost_rate + ) + reasoning_rate: Final = _resolve_billed_reasoning_rate( model_info=model_info, usage=usage, @@ -1679,9 +1761,9 @@ def _cost_map_billed_rates( return BilledTokenRates( input_cost_per_token=prompt_base_cost, output_cost_per_token=completion_base_cost, - cache_read_input_token_cost=cache_read_cost_rate, + cache_read_input_token_cost=effective_cache_read_cost_rate, cache_read_input_audio_token_cost=( - audio_cache_read_rate if audio_cache_read_rate is not None else cache_read_cost_rate + audio_cache_read_rate if audio_cache_read_rate is not None else effective_cache_read_cost_rate ), cache_creation_input_token_cost=cache_creation_cost_rate, cache_creation_input_token_cost_above_1hr=cache_creation_cost_above_1hr_rate, @@ -1806,6 +1888,13 @@ def calculate_prompt_caching_savings( current_time=billed_at, threshold_is_inclusive=_uses_inclusive_token_thresholds(custom_llm_provider), ) + provider_cache_read_cost: Final = resolve_explicit_cache_read_cost_rate( + model_info=model_info, + usage=usage, + current_time=billed_at, + service_tier=service_tier, + ) + effective_cache_read_cost: Final = cache_read_cost if provider_cache_read_cost is None else provider_cache_read_cost write_rate: Final = cache_creation_cost or prompt_base_cost write_rate_1h: Final = cache_creation_cost_above_1hr or write_rate prompt_tokens_details: Final = parse_prompt_tokens_details(usage) @@ -1820,7 +1909,7 @@ def calculate_prompt_caching_savings( if details is not None else None ) - read_discount: Final = cache_read_tokens * max(prompt_base_cost - cache_read_cost, 0.0) + read_discount: Final = cache_read_tokens * max(prompt_base_cost - effective_cache_read_cost, 0.0) write_premium: Final = calculate_cache_writing_cost( cache_creation_tokens=cache_creation_tokens, cache_creation_token_details=cache_creation_details, diff --git a/litellm/llms/dashscope/cost_calculator.py b/litellm/llms/dashscope/cost_calculator.py index 17f70ec5db7..98504f54533 100644 --- a/litellm/llms/dashscope/cost_calculator.py +++ b/litellm/llms/dashscope/cost_calculator.py @@ -7,7 +7,7 @@ cached, cache-creation, output, reasoning) is billed at that one tier's rate. See https://help.aliyun.com/zh/model-studio/billing-for-model-studio """ -from dataclasses import dataclass +from dataclasses import dataclass, replace from datetime import datetime from typing import Final @@ -17,6 +17,7 @@ from litellm.litellm_core_utils.llm_cost_calc.utils import ( apply_off_peak_pricing, parse_completion_tokens_details, parse_prompt_tokens_details, + resolve_explicit_cache_read_cost_rate, ) from litellm.types.utils import ModelInfo, Usage from litellm.utils import get_model_info @@ -137,5 +138,13 @@ def cost_per_token( ) standard_rates: Final = _flat_rates(model_info) if tier is None else _tier_rates(model_info, tier) rates: Final = apply_off_peak_pricing(model_info, current_time, standard_rates) + explicit_cache_read_rate: Final = resolve_explicit_cache_read_cost_rate( + model_info=model_info, + usage=usage, + current_time=current_time, + ) + billed_rates: Final = ( + rates if explicit_cache_read_rate is None else replace(rates, cache_read_rate=explicit_cache_read_rate) + ) - return _bill(breakdown, rates) + return _bill(breakdown, billed_rates) diff --git a/litellm/model_prices_and_context_window_backup.json b/litellm/model_prices_and_context_window_backup.json index 7f8a2c2ae93..30229f2a73f 100644 --- a/litellm/model_prices_and_context_window_backup.json +++ b/litellm/model_prices_and_context_window_backup.json @@ -18018,7 +18018,9 @@ ] }, "dashscope/qwen3.7-max": { + "cache_creation_input_token_cost": 3.125e-06, "cache_read_input_token_cost": 5e-07, + "explicit_cache_read_input_token_cost": 2.5e-07, "input_cost_per_token": 2.5e-06, "litellm_provider": "dashscope", "max_input_tokens": 991808, @@ -19285,7 +19287,9 @@ ] }, "qwencloud/qwen3.7-max": { + "cache_creation_input_token_cost": 3.125e-06, "cache_read_input_token_cost": 5e-07, + "explicit_cache_read_input_token_cost": 2.5e-07, "input_cost_per_token": 2.5e-06, "litellm_provider": "qwencloud", "max_input_tokens": 991808, @@ -20512,7 +20516,9 @@ ] }, "qwen_ai_platform/qwen3.7-max": { + "cache_creation_input_token_cost": 3.125e-06, "cache_read_input_token_cost": 5e-07, + "explicit_cache_read_input_token_cost": 2.5e-07, "input_cost_per_token": 2.5e-06, "litellm_provider": "qwen_ai_platform", "max_input_tokens": 991808, diff --git a/litellm/responses/litellm_completion_transformation/transformation.py b/litellm/responses/litellm_completion_transformation/transformation.py index a2acbc09784..b613b817225 100644 --- a/litellm/responses/litellm_completion_transformation/transformation.py +++ b/litellm/responses/litellm_completion_transformation/transformation.py @@ -3406,6 +3406,10 @@ class LiteLLMCompletionResponsesConfig: if cache_write_tokens is not None else MappingProxyType({}) ) + cache_type: Final = getattr(prompt_details, "cache_type", None) + cache_type_extra: Final[Mapping[str, str]] = ( + MappingProxyType({"cache_type": cache_type}) if cache_type is not None else MappingProxyType({}) + ) # The cost path reads the grounding counters off the input details, and a realtime # session's usage is rebuilt from its own response.done, so dropping them here bills # no per-query grounding fee at all. @@ -3432,6 +3436,7 @@ class LiteLLMCompletionResponsesConfig: cached_tokens_details if isinstance(cached_tokens_details, CachedTokensDetails) else None ), **cache_write_extra, + **cache_type_extra, **grounding_request_counts, ) diff --git a/litellm/responses/utils.py b/litellm/responses/utils.py index be8397c9270..503d85aaed9 100644 --- a/litellm/responses/utils.py +++ b/litellm/responses/utils.py @@ -1259,6 +1259,7 @@ class ResponseAPILoggingUtils: ), video_tokens=getattr(response_api_usage.input_tokens_details, "video_tokens", None), cache_write_tokens=getattr(response_api_usage.input_tokens_details, "cache_write_tokens", None), + cache_type=getattr(response_api_usage.input_tokens_details, "cache_type", None), web_search_requests=getattr(response_api_usage.input_tokens_details, "web_search_requests", None), google_maps_grounding_requests=getattr( response_api_usage.input_tokens_details, "google_maps_grounding_requests", None diff --git a/litellm/router.py b/litellm/router.py index 142b2019e6e..94755d0d5ba 100644 --- a/litellm/router.py +++ b/litellm/router.py @@ -9815,7 +9815,8 @@ class Router: does not silently bill cache_read/cache_creation at 0. User-specified cache fields always win; only ``None``/missing entries - are inherited. No-op when the backend model has no canonical entry. + are inherited, and a user-set cache_read price also covers explicit + cache hits. No-op when the backend model has no canonical entry. """ cache_fields: Final = ( "cache_creation_input_token_cost", @@ -9824,17 +9825,22 @@ class Router: "cache_read_input_token_cost", "cache_read_input_token_cost_above_200k_tokens", ) - if all(model_info.get(f) is not None for f in cache_fields): + inherited_fields: Final = ( + cache_fields + if model_info.get("cache_read_input_token_cost") is not None + else (*cache_fields, "explicit_cache_read_input_token_cost") + ) + missing_fields: Final = tuple(f for f in inherited_fields if model_info.get(f) is None) + if not missing_fields: return try: backend_info: Final = litellm.get_model_info(model=backend_model, custom_llm_provider=custom_llm_provider) except Exception: return - for field in cache_fields: - if model_info.get(field) is None: - backend_value = backend_info.get(field) - if backend_value is not None: - model_info[field] = backend_value + for field in missing_fields: + backend_value = backend_info.get(field) + if backend_value is not None: + model_info[field] = backend_value @staticmethod def _cost_map_backend_model(deployment: Deployment) -> str: diff --git a/litellm/types/utils.py b/litellm/types/utils.py index 99788dad4e1..576c4f093d3 100644 --- a/litellm/types/utils.py +++ b/litellm/types/utils.py @@ -291,6 +291,7 @@ class OffPeakPricing(TypedDict, total=False): output_cost_per_token: ReadOnly[float] output_cost_per_reasoning_token: ReadOnly[float] cache_read_input_token_cost: ReadOnly[float] + explicit_cache_read_input_token_cost: ReadOnly[float] cache_creation_input_token_cost: ReadOnly[float] @@ -320,6 +321,7 @@ class ModelInfoBase(ProviderSpecificModelInfo, total=False): cache_read_input_token_cost: float | None cache_read_input_audio_token_cost: ReadOnly[float | None] cache_read_input_image_token_cost: ReadOnly[float | None] + explicit_cache_read_input_token_cost: ReadOnly[float | None] cache_read_input_token_cost_flex: float | None # OpenAI flex service tier pricing cache_read_input_token_cost_priority: float | None # OpenAI priority service tier pricing cache_read_input_token_cost_balanced: ReadOnly[float | None] @@ -1840,6 +1842,13 @@ class PromptTokensDetailsWrapper( cached_tokens_details: CachedTokensDetails | None = None """Details of cached (cache-hit) tokens sent to the model. OpenAI realtime naming; carries the per-modality cache-read split.""" + cache_type: str | None = None + """Provider-reported cache mode. DashScope returns ``ephemeral`` for explicit Qwen Context Cache requests.""" + + @field_validator("cache_type", mode="before") + @classmethod + def _drop_non_string_cache_type(cls, value: object) -> str | None: + return value if isinstance(value, str) else None def __setattr__(self, name: str, value: object) -> None: super().__setattr__(name, value) @@ -1889,6 +1898,8 @@ class PromptTokensDetailsWrapper( del self.cache_creation_token_details if self.cached_tokens_details is None: del self.cached_tokens_details + if self.cache_type is None: + del self.cache_type class ServerToolUse(LiteLLMBaseModel): @@ -3921,6 +3932,7 @@ class MirroredPricingParams(LiteLLMBaseModel): input_cost_per_character: float | None = None output_cost_per_character: float | None = None cache_read_input_token_cost: float | None = None + explicit_cache_read_input_token_cost: float | None = None cache_creation_input_token_cost: float | None = None cache_creation_input_token_cost_above_1hr: float | None = None tiered_pricing: list[dict[str, Any]] | None = None diff --git a/litellm/utils.py b/litellm/utils.py index 3990a870484..fc47a4dba2e 100644 --- a/litellm/utils.py +++ b/litellm/utils.py @@ -3670,6 +3670,15 @@ def _registration_base( return _RegistrationWrite(key=key, entry=dict(fallback.entry)) +def _base_fields_replaced_by(value: Mapping[str, object]) -> frozenset[str]: + return ( + frozenset({"explicit_cache_read_input_token_cost"}) + if value.get("cache_read_input_token_cost") is not None + and value.get("explicit_cache_read_input_token_cost") is None + else frozenset() + ) + + def _registration_write( key: str, value: Mapping[str, object], custom_llm_provider: str | None, warning_display_name: str | None ) -> _RegistrationWrite: @@ -3684,7 +3693,10 @@ def _registration_write( 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)) + return _RegistrationWrite( + key=base.key, + entry={**update_dictionary(existing_model, value), **dict.fromkeys(_base_fields_replaced_by(value))}, + ) def _registration_attempt( @@ -6614,6 +6626,7 @@ def get_model_info_helper( ), cache_read_input_token_cost=_model_info.get("cache_read_input_token_cost", None), cache_read_input_audio_token_cost=_model_info.get("cache_read_input_audio_token_cost", None), + explicit_cache_read_input_token_cost=_model_info.get("explicit_cache_read_input_token_cost", None), prompt_cache_min_tokens=_model_info.get("prompt_cache_min_tokens", None), cache_read_input_token_cost_above_200k_tokens=_model_info.get( "cache_read_input_token_cost_above_200k_tokens", None diff --git a/model_prices_and_context_window.json b/model_prices_and_context_window.json index 7f8a2c2ae93..30229f2a73f 100644 --- a/model_prices_and_context_window.json +++ b/model_prices_and_context_window.json @@ -18018,7 +18018,9 @@ ] }, "dashscope/qwen3.7-max": { + "cache_creation_input_token_cost": 3.125e-06, "cache_read_input_token_cost": 5e-07, + "explicit_cache_read_input_token_cost": 2.5e-07, "input_cost_per_token": 2.5e-06, "litellm_provider": "dashscope", "max_input_tokens": 991808, @@ -19285,7 +19287,9 @@ ] }, "qwencloud/qwen3.7-max": { + "cache_creation_input_token_cost": 3.125e-06, "cache_read_input_token_cost": 5e-07, + "explicit_cache_read_input_token_cost": 2.5e-07, "input_cost_per_token": 2.5e-06, "litellm_provider": "qwencloud", "max_input_tokens": 991808, @@ -20512,7 +20516,9 @@ ] }, "qwen_ai_platform/qwen3.7-max": { + "cache_creation_input_token_cost": 3.125e-06, "cache_read_input_token_cost": 5e-07, + "explicit_cache_read_input_token_cost": 2.5e-07, "input_cost_per_token": 2.5e-06, "litellm_provider": "qwen_ai_platform", "max_input_tokens": 991808, diff --git a/model_prices_and_context_window.schema.json b/model_prices_and_context_window.schema.json index ce9af67eb94..0e8f4f596c0 100644 --- a/model_prices_and_context_window.schema.json +++ b/model_prices_and_context_window.schema.json @@ -297,6 +297,10 @@ "format": "date", "pattern": "^\\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\\d|3[01])$" }, + "explicit_cache_read_input_token_cost": { + "type": "number", + "minimum": 0 + }, "gemini_audio_only_live": { "type": "boolean" }, diff --git a/tests/unit/llms/dashscope/test_dashscope_cost_calculator.py b/tests/unit/llms/dashscope/test_dashscope_cost_calculator.py index a30d35d46f2..551caab7153 100644 --- a/tests/unit/llms/dashscope/test_dashscope_cost_calculator.py +++ b/tests/unit/llms/dashscope/test_dashscope_cost_calculator.py @@ -16,11 +16,18 @@ import pytest # Add the project root to Python path import litellm +from litellm.litellm_core_utils.llm_cost_calc.utils import ( + resolve_explicit_cache_read_cost_rate, + calculate_prompt_caching_savings, + generic_cost_per_token, + get_token_type_cost_breakdown, +) from litellm.llms.dashscope.cost_calculator import ( cost_per_token as dashscope_cost_per_token, ) from litellm.types.utils import ( CompletionTokensDetailsWrapper, + ModelInfo, PromptTokensDetailsWrapper, Usage, ) @@ -35,6 +42,196 @@ class TestDashscopeCostCalculator: os.environ["LITELLM_LOCAL_MODEL_COST_MAP"] = "True" litellm.model_cost = litellm.get_model_cost_map(url="") + @pytest.mark.parametrize( + "prompt_tokens_details", + [ + {"cached_tokens": 0, "cache_type": "ephemeral"}, + {"cached_tokens": 600}, + {"cached_tokens": 600, "cache_type": "unknown-future-type"}, + ], + ) + def test_explicit_cache_rate_requires_an_ephemeral_cache_hit( + self, + prompt_tokens_details: dict[str, int | str], + ): + assert ( + resolve_explicit_cache_read_cost_rate( + model_info={ + "key": "qwen-cache-rate-guard-test", + "explicit_cache_read_input_token_cost": 1e-07, + }, + usage=Usage( + prompt_tokens=1000, + completion_tokens=0, + prompt_tokens_details=prompt_tokens_details, + ), + current_time=None, + ) + is None + ) + + @pytest.mark.parametrize( + ("model_info", "current_time", "expected_cache_rate"), + [ + ( + { + "key": "qwen-tier-mode-rate-test", + "tiered_pricing": [ + { + "range": [0, 2000], + "input_cost_per_token": 1e-06, + "cache_read_input_token_cost": 2e-07, + "explicit_cache_read_input_token_cost": 1e-07, + } + ], + }, + None, + 1e-07, + ), + ( + { + "key": "qwen-tier-generic-fallback-test", + "tiered_pricing": [ + { + "range": [0, 2000], + "input_cost_per_token": 1e-06, + "cache_read_input_token_cost": 2e-07, + } + ], + }, + None, + None, + ), + ( + { + "key": "qwen-flat-generic-fallback-test", + "input_cost_per_token": 1e-06, + "cache_read_input_token_cost": 2e-07, + }, + None, + None, + ), + ( + { + "key": "qwen-invalid-explicit-rate-test", + "input_cost_per_token": 1e-06, + "off_peak_pricing": { + "hours_utc": "00:00-00:00", + "explicit_cache_read_input_token_cost": True, + }, + }, + datetime(2026, 9, 3, 17, 25, tzinfo=timezone.utc), + None, + ), + ( + { + "key": "qwen-tier-off-peak-mode-rate-test", + "tiered_pricing": [ + { + "range": [0, 2000], + "input_cost_per_token": 1e-06, + "cache_read_input_token_cost": 2e-07, + } + ], + "off_peak_pricing": { + "hours_utc": "00:00-00:00", + "explicit_cache_read_input_token_cost": 5e-08, + }, + }, + datetime(2026, 9, 3, 17, 25, tzinfo=timezone.utc), + 5e-08, + ), + ( + { + "key": "qwen-flat-off-peak-mode-rate-test", + "input_cost_per_token": 1e-06, + "cache_read_input_token_cost": 2e-07, + "off_peak_pricing": { + "hours_utc": "00:00-00:00", + "explicit_cache_read_input_token_cost": 5e-08, + }, + }, + datetime(2026, 9, 3, 17, 25, tzinfo=timezone.utc), + 5e-08, + ), + ( + { + "key": "qwen-off-peak-mode-rate-test", + "input_cost_per_token": 1e-06, + "explicit_cache_read_input_token_cost": 1e-07, + "off_peak_pricing": { + "hours_utc": "00:00-00:00", + "explicit_cache_read_input_token_cost": 5e-08, + }, + }, + datetime(2026, 9, 3, 17, 25, tzinfo=timezone.utc), + 5e-08, + ), + ( + { + "key": "qwen-off-peak-generic-rate-test", + "input_cost_per_token": 1e-06, + "explicit_cache_read_input_token_cost": 1e-07, + "off_peak_pricing": { + "hours_utc": "00:00-00:00", + "explicit_cache_read_input_token_cost": "invalid", + "cache_read_input_token_cost": 8e-08, + }, + }, + datetime(2026, 9, 3, 17, 25, tzinfo=timezone.utc), + 8e-08, + ), + ( + { + "key": "qwen-off-peak-standard-fallback-test", + "input_cost_per_token": 1e-06, + "explicit_cache_read_input_token_cost": 1e-07, + "off_peak_pricing": { + "hours_utc": "00:00-00:00", + }, + }, + datetime(2026, 9, 3, 17, 25, tzinfo=timezone.utc), + 1e-07, + ), + ], + ) + def test_provider_neutral_cache_rate_resolver_handles_qwen_rate_fallbacks( + self, + model_info: ModelInfo, + current_time: datetime | None, + expected_cache_rate: float | None, + ): + usage = Usage( + prompt_tokens=1000, + completion_tokens=0, + prompt_tokens_details={"cached_tokens": 600, "cache_type": "ephemeral"}, + ) + + cache_rate = resolve_explicit_cache_read_cost_rate( + model_info=model_info, + usage=usage, + current_time=current_time, + ) + + assert cache_rate == expected_cache_rate + + def test_provider_neutral_cache_rate_resolver_handles_service_tier_rate(self): + cache_rate = resolve_explicit_cache_read_cost_rate( + model_info={ + "key": "qwen-service-tier-mode-rate-test", + "explicit_cache_read_input_token_cost_priority": 3e-07, + }, + usage=Usage( + prompt_tokens=1000, + completion_tokens=0, + prompt_tokens_details={"cached_tokens": 600, "cache_type": "ephemeral"}, + ), + current_time=None, + service_tier="priority", + ) + + assert cache_rate == 3e-07 + def test_dashscope_flat_pricing_fallback(self): """ Tests that the dashscope calculator falls back to flat pricing for models @@ -53,6 +250,276 @@ class TestDashscopeCostCalculator: assert math.isclose(prompt_cost, expected_prompt_cost, rel_tol=1e-10) assert math.isclose(completion_cost, expected_completion_cost, rel_tol=1e-10) + @pytest.mark.parametrize("explicit_cache_rate", [1e-07, "1e-7"]) + @pytest.mark.parametrize( + ("cache_type", "expected_cache_rate"), + [ + ("ephemeral", 1e-07), + (None, 2e-07), + ("unknown-future-type", 2e-07), + ], + ) + def test_dashscope_flat_pricing_distinguishes_explicit_and_implicit_cache_reads( + self, cache_type: str | None, expected_cache_rate: float, explicit_cache_rate: float | str + ) -> None: + litellm.model_cost["dashscope/qwen-context-cache-mode-test"] = { + "litellm_provider": "dashscope", + "mode": "chat", + "input_cost_per_token": 1e-06, + "output_cost_per_token": 4e-06, + "explicit_cache_read_input_token_cost": explicit_cache_rate, + "cache_read_input_token_cost": 2e-07, + } + prompt_tokens_details: dict[str, int | str] = {"cached_tokens": 600} + if cache_type is not None: + prompt_tokens_details["cache_type"] = cache_type + usage = Usage( + prompt_tokens=1000, + completion_tokens=0, + prompt_tokens_details=prompt_tokens_details, + ) + + prompt_cost, _ = dashscope_cost_per_token(model="qwen-context-cache-mode-test", usage=usage) + + assert math.isclose( + prompt_cost, + (400 * 1e-06) + (600 * expected_cache_rate), + rel_tol=1e-10, + ) + + def test_dashscope_explicit_cache_read_falls_back_to_existing_cache_read_rate(self): + litellm.model_cost["dashscope/qwen-explicit-cache-fallback-test"] = { + "litellm_provider": "dashscope", + "mode": "chat", + "input_cost_per_token": 1e-06, + "output_cost_per_token": 4e-06, + "cache_read_input_token_cost": 1e-07, + } + usage = Usage( + prompt_tokens=1000, + completion_tokens=0, + prompt_tokens_details={"cached_tokens": 600, "cache_type": "ephemeral"}, + ) + + prompt_cost, _ = dashscope_cost_per_token( + model="qwen-explicit-cache-fallback-test", + usage=usage, + ) + + assert math.isclose(prompt_cost, (400 * 1e-06) + (600 * 1e-07), rel_tol=1e-10) + + @pytest.mark.parametrize( + ("prompt_tokens_details", "expected_cache_rate"), + [ + ({"cached_tokens": 600, "cache_type": "ephemeral"}, 1e-07), + ({"cached_tokens": 600}, 2e-07), + ], + ) + def test_completion_cost_routes_dashscope_cache_mode_pricing( + self, + prompt_tokens_details: dict[str, int | str], + expected_cache_rate: float, + ): + litellm.model_cost["dashscope/qwen-completion-cache-mode-test"] = { + "litellm_provider": "dashscope", + "mode": "chat", + "input_cost_per_token": 1e-06, + "output_cost_per_token": 4e-06, + "cache_read_input_token_cost": 2e-07, + "explicit_cache_read_input_token_cost": 1e-07, + } + response = litellm.ModelResponse( + model="qwen-completion-cache-mode-test", + usage=Usage( + prompt_tokens=1000, + completion_tokens=100, + total_tokens=1100, + prompt_tokens_details=prompt_tokens_details, + ), + ) + + cost = litellm.completion_cost( + completion_response=response, + model="qwen-completion-cache-mode-test", + custom_llm_provider="dashscope", + ) + + assert math.isclose( + cost, + (400 * 1e-06) + (600 * expected_cache_rate) + (100 * 4e-06), + rel_tol=1e-10, + ) + + def test_dashscope_tiered_pricing_uses_mode_specific_cache_read_rate(self): + self._register_tiered_model( + "dashscope/qwen-tiered-cache-mode-test", + [ + { + "range": [0, 1000], + "input_cost_per_token": 1e-06, + "output_cost_per_token": 4e-06, + "explicit_cache_read_input_token_cost": 1e-07, + "cache_read_input_token_cost": 2e-07, + } + ], + ) + usage = Usage( + prompt_tokens=1000, + completion_tokens=0, + prompt_tokens_details={"cached_tokens": 600, "cache_type": "ephemeral"}, + ) + + prompt_cost, _ = dashscope_cost_per_token(model="qwen-tiered-cache-mode-test", usage=usage) + + assert math.isclose(prompt_cost, (400 * 1e-06) + (600 * 1e-07), rel_tol=1e-10) + + @pytest.mark.parametrize( + ("explicit_cache_rate", "expected_cache_rate"), + [(1e-07, 1e-07), ("1e-7", 1e-07), (0.0, 0.0), ("0", 0.0)], + ) + def test_dashscope_flat_explicit_cache_read_rate_wins_even_at_zero( + self, explicit_cache_rate: float | str, expected_cache_rate: float + ) -> None: + litellm.model_cost["dashscope/qwen-flat-explicit-zero-rate-test"] = { + "litellm_provider": "dashscope", + "mode": "chat", + "input_cost_per_token": 1e-06, + "output_cost_per_token": 4e-06, + "cache_read_input_token_cost": 2e-07, + "explicit_cache_read_input_token_cost": explicit_cache_rate, + } + usage = Usage( + prompt_tokens=1000, + completion_tokens=0, + prompt_tokens_details={"cached_tokens": 600, "cache_type": "ephemeral"}, + ) + + prompt_cost, _ = dashscope_cost_per_token( + model="qwen-flat-explicit-zero-rate-test", + usage=usage, + ) + + assert math.isclose(prompt_cost, (400 * 1e-06) + (600 * expected_cache_rate), rel_tol=1e-10) + + def test_dashscope_tier_without_an_explicit_rate_bills_that_tiers_cache_read_rate(self): + litellm.model_cost["dashscope/qwen-two-tier-explicit-fallback-test"] = { + "litellm_provider": "dashscope", + "mode": "chat", + "explicit_cache_read_input_token_cost": 4e-08, + "tiered_pricing": [ + { + "range": [0, 1000], + "input_cost_per_token": 4e-07, + "output_cost_per_token": 1.6e-06, + "cache_read_input_token_cost": 8e-08, + }, + { + "range": [1000, 10000], + "input_cost_per_token": 1.2e-06, + "output_cost_per_token": 4.8e-06, + "cache_read_input_token_cost": 2.4e-07, + }, + ], + } + usage = Usage( + prompt_tokens=3000, + completion_tokens=0, + prompt_tokens_details={"cached_tokens": 2000, "cache_type": "ephemeral"}, + ) + + prompt_cost, _ = dashscope_cost_per_token(model="qwen-two-tier-explicit-fallback-test", usage=usage) + breakdown = get_token_type_cost_breakdown( + model="qwen-two-tier-explicit-fallback-test", + custom_llm_provider="dashscope", + usage=usage, + ) + + assert math.isclose(breakdown.cache_read_cost, 2000 * 2.4e-07, rel_tol=1e-10) + assert math.isclose(prompt_cost, (1000 * 1.2e-06) + breakdown.cache_read_cost, rel_tol=1e-10) + + def test_dashscope_prompt_caching_savings_uses_explicit_cache_read_rate(self): + model_info: ModelInfo = { + "key": "qwen-cache-savings-mode-test", + "litellm_provider": "dashscope", + "mode": "chat", + "input_cost_per_token": 1e-06, + "output_cost_per_token": 4e-06, + "cache_read_input_token_cost": 2e-07, + "explicit_cache_read_input_token_cost": 1e-07, + } + usage = Usage( + prompt_tokens=1000, + completion_tokens=0, + prompt_tokens_details={"cached_tokens": 600, "cache_type": "ephemeral"}, + ) + + savings = calculate_prompt_caching_savings( + model_info=model_info, + usage=usage, + custom_llm_provider="dashscope", + ) + + assert math.isclose(savings, 600 * (1e-06 - 1e-07), rel_tol=1e-10) + + def test_openai_compatible_qwen_total_uses_explicit_cache_read_rate(self): + model_info: ModelInfo = { + "key": "qwen-generic-cache-mode-test", + "input_cost_per_token": 1e-06, + "output_cost_per_token": 4e-06, + "cache_read_input_token_cost": 2e-07, + "explicit_cache_read_input_token_cost": 1e-07, + } + usage = Usage( + prompt_tokens=1000, + completion_tokens=0, + prompt_tokens_details={"cached_tokens": 600, "cache_type": "ephemeral"}, + ) + + prompt_cost, _ = generic_cost_per_token( + model="custom-qwen", + usage=usage, + custom_llm_provider="openai", + model_info=model_info, + ) + + assert math.isclose(prompt_cost, (400 * 1e-06) + (600 * 1e-07), rel_tol=1e-10) + + @pytest.mark.parametrize( + ("cache_type", "expected_cache_rate"), + [("ephemeral", 1e-07), (None, 2e-07)], + ) + def test_dashscope_cost_breakdown_matches_mode_specific_total( + self, + cache_type: str | None, + expected_cache_rate: float, + ): + litellm.model_cost["dashscope/qwen-cache-breakdown-mode-test"] = { + "litellm_provider": "dashscope", + "mode": "chat", + "input_cost_per_token": 1e-06, + "output_cost_per_token": 4e-06, + "cache_read_input_token_cost": 2e-07, + "explicit_cache_read_input_token_cost": 1e-07, + } + prompt_tokens_details: dict[str, int | str] = {"cached_tokens": 600} + if cache_type is not None: + prompt_tokens_details["cache_type"] = cache_type + usage = Usage( + prompt_tokens=1000, + completion_tokens=0, + prompt_tokens_details=prompt_tokens_details, + ) + + prompt_cost, _ = dashscope_cost_per_token(model="qwen-cache-breakdown-mode-test", usage=usage) + breakdown = get_token_type_cost_breakdown( + model="qwen-cache-breakdown-mode-test", + custom_llm_provider="dashscope", + usage=usage, + ) + + assert math.isclose(breakdown.cache_read_cost, 600 * expected_cache_rate, rel_tol=1e-10) + assert math.isclose(prompt_cost, (400 * 1e-06) + breakdown.cache_read_cost, rel_tol=1e-10) + def test_dashscope_tiered_pricing_within_first_tier(self): """ Tests the dashscope tiered pricing when the request's input falls in the first tier. @@ -577,6 +1044,45 @@ class TestDashscopeCostCalculator: assert math.isclose(peak_prompt_cost, (600 * 2.4e-06) + (300 * 2e-07) + (100 * 3e-06), rel_tol=1e-10) assert math.isclose(peak_completion_cost, 200 * 4.8e-06, rel_tol=1e-10) + @pytest.mark.parametrize( + ("cache_type", "expected_cache_rate"), + [("ephemeral", 5e-08), (None, 8e-08)], + ) + def test_dashscope_off_peak_uses_mode_specific_cache_read_rate( + self, cache_type: str | None, expected_cache_rate: float + ): + model_key = "dashscope/qwen-cache-mode-off-peak-test" + litellm.model_cost[model_key] = { + "litellm_provider": "dashscope", + "mode": "chat", + "input_cost_per_token": 2e-06, + "output_cost_per_token": 4e-06, + "cache_read_input_token_cost": 2e-07, + "explicit_cache_read_input_token_cost": 1e-07, + "off_peak_pricing": { + "hours_utc": self.OFF_PEAK_WINDOW, + "input_cost_per_token": 1e-06, + "cache_read_input_token_cost": 8e-08, + "explicit_cache_read_input_token_cost": 5e-08, + }, + } + prompt_tokens_details: dict[str, int | str] = {"cached_tokens": 600} + if cache_type is not None: + prompt_tokens_details["cache_type"] = cache_type + usage = Usage( + prompt_tokens=1000, + completion_tokens=0, + prompt_tokens_details=prompt_tokens_details, + ) + + prompt_cost, _ = dashscope_cost_per_token( + model="qwen-cache-mode-off-peak-test", + usage=usage, + current_time=self.INSIDE_WINDOW, + ) + + assert math.isclose(prompt_cost, (400 * 1e-06) + (600 * expected_cache_rate), rel_tol=1e-10) + def test_dashscope_off_peak_window_overrides_the_selected_tier(self): """An open off-peak window bills the whole request at the flat off-peak rates, whichever tier the input volume selected.""" diff --git a/tests/unit/proxy/spend_tracking/test_spend_tracking_utils.py b/tests/unit/proxy/spend_tracking/test_spend_tracking_utils.py index 11a881527e6..55358a3b0de 100644 --- a/tests/unit/proxy/spend_tracking/test_spend_tracking_utils.py +++ b/tests/unit/proxy/spend_tracking/test_spend_tracking_utils.py @@ -556,6 +556,7 @@ def test_get_logging_payload_maps_nested_cache_creation_input_tokens(): assert additional_usage_values["cache_creation_input_tokens"] == 2048 assert additional_usage_values["prompt_tokens_details"]["cache_write_tokens"] == 2048 + assert additional_usage_values["prompt_tokens_details"]["cache_type"] == "ephemeral" def test_get_logging_payload_preserves_anthropic_cache_creation_input_tokens(): diff --git a/tests/unit/responses/test_responses_utils.py b/tests/unit/responses/test_responses_utils.py index 55d90acaaee..6ee2c495705 100644 --- a/tests/unit/responses/test_responses_utils.py +++ b/tests/unit/responses/test_responses_utils.py @@ -7,9 +7,11 @@ import pytest import litellm from litellm.llms.openai.responses.transformation import OpenAIResponsesAPIConfig +from litellm.responses.litellm_completion_transformation.transformation import LiteLLMCompletionResponsesConfig from litellm.responses.utils import ResponseAPILoggingUtils, ResponsesAPIRequestUtils -from litellm.types.llms.openai import ResponseAPIUsage, ResponsesAPIOptionalRequestParams +from litellm.types.llms.openai import ResponseAPIUsage, ResponsesAPIOptionalRequestParams, ResponsesAPIResponse from litellm.types.utils import Usage +from litellm.utils import _invalidate_model_cost_lowercase_map class TestResponsesAPIRequestUtils: @@ -890,3 +892,45 @@ class TestResponsesInputToChatMessages: assert ResponsesAPIRequestUtils.responses_input_to_chat_messages( [reasoning_item, user_message, "stray"] ) == [user_message] + + +def test_responses_object_bills_an_explicit_cache_hit_at_the_explicit_rate(): + model = "dashscope/qwen-responses-explicit-cache-test" + previous_row = litellm.model_cost.get(model) + litellm.model_cost[model] = { + "litellm_provider": "dashscope", + "mode": "chat", + "input_cost_per_token": 2.5e-06, + "output_cost_per_token": 7.5e-06, + "cache_read_input_token_cost": 5e-07, + "explicit_cache_read_input_token_cost": 2.5e-07, + } + _invalidate_model_cost_lowercase_map() + try: + chat_usage = Usage( + prompt_tokens=1000, + completion_tokens=0, + prompt_tokens_details={"cached_tokens": 1000, "cache_type": "ephemeral"}, + ) + responses_object = ResponsesAPIResponse( + id="resp_explicit_cache", + created_at=0, + model="qwen-responses-explicit-cache-test", + object="response", + status="completed", + output=[], + parallel_tool_calls=False, + tool_choice="auto", + tools=[], + usage=LiteLLMCompletionResponsesConfig.transform_chat_completion_usage_to_responses_usage(chat_usage), + ) + + assert litellm.completion_cost(completion_response=responses_object, model=model) == pytest.approx( + 1000 * 2.5e-07 + ) + finally: + if previous_row is None: + litellm.model_cost.pop(model, None) + else: + litellm.model_cost[model] = previous_row + _invalidate_model_cost_lowercase_map() diff --git a/tests/unit/test_router_model_cost_isolation.py b/tests/unit/test_router_model_cost_isolation.py index 4cce5b50f0b..ffb71e86303 100644 --- a/tests/unit/test_router_model_cost_isolation.py +++ b/tests/unit/test_router_model_cost_isolation.py @@ -953,6 +953,67 @@ def test_partial_pricing_does_not_overwrite_explicit_cache_fields(): _restore_model_cost_entries(model_keys) +@pytest.mark.parametrize( + ("deployment_cache_prices", "billed_cache_read_rate"), + [({}, 2.5e-07), ({"cache_read_input_token_cost": 1e-07}, 1e-07)], + ids=["inherits-builtin-explicit-rate", "own-cache-read-price-wins"], +) +def test_partial_custom_pricing_bills_explicit_cache_hits( + deployment_cache_prices: dict[str, float], billed_cache_read_rate: float +): + backend_model = "dashscope/qwen-explicit-cache-inherit-backend" + deploy_id = "qwen-deploy-partial-pricing" + model_keys = { + deploy_id: litellm.model_cost.get(deploy_id), + backend_model: litellm.model_cost.get(backend_model), + } + litellm.model_cost[backend_model] = { + "litellm_provider": "dashscope", + "mode": "chat", + "input_cost_per_token": 2.5e-06, + "output_cost_per_token": 7.5e-06, + "cache_creation_input_token_cost": 3.125e-06, + "cache_read_input_token_cost": 5e-07, + "explicit_cache_read_input_token_cost": 2.5e-07, + } + _invalidate_model_cost_lowercase_map() + try: + Router( + model_list=[ + { + "model_name": "qwen-custom", + "litellm_params": { + "model": backend_model, + "api_key": "fake-key", + "input_cost_per_token": 2e-06, + "output_cost_per_token": 6e-06, + **deployment_cache_prices, + }, + "model_info": {"id": deploy_id}, + } + ], + ) + response = litellm.ModelResponse( + model=backend_model, + usage=litellm.Usage( + prompt_tokens=1000, + completion_tokens=0, + prompt_tokens_details={"cached_tokens": 1000, "cache_type": "ephemeral"}, + ), + ) + + cost = litellm.completion_cost( + completion_response=response, + model=backend_model, + custom_pricing=True, + router_model_id=deploy_id, + ) + + assert cost == pytest.approx(1000 * billed_cache_read_rate) + finally: + _restore_model_cost_entries(model_keys) + + def test_inherit_builtin_cache_pricing_fills_only_missing_fields(): """Direct unit test of the helper: missing cache fields are filled from the backend model's built-in entry, while an explicitly set cache field and the diff --git a/tests/unit/test_utils.py b/tests/unit/test_utils.py index 7df4910c24f..cf0bf9b5d79 100644 --- a/tests/unit/test_utils.py +++ b/tests/unit/test_utils.py @@ -866,6 +866,7 @@ def validate_model_cost_values(model_data, exceptions=None): "cache_creation_input_token_cost", "cache_creation_input_audio_token_cost", "cache_read_input_token_cost", + "explicit_cache_read_input_token_cost", "cache_read_input_audio_token_cost", "cache_read_input_image_token_cost", "input_dbu_cost_per_token", @@ -964,6 +965,7 @@ def test_aaamodel_prices_and_context_window_json_is_valid(): "cache_creation_input_token_cost_priority": {"type": "number"}, "cache_creation_input_token_cost_ultrafast": {"type": "number"}, "cache_read_input_token_cost": {"type": "number"}, + "explicit_cache_read_input_token_cost": {"type": "number"}, "cache_read_input_token_cost_above_32k_tokens": {"type": "number"}, "cache_read_input_token_cost_above_100k_tokens": {"type": "number"}, "cache_read_input_token_cost_above_100k_tokens_batches": {"type": "number"}, @@ -7777,6 +7779,51 @@ def test_register_model_without_a_provider_never_inherits_another_providers_entr _invalidate_model_cost_lowercase_map() +@pytest.mark.parametrize( + "registered_prices, billed_cache_read_rate", + [ + pytest.param({"input_cost_per_token": 2e-06}, 2.5e-07, id="inherits-builtin-explicit-rate"), + pytest.param({"cache_read_input_token_cost": 1e-07}, 1e-07, id="own-cache-read-price-wins"), + pytest.param( + {"cache_read_input_token_cost": 1e-07, "explicit_cache_read_input_token_cost": 5e-08}, + 5e-08, + id="own-explicit-price-wins", + ), + ], +) +def test_register_model_over_a_builtin_row_bills_explicit_cache_hits( + monkeypatch: pytest.MonkeyPatch, registered_prices: dict[str, float], billed_cache_read_rate: float +) -> None: + from litellm.utils import _invalidate_model_cost_lowercase_map + + key: Final = "dashscope/qwen-register-explicit-cache-test" + monkeypatch.setattr(litellm, "model_cost", dict(litellm.model_cost)) + litellm.model_cost[key] = { + "litellm_provider": "dashscope", + "mode": "chat", + "input_cost_per_token": 2.5e-06, + "output_cost_per_token": 7.5e-06, + "cache_read_input_token_cost": 5e-07, + "explicit_cache_read_input_token_cost": 2.5e-07, + } + _invalidate_model_cost_lowercase_map() + try: + litellm.register_model({key: registered_prices}, persist_across_reloads=False) + explicit_hit: Final = ModelResponse( + usage=Usage( + prompt_tokens=1000, + completion_tokens=0, + prompt_tokens_details={"cached_tokens": 1000, "cache_type": "ephemeral"}, + ) + ) + + assert litellm.completion_cost(completion_response=explicit_hit, model=key) == pytest.approx( + 1000 * billed_cache_read_rate + ) + finally: + _invalidate_model_cost_lowercase_map() + + @pytest.mark.parametrize( "model, litellm_params, expects_native_config", [ diff --git a/tests/unit/types/test_types_utils.py b/tests/unit/types/test_types_utils.py index fbe00ca260b..d85e48540f9 100644 --- a/tests/unit/types/test_types_utils.py +++ b/tests/unit/types/test_types_utils.py @@ -209,6 +209,46 @@ def test_prompt_tokens_details_maps_nested_cache_creation_input_tokens(): assert not hasattr(non_int, "cache_write_tokens") +def test_prompt_tokens_details_preserves_qwen_cache_type(): + from litellm.types.utils import PromptTokensDetailsWrapper + + details = PromptTokensDetailsWrapper( + cached_tokens=1889, + cache_creation_input_tokens=0, + cache_type="ephemeral", + ) + + assert details.cache_type == "ephemeral" + assert details.model_dump()["cache_type"] == "ephemeral" + + +@pytest.mark.parametrize("cache_type", [1, {"mode": "explicit"}, ["ephemeral"]]) +def test_usage_ignores_a_non_string_cache_type(cache_type: object): + from litellm.types.utils import Usage + + usage = Usage( + prompt_tokens=1000, + completion_tokens=0, + prompt_tokens_details={"cached_tokens": 600, "cache_type": cache_type}, + ) + + assert usage.prompt_tokens_details is not None + assert usage.prompt_tokens_details.cached_tokens == 600 + assert getattr(usage.prompt_tokens_details, "cache_type", None) is None + + +def test_custom_pricing_accepts_qwen_explicit_cache_rate(): + from litellm.types.utils import CustomPricingLiteLLMParams + + pricing = CustomPricingLiteLLMParams( + cache_read_input_token_cost=2e-07, + explicit_cache_read_input_token_cost=1e-07, + ) + + assert pricing.cache_read_input_token_cost == 2e-07 + assert pricing.explicit_cache_read_input_token_cost == 1e-07 + + def test_usage_server_tool_use_dict_is_coerced_and_round_trips(): from litellm.types.utils import ServerToolUse, Usage diff --git a/ui/litellm-dashboard/src/lib/http/schema.d.ts b/ui/litellm-dashboard/src/lib/http/schema.d.ts index ac096862b56..88d54d3731a 100644 --- a/ui/litellm-dashboard/src/lib/http/schema.d.ts +++ b/ui/litellm-dashboard/src/lib/http/schema.d.ts @@ -35162,6 +35162,8 @@ export interface components { default_api_key_tpm_limit?: number | null; /** Drop Params */ drop_params?: boolean | string | null; + /** Explicit Cache Read Input Token Cost */ + explicit_cache_read_input_token_cost?: number | null; /** * Fireworks Forward User Id * @description Send the LiteLLM user id of the calling key as the `user` field on Fireworks AI chat, responses and messages requests. @@ -50058,6 +50060,8 @@ export interface components { discoverable?: boolean | null; /** Enable Tag Filtering */ enable_tag_filtering?: boolean | null; + /** Explicit Cache Read Input Token Cost */ + explicit_cache_read_input_token_cost?: number | null; /** Id */ id: string | null; /** Input Cost Per Character */ @@ -50412,6 +50416,8 @@ export interface components { default_api_key_tpm_limit?: number | null; /** Drop Params */ drop_params?: boolean | string | null; + /** Explicit Cache Read Input Token Cost */ + explicit_cache_read_input_token_cost?: number | null; /** * Fireworks Forward User Id * @description Send the LiteLLM user id of the calling key as the `user` field on Fireworks AI chat, responses and messages requests.