From 5684267b1237348c06e78957eafa2c67db8589db Mon Sep 17 00:00:00 2001 From: Kerry Lu Date: Tue, 20 Oct 2026 16:04:07 -0700 Subject: [PATCH] fix(logging): report the cost-map entry billing used in model_map_information (#47182) Backport of #47182 to stable/1.104.x. Cherry-picked from 1212260ab15fe2c9ff28f0c465ad2d0833da8143 (main). Adapted to the line's private helper names (_select_model_name_for_cost_calc, _response_cost_calculator); the new test is appended to the line's test file. --- litellm/litellm_core_utils/litellm_logging.py | 15 +++- .../test_litellm_logging.py | 84 +++++++++++++++++++ 2 files changed, 98 insertions(+), 1 deletion(-) diff --git a/litellm/litellm_core_utils/litellm_logging.py b/litellm/litellm_core_utils/litellm_logging.py index f601503824d..e03bc59b838 100644 --- a/litellm/litellm_core_utils/litellm_logging.py +++ b/litellm/litellm_core_utils/litellm_logging.py @@ -5863,6 +5863,7 @@ class StandardLoggingPayloadSetup: custom_llm_provider: str | None, init_response_obj: object, api_base: str | None = None, + router_model_id: str | None = None, ) -> StandardLoggingModelInformation: model_cost_name: Final = _select_model_name_for_cost_calc( model=base_model if custom_pricing else None, @@ -5870,12 +5871,23 @@ class StandardLoggingPayloadSetup: base_model=base_model, custom_pricing=custom_pricing, ) + pricing_model_name: Final = ( + _select_model_name_for_cost_calc( + model=base_model if custom_pricing else None, + completion_response=init_response_obj, + base_model=base_model, + custom_pricing=custom_pricing, + router_model_id=router_model_id, + ) + if router_model_id is not None + else model_cost_name + ) if model_cost_name is None: model_cost_information = StandardLoggingModelInformation(model_map_key="", model_map_value=None) else: try: _model_cost_information: Final = litellm.get_model_info( - model=model_cost_name, + model=pricing_model_name or model_cost_name, custom_llm_provider=custom_llm_provider, api_base=api_base, ) @@ -6537,6 +6549,7 @@ def get_standard_logging_object_payload( custom_llm_provider=kwargs.get("custom_llm_provider"), init_response_obj=init_response_obj, api_base=litellm_params.get("api_base"), + router_model_id=logging_obj.get_router_model_id(), ) error_information, error_str = StandardLoggingPayloadSetup.get_error_information_for_logging_payload( diff --git a/tests/unit/litellm_core_utils/test_litellm_logging.py b/tests/unit/litellm_core_utils/test_litellm_logging.py index d5aa0159b8d..2a524f46731 100644 --- a/tests/unit/litellm_core_utils/test_litellm_logging.py +++ b/tests/unit/litellm_core_utils/test_litellm_logging.py @@ -26,6 +26,7 @@ from litellm.integrations.custom_logger import CustomLogger from litellm.litellm_core_utils.litellm_logging import Logging as LitellmLogging from litellm.litellm_core_utils.litellm_logging import ( _extract_response_obj_and_hidden_params, + get_standard_logging_object_payload, _get_status_fields, set_callbacks, ) @@ -8841,3 +8842,86 @@ async def test_prompt_management_with_unchanged_variables_replays_a_byte_identic assert json.dumps(messages_n_plus_one[: len(messages_n)], sort_keys=True) == json.dumps(messages_n, sort_keys=True) assert messages_n[0] == {"role": "system", "content": "You are a pirate. Answer in one sentence."} assert len(messages_n_plus_one) == len(messages_n) + 2 +@pytest.mark.parametrize( + ("input_cost_per_token", "output_cost_per_token"), + [(1e-06, 2e-06), (0.0, 0.0)], + ids=["custom_price", "explicit_zero_price"], +) +def test_standard_logging_payload_reports_the_deployment_entry_the_request_was_billed_from( + input_cost_per_token: float, output_cost_per_token: float +): + """A deployment with its own price keeps the catalog model as model_map_key but is logged at its own rates.""" + deployment_id: Final = "lit9660-priced-deployment" + deployment_model_info: Final = { + "id": deployment_id, + "input_cost_per_token": input_cost_per_token, + "output_cost_per_token": output_cost_per_token, + } + litellm.register_model( + model_cost={ + deployment_id: { + "litellm_provider": "openai", + "mode": "chat", + "input_cost_per_token": input_cost_per_token, + "output_cost_per_token": output_cost_per_token, + } + }, + persist_across_reloads=False, + ) + try: + logging_obj: Final = LitellmLogging( + model="gpt-5.4-nano", + messages=[{"role": "user", "content": "Hi"}], + stream=False, + call_type="completion", + start_time=time.time(), + litellm_call_id="lit9660", + function_id="fn", + ) + litellm_params: Final = { + "metadata": { + "model_group": "lit9660-group", + "deployment": "openai/gpt-5.4-nano", + "model_info": deployment_model_info, + } + } + logging_obj.update_environment_variables( + model="gpt-5.4-nano", user="", optional_params={}, litellm_params=litellm_params, custom_llm_provider="openai" + ) + response: Final = ModelResponse( + model="gpt-5.4-nano", + choices=[litellm.Choices(message=litellm.Message(role="assistant", content="hi"))], + usage=litellm.Usage(prompt_tokens=10, completion_tokens=6, total_tokens=16), + ) + + response_cost: Final = logging_obj._response_cost_calculator(result=response) + payload: Final = get_standard_logging_object_payload( + kwargs={ + "model": "gpt-5.4-nano", + "custom_llm_provider": "openai", + "litellm_params": litellm_params, + "response_cost": response_cost, + }, + init_response_obj=response, + start_time=datetime.datetime.now(), + end_time=datetime.datetime.now(), + logging_obj=logging_obj, + status="success", + ) + + assert response_cost == 10 * input_cost_per_token + 6 * output_cost_per_token + assert payload is not None + assert payload["model_map_information"] == { + "model_map_key": "openai/gpt-5.4-nano", + "model_map_value": litellm.get_model_info(model=f"openai/{deployment_id}", custom_llm_provider="openai"), + } + model_map_value: Final = payload["model_map_information"]["model_map_value"] + assert model_map_value is not None + assert (model_map_value["input_cost_per_token"], model_map_value["output_cost_per_token"]) == ( + input_cost_per_token, + output_cost_per_token, + ) + finally: + litellm.model_cost.pop(deployment_id, None) + +