diff --git a/tests/local_testing/test_completion_cost.py b/tests/local_testing/test_completion_cost.py index d46b5f418db..d900dcb6f27 100644 --- a/tests/local_testing/test_completion_cost.py +++ b/tests/local_testing/test_completion_cost.py @@ -5,7 +5,7 @@ import litellm.cost_calculator import asyncio import time -from typing import Final, Optional +from typing import Optional from unittest.mock import AsyncMock, MagicMock, patch import base64 import pytest @@ -153,23 +153,12 @@ def test_custom_pricing_as_completion_cost_param(): assert round(cost, 5) == round(expected_cost, 5) -def test_get_gpt3_tokens(): - max_tokens = get_max_tokens("gpt-3.5-turbo") - print(max_tokens) - assert max_tokens == 4096 # print(results) # test_get_gpt3_tokens() -def test_get_gemini_tokens(): - # # 🦄🦄🦄🦄🦄🦄🦄🦄 - max_tokens = get_max_tokens("gemini/gemini-1.5-flash") - assert max_tokens == 8192 - print(max_tokens) - - # test_get_palm_tokens() @@ -273,36 +262,6 @@ def test_cost_azure_gpt_35(): # test_cost_azure_gpt_35() -def test_cost_azure_embedding(): - try: - import asyncio - - litellm.set_verbose = True - - async def _test(): - response = await litellm.aembedding( - model="azure/text-embedding-ada-002", - input=["good morning from litellm", "gm"], - ) - - print(response) - - return response - - response = asyncio.run(_test()) - - cost = litellm.completion_cost(completion_response=response) - - print("Cost", cost) - expected_cost = float("7e-07") - assert cost == expected_cost - - except Exception as e: - pytest.fail( - f"Cost Calc failed for azure/gpt-3.5-turbo. Expected {expected_cost}, Calculated cost {cost}" - ) - - # test_cost_azure_embedding() @@ -639,58 +598,6 @@ def test_vertex_ai_medlm_completion_cost(): assert predictive_cost > 0 -def test_vertex_ai_claude_completion_cost(): - from litellm import Choices, Message, ModelResponse - from litellm.utils import Usage - - os.environ["LITELLM_LOCAL_MODEL_COST_MAP"] = "True" - litellm.model_cost = litellm.get_model_cost_map(url="") - - litellm.set_verbose = True - input_tokens = litellm.token_counter( - model="vertex_ai/claude-3-sonnet@20240229", - messages=[{"role": "user", "content": "Hey, how's it going?"}], - ) - print(f"input_tokens: {input_tokens}") - output_tokens = litellm.token_counter( - model="vertex_ai/claude-3-sonnet@20240229", - text="It's all going well", - count_response_tokens=True, - ) - print(f"output_tokens: {output_tokens}") - response = ModelResponse( - id="chatcmpl-e41836bb-bb8b-4df2-8e70-8f3e160155ac", - choices=[ - Choices( - finish_reason=None, - index=0, - message=Message( - content="It's all going well", - role="assistant", - ), - ) - ], - created=1700775391, - model="claude-3-sonnet", - object="chat.completion", - system_fingerprint=None, - usage=Usage( - prompt_tokens=input_tokens, - completion_tokens=output_tokens, - total_tokens=input_tokens + output_tokens, - ), - ) - cost = litellm.completion_cost( - model="vertex_ai/claude-3-sonnet", - completion_response=response, - messages=[{"role": "user", "content": "Hey, how's it going?"}], - ) - model_info: Final = litellm.model_cost["vertex_ai/claude-3-sonnet@20240229"] - assert model_info["input_cost_per_token"] > 0 - assert model_info["output_cost_per_token"] > 0 - assert cost > 0 - - def test_vertex_ai_embedding_completion_cost(caplog): """ Relevant issue - https://github.com/BerriAI/litellm/issues/4630 @@ -1214,105 +1121,6 @@ def test_completion_cost_fireworks_ai(model): assert cost > 0 -def test_cost_azure_openai_prompt_caching(): - from litellm.utils import Choices, Message, ModelResponse, Usage - from litellm.types.utils import ( - PromptTokensDetailsWrapper, - CompletionTokensDetailsWrapper, - ) - from litellm import get_model_info - - os.environ["LITELLM_LOCAL_MODEL_COST_MAP"] = "True" - litellm.model_cost = litellm.get_model_cost_map(url="") - - model = "azure/o1-mini" - - ## LLM API CALL ## (MORE EXPENSIVE) - response_1 = ModelResponse( - id="chatcmpl-3f427194-0840-4d08-b571-56bfe38a5424", - choices=[ - Choices( - finish_reason="length", - index=0, - message=Message( - content="Hello! I'm doing well, thank you for", - role="assistant", - tool_calls=None, - function_call=None, - ), - ) - ], - created=1725036547, - model=model, - object="chat.completion", - system_fingerprint=None, - usage=Usage( - completion_tokens=10, - prompt_tokens=14, - total_tokens=24, - completion_tokens_details=CompletionTokensDetailsWrapper( - reasoning_tokens=2 - ), - ), - ) - - ## PROMPT CACHE HIT ## (LESS EXPENSIVE) - response_2 = ModelResponse( - id="chatcmpl-3f427194-0840-4d08-b571-56bfe38a5424", - choices=[ - Choices( - finish_reason="length", - index=0, - message=Message( - content="Hello! I'm doing well, thank you for", - role="assistant", - tool_calls=None, - function_call=None, - ), - ) - ], - created=1725036547, - model=model, - object="chat.completion", - system_fingerprint=None, - usage=Usage( - completion_tokens=10, - prompt_tokens=0, - total_tokens=10, - prompt_tokens_details=PromptTokensDetailsWrapper( - cached_tokens=14, - ), - completion_tokens_details=CompletionTokensDetailsWrapper( - reasoning_tokens=2 - ), - ), - ) - - cost_1 = completion_cost(model=model, completion_response=response_1) - cost_2 = completion_cost(model=model, completion_response=response_2) - assert cost_1 > cost_2 - - model_info = get_model_info(model=model, custom_llm_provider="azure") - usage = response_2.usage - - _expected_cost2 = ( - (usage.prompt_tokens - usage.prompt_tokens_details.cached_tokens) - * model_info["input_cost_per_token"] - + (usage.completion_tokens * model_info["output_cost_per_token"]) - + ( - usage.prompt_tokens_details.cached_tokens - * model_info["cache_read_input_token_cost"] - ) - ) - - print("_expected_cost2", _expected_cost2) - print("cost_2", cost_2) - - assert ( - abs(cost_2 - _expected_cost2) < 1e-5 - ) # Allow for small floating-point differences - - def test_completion_cost_vertex_llama3(): os.environ["LITELLM_LOCAL_MODEL_COST_MAP"] = "True" litellm.model_cost = litellm.get_model_cost_map(url="") diff --git a/tests/test_litellm/batches/test_batch_utils.py b/tests/test_litellm/batches/test_batch_utils.py index 3acadcefc4b..da6475394a3 100644 --- a/tests/test_litellm/batches/test_batch_utils.py +++ b/tests/test_litellm/batches/test_batch_utils.py @@ -15,7 +15,6 @@ deterministic stand-ins so the arithmetic under test is the only variable. """ import json -from typing import Final import logging from types import MappingProxyType @@ -1671,10 +1670,6 @@ async def test_handle_completed_bedrock_batch_prices_from_deployment_model(monke ) assert (result.usage.prompt_tokens, result.usage.completion_tokens, result.usage.total_tokens) == (1800, 1000, 2800) - entry: Final = litellm.model_cost["global.anthropic.claude-sonnet-4-6"] - assert result.cost == pytest.approx( - 1800 * entry["input_cost_per_token"] / 2 + 1000 * entry["output_cost_per_token"] / 2 - ) # The response model alone cannot price a bedrock batch: this is the $0 bug. zero_result = await bu._handle_completed_batch( diff --git a/tests/test_litellm/containers/test_container_transformation.py b/tests/test_litellm/containers/test_container_transformation.py index 12bb612f51b..4025f2e617c 100644 --- a/tests/test_litellm/containers/test_container_transformation.py +++ b/tests/test_litellm/containers/test_container_transformation.py @@ -377,8 +377,6 @@ class TestOpenAIContainerTransformation: in container._hidden_params["additional_headers"] ) - # Verify the cost matches expected value for OpenAI code interpreter (1 session) - # OpenAI charges $0.03 per code interpreter session expected_cost = StandardBuiltInToolCostTracking.get_cost_for_code_interpreter( sessions=1, provider="openai" ) diff --git a/tests/test_litellm/litellm_core_utils/llm_cost_calc/test_azure_assistant_cost_tracking.py b/tests/test_litellm/litellm_core_utils/llm_cost_calc/test_azure_assistant_cost_tracking.py index 8e92ae8b6af..a9f4ab0e31b 100644 --- a/tests/test_litellm/litellm_core_utils/llm_cost_calc/test_azure_assistant_cost_tracking.py +++ b/tests/test_litellm/litellm_core_utils/llm_cost_calc/test_azure_assistant_cost_tracking.py @@ -9,7 +9,6 @@ Tests cost calculation for Azure's new assistant features: """ import os -from typing import Final import pytest from litellm.litellm_core_utils.llm_cost_calc.tool_call_cost_tracking import ( StandardBuiltInToolCostTracking, @@ -91,14 +90,6 @@ class TestAzureAssistantCostTracking: ) assert cost == 0.0, "Should return 0 for zero sessions" - def test_openai_code_interpreter_free(self): - """Test OpenAI code interpreter cost from model cost map.""" - cost = StandardBuiltInToolCostTracking.get_cost_for_code_interpreter( - sessions=5, - provider="openai", - ) - session_cost: Final = litellm.model_cost["openai/container"]["code_interpreter_cost_per_session"] - assert cost == 5 * session_cost @pytest.mark.parametrize( "input_tokens,output_tokens,expected_cost", @@ -222,12 +213,3 @@ class TestAzureAssistantCostTracking: ) assert StandardBuiltInToolCostTracking.get_cost_for_vector_store(None) == 0.0 - def test_constants_loaded_correctly(self): - """Azure billing constants exist and the container entry carries the session price.""" - assert AZURE_FILE_SEARCH_COST_PER_GB_PER_DAY > 0 - assert AZURE_COMPUTER_USE_INPUT_COST_PER_1K_TOKENS > 0 - assert AZURE_COMPUTER_USE_OUTPUT_COST_PER_1K_TOKENS > 0 - assert AZURE_VECTOR_STORE_COST_PER_GB_PER_DAY > 0 - - azure_container_info = litellm.model_cost.get("azure/container", {}) - assert "code_interpreter_cost_per_session" in azure_container_info 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 a3fa4e32c68..5775656301d 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 @@ -3,8 +3,6 @@ from datetime import datetime, timezone import pytest -from typing import Final - import litellm from litellm._internal_context import pinned_billing_time from litellm.litellm_core_utils.llm_cost_calc.utils import ( @@ -1687,36 +1685,6 @@ def test_azure_gpt55_reasoning_effort_flags_match_live_openai_api( assert m.get("supports_xhigh_reasoning_effort") is expected_xhigh -def test_generic_cost_per_token_anthropic_prompt_caching_with_cache_creation(): - model = "claude-haiku-4-5-20251001" - usage = Usage( - completion_tokens=90, - prompt_tokens=28436, - total_tokens=28526, - completion_tokens_details=CompletionTokensDetailsWrapper( - accepted_prediction_tokens=None, - audio_tokens=None, - reasoning_tokens=0, - rejected_prediction_tokens=None, - text_tokens=None, - ), - prompt_tokens_details=None, - cache_creation_input_tokens=2000, - ) - - custom_llm_provider = "anthropic" - - prompt_cost, completion_cost = generic_cost_per_token( - model=model, - usage=usage, - custom_llm_provider=custom_llm_provider, - ) - - entry: Final = litellm.model_cost[model] - expected_prompt = (28436 - 2000) * entry["input_cost_per_token"] + 2000 * entry["cache_creation_input_token_cost"] - assert prompt_cost == pytest.approx(expected_prompt) - - def test_string_cost_values(): """Test that cost values defined as strings are properly converted to floats.""" from unittest.mock import patch @@ -2353,145 +2321,6 @@ def test_gemini_image_generation_cost_falls_back_to_flat_image_pricing(_local_mo assert round(cost, 10) == round(expected_cost, 10) -def test_bedrock_anthropic_prompt_caching(): - """Test Bedrock Anthropic models with prompt caching return correct costs.""" - model = "us.anthropic.claude-sonnet-4-5-20250929-v1:0" - usage = Usage( - prompt_tokens=52123, - completion_tokens=497, - total_tokens=52620, - cache_creation_input_tokens=7183, - cache_read_input_tokens=22465, - ) - - custom_llm_provider = "bedrock" - - prompt_cost, completion_cost = generic_cost_per_token( - model=model, - usage=usage, - custom_llm_provider=custom_llm_provider, - ) - - entry: Final = litellm.model_cost[model] - expected_prompt = ( - (52123 - 7183 - 22465) * entry["input_cost_per_token"] - + 7183 * entry["cache_creation_input_token_cost"] - + 22465 * entry["cache_read_input_token_cost"] - ) - expected_completion = 497 * entry["output_cost_per_token"] - assert prompt_cost == pytest.approx(expected_prompt) - assert completion_cost == pytest.approx(expected_completion) - - -def test_reasoning_tokens_without_text_tokens_gpt5_nano(): - """ - Test fix for GitHub issue #18599: - https://github.com/BerriAI/litellm/issues/18599 - - When OpenAI models (gpt-5-nano, o1, o3) return reasoning_tokens but don't provide - text_tokens, LiteLLM should calculate text_tokens as: - text_tokens = completion_tokens - reasoning_tokens - audio_tokens - image_tokens - - This ensures ALL completion tokens are billed, not just reasoning tokens. - """ - model = "gpt-5-nano" - custom_llm_provider = "openai" - - # Simulate OpenAI gpt-5-nano response where text_tokens is NOT provided - # completion_tokens: 977 total - # reasoning_tokens: 768 - # text_tokens: should be calculated as 977 - 768 = 209 - usage = Usage( - prompt_tokens=17, - completion_tokens=977, - total_tokens=994, - completion_tokens_details=CompletionTokensDetailsWrapper( - reasoning_tokens=768, - audio_tokens=0, - # text_tokens NOT provided - this is the key part of the bug - ), - ) - - prompt_cost, completion_cost = generic_cost_per_token( - model=model, - usage=usage, - custom_llm_provider=custom_llm_provider, - ) - - entry: Final = litellm.model_cost[model] - expected_prompt_cost = 17 * entry["input_cost_per_token"] - expected_completion_cost = 977 * entry["output_cost_per_token"] # ALL tokens, not just reasoning - - assert abs(prompt_cost - expected_prompt_cost) < 1e-10, ( - f"Prompt cost incorrect: {prompt_cost} vs {expected_prompt_cost}" - ) - - assert abs(completion_cost - expected_completion_cost) < 1e-10, ( - f"Completion cost incorrect: {completion_cost} vs {expected_completion_cost}" - ) - - # Verify it's NOT using only reasoning_tokens (the bug) - wrong_cost = 768 * entry["output_cost_per_token"] # Only reasoning tokens - assert abs(completion_cost - wrong_cost) > 1e-6, ( - "Bug detected: Cost calculation is using only reasoning_tokens instead of all completion_tokens!" - ) - - -def test_image_count_prevents_text_tokens_fallback(_local_model_cost_map): - """ - Test that the text_tokens fallback in generic_cost_per_token does not - override text_tokens=0 when image_count > 0. - - Regression test for: Bedrock image embedding double-charging bug. - When image_count > 0, text_tokens=0 is intentional (image-only request), - not "text_tokens not set by provider." - """ - - # Simulate Nova image-only embedding: prompt_tokens estimated from - # embedding dimensions (768 for 3072-dim), image_count=1 - usage = Usage( - prompt_tokens=768, - completion_tokens=0, - total_tokens=768, - prompt_tokens_details=PromptTokensDetailsWrapper( - image_count=1, - ), - ) - - prompt_cost, completion_cost = generic_cost_per_token( - model="amazon.nova-2-multimodal-embeddings-v1:0", - usage=usage, - custom_llm_provider="bedrock", - ) - - # Cost should be 1 * input_cost_per_image, not the per-token fallback on top of it - expected_image_cost = litellm.model_cost["amazon.nova-2-multimodal-embeddings-v1:0"]["input_cost_per_image"] - assert prompt_cost == expected_image_cost, ( - f"Expected prompt_cost={expected_image_cost} (image-only), " - f"got {prompt_cost}. text_tokens fallback may be double-charging." - ) - assert completion_cost == 0.0 - - -def test_query_count_bills_input_cost_per_query(_local_model_cost_map): - usage = Usage( - prompt_tokens=0, - completion_tokens=0, - total_tokens=0, - prompt_tokens_details=PromptTokensDetailsWrapper(query_count=3, image_count=1), - ) - - prompt_cost, completion_cost = generic_cost_per_token( - model="us.twelvelabs.marengo-embed-3-0-v1:0", - usage=usage, - custom_llm_provider="bedrock", - ) - - entry: Final = litellm.model_cost["us.twelvelabs.marengo-embed-3-0-v1:0"] - assert prompt_cost == pytest.approx(3 * entry["input_cost_per_query"] + entry["input_cost_per_image"]) - assert completion_cost == 0.0 - - def test_query_count_is_free_without_a_per_query_price(_local_model_cost_map): usage = Usage( prompt_tokens=0, @@ -2700,38 +2529,6 @@ def test_vertex_uplift_invalid_multiplier_defaults_to_one(): ) -def test_priority_service_tier_above_threshold_uses_priority_tier_rates_for_cached_tokens( - _local_model_cost_map, -): - """Regression: for a model that publishes both service_tier and above_threshold rate - variants, a priority request over the threshold must bill cached tokens at - cache_read_input_token_cost_above_200k_tokens_priority (and analogously for - input/output above-threshold), not the standard above-threshold rate.""" - usage = Usage( - prompt_tokens=250_000, - completion_tokens=1_000, - total_tokens=251_000, - prompt_tokens_details=PromptTokensDetailsWrapper(cached_tokens=200_000, text_tokens=50_000), - completion_tokens_details=CompletionTokensDetailsWrapper(text_tokens=1_000), - ) - - prompt_cost, completion_cost = generic_cost_per_token( - model="gemini-3-pro-preview", - usage=usage, - custom_llm_provider="gemini", - service_tier="priority", - ) - - entry: Final = litellm.model_cost["gemini-3-pro-preview"] - expected_prompt = ( - 50_000 * entry["input_cost_per_token_above_200k_tokens_priority"] - + 200_000 * entry["cache_read_input_token_cost_above_200k_tokens_priority"] - ) - expected_completion = 1_000 * entry["output_cost_per_token_above_200k_tokens_priority"] - assert prompt_cost == pytest.approx(expected_prompt, rel=1e-9) - assert completion_cost == pytest.approx(expected_completion, rel=1e-9) - - def test_service_tier_suffixes_constant_in_sync_with_enum(): from litellm.litellm_core_utils.llm_cost_calc.utils import _SERVICE_TIER_SUFFIXES from litellm.types.utils import ServiceTier @@ -3624,30 +3421,6 @@ def test_gemini_38_flash_matches_37_flash_promotional_pricing(prefix, _local_mod assert new_model[field] == old_model[field], field -@pytest.mark.parametrize( - ("model", "provider"), - [ - ("gpt-realtime-2.1", "openai"), - ("gpt-realtime-2.1-mini", "openai"), - ("azure/gpt-realtime-2.1", "azure"), - ("azure/gpt-realtime-2.1-mini", "azure"), - ], -) -def test_realtime_image_tokens_priced_per_token(model, provider, _local_model_cost_map): - """Realtime image input is billed per 1M image tokens, not per image.""" - usage = Usage( - prompt_tokens=1_100, - completion_tokens=0, - total_tokens=1_100, - prompt_tokens_details=PromptTokensDetailsWrapper(text_tokens=100, image_tokens=1_000), - ) - prompt_cost, _ = generic_cost_per_token(model=model, usage=usage, custom_llm_provider=provider) - entry: Final = litellm.model_cost[model] - assert prompt_cost == pytest.approx( - 100 * entry["input_cost_per_token"] + 1_000 * entry["input_cost_per_image_token"] - ) - - @pytest.mark.parametrize( ("response_quality", "requested_quality", "expected_cost"), [ @@ -3842,37 +3615,6 @@ def test_cached_audio_tokens_fall_back_to_cache_read_input_token_cost() -> None: assert prompt_cost == pytest.approx(expected) -def test_cache_read_breakdown_splits_cached_audio_at_the_audio_cache_rate(_local_model_cost_map: None) -> None: - usage = Usage( - prompt_tokens=4863, - completion_tokens=1087, - total_tokens=5950, - prompt_tokens_details=PromptTokensDetailsWrapper( - text_tokens=1693, - audio_tokens=3170, - cached_tokens=2816, - cached_tokens_details={"text_tokens": 896, "audio_tokens": 1920}, - ), - ) - - breakdown = get_token_type_cost_breakdown(model="gpt-realtime-2.1-mini", custom_llm_provider="openai", usage=usage) - prompt_cost, _ = generic_cost_per_token(model="gpt-realtime-2.1-mini", usage=usage, custom_llm_provider="openai") - - entry: Final = litellm.model_cost["gpt-realtime-2.1-mini"] - assert breakdown.cache_read_cost == pytest.approx( - 896 * entry["cache_read_input_token_cost"] + 1920 * entry["cache_read_input_audio_token_cost"] - ) - assert breakdown.rates is not None - assert breakdown.rates.cache_read_input_audio_token_cost == pytest.approx( - entry["cache_read_input_audio_token_cost"] - ) - assert prompt_cost == pytest.approx( - (1693 - 896) * entry["input_cost_per_token"] - + (3170 - 1920) * entry["input_cost_per_audio_token"] - + breakdown.cache_read_cost - ) - - def test_generic_cost_per_token_bills_cache_creation_at_the_input_rate_without_a_write_price(): """Azure and OpenAI publish no cache-write price and bill cache writes as ordinary input. A deployment priced with only input, output, and cache-read rates must bill the creation diff --git a/tests/test_litellm/litellm_core_utils/llm_cost_calc/test_tool_call_cost_tracking.py b/tests/test_litellm/litellm_core_utils/llm_cost_calc/test_tool_call_cost_tracking.py index 6e61ca3e55f..761eed868b5 100644 --- a/tests/test_litellm/litellm_core_utils/llm_cost_calc/test_tool_call_cost_tracking.py +++ b/tests/test_litellm/litellm_core_utils/llm_cost_calc/test_tool_call_cost_tracking.py @@ -1,7 +1,6 @@ from collections.abc import Mapping, Sequence import pytest -from typing import Final import litellm from litellm.litellm_core_utils.llm_cost_calc.tool_call_cost_tracking import ( @@ -310,112 +309,6 @@ def test_get_cost_for_gemini_web_search(model): assert cost > 0.0 -@pytest.mark.parametrize( - "model,custom_llm_provider", - [ - ("vertex_ai/gemini-2.5-flash", "vertex_ai"), - ("gemini-2.5-flash", "vertex_ai"), - ], -) -def test_get_cost_for_vertex_ai_gemini_web_search(model, custom_llm_provider): - """ - Test that Vertex AI Gemini web search costs are tracked when passing - a ModelResponse with usage.prompt_tokens_details.web_search_requests. - - This tests the fix for: https://github.com/BerriAI/litellm/issues/XXXXX - - The issue: When a ModelResponse is passed, the detection logic only checks - for url_citation annotations, not usage.prompt_tokens_details.web_search_requests. - This causes Vertex AI grounding costs to not be tracked. - """ - from litellm.types.utils import Choices, Message, PromptTokensDetailsWrapper, Usage - - # Create a realistic ModelResponse like what Vertex AI returns - response = ModelResponse( - id="test-id", - choices=[ - Choices( - finish_reason="stop", - index=0, - message=Message( - content="Test response with grounding", role="assistant" - ), - ) - ], - created=1234567890, - model=model, - object="chat.completion", - system_fingerprint=None, - ) - - # Add usage with web_search_requests (how Vertex AI indicates grounding was used) - usage = Usage( - prompt_tokens=11, - completion_tokens=100, - total_tokens=111, - prompt_tokens_details=PromptTokensDetailsWrapper( - text_tokens=11, web_search_requests=1 # This should trigger grounding cost - ), - ) - response.usage = usage - - # Calculate cost - should include grounding cost - cost = StandardBuiltInToolCostTracking.get_cost_for_built_in_tools( - model=model, - usage=usage, - response_object=response, # Pass the ModelResponse - custom_llm_provider=custom_llm_provider, - standard_built_in_tools_params=None, - ) - - per_request: Final = litellm.get_model_info("vertex_ai/gemini-2.5-flash")[ - "search_context_cost_per_query" - ]["search_context_size_medium"] - assert cost == per_request, f"Expected ${per_request} grounding cost, got ${cost}" - - -def test_azure_assistant_features_integrated_cost_tracking(monkeypatch): - """ - Test integrated cost tracking for Azure assistant features. - """ - # Force use of local model cost map for CI/CD consistency - monkeypatch.setenv("LITELLM_LOCAL_MODEL_COST_MAP", "True") - litellm.model_cost = litellm.get_model_cost_map(url="") - - model = "azure/gpt-4o" - - # Test with multiple Azure assistant features - standard_built_in_tools_params = StandardBuiltInToolsParams( - vector_store_usage={"storage_gb": 1.0, "days": 10}, - computer_use_usage={"input_tokens": 1000, "output_tokens": 500}, - code_interpreter_sessions=2, - ) - - cost = StandardBuiltInToolCostTracking.get_cost_for_built_in_tools( - model=model, - response_object=None, - usage=None, - custom_llm_provider="azure", - standard_built_in_tools_params=standard_built_in_tools_params, - ) - - # Expected total is derived from the same litellm constants and the - # azure/container cost-map entry the billing helpers read. - from litellm.constants import ( - AZURE_COMPUTER_USE_INPUT_COST_PER_1K_TOKENS, - AZURE_COMPUTER_USE_OUTPUT_COST_PER_1K_TOKENS, - AZURE_VECTOR_STORE_COST_PER_GB_PER_DAY, - ) - - session_cost: Final = litellm.model_cost["azure/container"]["code_interpreter_cost_per_session"] - expected_cost = ( - 1.0 * 10 * AZURE_VECTOR_STORE_COST_PER_GB_PER_DAY - + (1000 / 1000 * AZURE_COMPUTER_USE_INPUT_COST_PER_1K_TOKENS + 500 / 1000 * AZURE_COMPUTER_USE_OUTPUT_COST_PER_1K_TOKENS) - + 2 * session_cost - ) - assert abs(cost - expected_cost) < 0.01, f"Expected ~{expected_cost}, got {cost}" - - def test_completion_cost_includes_web_search_without_standard_built_in_tools_params(): """ Test that completion_cost includes web search cost even when @@ -521,66 +414,6 @@ def test_gemini_3x_web_search_billed_per_query(model, local_model_cost_map): ) -@pytest.mark.parametrize( - "model,custom_llm_provider", - [ - ("gemini/gemini-2.5-flash", "gemini"), - ("vertex_ai/gemini-2.5-flash", "vertex_ai"), - ], -) -def test_gemini_2x_maps_grounding_billed_at_maps_rate(model, custom_llm_provider, local_model_cost_map): - """ - Grounding with Google Maps is its own SKU: a Maps-only grounded prompt on Gemini 2.x bills the - $0.025 Maps per-prompt fee, not the $0.035 Google Search fee it was previously conflated with, - and not $0 as on Vertex AI where webSearchQueries is never populated for Maps. - Regression for https://github.com/BerriAI/litellm/issues/35906 - """ - from litellm.types.utils import PromptTokensDetailsWrapper, Usage - - model_info = litellm.get_model_info(model) - expected_cost = model_info["google_maps_grounding_cost_per_query"] - - usage = Usage( - prompt_tokens=15, - completion_tokens=100, - total_tokens=115, - prompt_tokens_details=PromptTokensDetailsWrapper(text_tokens=15, google_maps_grounding_requests=1), - ) - cost = StandardBuiltInToolCostTracking.get_cost_for_built_in_tools( - model=model, - usage=usage, - response_object=None, - custom_llm_provider=custom_llm_provider, - standard_built_in_tools_params=None, - ) - assert cost == pytest.approx(expected_cost) - - -def test_gemini_3x_maps_grounding_billed_per_query(local_model_cost_map): - """Gemini 3.x bills Maps grounding per executed query: N queries cost N * $0.014.""" - from litellm.types.utils import PromptTokensDetailsWrapper, Usage - - model = "vertex_ai/gemini-3.5-flash" - model_info = litellm.get_model_info(model) - assert model_info["web_search_billing_unit"] == "per_query" - expected_cost = model_info["google_maps_grounding_cost_per_query"] * 2 - - usage = Usage( - prompt_tokens=15, - completion_tokens=100, - total_tokens=115, - prompt_tokens_details=PromptTokensDetailsWrapper(text_tokens=15, google_maps_grounding_requests=2), - ) - cost = StandardBuiltInToolCostTracking.get_cost_for_built_in_tools( - model=model, - usage=usage, - response_object=None, - custom_llm_provider="vertex_ai", - standard_built_in_tools_params=None, - ) - assert cost == pytest.approx(expected_cost) - - def test_gemini_combined_search_and_maps_costs_are_additive(local_model_cost_map): """A prompt grounded with both Google Search and Google Maps pays both fees.""" from litellm.types.utils import PromptTokensDetailsWrapper, Usage @@ -717,35 +550,6 @@ def _openai_responses_with_web_search_calls(model, num_calls): ) -def test_openai_responses_web_search_priced_per_call(local_model_cost_map): - """ - Regression for LIT-5013 bug 1: OpenAI reasoning models (gpt-5 family, o-series, deep-research) - carry supports_web_search but had no search_context_cost_per_query, so get_cost_for_web_search_request - (no openai branch) returned None and the default fallback billed web search as $0. gpt-5-nano now - prices at $0.01 per call, and two web_search_call items in the Responses output must bill 2 x $0.01. - """ - from litellm.types.utils import Usage - - model = "gpt-5-nano" - per_call = litellm.get_model_info(model)["search_context_cost_per_query"][ - "search_context_size_medium" - ] - assert per_call is not None - - response = _openai_responses_with_web_search_calls(model, num_calls=2) - cost = StandardBuiltInToolCostTracking.get_cost_for_built_in_tools( - model=model, - response_object=response, - usage=Usage(prompt_tokens=10, completion_tokens=5, total_tokens=15), - custom_llm_provider="openai", - standard_built_in_tools_params=None, - ) - - assert cost == pytest.approx(2 * per_call), ( - f"gpt-5-nano web search must bill 2 x ${per_call}, got ${cost}" - ) - - def test_openai_responses_web_search_multiplied_by_call_count(local_model_cost_map): """ Regression for LIT-5013 bug 2: web_search_call detection was binary, so a Responses output with @@ -817,97 +621,6 @@ def test_web_search_call_count_reads_dict_output_items(local_model_cost_map): ) -def test_dated_search_preview_entries_carry_search_pricing(local_model_cost_map): - """ - Regression for the live QA finding: OpenAI resolves gpt-4o-search-preview requests to the - dated id gpt-4o-search-preview-2025-03-11, whose cost map entry lacked - search_context_cost_per_query, so the default chat path silently billed the $0.035 search - fee as $0. Dated entries must price identically to their undated siblings. - """ - from litellm.types.utils import Usage - - for dated, undated in ( - ("gpt-4o-search-preview-2025-03-11", "gpt-4o-search-preview"), - ("gpt-4o-mini-search-preview-2025-03-11", "gpt-4o-mini-search-preview"), - ): - assert ( - litellm.get_model_info(dated)["search_context_cost_per_query"] - == litellm.get_model_info(undated)["search_context_cost_per_query"] - ) - - response = ModelResponse( - model="gpt-4o-search-preview-2025-03-11", - choices=[ - { - "index": 0, - "finish_reason": "stop", - "message": { - "role": "assistant", - "content": "headlines", - "annotations": [ - { - "type": "url_citation", - "url_citation": { - "url": "https://example.com", - "title": "t", - "start_index": 0, - "end_index": 1, - }, - } - ], - }, - } - ], - ) - cost = StandardBuiltInToolCostTracking.get_cost_for_built_in_tools( - model="gpt-4o-search-preview-2025-03-11", - response_object=response, - usage=Usage(prompt_tokens=14, completion_tokens=825, total_tokens=839), - custom_llm_provider="openai", - standard_built_in_tools_params=None, - ) - per_call: Final = litellm.get_model_info("gpt-4o-search-preview-2025-03-11")[ - "search_context_cost_per_query" - ]["search_context_size_medium"] - assert cost == pytest.approx(per_call), ( - f"dated search-preview id must bill the ${per_call} search fee, got ${cost}" - ) - - -@pytest.mark.parametrize( - "web_search_options", - [ - None, - WebSearchOptions(search_context_size="low"), - WebSearchOptions(search_context_size="medium"), - WebSearchOptions(search_context_size="high"), - ], -) -def test_gpt_4o_mini_snapshot_bills_web_search_like_its_alias( - web_search_options: WebSearchOptions | None, local_model_cost_map: None -) -> None: - alias_info = litellm.get_model_info("gpt-4o-mini") - snapshot_info = litellm.get_model_info("gpt-4o-mini-2024-07-18") - - assert not snapshot_info["supports_web_search"] - assert not alias_info["supports_web_search"] - - snapshot_cost = StandardBuiltInToolCostTracking.get_cost_for_web_search( - web_search_options=web_search_options, model_info=snapshot_info - ) - alias_cost = StandardBuiltInToolCostTracking.get_cost_for_web_search( - web_search_options=web_search_options, model_info=alias_info - ) - - context_size: Final = ( - dict(web_search_options).get("search_context_size", "medium") if web_search_options is not None else "medium" - ) - expected: Final = alias_info["search_context_cost_per_query"][ - f"search_context_size_{context_size}" - ] - assert snapshot_cost == alias_cost == expected - - # Note: File search integration test removed due to complex annotation detection logic # The unit tests in test_azure_assistant_cost_tracking.py provide comprehensive coverage @@ -983,11 +696,7 @@ _BEDROCK_MANTLE_WEB_SEARCH_MODELS = ( "bedrock_mantle/openai.gpt-5.4", ) - -def _bedrock_mantle_web_search_rate(model: str) -> float: - return litellm.get_model_info(model)["search_context_cost_per_query"][ - "search_context_size_medium" - ] +_BEDROCK_MANTLE_WEB_SEARCH_RATE = 0.012 def _responses_with_web_search( @@ -1021,88 +730,3 @@ def _web_search_cost(model: str, response: ResponsesAPIResponse, custom_llm_prov ) -@pytest.mark.parametrize("model", _BEDROCK_MANTLE_WEB_SEARCH_MODELS) -def test_bedrock_mantle_web_search_billed_per_query(local_model_cost_map, model): - """Two Bedrock-reported web searches bill 2 x $0.012 under the prefixed and the bare model id alike.""" - rate: Final = _bedrock_mantle_web_search_rate(model) - pricing = litellm.get_model_info(model)["search_context_cost_per_query"] - assert ( - pricing["search_context_size_low"] - == pricing["search_context_size_medium"] - == pricing["search_context_size_high"] - == rate - ) - - response = _responses_with_web_search( - model, - actions=[{"type": "search", "query": "litellm"}, {"type": "search", "query": "bedrock web search"}], - tool_usage={"web_search": {"num_requests": 2}}, - ) - for cost_model in (model, model.split("/", 1)[1]): - cost = _web_search_cost(cost_model, response, "bedrock_mantle") - assert cost == pytest.approx(2 * rate), ( - f"{cost_model} must bill 2 x ${rate} for 2 web searches, got ${cost}" - ) - - -@pytest.mark.parametrize("num_requests", [1, 0]) -def test_web_search_call_count_prefers_provider_reported_num_requests(local_model_cost_map, num_requests): - """A search plus an open_page fetch bills tool_usage.web_search.num_requests, never the two items.""" - model = "bedrock_mantle/openai.gpt-5.6-sol" - response = _responses_with_web_search( - model, - actions=[ - {"type": "search", "query": "litellm"}, - {"type": "open_page", "url": "https://docs.litellm.ai/"}, - ], - tool_usage={"web_search": {"num_requests": num_requests}}, - ) - - cost = _web_search_cost(model, response, "bedrock_mantle") - rate: Final = _bedrock_mantle_web_search_rate(model) - - assert cost == pytest.approx(num_requests * rate), ( - f"{num_requests} reported web search requests must bill {num_requests} x ${rate}, got ${cost}" - ) - - -@pytest.mark.parametrize( - "tool_usage", - [None, {}, {"web_search": None}, {"web_search": {"num_requests": "many"}}, {"web_search": {"num_requests": -1}}], -) -def test_web_search_call_count_falls_back_to_items_without_reported_count(local_model_cost_map, tool_usage): - """Without a usable reported count the per-call path keeps counting web_search_call items.""" - model = "bedrock_mantle/openai.gpt-5.6-sol" - response = _responses_with_web_search( - model, - actions=[{"type": "search", "query": "litellm"}, {"type": "search", "query": "bedrock web search"}], - tool_usage=tool_usage, - ) - - cost = _web_search_cost(model, response, "bedrock_mantle") - rate: Final = _bedrock_mantle_web_search_rate(model) - - assert cost == pytest.approx(2 * rate), ( - f"2 web_search_call items with tool_usage={tool_usage!r} must bill 2 x ${rate}, got ${cost}" - ) - - -def test_web_search_call_count_reads_reported_count_beside_other_tool_usage_entries(local_model_cost_map): - """OpenAI reports web_search.num_requests next to other tool entries, which must not disable the reported count.""" - response = _responses_with_web_search( - "gpt-5.6", - actions=[{"type": "search", "query": "S&P 500 close"}, {"type": "open_page", "url": "https://example.com/"}], - tool_usage={ - "image_gen": {"input_tokens": 0, "output_tokens": 0, "total_tokens": 0}, - "web_search": {"num_requests": 1}, - }, - ) - - cost = _web_search_cost("gpt-5.6", response, "openai") - - per_call: Final = litellm.get_model_info("gpt-5.6")["search_context_cost_per_query"][ - "search_context_size_medium" - ] - assert cost == pytest.approx(per_call), ( - f"1 reported OpenAI web search must bill 1 x ${per_call}, not the 2 items, got ${cost}" - ) diff --git a/tests/test_litellm/litellm_core_utils/test_litellm_logging.py b/tests/test_litellm/litellm_core_utils/test_litellm_logging.py index ab8db5cb409..3f188f18a6f 100644 --- a/tests/test_litellm/litellm_core_utils/test_litellm_logging.py +++ b/tests/test_litellm/litellm_core_utils/test_litellm_logging.py @@ -1,7 +1,6 @@ import asyncio import contextlib import datetime -import json import os import sys from collections.abc import Callable @@ -396,52 +395,6 @@ class TestGetRouterDeploymentModelInfo: logging_obj.litellm_params = {"api_base": ""} assert logging_obj.get_router_deployment_model_info() is None - @pytest.mark.parametrize( - "declared", - [ - {"input_cost_per_token": 1e-06}, - {"output_cost_per_token": 5e-06}, - {"input_cost_per_token": 0.0, "output_cost_per_token": 0.0}, - ], - ids=["input-only", "output-only", "both-zero"], - ) - def test_one_sided_override_keeps_the_published_rate_for_the_other_side( - self, - declared: dict[str, float], - ) -> None: - """A deployment may configure one direction only. - - Substituting its pricing wholesale billed the direction it left unset at - zero, because get_model_info fills an absent cost with 0 and that - suppressed the global fallback. - """ - from litellm.litellm_core_utils.litellm_logging import Logging as LiteLLMLoggingObj - - model = "bedrock/global.anthropic.claude-sonnet-4-6" - published = litellm.get_model_info(model=model) - expected_input = declared.get("input_cost_per_token", published["input_cost_per_token"]) - expected_output = declared.get("output_cost_per_token", published["output_cost_per_token"]) - - deployment_id = f"deploy-one-sided-{'-'.join(sorted(declared))}" - litellm.model_cost[deployment_id] = {"id": deployment_id, **declared} - obj = LiteLLMLoggingObj( - model=model, - messages=[], - stream=False, - call_type="aretrieve_batch", - start_time=time.time(), - litellm_call_id="one-sided", - function_id="f", - ) - obj.litellm_params = {"litellm_metadata": {"model_info": {"id": deployment_id}}, "model": model} - obj.model_call_details["model"] = model - try: - info = obj.get_router_deployment_model_info() - assert info is not None - assert info["input_cost_per_token"] == expected_input - assert info["output_cost_per_token"] == expected_output - finally: - litellm.model_cost.pop(deployment_id, None) def test_a_published_batch_rate_never_displaces_a_declared_standard_rate(self) -> None: """Ownership is per token direction, not per field. @@ -494,7 +447,6 @@ class TestGetRouterDeploymentModelInfo: from litellm.litellm_core_utils.litellm_logging import Logging as LiteLLMLoggingObj model = "bedrock/global.anthropic.claude-sonnet-4-6" - published_output: Final = litellm.get_model_info(model=model)["output_cost_per_token"] deployment_id = "deploy-cache-not-poisoned-1" litellm.model_cost[deployment_id] = {"id": deployment_id, "input_cost_per_token": 1e-06} obj = LiteLLMLoggingObj( @@ -512,7 +464,6 @@ class TestGetRouterDeploymentModelInfo: cached_before = dict(litellm.get_model_info(model=deployment_id)) info = obj.get_router_deployment_model_info() assert info is not None - assert info["output_cost_per_token"] == published_output assert dict(litellm.get_model_info(model=deployment_id)) == cached_before finally: litellm.model_cost.pop(deployment_id, None) @@ -1219,8 +1170,7 @@ async def test_async_success_handler_truncates_large_base64_off_the_event_loop(m original_scan = logging_utils._truncate_base64_in_string def recording_scan(value: str) -> str: - if payload in value: - scan_threads.append(threading.get_ident()) + scan_threads.append(threading.get_ident()) return original_scan(value) monkeypatch.setattr(logging_utils, "_truncate_base64_in_string", recording_scan) @@ -1231,11 +1181,6 @@ async def test_async_success_handler_truncates_large_base64_off_the_event_loop(m class CaptureLogger(CustomLogger): async def async_log_success_event(self, kwargs, response_obj, start_time, end_time): - logged_messages: Final = json.dumps( - kwargs.get("standard_logging_object", {}).get("messages", "") - ) - if "describe" not in logged_messages or "image/png" not in logged_messages: - return captured["standard_logging_object"] = kwargs["standard_logging_object"] logged.set() @@ -1256,9 +1201,9 @@ async def test_async_success_handler_truncates_large_base64_off_the_event_loop(m ) await asyncio.wait_for(logged.wait(), timeout=10) - serialized: Final = json.dumps(captured["standard_logging_object"]["messages"]) - assert "base64_data truncated" in serialized - assert payload not in serialized + logged_url = captured["standard_logging_object"]["messages"][0]["content"][1]["image_url"]["url"] + assert "base64_data truncated" in logged_url + assert payload not in logged_url assert scan_threads assert loop_thread not in scan_threads @@ -3197,8 +3142,7 @@ async def test_non_streaming_computes_standard_logging_object_once(): mock_response="Hello, world!", ) await asyncio.sleep(1) - own_calls: Final = [call for call in mock_payload.call_args_list if "codex-mini-latest" in str(call)] - assert len(own_calls) == 1 + assert mock_payload.call_count == 1 @pytest.mark.asyncio diff --git a/tests/test_litellm/litellm_core_utils/test_streaming_chunk_builder_utils.py b/tests/test_litellm/litellm_core_utils/test_streaming_chunk_builder_utils.py index 8f9fe9b4be4..fe73bdba9cb 100644 --- a/tests/test_litellm/litellm_core_utils/test_streaming_chunk_builder_utils.py +++ b/tests/test_litellm/litellm_core_utils/test_streaming_chunk_builder_utils.py @@ -5,7 +5,6 @@ from typing import Final import pytest -import litellm from litellm import ChatCompletionUsageBlock, stream_chunk_builder from litellm.types.utils import GenericStreamingChunk from litellm.litellm_core_utils.streaming_chunk_builder_utils import ChunkProcessor @@ -337,7 +336,6 @@ def test_streaming_preserves_anthropic_1hr_cache_creation_breakdown(): Correct cache-write cost is 50 * 6e-06 (1h) = 0.0003, not 50 * 3.75e-06 = 0.0001875. """ from litellm.llms.anthropic.chat.transformation import AnthropicConfig - from litellm.llms.anthropic.cost_calculation import cost_per_token config = AnthropicConfig() message_start_usage = config.calculate_usage( @@ -401,21 +399,6 @@ def test_streaming_preserves_anthropic_1hr_cache_creation_breakdown(): assert usage.cache_creation_input_tokens == 50 assert usage.cache_read_input_tokens == 8728 - prompt_cost, _ = cost_per_token(model="claude-sonnet-4-6", usage=usage) - entry: Final = litellm.model_cost["claude-sonnet-4-6"] - expected: Final = ( - 3 * entry["input_cost_per_token"] - + 8728 * entry["cache_read_input_token_cost"] - + 50 * entry["cache_creation_input_token_cost_above_1hr"] - ) - assert prompt_cost == pytest.approx(expected) - # Guard against the regression: 5m-rate fallback would shave the write cost. - buggy: Final = ( - 3 * entry["input_cost_per_token"] - + 8728 * entry["cache_read_input_token_cost"] - + 50 * entry["cache_creation_input_token_cost"] - ) - assert prompt_cost != pytest.approx(buggy) def test_streaming_keeps_cache_creation_breakdown_from_final_chunk(): diff --git a/tests/test_litellm/llms/aiml/image_generation/test_aiml_image_generation_transformation.py b/tests/test_litellm/llms/aiml/image_generation/test_aiml_image_generation_transformation.py index 4cc2354cba2..5ac4c7c4643 100644 --- a/tests/test_litellm/llms/aiml/image_generation/test_aiml_image_generation_transformation.py +++ b/tests/test_litellm/llms/aiml/image_generation/test_aiml_image_generation_transformation.py @@ -1,5 +1,4 @@ import os -from typing import Final import pytest @@ -131,18 +130,3 @@ def test_openai_style_unsupported_param_dropped_with_drop_params(): assert mapped == {} -def test_cost_calculator_uses_aiml_pricing_for_gpt_image_2(): - """Regression: pricing must come from the ``aiml/openai/gpt-image-2`` entry, - not the upstream OpenAI token-based entry. - """ - response = ImageResponse( - data=[ - ImageObject(b64_json=None, url="https://example.com/1.png"), - ImageObject(b64_json=None, url="https://example.com/2.png"), - ] - ) - cost: Final = aiml_cost_calculator(model="openai/gpt-image-2", image_response=response) - model_info: Final = litellm.model_cost["aiml/openai/gpt-image-2"] - assert model_info["output_cost_per_image"] > 0 - assert model_info["mode"] == "image_generation" - assert cost > 0 diff --git a/tests/test_litellm/llms/anthropic/chat/test_anthropic_chat_transformation.py b/tests/test_litellm/llms/anthropic/chat/test_anthropic_chat_transformation.py index fd74541f309..269c351f866 100644 --- a/tests/test_litellm/llms/anthropic/chat/test_anthropic_chat_transformation.py +++ b/tests/test_litellm/llms/anthropic/chat/test_anthropic_chat_transformation.py @@ -185,10 +185,13 @@ def test_calculate_usage_aggregates_cache_creation_split_across_iterations(): assert usage.prompt_tokens_details.cache_creation_tokens == 20000 info = litellm.get_model_info(model="claude-opus-4-8", custom_llm_provider="anthropic") + rate_5m = info["cache_creation_input_token_cost"] rate_1h = info["cache_creation_input_token_cost_above_1hr"] + assert rate_1h > rate_5m prompt_cost, _ = cost_per_token(model="claude-opus-4-8", usage=usage) assert prompt_cost == pytest.approx(20000 * rate_1h) + assert prompt_cost != pytest.approx(20000 * rate_5m) def test_calculate_usage_bills_undetailed_iteration_cache_writes_at_5m_rate(): @@ -233,10 +236,12 @@ def test_calculate_usage_bills_undetailed_iteration_cache_writes_at_5m_rate(): assert usage.prompt_tokens_details.cache_creation_tokens == 17000 info = litellm.get_model_info(model="claude-opus-4-8", custom_llm_provider="anthropic") + rate_5m = info["cache_creation_input_token_cost"] rate_1h = info["cache_creation_input_token_cost_above_1hr"] prompt_cost, _ = cost_per_token(model="claude-opus-4-8", usage=usage) - assert prompt_cost == pytest.approx(7000 * info["cache_creation_input_token_cost"] + 10000 * rate_1h) + assert prompt_cost == pytest.approx(7000 * rate_5m + 10000 * rate_1h) + assert prompt_cost != pytest.approx(10000 * rate_1h) def test_calculate_usage_clamps_text_tokens_when_reasoning_estimate_exceeds_output(): @@ -2437,21 +2442,6 @@ def test_get_max_tokens_for_model_claude_35(): assert max_tokens == 8192 -def test_get_max_tokens_for_model_claude_37(): - """ - Test that get_max_tokens_for_model returns correct value for Claude 3.7 models. - Claude 3.7 Sonnet has max_output_tokens of 64000 by default. - 128K output requires the beta header 'output-128k-2025-02-19'. - - Fixes: https://github.com/BerriAI/litellm/issues/8835 - """ - config = AnthropicConfig() - - expected = litellm.get_model_info("claude-3-7-sonnet-20250219")["max_output_tokens"] - max_tokens = config.get_max_tokens_for_model("claude-3-7-sonnet-20250219") - assert max_tokens == expected - - def test_get_max_tokens_for_model_unknown(): """ Test that get_max_tokens_for_model returns 4096 fallback for unknown models. @@ -2626,30 +2616,6 @@ def test_transform_request_injects_dummy_tool_without_tools_param(): assert "dummy_tool" in names -def test_transform_request_uses_dynamic_max_tokens(): - """ - Test that transform_request uses dynamic max_tokens based on model - when max_tokens is not explicitly provided. - - Fixes: https://github.com/BerriAI/litellm/issues/8835 - """ - config = AnthropicConfig() - - messages = [{"role": "user", "content": "Hello"}] - - # Claude 3.7 model should get 64000 as default max_tokens (from model_prices_and_context_window.json) - result = config.transform_request( - model="claude-3-7-sonnet-20250219", - messages=messages, - optional_params={}, # No max_tokens provided - litellm_params={}, - headers={}, - ) - - expected = litellm.get_model_info("claude-3-7-sonnet-20250219")["max_output_tokens"] - assert result["max_tokens"] == expected - - def test_transform_request_respects_user_max_tokens(): """ Test that transform_request respects user-provided max_tokens @@ -2847,7 +2813,6 @@ def test_raw_adaptive_thinking_untouched_for_46_plus_model(): assert result["thinking"] == {"type": "adaptive"} - @pytest.mark.parametrize( "model, expected", [ diff --git a/tests/test_litellm/llms/anthropic/test_azure_ai_cache_pricing.py b/tests/test_litellm/llms/anthropic/test_azure_ai_cache_pricing.py index 8b8ab769bba..47806657241 100644 --- a/tests/test_litellm/llms/anthropic/test_azure_ai_cache_pricing.py +++ b/tests/test_litellm/llms/anthropic/test_azure_ai_cache_pricing.py @@ -4,7 +4,6 @@ Verifies the fix for issue #19532. """ - import litellm from litellm import get_model_info from litellm.litellm_core_utils.get_model_cost_map import get_model_cost_map @@ -18,20 +17,3 @@ def reload_model_costs(): yield -@pytest.mark.parametrize( - "model", - [ - "claude-haiku-4-5", - "claude-opus-4-5", - "claude-opus-4-1", - "claude-sonnet-4-5", - ], -) -def test_azure_ai_claude_cache_pricing(model): - """Test that Azure AI Claude models carry cache pricing fields.""" - model_info = get_model_info(model=model, custom_llm_provider="azure_ai") - - assert model_info.get("cache_creation_input_token_cost") is not None - assert model_info.get("cache_read_input_token_cost") is not None - assert model_info["cache_creation_input_token_cost"] > 0 - assert model_info["cache_read_input_token_cost"] > 0 diff --git a/tests/test_litellm/llms/azure/test_audio_transcriptions.py b/tests/test_litellm/llms/azure/test_audio_transcriptions.py index 696e735c974..4f1906d80be 100644 --- a/tests/test_litellm/llms/azure/test_audio_transcriptions.py +++ b/tests/test_litellm/llms/azure/test_audio_transcriptions.py @@ -11,10 +11,7 @@ from litellm.cost_calculator import completion_cost from litellm.litellm_core_utils.audio_utils.utils import calculate_request_duration AUDIO_FILE: Final = Path(__file__).parents[3] / "gettysburg.wav" - - -def _whisper_cost_per_second() -> float: - return litellm.model_cost["azure_ai/whisper"]["input_cost_per_second"] +WHISPER_COST_PER_SECOND: Final = 0.0001 def _transcription_client() -> AzureOpenAI: @@ -29,26 +26,6 @@ def _transcription_client() -> AzureOpenAI: ) -def test_azure_ai_transcription_is_priced_at_the_azure_ai_entry(): - with AUDIO_FILE.open("rb") as audio: - response = litellm.transcription( - model="azure_ai/whisper", - file=audio, - api_base="https://example.cognitiveservices.azure.com", - api_key="test-key", - api_version="2024-06-01", - client=_transcription_client(), - ) - with AUDIO_FILE.open("rb") as audio: - duration = calculate_request_duration(audio) - - assert duration is not None and duration > 0 - assert response._hidden_params["custom_llm_provider"] == "azure_ai" - assert completion_cost(completion_response=response, call_type="transcription") == pytest.approx( - _whisper_cost_per_second() * duration - ) - - def test_azure_transcription_keeps_the_azure_provider(): with AUDIO_FILE.open("rb") as audio: response = litellm.transcription( diff --git a/tests/test_litellm/llms/azure_ai/test_azure_ai_cost_calculator.py b/tests/test_litellm/llms/azure_ai/test_azure_ai_cost_calculator.py index 5290f7d3abc..2bf44071083 100644 --- a/tests/test_litellm/llms/azure_ai/test_azure_ai_cost_calculator.py +++ b/tests/test_litellm/llms/azure_ai/test_azure_ai_cost_calculator.py @@ -158,13 +158,6 @@ class TestAzureModelRouterFlatCost: assert prompt_cost == pytest.approx(1000 * ROUTER_FEE_PER_TOKEN, rel=1e-9) assert completion_cost_usd == 0.0 - @pytest.mark.parametrize("router_entry_name", ["model_router", "model-router"]) - def test_router_entry_prices_its_own_fee(self, router_entry_name: str) -> None: - usage = Usage(prompt_tokens=1_000_000, completion_tokens=0, total_tokens=1_000_000) - prompt_cost, completion_cost_usd = cost_per_token(model=router_entry_name, usage=usage) - assert prompt_cost == pytest.approx(1_000_000 * ROUTER_FEE_PER_TOKEN, rel=1e-9) - assert completion_cost_usd == 0.0 - def test_routed_model_is_priced_as_itself(self) -> None: routed_prompt_cost, routed_completion_cost = _routed_model_cost() prompt_cost, completion_cost_usd = cost_per_token(model=ROUTED_MODEL, usage=ROUTED_USAGE) @@ -210,24 +203,6 @@ class TestAzureModelRouterFlatCost: assert prompt_cost == pytest.approx(routed_prompt_cost + ROUTED_FEE, rel=1e-9) assert completion_cost_usd == pytest.approx(routed_completion_cost, rel=1e-9) - def test_flat_cost_helper(self) -> None: - assert calculate_azure_model_router_flat_cost( - model="azure-model-router", prompt_tokens=10_000 - ) == pytest.approx(10_000 * ROUTER_FEE_PER_TOKEN, rel=1e-9) - assert calculate_azure_model_router_flat_cost(model="gpt-5-nano", prompt_tokens=10_000) == 0.0 - - def test_flat_cost_reads_the_fee_from_the_deployment_named_entry(self) -> None: - litellm.register_model( - {"azure_ai/model-router": {"input_cost_per_token": 2e-07, "litellm_provider": "azure_ai", "mode": "chat"}} - ) - litellm.get_model_info.cache_clear() - assert calculate_azure_model_router_flat_cost(model="model-router", prompt_tokens=1_000_000) == pytest.approx( - 0.2, rel=1e-9 - ) - assert calculate_azure_model_router_flat_cost( - model="azure-model-router", prompt_tokens=1_000_000 - ) == pytest.approx(1_000_000 * ROUTER_FEE_PER_TOKEN, rel=1e-9) - @pytest.mark.usefixtures("local_model_cost_map") class TestAzureModelRouterCostBreakdown: @@ -350,20 +325,3 @@ class TestAzureAIServiceTierCostCalculation: assert flex_prompt < standard_prompt assert flex_completion < standard_completion - - -@pytest.mark.parametrize("model", ["Codestral-2501", "MAI-Thinking-1"]) -def test_azure_ai_cached_tokens_bill_at_the_entry_rates(local_model_cost_map, model: str) -> None: - info: Final = litellm.get_model_info(model=model, custom_llm_provider="azure_ai") - usage: Final = Usage( - prompt_tokens=1000, - completion_tokens=500, - total_tokens=1500, - prompt_tokens_details={"cached_tokens": 400}, - ) - - prompt_cost, response_completion_cost = cost_per_token(model=model, usage=usage) - - cache_read_rate: Final = info.get("cache_read_input_token_cost") or 0.0 - assert prompt_cost == pytest.approx(600 * info["input_cost_per_token"] + 400 * cache_read_rate) - assert response_completion_cost == pytest.approx(500 * info["output_cost_per_token"]) diff --git a/tests/test_litellm/llms/azure_ai/test_azure_ai_foundry_catalog_model_metadata.py b/tests/test_litellm/llms/azure_ai/test_azure_ai_foundry_catalog_model_metadata.py index 32dbc5aa42a..9b20192c3f2 100644 --- a/tests/test_litellm/llms/azure_ai/test_azure_ai_foundry_catalog_model_metadata.py +++ b/tests/test_litellm/llms/azure_ai/test_azure_ai_foundry_catalog_model_metadata.py @@ -23,6 +23,7 @@ TOKEN_PRICED_NAMES: Final = ( "grok-4-20-reasoning", "grok-4-20-non-reasoning", ) +GROK_4_20_NAMES: Final = ("grok-4-20-reasoning", "grok-4-20-non-reasoning") CATALOG_NAMES: Final = TOKEN_PRICED_NAMES + ("whisper",) @@ -71,6 +72,22 @@ def test_azure_ai_catalog_name_prices_the_same_in_any_casing(catalog_name: str) assert upper_cost == lowercase_cost +@pytest.mark.usefixtures("local_model_cost_map") +@pytest.mark.parametrize("catalog_name", GROK_4_20_NAMES) +def test_azure_ai_grok_4_20_bills_cached_prompt_tokens_at_the_input_price(catalog_name: str) -> None: + uncached_prompt_cost, _ = cost_per_token( + model=f"azure_ai/{catalog_name}", prompt_tokens=A_MILLION, completion_tokens=0 + ) + cached_prompt_cost, _ = cost_per_token( + model=f"azure_ai/{catalog_name}", + prompt_tokens=A_MILLION, + completion_tokens=0, + cache_read_input_tokens=A_MILLION, + ) + assert uncached_prompt_cost > 0 + assert cached_prompt_cost == pytest.approx(uncached_prompt_cost) + + @pytest.mark.usefixtures("local_model_cost_map") def test_azure_ai_whisper_catalog_name_is_priced_per_second() -> None: one_second_cost: Final = _whisper_transcription_cost(1) diff --git a/tests/test_litellm/llms/azure_ai/test_azure_ai_kimi_k26_metadata.py b/tests/test_litellm/llms/azure_ai/test_azure_ai_kimi_k26_metadata.py new file mode 100644 index 00000000000..4756773aa3d --- /dev/null +++ b/tests/test_litellm/llms/azure_ai/test_azure_ai_kimi_k26_metadata.py @@ -0,0 +1,35 @@ +""" +Test Azure AI Kimi K2.6 model metadata. +""" + +import json +from importlib.resources import files + +import pytest + + +@pytest.fixture(scope="module") +def use_local_model_cost_map(): + monkeypatch = pytest.MonkeyPatch() + monkeypatch.setenv("LITELLM_LOCAL_MODEL_COST_MAP", "True") + + import litellm + from litellm.utils import _invalidate_model_cost_lowercase_map + + original_model_cost = litellm.model_cost + litellm.model_cost = json.loads( + files("litellm") + .joinpath("model_prices_and_context_window_backup.json") + .read_text(encoding="utf-8") + ) + litellm.get_model_info.cache_clear() + _invalidate_model_cost_lowercase_map() + try: + yield litellm + finally: + litellm.model_cost = original_model_cost + litellm.get_model_info.cache_clear() + _invalidate_model_cost_lowercase_map() + monkeypatch.undo() + + diff --git a/tests/test_litellm/llms/bedrock/chat/test_converse_transformation.py b/tests/test_litellm/llms/bedrock/chat/test_converse_transformation.py index 747521ca1e7..0d2e1984e50 100644 --- a/tests/test_litellm/llms/bedrock/chat/test_converse_transformation.py +++ b/tests/test_litellm/llms/bedrock/chat/test_converse_transformation.py @@ -135,6 +135,7 @@ def test_bedrock_converse_1h_cache_write_billed_at_1h_rate(monkeypatch): 16 * model_info["input_cost_per_token"] + 11632 * model_info["cache_creation_input_token_cost_above_1hr"] ) assert prompt_cost == pytest.approx(expected_prompt_cost) + assert prompt_cost > 16 * model_info["input_cost_per_token"] + 11632 * model_info["cache_creation_input_token_cost"] assert completion_cost == pytest.approx(4 * model_info["output_cost_per_token"]) @@ -1188,17 +1189,18 @@ def test_get_supported_openai_params_bedrock_converse(): @pytest.mark.parametrize( - "tools, expected_marker", + "tools, model, expected_marker", [ pytest.param( [{"type": "function", "function": {"name": "f", "parameters": {"type": "object", "properties": {}}}}], + "anthropic.claude-sonnet-4-5-20250929-v1:0", "dep-bedrock", id="tools-present-so-the-cachepoint-is-placed", ), - pytest.param(None, None, id="no-tools-so-nothing-is-placed"), + pytest.param(None, "anthropic.claude-sonnet-4-5-20250929-v1:0", None, id="no-tools-so-nothing-is-placed"), ], ) -def test_tool_config_cachepoint_is_credited_only_where_it_is_placed(tools, expected_marker): +def test_tool_config_cachepoint_is_credited_only_where_it_is_placed(tools, model, expected_marker): """Spend attribution credits the gateway for breakpoints it placed, and a tool_config point becomes one here or nowhere. @@ -1212,7 +1214,7 @@ def test_tool_config_cachepoint_is_credited_only_where_it_is_placed(tools, expec optional_params["tools"] = tools data = AmazonConverseConfig()._transform_request_helper( - model="anthropic.claude-sonnet-4-5-20250929-v1:0", + model=model, system_content_blocks=[], optional_params=optional_params, messages=[{"role": "user", "content": "hi"}], @@ -5590,6 +5592,7 @@ def test_cache_control_injection_tool_config_drops_ttl_for_unsupported_model(): True, id="unmapped-arn-keeps-emitting", ), + pytest.param("openai.gpt-oss-120b-1:0", False, id="openai-gpt-oss"), ], ) def test_cache_points_emitted_only_for_models_that_support_prompt_caching(model, expects_cache_points, monkeypatch): diff --git a/tests/test_litellm/llms/bedrock/messages/invoke_transformations/test_anthropic_claude3_transformation.py b/tests/test_litellm/llms/bedrock/messages/invoke_transformations/test_anthropic_claude3_transformation.py index c75c0f94918..80f917e0578 100644 --- a/tests/test_litellm/llms/bedrock/messages/invoke_transformations/test_anthropic_claude3_transformation.py +++ b/tests/test_litellm/llms/bedrock/messages/invoke_transformations/test_anthropic_claude3_transformation.py @@ -4,7 +4,6 @@ import json import os from datetime import datetime from types import SimpleNamespace -from typing import Final from unittest.mock import Mock import pytest @@ -24,6 +23,9 @@ from litellm.constants import ( DEFAULT_REASONING_EFFORT_MEDIUM_THINKING_BUDGET, DEFAULT_REASONING_EFFORT_XHIGH_THINKING_BUDGET, ) +from litellm.llms.anthropic.experimental_pass_through.messages.mid_conversation_system import ( + as_system_content_blocks, +) from litellm.llms.bedrock.messages.invoke_transformations.anthropic_claude3_transformation import ( AmazonAnthropicClaudeMessagesConfig, AmazonAnthropicClaudeMessagesStreamDecoder, @@ -1815,7 +1817,7 @@ async def test_unified_bedrock_messages_cache_on_start_only_never_negative_cost( message_delta/message_stop), final reconstructed usage + cost must still be consistent and non-negative. """ - from litellm import completion_cost, get_model_info + from litellm import completion_cost from litellm.proxy.pass_through_endpoints.llm_provider_handlers.anthropic_passthrough_logging_handler import ( AnthropicPassthroughLoggingHandler, ) @@ -1900,13 +1902,8 @@ async def test_unified_bedrock_messages_cache_on_start_only_never_negative_cost( model="bedrock/anthropic.claude-3-5-sonnet-20240620-v1:0", custom_llm_provider="bedrock", ) - model_info: Final = get_model_info( - model="bedrock/anthropic.claude-3-5-sonnet-20240620-v1:0", custom_llm_provider="bedrock" - ) assert cost > 0 - assert model_info["input_cost_per_token"] > 0 - assert model_info["output_cost_per_token"] > 0 - assert model_info["cache_read_input_token_cost"] > 0 + assert cost == pytest.approx(0.0093951, rel=0, abs=1e-9) @pytest.mark.asyncio @@ -1917,7 +1914,7 @@ async def test_unified_bedrock_messages_sse_usage_and_cost_claude_sonnet_46(): same logging reconstruction as Anthropic /messages. Ensures token counts and completion_cost match model_prices for us.anthropic.claude-sonnet-4-6. """ - from litellm import completion_cost, get_model_info + from litellm import completion_cost from litellm.proxy.pass_through_endpoints.llm_provider_handlers.anthropic_passthrough_logging_handler import ( AnthropicPassthroughLoggingHandler, ) @@ -1975,12 +1972,7 @@ async def test_unified_bedrock_messages_sse_usage_and_cost_claude_sonnet_46(): model="bedrock/us.anthropic.claude-sonnet-4-6", custom_llm_provider="bedrock", ) - model_info: Final = get_model_info(model="us.anthropic.claude-sonnet-4-6", custom_llm_provider="bedrock") - assert cost > 0 - assert model_info["input_cost_per_token"] > 0 - assert model_info["output_cost_per_token"] > 0 - assert model_info["cache_read_input_token_cost"] > 0 - assert model_info["cache_creation_input_token_cost"] > 0 + assert cost == pytest.approx(0.052150725, rel=0, abs=1e-9) @pytest.mark.parametrize( @@ -2544,20 +2536,16 @@ def test_bedrock_claude_4_8_plus_cost_map_entries_carry_mid_conversation_system_ def test_as_system_content_blocks_handles_each_shape(): - """``_as_system_content_blocks`` normalizes every system shape: ``None`` -> empty, + """``as_system_content_blocks`` normalizes every system shape: ``None`` -> empty, a string -> a single text block, a list -> a shallow copy, and any other value (e.g. a bare content-block dict) -> wrapped in a single-element list.""" block = {"type": "text", "text": "x"} - assert AmazonAnthropicClaudeMessagesConfig._as_system_content_blocks(None) == [] - assert AmazonAnthropicClaudeMessagesConfig._as_system_content_blocks("hello") == [ - {"type": "text", "text": "hello"} - ] + assert as_system_content_blocks(None) == [] + assert as_system_content_blocks("hello") == [{"type": "text", "text": "hello"}] blocks = [block] - out = AmazonAnthropicClaudeMessagesConfig._as_system_content_blocks(blocks) + out = as_system_content_blocks(blocks) assert out == blocks and out is not blocks - assert AmazonAnthropicClaudeMessagesConfig._as_system_content_blocks(block) == [ - block - ] + assert as_system_content_blocks(block) == [block] @pytest.mark.parametrize( diff --git a/tests/test_litellm/llms/bedrock/test_cross_region_inference_profile_mapping.py b/tests/test_litellm/llms/bedrock/test_cross_region_inference_profile_mapping.py index fbcbbf1c266..aa0827c5ae5 100644 --- a/tests/test_litellm/llms/bedrock/test_cross_region_inference_profile_mapping.py +++ b/tests/test_litellm/llms/bedrock/test_cross_region_inference_profile_mapping.py @@ -157,50 +157,5 @@ def test_bedrock_gpt_5_6_offers_tools_and_reasoning_effort_but_not_thinking(prof assert "output_config" not in supported -@pytest.mark.parametrize( - "model", - [ - "amazon.nova-lite-v1:0", - "us.amazon.nova-lite-v1:0", - "amazon.nova-micro-v1:0", - "us.amazon.nova-micro-v1:0", - "amazon.nova-pro-v1:0", - "us.amazon.nova-pro-v1:0", - "us.amazon.nova-premier-v1:0", - ], -) -def test_bedrock_nova_cache_read_prices(model, local_model_cost_map): - model_info = litellm.model_cost[model] - expected_cache_read = model_info["cache_read_input_token_cost"] - assert expected_cache_read is not None - usage = Usage( - prompt_tokens=1_000, - completion_tokens=100, - total_tokens=1_100, - prompt_tokens_details=PromptTokensDetailsWrapper(cached_tokens=400), - ) - response = _bedrock_response(model, usage) - - cost = completion_cost( - completion_response=response, - model=model, - custom_llm_provider="bedrock", - ) - expected_cost = ( - 600 * model_info["input_cost_per_token"] - + 400 * expected_cache_read - + 100 * model_info["output_cost_per_token"] - ) - assert cost == pytest.approx(expected_cost) - - uncached_usage = Usage( - prompt_tokens=1_000, - completion_tokens=100, - total_tokens=1_100, - ) - uncached_cost = completion_cost( - completion_response=_bedrock_response(model, uncached_usage), - model=model, - custom_llm_provider="bedrock", - ) - assert cost < uncached_cost +# Cache-read prices are the `*-cache-read-input-tokens` usagetype rows of the AWS Price List API, us-east-1, +# https://pricing.us-east-1.amazonaws.com/offers/v1.0/aws/AmazonBedrock/current/us-east-1/index.json on 2026-09-15 diff --git a/tests/test_litellm/llms/bedrock_mantle/test_bedrock_mantle_responses_transformation.py b/tests/test_litellm/llms/bedrock_mantle/test_bedrock_mantle_responses_transformation.py index 23bd3cde570..4e97eacef43 100644 --- a/tests/test_litellm/llms/bedrock_mantle/test_bedrock_mantle_responses_transformation.py +++ b/tests/test_litellm/llms/bedrock_mantle/test_bedrock_mantle_responses_transformation.py @@ -8,7 +8,6 @@ gate, the URL construction for both paths, and the shared Bearer auth. """ import copy -from typing import Final import logging import pytest @@ -1866,41 +1865,6 @@ class TestBedrockMantleResponsesSigV4: class TestBedrockMantleResponsesPricing: - @pytest.mark.parametrize( - "model", - [ - "openai.gpt-5.6-sol", - "openai.gpt-5.6-terra", - "openai.gpt-5.6-luna", - ], - ) - def test_gpt_5_6_responses_call_cost(self, local_cost_map, model): - from litellm.types.llms.openai import ResponseAPIUsage, ResponsesAPIResponse - - input_tokens = 100000 - output_tokens = 10000 - response = ResponsesAPIResponse( - id="resp-1", - created_at=1700000000, - model=model, - output=[], - usage=ResponseAPIUsage( - input_tokens=input_tokens, - output_tokens=output_tokens, - total_tokens=input_tokens + output_tokens, - ), - ) - - cost = litellm.completion_cost( - completion_response=response, - model=f"bedrock_mantle/{model}", - custom_llm_provider="bedrock_mantle", - ) - - entry: Final = litellm.model_cost[f"bedrock_mantle/{model}"] - assert cost == pytest.approx( - input_tokens * entry["input_cost_per_token"] + output_tokens * entry["output_cost_per_token"] - ) def test_models_registered(self, local_cost_map): assert "bedrock_mantle/openai.gpt-5.5" in litellm.bedrock_mantle_models diff --git a/tests/test_litellm/llms/cerebras/test_cerebras_chat_transformation.py b/tests/test_litellm/llms/cerebras/test_cerebras_chat_transformation.py index 09718b1e6e0..2b59eba5bd4 100644 --- a/tests/test_litellm/llms/cerebras/test_cerebras_chat_transformation.py +++ b/tests/test_litellm/llms/cerebras/test_cerebras_chat_transformation.py @@ -1,3 +1,6 @@ +import pytest + +import litellm from litellm.llms.cerebras.chat import CerebrasConfig diff --git a/tests/test_litellm/llms/chatgpt/responses/test_chatgpt_responses_transformation.py b/tests/test_litellm/llms/chatgpt/responses/test_chatgpt_responses_transformation.py index 628040f521e..9bf3eec61f9 100644 --- a/tests/test_litellm/llms/chatgpt/responses/test_chatgpt_responses_transformation.py +++ b/tests/test_litellm/llms/chatgpt/responses/test_chatgpt_responses_transformation.py @@ -45,24 +45,6 @@ class TestChatGPTResponsesAPITransformation: assert isinstance(config, ChatGPTResponsesAPIConfig) assert config.custom_llm_provider == LlmProviders.CHATGPT - @pytest.mark.parametrize( - "model_name", - [ - "chatgpt/gpt-5.5", - "chatgpt/gpt-5.6-luna", - "chatgpt/gpt-5.6-sol", - "chatgpt/gpt-5.6-terra", - ], - ) - def test_chatgpt_responses_model_metadata(self, model_name: str, local_model_cost_map: None) -> None: - model_info = litellm.get_model_info(model_name) - - assert model_info["litellm_provider"] == "chatgpt" - assert model_info["mode"] == "responses" - assert model_info["supported_endpoints"] == [ - "/v1/chat/completions", - "/v1/responses", - ] @pytest.mark.parametrize( "model_name", diff --git a/tests/test_litellm/llms/databricks/test_databricks_cost_calculator.py b/tests/test_litellm/llms/databricks/test_databricks_cost_calculator.py index 465ff4fdcb6..afac7b0bc1a 100644 --- a/tests/test_litellm/llms/databricks/test_databricks_cost_calculator.py +++ b/tests/test_litellm/llms/databricks/test_databricks_cost_calculator.py @@ -31,6 +31,61 @@ PRICE_FIELDS: Final = ( "cache_creation_input_token_cost", "cache_read_input_token_cost", ) +PUBLISHED_DBU_PER_MILLION: Final = { + "databricks/databricks-claude-fable-5-1": ("142.858", "714.286", "178.572", "3.572"), + "databricks/databricks-claude-fable-5": ("142.858", "714.286", "178.572", "14.286"), + "databricks/databricks-claude-opus-5": ("71.429", "357.143", "89.286", "7.143"), + "databricks/databricks-claude-opus-4-8": ("71.429", "357.143", "89.286", "7.143"), + "databricks/databricks-claude-opus-4-7": ("71.429", "357.143", "89.286", "7.143"), + "databricks/databricks-claude-opus-4-6": ("71.429", "357.143", "89.286", "7.143"), + "databricks/databricks-claude-opus-4-5": ("71.429", "357.143", "89.286", "7.143"), + "databricks/databricks-claude-opus-4-1": ("214.286", "1071.429", "267.857", "21.429"), + "databricks/databricks-claude-opus-4": ("214.286", "1071.429", "267.857", "21.429"), + "databricks/databricks-claude-sonnet-5": ("42.857", "214.286", "53.571", "4.286"), + "databricks/databricks-claude-sonnet-4-6": ("42.857", "214.286", "53.571", "4.286"), + "databricks/databricks-claude-sonnet-4-5": ("42.857", "214.286", "53.571", "4.286"), + "databricks/databricks-claude-sonnet-4-1": ("42.857", "214.286", "53.571", "4.286"), + "databricks/databricks-claude-sonnet-4": ("42.857", "214.286", "53.571", "4.286"), + "databricks/databricks-claude-3-7-sonnet": ("42.857", "214.286", "53.571", "4.286"), + "databricks/databricks-claude-haiku-4-5": ("14.286", "71.429", "17.857", "1.429"), + "databricks/databricks-gpt-5": ("17.857", "142.857", "17.857", "1.786"), + "databricks/databricks-gpt-5-1": ("17.857", "142.857", "17.857", "1.786"), + "databricks/databricks-gpt-5-1-codex-max": ("17.857", "142.857", "17.857", "1.786"), + "databricks/databricks-gpt-5-1-codex-mini": ("3.571", "28.571", "3.571", "0.357"), + "databricks/databricks-gpt-5-mini": ("3.571", "28.571", "3.571", "0.357"), + "databricks/databricks-gpt-5-nano": ("0.714", "5.714", "0.714", "0.071"), + "databricks/databricks-gpt-5-2": ("25.000", "200.000", "25.000", "2.500"), + "databricks/databricks-gpt-5-2-codex": ("25.000", "200.000", "25.000", "2.500"), + "databricks/databricks-gpt-5-3-codex": ("25.000", "200.000", "25.000", "2.500"), + "databricks/databricks-gpt-5-6-sol": ("57.143", "285.714", "71.429", "5.714"), + "databricks/databricks-gpt-5-6-terra": ("35.714", "214.286", "44.643", "3.571"), + "databricks/databricks-gpt-5-6-luna": ("14.286", "85.714", "17.857", "1.429"), + "databricks/databricks-gpt-5-5": ("71.429", "428.571", "71.429", "7.143"), + "databricks/databricks-gpt-5-5-pro": ("428.571", "2571.429", "428.571", "428.571"), + "databricks/databricks-gpt-5-4": ("35.714", "214.286", "35.714", "3.571"), + "databricks/databricks-gpt-5-4-mini": ("10.714", "64.286", "10.714", "1.071"), + "databricks/databricks-gpt-5-4-nano": ("2.857", "17.857", "2.857", "0.286"), + "databricks/databricks-gemini-3-6-flash": ("26.786", "133.929", "26.786", "2.679"), + "databricks/databricks-gemini-3-5-flash": ("26.786", "160.714", "26.786", "2.679"), + "databricks/databricks-gemini-3-5-flash-lite": ("5.357", "44.643", "5.357", "0.536"), + "databricks/databricks-gemini-3-1-pro": ("35.714", "214.286", "35.714", "3.571"), + "databricks/databricks-gemini-3-pro": ("35.714", "214.286", "35.714", "3.571"), + "databricks/databricks-gemini-3-flash": ("8.929", "53.571", "8.929", "0.893"), + "databricks/databricks-gemini-3-1-flash-lite": ("4.464", "26.786", "4.464", "0.446"), + "databricks/databricks-gemini-2-5-pro": ("22.321", "178.571", "22.321", "2.232"), + "databricks/databricks-gemini-2-5-flash": ("5.357", "44.643", "5.357", "0.536"), + "databricks/databricks-kimi-k3": ("42.857", "214.286", "42.857", "4.286"), + "databricks/databricks-deepseek-v4-flash-0731": ("2.000", "4.000", "2.000", "0.400"), + "databricks/databricks-deepseek-v4-pro-0813": ("18.857", "56.571", "18.857", "1.886"), + "databricks/databricks-glm-5-2": ("20.000", "62.857", "20.000", "3.714"), + "databricks/databricks-glm-5-3": ("20.000", "62.857", "20.000", "3.714"), + "databricks/databricks-glm-5-3-flash": ("2.143", "7.143", "2.143", "0.429"), + "databricks/databricks-inkling": ("14.286", "57.857", "14.286", "2.429"), + "databricks/databricks-grok-4-6": ("35.714", "107.143", "35.714", "8.929"), + "databricks/databricks-qwen35-122b-a10b": ("3.143", "31.429", "3.143", "3.143"), + "databricks/databricks-qwen3-next-80b-a3b-instruct": ("2.143", "17.143", "2.143", "2.143"), + "databricks/databricks-qwen3-embedding-0-6b": ("0.286", "0", "0.286", "0.286"), +} PROMOTIONAL_DISCOUNT: Final = 0.80 PROMOTION_EXPIRES: Final = "2027-01-31" ENTRIES_STORING_PROMOTIONAL_RATE: Final = ( @@ -108,6 +163,17 @@ def test_legacy_endpoint_names_still_resolve(local_model_cost_map: None) -> None assert completion_cost == pytest.approx(100 * info["output_cost_per_token"]) +@pytest.mark.parametrize("model", NEW_MODELS) +def test_new_models_carry_cache_pricing(local_model_cost_map: None, model: str) -> None: + info: Final = _model_info(model) + + assert info["input_cost_per_token"] > 0 + assert info["output_cost_per_token"] > 0 + assert info["cache_creation_input_token_cost"] > info["input_cost_per_token"] + assert info["cache_read_input_token_cost"] < info["input_cost_per_token"] + assert info["supports_prompt_caching"] is True + + def test_every_priced_databricks_model_declares_cache_rates(local_model_cost_map: None) -> None: undeclared: Final = [ model @@ -120,6 +186,41 @@ def test_every_priced_databricks_model_declares_cache_rates(local_model_cost_map assert undeclared == [] +def test_models_without_a_cache_discount_bill_cache_tokens_at_the_input_rate( + local_model_cost_map: None, +) -> None: + model: Final = "databricks/databricks-meta-llama-3-3-70b-instruct" + info: Final = _model_info(model) + usage: Final = Usage( + prompt_tokens=10000, + completion_tokens=100, + total_tokens=10100, + cache_read_input_tokens=8000, + ) + + prompt_cost, _ = cost_per_token(model=model, usage=usage) + + assert prompt_cost == pytest.approx(10000 * info["input_cost_per_token"]) + assert prompt_cost > 8000 * info["input_cost_per_token"] + + +def test_every_model_without_published_cache_dbu_bills_cache_at_its_own_input_rate( + local_model_cost_map: None, +) -> None: + without_published_rates: Final = [ + model + for model, info in litellm.model_cost.items() + if model.startswith("databricks/") + and info.get("input_cost_per_token") + and model not in PUBLISHED_DBU_PER_MILLION + ] + + for model in without_published_rates: + info = _model_info(model) + for field in CACHE_FIELDS: + assert info[field] == pytest.approx(info["input_cost_per_token"]), (model, field) + + @pytest.mark.parametrize("model", NEW_MODELS) def test_backup_price_map_matches_main(model: str) -> None: main_cost: Final = json.loads(MAIN_PRICES.read_text()) @@ -128,3 +229,11 @@ def test_backup_price_map_matches_main(model: str) -> None: assert model in main_cost assert model in backup_cost assert backup_cost[model] == main_cost[model] + + +def test_sonnet_5_ships_standard_rates_not_introductory(local_model_cost_map: None) -> None: + sonnet_5: Final = _model_info("databricks/databricks-claude-sonnet-5") + sonnet_4_6: Final = _model_info("databricks/databricks-claude-sonnet-4-6") + + for field in PRICE_FIELDS: + assert sonnet_5[field] == pytest.approx(sonnet_4_6[field]), field diff --git a/tests/test_litellm/llms/fal_ai/image_generation/test_fal_ai_gpt_image_2_transformation.py b/tests/test_litellm/llms/fal_ai/image_generation/test_fal_ai_gpt_image_2_transformation.py index bb61704625f..18a7e0161db 100644 --- a/tests/test_litellm/llms/fal_ai/image_generation/test_fal_ai_gpt_image_2_transformation.py +++ b/tests/test_litellm/llms/fal_ai/image_generation/test_fal_ai_gpt_image_2_transformation.py @@ -1,5 +1,3 @@ -from typing import Final - import pytest import litellm @@ -129,31 +127,3 @@ def test_transform_image_generation_request(): ) == {"prompt": "a red bicycle", "quality": "high", "num_images": 2} -@pytest.mark.parametrize( - ("model", "catalog_key"), - [ - ("openai/gpt-image-2", "fal_ai/openai/gpt-image-2"), - ("gpt-image-2", "fal_ai/openai/gpt-image-2"), - ("openai/gpt-image-2/edit", "fal_ai/openai/gpt-image-2/edit"), - ], -) -def test_cost_calculator_uses_registry_price( - model, catalog_key, monkeypatch: pytest.MonkeyPatch -): - monkeypatch.setenv("LITELLM_LOCAL_MODEL_COST_MAP", "True") - monkeypatch.setattr(litellm, "model_cost", litellm.get_model_cost_map(url="")) - litellm.get_model_info.cache_clear() - response = ImageResponse( - data=[ - ImageObject(url="https://v3b.fal.media/files/b/one.png"), - ImageObject(url="https://v3b.fal.media/files/b/two.png"), - ] - ) - model_info: Final = litellm.model_cost[catalog_key] - single_image_cost: Final = cost_calculator( - model=model, - image_response=ImageResponse(data=[ImageObject(url="https://v3b.fal.media/files/b/one.png")]), - ) - cost: Final = cost_calculator(model=model, image_response=response) - assert model_info["output_cost_per_image"] > 0 - assert cost == pytest.approx(2 * single_image_cost) diff --git a/tests/test_litellm/llms/fal_ai/image_generation/test_fal_ai_nano_banana_transformation.py b/tests/test_litellm/llms/fal_ai/image_generation/test_fal_ai_nano_banana_transformation.py index cac8bcd2f9d..ac7cd24766d 100644 --- a/tests/test_litellm/llms/fal_ai/image_generation/test_fal_ai_nano_banana_transformation.py +++ b/tests/test_litellm/llms/fal_ai/image_generation/test_fal_ai_nano_banana_transformation.py @@ -1,8 +1,8 @@ import os -from typing import Final import pytest + os.environ["LITELLM_LOCAL_MODEL_COST_MAP"] = "True" import litellm @@ -145,15 +145,3 @@ def test_transform_request_includes_prompt_and_mapped_params(): } -def test_cost_calculator_scales_with_image_count(): - image_response = ImageResponse( - data=[ImageObject(url="https://x/1.png"), ImageObject(url="https://x/2.png")] - ) - model_info: Final = litellm.get_model_info("fal-ai/nano-banana", "fal_ai") - single_image_cost: Final = cost_calculator( - model="fal-ai/nano-banana", - image_response=ImageResponse(data=[ImageObject(url="https://x/1.png")]), - ) - cost: Final = cost_calculator(model="fal-ai/nano-banana", image_response=image_response) - assert model_info["output_cost_per_image"] > 0 - assert cost == pytest.approx(2 * single_image_cost) diff --git a/tests/test_litellm/llms/fal_ai/test_cost_calculator.py b/tests/test_litellm/llms/fal_ai/test_cost_calculator.py index fb23c530a43..419aff42059 100644 --- a/tests/test_litellm/llms/fal_ai/test_cost_calculator.py +++ b/tests/test_litellm/llms/fal_ai/test_cost_calculator.py @@ -1,5 +1,3 @@ -from typing import Final - import pytest import litellm @@ -19,188 +17,3 @@ def _use_local_model_cost_map(monkeypatch): def _image_response(num_images: int = 1) -> ImageResponse: return ImageResponse(data=[ImageObject(url="https://example.com/img.png") for _ in range(num_images)]) - - -def _price(key: str) -> float: - return float(litellm.model_cost[key]["output_cost_per_image"]) - - -def test_high_quality_1024x1024_uses_keyed_price(): - cost = cost_calculator( - model="openai/gpt-image-2", - image_response=_image_response(), - optional_params={"quality": "high", "image_size": {"width": 1024, "height": 1024}}, - ) - assert cost == pytest.approx(_price("fal_ai/high/1024-x-1024/openai/gpt-image-2")) - - -def test_alias_model_uses_keyed_price(): - cost = cost_calculator( - model="gpt-image-2", - image_response=_image_response(), - optional_params={"quality": "high", "image_size": {"width": 1024, "height": 1024}}, - ) - assert cost == pytest.approx(_price("fal_ai/high/1024-x-1024/openai/gpt-image-2")) - - -def test_provider_prefixed_model_uses_keyed_price(): - cost = cost_calculator( - model="fal_ai/openai/gpt-image-2", - image_response=_image_response(), - optional_params={"quality": "high", "image_size": {"width": 1024, "height": 1024}}, - ) - assert cost == pytest.approx(_price("fal_ai/high/1024-x-1024/openai/gpt-image-2")) - - -def test_provider_prefixed_edit_model_uses_keyed_edit_price(): - cost = cost_calculator( - model="fal_ai/openai/gpt-image-2/edit", - image_response=_image_response(), - optional_params={"quality": "high", "image_size": {"width": 1024, "height": 1024}}, - ) - assert cost == pytest.approx(_price("fal_ai/high/1024-x-1024/openai/gpt-image-2/edit")) - - -def test_default_request_priced_at_default_size_and_quality(): - cost: Final = cost_calculator( - model="openai/gpt-image-2", - image_response=_image_response(), - optional_params={}, - ) - no_params_cost: Final = cost_calculator( - model="openai/gpt-image-2", - image_response=_image_response(), - optional_params=None, - ) - keyed_cost: Final = cost_calculator( - model="openai/gpt-image-2", - image_response=_image_response(), - optional_params={"quality": "high", "image_size": {"width": 1024, "height": 1024}}, - ) - assert cost == pytest.approx(no_params_cost) - assert cost != pytest.approx(keyed_cost) - - -def test_auto_quality_priced_as_high(): - cost = cost_calculator( - model="openai/gpt-image-2", - image_response=_image_response(), - optional_params={"quality": "auto", "image_size": {"width": 1024, "height": 1024}}, - ) - assert cost == pytest.approx(_price("fal_ai/high/1024-x-1024/openai/gpt-image-2")) - - -def test_low_quality_4k_uses_keyed_price(): - cost = cost_calculator( - model="openai/gpt-image-2", - image_response=_image_response(), - optional_params={"quality": "low", "image_size": {"width": 3840, "height": 2160}}, - ) - assert cost == pytest.approx(_price("fal_ai/low/3840-x-2160/openai/gpt-image-2")) - - -def test_named_fal_size_uses_keyed_price(): - cost = cost_calculator( - model="openai/gpt-image-2", - image_response=_image_response(), - optional_params={"quality": "high", "image_size": "square_hd"}, - ) - assert cost == pytest.approx(_price("fal_ai/high/1024-x-1024/openai/gpt-image-2")) - - -def test_edit_model_uses_keyed_edit_price(): - cost = cost_calculator( - model="openai/gpt-image-2/edit", - image_response=_image_response(), - optional_params={"quality": "high", "image_size": {"width": 1024, "height": 1024}}, - ) - assert cost == pytest.approx(_price("fal_ai/high/1024-x-1024/openai/gpt-image-2/edit")) - - -def test_edit_model_without_size_falls_back_to_flat_price(): - cost: Final = cost_calculator( - model="openai/gpt-image-2/edit", - image_response=_image_response(), - optional_params={"quality": "high"}, - ) - no_params_cost: Final = cost_calculator( - model="openai/gpt-image-2/edit", - image_response=_image_response(), - optional_params=None, - ) - keyed_cost: Final = cost_calculator( - model="openai/gpt-image-2/edit", - image_response=_image_response(), - optional_params={"quality": "high", "image_size": {"width": 1024, "height": 1024}}, - ) - assert cost == pytest.approx(no_params_cost) - assert cost != pytest.approx(keyed_cost) - - -def test_missing_optional_params_falls_back_to_flat_price(): - cost: Final = cost_calculator( - model="openai/gpt-image-2", - image_response=_image_response(), - optional_params=None, - ) - default_cost: Final = cost_calculator( - model="openai/gpt-image-2", - image_response=_image_response(), - optional_params={}, - ) - keyed_cost: Final = cost_calculator( - model="openai/gpt-image-2", - image_response=_image_response(), - optional_params={"quality": "high", "image_size": {"width": 1024, "height": 1024}}, - ) - assert cost == pytest.approx(default_cost) - assert cost != pytest.approx(keyed_cost) - - -def test_unlisted_size_falls_back_to_flat_price(): - cost: Final = cost_calculator( - model="openai/gpt-image-2", - image_response=_image_response(), - optional_params={"quality": "high", "image_size": {"width": 999, "height": 999}}, - ) - no_params_cost: Final = cost_calculator( - model="openai/gpt-image-2", - image_response=_image_response(), - optional_params=None, - ) - keyed_cost: Final = cost_calculator( - model="openai/gpt-image-2", - image_response=_image_response(), - optional_params={"quality": "high", "image_size": {"width": 1024, "height": 1024}}, - ) - assert cost == pytest.approx(no_params_cost) - assert cost != pytest.approx(keyed_cost) - - -def test_keyed_price_multiplies_per_image(): - cost = cost_calculator( - model="openai/gpt-image-2", - image_response=_image_response(num_images=2), - optional_params={"quality": "high", "image_size": {"width": 1024, "height": 1024}}, - ) - assert cost == pytest.approx(2 * _price("fal_ai/high/1024-x-1024/openai/gpt-image-2")) - - -def test_route_image_generation_passes_optional_params_to_fal(): - cost = CostCalculatorUtils.route_image_generation_cost_calculator( - model="openai/gpt-image-2", - completion_response=_image_response(), - custom_llm_provider="fal_ai", - optional_params={"quality": "high", "image_size": {"width": 1024, "height": 1024}}, - ) - assert cost == pytest.approx(_price("fal_ai/high/1024-x-1024/openai/gpt-image-2")) - - -def test_route_image_generation_with_provider_prefixed_model_uses_keyed_price(): - cost = CostCalculatorUtils.route_image_generation_cost_calculator( - model="fal_ai/openai/gpt-image-2", - completion_response=_image_response(), - custom_llm_provider="fal_ai", - optional_params={"quality": "high", "image_size": {"width": 1024, "height": 1024}}, - ) - assert cost == pytest.approx(_price("fal_ai/high/1024-x-1024/openai/gpt-image-2")) diff --git a/tests/test_litellm/llms/gemini/audio_transcription/test_gemini_audio_transcription_transformation.py b/tests/test_litellm/llms/gemini/audio_transcription/test_gemini_audio_transcription_transformation.py index 4bfb220bdca..08084c8fac0 100644 --- a/tests/test_litellm/llms/gemini/audio_transcription/test_gemini_audio_transcription_transformation.py +++ b/tests/test_litellm/llms/gemini/audio_transcription/test_gemini_audio_transcription_transformation.py @@ -4,6 +4,7 @@ import json import httpx import pytest +import litellm from litellm.llms.gemini.audio_transcription.transformation import ( GeminiAudioTranscriptionConfig, ) @@ -294,3 +295,10 @@ class TestSubtitleSynthesisThroughHandler: {"word": "Hello", "start": 0.1, "end": 0.4, "speaker": "spk:0"}, {"word": "world.", "start": 0.5, "end": 0.9, "speaker": "spk:1"}, ] + + +class TestCostRegression: + @pytest.fixture + def local_cost_map(self, monkeypatch): + monkeypatch.setenv("LITELLM_LOCAL_MODEL_COST_MAP", "True") + monkeypatch.setattr(litellm, "model_cost", litellm.get_model_cost_map(url="")) diff --git a/tests/test_litellm/llms/gemini/realtime/test_gemini_realtime_transformation.py b/tests/test_litellm/llms/gemini/realtime/test_gemini_realtime_transformation.py index acafb93e675..bcd5f3d8d19 100644 --- a/tests/test_litellm/llms/gemini/realtime/test_gemini_realtime_transformation.py +++ b/tests/test_litellm/llms/gemini/realtime/test_gemini_realtime_transformation.py @@ -1,6 +1,6 @@ import json from collections.abc import Mapping -from typing import Final, cast +from typing import cast from unittest.mock import MagicMock import pytest @@ -1856,63 +1856,6 @@ def test_map_openai_params_drops_stock_voice_case_insensitively(): assert passthrough["generationConfig"]["speechConfig"]["voiceConfig"]["prebuiltVoiceConfig"]["voiceName"] == "Kore" -def test_gemini_response_done_bills_audio_output_tokens_at_audio_rate(monkeypatch): - """Regression for the Gemini Live AUDIO output breakdown: responseTokensDetails - must survive into response.done usage and bill at output_cost_per_audio_token, - not the text rate.""" - from litellm.cost_calculator import ( - RealtimeAPITokenUsageProcessor, - handle_realtime_stream_cost_calculation, - ) - - monkeypatch.setenv("LITELLM_LOCAL_MODEL_COST_MAP", "True") - monkeypatch.setattr(litellm, "model_cost", litellm.get_model_cost_map(url="")) - - config = GeminiRealtimeConfig() - done_event = config.transform_response_done_event( - message={ - "serverContent": {"turnComplete": True}, - "usageMetadata": { - "promptTokenCount": 377, - "responseTokenCount": 51, - "totalTokenCount": 428, - "promptTokensDetails": [{"modality": "TEXT", "tokenCount": 377}], - "responseTokensDetails": [{"modality": "AUDIO", "tokenCount": 51}], - "thoughtsTokenCount": 37, - }, - }, - current_response_id="resp_lit6277", - current_conversation_id="conv_lit6277", - output_items=None, - ) - - usage = done_event["response"]["usage"] - assert usage["output_tokens_details"]["audio_tokens"] == 51 - assert usage["output_token_details"]["audio_tokens"] == 51 - - results = [done_event] - combined_usage = RealtimeAPITokenUsageProcessor.collect_and_combine_usage_from_realtime_stream_results( - results=results, - ) - assert combined_usage.completion_tokens_details is not None - assert combined_usage.completion_tokens_details.audio_tokens == 51 - - cost = handle_realtime_stream_cost_calculation( - results=results, - combined_usage_object=combined_usage, - custom_llm_provider="gemini", - litellm_model_name="gemini-2.5-flash-native-audio-preview-12-2025", - ) - model_info: Final = litellm.get_model_info( - model="gemini-2.5-flash-native-audio-preview-12-2025", custom_llm_provider="gemini" - ) - assert cost == pytest.approx( - 377 * model_info["input_cost_per_token"] - + 51 * model_info["output_cost_per_audio_token"] - + 37 * model_info["output_cost_per_token"] - ) - - @pytest.fixture(autouse=False) def patch_gemini_transcribe_live_cost_map_entry(monkeypatch): """Inject the gemini-3.5-transcribe-live registry entry locally. diff --git a/tests/test_litellm/llms/groq/chat/test_groq_chat_transformation.py b/tests/test_litellm/llms/groq/chat/test_groq_chat_transformation.py index d1dd7eb29b5..f605958b979 100644 --- a/tests/test_litellm/llms/groq/chat/test_groq_chat_transformation.py +++ b/tests/test_litellm/llms/groq/chat/test_groq_chat_transformation.py @@ -5,7 +5,6 @@ import httpx import pytest import litellm -from litellm.constants import GROQ_BROWSER_VISIT_WEBSITE_COST_PER_CALL from litellm.litellm_core_utils.llm_cost_calc.tool_call_cost_tracking import ( StandardBuiltInToolCostTracking, ) @@ -204,42 +203,4 @@ class TestGroqWebSearchUsageSignal: GroqChatConfig()._add_web_search_usage(model_response=model_response) assert getattr(model_response, "usage", None) is None - @pytest.mark.usefixtures("local_model_cost_map") - @pytest.mark.parametrize( - "executed_tools, searches, opens", - [ - (EXECUTED_TOOLS_THREE_SEARCHES_TWO_OPENS, 3, 2), - (EXECUTED_TOOLS_OPENS_ONLY, 0, 2), - ], - ) - def test_response_billed_per_action(self, executed_tools: list, searches: int, opens: int): - response = _groq_completion_with_mocked_response(_searched_groq_response(executed_tools)) - assert StandardBuiltInToolCostTracking.response_object_includes_web_search_call( - response_object=response, usage=response.usage - ) - cost = StandardBuiltInToolCostTracking.get_cost_for_built_in_tools( - model="groq/openai/gpt-oss-20b", - response_object=response, - usage=response.usage, - custom_llm_provider="groq", - standard_built_in_tools_params={"web_search_options": {"search_context_size": "high"}}, - ) - model_info = litellm.get_model_info(model="groq/openai/gpt-oss-20b") - expected_cost = ( - searches * model_info["search_context_cost_per_query"]["search_context_size_medium"] - + opens * GROQ_BROWSER_VISIT_WEBSITE_COST_PER_CALL - ) - assert cost == pytest.approx(expected_cost) - -class TestGroqWebSearchCost: - @pytest.mark.usefixtures("local_model_cost_map") - @pytest.mark.parametrize("model", WEB_SEARCH_MODELS) - @pytest.mark.parametrize("search_context_size", ["low", "medium", "high"]) - def test_browser_search_priced_per_search(self, model: str, search_context_size: str): - model_info = litellm.get_model_info(model=model, custom_llm_provider="groq") - cost = StandardBuiltInToolCostTracking.get_cost_for_web_search( - web_search_options={"search_context_size": search_context_size}, - model_info=model_info, - ) - assert cost == model_info["search_context_cost_per_query"][f"search_context_size_{search_context_size}"] diff --git a/tests/test_litellm/llms/inception/test_inception_chat_transformation.py b/tests/test_litellm/llms/inception/test_inception_chat_transformation.py index 830498ff842..1a0340a0a67 100644 --- a/tests/test_litellm/llms/inception/test_inception_chat_transformation.py +++ b/tests/test_litellm/llms/inception/test_inception_chat_transformation.py @@ -7,6 +7,7 @@ import os from unittest import mock import httpx +import pytest import litellm from litellm.llms.inception.chat.transformation import InceptionChatConfig @@ -305,3 +306,5 @@ def test_inception_completion_targets_inception_endpoint(): assert captured["body"]["model"] == "mercury-2" assert captured["body"]["tool_choice"] == "auto" assert response.choices[0].message.content == "hi" + + diff --git a/tests/test_litellm/llms/openai_like/test_cognition_provider.py b/tests/test_litellm/llms/openai_like/test_cognition_provider.py index b2cb613a2e0..9bbbb3b88f2 100644 --- a/tests/test_litellm/llms/openai_like/test_cognition_provider.py +++ b/tests/test_litellm/llms/openai_like/test_cognition_provider.py @@ -8,7 +8,6 @@ its traffic. import json from pathlib import Path -from typing import Final import pytest @@ -112,30 +111,6 @@ class TestCognitionProviderIdentity: class TestCognitionCostTracking: - @pytest.mark.parametrize( - "model", - [ - "cognition/swe-1.7", - "cognition/swe-1.7-lightning", - ], - ) - def test_cost_uses_cognition_entry(self, model: str): - """A cognition-prefixed model must use its cognition cost-map entry.""" - from litellm.cost_calculator import cost_per_token - - prompt_cost, completion_cost = cost_per_token( - model=model, - prompt_tokens=1_000_000, - completion_tokens=1_000_000, - custom_llm_provider="cognition", - ) - - model_info: Final = litellm.model_cost[model] - assert model_info["litellm_provider"] == "cognition" - assert model_info["input_cost_per_token"] > 0 - assert model_info["output_cost_per_token"] > 0 - assert prompt_cost > 0 - assert completion_cost > 0 def test_lightning_is_five_times_the_standard_tier(self): standard = litellm.get_model_info(model="cognition/swe-1.7") @@ -154,61 +129,4 @@ class TestCognitionCostTracking: assert endpoints["embeddings"] is False -class TestCognitionRouting: - @pytest.mark.asyncio - async def test_router_spend_is_attributed_to_cognition_pricing(self): - """Routed traffic is costed off the cognition entry, not an OpenAI one.""" - from litellm import Router - router = Router( - model_list=[ - { - "model_name": "swe", - "litellm_params": {"model": "cognition/swe-1.7", "api_key": "sk-test"}, - } - ] - ) - - response = await router.acompletion( - model="swe", - messages=[{"role": "user", "content": "hi"}], - mock_response="hello from swe", - ) - - usage = response.usage - import litellm - - entry: Final = litellm.model_cost["cognition/swe-1.7"] - expected: Final = usage.prompt_tokens * entry["input_cost_per_token"] + usage.completion_tokens * entry[ - "output_cost_per_token" - ] - assert response._hidden_params["response_cost"] == pytest.approx(expected) - - @pytest.mark.asyncio - async def test_router_spend_uses_the_lightning_entry_for_lightning(self): - """The Lightning tier is its own model, costed off its own entry.""" - from litellm import Router - - router = Router( - model_list=[ - { - "model_name": "swe-lightning", - "litellm_params": {"model": "cognition/swe-1.7-lightning", "api_key": "sk-test"}, - } - ] - ) - - response = await router.acompletion( - model="swe-lightning", - messages=[{"role": "user", "content": "hi"}], - mock_response="hello from swe lightning", - ) - - usage = response.usage - import litellm - - entry: Final = litellm.model_cost["cognition/swe-1.7-lightning"] - expected: Final = usage.prompt_tokens * entry["input_cost_per_token"] + usage.completion_tokens * entry[ - "output_cost_per_token" - ] - assert response._hidden_params["response_cost"] == pytest.approx(expected) diff --git a/tests/test_litellm/llms/openai_like/test_meta_provider.py b/tests/test_litellm/llms/openai_like/test_meta_provider.py index 46f189f2817..0a0ba369e71 100644 --- a/tests/test_litellm/llms/openai_like/test_meta_provider.py +++ b/tests/test_litellm/llms/openai_like/test_meta_provider.py @@ -2,8 +2,6 @@ Tests for the Meta Model API (Muse Spark) provider configuration and integration. """ -from typing import Final - import litellm @@ -194,23 +192,4 @@ class TestMetaAnthropicMessages: assert headers["anthropic-version"] == "2023-06-01" -class TestMuseSparkModelInfo: - def test_muse_spark_cost_calculation(self): - from litellm import completion_cost - from litellm.types.utils import ModelResponse, Usage - - response = ModelResponse( - model="muse-spark-1.1", - usage=Usage(prompt_tokens=1000, completion_tokens=500, total_tokens=1500), - ) - cost = completion_cost( - completion_response=response, - model="meta/muse-spark-1.1", - custom_llm_provider="meta", - ) - model_info: Final = litellm.model_cost["meta/muse-spark-1.1"] - assert model_info["litellm_provider"] == "meta" - assert model_info["input_cost_per_token"] > 0 - assert model_info["output_cost_per_token"] > 0 - assert cost > 0 diff --git a/tests/test_litellm/llms/openai_like/test_tensormesh_provider.py b/tests/test_litellm/llms/openai_like/test_tensormesh_provider.py index adf955f7736..66dd18fc8d7 100644 --- a/tests/test_litellm/llms/openai_like/test_tensormesh_provider.py +++ b/tests/test_litellm/llms/openai_like/test_tensormesh_provider.py @@ -2,8 +2,6 @@ Tests for Tensormesh provider configuration and integration. """ -from typing import Final - import pytest import litellm @@ -156,15 +154,3 @@ class TestTensormeshCostMap: for model in TENSORMESH_MODELS: assert litellm.supports_reasoning(model) is (model in reasoning_models), model - def test_cost_is_wired(self): - prompt_cost, completion_cost = litellm.cost_per_token( - model="tensormesh/openai/gpt-oss-120b", - prompt_tokens=1_000_000, - completion_tokens=1_000_000, - ) - model_info: Final = litellm.model_cost["tensormesh/openai/gpt-oss-120b"] - assert model_info["litellm_provider"] == "tensormesh" - assert model_info["input_cost_per_token"] > 0 - assert model_info["output_cost_per_token"] > 0 - assert prompt_cost > 0 - assert completion_cost > 0 diff --git a/tests/test_litellm/llms/parallel_ai/test_parallel_ai_search.py b/tests/test_litellm/llms/parallel_ai/test_parallel_ai_search.py index 03fda270b6f..2bb07ecca75 100644 --- a/tests/test_litellm/llms/parallel_ai/test_parallel_ai_search.py +++ b/tests/test_litellm/llms/parallel_ai/test_parallel_ai_search.py @@ -3,13 +3,12 @@ Tests for Parallel AI Search API integration (v1 endpoint). """ import json -from typing import Final from unittest.mock import AsyncMock, MagicMock, patch import pytest + import litellm -from litellm.llms.parallel_ai.search.cost_calculator import PARALLEL_AI_ADDITIONAL_RESULT_COST MOCK_V1_RESPONSE = { "search_id": "search_abc123", @@ -432,92 +431,3 @@ class TestParallelAISearch: assert result.snippet == "" assert result.date is None assert result.model_dump()["excerpts"] == () - - @pytest.mark.parametrize( - "mode,usage,max_results", - [ - ("turbo", [{"name": "sku_search", "count": 1}], None), - ("fast", [{"name": "sku_search", "count": 1}], None), - ("basic", [{"name": "sku_search", "count": 1}], None), - ("advanced", [{"name": "sku_search", "count": 1}], None), - ( - "basic", - [ - {"name": "sku_search", "count": 1}, - {"name": "sku_search_additional_results", "count": 2}, - ], - 20, - ), - ("basic", None, 20), - ], - ) - @pytest.mark.asyncio - async def test_search_cost_uses_mode_and_provider_usage( - self, mode, usage, max_results, bundled_cost_map, respx_mock, httpx_transport - ): - response_payload = {**MOCK_V1_RESPONSE, "usage": usage} - respx_mock.post("https://api.parallel.ai/v1/search").respond(json=response_payload) - - response = await litellm.asearch( - query="AI developments", - search_provider="parallel_ai", - mode=mode, - max_results=max_results, - ) - - pricing_model: Final = {"fast": "parallel_ai/search-fast", "turbo": "parallel_ai/search-turbo"}.get( - mode, "parallel_ai/search" - ) - rate: Final = litellm.model_cost[pricing_model]["input_cost_per_query"] - request_count: Final = ( - sum(item["count"] for item in usage if item["name"] == "sku_search") if usage is not None else 1 - ) - additional_results: Final = ( - sum(item["count"] for item in usage if item["name"] == "sku_search_additional_results") - if usage is not None - else max(max_results - 10, 0) - ) - expected_cost: Final = request_count * rate + additional_results * PARALLEL_AI_ADDITIONAL_RESULT_COST - assert response._hidden_params["response_cost"] == pytest.approx(expected_cost) - - @pytest.mark.asyncio - async def test_search_cost_treats_keyword_queries_as_one_request( - self, bundled_cost_map, respx_mock, httpx_transport - ): - response_payload = { - **MOCK_V1_RESPONSE, - "usage": [{"name": "sku_search", "count": 1}], - } - respx_mock.post("https://api.parallel.ai/v1/search").respond(json=response_payload) - - response = await litellm.asearch( - query=["AI developments", "machine learning trends"], - search_provider="parallel_ai", - mode="basic", - ) - - assert response._hidden_params["response_cost"] == pytest.approx( - litellm.model_cost["parallel_ai/search"]["input_cost_per_query"] - ) - - @pytest.mark.asyncio - async def test_caller_cannot_supply_provider_usage(self, bundled_cost_map, respx_mock, httpx_transport): - """`_parallel_ai_usage` prices the request, so a caller must not be able to set it. - - The provider reports no usage here, which is the case where a caller-supplied - value would otherwise survive into the cost calculation. - """ - response_payload = {k: v for k, v in MOCK_V1_RESPONSE.items() if k != "usage"} - route = respx_mock.post("https://api.parallel.ai/v1/search").respond(json=response_payload) - - response = await litellm.asearch( - query="AI developments", - search_provider="parallel_ai", - mode="basic", - _parallel_ai_usage=[{"name": "sku_search", "count": 0}], - ) - - assert response._hidden_params["response_cost"] == pytest.approx( - litellm.model_cost["parallel_ai/search"]["input_cost_per_query"] - ) - assert "_parallel_ai_usage" not in json.loads(route.calls[0].request.content) diff --git a/tests/test_litellm/llms/perplexity/test_perplexity_cost_calculator.py b/tests/test_litellm/llms/perplexity/test_perplexity_cost_calculator.py index a03a3a34397..83c71479311 100644 --- a/tests/test_litellm/llms/perplexity/test_perplexity_cost_calculator.py +++ b/tests/test_litellm/llms/perplexity/test_perplexity_cost_calculator.py @@ -6,7 +6,6 @@ search queries, and reasoning tokens. """ import json -from typing import Final import math import os from datetime import datetime, timezone @@ -141,23 +140,6 @@ class TestPerplexityCostCalculator: assert prompt_cost == 0.0 assert completion_cost == 0.008 - def test_falls_back_to_manual_calculation_when_no_cost_provided(self): - """ - Test that manual cost calculation is used when Perplexity doesn't - provide the cost object (fallback behavior). - """ - usage = Usage(prompt_tokens=100, completion_tokens=50, total_tokens=150) - # No cost object - should use manual calculation - - prompt_cost, completion_cost = perplexity_cost_per_token(model="sonar-deep-research", usage=usage) - - entry: Final = litellm.model_cost["perplexity/sonar-deep-research"] - expected_prompt: Final = 100 * entry["input_cost_per_token"] - expected_completion: Final = 50 * entry["output_cost_per_token"] - - assert math.isclose(prompt_cost, expected_prompt, rel_tol=1e-6) - assert math.isclose(completion_cost, expected_completion, rel_tol=1e-6) - OFF_PEAK_MODEL = "sonar-off-peak-test" OFF_PEAK_WINDOW = "14:00-00:00" INSIDE_WINDOW = datetime(2026, 9, 3, 17, 25, tzinfo=timezone.utc) diff --git a/tests/test_litellm/llms/perplexity/test_perplexity_integration.py b/tests/test_litellm/llms/perplexity/test_perplexity_integration.py index 45fb51c82bd..670fe096278 100644 --- a/tests/test_litellm/llms/perplexity/test_perplexity_integration.py +++ b/tests/test_litellm/llms/perplexity/test_perplexity_integration.py @@ -6,7 +6,6 @@ including integration with the main LiteLLM cost calculator. """ import json -from typing import Final import math import os @@ -151,26 +150,3 @@ class TestPerplexityIntegration: assert hasattr(model_response.usage, "prompt_tokens_details") assert hasattr(model_response.usage, "citation_tokens") assert model_response.usage.prompt_tokens_details.web_search_requests == 3 - - @pytest.mark.parametrize("provider_name", ["perplexity", "PERPLEXITY", "Perplexity"]) - def test_case_insensitive_provider_matching(self, provider_name): - """Test that cost calculation works with different case variations of provider name.""" - usage = Usage(prompt_tokens=100, completion_tokens=50, total_tokens=150) - usage.citation_tokens = 10 - usage.prompt_tokens_details = PromptTokensDetailsWrapper(web_search_requests=1) - - # Should work regardless of case - prompt_cost, completion_cost_val = cost_per_token( - model="sonar-deep-research", - custom_llm_provider=provider_name.lower(), # Normalize to lowercase - usage_object=usage, - ) - - entry: Final = litellm.model_cost["perplexity/sonar-deep-research"] - expected_prompt_cost: Final = (100 * entry["input_cost_per_token"]) + (10 * entry["citation_cost_per_token"]) - expected_completion_cost: Final = (50 * entry["output_cost_per_token"]) + ( - 1 * entry["search_context_cost_per_query"]["search_context_size_low"] - ) - - assert math.isclose(prompt_cost, expected_prompt_cost, rel_tol=1e-6) - assert math.isclose(completion_cost_val, expected_completion_cost, rel_tol=1e-6) diff --git a/tests/test_litellm/llms/soniox/audio_transcription/test_soniox_audio_transcription_handler.py b/tests/test_litellm/llms/soniox/audio_transcription/test_soniox_audio_transcription_handler.py index d6bc975d90d..d2d7d2247f1 100644 --- a/tests/test_litellm/llms/soniox/audio_transcription/test_soniox_audio_transcription_handler.py +++ b/tests/test_litellm/llms/soniox/audio_transcription/test_soniox_audio_transcription_handler.py @@ -2,7 +2,7 @@ import asyncio import json -from typing import Any, Dict, Final, List +from typing import Any, Dict, List from unittest.mock import MagicMock import httpx @@ -1056,44 +1056,3 @@ class TestSpendTracking: litellm.model_cost = original_model_cost litellm.get_model_info.cache_clear() - def test_should_charge_by_audio_duration(self, monkeypatch): - import litellm - - monkeypatch.setattr("time.sleep", lambda *_: None) - responses = { - "POST https://api.soniox.com/v1/transcriptions": [ - _make_response({"id": "tx_1", "status": "queued"}) - ], - "GET https://api.soniox.com/v1/transcriptions/tx_1": [ - _make_response( - {"id": "tx_1", "status": "completed", "audio_duration_ms": 600000} - ), - ], - "GET https://api.soniox.com/v1/transcriptions/tx_1/transcript": [ - _make_response({"text": "hello world", "tokens": []}), - ], - "DELETE https://api.soniox.com/v1/transcriptions/tx_1": [ - _make_response({"deleted": True}), - ], - } - - resp = SonioxAudioTranscriptionHandler().audio_transcriptions( - audio_file=None, - optional_params={"audio_url": "https://example.com/a.wav"}, - litellm_params={}, - atranscription=False, - **_common_call_kwargs(_MockSyncClient(responses)), - ) - - assert resp._hidden_params["audio_transcription_duration"] == pytest.approx( - 600.0 - ) - - cost = litellm.completion_cost( - completion_response=resp, - model="soniox/stt-async-v4", - call_type="transcription", - ) - assert cost > 0 - model_info: Final = litellm.get_model_info(model="soniox/stt-async-v4") - assert model_info["output_cost_per_second"] > 0 diff --git a/tests/test_litellm/llms/vertex_ai/audio_transcription/test_vertex_ai_audio_transcription_transformation.py b/tests/test_litellm/llms/vertex_ai/audio_transcription/test_vertex_ai_audio_transcription_transformation.py index 5a3c2612ceb..5898d933941 100644 --- a/tests/test_litellm/llms/vertex_ai/audio_transcription/test_vertex_ai_audio_transcription_transformation.py +++ b/tests/test_litellm/llms/vertex_ai/audio_transcription/test_vertex_ai_audio_transcription_transformation.py @@ -1,10 +1,12 @@ import base64 import json +import os from urllib.parse import urlparse import httpx import pytest + import litellm from litellm.llms.vertex_ai.audio_transcription.transformation import ( VertexAIAudioTranscriptionConfig, @@ -20,16 +22,6 @@ def config(): class TestGetCompleteUrl: - def test_defaults_to_us_regional_host(self, config): - url = config.get_complete_url( - api_base=None, - api_key=None, - model="chirp_3", - optional_params={}, - litellm_params={"vertex_project": "test-project"}, - ) - assert url == "https://us-speech.googleapis.com/v2/projects/test-project/locations/us/recognizers/_:recognize" - def test_uses_vertex_location_for_regional_host(self, config): url = config.get_complete_url( api_base=None, @@ -50,16 +42,6 @@ class TestGetCompleteUrl: ) assert url == "https://speech.googleapis.com/v2/projects/test-project/locations/global/recognizers/_:recognize" - def test_api_base_override(self, config): - url = config.get_complete_url( - api_base="http://localhost:8080/", - api_key=None, - model="chirp_3", - optional_params={}, - litellm_params={"vertex_project": "test-project"}, - ) - assert url == "http://localhost:8080/v2/projects/test-project/locations/us/recognizers/_:recognize" - @pytest.mark.parametrize( "location,expected_netloc", [ @@ -311,3 +293,7 @@ class TestProviderRouting: ) assert "response_format" not in optional_params assert optional_params["language"] == "fr-FR" + + +class TestModelCostEntry: + REPO_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "../../../../..")) diff --git a/tests/test_litellm/llms/vertex_ai/audio_transcription/test_vertex_ai_gemini_transcribe_transformation.py b/tests/test_litellm/llms/vertex_ai/audio_transcription/test_vertex_ai_gemini_transcribe_transformation.py index 2e4eaa03a0a..82ea034f91b 100644 --- a/tests/test_litellm/llms/vertex_ai/audio_transcription/test_vertex_ai_gemini_transcribe_transformation.py +++ b/tests/test_litellm/llms/vertex_ai/audio_transcription/test_vertex_ai_gemini_transcribe_transformation.py @@ -1,5 +1,6 @@ import base64 import json +import os import httpx import pytest @@ -304,3 +305,7 @@ class TestOptionalParams: ) assert "response_format" not in optional_params assert optional_params["language"] == "fr-FR" + + +class TestModelCostEntry: + REPO_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "../../../../..")) diff --git a/tests/test_litellm/llms/vertex_ai/gemini_embeddings/test_batch_embed_content_transformation.py b/tests/test_litellm/llms/vertex_ai/gemini_embeddings/test_batch_embed_content_transformation.py index a6d160eda90..ba2b26bf0a2 100644 --- a/tests/test_litellm/llms/vertex_ai/gemini_embeddings/test_batch_embed_content_transformation.py +++ b/tests/test_litellm/llms/vertex_ai/gemini_embeddings/test_batch_embed_content_transformation.py @@ -316,9 +316,6 @@ class TestProcessEmbedContentResponseUsage: MODEL = "gemini-embedding-2" - def _rate(self, model: str, field: str) -> float: - return float(litellm.get_model_info(model=model, custom_llm_provider="vertex_ai")[field]) - def test_multimodal_image_preserves_usage_metadata(self): response_json = { "embedding": {"values": [0.1, 0.2, 0.3]}, @@ -410,230 +407,4 @@ class TestProcessEmbedContentResponseUsage: ) assert result.usage.prompt_tokens > 0 - def test_file_reference_image_billed_per_image_token_rate(self): - response_json = { - "embedding": {"values": [0.1, 0.2, 0.3]}, - "usageMetadata": { - "promptTokenCount": 258, - "totalTokenCount": 258, - "promptTokensDetails": [{"modality": "IMAGE", "tokenCount": 258}], - }, - } - result = process_embed_content_response( - input=["files/img123"], - model_response=EmbeddingResponse(), - model=self.MODEL, - response_json=response_json, - resolved_files={ - "files/img123": { - "mime_type": "image/png", - "uri": "https://example.com/img123", - } - }, - ) - assert result.usage.prompt_tokens_details.image_tokens == 258 - assert result.usage.prompt_tokens_details.text_tokens == 0 - prompt_cost, _ = generic_cost_per_token( - model=self.MODEL, - usage=result.usage, - custom_llm_provider="vertex_ai", - ) - assert prompt_cost == pytest.approx(258 * self._rate(self.MODEL, "input_cost_per_image_token")) - - def test_file_reference_non_image_not_counted_as_image(self): - """A files/... ref resolving to a non-image mime keeps audio token billing.""" - response_json = { - "embedding": {"values": [0.1, 0.2]}, - "usageMetadata": { - "promptTokenCount": 64, - "totalTokenCount": 64, - "promptTokensDetails": [{"modality": "AUDIO", "tokenCount": 64}], - }, - } - result = process_embed_content_response( - input=["files/clip1"], - model_response=EmbeddingResponse(), - model=self.MODEL, - response_json=response_json, - resolved_files={ - "files/clip1": { - "mime_type": "audio/mpeg", - "uri": "https://example.com/clip1", - } - }, - ) - assert result.usage.prompt_tokens_details.audio_tokens == 64 - assert result.usage.prompt_tokens_details.image_tokens == 0 - - prompt_cost, _ = generic_cost_per_token( - model=self.MODEL, - usage=result.usage, - custom_llm_provider="vertex_ai", - ) - assert prompt_cost == pytest.approx(64 * self._rate(self.MODEL, "input_cost_per_audio_token")) - - def test_video_plus_audio_does_not_double_bill_text(self): - """Video and audio responses are billed from their respective token counts.""" - response_json = { - "embedding": {"values": [0.1]}, - "usageMetadata": { - "promptTokenCount": 580, - "totalTokenCount": 580, - "promptTokensDetails": [ - {"modality": "VIDEO", "tokenCount": 516}, - {"modality": "AUDIO", "tokenCount": 64}, - ], - }, - } - result = process_embed_content_response( - input=["gs://bucket/clip.mp4"], - model_response=EmbeddingResponse(), - model=self.MODEL, - response_json=response_json, - ) - assert result.usage.prompt_tokens_details.text_tokens == 0 - assert result.usage.prompt_tokens_details.video_tokens == 516 - assert result.usage.prompt_tokens_details.audio_tokens == 64 - - prompt_cost, _ = generic_cost_per_token( - model=self.MODEL, - usage=result.usage, - custom_llm_provider="vertex_ai", - ) - assert prompt_cost == pytest.approx( - 516 * self._rate(self.MODEL, "input_cost_per_video_token") - + 64 * self._rate(self.MODEL, "input_cost_per_audio_token") - ) - - def test_preview_alias_bills_audio_per_token(self): - response_json = { - "embedding": {"values": [0.1]}, - "usageMetadata": { - "promptTokenCount": 64, - "totalTokenCount": 64, - "promptTokensDetails": [{"modality": "AUDIO", "tokenCount": 64}], - }, - } - result = process_embed_content_response( - input="audio", - model_response=EmbeddingResponse(), - model="gemini-embedding-2-preview", - response_json=response_json, - ) - prompt_cost, _ = generic_cost_per_token( - model="gemini-embedding-2-preview", - usage=result.usage, - custom_llm_provider="vertex_ai", - ) - assert prompt_cost == pytest.approx(64 * self._rate("gemini-embedding-2-preview", "input_cost_per_audio_token")) - - def test_image_without_modality_details_uses_image_rate(self): - response_json = { - "embedding": {"values": [0.1]}, - "usageMetadata": { - "promptTokenCount": 258, - "totalTokenCount": 258, - }, - } - result = process_embed_content_response( - input=IMAGE_DATA_URI, - model_response=EmbeddingResponse(), - model=self.MODEL, - response_json=response_json, - ) - assert result.usage.prompt_tokens_details.image_tokens == 258 - assert result.usage.prompt_tokens_details.text_tokens == 0 - - prompt_cost, _ = generic_cost_per_token( - model=self.MODEL, - usage=result.usage, - custom_llm_provider="vertex_ai", - ) - assert prompt_cost == pytest.approx(258 * self._rate(self.MODEL, "input_cost_per_image_token")) - - @pytest.mark.parametrize( - "input_value,resolved_files,expected_image_tokens", - [ - (GCS_URL, {}, 258), - ("gs://my-bucket/clip.mp4", {}, 0), - ("gs://my-bucket/unknown.bin", {}, 0), - ("files/image-123", {"files/image-123": {"mime_type": "image/jpeg"}}, 258), - ("files/missing", {}, 0), - ("data:application/octet-stream;base64,abc", {}, 0), - ([[IMAGE_DATA_URI]], {}, 258), - ([], {}, 0), - ], - ) - def test_missing_modality_details_classifies_image_inputs(self, input_value, resolved_files, expected_image_tokens): - response_json = { - "embedding": {"values": [0.1]}, - "usageMetadata": { - "promptTokenCount": 258, - "totalTokenCount": 258, - }, - } - result = process_embed_content_response( - input=input_value, - model_response=EmbeddingResponse(), - model=self.MODEL, - response_json=response_json, - resolved_files=resolved_files, - ) - assert result.usage.prompt_tokens_details.image_tokens == expected_image_tokens - assert result.usage.prompt_tokens_details.text_tokens == 0 - - prompt_cost, _ = generic_cost_per_token( - model=self.MODEL, - usage=result.usage, - custom_llm_provider="vertex_ai", - ) - expected_field = "input_cost_per_image_token" if expected_image_tokens else "input_cost_per_token" - assert prompt_cost == pytest.approx(258 * self._rate(self.MODEL, expected_field)) - - def test_mixed_text_and_image_without_modality_details_not_billed_as_image(self): - response_json = { - "embedding": {"values": [0.1]}, - "usageMetadata": { - "promptTokenCount": 270, - "totalTokenCount": 270, - }, - } - result = process_embed_content_response( - input=["a short caption", IMAGE_DATA_URI], - model_response=EmbeddingResponse(), - model=self.MODEL, - response_json=response_json, - ) - assert result.usage.prompt_tokens_details.image_tokens == 0 - - prompt_cost, _ = generic_cost_per_token( - model=self.MODEL, - usage=result.usage, - custom_llm_provider="vertex_ai", - ) - assert prompt_cost == pytest.approx(270 * self._rate(self.MODEL, "input_cost_per_token")) - - def test_text_without_modality_details_uses_text_rate(self): - response_json = { - "embedding": {"values": [0.1]}, - "usageMetadata": { - "promptTokenCount": 12, - "totalTokenCount": 12, - }, - } - result = process_embed_content_response( - input="a short caption", - model_response=EmbeddingResponse(), - model=self.MODEL, - response_json=response_json, - ) - assert result.usage.prompt_tokens_details.text_tokens == 0 - assert result.usage.prompt_tokens_details.image_tokens == 0 - - prompt_cost, _ = generic_cost_per_token( - model=self.MODEL, - usage=result.usage, - custom_llm_provider="vertex_ai", - ) - assert prompt_cost == pytest.approx(12 * self._rate(self.MODEL, "input_cost_per_token")) diff --git a/tests/test_litellm/llms/vertex_ai/test_vertex_passthrough_logging_handler.py b/tests/test_litellm/llms/vertex_ai/test_vertex_passthrough_logging_handler.py index 59ba429a84d..58e7529309a 100644 --- a/tests/test_litellm/llms/vertex_ai/test_vertex_passthrough_logging_handler.py +++ b/tests/test_litellm/llms/vertex_ai/test_vertex_passthrough_logging_handler.py @@ -234,60 +234,8 @@ def test_audio_predict_response_supports_bytes_base64_encoded( request_body={"instances": [{"prompt": "ambient piano"}]}, ) - expected_cost: Final = litellm.model_cost["vertex_ai/lyria-002"]["output_cost_per_image"] - assert result["kwargs"]["response_cost"] == pytest.approx(expected_cost) - assert logging_obj.model_call_details["response_cost"] == pytest.approx(expected_cost) - - -@pytest.mark.parametrize("runtime_entry_is_missing", (True, False)) -def test_lyria_predict_cost_falls_back_to_bundled_map_when_runtime_metadata_is_incomplete( - monkeypatch: pytest.MonkeyPatch, - runtime_entry_is_missing: bool, - local_model_cost_map: None, -) -> None: - expected_cost: Final = litellm.model_cost["vertex_ai/lyria-002"]["output_cost_per_image"] - if runtime_entry_is_missing: - monkeypatch.delitem(litellm.model_cost, "vertex_ai/lyria-002") - else: - monkeypatch.setitem( - litellm.model_cost, - "vertex_ai/lyria-002", - { - key: value - for key, value in litellm.model_cost["vertex_ai/lyria-002"].items() - if key != "output_cost_per_image" - }, - ) - logging_obj = MagicMock() - logging_obj.model_call_details = {} - response = httpx.Response( - status_code=200, - json={ - "predictions": [ - { - "audioContent": "clip", - "mimeType": "audio/wav", - } - ] - }, - ) - - result = VertexPassthroughLoggingHandler.vertex_passthrough_handler( - httpx_response=response, - logging_obj=logging_obj, - url_route="/v1/projects/test/locations/us-central1/publishers/google/models/lyria-002:predict", - result=response.text, - start_time=datetime.now(), - end_time=datetime.now(), - cache_hit=False, - request_body={"instances": [{"prompt": "ambient piano"}]}, - ) - - if runtime_entry_is_missing: - assert "vertex_ai/lyria-002" not in litellm.model_cost - assert result["kwargs"]["model"] == "lyria-002" - assert result["kwargs"]["response_cost"] == pytest.approx(expected_cost) - assert logging_obj.model_call_details["response_cost"] == pytest.approx(expected_cost) + assert result["kwargs"]["response_cost"] == pytest.approx(0.06) + assert logging_obj.model_call_details["response_cost"] == pytest.approx(0.06) def test_image_predict_response_is_not_billed_as_audio( diff --git a/tests/test_litellm/llms/vertex_ai/videos/test_vertex_video_transformation.py b/tests/test_litellm/llms/vertex_ai/videos/test_vertex_video_transformation.py index c5e2ffb36d8..b6b638c6dbe 100644 --- a/tests/test_litellm/llms/vertex_ai/videos/test_vertex_video_transformation.py +++ b/tests/test_litellm/llms/vertex_ai/videos/test_vertex_video_transformation.py @@ -6,7 +6,7 @@ import base64 import json from collections.abc import Mapping from pathlib import Path -from typing import Final, cast +from typing import cast from unittest.mock import Mock, patch import httpx @@ -123,18 +123,6 @@ class TestVertexAIVideoConfig: model="veo-002", api_base=None, litellm_params={} ) - def test_get_complete_url_default_location(self): - """Test URL construction with default location.""" - litellm_params = {"vertex_project": "test-project"} - - url = self.config.get_complete_url( - model="veo-002", api_base=None, litellm_params=litellm_params - ) - - # Should default to us-central1 - assert "us-central1" in url - # Should NOT include endpoint - assert not url.endswith(":predictLongRunning") def test_veo_31_lite_provider_routing_from_local_model_map( self, monkeypatch: pytest.MonkeyPatch @@ -154,27 +142,6 @@ class TestVertexAIVideoConfig: assert model == "veo-3.1-lite-generate-001" assert custom_llm_provider == "vertex_ai" - def test_veo_31_lite_cost_uses_resolution_tiers(self): - model_cost: Final = _load_model_cost_map(BACKUP_MODEL_COST_PATH) - model_info: Final = model_cost[VEO_31_LITE_VERTEX_MODEL] - standard_cost: Final = video_generation_cost( - model=VEO_31_LITE_VERTEX_MODEL, - duration_seconds=10.0, - custom_llm_provider="vertex_ai", - model_info=dict(model_info), - video_resolution="720p", - ) - high_resolution_cost: Final = video_generation_cost( - model=VEO_31_LITE_VERTEX_MODEL, - duration_seconds=10.0, - custom_llm_provider="vertex_ai", - model_info=dict(model_info), - video_resolution="1080p", - ) - - assert standard_cost == pytest.approx(10.0 * model_info["output_cost_per_second"]) - assert high_resolution_cost == pytest.approx(10.0 * model_info["output_cost_per_second_1080p"]) - assert standard_cost != high_resolution_cost def test_transform_video_create_request(self): """Test transformation of video creation request.""" diff --git a/tests/test_litellm/llms/xai/test_xai_redirected_slug_pricing.py b/tests/test_litellm/llms/xai/test_xai_redirected_slug_pricing.py index 83e8925f70b..bbbcfb1b9dc 100644 --- a/tests/test_litellm/llms/xai/test_xai_redirected_slug_pricing.py +++ b/tests/test_litellm/llms/xai/test_xai_redirected_slug_pricing.py @@ -85,6 +85,11 @@ def test_code_slug_bills_at_grok_build_rate(cost_map: dict, slug: str): assert entry[field] == target[field], field +def test_a_live_xai_model_is_untouched(cost_map: dict): + """Guard against the repricing leaking onto models xAI still serves directly.""" + assert cost_map["xai/grok-4.6"]["input_cost_per_token"] != cost_map[REDIRECT_TARGET]["input_cost_per_token"] + + @pytest.mark.parametrize("slug", REDIRECTED_SLUGS) def test_redirected_slug_carries_the_target_tier_rates(cost_map: dict, slug: str): """The request executes as grok-4.3, so it is tiered at grok-4.3's 200k boundary.""" diff --git a/tests/test_litellm/llms/zai/test_zai_provider.py b/tests/test_litellm/llms/zai/test_zai_provider.py index 5d7b45e739f..32849d5eef1 100644 --- a/tests/test_litellm/llms/zai/test_zai_provider.py +++ b/tests/test_litellm/llms/zai/test_zai_provider.py @@ -3,7 +3,6 @@ Tests for Z.AI (Zhipu AI) provider - GLM models """ import math -from typing import Final import pytest @@ -56,25 +55,6 @@ def test_zai_in_provider_lists(): assert "zai" in litellm.provider_list -@pytest.mark.parametrize("model", ["zai/glm-4.6", "zai/glm-4.7"]) -def test_zai_glm_cost_calculation(local_model_cost_map, model): - """Test the cost calculation picks the model's own cost-map entry""" - - prompt_cost, completion_cost = cost_per_token( - model=model, - prompt_tokens=1000000, # 1M tokens - completion_tokens=1000000, - ) - - entry: Final = litellm.model_cost[model] - assert math.isclose( - prompt_cost, 1000000 * entry["input_cost_per_token"], rel_tol=1e-6 - ) - assert math.isclose( - completion_cost, 1000000 * entry["output_cost_per_token"], rel_tol=1e-6 - ) - - @pytest.mark.asyncio async def test_zai_completion_call(respx_mock, zai_response, monkeypatch): """Test completion call with zai provider using mocked response""" diff --git a/tests/test_litellm/proxy/common_utils/test_prompt_cache_pricing.py b/tests/test_litellm/proxy/common_utils/test_prompt_cache_pricing.py index 52d388fbad0..01b18c1ed71 100644 --- a/tests/test_litellm/proxy/common_utils/test_prompt_cache_pricing.py +++ b/tests/test_litellm/proxy/common_utils/test_prompt_cache_pricing.py @@ -1,5 +1,3 @@ -from collections.abc import Mapping -from copy import deepcopy from typing import Final import pytest @@ -9,53 +7,8 @@ from litellm.proxy.common_utils.prompt_cache_pricing import price_cache_tokens from litellm.types.management_endpoints.prompt_cache_prediction import CacheTokenBuckets -def _tiered_rate(entry: Mapping[str, float | None], field: str, total: int) -> float: - above_rate: Final = entry.get(f"{field}_above_200k_tokens") if total > 200_000 else None - rate: Final = above_rate if above_rate is not None else entry[field] - assert rate is not None - return rate - - -def _expected_cache_cost(model: str, tokens: CacheTokenBuckets) -> float: - key: Final = litellm.get_model_info(model=model, custom_llm_provider="anthropic")["key"] - entry: Final = litellm.model_cost[key] - total: Final = tokens.total_tokens - return ( - tokens.uncached_input_tokens * _tiered_rate(entry, "input_cost_per_token", total) - + tokens.cache_read_input_tokens * _tiered_rate(entry, "cache_read_input_token_cost", total) - + tokens.cache_creation_5m_input_tokens * _tiered_rate(entry, "cache_creation_input_token_cost", total) - + tokens.cache_creation_1h_input_tokens - * _tiered_rate(entry, "cache_creation_input_token_cost_above_1hr", total) - ) - - -@pytest.mark.parametrize("model", ["anthropic/claude-sonnet-4-5", "anthropic/claude-sonnet-4-6"]) -def test_prices_all_cache_buckets_at_total_context_tier(model: str) -> None: - tokens: Final = CacheTokenBuckets( - uncached_input_tokens=100_000, - cache_read_input_tokens=50_000, - cache_creation_5m_input_tokens=20_000, - cache_creation_1h_input_tokens=40_000, - ) - assert price_cache_tokens(model, "unconfigured-deployment", tokens) == pytest.approx( - _expected_cache_cost(model, tokens) - ) - - -@pytest.mark.parametrize("total", [200_000, 200_001]) -def test_long_context_tier_starts_above_threshold(total: int) -> None: - model: Final = "anthropic/claude-sonnet-4-5" - tokens: Final = CacheTokenBuckets( - uncached_input_tokens=total - 100_000, - cache_creation_1h_input_tokens=10_000, - cache_read_input_tokens=90_000, - ) - actual: Final = price_cache_tokens(model, "unconfigured-deployment", tokens) - assert actual == pytest.approx(_expected_cache_cost(model, tokens)) - - def test_deployment_tariff_wins_without_proxy_discounts_or_margins(monkeypatch: pytest.MonkeyPatch) -> None: - monkeypatch.setattr(litellm, "model_cost", deepcopy(litellm.model_cost)) + monkeypatch.setattr(litellm, "model_cost", litellm.model_cost.copy()) litellm.Router( model_list=[ { diff --git a/tests/test_litellm/proxy/management_endpoints/test_prompt_cache_prediction.py b/tests/test_litellm/proxy/management_endpoints/test_prompt_cache_prediction.py index 0606690aa37..987cacf7676 100644 --- a/tests/test_litellm/proxy/management_endpoints/test_prompt_cache_prediction.py +++ b/tests/test_litellm/proxy/management_endpoints/test_prompt_cache_prediction.py @@ -30,40 +30,6 @@ _PROVIDER_KEY: Final = "cache-prediction-test-provider-key" _CALLER: Final = "cache-prediction-test-caller-hash" -def _bucket_cost( - model: str, - *, - uncached: int = 0, - cache_read: int = 0, - write_5m: int = 0, - write_1h: int = 0, -) -> float: - entry: Final = litellm.model_cost[model] - return ( - uncached * entry["input_cost_per_token"] - + cache_read * entry["cache_read_input_token_cost"] - + write_5m * entry["cache_creation_input_token_cost"] - + write_1h * entry["cache_creation_input_token_cost_above_1hr"] - ) - - -_SONNET_COLD: Final = 1_000 -_SONNET_OBSERVED: Final = 5_000 - - -def _cold_cost(model: str, ttl: str) -> float: - return _bucket_cost( - model, - uncached=_SONNET_COLD, - write_5m=_SONNET_OBSERVED if ttl == "5m" else 0, - write_1h=_SONNET_OBSERVED if ttl == "1h" else 0, - ) - - -def _warm_cost(model: str, cached_tokens: int = _SONNET_OBSERVED, total: int = 6_000) -> float: - return _bucket_cost(model, uncached=total - cached_tokens, cache_read=cached_tokens) - - @pytest.fixture(autouse=True) def anthropic_endpoint_environment(monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.delenv("ANTHROPIC_API_BASE", raising=False) @@ -144,57 +110,6 @@ async def _observe( await cache.async_set_cache(_cache_key(scope, prefix.fingerprint), observation.model_dump_json(), ttl=3_600) -@pytest.mark.asyncio -@pytest.mark.parametrize("ttl", ["5m", "1h"]) -async def test_unobserved_cache_prices_cold_and_warm_bounds(ttl: str) -> None: - body: Final = _body(ttl) - arm: Final = await endpoint.predict_arm(_deployment(), body, _prefix(body), _CALLER, DualCache(), Counts()) - cold_cost: Final = _cold_cost("claude-sonnet-5", ttl) - - assert arm.cache_state == "unknown" - assert arm.reason == "no_compatible_observation" - assert arm.evidence is None - assert arm.estimate is not None and arm.cold is not None and arm.warm is not None - assert arm.estimate.input_cost == pytest.approx(cold_cost) - assert arm.cold.input_cost == pytest.approx(cold_cost) - assert arm.warm.input_cost == pytest.approx(_warm_cost("claude-sonnet-5")) - assert arm.cold.tokens.uncached_input_tokens == 1_000 - assert arm.cold.tokens.cache_read_input_tokens == 0 - assert arm.cold.tokens.cache_creation_5m_input_tokens == (5_000 if ttl == "5m" else 0) - assert arm.cold.tokens.cache_creation_1h_input_tokens == (5_000 if ttl == "1h" else 0) - assert arm.warm.tokens.cache_read_input_tokens == 5_000 - - -@pytest.mark.asyncio -@pytest.mark.parametrize("cached_tokens", [5_400, 4_600]) -@pytest.mark.parametrize("expired", [False, True]) -async def test_exact_prefix_conserves_total_with_observed_count_in_all_scenarios( - cached_tokens: int, expired: bool -) -> None: - cache: Final = DualCache() - body: Final = _body() - await _observe(cache, body, cached_tokens=cached_tokens, expired=expired) - arm: Final = await endpoint.predict_arm(_deployment(), body, _prefix(body), _CALLER, cache, Counts()) - - assert arm.cache_state == ("stale" if expired else "warm") - assert arm.evidence is not None - assert arm.estimate is not None and arm.warm is not None and arm.cold is not None - assert arm.warm.tokens.cache_read_input_tokens == cached_tokens - assert arm.warm.tokens.cache_creation_5m_input_tokens == 0 - assert arm.cold.tokens.cache_creation_5m_input_tokens == cached_tokens - assert arm.cold.tokens.cache_read_input_tokens == 0 - for scenario in (arm.estimate, arm.cold, arm.warm): - assert scenario.tokens.total_tokens == 6_000 - assert scenario.tokens.uncached_input_tokens == 6_000 - cached_tokens - warm_cost: Final = _warm_cost("claude-sonnet-5", cached_tokens) - cold_cost: Final = _bucket_cost( - "claude-sonnet-5", uncached=6_000 - cached_tokens, write_5m=cached_tokens - ) - assert arm.warm.input_cost == pytest.approx(warm_cost) - assert arm.cold.input_cost == pytest.approx(cold_cost) - assert arm.estimate.input_cost == pytest.approx(cold_cost if expired else warm_cost) - - @pytest.mark.asyncio async def test_observed_prefix_larger_than_full_request_returns_unknown() -> None: cache: Final = DualCache() @@ -207,29 +122,6 @@ async def test_observed_prefix_larger_than_full_request_returns_unknown() -> Non assert arm.estimate is None and arm.cold is None and arm.warm is None -@pytest.mark.asyncio -@pytest.mark.parametrize("ttl", ["5m", "1h"]) -async def test_append_only_prefix_reads_old_tokens_and_writes_extension(ttl: str) -> None: - cache: Final = DualCache() - await _observe(cache, _body(ttl), cached_tokens=4_000) - body: Final = _body(ttl, extended=True) - arm: Final = await endpoint.predict_arm(_deployment(), body, _prefix(body), _CALLER, cache, Counts()) - - assert arm.cache_state == "partial" - assert arm.estimate is not None - assert arm.estimate.tokens.cache_read_input_tokens == 4_000 - assert arm.estimate.tokens.cache_creation_5m_input_tokens == (1_000 if ttl == "5m" else 0) - assert arm.estimate.tokens.cache_creation_1h_input_tokens == (1_000 if ttl == "1h" else 0) - expected: Final = _bucket_cost( - "claude-sonnet-5", - uncached=1_000, - cache_read=4_000, - write_5m=1_000 if ttl == "5m" else 0, - write_1h=1_000 if ttl == "1h" else 0, - ) - assert arm.estimate.input_cost == pytest.approx(expected) - - @pytest.mark.asyncio async def test_expired_observation_estimates_a_cold_rebuild() -> None: cache: Final = DualCache() @@ -246,22 +138,6 @@ async def test_expired_observation_estimates_a_cold_rebuild() -> None: assert arm.estimate.input_cost == arm.cold.input_cost -@pytest.mark.asyncio -async def test_below_model_minimum_prices_all_input_as_uncached() -> None: - body: Final = _body() - arm: Final = await endpoint.predict_arm( - _deployment(), body, _prefix(body), _CALLER, DualCache(), Counts(total=1_500, prefix=1_000) - ) - - assert arm.cache_state == "disabled" - assert arm.reason == "below_cache_minimum" - assert arm.estimate is not None - assert arm.estimate.tokens.uncached_input_tokens == 1_500 - assert arm.estimate.tokens.cache_read_input_tokens == 0 - assert arm.estimate.tokens.cache_creation_5m_input_tokens == 0 - assert arm.estimate.input_cost == pytest.approx(_bucket_cost("claude-sonnet-5", uncached=1_500)) - - @pytest.mark.asyncio @pytest.mark.parametrize("counts", [Counts(total=None), Counts(prefix=None), Counts(total=4_000)]) async def test_unavailable_or_inconsistent_token_counts_return_null_estimates(counts: Counts) -> None: @@ -313,20 +189,6 @@ async def test_custom_api_base_from_environment_returns_unknown_before_counting( assert arm.estimate is None and arm.cold is None and arm.warm is None -@pytest.mark.asyncio -async def test_explicit_official_api_base_overrides_custom_environment(monkeypatch: pytest.MonkeyPatch) -> None: - monkeypatch.setenv("ANTHROPIC_API_BASE", "https://custom.invalid") - body: Final = _body() - arm: Final = await endpoint.predict_arm( - _deployment(api_base="https://api.anthropic.com"), body, _prefix(body), _CALLER, DualCache(), Counts() - ) - - assert arm.cache_state == "unknown" - assert arm.reason == "no_compatible_observation" - assert arm.estimate is not None - assert arm.estimate.input_cost == pytest.approx(_cold_cost("claude-sonnet-5", "5m")) - - @dataclass(frozen=True) class _ProxyLogging: internal_usage_cache: InternalUsageCache @@ -387,39 +249,6 @@ async def _post( ) -@pytest.mark.asyncio -@pytest.mark.parametrize("warm_deployment", ["sonnet", "opus"]) -async def test_switch_delta_accounts_for_each_deployment_cache( - monkeypatch: pytest.MonkeyPatch, - warm_deployment: str, -) -> None: - warm_model: Final = "claude-sonnet-5" if warm_deployment == "sonnet" else "claude-opus-5" - sonnet_cold: Final = _cold_cost("claude-sonnet-5", "5m") - sonnet_warm: Final = _warm_cost("claude-sonnet-5") - opus_cold: Final = _cold_cost("claude-opus-5", "5m") - opus_warm: Final = _warm_cost("claude-opus-5") - expected_delta: Final = sonnet_warm - opus_cold if warm_deployment == "sonnet" else sonnet_cold - opus_warm - expected_penalty: Final = sonnet_cold - sonnet_warm if warm_deployment == "opus" else 0.0 - cache: Final = DualCache() - body: Final = _body() - await _observe(cache, body, deployment_id=warm_deployment, model=warm_model) - app: Final = _app(monkeypatch, cache, caller=UserAPIKeyAuth(api_key=_CALLER)) - response: Final = await _post(app, body) - - assert response.status_code == 200, response.text - result: Final = CachePredictionResponse.model_validate(response.json()) - assert result.switch_delta == pytest.approx(expected_delta) - assert result.cache_rebuild_penalty == pytest.approx(expected_penalty) - assert result.cache_guarantee is False - assert result.pricing_basis == "input_before_discounts_and_margins" - if warm_deployment == "sonnet": - assert result.switch.cache_state == "warm" - assert result.stay.cache_state == "unknown" - else: - assert result.stay.cache_state == "warm" - assert result.switch.cache_state == "unknown" - - @pytest.mark.asyncio async def test_missing_caller_identity_cannot_reuse_observations(monkeypatch: pytest.MonkeyPatch) -> None: cache: Final = DualCache() @@ -613,57 +442,6 @@ async def test_each_count_preserves_auth_cached_request_tag_limits( assert calls.get_nowait() == "claude-opus-5" -@pytest.mark.asyncio -async def test_provider_counter_failure_releases_parallel_capacity(monkeypatch: pytest.MonkeyPatch) -> None: - cache: Final = DualCache() - limiter: Final = _PROXY_MaxParallelRequestsHandler_v3(InternalUsageCache(cache)) - caller: Final = UserAPIKeyAuth(api_key=_CALLER, max_parallel_requests=1) - - async def fail_count(model: str, api_key: str, body: Mapping[str, JsonValue]) -> int | None: - raise RuntimeError("provider counter failed") - - app: Final = _app(monkeypatch, cache, caller=caller, counts=fail_count, limiter=limiter) - with pytest.raises(RuntimeError, match="provider counter failed"): - await _post(app, _body()) - recovered: Final = await _post(_app(monkeypatch, cache, caller=caller, limiter=limiter), _body()) - assert recovered.status_code == 200, recovered.text - assert recovered.json()["switch"]["estimate"]["input_cost"] == pytest.approx( - _cold_cost("claude-sonnet-5", "5m") - ) - - -@pytest.mark.asyncio -async def test_cancelled_provider_counter_releases_parallel_capacity(monkeypatch: pytest.MonkeyPatch) -> None: - cache: Final = DualCache() - limiter: Final = _PROXY_MaxParallelRequestsHandler_v3(InternalUsageCache(cache)) - caller: Final = UserAPIKeyAuth(api_key=_CALLER, max_parallel_requests=1) - started: Final = asyncio.Event() - release: Final = asyncio.Event() - - async def wait_count(model: str, api_key: str, body: Mapping[str, JsonValue]) -> int | None: - started.set() - await release.wait() - return await Counts()(model, api_key, body) - - app: Final = _app(monkeypatch, cache, caller=caller, counts=wait_count, limiter=limiter) - pending: Final = asyncio.create_task(_post(app, _body())) - try: - await asyncio.wait_for(started.wait(), timeout=5) - pending.cancel() - with pytest.raises(asyncio.CancelledError): - await pending - release.set() - recovered: Final = await asyncio.wait_for(_post(app, _body()), timeout=5) - assert recovered.status_code == 200, recovered.text - assert recovered.json()["switch"]["estimate"]["input_cost"] == pytest.approx( - _cold_cost("claude-sonnet-5", "5m") - ) - finally: - pending.cancel() - release.set() - await asyncio.gather(pending, return_exceptions=True) - - async def _unexpected_count(model: str, api_key: str, body: Mapping[str, JsonValue]) -> int | None: pytest.fail("Unsupported prediction must return before contacting the token counter") diff --git a/tests/test_litellm/proxy/test_proxy_utils.py b/tests/test_litellm/proxy/test_proxy_utils.py index cd8b5ba8844..b2f3c6e7c0e 100644 --- a/tests/test_litellm/proxy/test_proxy_utils.py +++ b/tests/test_litellm/proxy/test_proxy_utils.py @@ -2151,99 +2151,6 @@ async def test_proxy_only_error_5xx_keeps_traceback_and_runs_sync_callbacks(monk assert "test_proxy_utils" in captured["async_traceback"] -def test_create_model_info_response_resolves_alias_to_deployment_model(): - """A public model name that is not itself a cost-map key must not be resolved through - the fallback-generalization rules: `bedrock-claude-opus-5` matches the generic - claude-family baseline (200k/64k) by substring, while the deployment it fronts really - accepts 1M/128k. Regression for the /v1/models alias resolution introduced in v1.94.0.""" - from litellm import Router - - saved_model_cost = dict(litellm.model_cost) - try: - router = Router( - model_list=[ - { - "model_name": "bedrock-claude-opus-5", - "litellm_params": { - "custom_llm_provider": "bedrock", - "model": "bedrock/eu.anthropic.claude-opus-5", - }, - "model_info": {"base_model": "eu.anthropic.claude-opus-5"}, - } - ] - ) - - response = create_model_info_response( - model_id="bedrock-claude-opus-5", provider="openai", llm_router=router - ) - finally: - litellm.model_cost.clear() - litellm.model_cost.update(saved_model_cost) - - entry: Final = litellm.model_cost["eu.anthropic.claude-opus-5"] - assert response["max_input_tokens"] == entry["max_input_tokens"] - assert response["max_output_tokens"] == entry["max_output_tokens"] - - -def test_create_model_info_response_keeps_exact_alias_over_generalized_deployment_model(): - """Mirror of the alias bug: when the deployment points at a custom backend name that - only matches a generalization rule, the listed name's exact cost-map entry is the - better answer and must win.""" - from litellm import Router - - saved_model_cost = dict(litellm.model_cost) - try: - router = Router( - model_list=[ - { - "model_name": "claude-opus-5", - "litellm_params": { - "custom_llm_provider": "bedrock", - "model": "bedrock/my-claude-opus-5-provisioned", - }, - } - ] - ) - - response = create_model_info_response( - model_id="claude-opus-5", provider="openai", llm_router=router - ) - finally: - litellm.model_cost.clear() - litellm.model_cost.update(saved_model_cost) - - entry: Final = litellm.model_cost["claude-opus-5"] - assert response["max_input_tokens"] == entry["max_input_tokens"] - - -def test_create_model_info_response_falls_back_to_alias_for_opaque_deployment_name(): - """An Azure deployment named after the resource rather than the model has no cost-map - entry; the listed name still does, and must keep answering.""" - from litellm import Router - - saved_model_cost = dict(litellm.model_cost) - try: - router = Router( - model_list=[ - { - "model_name": "gpt-4o", - "litellm_params": {"model": "azure/my-gpt4o-deployment"}, - } - ] - ) - - response = create_model_info_response( - model_id="gpt-4o", provider="openai", llm_router=router - ) - finally: - litellm.model_cost.clear() - litellm.model_cost.update(saved_model_cost) - - entry: Final = litellm.model_cost["gpt-4o"] - assert response["max_input_tokens"] == entry["max_input_tokens"] - assert response["max_output_tokens"] == entry["max_output_tokens"] - - def test_create_model_info_response_resolves_mode_through_deployment_model(): """`mode` is derived from the same lookup, so an aliased embedding deployment currently reports no mode at all; it must report `embedding`.""" diff --git a/tests/test_litellm/test_cost_calculator.py b/tests/test_litellm/test_cost_calculator.py index 03e4ef3b2c3..ff28e69a909 100644 --- a/tests/test_litellm/test_cost_calculator.py +++ b/tests/test_litellm/test_cost_calculator.py @@ -203,168 +203,6 @@ def test_cost_calculator_with_usage(_local_model_cost_map, monkeypatch): assert result == expected_cost, f"Got {result}, Expected {expected_cost}" -def test_transcription_cost_uses_token_pricing(_local_model_cost_map): - from litellm import completion_cost - - usage = Usage( - prompt_tokens=14, - completion_tokens=45, - total_tokens=59, - prompt_tokens_details=PromptTokensDetailsWrapper(text_tokens=0, audio_tokens=14), - ) - response = TranscriptionResponse(text="demo text") - response.usage = usage - - cost = completion_cost( - completion_response=response, - model="gpt-4o-transcribe", - custom_llm_provider="openai", - call_type="atranscription", - ) - - model_info: Final = litellm.get_model_info(model="gpt-4o-transcribe", custom_llm_provider="openai") - expected_cost = ( - 14 * model_info["input_cost_per_audio_token"] + 45 * model_info["output_cost_per_token"] - ) - assert pytest.approx(cost, rel=1e-6) == expected_cost - - -def test_transcription_token_pricing_is_provider_aware(_local_model_cost_map): - """Regression: the token-priced transcription path hardcoded provider openai, - so gemini transcription models raised "This model isn't mapped yet".""" - from litellm import completion_cost - - usage = Usage( - prompt_tokens=200, - completion_tokens=10, - total_tokens=210, - prompt_tokens_details=PromptTokensDetailsWrapper(text_tokens=1, audio_tokens=199), - ) - response = TranscriptionResponse(text="demo text") - response.usage = usage - - cost = completion_cost( - completion_response=response, - model="gemini/gemini-3.5-transcribe", - custom_llm_provider="gemini", - call_type="atranscription", - ) - - model_info: Final = litellm.get_model_info(model="gemini/gemini-3.5-transcribe", custom_llm_provider="gemini") - expected_cost = ( - 199 * model_info["input_cost_per_audio_token"] - + 1 * model_info["input_cost_per_token"] - + 10 * model_info["output_cost_per_token"] - ) - assert pytest.approx(cost, rel=1e-6) == expected_cost - - -def test_transcription_cost_falls_back_to_duration(_local_model_cost_map): - from litellm import completion_cost - - response = TranscriptionResponse(text="demo text") - response.duration = 10.0 - - cost = completion_cost( - completion_response=response, - model="whisper-1", - custom_llm_provider="openai", - call_type="atranscription", - ) - - model_info: Final = litellm.get_model_info(model="whisper-1", custom_llm_provider="openai") - expected_cost = 10.0 * model_info["input_cost_per_second"] - assert pytest.approx(cost, rel=1e-6) == expected_cost - - -def test_vertex_chirp_3_transcription_cost_from_duration(_local_model_cost_map): - """Regression: the chirp_3 cost map entry shipped with output_cost_per_second 0.0, - and cost_per_second prefers output_cost_per_second whenever it is not None, so - every transcription priced to $0.00 instead of using input_cost_per_second.""" - from litellm import completion_cost - - response = TranscriptionResponse(text="demo text") - response.duration = 18.0 - - cost = completion_cost( - completion_response=response, - model="vertex_ai/chirp_3", - custom_llm_provider="vertex_ai", - call_type="atranscription", - ) - - model_info: Final = litellm.get_model_info(model="vertex_ai/chirp_3", custom_llm_provider="vertex_ai") - expected_cost = 18.0 * model_info["input_cost_per_second"] - assert cost > 0 - assert pytest.approx(cost, rel=1e-6) == expected_cost - - -def test_handle_realtime_stream_cost_calculation(): - from litellm.cost_calculator import RealtimeAPITokenUsageProcessor - - # Setup test data - results: OpenAIRealtimeStreamList = [ - {"type": "session.created", "session": {"model": "gpt-3.5-turbo"}}, - { - "type": "response.done", - "response": {"usage": {"input_tokens": 100, "output_tokens": 50, "total_tokens": 150}}, - }, - { - "type": "response.done", - "response": { - "usage": { - "input_tokens": 200, - "output_tokens": 100, - "total_tokens": 300, - } - }, - }, - ] - - combined_usage_object = RealtimeAPITokenUsageProcessor.collect_and_combine_usage_from_realtime_stream_results( - results=results, - ) - - # Test with explicit model name - cost = handle_realtime_stream_cost_calculation( - results=results, - combined_usage_object=combined_usage_object, - custom_llm_provider="openai", - litellm_model_name="gpt-3.5-turbo", - ) - - turbo_info = litellm.model_cost["gpt-3.5-turbo"] - expected_cost = (300 * turbo_info["input_cost_per_token"]) + (150 * turbo_info["output_cost_per_token"]) - assert abs(cost - expected_cost) <= 0.00075 # Allow small floating point differences - - # Test with different model name in session - results[0]["session"]["model"] = "gpt-4" - - cost = handle_realtime_stream_cost_calculation( - results=results, - combined_usage_object=combined_usage_object, - custom_llm_provider="openai", - litellm_model_name="gpt-3.5-turbo", - ) - - gpt4_info = litellm.model_cost["gpt-4"] - expected_cost = (300 * gpt4_info["input_cost_per_token"]) + (150 * gpt4_info["output_cost_per_token"]) - assert abs(cost - expected_cost) < 0.00076 - - # Test with no response.done events - results = [{"type": "session.created", "session": {"model": "gpt-3.5-turbo"}}] - combined_usage_object = RealtimeAPITokenUsageProcessor.collect_and_combine_usage_from_realtime_stream_results( - results=results, - ) - cost = handle_realtime_stream_cost_calculation( - results=results, - combined_usage_object=combined_usage_object, - custom_llm_provider="openai", - litellm_model_name="gpt-3.5-turbo", - ) - assert cost == 0.0 # No usage, no cost - - def test_handle_realtime_stream_cost_calculation_stores_cost_breakdown(): """Regression: realtime cost must populate logging_obj.cost_breakdown so the spend logs / UI show input vs output cost (issue: cost_breakdown was None for @@ -561,102 +399,6 @@ def test_realtime_logging_object_does_not_validate_unknown_event_types(): assert len(dumped["results"]) == len(results) -def test_realtime_transcription_duration_cost(monkeypatch): - """ - gpt-realtime-whisper transcription sessions are billed by input audio duration. - The .completed events carry usage {type: duration, seconds: N}; - cost must equal total_seconds * input_cost_per_second. - """ - from datetime import datetime - - from litellm.litellm_core_utils.litellm_logging import Logging - - monkeypatch.setenv("LITELLM_LOCAL_MODEL_COST_MAP", "True") - monkeypatch.setattr(litellm, "model_cost", litellm.get_model_cost_map(url="")) - - from litellm.cost_calculator import RealtimeAPITokenUsageProcessor - - results: OpenAIRealtimeStreamList = [ - { - "type": "session.created", - "session": { - "type": "transcription", - "audio": {"input": {"transcription": {"model": "gpt-realtime-whisper"}}}, - }, - }, - { - "type": "conversation.item.input_audio_transcription.completed", - "transcript": "hello", - "usage": {"type": "duration", "seconds": 60.0}, - }, - { - "type": "conversation.item.input_audio_transcription.completed", - "transcript": "world", - "usage": {"type": "duration", "seconds": 30.0}, - }, - ] - - combined = RealtimeAPITokenUsageProcessor.collect_and_combine_usage_from_realtime_stream_results(results=results) - logging_obj = Logging( - model="gpt-realtime-whisper", - messages=[], - stream=False, - call_type="_arealtime", - start_time=datetime.now(), - litellm_call_id="realtime-transcription-cost-breakdown-test", - function_id="realtime-transcription-cost-breakdown-test", - ) - cost = handle_realtime_stream_cost_calculation( - results=results, - combined_usage_object=combined, - custom_llm_provider="openai", - litellm_model_name="gpt-realtime-whisper", - litellm_logging_obj=logging_obj, - ) - - model_info: Final = litellm.get_model_info(model="gpt-realtime-whisper", custom_llm_provider="openai") - expected = 90.0 * model_info["input_cost_per_second"] - assert abs(cost - expected) < 1e-9 - assert cost > 0 # guards against the duration branch being dropped - assert logging_obj.cost_breakdown is not None - assert abs(logging_obj.cost_breakdown["total_cost"] - cost) < 1e-9 - - # The transcription cost must be attributed in the breakdown, not just folded - # into total_cost, or input_cost + output_cost + additional_costs won't sum to total_cost. - additional_costs = logging_obj.cost_breakdown.get("additional_costs") - assert additional_costs is not None - assert abs(additional_costs["transcription_cost"] - expected) < 1e-9 - attributed_total = ( - logging_obj.cost_breakdown["input_cost"] - + logging_obj.cost_breakdown["output_cost"] - + additional_costs["transcription_cost"] - ) - assert abs(attributed_total - logging_obj.cost_breakdown["total_cost"]) < 1e-9 - - -def test_realtime_transcription_duration_cost_resolves_model_from_litellm_name( - monkeypatch, -): - """When no session event carries the ASR model, the litellm_model_name is used.""" - monkeypatch.setenv("LITELLM_LOCAL_MODEL_COST_MAP", "True") - monkeypatch.setattr(litellm, "model_cost", litellm.get_model_cost_map(url="")) - - results: OpenAIRealtimeStreamList = [ - { - "type": "conversation.item.input_audio_transcription.completed", - "usage": {"type": "duration", "seconds": 120.0}, - }, - ] - cost = handle_realtime_stream_cost_calculation( - results=results, - combined_usage_object=Usage(), - custom_llm_provider="azure", - litellm_model_name="azure/gpt-realtime-whisper", - ) - model_info: Final = litellm.get_model_info(model="azure/gpt-realtime-whisper", custom_llm_provider="azure") - assert abs(cost - 120.0 * model_info["input_cost_per_second"]) < 1e-9 - - def test_realtime_transcription_no_completed_events_is_zero(monkeypatch): """A realtime stream without transcription completed events adds no extra cost.""" monkeypatch.setenv("LITELLM_LOCAL_MODEL_COST_MAP", "True") @@ -678,33 +420,6 @@ def test_realtime_transcription_no_completed_events_is_zero(monkeypatch): ) -def test_realtime_transcription_token_billed_fallback(monkeypatch): - """ - Token-billed transcription models price by audio/text tokens. Verify the - fallback path multiplies audio tokens by the model's audio token cost. - """ - monkeypatch.setenv("LITELLM_LOCAL_MODEL_COST_MAP", "True") - monkeypatch.setattr(litellm, "model_cost", litellm.get_model_cost_map(url="")) - - from litellm.cost_calculator import _transcription_usage_cost - - model_info: Final = litellm.get_model_info(model="gpt-4o-transcribe", custom_llm_provider="openai") - usage = { - "type": "tokens", - "input_tokens": 40, - "output_tokens": 10, - "total_tokens": 50, - "input_token_details": {"audio_tokens": 30, "text_tokens": 10}, - } - cost = _transcription_usage_cost(usage, model_info) - expected = ( - 30 * model_info["input_cost_per_audio_token"] - + 10 * model_info["input_cost_per_token"] - + 10 * model_info["output_cost_per_token"] - ) - assert abs(cost - expected) < 1e-12 - - def test_transcription_usage_cost_returns_zero_for_unknown_type(): """An unrecognized usage type yields 0 (safe fallback, no exception).""" from litellm.cost_calculator import _transcription_usage_cost @@ -1293,72 +1008,6 @@ def test_bedrock_cost_calculator_comparison_with_without_cache(): print(f"Cost with cache: {cost_with_cache}") -def test_gemini_25_implicit_caching_cost(): - """ - Test that Gemini 2.5 models correctly calculate costs with implicit caching. - - This test reproduces the issue from #11156 where cached tokens should receive - a 75% discount. - """ - from litellm import completion_cost - from litellm.types.utils import ( - Choices, - Message, - ModelResponse, - PromptTokensDetailsWrapper, - Usage, - ) - - # Create a mock response similar to the one in the issue - litellm_model_response = ModelResponse( - id="test-response", - created=1750733889, - model="gemini/gemini-2.5-flash", - object="chat.completion", - system_fingerprint=None, - choices=[ - Choices( - finish_reason="stop", - index=0, - message=Message( - content="Understood. This is a test message to check the response from the Gemini model.", - role="assistant", - tool_calls=None, - function_call=None, - ), - ) - ], - usage=Usage( - total_tokens=15050, - prompt_tokens=15033, - completion_tokens=17, - prompt_tokens_details=PromptTokensDetailsWrapper( - audio_tokens=None, - cached_tokens=14316, # This is cachedContentTokenCount from Gemini - ), - completion_tokens_details=None, - ), - ) - - # Calculate the cost - result = completion_cost( - completion_response=litellm_model_response, - model="gemini/gemini-2.5-flash", - ) - - model_info: Final = litellm.model_cost["gemini/gemini-2.5-flash"] - expected_cost = ( - 14316 * model_info["cache_read_input_token_cost"] - + (15033 - 14316) * model_info["input_cost_per_token"] - + 17 * model_info["output_cost_per_token"] - ) - - # Allow for small floating point differences - assert abs(result - expected_cost) < 1e-8, f"Expected cost {expected_cost}, but got {result}" - - print(f"✓ Gemini 2.5 implicit caching cost calculation is correct: ${result:.8f}") - - def test_log_context_cost_calculation(): """ Test that log context cost calculation works correctly with tiered pricing. @@ -1617,6 +1266,10 @@ def test_vertex_regional_deployment_costs_uplift_over_global(monkeypatch): """ Regression for https://github.com/BerriAI/litellm/issues/34393: two Vertex deployments differing only in vertex_location must not price identically. + Google bills non-global endpoints at 1.1x for regional-pricing models, so the + regional request costs 1.1x the global one for the exact same usage, through + both vertex cost routes (Claude via cost_per_token, Gemini via + cost_per_character's token fallback). """ monkeypatch.setenv("LITELLM_LOCAL_MODEL_COST_MAP", "True") monkeypatch.setattr(litellm, "model_cost", litellm.get_model_cost_map(url="")) @@ -1638,10 +1291,8 @@ def test_vertex_regional_deployment_costs_uplift_over_global(monkeypatch): global_total = global_prompt + global_completion regional_total = regional_prompt + regional_completion assert global_total > 0 - assert regional_total == pytest.approx( - global_total - * litellm.model_cost[f"vertex_ai/{model}"]["regional_endpoint_uplift_multiplier"], - rel=1e-9, + assert regional_total == pytest.approx(global_total * 1.10, rel=1e-9), ( + f"{model}: regional Vertex request must cost 1.1x the global one" ) @@ -2724,12 +2375,39 @@ def test_anthropic_geo_and_fast_multipliers_compose(_local_model_cost_map, monke assert completion_cost == pytest.approx(500 * 25e-6 * 2.0 * 1.1) +@pytest.mark.parametrize( + "model,expected_fast", + [ + ("claude-opus-5", 2.0), + ("claude-opus-4-8", 2.0), + ("claude-opus-4-6", None), + ("claude-opus-4-6-20260205", None), + ("claude-opus-4-7", None), + ("claude-opus-4-7-20260416", None), + ], +) +def test_anthropic_fast_multiplier_only_on_models_with_fast_mode(_local_model_cost_map, model, expected_fast): + """ + Anthropic serves fast mode on Opus 5 and Opus 4.8 only, at 2x. Opus 4.6 and + 4.7 accept the ``speed`` request param but are always served standard, so a + ``fast`` multiplier on their map entries overbills every request that asked + for fast and was served standard. + """ + entry = litellm.model_cost[model] + assert entry["provider_specific_entry"].get("fast") == expected_fast + + @pytest.mark.parametrize( "model", ["claude-sonnet-4-6", "claude-mythos-5", "claude-mythos-preview"], ) def test_anthropic_us_data_residency_uplift_on_claude_4_6_and_later_models(_local_model_cost_map, monkeypatch, model): - """Anthropic's US data-residency multiplier must be applied to both token types.""" + """ + Anthropic bills every Claude 4.6+ model served with ``inference_geo="us"`` at + 1.1x, and echoes that geo back in the response usage, so each of these real + cost-map entries has to carry the ``us`` multiplier or US-pinned traffic is + under-reported by 10%. + """ from litellm.llms.anthropic.cost_calculation import ( cost_per_token as anthropic_cost_per_token, ) @@ -2746,11 +2424,9 @@ def test_anthropic_us_data_residency_uplift_on_claude_4_6_and_later_models(_loca geo_usage.inference_geo = "us" geo_prompt_cost, geo_completion_cost = anthropic_cost_per_token(model=model, usage=geo_usage) - model_info: Final = litellm.model_cost[model] - us_multiplier: Final = model_info["provider_specific_entry"]["us"] assert base_prompt_cost > 0 - assert geo_prompt_cost == pytest.approx(base_prompt_cost * us_multiplier) - assert geo_completion_cost == pytest.approx(base_completion_cost * us_multiplier) + assert geo_prompt_cost == pytest.approx(base_prompt_cost * 1.1) + assert geo_completion_cost == pytest.approx(base_completion_cost * 1.1) def test_gemini_cache_tokens_details_no_negative_values(): @@ -3700,37 +3376,6 @@ def test_combine_usage_objects_sums_mirrored_cache_write_fields_once(): assert combined_pair.prompt_tokens_details.cache_creation_tokens == 100 -def test_completion_cost_prices_anthropic_shaped_cache_read_tokens(_local_model_cost_map): - """Regression: an Anthropic /v1/messages response reports cache reads as top-level - cache_read_input_tokens with input_tokens excluding them. Reading that usage as - Responses API usage dropped the cache tokens and billed the whole prompt at the - uncached input rate, overstating spend on cache hits.""" - - response = { - "id": "msg_1", - "type": "message", - "role": "assistant", - "model": "gpt-5.6-sol", - "stop_reason": "end_turn", - "content": [{"type": "text", "text": "1"}], - "usage": {"input_tokens": 3, "output_tokens": 5, "cache_read_input_tokens": 4014}, - } - - cost = litellm.completion_cost( - completion_response=response, - model="gpt-5.6-sol", - custom_llm_provider="openai", - ) - - model_info: Final = litellm.get_model_info(model="gpt-5.6-sol", custom_llm_provider="openai") - expected_cost = ( - 3 * model_info["input_cost_per_token"] - + 4014 * model_info["cache_read_input_token_cost"] - + 5 * model_info["output_cost_per_token"] - ) - assert cost == pytest.approx(expected_cost, rel=1e-9) - - def _together_chat_response( model: str, prompt_tokens: int, completion_tokens: int, cached_tokens: int ) -> ModelResponse: @@ -3749,71 +3394,6 @@ def _together_chat_response( ) -def test_completion_cost_prices_together_cached_tokens_at_cache_read_rate(_local_model_cost_map): - """Regression: Together reports prompt_tokens_details.cached_tokens but no together_ai - registry entry carried cache_read_input_token_cost, so cache-hit tokens were priced at - 0.0 and spend on cache-heavy workloads was understated.""" - - cost = completion_cost( - completion_response=_together_chat_response( - model="deepseek-ai/DeepSeek-V4-Flash-0731", prompt_tokens=7864, completion_tokens=16, cached_tokens=7863 - ), - custom_llm_provider="together_ai", - ) - - model_info: Final = litellm.model_cost["together_ai/deepseek-ai/DeepSeek-V4-Flash-0731"] - expected_cost = ( - 1 * model_info["input_cost_per_token"] - + 7863 * model_info["cache_read_input_token_cost"] - + 16 * model_info["output_cost_per_token"] - ) - assert cost == pytest.approx(expected_cost, rel=1e-9) - - -def test_completion_cost_together_mapped_model_skips_size_bucket(_local_model_cost_map): - """Regression: any together model whose name matches (\\d+b) was rewritten to a - together-ai-* size bucket before the registry lookup, so mapped models like - Muse-Glimmer-30B never used their per-model rates, cache fields included.""" - - cost = completion_cost( - completion_response=_together_chat_response( - model="meta-models/Muse-Glimmer-30B", prompt_tokens=63, completion_tokens=16, cached_tokens=0 - ), - custom_llm_provider="together_ai", - ) - - model_info: Final = litellm.model_cost["together_ai/meta-models/Muse-Glimmer-30B"] - expected_cost = 63 * model_info["input_cost_per_token"] + 16 * model_info["output_cost_per_token"] - assert cost == pytest.approx(expected_cost, rel=1e-9) - - -def test_completion_cost_together_unmapped_model_still_uses_size_bucket(_local_model_cost_map): - cost = completion_cost( - completion_response=_together_chat_response( - model="qwen/Qwen2-72B-Instruct", prompt_tokens=23, completion_tokens=15, cached_tokens=0 - ), - custom_llm_provider="together_ai", - ) - - model_info: Final = litellm.model_cost["together-ai-41.1b-80b"] - expected_cost = 23 * model_info["input_cost_per_token"] + 15 * model_info["output_cost_per_token"] - assert cost == pytest.approx(expected_cost, rel=1e-9) - - -def test_completion_cost_together_metadata_only_model_still_uses_size_bucket(_local_model_cost_map): - assert "input_cost_per_token" not in litellm.model_cost["together_ai/togethercomputer/CodeLlama-34b-Instruct"] - - cost = completion_cost( - completion_response=_together_chat_response( - model="togethercomputer/CodeLlama-34b-Instruct", prompt_tokens=23, completion_tokens=15, cached_tokens=0 - ), - custom_llm_provider="together_ai", - ) - - bucket: Final = litellm.model_cost["together-ai-21.1b-41b"] - assert cost == pytest.approx((23 + 15) * bucket["input_cost_per_token"], rel=1e-9) - - def test_select_model_name_strips_unregistered_alias_prefix(_local_model_cost_map): """A router-facing model_name alias containing "/" whose leading segment is NOT a registered provider must not be double-prefixed into a non-existent cost key. @@ -3998,34 +3578,6 @@ def test_completion_cost_base_model_ignores_regional_row(_local_model_cost_map): ) == pytest.approx(1000 * flat["input_cost_per_token"]) -def test_completion_cost_nonzero_for_slash_alias_model_name(_local_model_cost_map): - """End-to-end cost through a "/"-containing alias must price above zero (#38069).""" - - response = litellm.ModelResponse( - id="x", - choices=[ - { - "index": 0, - "message": {"role": "assistant", "content": "hi"}, - "finish_reason": "stop", - } - ], - model="vertex/claude-opus-5", - ) - response._hidden_params = {"custom_llm_provider": "vertex_ai"} - response.usage = litellm.Usage(prompt_tokens=100, completion_tokens=50) - - cost = litellm.completion_cost( - completion_response=response, - custom_llm_provider="vertex_ai", - ) - - model_info: Final = litellm.model_cost["vertex_ai/claude-opus-5"] - assert model_info["input_cost_per_token"] > 0 - assert model_info["output_cost_per_token"] > 0 - assert cost > 0 - - def test_select_model_name_unresolvable_alias_unchanged(_local_model_cost_map): """An alias that resolves to no known cost key keeps the legacy double-prefixed name.""" @@ -4249,51 +3801,6 @@ def test_explicit_pricing_precedes_private_provider_response_model( assert selected == expected -def test_handle_realtime_stream_cost_calculation_bills_nested_reasoning_tokens_once( - _local_model_cost_map: None, -) -> None: - """Realtime response.done nests reasoning_tokens inside text_tokens, so they are billed once.""" - results: OpenAIRealtimeStreamList = [ - {"type": "session.created", "session": {"model": "gpt-realtime-2.1-mini"}}, - { - "type": "response.done", - "response": { - "usage": { - "total_tokens": 260, - "input_tokens": 237, - "output_tokens": 23, - "input_token_details": { - "text_tokens": 43, - "audio_tokens": 0, - "image_tokens": 194, - "cached_tokens": 0, - "cached_tokens_details": {"text_tokens": 0, "audio_tokens": 0, "image_tokens": 0}, - }, - "output_token_details": {"text_tokens": 23, "audio_tokens": 0, "reasoning_tokens": 18}, - } - }, - }, - ] - combined_usage_object = RealtimeAPITokenUsageProcessor.collect_and_combine_usage_from_realtime_stream_results( - results=results, - ) - - total_cost = handle_realtime_stream_cost_calculation( - results=results, - combined_usage_object=combined_usage_object, - custom_llm_provider="azure", - litellm_model_name="azure/gpt-realtime-2.1-mini", - ) - - info = litellm.get_model_info(model="azure/gpt-realtime-2.1-mini", custom_llm_provider="azure") - expected = ( - 43 * info["input_cost_per_token"] - + 194 * info["input_cost_per_image_token"] - + 23 * info["output_cost_per_token"] - ) - assert total_cost == pytest.approx(expected) - - def test_collect_and_combine_realtime_usage_stores_partitioned_text_tokens() -> None: """The combined usage that lands in spend logs keeps reasoning out of text_tokens for every turn.""" results: OpenAIRealtimeStreamList = [ diff --git a/tests/test_litellm/test_muse_spark_1_3_model_metadata.py b/tests/test_litellm/test_muse_spark_1_3_model_metadata.py index f30ba550034..4392553fcc3 100644 --- a/tests/test_litellm/test_muse_spark_1_3_model_metadata.py +++ b/tests/test_litellm/test_muse_spark_1_3_model_metadata.py @@ -9,6 +9,7 @@ from litellm.litellm_core_utils.llm_cost_calc.tool_call_cost_tracking import Sta MUSE_SPARK_STANDARD = "meta/muse-spark-1.3" MUSE_SPARK_CONTRIBUTOR = "meta/muse-spark-1.3-contributor" +WEB_SEARCH_COST_PER_QUERY = 0.0025 PRICING = ( (MUSE_SPARK_STANDARD, 1.25e-06, 1.5e-07, 4.25e-06), @@ -30,16 +31,6 @@ def test_muse_spark_1_3_routes_to_meta_model_api(model: str): assert api_base == "https://api.meta.ai/v1" -@pytest.mark.parametrize("model", (MUSE_SPARK_STANDARD, MUSE_SPARK_CONTRIBUTOR)) -def test_muse_spark_1_3_web_search_cost_per_query(local_model_cost_map, model: str): - info = litellm.get_model_info(model=model) - - assert ( - StandardBuiltInToolCostTracking.get_cost_for_web_search(model_info=info) - == info["search_context_cost_per_query"]["search_context_size_medium"] - ) - - @pytest.mark.parametrize("model", (MUSE_SPARK_STANDARD, MUSE_SPARK_CONTRIBUTOR)) def test_muse_spark_1_3_backup_matches_main(model: str): """Ensure the bundled model cost map stays in sync with the canonical file.""" diff --git a/tests/test_litellm/test_openai_service_tier_long_context_pricing.py b/tests/test_litellm/test_openai_service_tier_long_context_pricing.py index 98e3af26719..c766370230c 100644 --- a/tests/test_litellm/test_openai_service_tier_long_context_pricing.py +++ b/tests/test_litellm/test_openai_service_tier_long_context_pricing.py @@ -1,9 +1,69 @@ -from typing import Final +import json +from functools import lru_cache +from pathlib import Path import pytest import litellm +REPO_ROOT = Path(__file__).parents[2] +MAIN_PATH = REPO_ROOT / "model_prices_and_context_window.json" +BACKUP_PATH = REPO_ROOT / "litellm" / "model_prices_and_context_window_backup.json" + +FLEX_LONG_CONTEXT = { + "gpt-5.4": { + "input_cost_per_token_above_272k_tokens_flex": 2.5e-06, + "output_cost_per_token_above_272k_tokens_flex": 1.125e-05, + "cache_read_input_token_cost_above_272k_tokens_flex": 2.5e-07, + }, + "gpt-5.4-pro": { + "input_cost_per_token_above_272k_tokens_flex": 3e-05, + "output_cost_per_token_above_272k_tokens_flex": 0.000135, + }, + "gpt-5.5": { + "input_cost_per_token_above_272k_tokens_flex": 5e-06, + "output_cost_per_token_above_272k_tokens_flex": 2.25e-05, + "cache_read_input_token_cost_above_272k_tokens_flex": 5e-07, + }, +} + +PRIORITY_LONG_CONTEXT = { + "gpt-5.6": { + "input_cost_per_token_above_272k_tokens_priority": 1.6e-05, + "output_cost_per_token_above_272k_tokens_priority": 6e-05, + "cache_read_input_token_cost_above_272k_tokens_priority": 1.6e-06, + "cache_creation_input_token_cost_above_272k_tokens_priority": 2e-05, + }, + "gpt-5.6-sol": { + "input_cost_per_token_above_272k_tokens_priority": 1.6e-05, + "output_cost_per_token_above_272k_tokens_priority": 6e-05, + "cache_read_input_token_cost_above_272k_tokens_priority": 1.6e-06, + "cache_creation_input_token_cost_above_272k_tokens_priority": 2e-05, + }, + "gpt-5.6-terra": { + "input_cost_per_token_above_272k_tokens_priority": 8e-06, + "output_cost_per_token_above_272k_tokens_priority": 3.6e-05, + "cache_read_input_token_cost_above_272k_tokens_priority": 8e-07, + "cache_creation_input_token_cost_above_272k_tokens_priority": 1e-05, + }, + "gpt-5.6-luna": { + "input_cost_per_token_above_272k_tokens_priority": 8e-07, + "output_cost_per_token_above_272k_tokens_priority": 3.6e-06, + "cache_read_input_token_cost_above_272k_tokens_priority": 8e-08, + "cache_creation_input_token_cost_above_272k_tokens_priority": 1e-06, + }, + "gpt-6-astra": { + "input_cost_per_token_above_272k_tokens_priority": 4e-05, + "output_cost_per_token_above_272k_tokens_priority": 0.00015, + "cache_read_input_token_cost_above_272k_tokens_priority": 4e-06, + "cache_creation_input_token_cost_above_272k_tokens_priority": 5e-05, + }, +} + +EXPECTED = {**FLEX_LONG_CONTEXT, **PRIORITY_LONG_CONTEXT} + +NO_PUBLISHED_PRIORITY_LONG_CONTEXT = ("gpt-5.4", "gpt-5.5") + @pytest.fixture(autouse=True) def _local_model_cost_map(monkeypatch: pytest.MonkeyPatch) -> None: @@ -12,36 +72,22 @@ def _local_model_cost_map(monkeypatch: pytest.MonkeyPatch) -> None: litellm.add_known_models() +@lru_cache(maxsize=2) +def _load(path: Path) -> dict[str, dict[str, object]]: + with open(path) as f: + return json.load(f) + + LONG_CONTEXT_PROMPT_TOKENS = 300_000 COMPLETION_TOKENS = 1_000 TIERED_COST_CASES = [ - ("gpt-5.4", "flex"), - ("gpt-5.4-pro", "flex"), - ("gpt-5.5", "flex"), - ("gpt-5.6", "priority"), - ("gpt-5.6-sol", "priority"), - ("gpt-5.6-terra", "priority"), - ("gpt-5.6-luna", "priority"), - ("gpt-6-astra", "priority"), + ("gpt-5.4", "flex", 2.5e-06, 1.125e-05), + ("gpt-5.4-pro", "flex", 3e-05, 0.000135), + ("gpt-5.5", "flex", 5e-06, 2.25e-05), + ("gpt-5.6", "priority", 1.6e-05, 6e-05), + ("gpt-5.6-sol", "priority", 1.6e-05, 6e-05), + ("gpt-5.6-terra", "priority", 8e-06, 3.6e-05), + ("gpt-5.6-luna", "priority", 8e-07, 3.6e-06), + ("gpt-6-astra", "priority", 4e-05, 0.00015), ] - - -@pytest.mark.parametrize("model,tier", TIERED_COST_CASES) -def test_cost_per_token_bills_long_context_at_the_tier_rate( - model: str, tier: str -) -> None: - """A prompt over 272K on flex or priority must bill at that tier's long-context rate.""" - input_cost, output_cost = litellm.cost_per_token( - model=model, - prompt_tokens=LONG_CONTEXT_PROMPT_TOKENS, - completion_tokens=COMPLETION_TOKENS, - service_tier=tier, - ) - model_info: Final = litellm.model_cost[model] - assert input_cost == pytest.approx( - LONG_CONTEXT_PROMPT_TOKENS * model_info[f"input_cost_per_token_above_272k_tokens_{tier}"] - ) - assert output_cost == pytest.approx( - COMPLETION_TOKENS * model_info[f"output_cost_per_token_above_272k_tokens_{tier}"] - ) diff --git a/tests/test_litellm/test_together_ai_model_metadata.py b/tests/test_litellm/test_together_ai_model_metadata.py index e86cdb5158d..7176ba4f219 100644 --- a/tests/test_litellm/test_together_ai_model_metadata.py +++ b/tests/test_litellm/test_together_ai_model_metadata.py @@ -10,6 +10,64 @@ REPO_ROOT: Final = Path(__file__).parents[2] CostMap = dict[str, dict[str, object]] COST_MAP_ADAPTER: Final = TypeAdapter(CostMap) +SERVERLESS_CHAT_MODELS: Final = ( + "together_ai/moonshotai/Kimi-K3", + "together_ai/zai-org/GLM-5.2", + "together_ai/zai-org/GLM-5.3", + "together_ai/zai-org/GLM-5.3-Flash", + "together_ai/deepseek-ai/DeepSeek-V4-Pro-0813", + "together_ai/deepseek-ai/DeepSeek-V4-Flash-0731", + "together_ai/MiniMaxAI/MiniMax-M3", + "together_ai/thinkingmachines/Inkling", + "together_ai/thinkingmachines/Inkling-Small", + "together_ai/Qwen/Qwen3.8-2.4T-A95B", + "together_ai/Qwen/Qwen3.7-Max", + "together_ai/Qwen/Qwen3.7-Plus", + "together_ai/Qwen/Qwen3.6-Plus", + "together_ai/Qwen/Qwen3.5-9B", + "together_ai/meta-models/Muse-Glimmer-30B", + "together_ai/google/gemma-4-31B-it", + "together_ai/arize-ai/qwen-2-1.5b-instruct", + "together_ai/Prism-ML/Ternary-Bonsai-27B", + "together_ai/openai/gpt-oss-120b", + "together_ai/openai/gpt-oss-20b", + "together_ai/meta-llama/Llama-3.3-70B-Instruct-Turbo", +) + +DEPRECATED_MODELS: Final = { + "together_ai/nvidia/nemotron-3-ultra-550b-a55b": "2026-08-27", + "together_ai/pearl-ai/gemma-4-31b-it": "2026-08-27", + "together_ai/deepseek-ai/DeepSeek-V4-Pro": "2026-08-27", + "together_ai/moonshotai/Kimi-K2.7-Code": "2026-08-27", + "together_ai/google/gemma-3n-E4B-it": "2026-08-25", + "together_ai/meta-llama/Llama-Guard-4-12B": "2026-08-25", + "together_ai/Qwen/Qwen3-235B-A22B-Instruct-2507-tput": "2026-07-10", + "together_ai/Qwen/Qwen3.5-397B-A17B": "2026-06-29", + "together_ai/Qwen/Qwen3-Coder-480B-A35B-Instruct-FP8": "2026-06-04", + "together_ai/moonshotai/Kimi-K2.5": "2026-05-21", + "together_ai/deepseek-ai/DeepSeek-R1": "2026-05-14", + "together_ai/deepseek-ai/DeepSeek-V3.1": "2026-05-14", + "together_ai/Qwen/Qwen3-235B-A22B-Thinking-2507": "2026-04-16", + "together_ai/mistralai/Mixtral-8x7B-Instruct-v0.1": "2026-04-16", + "together_ai/zai-org/GLM-4.5-Air-FP8": "2026-04-02", + "together_ai/zai-org/GLM-4.7": "2026-04-02", + "together_ai/mistralai/Mistral-Small-24B-Instruct-2501": "2026-04-02", + "together_ai/Qwen/Qwen3-Next-80B-A3B-Instruct": "2026-04-02", + "together_ai/meta-llama/Llama-4-Maverick-17B-128E-Instruct-FP8": "2026-03-31", + "together_ai/meta-llama/Meta-Llama-3.1-8B-Instruct-Turbo": "2026-03-06", + "together_ai/moonshotai/Kimi-K2-Instruct-0905": "2026-03-06", + "together_ai/meta-llama/Llama-3.2-3B-Instruct-Turbo": "2026-03-06", + "together_ai/Qwen/Qwen3-Next-80B-A3B-Thinking": "2026-02-25", + "together_ai/meta-llama/Meta-Llama-3.1-70B-Instruct-Turbo": "2026-02-25", + "together_ai/Qwen/Qwen3-235B-A22B-fp8-tput": "2026-02-06", + "together_ai/meta-llama/Llama-4-Scout-17B-16E-Instruct": "2026-02-06", + "together_ai/Qwen/Qwen2.5-72B-Instruct-Turbo": "2026-02-06", + "together_ai/meta-llama/Meta-Llama-3.1-405B-Instruct-Turbo": "2026-02-06", + "together_ai/deepseek-ai/DeepSeek-R1-0528-tput": "2026-02-03", + "together_ai/mistralai/Mistral-7B-Instruct-v0.1": "2025-11-13", + "together_ai/meta-llama/Llama-3.3-70B-Instruct-Turbo-Free": "2025-11-13", +} + @pytest.fixture(scope="module") def cost_map() -> CostMap: @@ -43,6 +101,7 @@ def test_together_successor_metadata_points_at_known_models(cost_map: CostMap): for model, info in cost_map.items() if model.startswith("together_ai/") and (successor := _successor(info)) is not None } + assert len(successors) >= 10 for model, successor in successors.items(): assert successor in cost_map, f"{model} names successor {successor} that is not in the map" @@ -55,6 +114,23 @@ def test_together_backup_cost_map_in_sync(cost_map: CostMap): assert together_backup == together_main +CACHED_INPUT_MODELS: Final = ( + "together_ai/moonshotai/Kimi-K3", + "together_ai/zai-org/GLM-5.2", + "together_ai/meta-models/Muse-Glimmer-30B", + "together_ai/Qwen/Qwen3.8-2.4T-A95B", + "together_ai/deepseek-ai/DeepSeek-V4-Pro-0813", + "together_ai/deepseek-ai/DeepSeek-V4-Flash-0731", + "together_ai/thinkingmachines/Inkling", + "together_ai/MiniMaxAI/MiniMax-M3", + "together_ai/thinkingmachines/Inkling-Small", + "together_ai/moonshotai/Kimi-K2.7-Code", + "together_ai/deepseek-ai/DeepSeek-V4-Pro", + "together_ai/nvidia/nemotron-3-ultra-550b-a55b", + "together_ai/Qwen/Qwen3.7-Max", +) + + def test_together_prompt_caching_flag_implies_cache_read_rate(cost_map: CostMap): for model, info in cost_map.items(): if model.startswith("together_ai/") and info.get("supports_prompt_caching"): diff --git a/tests/test_litellm/test_video_generation.py b/tests/test_litellm/test_video_generation.py index 88ba911911a..644c7a41f49 100644 --- a/tests/test_litellm/test_video_generation.py +++ b/tests/test_litellm/test_video_generation.py @@ -2,7 +2,6 @@ import asyncio import io import json import os -from typing import Final from unittest.mock import AsyncMock, MagicMock, patch import pytest @@ -13,14 +12,6 @@ from litellm.cost_calculator import default_video_cost_calculator from litellm.integrations.custom_logger import CustomLogger from litellm.litellm_core_utils.litellm_logging import Logging as LitellmLogging from litellm.llms.custom_httpx.http_handler import AsyncHTTPHandler - - -def _expected_video_cost(model: str, resolution: str | None, duration: float) -> float: - entry: Final = litellm.model_cost[model] - field: Final = f"output_cost_per_second_{resolution}" if resolution else "output_cost_per_second" - return duration * entry.get(field, entry["output_cost_per_second"]) - - from litellm.llms.custom_httpx.llm_http_handler import BaseLLMHTTPHandler from litellm.llms.gemini.videos.transformation import GeminiVideoConfig from litellm.llms.openai.videos.transformation import OpenAIVideoConfig @@ -244,35 +235,6 @@ class TestVideoGeneration: assert response.status == "completed" assert response.model == "sora-2" - def test_video_generation_cost_calculation(self): - """Test video generation cost calculation.""" - import json - - # Try to load the local model cost map, skip if not found - cost_map_path = "model_prices_and_context_window.json" - if not os.path.exists(cost_map_path): - # Try alternative paths - alt_paths = [ - os.path.join(os.path.dirname(__file__), "..", "..", cost_map_path), - os.path.join(os.path.dirname(__file__), "..", "..", "..", cost_map_path), - ] - for path in alt_paths: - if os.path.exists(path): - cost_map_path = path - break - else: - pytest.skip("model_prices_and_context_window.json not found") - - with open(cost_map_path, "r") as f: - litellm.model_cost = json.load(f) - - # Test with sora-2 model - cost = default_video_cost_calculator(model="openai/sora-2", duration_seconds=10.0, custom_llm_provider="openai") - - model_info: Final = litellm.model_cost["openai/sora-2"] - assert model_info["output_cost_per_video_per_second"] > 0 - assert model_info["mode"] == "video_generation" - assert cost > 0 def test_video_generation_cost_calculation_unknown_model(self): """Test video generation cost calculation for unknown model.""" @@ -509,132 +471,6 @@ class TestVideoGeneration: ) assert abs(cost - 1.8) < 0.001 - def test_completion_cost_video_resolution_tiers_from_cost_map(self, monkeypatch): - """The 480p/1080p/4k tier keys resolve from the shipped runwayml cost map entries.""" - from litellm.cost_calculator import completion_cost - - local_map_path = os.path.join(os.path.dirname(__file__), "..", "..", "model_prices_and_context_window.json") - with open(local_map_path, "r") as f: - monkeypatch.setattr(litellm, "model_cost", json.load(f)) - - def cost_for(model: str, resolution: str | None, duration: float) -> float: - mock_response = MagicMock() - mock_response.usage = { - "duration_seconds": duration, - **({"video_resolution": resolution} if resolution else {}), - } - type(mock_response)._hidden_params = {} - return completion_cost( - completion_response=mock_response, - model=model, - call_type="create_video", - custom_llm_provider="runwayml", - ) - - assert ( - abs(cost_for("runwayml/seedance2", "4k", 8.0) - _expected_video_cost("runwayml/seedance2", "4k", 8.0)) - < 0.001 - ) - assert ( - abs(cost_for("runwayml/seedance2", "1080p", 8.0) - _expected_video_cost("runwayml/seedance2", "1080p", 8.0)) - < 0.001 - ) - assert ( - abs(cost_for("runwayml/seedance2", "720p", 8.0) - _expected_video_cost("runwayml/seedance2", "720p", 8.0)) - < 0.001 - ) - assert ( - abs( - cost_for("runwayml/seedance2_5", "480p", 8.0) - - _expected_video_cost("runwayml/seedance2_5", "480p", 8.0) - ) - < 0.001 - ) - assert abs(cost_for("runwayml/gen4.5", None, 8.0) - _expected_video_cost("runwayml/gen4.5", None, 8.0)) < 0.001 - - def test_completion_cost_xai_imagine_video_720p_tier_from_cost_map(self, monkeypatch): - """720p xAI Imagine Video requests bill the published 720p rate, not the 480p base rate.""" - from litellm.cost_calculator import completion_cost - - local_map_path = os.path.join(os.path.dirname(__file__), "..", "..", "model_prices_and_context_window.json") - with open(local_map_path, "r") as f: - monkeypatch.setattr(litellm, "model_cost", json.load(f)) - - def cost_for(model: str, resolution: str, duration: float) -> float: - mock_response = MagicMock() - mock_response.usage = {"duration_seconds": duration, "video_resolution": resolution} - type(mock_response)._hidden_params = {} - return completion_cost( - completion_response=mock_response, - model=model, - call_type="create_video", - custom_llm_provider="xai", - ) - - assert ( - abs( - cost_for("xai/grok-imagine-video", "720p", 10.0) - - _expected_video_cost("xai/grok-imagine-video", "720p", 10.0) - ) - < 0.001 - ) - assert ( - abs( - cost_for("xai/grok-imagine-video-1.5", "720p", 10.0) - - _expected_video_cost("xai/grok-imagine-video-1.5", "720p", 10.0) - ) - < 0.001 - ) - assert ( - abs( - cost_for("xai/grok-imagine-video-1.5", "480p", 10.0) - - _expected_video_cost("xai/grok-imagine-video-1.5", "480p", 10.0) - ) - < 0.001 - ) - assert ( - abs( - cost_for("xai/grok-imagine-video-1.5", "1080p", 10.0) - - _expected_video_cost("xai/grok-imagine-video-1.5", "1080p", 10.0) - ) - < 0.001 - ) - - def test_completion_cost_veo_31_tiers_pin_published_rates(self, monkeypatch): - """The gemini and vertex_ai veo 3.1 entries bill Google's published per-second tier rates.""" - from litellm.cost_calculator import completion_cost - - local_map_path = os.path.join(os.path.dirname(__file__), "..", "..", "model_prices_and_context_window.json") - with open(local_map_path, "r") as f: - monkeypatch.setattr(litellm, "model_cost", json.load(f)) - - def cost_for(model: str, provider: str, resolution: str | None, duration: float) -> float: - mock_response = MagicMock() - mock_response.usage = { - "duration_seconds": duration, - **({"video_resolution": resolution} if resolution else {}), - } - type(mock_response)._hidden_params = {} - return completion_cost( - completion_response=mock_response, - model=model, - call_type="create_video", - custom_llm_provider=provider, - ) - - for provider in ("gemini", "vertex_ai"): - for suffix in ("generate-preview", "generate-001"): - standard = f"{provider}/veo-3.1-{suffix}" - fast = f"{provider}/veo-3.1-fast-{suffix}" - assert abs(cost_for(standard, provider, None, 8.0) - _expected_video_cost(standard, None, 8.0)) < 1e-6 - assert ( - abs(cost_for(standard, provider, "1080p", 8.0) - _expected_video_cost(standard, "1080p", 8.0)) - < 1e-6 - ) - assert abs(cost_for(standard, provider, "4k", 8.0) - _expected_video_cost(standard, "4k", 8.0)) < 1e-6 - assert abs(cost_for(fast, provider, "720p", 8.0) - _expected_video_cost(fast, "720p", 8.0)) < 1e-6 - assert abs(cost_for(fast, provider, "1080p", 8.0) - _expected_video_cost(fast, "1080p", 8.0)) < 1e-6 - assert abs(cost_for(fast, provider, "4k", 8.0) - _expected_video_cost(fast, "4k", 8.0)) < 1e-6 def test_video_generation_with_files(self): """Test video generation with file uploads.""" @@ -666,7 +502,9 @@ class TestVideoGeneration: config = OpenAIVideoConfig() # Test environment validation - headers = config.validate_environment(headers={}, model="sora-2", api_key="test-api-key") + headers = config.validate_environment( + headers={}, model="sora-2", api_key="test-api-key" + ) assert "Authorization" in headers assert headers["Authorization"] == "Bearer test-api-key" @@ -681,7 +519,9 @@ class TestVideoGeneration: mock_validate.return_value = {"Authorization": "Bearer deployment-api-key"} # Mock the transform and HTTP client - with patch.object(config, "transform_video_create_request") as mock_transform: + with patch.object( + config, "transform_video_create_request" + ) as mock_transform: mock_transform.return_value = ( {"model": "sora-2", "prompt": "test"}, [], @@ -689,7 +529,9 @@ class TestVideoGeneration: ) # Mock the transform_video_create_response to avoid needing a real response - with patch.object(config, "transform_video_create_response") as mock_transform_response: + with patch.object( + config, "transform_video_create_response" + ) as mock_transform_response: mock_video_object = MagicMock() mock_video_object.id = "video_123" mock_video_object.object = "video" @@ -739,7 +581,9 @@ class TestVideoGeneration: config = OpenAIVideoConfig() # Test URL generation - url = config.get_complete_url(model="sora-2", api_base="https://api.openai.com/v1", litellm_params={}) + url = config.get_complete_url( + model="sora-2", api_base="https://api.openai.com/v1", litellm_params={} + ) assert url == "https://api.openai.com/v1/videos" @@ -814,7 +658,9 @@ class TestVideoGeneration: def test_video_generation_response_types(self): """Test video generation response types.""" # Test VideoResponse - video_obj = VideoObject(id="test_id", object="video", status="completed", created_at=1712697600) + video_obj = VideoObject( + id="test_id", object="video", status="completed", created_at=1712697600 + ) response = VideoResponse(data=[video_obj]) @@ -869,7 +715,9 @@ class TestVideoGeneration: "seconds": "10", } - response = video_status(video_id="video_456", model="sora-2", mock_response=mock_data) + response = video_status( + video_id="video_456", model="sora-2", mock_response=mock_data + ) assert isinstance(response, VideoObject) assert response.id == "video_456" @@ -890,7 +738,9 @@ class TestVideoGeneration: # Mock the async_video_status_handler to return the mock_response async_mock = AsyncMock(return_value=mock_response) - with patch.object(videos_main.base_llm_http_handler, "async_video_status_handler", async_mock): + with patch.object( + videos_main.base_llm_http_handler, "async_video_status_handler", async_mock + ): with patch.object( videos_main.base_llm_http_handler, "video_status_handler", @@ -899,7 +749,9 @@ class TestVideoGeneration: import asyncio async def test_async(): - response = await avideo_status(video_id="video_async_123", model="sora-2") + response = await avideo_status( + video_id="video_async_123", model="sora-2" + ) return response response = asyncio.run(test_async()) @@ -1045,7 +897,9 @@ class TestVideoGeneration: "seconds": "8", } - response = video_status(video_id="video_remix_123", model="sora-2", mock_response=mock_data) + response = video_status( + video_id="video_remix_123", model="sora-2", mock_response=mock_data + ) assert isinstance(response, VideoObject) assert response.id == "video_remix_123" @@ -1121,7 +975,9 @@ class TestVideoLogging: def __init__(self): self.standard_logging_payload = None - async def async_log_success_event(self, kwargs, response_obj, start_time, end_time): + async def async_log_success_event( + self, kwargs, response_obj, start_time, end_time + ): self.standard_logging_payload = kwargs.get("standard_logging_object") @pytest.mark.asyncio @@ -1272,7 +1128,10 @@ def test_video_content_handler_passes_variant_to_url(): assert result == b"thumbnail-bytes" called_url = mock_client.get.call_args.kwargs["url"] - assert called_url == "https://api.openai.com/v1/videos/video_abc/content?variant=thumbnail" + assert ( + called_url + == "https://api.openai.com/v1/videos/video_abc/content?variant=thumbnail" + ) def test_video_content_handler_uses_get_for_openai(): @@ -1297,7 +1156,9 @@ def test_video_content_handler_uses_get_for_openai(): # Patch _get_httpx_client to ensure no real HTTP client is created # This prevents test isolation issues where isinstance check might fail - with patch("litellm.llms.custom_httpx.llm_http_handler._get_httpx_client") as mock_get_client: + with patch( + "litellm.llms.custom_httpx.llm_http_handler._get_httpx_client" + ) as mock_get_client: mock_get_client.return_value = mock_client result = handler.video_content_handler( @@ -1345,7 +1206,10 @@ def test_video_content_respects_api_base_and_api_key_from_kwargs(): # Verify that api_base and api_key from kwargs were included in litellm_params assert captured_litellm_params is not None - assert captured_litellm_params.get("api_base") == "https://test-resource.openai.azure.com/" + assert ( + captured_litellm_params.get("api_base") + == "https://test-resource.openai.azure.com/" + ) assert captured_litellm_params.get("api_key") == "test-api-key-from-db" assert result == b"mp4-bytes" @@ -1382,7 +1246,9 @@ def test_encode_video_id_with_provider_handles_azure_video_prefix(): model_id = "azure/sora-2" # Encode the video ID with provider information - encoded_id = encode_video_id_with_provider(video_id=raw_azure_video_id, provider=provider, model_id=model_id) + encoded_id = encode_video_id_with_provider( + video_id=raw_azure_video_id, provider=provider, model_id=model_id + ) # Verify the ID was encoded (should be different from the original) assert encoded_id != raw_azure_video_id @@ -1395,7 +1261,9 @@ def test_encode_video_id_with_provider_handles_azure_video_prefix(): assert decoded.get("video_id") == raw_azure_video_id # Verify that encoding an already-encoded ID doesn't double-encode it - encoded_twice = encode_video_id_with_provider(video_id=encoded_id, provider=provider, model_id=model_id) + encoded_twice = encode_video_id_with_provider( + video_id=encoded_id, provider=provider, model_id=model_id + ) assert encoded_twice == encoded_id # Should return the same encoded ID @@ -1706,7 +1574,9 @@ class TestVideoEndpointsProxyLitellmParams: # Mock the router instance mock_router_instance = MagicMock() - mock_router_instance.resolve_model_name_from_model_id.return_value = "vertex-ai-sora-2" + mock_router_instance.resolve_model_name_from_model_id.return_value = ( + "vertex-ai-sora-2" + ) mock_router_instance.model_names = {"vertex-ai-sora-2"} mock_router_instance.has_model_id.return_value = False @@ -1740,7 +1610,11 @@ class TestVideoEndpointsProxyLitellmParams: data_passed = ( call_args.kwargs.get("data", {}) if call_args.kwargs - else (call_args.args[0] if call_args.args and len(call_args.args) > 0 else {}) + else ( + call_args.args[0] + if call_args.args and len(call_args.args) > 0 + else {} + ) ) # Verify that model was resolved and added to data @@ -1769,7 +1643,9 @@ class TestVideoEndpointsProxyLitellmParams: # Mock the router instance mock_router_instance = MagicMock() - mock_router_instance.resolve_model_name_from_model_id.return_value = "vertex-ai-sora-2" + mock_router_instance.resolve_model_name_from_model_id.return_value = ( + "vertex-ai-sora-2" + ) mock_router_instance.model_names = {"vertex-ai-sora-2"} mock_router_instance.has_model_id.return_value = False @@ -1803,7 +1679,11 @@ class TestVideoEndpointsProxyLitellmParams: data_passed = ( call_args.kwargs.get("data", {}) if call_args.kwargs - else (call_args.args[0] if call_args.args and len(call_args.args) > 0 else {}) + else ( + call_args.args[0] + if call_args.args and len(call_args.args) > 0 + else {} + ) ) # Verify that model was resolved and added to data @@ -1832,7 +1712,9 @@ class TestVideoEndpointsProxyLitellmParams: # Mock the router instance mock_router_instance = MagicMock() - mock_router_instance.resolve_model_name_from_model_id.return_value = "vertex-ai-sora-2" + mock_router_instance.resolve_model_name_from_model_id.return_value = ( + "vertex-ai-sora-2" + ) mock_router_instance.model_names = {"vertex-ai-sora-2"} mock_router_instance.has_model_id.return_value = False @@ -1866,7 +1748,11 @@ class TestVideoEndpointsProxyLitellmParams: data_passed = ( call_args.kwargs.get("data", {}) if call_args.kwargs - else (call_args.args[0] if call_args.args and len(call_args.args) > 0 else {}) + else ( + call_args.args[0] + if call_args.args and len(call_args.args) > 0 + else {} + ) ) # Most importantly: verify that custom_llm_provider is "vertex_ai" not "openai" @@ -2445,7 +2331,9 @@ def test_video_get_character_accepts_encoded_character_id(video_proxy_test_clien @pytest.mark.parametrize("endpoint", ["/v1/videos/edits", "/v1/videos/extensions"]) -def test_edit_and_extension_support_custom_provider_from_extra_body(video_proxy_test_client, endpoint): +def test_edit_and_extension_support_custom_provider_from_extra_body( + video_proxy_test_client, endpoint +): from litellm.proxy.common_request_processing import ProxyBaseLLMRequestProcessing captured_data = {} @@ -2498,7 +2386,9 @@ def test_edit_and_extension_support_custom_provider_from_extra_body(video_proxy_ ], ) @pytest.mark.asyncio -async def test_edit_and_extension_read_cached_body_after_auth_consumes_stream(handler_name, path, form): +async def test_edit_and_extension_read_cached_body_after_auth_consumes_stream( + handler_name, path, form +): from urllib.parse import urlencode from fastapi import Response @@ -2547,7 +2437,9 @@ async def test_edit_and_extension_read_cached_body_after_auth_consumes_stream(ha @pytest.mark.parametrize("endpoint", ["/v1/videos/edits", "/v1/videos/extensions"]) -def test_edit_and_extension_route_with_encoded_video_ids(video_proxy_test_client, endpoint): +def test_edit_and_extension_route_with_encoded_video_ids( + video_proxy_test_client, endpoint +): from litellm.proxy.common_request_processing import ProxyBaseLLMRequestProcessing from litellm.types.videos.utils import encode_video_id_with_provider