diff --git a/litellm/litellm_core_utils/llm_cost_calc/utils.py b/litellm/litellm_core_utils/llm_cost_calc/utils.py index c039f0f43ee..33bf546c239 100644 --- a/litellm/litellm_core_utils/llm_cost_calc/utils.py +++ b/litellm/litellm_core_utils/llm_cost_calc/utils.py @@ -445,6 +445,7 @@ class PromptTokensDetailsResult(TypedDict): text_tokens: int audio_tokens: int image_tokens: int + video_tokens: int character_count: int image_count: int video_length_seconds: float @@ -473,6 +474,7 @@ def _parse_prompt_tokens_details(usage: Usage) -> PromptTokensDetailsResult: ) audio_tokens = cast(Optional[int], getattr(usage.prompt_tokens_details, "audio_tokens", 0)) or 0 image_tokens = cast(Optional[int], getattr(usage.prompt_tokens_details, "image_tokens", 0)) or 0 + video_tokens = _coerce_token_count(getattr(usage.prompt_tokens_details, "video_tokens", 0)) character_count = ( cast( Optional[int], @@ -503,6 +505,7 @@ def _parse_prompt_tokens_details(usage: Usage) -> PromptTokensDetailsResult: text_tokens=text_tokens, audio_tokens=audio_tokens, image_tokens=image_tokens, + video_tokens=video_tokens, character_count=character_count, image_count=image_count, video_length_seconds=float(video_length_seconds), @@ -515,6 +518,7 @@ class CompletionTokensDetailsResult(TypedDict): text_tokens: int reasoning_tokens: int image_tokens: int + video_tokens: int def _parse_completion_tokens_details(usage: Usage) -> CompletionTokensDetailsResult: @@ -546,12 +550,14 @@ def _parse_completion_tokens_details(usage: Usage) -> CompletionTokensDetailsRes ) or 0 ) + video_tokens = _coerce_token_count(getattr(usage.completion_tokens_details, "video_tokens", 0)) return CompletionTokensDetailsResult( audio_tokens=audio_tokens, text_tokens=text_tokens, reasoning_tokens=reasoning_tokens, image_tokens=image_tokens, + video_tokens=video_tokens, ) @@ -586,6 +592,13 @@ def _calculate_input_cost( image_token_cost_key = "input_cost_per_token" prompt_cost += calculate_cost_component(model_info, image_token_cost_key, prompt_tokens_details["image_tokens"]) + ### VIDEO TOKEN COST + if prompt_tokens_details["video_tokens"]: + video_token_cost_key = "input_cost_per_video_token" + if model_info.get(video_token_cost_key) is None: + video_token_cost_key = "input_cost_per_token" + prompt_cost += calculate_cost_component(model_info, video_token_cost_key, prompt_tokens_details["video_tokens"]) + ### CACHE WRITING COST - Now uses tiered pricing if ( prompt_tokens_details["cache_creation_tokens"] @@ -698,6 +711,7 @@ def generic_cost_per_token( text_tokens=usage.prompt_tokens, audio_tokens=0, image_tokens=0, + video_tokens=0, character_count=0, image_count=0, video_length_seconds=0.0, @@ -716,13 +730,14 @@ def generic_cost_per_token( audio_tokens = prompt_tokens_details["audio_tokens"] cache_creation = prompt_tokens_details["cache_creation_tokens"] image_tokens = prompt_tokens_details["image_tokens"] + video_tokens = prompt_tokens_details["video_tokens"] # Check for double-counting: sum of details > prompt_tokens means overlap - total_details = text_tokens + cache_hit + audio_tokens + cache_creation + image_tokens + total_details = text_tokens + cache_hit + audio_tokens + cache_creation + image_tokens + video_tokens has_double_counting = cache_hit > 0 and total_details > usage.prompt_tokens if (text_tokens == 0 and prompt_tokens_details["image_count"] == 0) or has_double_counting: - text_tokens = usage.prompt_tokens - cache_hit - audio_tokens - cache_creation - image_tokens + text_tokens = usage.prompt_tokens - cache_hit - audio_tokens - cache_creation - image_tokens - video_tokens # Clamp to zero: inconsistent streaming usage if text_tokens < 0: text_tokens = 0 @@ -751,6 +766,7 @@ def generic_cost_per_token( audio_tokens = 0 reasoning_tokens = 0 image_tokens = 0 + video_tokens = 0 is_text_tokens_total = False if usage.completion_tokens_details is not None: completion_tokens_details = _parse_completion_tokens_details(usage) @@ -758,19 +774,20 @@ def generic_cost_per_token( text_tokens = completion_tokens_details["text_tokens"] reasoning_tokens = completion_tokens_details["reasoning_tokens"] image_tokens = completion_tokens_details["image_tokens"] + video_tokens = completion_tokens_details["video_tokens"] # Handle text_tokens calculation: # 1. If text_tokens is explicitly provided and > 0, use it - # 2. If there's a breakdown (reasoning/audio/image tokens), calculate text_tokens as the remainder + # 2. If there's a breakdown (reasoning/audio/image/video tokens), calculate text_tokens as the remainder # 3. If no breakdown at all, assume all completion_tokens are text_tokens - has_token_breakdown = image_tokens > 0 or audio_tokens > 0 or reasoning_tokens > 0 + has_token_breakdown = image_tokens > 0 or audio_tokens > 0 or reasoning_tokens > 0 or video_tokens > 0 if text_tokens == 0: if has_token_breakdown: # Calculate text tokens as remainder when we have a breakdown # This handles cases like OpenAI's reasoning models where text_tokens isn't provided text_tokens = max( 0, - usage.completion_tokens - reasoning_tokens - audio_tokens - image_tokens, + usage.completion_tokens - reasoning_tokens - audio_tokens - image_tokens - video_tokens, ) else: # No breakdown at all, all tokens are text tokens @@ -803,6 +820,14 @@ def generic_cost_per_token( ) completion_cost += float(image_tokens) * _output_cost_per_image_token + ## VIDEO COST + if not is_text_tokens_total and video_tokens and video_tokens > 0: + _output_cost_per_video_token = _get_cost_per_unit(model_info, "output_cost_per_video_token", None) + _output_cost_per_video_token = ( + _output_cost_per_video_token if _output_cost_per_video_token is not None else completion_base_cost + ) + completion_cost += float(video_tokens) * _output_cost_per_video_token + ## REGIONAL DATA-RESIDENCY UPLIFT # Applied as a flat multiplier across all token costs for the request # when the upstream is a regionalized OpenAI host (eu./us.api.openai.com). diff --git a/litellm/model_prices_and_context_window_backup.json b/litellm/model_prices_and_context_window_backup.json index a6f59abc140..c51cfcd81cf 100644 --- a/litellm/model_prices_and_context_window_backup.json +++ b/litellm/model_prices_and_context_window_backup.json @@ -19600,6 +19600,39 @@ }, "web_search_billing_unit": "per_query" }, + "gemini/gemini-omni-flash-preview": { + "input_cost_per_audio_token": 1.5e-06, + "input_cost_per_token": 1.5e-06, + "litellm_provider": "gemini", + "max_input_tokens": 1048576, + "max_output_tokens": 65535, + "max_tokens": 65535, + "mode": "chat", + "output_cost_per_reasoning_token": 9e-06, + "output_cost_per_token": 9e-06, + "output_cost_per_video_token": 1.75e-05, + "rpm": 2000, + "source": "https://ai.google.dev/gemini-api/docs/pricing", + "supported_endpoints": [ + "/v1/chat/completions" + ], + "supported_modalities": [ + "text", + "image", + "audio", + "video" + ], + "supported_output_modalities": [ + "text", + "video" + ], + "supports_audio_input": true, + "supports_reasoning": true, + "supports_system_messages": true, + "supports_video_input": true, + "supports_vision": true, + "tpm": 800000 + }, "gemini/gemini-3.1-pro-preview": { "cache_read_input_token_cost": 2e-07, "cache_read_input_token_cost_above_200k_tokens": 4e-07, @@ -19764,6 +19797,37 @@ }, "web_search_billing_unit": "per_query" }, + "gemini-omni-flash-preview": { + "input_cost_per_audio_token": 1.5e-06, + "input_cost_per_token": 1.5e-06, + "litellm_provider": "vertex_ai-language-models", + "max_input_tokens": 1048576, + "max_output_tokens": 65535, + "max_tokens": 65535, + "mode": "chat", + "output_cost_per_reasoning_token": 9e-06, + "output_cost_per_token": 9e-06, + "output_cost_per_video_token": 1.75e-05, + "source": "https://docs.cloud.google.com/gemini-enterprise-agent-platform/models/gemini/omni-flash-preview", + "supported_endpoints": [ + "/v1/chat/completions" + ], + "supported_modalities": [ + "text", + "image", + "audio", + "video" + ], + "supported_output_modalities": [ + "text", + "video" + ], + "supports_audio_input": true, + "supports_reasoning": true, + "supports_system_messages": true, + "supports_video_input": true, + "supports_vision": true + }, "gemini-3.5-flash": { "cache_read_input_token_cost": 1.5e-07, "input_cost_per_audio_token": 1e-06, diff --git a/litellm/types/utils.py b/litellm/types/utils.py index 90ea99ceb23..8799b622ad1 100644 --- a/litellm/types/utils.py +++ b/litellm/types/utils.py @@ -209,6 +209,7 @@ class ModelInfoBase(ProviderSpecificModelInfo, total=False): input_cost_per_query: Optional[float] # only for rerank models input_cost_per_image: Optional[float] # only for vertex ai models input_cost_per_image_token: Optional[float] # for gpt-image-1 and similar models + input_cost_per_video_token: Optional[float] # for gemini omni models with video input input_cost_per_audio_per_second: Optional[float] # only for vertex ai models input_cost_per_video_per_second: Optional[float] # only for vertex ai models input_cost_per_second: Optional[float] # for OpenAI Speech models @@ -234,6 +235,7 @@ class ModelInfoBase(ProviderSpecificModelInfo, total=False): output_cost_per_character_above_128k_tokens: Optional[float] # only for vertex ai models output_cost_per_image: Optional[float] output_cost_per_image_token: Optional[float] + output_cost_per_video_token: Optional[float] # for gemini omni models with video output output_vector_size: Optional[int] output_cost_per_reasoning_token: Optional[float] output_cost_per_video_per_second: Optional[float] # only for vertex ai models @@ -3046,6 +3048,7 @@ class CustomPricingLiteLLMParams(BaseModel): output_cost_per_character_above_128k_tokens: Optional[float] = None output_cost_per_image: Optional[float] = None output_cost_per_image_token: Optional[float] = None + output_cost_per_video_token: Optional[float] = None output_cost_per_reasoning_token: Optional[float] = None output_cost_per_video_per_second: Optional[float] = None output_cost_per_audio_per_second: Optional[float] = None @@ -3055,6 +3058,7 @@ class CustomPricingLiteLLMParams(BaseModel): cache_read_input_token_cost_above_272k_tokens: Optional[float] = None cache_read_input_token_cost_above_512k_tokens: Optional[float] = None input_cost_per_image_token: Optional[float] = None + input_cost_per_video_token: Optional[float] = None input_cost_per_token_above_272k_tokens: Optional[float] = None input_cost_per_token_above_512k_tokens: Optional[float] = None output_cost_per_token_above_272k_tokens: Optional[float] = None diff --git a/litellm/utils.py b/litellm/utils.py index 18b89ee0d13..0636d3683b7 100644 --- a/litellm/utils.py +++ b/litellm/utils.py @@ -5437,6 +5437,7 @@ def _get_model_info_helper( input_cost_per_second=_model_info.get("input_cost_per_second", None), input_cost_per_audio_token=_model_info.get("input_cost_per_audio_token", None), input_cost_per_image_token=_model_info.get("input_cost_per_image_token", None), + input_cost_per_video_token=_model_info.get("input_cost_per_video_token", None), input_cost_per_image=_model_info.get("input_cost_per_image", None), input_cost_per_audio_per_second=_model_info.get("input_cost_per_audio_per_second", None), input_cost_per_video_per_second=_model_info.get("input_cost_per_video_per_second", None), @@ -5480,6 +5481,7 @@ def _get_model_info_helper( output_cost_per_video_per_second=_model_info.get("output_cost_per_video_per_second", None), output_cost_per_image=_model_info.get("output_cost_per_image", None), output_cost_per_image_token=_model_info.get("output_cost_per_image_token", None), + output_cost_per_video_token=_model_info.get("output_cost_per_video_token", None), output_vector_size=_model_info.get("output_vector_size", None), citation_cost_per_token=_model_info.get("citation_cost_per_token", None), tiered_pricing=_model_info.get("tiered_pricing", None), diff --git a/model_prices_and_context_window.json b/model_prices_and_context_window.json index 08a452be844..177c887d649 100644 --- a/model_prices_and_context_window.json +++ b/model_prices_and_context_window.json @@ -19678,6 +19678,39 @@ }, "web_search_billing_unit": "per_query" }, + "gemini/gemini-omni-flash-preview": { + "input_cost_per_audio_token": 1.5e-06, + "input_cost_per_token": 1.5e-06, + "litellm_provider": "gemini", + "max_input_tokens": 1048576, + "max_output_tokens": 65535, + "max_tokens": 65535, + "mode": "chat", + "output_cost_per_reasoning_token": 9e-06, + "output_cost_per_token": 9e-06, + "output_cost_per_video_token": 1.75e-05, + "rpm": 2000, + "source": "https://ai.google.dev/gemini-api/docs/pricing", + "supported_endpoints": [ + "/v1/chat/completions" + ], + "supported_modalities": [ + "text", + "image", + "audio", + "video" + ], + "supported_output_modalities": [ + "text", + "video" + ], + "supports_audio_input": true, + "supports_reasoning": true, + "supports_system_messages": true, + "supports_video_input": true, + "supports_vision": true, + "tpm": 800000 + }, "gemini/gemini-3.1-pro-preview": { "cache_read_input_token_cost": 2e-07, "cache_read_input_token_cost_above_200k_tokens": 4e-07, @@ -19842,6 +19875,37 @@ }, "web_search_billing_unit": "per_query" }, + "gemini-omni-flash-preview": { + "input_cost_per_audio_token": 1.5e-06, + "input_cost_per_token": 1.5e-06, + "litellm_provider": "vertex_ai-language-models", + "max_input_tokens": 1048576, + "max_output_tokens": 65535, + "max_tokens": 65535, + "mode": "chat", + "output_cost_per_reasoning_token": 9e-06, + "output_cost_per_token": 9e-06, + "output_cost_per_video_token": 1.75e-05, + "source": "https://docs.cloud.google.com/gemini-enterprise-agent-platform/models/gemini/omni-flash-preview", + "supported_endpoints": [ + "/v1/chat/completions" + ], + "supported_modalities": [ + "text", + "image", + "audio", + "video" + ], + "supported_output_modalities": [ + "text", + "video" + ], + "supports_audio_input": true, + "supports_reasoning": true, + "supports_system_messages": true, + "supports_video_input": true, + "supports_vision": true + }, "gemini-3.5-flash": { "cache_read_input_token_cost": 1.5e-07, "input_cost_per_audio_token": 1e-06, diff --git a/tests/test_litellm/litellm_core_utils/llm_cost_calc/test_llm_cost_calc_utils.py b/tests/test_litellm/litellm_core_utils/llm_cost_calc/test_llm_cost_calc_utils.py index ca23e61352e..b156faf3ea6 100644 --- a/tests/test_litellm/litellm_core_utils/llm_cost_calc/test_llm_cost_calc_utils.py +++ b/tests/test_litellm/litellm_core_utils/llm_cost_calc/test_llm_cost_calc_utils.py @@ -270,6 +270,105 @@ def test_image_tokens_fallback_to_base_cost(): assert round(completion_cost, 12) == round(expected_completion_cost, 12) +def test_video_output_tokens_gemini_omni_flash_preview(): + """Video output tokens are billed at output_cost_per_video_token, not the text rate and not zero.""" + model = "gemini-omni-flash-preview" + os.environ["LITELLM_LOCAL_MODEL_COST_MAP"] = "True" + litellm.model_cost = litellm.get_model_cost_map(url="") + + text_tokens = 100 + video_tokens = 46336 + usage = Usage( + completion_tokens=text_tokens + video_tokens, + prompt_tokens=20, + total_tokens=20 + text_tokens + video_tokens, + completion_tokens_details=CompletionTokensDetailsWrapper( + text_tokens=text_tokens, + video_tokens=video_tokens, + ), + prompt_tokens_details=PromptTokensDetailsWrapper(text_tokens=20), + ) + model_cost_map = litellm.model_cost[f"gemini/{model}"] + assert model_cost_map["input_cost_per_token"] == 1.5e-06 + assert model_cost_map["output_cost_per_token"] == 9e-06 + assert model_cost_map["output_cost_per_video_token"] == 1.75e-05 + + prompt_cost, completion_cost = generic_cost_per_token( + model=model, + usage=usage, + custom_llm_provider="gemini", + ) + + assert round(prompt_cost, 10) == round( + model_cost_map["input_cost_per_token"] * usage.prompt_tokens, + 10, + ) + assert round(completion_cost, 10) == round( + (model_cost_map["output_cost_per_token"] * text_tokens) + + (model_cost_map["output_cost_per_video_token"] * video_tokens), + 10, + ) + + +def test_video_input_tokens_gemini_omni_flash_preview(): + """Video input tokens are billed at the standard input rate instead of being dropped.""" + model = "gemini-omni-flash-preview" + os.environ["LITELLM_LOCAL_MODEL_COST_MAP"] = "True" + litellm.model_cost = litellm.get_model_cost_map(url="") + + usage = Usage( + completion_tokens=10, + prompt_tokens=10050, + total_tokens=10060, + completion_tokens_details=CompletionTokensDetailsWrapper(text_tokens=10), + prompt_tokens_details=PromptTokensDetailsWrapper(text_tokens=50, video_tokens=10000), + ) + model_cost_map = litellm.model_cost[f"gemini/{model}"] + + prompt_cost, _ = generic_cost_per_token( + model=model, + usage=usage, + custom_llm_provider="gemini", + ) + + assert round(prompt_cost, 10) == round( + model_cost_map["input_cost_per_token"] * usage.prompt_tokens, + 10, + ) + + +def test_video_tokens_fallback_to_base_cost(): + """Video output tokens fall back to the base output rate when output_cost_per_video_token is not set.""" + from unittest.mock import patch + + mock_model_info = { + "input_cost_per_token": 1e-6, + "output_cost_per_token": 2e-6, + } + + usage = Usage( + completion_tokens=1720, + prompt_tokens=14, + total_tokens=1734, + completion_tokens_details=CompletionTokensDetailsWrapper( + text_tokens=600, + video_tokens=1120, + ), + prompt_tokens_details=PromptTokensDetailsWrapper(text_tokens=14), + ) + + with patch( + "litellm.litellm_core_utils.llm_cost_calc.utils.get_model_info", + return_value=mock_model_info, + ): + prompt_cost, completion_cost = generic_cost_per_token( + model="test-model", usage=usage, custom_llm_provider="gemini" + ) + + assert round(prompt_cost, 12) == round(14 * 1e-6, 12) + assert round(completion_cost, 12) == round((600 + 1120) * 2e-6, 12) + + def test_generic_cost_per_token_above_200k_tokens(): # gemini-2.5-pro-exp-03-25 was removed; gemini-2.5-pro has same above-200k pricing model = "gemini-2.5-pro" @@ -1086,6 +1185,7 @@ def test_cache_writing_cost_with_zero_creation_tokens_and_ephemeral_details(): "text_tokens": 0, "audio_tokens": 0, "image_tokens": 0, + "video_tokens": 0, "character_count": 0, "image_count": 0, "video_length_seconds": 0.0, diff --git a/tests/test_litellm/test_utils.py b/tests/test_litellm/test_utils.py index 1a5c419754b..6d515ecdc73 100644 --- a/tests/test_litellm/test_utils.py +++ b/tests/test_litellm/test_utils.py @@ -615,6 +615,8 @@ def validate_model_cost_values(model_data, exceptions=None): "input_cost_per_audio_token", "output_cost_per_audio_token", "output_cost_per_image_token", + "input_cost_per_video_token", + "output_cost_per_video_token", "input_cost_per_audio_per_second", "input_cost_per_video_per_second", "input_cost_per_token_above_128k_tokens", @@ -732,6 +734,7 @@ def test_aaamodel_prices_and_context_window_json_is_valid(): "input_cost_per_image": {"type": "number"}, "input_cost_per_image_above_128k_tokens": {"type": "number"}, "input_cost_per_image_token": {"type": "number"}, + "input_cost_per_video_token": {"type": "number"}, "input_cost_per_token_above_200k_tokens": {"type": "number"}, "input_cost_per_token_above_256k_tokens": {"type": "number"}, "input_cost_per_token_above_272k_tokens": {"type": "number"}, @@ -807,6 +810,7 @@ def test_aaamodel_prices_and_context_window_json_is_valid(): "output_cost_per_character_above_128k_tokens": {"type": "number"}, "output_cost_per_image": {"type": "number"}, "output_cost_per_image_token": {"type": "number"}, + "output_cost_per_video_token": {"type": "number"}, "output_cost_per_pixel": {"type": "number"}, "output_cost_per_second": {"type": "number"}, "output_cost_per_second_1080p": {"type": "number"}, diff --git a/ui/litellm-dashboard/src/lib/http/schema.d.ts b/ui/litellm-dashboard/src/lib/http/schema.d.ts index 5629cd6c8d4..80e9b41852f 100644 --- a/ui/litellm-dashboard/src/lib/http/schema.d.ts +++ b/ui/litellm-dashboard/src/lib/http/schema.d.ts @@ -25706,6 +25706,8 @@ export interface components { input_cost_per_video_per_second_above_15s_interval?: number | null; /** Input Cost Per Video Per Second Above 8S Interval */ input_cost_per_video_per_second_above_8s_interval?: number | null; + /** Input Cost Per Video Token */ + input_cost_per_video_token?: number | null; /** Itpm */ itpm?: number | null; /** Litellm Credential Name */ @@ -25787,6 +25789,8 @@ export interface components { output_cost_per_token_priority?: number | null; /** Output Cost Per Video Per Second */ output_cost_per_video_per_second?: number | null; + /** Output Cost Per Video Token */ + output_cost_per_video_token?: number | null; /** Output Vector Size */ output_vector_size?: number | null; /** Quality Router Config */ @@ -33536,6 +33540,8 @@ export interface components { input_cost_per_video_per_second_above_15s_interval?: number | null; /** Input Cost Per Video Per Second Above 8S Interval */ input_cost_per_video_per_second_above_8s_interval?: number | null; + /** Input Cost Per Video Token */ + input_cost_per_video_token?: number | null; /** Itpm */ itpm?: number | null; /** Litellm Credential Name */ @@ -33617,6 +33623,8 @@ export interface components { output_cost_per_token_priority?: number | null; /** Output Cost Per Video Per Second */ output_cost_per_video_per_second?: number | null; + /** Output Cost Per Video Token */ + output_cost_per_video_token?: number | null; /** Output Vector Size */ output_vector_size?: number | null; /** Quality Router Config */