diff --git a/litellm/llms/dashscope/cost_calculator.py b/litellm/llms/dashscope/cost_calculator.py index 0c42f77e8ac..3b3328d02a7 100644 --- a/litellm/llms/dashscope/cost_calculator.py +++ b/litellm/llms/dashscope/cost_calculator.py @@ -88,19 +88,20 @@ def _calculate_completion_cost( model_info: ModelInfo, tier: dict | None, ) -> float: - # A tier declaring no output rate falls back to the model's own, since a table spelling out - # only input rates would otherwise serve every completion for free + # A tier that declares output rates keeps the request on them, all-or-nothing. A tier table + # spelling out only input rates would serve every completion for free, so there the model's + # own output rates stand in + tier_declares_output: Final = tier is not None and "output_cost_per_token" in tier output_cost: Final = ( tier_rate(tier, "output_cost_per_token") - if tier is not None and "output_cost_per_token" in tier + if tier_declares_output else float(model_info.get("output_cost_per_token") or 0.0) ) - if tier is not None: - return (breakdown.completion_tokens * output_cost) + ( - breakdown.reasoning_tokens * (tier_rate(tier, "output_cost_per_reasoning_token") or output_cost) - ) - - reasoning_cost: Final = _flat_rate(model_info, "output_cost_per_reasoning_token", "output_cost_per_token") + tier_reasoning_cost: Final = tier_rate(tier, "output_cost_per_reasoning_token") if tier is not None else 0.0 + model_reasoning_cost: Final = ( + 0.0 if tier_declares_output else float(model_info.get("output_cost_per_reasoning_token") or 0.0) + ) + reasoning_cost: Final = tier_reasoning_cost or model_reasoning_cost or output_cost return (breakdown.completion_tokens * output_cost) + (breakdown.reasoning_tokens * reasoning_cost) diff --git a/tests/test_litellm/llms/dashscope/test_dashscope_cost_calculator.py b/tests/test_litellm/llms/dashscope/test_dashscope_cost_calculator.py index 42577ec44e3..4188cb6a4f3 100644 --- a/tests/test_litellm/llms/dashscope/test_dashscope_cost_calculator.py +++ b/tests/test_litellm/llms/dashscope/test_dashscope_cost_calculator.py @@ -21,7 +21,11 @@ import litellm from litellm.llms.dashscope.cost_calculator import ( cost_per_token as dashscope_cost_per_token, ) -from litellm.types.utils import Usage, PromptTokensDetailsWrapper +from litellm.types.utils import ( + CompletionTokensDetailsWrapper, + PromptTokensDetailsWrapper, + Usage, +) class TestDashscopeCostCalculator: @@ -344,6 +348,61 @@ class TestDashscopeCostCalculator: assert math.isclose(prompt_cost, 500 * 4e-07, rel_tol=1e-10) assert math.isclose(completion_cost, 200 * 1.6e-06, rel_tol=1e-10) + def test_dashscope_tier_without_an_output_rate_bills_the_model_reasoning_rate(self): + """ + Regression: a tier declaring only an input rate billed reasoning tokens at the model's + plain output rate, ignoring the model's dedicated reasoning rate. + """ + litellm.model_cost["dashscope/qwen-input-only-reasoning-test"] = { + "litellm_provider": "dashscope", + "mode": "chat", + "output_cost_per_token": 1.6e-06, + "output_cost_per_reasoning_token": 4e-06, + "tiered_pricing": [{"range": [0, 1000], "input_cost_per_token": 4e-07}], + } + + usage = Usage( + prompt_tokens=500, + completion_tokens=200, + completion_tokens_details=CompletionTokensDetailsWrapper(reasoning_tokens=150), + ) + _, completion_cost = dashscope_cost_per_token( + model="qwen-input-only-reasoning-test", usage=usage + ) + + assert math.isclose( + completion_cost, (50 * 1.6e-06) + (150 * 4e-06), rel_tol=1e-10 + ) + + def test_dashscope_tier_output_rate_wins_over_the_model_reasoning_rate(self): + """ + A tier declaring its own output rate keeps reasoning tokens on that tier rather than + mixing in a model-level reasoning rate. + """ + litellm.model_cost["dashscope/qwen-tier-output-reasoning-test"] = { + "litellm_provider": "dashscope", + "mode": "chat", + "output_cost_per_reasoning_token": 4e-06, + "tiered_pricing": [ + { + "range": [0, 1000], + "input_cost_per_token": 4e-07, + "output_cost_per_token": 1.6e-06, + } + ], + } + + usage = Usage( + prompt_tokens=500, + completion_tokens=200, + completion_tokens_details=CompletionTokensDetailsWrapper(reasoning_tokens=150), + ) + _, completion_cost = dashscope_cost_per_token( + model="qwen-tier-output-reasoning-test", usage=usage + ) + + assert math.isclose(completion_cost, 200 * 1.6e-06, rel_tol=1e-10) + def test_dashscope_tiered_pricing_zero_input_falls_back_to_flat_rates(self): """ No tier can be selected without input tokens, so an empty-prompt request must