test(interactions): cover usage transformation, cost calculation, and success-handler billing

This commit is contained in:
mateo-berri 2026-07-14 16:19:26 -07:00
parent 06a0a45c40
commit a798aca9a0
3 changed files with 251 additions and 0 deletions

View file

@ -0,0 +1,73 @@
from litellm.litellm_core_utils.llm_cost_calc.usage_object_transformation import (
InteractionsUsageObjectTransformation,
)
def _omni_flash_video_usage() -> dict:
return {
"total_tokens": 18247,
"total_input_tokens": 16,
"input_tokens_by_modality": [{"modality": "text", "tokens": 16}],
"total_cached_tokens": 0,
"total_output_tokens": 17937,
"output_tokens_by_modality": [
{"modality": "text", "tokens": 561},
{"modality": "video", "tokens": 17376},
],
"total_tool_use_tokens": 0,
"total_thought_tokens": 294,
}
def test_is_interactions_usage_dict():
assert InteractionsUsageObjectTransformation.is_interactions_usage_dict(_omni_flash_video_usage())
assert InteractionsUsageObjectTransformation.is_interactions_usage_dict({"total_output_tokens": 5})
assert not InteractionsUsageObjectTransformation.is_interactions_usage_dict(
{"prompt_tokens": 1, "completion_tokens": 2}
)
assert not InteractionsUsageObjectTransformation.is_interactions_usage_dict(
{"input_tokens": 1, "output_tokens": 2}
)
def test_transform_interactions_usage_maps_modalities():
usage = InteractionsUsageObjectTransformation.transform_interactions_usage_to_chat_usage(
_omni_flash_video_usage()
)
assert usage.prompt_tokens == 16
assert usage.completion_tokens == 17937 + 294
assert usage.total_tokens == 18247
assert usage.prompt_tokens_details is not None
assert usage.prompt_tokens_details.text_tokens == 16
assert usage.prompt_tokens_details.cached_tokens == 0
assert usage.completion_tokens_details is not None
assert usage.completion_tokens_details.reasoning_tokens == 294
assert usage.completion_tokens_details.text_tokens == 561
assert usage.completion_tokens_details.video_tokens == 17376
def test_transform_interactions_usage_reasoning_key_fallback():
usage = InteractionsUsageObjectTransformation.transform_interactions_usage_to_chat_usage(
{"total_input_tokens": 10, "total_output_tokens": 20, "total_reasoning_tokens": 5}
)
assert usage.prompt_tokens == 10
assert usage.completion_tokens == 25
assert usage.total_tokens == 35
assert usage.completion_tokens_details is not None
assert usage.completion_tokens_details.reasoning_tokens == 5
def test_transform_interactions_usage_cached_tokens():
usage = InteractionsUsageObjectTransformation.transform_interactions_usage_to_chat_usage(
{"total_input_tokens": 100, "total_cached_tokens": 40, "total_output_tokens": 10}
)
assert usage.prompt_tokens == 100
assert usage.prompt_tokens_details is not None
assert usage.prompt_tokens_details.cached_tokens == 40
def test_transform_interactions_usage_empty():
usage = InteractionsUsageObjectTransformation.transform_interactions_usage_to_chat_usage(None)
assert usage.prompt_tokens == 0
assert usage.completion_tokens == 0
assert usage.total_tokens == 0

View file

@ -3773,3 +3773,140 @@ def test_zero_token_video_usage_preserves_duration_seconds(logging_obj):
assert payload["metadata"]["usage_object"]["duration_seconds"] == 4.0
assert payload["total_tokens"] == 0
assert payload["completion_tokens"] == 0
def _register_fake_interactions_model():
litellm.register_model(
model_cost={
"gemini/fake-interactions-model": {
"litellm_provider": "gemini",
"mode": "chat",
"input_cost_per_token": 3e-7,
"output_cost_per_token": 2.5e-6,
}
}
)
def _interactions_usage_dict():
return {
"total_tokens": 871,
"total_input_tokens": 16,
"input_tokens_by_modality": [{"modality": "text", "tokens": 16}],
"total_cached_tokens": 0,
"total_output_tokens": 561,
"output_tokens_by_modality": [{"modality": "text", "tokens": 561}],
"total_tool_use_tokens": 0,
"total_thought_tokens": 294,
}
_EXPECTED_INTERACTIONS_COST = 16 * 3e-7 + (561 + 294) * 2.5e-6
def _interactions_logging_obj(call_type: str, stream: bool) -> LitellmLogging:
import datetime
logging_obj = LitellmLogging(
model="gemini/fake-interactions-model",
messages=[{"role": "user", "content": "hi"}],
stream=stream,
call_type=call_type,
start_time=datetime.datetime.now(),
litellm_call_id="interactions-cost-test",
function_id="interactions-cost-test",
)
logging_obj.update_from_kwargs(
kwargs={},
model="gemini/fake-interactions-model",
optional_params={},
litellm_params={"litellm_call_id": "interactions-cost-test"},
custom_llm_provider="gemini",
)
return logging_obj
def test_interactions_create_success_tracks_response_cost():
import datetime
from litellm.types.interactions import InteractionsAPIResponse
_register_fake_interactions_model()
logging_obj = _interactions_logging_obj(call_type="acreate_interaction", stream=False)
result = InteractionsAPIResponse(
id="interaction_123",
model="fake-interactions-model",
status="completed",
steps=[],
usage=_interactions_usage_dict(),
)
logging_obj._success_handler_helper_fn(
result=result,
start_time=datetime.datetime.now(),
end_time=datetime.datetime.now(),
cache_hit=False,
)
assert logging_obj.model_call_details["response_cost"] == pytest.approx(_EXPECTED_INTERACTIONS_COST)
standard_logging_object = logging_obj.model_call_details["standard_logging_object"]
assert standard_logging_object["prompt_tokens"] == 16
assert standard_logging_object["completion_tokens"] == 561 + 294
assert standard_logging_object["total_tokens"] == 871
assert standard_logging_object["response_cost"] == pytest.approx(_EXPECTED_INTERACTIONS_COST)
def test_interactions_streaming_completed_event_tracks_response_cost():
import datetime
from litellm.types.interactions import InteractionsAPIStreamingResponse
_register_fake_interactions_model()
logging_obj = _interactions_logging_obj(call_type="acreate_interaction", stream=True)
result = InteractionsAPIStreamingResponse(
event_type="interaction.completed",
interaction={
"id": "interaction_123",
"model": "fake-interactions-model",
"status": "completed",
"steps": [],
"usage": _interactions_usage_dict(),
},
)
logging_obj._success_handler_helper_fn(
result=result,
start_time=datetime.datetime.now(),
end_time=datetime.datetime.now(),
cache_hit=False,
)
assert logging_obj.model_call_details["response_cost"] == pytest.approx(_EXPECTED_INTERACTIONS_COST)
standard_logging_object = logging_obj.model_call_details["standard_logging_object"]
assert standard_logging_object["prompt_tokens"] == 16
assert standard_logging_object["completion_tokens"] == 561 + 294
def test_interactions_get_poll_does_not_bill():
import datetime
from litellm.types.interactions import InteractionsAPIResponse
_register_fake_interactions_model()
logging_obj = _interactions_logging_obj(call_type="aget_interaction", stream=False)
result = InteractionsAPIResponse(
id="interaction_123",
model="fake-interactions-model",
status="completed",
steps=[],
usage=_interactions_usage_dict(),
)
logging_obj._success_handler_helper_fn(
result=result,
start_time=datetime.datetime.now(),
end_time=datetime.datetime.now(),
cache_hit=False,
)
assert logging_obj.model_call_details.get("response_cost") is None

View file

@ -3479,3 +3479,44 @@ def test_batch_cost_calculator_cache_creation_falls_back_to_input_rate():
)
assert prompt_cost == pytest.approx((1000 * 3e-6 + 8000 * 3e-7 + 2000 * 3e-6) / 2)
def test_completion_cost_interactions_api_response():
from litellm.types.interactions import InteractionsAPIResponse
litellm.register_model(
model_cost={
"gemini/fake-interactions-model": {
"litellm_provider": "gemini",
"mode": "chat",
"input_cost_per_token": 3e-7,
"output_cost_per_token": 2.5e-6,
}
}
)
response = InteractionsAPIResponse(
id="interaction_123",
model="fake-interactions-model",
status="completed",
steps=[],
usage={
"total_tokens": 871,
"total_input_tokens": 16,
"input_tokens_by_modality": [{"modality": "text", "tokens": 16}],
"total_cached_tokens": 0,
"total_output_tokens": 561,
"output_tokens_by_modality": [{"modality": "text", "tokens": 561}],
"total_tool_use_tokens": 0,
"total_thought_tokens": 294,
},
)
cost = completion_cost(
completion_response=response,
model="gemini/fake-interactions-model",
custom_llm_provider="gemini",
call_type="acreate_interaction",
)
assert cost == pytest.approx(16 * 3e-7 + (561 + 294) * 2.5e-6)