diff --git a/ci_cd/generate_model_prices_schema.py b/ci_cd/generate_model_prices_schema.py index 0f449f01ec9..1f2a8f46e22 100644 --- a/ci_cd/generate_model_prices_schema.py +++ b/ci_cd/generate_model_prices_schema.py @@ -139,6 +139,14 @@ NUMBER_KEYS: dict[str, JsonSchema] = { "minimum": 1, "description": "Multiplier applied to all token costs for US data residency (e.g. 1.10 = +10%).", }, + "regional_endpoint_uplift_multiplier": { + "type": "number", + "minimum": 1, + "description": ( + "Multiplier applied to all token costs when the request is served from a regional or " + "multi-region endpoint instead of the global one (e.g. 1.10 = +10%)." + ), + }, } COST_DESCRIPTIONS: dict[str, str] = { diff --git a/litellm/cost_calculator.py b/litellm/cost_calculator.py index 6b6653c5646..b14b12910b7 100644 --- a/litellm/cost_calculator.py +++ b/litellm/cost_calculator.py @@ -326,6 +326,8 @@ def cost_per_token( service_tier: str | None = None, # for OpenAI service tier pricing ### DATA RESIDENCY ### data_residency: str | None = None, # for OpenAI regional-processing uplift (e.g. "eu", "us") + ### VERTEX LOCATION ### + vertex_location: str | None = None, # for Vertex AI regional endpoint uplift (e.g. "us-east5") response: Any | None = None, ### REQUEST MODEL ### request_model: str | None = None, # original request model for router detection @@ -593,6 +595,7 @@ def cost_per_token( custom_llm_provider=custom_llm_provider, usage=usage_block, service_tier=service_tier, + vertex_location=vertex_location, ) elif custom_llm_provider == "anthropic": return anthropic_cost_per_token(model=model, usage=usage_block, service_tier=service_tier) @@ -1134,6 +1137,8 @@ def completion_cost( service_tier: str | None = None, # for OpenAI service tier pricing ### DATA RESIDENCY ### data_residency: str | None = None, # for OpenAI regional-processing uplift (e.g. "eu", "us") + ### VERTEX LOCATION ### + vertex_location: str | None = None, # for Vertex AI regional endpoint uplift (e.g. "us-east5") ) -> float: """ Calculate the cost of a given completion call fot GPT-3.5-turbo, llama2, any litellm supported llm. @@ -1568,6 +1573,7 @@ def completion_cost( rerank_billed_units=rerank_billed_units, service_tier=service_tier, data_residency=data_residency, + vertex_location=vertex_location, response=completion_response, request_model=request_model_for_cost, ) @@ -1756,6 +1762,8 @@ def response_cost_calculator( service_tier: str | None = None, # for OpenAI service tier pricing ### DATA RESIDENCY ### data_residency: str | None = None, # for OpenAI regional-processing uplift (e.g. "eu", "us") + ### VERTEX LOCATION ### + vertex_location: str | None = None, # for Vertex AI regional endpoint uplift (e.g. "us-east5") ) -> float: """ Returns @@ -1788,6 +1796,7 @@ def response_cost_calculator( litellm_logging_obj=litellm_logging_obj, service_tier=service_tier, data_residency=data_residency, + vertex_location=vertex_location, ) return response_cost except Exception as e: diff --git a/litellm/litellm_core_utils/litellm_logging.py b/litellm/litellm_core_utils/litellm_logging.py index a72d46e3fe8..5d33b5ff490 100644 --- a/litellm/litellm_core_utils/litellm_logging.py +++ b/litellm/litellm_core_utils/litellm_logging.py @@ -1484,6 +1484,11 @@ class Logging(LiteLLMLoggingBaseClass): if hasattr(self, "litellm_params") and self.litellm_params else None ), + "vertex_location": ( + self.litellm_params.get("vertex_location") or self.litellm_params.get("vertex_ai_location") + if hasattr(self, "litellm_params") and self.litellm_params + else None + ), } except Exception as e: # error creating kwargs for cost calculation debug_info = StandardLoggingModelCostFailureDebugInformation( diff --git a/litellm/llms/vertex_ai/cost_calculator.py b/litellm/llms/vertex_ai/cost_calculator.py index 86a5bb207ec..8b4aa52e2f9 100644 --- a/litellm/llms/vertex_ai/cost_calculator.py +++ b/litellm/llms/vertex_ai/cost_calculator.py @@ -26,6 +26,22 @@ Google AI Studio -> token based pricing models_without_dynamic_pricing: Final = ["gemini-1.0-pro", "gemini-pro", "gemini-2"] +GLOBAL_VERTEX_LOCATION: Final = "global" + + +def _regional_endpoint_uplift(model_info: ModelInfo, vertex_location: str | None) -> float: + """ + Vertex bills a flat premium (currently +10%) on every token type when a request is served + from a regional or multi-region endpoint instead of the global one, so the location the + request was routed to decides the rate, not just the model. + """ + if vertex_location is None or vertex_location.lower() == GLOBAL_VERTEX_LOCATION: + return 1.0 + multiplier: Final = model_info.get("regional_endpoint_uplift_multiplier") + if multiplier is None: + return 1.0 + return float(multiplier) + def cost_router( model: str, @@ -196,6 +212,7 @@ def cost_per_token( custom_llm_provider: str, usage: Usage, service_tier: str | None = None, + vertex_location: str | None = None, ) -> tuple[float, float]: """ Calculates the cost per token for a given model, prompt tokens, and completion tokens. @@ -207,6 +224,8 @@ def cost_per_token( - completion_tokens: float, the number of output tokens - service_tier: optional tier derived from Gemini trafficType ("priority" for ON_DEMAND_PRIORITY, "flex" for FLEX/batch). + - vertex_location: optional Vertex location the request was served from + (e.g. "us-east5", "us", "global"), used to apply the regional endpoint uplift. Returns: Tuple[float, float] - prompt_cost_in_usd, completion_cost_in_usd @@ -222,14 +241,17 @@ def cost_per_token( input_cost_per_token_above_128k_tokens: Final = model_info.get("input_cost_per_token_above_128k_tokens") output_cost_per_token_above_128k_tokens: Final = model_info.get("output_cost_per_token_above_128k_tokens") if input_cost_per_token_above_128k_tokens is not None or output_cost_per_token_above_128k_tokens is not None: - return _handle_128k_pricing( + prompt_cost, completion_cost = _handle_128k_pricing( model_info=model_info, usage=usage, ) + else: + prompt_cost, completion_cost = generic_cost_per_token( + model=model, + custom_llm_provider=custom_llm_provider, + usage=usage, + service_tier=service_tier, + ) - return generic_cost_per_token( - model=model, - custom_llm_provider=custom_llm_provider, - usage=usage, - service_tier=service_tier, - ) + uplift: Final = _regional_endpoint_uplift(model_info=model_info, vertex_location=vertex_location) + return prompt_cost * uplift, completion_cost * uplift diff --git a/litellm/model_prices_and_context_window_backup.json b/litellm/model_prices_and_context_window_backup.json index f63a61f3c6e..3cedf75e432 100644 --- a/litellm/model_prices_and_context_window_backup.json +++ b/litellm/model_prices_and_context_window_backup.json @@ -37631,6 +37631,7 @@ "max_tokens": 8192, "mode": "chat", "output_cost_per_token": 5e-06, + "regional_endpoint_uplift_multiplier": 1.1, "source": "https://cloud.google.com/vertex-ai/generative-ai/docs/partner-models/claude/haiku-4-5", "supports_assistant_prefill": true, "supports_function_calling": true, @@ -37654,6 +37655,7 @@ "max_tokens": 8192, "mode": "chat", "output_cost_per_token": 5e-06, + "regional_endpoint_uplift_multiplier": 1.1, "source": "https://cloud.google.com/vertex-ai/generative-ai/docs/partner-models/claude/haiku-4-5", "supports_assistant_prefill": true, "supports_function_calling": true, @@ -37869,6 +37871,7 @@ "max_tokens": 64000, "mode": "chat", "output_cost_per_token": 2.5e-05, + "regional_endpoint_uplift_multiplier": 1.1, "search_context_cost_per_query": { "search_context_size_high": 0.01, "search_context_size_low": 0.01, @@ -37897,6 +37900,7 @@ "max_tokens": 64000, "mode": "chat", "output_cost_per_token": 2.5e-05, + "regional_endpoint_uplift_multiplier": 1.1, "search_context_cost_per_query": { "search_context_size_high": 0.01, "search_context_size_low": 0.01, @@ -37927,6 +37931,7 @@ "max_tokens": 128000, "mode": "chat", "output_cost_per_token": 2.5e-05, + "regional_endpoint_uplift_multiplier": 1.1, "search_context_cost_per_query": { "search_context_size_high": 0.01, "search_context_size_low": 0.01, @@ -37957,6 +37962,7 @@ "max_tokens": 128000, "mode": "chat", "output_cost_per_token": 2.5e-05, + "regional_endpoint_uplift_multiplier": 1.1, "search_context_cost_per_query": { "search_context_size_high": 0.01, "search_context_size_low": 0.01, @@ -37987,6 +37993,7 @@ "max_tokens": 128000, "mode": "chat", "output_cost_per_token": 2.5e-05, + "regional_endpoint_uplift_multiplier": 1.1, "search_context_cost_per_query": { "search_context_size_high": 0.01, "search_context_size_low": 0.01, @@ -38018,6 +38025,7 @@ "max_tokens": 128000, "mode": "chat", "output_cost_per_token": 2.5e-05, + "regional_endpoint_uplift_multiplier": 1.1, "search_context_cost_per_query": { "search_context_size_high": 0.01, "search_context_size_low": 0.01, @@ -38049,6 +38057,7 @@ "max_tokens": 128000, "mode": "chat", "output_cost_per_token": 5e-05, + "regional_endpoint_uplift_multiplier": 1.1, "search_context_cost_per_query": { "search_context_size_high": 0.01, "search_context_size_low": 0.01, @@ -38080,6 +38089,7 @@ "max_tokens": 128000, "mode": "chat", "output_cost_per_token": 5e-05, + "regional_endpoint_uplift_multiplier": 1.1, "search_context_cost_per_query": { "search_context_size_high": 0.01, "search_context_size_low": 0.01, @@ -38112,6 +38122,7 @@ "max_tokens": 128000, "mode": "chat", "output_cost_per_token": 2.5e-05, + "regional_endpoint_uplift_multiplier": 1.1, "search_context_cost_per_query": { "search_context_size_high": 0.01, "search_context_size_low": 0.01, @@ -38144,6 +38155,7 @@ "max_tokens": 128000, "mode": "chat", "output_cost_per_token": 2.5e-05, + "regional_endpoint_uplift_multiplier": 1.1, "search_context_cost_per_query": { "search_context_size_high": 0.01, "search_context_size_low": 0.01, @@ -38176,6 +38188,7 @@ "max_tokens": 128000, "mode": "chat", "output_cost_per_token": 2.5e-05, + "regional_endpoint_uplift_multiplier": 1.1, "search_context_cost_per_query": { "search_context_size_high": 0.01, "search_context_size_low": 0.01, @@ -38208,6 +38221,7 @@ "max_tokens": 128000, "mode": "chat", "output_cost_per_token": 2.5e-05, + "regional_endpoint_uplift_multiplier": 1.1, "search_context_cost_per_query": { "search_context_size_high": 0.01, "search_context_size_low": 0.01, @@ -38243,6 +38257,7 @@ "max_tokens": 64000, "mode": "chat", "output_cost_per_token": 1.5e-05, + "regional_endpoint_uplift_multiplier": 1.1, "output_cost_per_token_batches": 7.5e-06, "supports_assistant_prefill": true, "supports_computer_use": true, @@ -38267,6 +38282,7 @@ "max_tokens": 128000, "mode": "chat", "output_cost_per_token": 1e-05, + "regional_endpoint_uplift_multiplier": 1.1, "search_context_cost_per_query": { "search_context_size_high": 0.01, "search_context_size_low": 0.01, @@ -38299,6 +38315,7 @@ "max_tokens": 64000, "mode": "chat", "output_cost_per_token": 1.5e-05, + "regional_endpoint_uplift_multiplier": 1.1, "supports_assistant_prefill": true, "supports_computer_use": true, "supports_function_calling": true, @@ -38333,6 +38350,7 @@ "max_tokens": 64000, "mode": "chat", "output_cost_per_token": 1.5e-05, + "regional_endpoint_uplift_multiplier": 1.1, "output_cost_per_token_batches": 7.5e-06, "supports_assistant_prefill": true, "supports_computer_use": true, @@ -45925,6 +45943,7 @@ "max_tokens": 128000, "mode": "chat", "output_cost_per_token": 1e-05, + "regional_endpoint_uplift_multiplier": 1.1, "search_context_cost_per_query": { "search_context_size_high": 0.01, "search_context_size_low": 0.01, @@ -45957,6 +45976,7 @@ "max_tokens": 64000, "mode": "chat", "output_cost_per_token": 1.5e-05, + "regional_endpoint_uplift_multiplier": 1.1, "supports_assistant_prefill": true, "supports_computer_use": true, "supports_function_calling": true, diff --git a/litellm/types/utils.py b/litellm/types/utils.py index d9ef538d530..d3dc50f4cbd 100644 --- a/litellm/types/utils.py +++ b/litellm/types/utils.py @@ -39,7 +39,7 @@ from pydantic import ( field_serializer, field_validator, ) -from typing_extensions import Required, TypedDict +from typing_extensions import ReadOnly, Required, TypedDict from litellm._logging import verbose_logger from litellm._uuid import uuid @@ -244,6 +244,9 @@ class ModelInfoBase(ProviderSpecificModelInfo, total=False): regional_processing_uplift_multiplier_us: ( float | None ) # OpenAI US data-residency uplift multiplier applied to all token costs (e.g. 1.10 = +10%) + regional_endpoint_uplift_multiplier: ReadOnly[ + float | None + ] # Vertex AI uplift multiplier applied to all token costs on regional/multi-region endpoints output_cost_per_character: float | None # only for vertex ai models output_cost_per_audio_token: float | None output_cost_per_token_above_128k_tokens: float | None # only for vertex ai models diff --git a/litellm/utils.py b/litellm/utils.py index 79372f00284..98ad332bea7 100644 --- a/litellm/utils.py +++ b/litellm/utils.py @@ -5650,6 +5650,7 @@ def _get_model_info_helper( regional_processing_uplift_multiplier_us=_model_info.get( "regional_processing_uplift_multiplier_us", None ), + regional_endpoint_uplift_multiplier=_model_info.get("regional_endpoint_uplift_multiplier", None), output_cost_per_audio_token=_model_info.get("output_cost_per_audio_token", None), output_cost_per_character=_model_info.get("output_cost_per_character", None), output_cost_per_reasoning_token=_model_info.get("output_cost_per_reasoning_token", None), diff --git a/model_prices_and_context_window.json b/model_prices_and_context_window.json index f63a61f3c6e..3cedf75e432 100644 --- a/model_prices_and_context_window.json +++ b/model_prices_and_context_window.json @@ -37631,6 +37631,7 @@ "max_tokens": 8192, "mode": "chat", "output_cost_per_token": 5e-06, + "regional_endpoint_uplift_multiplier": 1.1, "source": "https://cloud.google.com/vertex-ai/generative-ai/docs/partner-models/claude/haiku-4-5", "supports_assistant_prefill": true, "supports_function_calling": true, @@ -37654,6 +37655,7 @@ "max_tokens": 8192, "mode": "chat", "output_cost_per_token": 5e-06, + "regional_endpoint_uplift_multiplier": 1.1, "source": "https://cloud.google.com/vertex-ai/generative-ai/docs/partner-models/claude/haiku-4-5", "supports_assistant_prefill": true, "supports_function_calling": true, @@ -37869,6 +37871,7 @@ "max_tokens": 64000, "mode": "chat", "output_cost_per_token": 2.5e-05, + "regional_endpoint_uplift_multiplier": 1.1, "search_context_cost_per_query": { "search_context_size_high": 0.01, "search_context_size_low": 0.01, @@ -37897,6 +37900,7 @@ "max_tokens": 64000, "mode": "chat", "output_cost_per_token": 2.5e-05, + "regional_endpoint_uplift_multiplier": 1.1, "search_context_cost_per_query": { "search_context_size_high": 0.01, "search_context_size_low": 0.01, @@ -37927,6 +37931,7 @@ "max_tokens": 128000, "mode": "chat", "output_cost_per_token": 2.5e-05, + "regional_endpoint_uplift_multiplier": 1.1, "search_context_cost_per_query": { "search_context_size_high": 0.01, "search_context_size_low": 0.01, @@ -37957,6 +37962,7 @@ "max_tokens": 128000, "mode": "chat", "output_cost_per_token": 2.5e-05, + "regional_endpoint_uplift_multiplier": 1.1, "search_context_cost_per_query": { "search_context_size_high": 0.01, "search_context_size_low": 0.01, @@ -37987,6 +37993,7 @@ "max_tokens": 128000, "mode": "chat", "output_cost_per_token": 2.5e-05, + "regional_endpoint_uplift_multiplier": 1.1, "search_context_cost_per_query": { "search_context_size_high": 0.01, "search_context_size_low": 0.01, @@ -38018,6 +38025,7 @@ "max_tokens": 128000, "mode": "chat", "output_cost_per_token": 2.5e-05, + "regional_endpoint_uplift_multiplier": 1.1, "search_context_cost_per_query": { "search_context_size_high": 0.01, "search_context_size_low": 0.01, @@ -38049,6 +38057,7 @@ "max_tokens": 128000, "mode": "chat", "output_cost_per_token": 5e-05, + "regional_endpoint_uplift_multiplier": 1.1, "search_context_cost_per_query": { "search_context_size_high": 0.01, "search_context_size_low": 0.01, @@ -38080,6 +38089,7 @@ "max_tokens": 128000, "mode": "chat", "output_cost_per_token": 5e-05, + "regional_endpoint_uplift_multiplier": 1.1, "search_context_cost_per_query": { "search_context_size_high": 0.01, "search_context_size_low": 0.01, @@ -38112,6 +38122,7 @@ "max_tokens": 128000, "mode": "chat", "output_cost_per_token": 2.5e-05, + "regional_endpoint_uplift_multiplier": 1.1, "search_context_cost_per_query": { "search_context_size_high": 0.01, "search_context_size_low": 0.01, @@ -38144,6 +38155,7 @@ "max_tokens": 128000, "mode": "chat", "output_cost_per_token": 2.5e-05, + "regional_endpoint_uplift_multiplier": 1.1, "search_context_cost_per_query": { "search_context_size_high": 0.01, "search_context_size_low": 0.01, @@ -38176,6 +38188,7 @@ "max_tokens": 128000, "mode": "chat", "output_cost_per_token": 2.5e-05, + "regional_endpoint_uplift_multiplier": 1.1, "search_context_cost_per_query": { "search_context_size_high": 0.01, "search_context_size_low": 0.01, @@ -38208,6 +38221,7 @@ "max_tokens": 128000, "mode": "chat", "output_cost_per_token": 2.5e-05, + "regional_endpoint_uplift_multiplier": 1.1, "search_context_cost_per_query": { "search_context_size_high": 0.01, "search_context_size_low": 0.01, @@ -38243,6 +38257,7 @@ "max_tokens": 64000, "mode": "chat", "output_cost_per_token": 1.5e-05, + "regional_endpoint_uplift_multiplier": 1.1, "output_cost_per_token_batches": 7.5e-06, "supports_assistant_prefill": true, "supports_computer_use": true, @@ -38267,6 +38282,7 @@ "max_tokens": 128000, "mode": "chat", "output_cost_per_token": 1e-05, + "regional_endpoint_uplift_multiplier": 1.1, "search_context_cost_per_query": { "search_context_size_high": 0.01, "search_context_size_low": 0.01, @@ -38299,6 +38315,7 @@ "max_tokens": 64000, "mode": "chat", "output_cost_per_token": 1.5e-05, + "regional_endpoint_uplift_multiplier": 1.1, "supports_assistant_prefill": true, "supports_computer_use": true, "supports_function_calling": true, @@ -38333,6 +38350,7 @@ "max_tokens": 64000, "mode": "chat", "output_cost_per_token": 1.5e-05, + "regional_endpoint_uplift_multiplier": 1.1, "output_cost_per_token_batches": 7.5e-06, "supports_assistant_prefill": true, "supports_computer_use": true, @@ -45925,6 +45943,7 @@ "max_tokens": 128000, "mode": "chat", "output_cost_per_token": 1e-05, + "regional_endpoint_uplift_multiplier": 1.1, "search_context_cost_per_query": { "search_context_size_high": 0.01, "search_context_size_low": 0.01, @@ -45957,6 +45976,7 @@ "max_tokens": 64000, "mode": "chat", "output_cost_per_token": 1.5e-05, + "regional_endpoint_uplift_multiplier": 1.1, "supports_assistant_prefill": true, "supports_computer_use": true, "supports_function_calling": true, diff --git a/model_prices_and_context_window.schema.json b/model_prices_and_context_window.schema.json index 56400e0666b..e1f23e12395 100644 --- a/model_prices_and_context_window.schema.json +++ b/model_prices_and_context_window.schema.json @@ -505,6 +505,11 @@ "type": "object", "description": "Provider-internal routing hints (e.g. bedrock_invocation_schema)." }, + "regional_endpoint_uplift_multiplier": { + "type": "number", + "minimum": 1, + "description": "Multiplier applied to all token costs when the request is served from a regional or multi-region endpoint instead of the global one (e.g. 1.10 = +10%)." + }, "regional_processing_uplift_multiplier_eu": { "type": "number", "minimum": 1, 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 28a6c8dd18d..498c3f04937 100644 --- a/tests/test_litellm/litellm_core_utils/test_litellm_logging.py +++ b/tests/test_litellm/litellm_core_utils/test_litellm_logging.py @@ -4539,3 +4539,39 @@ async def test_restore_correlation_context_works_across_asyncio_task_boundary(): finally: trace_id_var.set("") session_id_var.set("") + + +@pytest.mark.parametrize( + "vertex_location, uplift", + [("global", 1.0), ("us-east5", 1.1)], +) +def test_response_cost_calculator_passes_vertex_location(monkeypatch, vertex_location, uplift): + """Vertex charges 10% more on regional and multi-region endpoints than on the global one, so the + deployment's vertex_location has to reach the cost calculator or regional spend is undercounted.""" + monkeypatch.setenv("LITELLM_LOCAL_MODEL_COST_MAP", "True") + monkeypatch.setattr(litellm, "model_cost", litellm.get_model_cost_map(url="")) + + logging_obj = LitellmLogging( + model="vertex_ai/claude-sonnet-4-6", + messages=[{"role": "user", "content": "Hey"}], + stream=False, + call_type="completion", + start_time=time.time(), + litellm_call_id="vertex-location-123", + function_id="test-fn", + ) + logging_obj.update_environment_variables( + model="vertex_ai/claude-sonnet-4-6", + user="", + optional_params={}, + litellm_params={"vertex_location": vertex_location, "vertex_project": "test-project"}, + ) + logging_obj.model_call_details["custom_llm_provider"] = "vertex_ai" + + response = ModelResponse( + model="claude-sonnet-4-6", + usage=litellm.Usage(prompt_tokens=1000, completion_tokens=200, total_tokens=1200), + ) + cost = logging_obj._response_cost_calculator(result=response) + + assert cost == pytest.approx((1000 * 3e-6 + 200 * 1.5e-5) * uplift, rel=1e-9) diff --git a/tests/test_litellm/test_cost_calculator.py b/tests/test_litellm/test_cost_calculator.py index 3f024e2fd03..a45ac3a3478 100644 --- a/tests/test_litellm/test_cost_calculator.py +++ b/tests/test_litellm/test_cost_calculator.py @@ -3530,3 +3530,111 @@ def test_completion_cost_prices_anthropic_shaped_cache_read_tokens(): ) assert cost == pytest.approx(3 * 5e-6 + 4014 * 5e-7 + 5 * 3e-5, rel=1e-9) + + +def _vertex_claude_usage() -> Usage: + return Usage( + prompt_tokens=3500, + completion_tokens=200, + total_tokens=3700, + cache_creation_input_tokens=500, + cache_read_input_tokens=2000, + ) + + +def test_vertex_claude_regional_endpoint_uplift_applied(monkeypatch): + """Regression: Vertex bills regional and multi-region endpoints 10% above the global + endpoint, so a deployment pinned to vertex_location=us-east5 was undercharged by 10% + on every token type while we priced it at global rates.""" + monkeypatch.setenv("LITELLM_LOCAL_MODEL_COST_MAP", "True") + monkeypatch.setattr(litellm, "model_cost", litellm.get_model_cost_map(url="")) + + usage = _vertex_claude_usage() + global_prompt, global_completion = cost_per_token( + model="claude-sonnet-4-6", + custom_llm_provider="vertex_ai", + usage_object=usage, + vertex_location="global", + ) + regional_prompt, regional_completion = cost_per_token( + model="claude-sonnet-4-6", + custom_llm_provider="vertex_ai", + usage_object=usage, + vertex_location="us-east5", + ) + + expected_global_prompt = 1000 * 3e-6 + 500 * 3.75e-6 + 2000 * 3e-7 + assert global_prompt == pytest.approx(expected_global_prompt, rel=1e-9) + assert global_completion == pytest.approx(200 * 1.5e-5, rel=1e-9) + assert regional_prompt == pytest.approx(global_prompt * 1.1, rel=1e-9) + assert regional_completion == pytest.approx(global_completion * 1.1, rel=1e-9) + + +def test_vertex_claude_no_uplift_without_location(monkeypatch): + """An unknown location must not change pricing, so existing global-endpoint spend stays put.""" + monkeypatch.setenv("LITELLM_LOCAL_MODEL_COST_MAP", "True") + monkeypatch.setattr(litellm, "model_cost", litellm.get_model_cost_map(url="")) + + usage = _vertex_claude_usage() + baseline = cost_per_token( + model="claude-opus-4-7", + custom_llm_provider="vertex_ai", + usage_object=usage, + ) + assert baseline == cost_per_token( + model="claude-opus-4-7", + custom_llm_provider="vertex_ai", + usage_object=usage, + vertex_location="GLOBAL", + ) + regional = cost_per_token( + model="claude-opus-4-7", + custom_llm_provider="vertex_ai", + usage_object=usage, + vertex_location="us", + ) + assert regional[0] == pytest.approx(baseline[0] * 1.1, rel=1e-9) + assert regional[1] == pytest.approx(baseline[1] * 1.1, rel=1e-9) + + +def test_vertex_gemini_unaffected_by_location(monkeypatch): + """Only Vertex Claude models carry the regional uplift, Gemini pricing is location independent.""" + monkeypatch.setenv("LITELLM_LOCAL_MODEL_COST_MAP", "True") + monkeypatch.setattr(litellm, "model_cost", litellm.get_model_cost_map(url="")) + + usage = Usage(prompt_tokens=1000, completion_tokens=200, total_tokens=1200) + assert cost_per_token( + model="gemini-2.5-pro", + custom_llm_provider="vertex_ai", + usage_object=usage, + vertex_location="us-east5", + ) == cost_per_token( + model="gemini-2.5-pro", + custom_llm_provider="vertex_ai", + usage_object=usage, + ) + + +def test_completion_cost_applies_vertex_regional_uplift(monkeypatch): + """End-to-end: the location on the deployment's litellm_params must reach the cost calculator.""" + monkeypatch.setenv("LITELLM_LOCAL_MODEL_COST_MAP", "True") + monkeypatch.setattr(litellm, "model_cost", litellm.get_model_cost_map(url="")) + + response = ModelResponse( + model="claude-sonnet-4-6", + usage=Usage(prompt_tokens=1000, completion_tokens=200, total_tokens=1200), + ) + global_cost = completion_cost( + completion_response=response, + model="claude-sonnet-4-6", + custom_llm_provider="vertex_ai", + ) + regional_cost = completion_cost( + completion_response=response, + model="claude-sonnet-4-6", + custom_llm_provider="vertex_ai", + vertex_location="europe-west1", + ) + + assert global_cost == pytest.approx(1000 * 3e-6 + 200 * 1.5e-5, rel=1e-9) + assert regional_cost == pytest.approx(global_cost * 1.1, rel=1e-9)