From 4427f56a122d3d7fdeb5d991c62cda37d683482c Mon Sep 17 00:00:00 2001 From: wonder_land <61005155+kunwl123456@users.noreply.github.com> Date: Tue, 28 Jul 2026 17:00:51 +0800 Subject: [PATCH 1/2] fix(volcengine): support tiered Doubao pricing Co-authored-by: Cursor --- litellm/cost_calculator.py | 6 + litellm/llms/volcengine/cost_calculator.py | 83 ++++++++ ...odel_prices_and_context_window_backup.json | 41 ++++ model_prices_and_context_window.json | 41 ++++ .../llms/volcengine/test_cost_calculator.py | 177 ++++++++++++++++++ .../components/ModelsTableColumns.tsx | 2 +- .../utils/modelDataTransformer.test.ts | 40 ++++ .../utils/modelDataTransformer.ts | 25 ++- .../src/components/model_dashboard/types.ts | 5 +- 9 files changed, 415 insertions(+), 5 deletions(-) create mode 100644 litellm/llms/volcengine/cost_calculator.py create mode 100644 tests/test_litellm/llms/volcengine/test_cost_calculator.py diff --git a/litellm/cost_calculator.py b/litellm/cost_calculator.py index 96aed20529f6..486d1cfb437c 100644 --- a/litellm/cost_calculator.py +++ b/litellm/cost_calculator.py @@ -642,6 +642,12 @@ def cost_per_token( ) return dashscope_cost_per_token(model=model, usage=usage_block) + elif custom_llm_provider == "volcengine": + from litellm.llms.volcengine.cost_calculator import ( + cost_per_token as volcengine_cost_per_token, + ) + + return volcengine_cost_per_token(model=model, usage=usage_block) elif custom_llm_provider == "azure_ai": return azure_ai_cost_per_token( model=model, diff --git a/litellm/llms/volcengine/cost_calculator.py b/litellm/llms/volcengine/cost_calculator.py new file mode 100644 index 000000000000..855c88da368c --- /dev/null +++ b/litellm/llms/volcengine/cost_calculator.py @@ -0,0 +1,83 @@ +""" +Cost calculator for Volcengine chat models. + +Volcengine selects one pricing tier from the request's total input length and +charges every input/output token at that tier. This is not graduated pricing. +""" + +from litellm.litellm_core_utils.llm_cost_calc.tiered_pricing import ( + select_tier_for_input, + tier_rate, +) +from litellm.types.utils import ModelInfo, Usage +from litellm.utils import get_model_info + + +def _cached_prompt_tokens(usage: Usage) -> int: + prompt_details = usage.prompt_tokens_details + if prompt_details is None: + return 0 + return int(getattr(prompt_details, "cached_tokens", 0) or 0) + + +def _select_pricing_tier( + tiered_pricing: list[dict] | None, + prompt_tokens: int, +) -> dict | None: + if not tiered_pricing: + return None + + # Output-only synthetic usage blocks have no input length. Use the first + # tier instead of returning zero cost for their completion tokens. + return select_tier_for_input( + tiered_pricing=tiered_pricing, + input_tokens=max(prompt_tokens, 1), + ) + + +def _output_rate(tier: dict, completion_tokens: int) -> float: + """Read the output rate, including Seed 1.8's short-output discount.""" + if completion_tokens > 200 and tier.get("output_cost_per_token_above_200_tokens") is not None: + return tier_rate(tier, "output_cost_per_token_above_200_tokens") + return tier_rate(tier, "output_cost_per_token") + + +def cost_per_token(model: str, usage: Usage) -> tuple[float, float]: + """ + Return ``(prompt_cost_usd, completion_cost_usd)`` for a Volcengine request. + + Tier selection is based on total prompt tokens. Cached prompt tokens still + count toward the tier boundary, but use the cache-read rate when one is + declared by the model. + """ + model_info: ModelInfo = get_model_info( + model=model, + custom_llm_provider="volcengine", + ) + + prompt_tokens = int(usage.prompt_tokens or 0) + completion_tokens = int(usage.completion_tokens or 0) + cached_tokens = min(_cached_prompt_tokens(usage), prompt_tokens) + uncached_tokens = prompt_tokens - cached_tokens + + raw_tiered_pricing = model_info.get("tiered_pricing") + tiered_pricing = raw_tiered_pricing if isinstance(raw_tiered_pricing, list) else None + tier = _select_pricing_tier( + tiered_pricing=tiered_pricing, + prompt_tokens=prompt_tokens, + ) + + if tier is not None: + input_rate = tier_rate(tier, "input_cost_per_token") + output_rate = _output_rate(tier=tier, completion_tokens=completion_tokens) + cache_rate_value = model_info.get("cache_read_input_token_cost") + cache_rate = float(cache_rate_value) if cache_rate_value is not None else input_rate + else: + input_rate = float(model_info.get("input_cost_per_token") or 0.0) + output_rate = float(model_info.get("output_cost_per_token") or 0.0) + cache_rate_value = model_info.get("cache_read_input_token_cost") + cache_rate = float(cache_rate_value) if cache_rate_value is not None else input_rate + + prompt_cost = (uncached_tokens * input_rate) + (cached_tokens * cache_rate) + completion_cost = completion_tokens * output_rate + return prompt_cost, completion_cost diff --git a/litellm/model_prices_and_context_window_backup.json b/litellm/model_prices_and_context_window_backup.json index 2cef600ea32f..18f78fb3e1f5 100644 --- a/litellm/model_prices_and_context_window_backup.json +++ b/litellm/model_prices_and_context_window_backup.json @@ -45281,6 +45281,47 @@ "supports_vision": true, "source": "https://aws.amazon.com/bedrock/pricing/" }, + "volcengine/doubao-seed-1-8-251228": { + "cache_read_input_token_cost": 2.2e-08, + "litellm_provider": "volcengine", + "max_input_tokens": 256000, + "max_output_tokens": 64000, + "max_tokens": 64000, + "mode": "chat", + "source": "https://www.volcengine.com/docs/82379/1330310", + "supports_function_calling": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_tool_choice": false, + "supports_vision": true, + "tiered_pricing": [ + { + "input_cost_per_token": 1.1e-07, + "output_cost_per_token": 2.8e-07, + "output_cost_per_token_above_200_tokens": 1.1e-06, + "range": [ + 0, + 32000.0 + ] + }, + { + "input_cost_per_token": 1.7e-07, + "output_cost_per_token": 2.2e-06, + "range": [ + 32000.0, + 128000.0 + ] + }, + { + "input_cost_per_token": 3.3e-07, + "output_cost_per_token": 3.3e-06, + "range": [ + 128000.0, + 256000.0 + ] + } + ] + }, "volcengine/doubao-seed-2-0-pro-260215": { "litellm_provider": "volcengine", "max_input_tokens": 256000, diff --git a/model_prices_and_context_window.json b/model_prices_and_context_window.json index db28118d52be..ec24026d6e0d 100644 --- a/model_prices_and_context_window.json +++ b/model_prices_and_context_window.json @@ -45402,6 +45402,47 @@ "supports_vision": true, "source": "https://aws.amazon.com/bedrock/pricing/" }, + "volcengine/doubao-seed-1-8-251228": { + "cache_read_input_token_cost": 2.2e-08, + "litellm_provider": "volcengine", + "max_input_tokens": 256000, + "max_output_tokens": 64000, + "max_tokens": 64000, + "mode": "chat", + "source": "https://www.volcengine.com/docs/82379/1330310", + "supports_function_calling": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_tool_choice": false, + "supports_vision": true, + "tiered_pricing": [ + { + "input_cost_per_token": 1.1e-07, + "output_cost_per_token": 2.8e-07, + "output_cost_per_token_above_200_tokens": 1.1e-06, + "range": [ + 0, + 32000.0 + ] + }, + { + "input_cost_per_token": 1.7e-07, + "output_cost_per_token": 2.2e-06, + "range": [ + 32000.0, + 128000.0 + ] + }, + { + "input_cost_per_token": 3.3e-07, + "output_cost_per_token": 3.3e-06, + "range": [ + 128000.0, + 256000.0 + ] + } + ] + }, "volcengine/doubao-seed-2-0-pro-260215": { "litellm_provider": "volcengine", "max_input_tokens": 256000, diff --git a/tests/test_litellm/llms/volcengine/test_cost_calculator.py b/tests/test_litellm/llms/volcengine/test_cost_calculator.py new file mode 100644 index 000000000000..9834d5ee417c --- /dev/null +++ b/tests/test_litellm/llms/volcengine/test_cost_calculator.py @@ -0,0 +1,177 @@ +import math +from typing import Generator + +import pytest + +import litellm +from litellm.llms.volcengine.cost_calculator import ( + cost_per_token as volcengine_cost_per_token, +) +from litellm.types.utils import PromptTokensDetailsWrapper, Usage + + +@pytest.fixture(autouse=True) +def local_model_cost_map(monkeypatch: pytest.MonkeyPatch) -> Generator[None, None, None]: + original_model_cost = litellm.model_cost + monkeypatch.setenv("LITELLM_LOCAL_MODEL_COST_MAP", "True") + litellm.model_cost = litellm.get_model_cost_map(url="") + litellm.get_model_info.cache_clear() + yield + litellm.model_cost = original_model_cost + litellm.get_model_info.cache_clear() + + +@pytest.mark.parametrize( + ("model", "prompt_tokens", "completion_tokens", "tier_index"), + [ + ("doubao-seed-2-0-lite-260215", 32000, 1000, 0), + ("doubao-seed-2-0-lite-260215", 32001, 1000, 1), + ("doubao-seed-2-0-pro-260215", 128000, 1000, 1), + ("doubao-seed-2-0-pro-260215", 128001, 1000, 2), + ], +) +def test_seed_2_uses_one_tier_selected_by_total_input_length( + model: str, + prompt_tokens: int, + completion_tokens: int, + tier_index: int, +) -> None: + usage = Usage( + prompt_tokens=prompt_tokens, + completion_tokens=completion_tokens, + ) + prompt_cost, completion_cost = volcengine_cost_per_token(model=model, usage=usage) + + model_info = litellm.get_model_info( + model=model, + custom_llm_provider="volcengine", + ) + tier = model_info["tiered_pricing"][tier_index] + + assert math.isclose( + prompt_cost, + prompt_tokens * tier["input_cost_per_token"], + rel_tol=1e-12, + ) + assert math.isclose( + completion_cost, + completion_tokens * tier["output_cost_per_token"], + rel_tol=1e-12, + ) + + +def test_seed_1_8_applies_short_output_discount_only_through_200_tokens() -> None: + short_usage = Usage(prompt_tokens=10000, completion_tokens=200) + long_usage = Usage(prompt_tokens=10000, completion_tokens=201) + + _, short_output_cost = volcengine_cost_per_token( + model="doubao-seed-1-8-251228", + usage=short_usage, + ) + _, long_output_cost = volcengine_cost_per_token( + model="doubao-seed-1-8-251228", + usage=long_usage, + ) + + model_info = litellm.get_model_info( + model="doubao-seed-1-8-251228", + custom_llm_provider="volcengine", + ) + first_tier = model_info["tiered_pricing"][0] + assert math.isclose( + short_output_cost, + 200 * first_tier["output_cost_per_token"], + rel_tol=1e-12, + ) + assert math.isclose( + long_output_cost, + 201 * first_tier["output_cost_per_token_above_200_tokens"], + rel_tol=1e-12, + ) + + +@pytest.mark.parametrize( + ("prompt_tokens", "tier_index"), + [ + (32000, 0), + (32001, 1), + (128000, 1), + (128001, 2), + ], +) +def test_seed_1_8_input_and_output_rates_follow_input_tier( + prompt_tokens: int, + tier_index: int, +) -> None: + completion_tokens = 1000 + usage = Usage( + prompt_tokens=prompt_tokens, + completion_tokens=completion_tokens, + ) + prompt_cost, completion_cost = volcengine_cost_per_token( + model="doubao-seed-1-8-251228", + usage=usage, + ) + + model_info = litellm.get_model_info( + model="doubao-seed-1-8-251228", + custom_llm_provider="volcengine", + ) + tier = model_info["tiered_pricing"][tier_index] + output_key = "output_cost_per_token_above_200_tokens" if tier_index == 0 else "output_cost_per_token" + + assert math.isclose( + prompt_cost, + prompt_tokens * tier["input_cost_per_token"], + rel_tol=1e-12, + ) + assert math.isclose( + completion_cost, + completion_tokens * tier[output_key], + rel_tol=1e-12, + ) + + +def test_seed_1_8_cached_tokens_use_cache_rate_but_still_select_input_tier() -> None: + usage = Usage( + prompt_tokens=50000, + completion_tokens=1000, + prompt_tokens_details=PromptTokensDetailsWrapper(cached_tokens=10000), + ) + prompt_cost, _ = volcengine_cost_per_token( + model="doubao-seed-1-8-251228", + usage=usage, + ) + + model_info = litellm.get_model_info( + model="doubao-seed-1-8-251228", + custom_llm_provider="volcengine", + ) + second_tier = model_info["tiered_pricing"][1] + expected = (40000 * second_tier["input_cost_per_token"]) + (10000 * model_info["cache_read_input_token_cost"]) + assert math.isclose(prompt_cost, expected, rel_tol=1e-12) + + +def test_top_level_cost_calculator_routes_volcengine_to_tiered_calculator() -> None: + prompt_cost, completion_cost = litellm.cost_per_token( + model="doubao-seed-2-0-lite-260215", + prompt_tokens=50000, + completion_tokens=1000, + custom_llm_provider="volcengine", + ) + model_info = litellm.get_model_info( + model="doubao-seed-2-0-lite-260215", + custom_llm_provider="volcengine", + ) + second_tier = model_info["tiered_pricing"][1] + + assert math.isclose( + prompt_cost, + 50000 * second_tier["input_cost_per_token"], + rel_tol=1e-12, + ) + assert math.isclose( + completion_cost, + 1000 * second_tier["output_cost_per_token"], + rel_tol=1e-12, + ) diff --git a/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/components/ModelsTableColumns.tsx b/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/components/ModelsTableColumns.tsx index 93ee6d9f0ab5..7d50515c8ed6 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/components/ModelsTableColumns.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/components/ModelsTableColumns.tsx @@ -190,7 +190,7 @@ function CostsCell({ model }: { model: ModelData }) { return ( {inputCost != null && ( diff --git a/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/utils/modelDataTransformer.test.ts b/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/utils/modelDataTransformer.test.ts index 42b767269224..b8da955ab5d8 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/utils/modelDataTransformer.test.ts +++ b/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/utils/modelDataTransformer.test.ts @@ -76,6 +76,46 @@ describe("transformModelData", () => { expect(result.data[0]).toHaveProperty("output_cost", "0.00"); }); + it("should display tiered prices as ranges instead of zero", () => { + const rawData = { + data: [ + { + model_name: "volcengine/doubao-seed-2-0-lite-260215", + litellm_params: { + model: "volcengine/doubao-seed-2-0-lite-260215", + }, + model_info: { + input_cost_per_token: 0, + output_cost_per_token: 0, + tiered_pricing: [ + { + range: [0, 32000], + input_cost_per_token: 8.7e-8, + output_cost_per_token: 5.2e-7, + }, + { + range: [32000, 128000], + input_cost_per_token: 1.3e-7, + output_cost_per_token: 7.8e-7, + }, + { + range: [128000, 256000], + input_cost_per_token: 2.6e-7, + output_cost_per_token: 1.6e-6, + }, + ], + }, + }, + ], + }; + + const result = transformModelData(rawData, mockGetProviderFromModel); + + expect(result.data[0]).toHaveProperty("input_cost", "0.09–$0.26"); + expect(result.data[0]).toHaveProperty("output_cost", "0.52–$1.60"); + expect(result.data[0]).toHaveProperty("has_tiered_pricing", true); + }); + it("should handle null cost fields in model_info", () => { const rawData = { data: [ diff --git a/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/utils/modelDataTransformer.ts b/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/utils/modelDataTransformer.ts index 963fba57507a..df5ad4a8e629 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/utils/modelDataTransformer.ts +++ b/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/utils/modelDataTransformer.ts @@ -2,6 +2,20 @@ * Utility function to transform raw model data into the format expected by UI components * This creates a new transformed data object without mutating the original */ +const formatTieredCostRange = (tiers: any[], costKey: string): string | null => { + const rates = tiers.flatMap((tier) => + Object.entries(tier) + .filter(([key, value]) => (key === costKey || key.startsWith(`${costKey}_above_`)) && value != null) + .map(([, value]) => Number(value) * 1_000_000), + ); + const validRates = rates.filter(Number.isFinite); + if (validRates.length === 0) return null; + + const minimum = Math.min(...validRates).toFixed(2); + const maximum = Math.max(...validRates).toFixed(2); + return minimum === maximum ? minimum : `${minimum}–$${maximum}`; +}; + export const transformModelData = (rawModelData: any, getProviderFromModel: (model: string) => string) => { if (!rawModelData?.data) return { data: [] }; @@ -44,6 +58,13 @@ export const transformModelData = (rawModelData: any, getProviderFromModel: (mod output_cost = model_info?.output_cost_per_token; max_tokens = model_info?.max_tokens; max_input_tokens = model_info?.max_input_tokens; + + const tieredPricing = Array.isArray(model_info?.tiered_pricing) ? model_info.tiered_pricing : []; + if (tieredPricing.length > 0) { + input_cost = formatTieredCostRange(tieredPricing, "input_cost_per_token") ?? input_cost; + output_cost = formatTieredCostRange(tieredPricing, "output_cost_per_token") ?? output_cost; + transformedData[i].has_tiered_pricing = true; + } } if (curr_model?.litellm_params) { @@ -58,11 +79,11 @@ export const transformModelData = (rawModelData: any, getProviderFromModel: (mod transformedData[i].litellm_model_name = litellm_model_name; // Convert Cost in terms of Cost per 1M tokens - if (transformedData[i].input_cost != null) { + if (transformedData[i].input_cost != null && !transformedData[i].has_tiered_pricing) { transformedData[i].input_cost = (Number(transformedData[i].input_cost) * 1000000).toFixed(2); } - if (transformedData[i].output_cost != null) { + if (transformedData[i].output_cost != null && !transformedData[i].has_tiered_pricing) { transformedData[i].output_cost = (Number(transformedData[i].output_cost) * 1000000).toFixed(2); } diff --git a/ui/litellm-dashboard/src/components/model_dashboard/types.ts b/ui/litellm-dashboard/src/components/model_dashboard/types.ts index e58204995ddd..09e32ef64dbd 100644 --- a/ui/litellm-dashboard/src/components/model_dashboard/types.ts +++ b/ui/litellm-dashboard/src/components/model_dashboard/types.ts @@ -25,8 +25,9 @@ export interface ModelData { model_name: string; provider: string; litellm_model_name: string; - input_cost: number; - output_cost: number; + input_cost: number | string; + output_cost: number | string; + has_tiered_pricing?: boolean; max_tokens: number; max_input_tokens: number; api_base?: string; From d53f57404a8cbdc577c00a57be41d21d33c81d02 Mon Sep 17 00:00:00 2001 From: wonder_land <61005155+kunwl123456@users.noreply.github.com> Date: Tue, 28 Jul 2026 17:37:06 +0800 Subject: [PATCH 2/2] fix: address Volcengine pricing CI checks Co-authored-by: Cursor --- litellm/llms/volcengine/cost_calculator.py | 9 ++-- .../llms/volcengine/test_cost_calculator.py | 44 +++++++++++++++++++ tests/test_litellm/test_utils.py | 1 + 3 files changed, 51 insertions(+), 3 deletions(-) diff --git a/litellm/llms/volcengine/cost_calculator.py b/litellm/llms/volcengine/cost_calculator.py index 855c88da368c..2d6481e5045a 100644 --- a/litellm/llms/volcengine/cost_calculator.py +++ b/litellm/llms/volcengine/cost_calculator.py @@ -21,9 +21,9 @@ def _cached_prompt_tokens(usage: Usage) -> int: def _select_pricing_tier( - tiered_pricing: list[dict] | None, + tiered_pricing: list[dict] | None, # mutable-ok: shared tier helper accepts JSON-backed lists of dictionaries prompt_tokens: int, -) -> dict | None: +) -> dict | None: # mutable-ok: returns the JSON-backed tier selected by the shared helper if not tiered_pricing: return None @@ -35,7 +35,10 @@ def _select_pricing_tier( ) -def _output_rate(tier: dict, completion_tokens: int) -> float: +def _output_rate( + tier: dict, # mutable-ok: tier_rate accepts a JSON-backed dictionary + completion_tokens: int, +) -> float: """Read the output rate, including Seed 1.8's short-output discount.""" if completion_tokens > 200 and tier.get("output_cost_per_token_above_200_tokens") is not None: return tier_rate(tier, "output_cost_per_token_above_200_tokens") diff --git a/tests/test_litellm/llms/volcengine/test_cost_calculator.py b/tests/test_litellm/llms/volcengine/test_cost_calculator.py index 9834d5ee417c..27970e8c3367 100644 --- a/tests/test_litellm/llms/volcengine/test_cost_calculator.py +++ b/tests/test_litellm/llms/volcengine/test_cost_calculator.py @@ -152,6 +152,50 @@ def test_seed_1_8_cached_tokens_use_cache_rate_but_still_select_input_tier() -> assert math.isclose(prompt_cost, expected, rel_tol=1e-12) +def test_output_only_usage_uses_first_pricing_tier() -> None: + usage = Usage(prompt_tokens=0, completion_tokens=100) + _, completion_cost = volcengine_cost_per_token( + model="doubao-seed-2-0-lite-260215", + usage=usage, + ) + + model_info = litellm.get_model_info( + model="doubao-seed-2-0-lite-260215", + custom_llm_provider="volcengine", + ) + first_tier = model_info["tiered_pricing"][0] + assert math.isclose( + completion_cost, + 100 * first_tier["output_cost_per_token"], + rel_tol=1e-12, + ) + + +def test_flat_pricing_fallback(monkeypatch: pytest.MonkeyPatch) -> None: + model_info = { + "input_cost_per_token": 1e-6, + "output_cost_per_token": 2e-6, + "cache_read_input_token_cost": 2e-7, + } + monkeypatch.setattr( + "litellm.llms.volcengine.cost_calculator.get_model_info", + lambda **_: model_info, + ) + usage = Usage( + prompt_tokens=100, + completion_tokens=50, + prompt_tokens_details=PromptTokensDetailsWrapper(cached_tokens=20), + ) + + prompt_cost, completion_cost = volcengine_cost_per_token( + model="flat-priced-model", + usage=usage, + ) + + assert math.isclose(prompt_cost, (80 * 1e-6) + (20 * 2e-7), rel_tol=1e-12) + assert math.isclose(completion_cost, 50 * 2e-6, rel_tol=1e-12) + + def test_top_level_cost_calculator_routes_volcengine_to_tiered_calculator() -> None: prompt_cost, completion_cost = litellm.cost_per_token( model="doubao-seed-2-0-lite-260215", diff --git a/tests/test_litellm/test_utils.py b/tests/test_litellm/test_utils.py index b22e69f09420..e5cfe16482ad 100644 --- a/tests/test_litellm/test_utils.py +++ b/tests/test_litellm/test_utils.py @@ -1000,6 +1000,7 @@ def test_aaamodel_prices_and_context_window_json_is_valid(): }, "input_cost_per_token": {"type": "number"}, "output_cost_per_token": {"type": "number"}, + "output_cost_per_token_above_200_tokens": {"type": "number"}, "cache_read_input_token_cost": {"type": "number"}, "output_cost_per_reasoning_token": {"type": "number"}, "max_results_range": {