feat(interactions): map Interactions API usage into litellm Usage for cost tracking

This commit is contained in:
mateo-berri 2026-07-14 16:19:26 -07:00
parent 668df9494a
commit 06a0a45c40
5 changed files with 135 additions and 3 deletions

View file

@ -19,6 +19,7 @@ from litellm.litellm_core_utils.llm_cost_calc.tool_call_cost_tracking import (
StandardBuiltInToolCostTracking,
)
from litellm.litellm_core_utils.llm_cost_calc.usage_object_transformation import (
InteractionsUsageObjectTransformation,
TranscriptionUsageObjectTransformation,
)
from litellm.litellm_core_utils.llm_cost_calc.utils import (
@ -899,6 +900,8 @@ def _get_usage_object(
usage_obj,
)
)
elif isinstance(usage_obj, dict) and InteractionsUsageObjectTransformation.is_interactions_usage_dict(usage_obj):
return InteractionsUsageObjectTransformation.transform_interactions_usage_to_chat_usage(usage_obj)
elif isinstance(usage_obj, dict):
return Usage(**usage_obj)
elif isinstance(usage_obj, BaseModel):
@ -1267,6 +1270,12 @@ def completion_cost(
)
if tr_usage is not None:
_usage = tr_usage.model_dump()
elif isinstance(_usage, dict) and InteractionsUsageObjectTransformation.is_interactions_usage_dict(
_usage
):
_usage = InteractionsUsageObjectTransformation.transform_interactions_usage_to_chat_usage(
_usage
).model_dump()
else:
_usage = _usage

View file

@ -77,9 +77,13 @@ from litellm.litellm_core_utils.redact_messages import (
)
from litellm.llms.base_llm.ocr.transformation import OCRResponse
from litellm.llms.base_llm.search.transformation import SearchResponse
from litellm.litellm_core_utils.llm_cost_calc.usage_object_transformation import (
InteractionsUsageObjectTransformation,
)
from litellm.responses.utils import ResponseAPILoggingUtils
from litellm.types.agents import LiteLLMSendMessageResponse
from litellm.types.containers.main import ContainerObject
from litellm.types.interactions import InteractionsAPIResponse, InteractionsAPIStreamingResponse
from litellm.types.llms.openai import (
AllMessageValues,
Batch,
@ -1675,6 +1679,9 @@ class Logging(LiteLLMLoggingBaseClass):
results=result,
)
elif self._is_interactions_create_call_type() and isinstance(result, InteractionsAPIStreamingResponse):
logging_result = self._interactions_streaming_result_as_response(result)
elif (
self.call_type == CallTypes.llm_passthrough_route.value
or self.call_type == CallTypes.allm_passthrough_route.value
@ -1696,6 +1703,28 @@ class Logging(LiteLLMLoggingBaseClass):
)
return logging_result
def _is_interactions_create_call_type(self) -> bool:
return self.call_type in (
CallTypes.create_interaction.value,
CallTypes.acreate_interaction.value,
)
def _interactions_streaming_result_as_response(
self, result: InteractionsAPIStreamingResponse
) -> InteractionsAPIResponse:
interaction = result.interaction if isinstance(result.interaction, dict) else {}
return InteractionsAPIResponse(
id=interaction.get("id") or result.id or result.interaction_id,
model=interaction.get("model") or result.model,
agent=interaction.get("agent") or result.agent,
status=interaction.get("status") or result.status,
created=interaction.get("created") or result.created,
updated=interaction.get("updated") or result.updated,
outputs=interaction.get("outputs") or result.outputs,
steps=interaction.get("steps") or result.steps,
usage=interaction.get("usage") or result.usage,
)
def _merge_hidden_params_from_response_into_metadata(self, logging_result: Any) -> None:
"""
Copy response._hidden_params into litellm_params.metadata['hidden_params'].
@ -1787,6 +1816,20 @@ class Logging(LiteLLMLoggingBaseClass):
else dict(transformed_usage)
)
standard_logging_payload["response"] = response_dict
elif isinstance(result, InteractionsAPIResponse):
result = result.model_copy()
transformed_usage = InteractionsUsageObjectTransformation.transform_interactions_usage_to_chat_usage(
result.usage
)
setattr(result, "usage", transformed_usage)
if (standard_logging_payload := self.model_call_details.get("standard_logging_object")) is not None:
response_dict = result.model_dump() if hasattr(result, "model_dump") else dict(result)
response_dict["usage"] = (
transformed_usage.model_dump()
if hasattr(transformed_usage, "model_dump")
else dict(transformed_usage)
)
standard_logging_payload["response"] = response_dict
elif isinstance(result, TranscriptionResponse):
from litellm.litellm_core_utils.llm_cost_calc.usage_object_transformation import (
TranscriptionUsageObjectTransformation,
@ -1832,7 +1875,14 @@ class Logging(LiteLLMLoggingBaseClass):
logging_result = self.normalize_logging_result(result=result)
if standard_logging_object is None and result is not None and self.stream is not True:
is_final_interactions_stream_result = self._is_interactions_create_call_type() and isinstance(
logging_result, InteractionsAPIResponse
)
if (
standard_logging_object is None
and result is not None
and (self.stream is not True or is_final_interactions_stream_result)
):
if self._is_recognized_call_type_for_logging(logging_result=logging_result) or isinstance(
logging_result, (dict, list)
):
@ -1888,6 +1938,7 @@ class Logging(LiteLLMLoggingBaseClass):
or isinstance(logging_result, FineTuningJob)
or isinstance(logging_result, LiteLLMBatch)
or isinstance(logging_result, ResponsesAPIResponse)
or (isinstance(logging_result, InteractionsAPIResponse) and self._is_interactions_create_call_type())
or isinstance(logging_result, OpenAIFileObject)
or isinstance(logging_result, LiteLLMRealtimeStreamLoggingObject)
or isinstance(logging_result, OpenAIModerationResponse)
@ -4687,6 +4738,8 @@ class StandardLoggingPayloadSetup:
elif isinstance(usage, dict):
if ResponseAPILoggingUtils._is_response_api_usage(usage):
return ResponseAPILoggingUtils._transform_response_api_usage_to_chat_usage(usage)
if InteractionsUsageObjectTransformation.is_interactions_usage_dict(usage):
return InteractionsUsageObjectTransformation.transform_interactions_usage_to_chat_usage(usage)
return Usage(**usage)
raise ValueError(f"usage is required, got={usage} of type {type(usage)}")
@ -4713,6 +4766,10 @@ class StandardLoggingPayloadSetup:
if isinstance(_raw, dict):
if ResponseAPILoggingUtils._is_response_api_usage(_raw):
return ResponseAPILoggingUtils._transform_response_api_usage_to_chat_usage(_raw).model_dump()
if InteractionsUsageObjectTransformation.is_interactions_usage_dict(_raw):
return InteractionsUsageObjectTransformation.transform_interactions_usage_to_chat_usage(
_raw
).model_dump()
return _raw
if isinstance(_raw, Usage):
return _raw.model_dump()

View file

@ -1,6 +1,7 @@
from typing import Any, Optional, Union
from typing import Any, Mapping, Optional, Sequence, Union
from litellm.types.utils import (
CompletionTokensDetailsWrapper,
PromptTokensDetailsWrapper,
TranscriptionUsageDurationObject,
TranscriptionUsageTokensObject,
@ -34,3 +35,62 @@ class TranscriptionUsageObjectTransformation:
),
)
return None
def _coerce_optional_token_count(value: object) -> int:
return value if isinstance(value, int) else 0
def _tokens_by_modality(modality_entries: object) -> Mapping[str, int]:
if not isinstance(modality_entries, Sequence):
return {}
return {
str(entry.get("modality") or "").lower(): _coerce_optional_token_count(entry.get("tokens"))
for entry in modality_entries
if isinstance(entry, Mapping)
}
class InteractionsUsageObjectTransformation:
@staticmethod
def is_interactions_usage_dict(usage_object: Mapping[str, Any]) -> bool:
return (
"total_input_tokens" in usage_object
or "total_output_tokens" in usage_object
or "input_tokens_by_modality" in usage_object
or "output_tokens_by_modality" in usage_object
)
@staticmethod
def transform_interactions_usage_to_chat_usage(
usage_object: "Mapping[str, Any] | None",
) -> Usage:
usage = usage_object or {}
input_tokens_by_modality = _tokens_by_modality(usage.get("input_tokens_by_modality"))
output_tokens_by_modality = _tokens_by_modality(usage.get("output_tokens_by_modality"))
prompt_tokens = _coerce_optional_token_count(usage.get("total_input_tokens"))
reasoning_tokens = _coerce_optional_token_count(
usage.get("total_thought_tokens") or usage.get("total_reasoning_tokens")
)
completion_tokens = _coerce_optional_token_count(usage.get("total_output_tokens")) + reasoning_tokens
cached_tokens = _coerce_optional_token_count(usage.get("total_cached_tokens"))
total_tokens = _coerce_optional_token_count(usage.get("total_tokens")) or (prompt_tokens + completion_tokens)
return Usage(
prompt_tokens=prompt_tokens,
completion_tokens=completion_tokens,
total_tokens=total_tokens,
prompt_tokens_details=PromptTokensDetailsWrapper(
cached_tokens=cached_tokens,
text_tokens=input_tokens_by_modality.get("text"),
image_tokens=input_tokens_by_modality.get("image"),
audio_tokens=input_tokens_by_modality.get("audio"),
video_tokens=input_tokens_by_modality.get("video"),
),
completion_tokens_details=CompletionTokensDetailsWrapper(
reasoning_tokens=reasoning_tokens,
text_tokens=output_tokens_by_modality.get("text"),
image_tokens=output_tokens_by_modality.get("image"),
audio_tokens=output_tokens_by_modality.get("audio"),
video_tokens=output_tokens_by_modality.get("video"),
),
)

View file

@ -396,6 +396,12 @@ class CallTypes(str, Enum):
vector_store_search = "vector_store_search"
avector_store_search = "avector_store_search"
#########################################################
# Google Interactions API Call Types
#########################################################
create_interaction = "create_interaction"
acreate_interaction = "acreate_interaction"
#########################################################
# Container Call Types
#########################################################

View file

@ -21685,7 +21685,7 @@ export interface components {
* CallTypes
* @enum {string}
*/
CallTypes: "embedding" | "aembedding" | "completion" | "acompletion" | "atext_completion" | "text_completion" | "image_generation" | "aimage_generation" | "image_edit" | "aimage_edit" | "moderation" | "amoderation" | "atranscription" | "transcription" | "aspeech" | "speech" | "rerank" | "arerank" | "search" | "asearch" | "_arealtime" | "_aresponses_websocket" | "create_batch" | "acreate_batch" | "aretrieve_batch" | "retrieve_batch" | "acancel_batch" | "cancel_batch" | "pass_through_endpoint" | "anthropic_messages" | "get_assistants" | "aget_assistants" | "create_assistants" | "acreate_assistants" | "delete_assistant" | "adelete_assistant" | "acreate_thread" | "create_thread" | "aget_thread" | "get_thread" | "a_add_message" | "add_message" | "aget_messages" | "get_messages" | "arun_thread" | "run_thread" | "arun_thread_stream" | "run_thread_stream" | "afile_retrieve" | "file_retrieve" | "afile_delete" | "file_delete" | "afile_list" | "file_list" | "acreate_file" | "create_file" | "afile_content" | "file_content" | "create_fine_tuning_job" | "acreate_fine_tuning_job" | "create_video" | "acreate_video" | "avideo_retrieve" | "video_retrieve" | "avideo_content" | "video_content" | "video_remix" | "avideo_remix" | "video_list" | "avideo_list" | "video_retrieve_job" | "avideo_retrieve_job" | "video_delete" | "avideo_delete" | "video_create_character" | "avideo_create_character" | "video_get_character" | "avideo_get_character" | "video_edit" | "avideo_edit" | "video_extension" | "avideo_extension" | "vector_store_file_create" | "avector_store_file_create" | "vector_store_file_list" | "avector_store_file_list" | "vector_store_file_retrieve" | "avector_store_file_retrieve" | "vector_store_file_content" | "avector_store_file_content" | "vector_store_file_update" | "avector_store_file_update" | "vector_store_file_delete" | "avector_store_file_delete" | "vector_store_create" | "avector_store_create" | "vector_store_search" | "avector_store_search" | "create_container" | "acreate_container" | "list_containers" | "alist_containers" | "retrieve_container" | "aretrieve_container" | "delete_container" | "adelete_container" | "list_container_files" | "alist_container_files" | "upload_container_file" | "aupload_container_file" | "create_sandbox" | "acreate_sandbox" | "delete_sandbox" | "adelete_sandbox" | "run_code" | "arun_code" | "code_interpreter_tool" | "acode_interpreter_tool" | "acancel_fine_tuning_job" | "cancel_fine_tuning_job" | "alist_fine_tuning_jobs" | "list_fine_tuning_jobs" | "aretrieve_fine_tuning_job" | "retrieve_fine_tuning_job" | "responses" | "aresponses" | "alist_input_items" | "llm_passthrough_route" | "allm_passthrough_route" | "generate_content" | "agenerate_content" | "generate_content_stream" | "agenerate_content_stream" | "ocr" | "aocr" | "call_mcp_tool" | "list_mcp_tools" | "asend_message" | "send_message" | "acreate_skill";
CallTypes: "embedding" | "aembedding" | "completion" | "acompletion" | "atext_completion" | "text_completion" | "image_generation" | "aimage_generation" | "image_edit" | "aimage_edit" | "moderation" | "amoderation" | "atranscription" | "transcription" | "aspeech" | "speech" | "rerank" | "arerank" | "search" | "asearch" | "_arealtime" | "_aresponses_websocket" | "create_batch" | "acreate_batch" | "aretrieve_batch" | "retrieve_batch" | "acancel_batch" | "cancel_batch" | "pass_through_endpoint" | "anthropic_messages" | "get_assistants" | "aget_assistants" | "create_assistants" | "acreate_assistants" | "delete_assistant" | "adelete_assistant" | "acreate_thread" | "create_thread" | "aget_thread" | "get_thread" | "a_add_message" | "add_message" | "aget_messages" | "get_messages" | "arun_thread" | "run_thread" | "arun_thread_stream" | "run_thread_stream" | "afile_retrieve" | "file_retrieve" | "afile_delete" | "file_delete" | "afile_list" | "file_list" | "acreate_file" | "create_file" | "afile_content" | "file_content" | "create_fine_tuning_job" | "acreate_fine_tuning_job" | "create_video" | "acreate_video" | "avideo_retrieve" | "video_retrieve" | "avideo_content" | "video_content" | "video_remix" | "avideo_remix" | "video_list" | "avideo_list" | "video_retrieve_job" | "avideo_retrieve_job" | "video_delete" | "avideo_delete" | "video_create_character" | "avideo_create_character" | "video_get_character" | "avideo_get_character" | "video_edit" | "avideo_edit" | "video_extension" | "avideo_extension" | "vector_store_file_create" | "avector_store_file_create" | "vector_store_file_list" | "avector_store_file_list" | "vector_store_file_retrieve" | "avector_store_file_retrieve" | "vector_store_file_content" | "avector_store_file_content" | "vector_store_file_update" | "avector_store_file_update" | "vector_store_file_delete" | "avector_store_file_delete" | "vector_store_create" | "avector_store_create" | "vector_store_search" | "avector_store_search" | "create_interaction" | "acreate_interaction" | "create_container" | "acreate_container" | "list_containers" | "alist_containers" | "retrieve_container" | "aretrieve_container" | "delete_container" | "adelete_container" | "list_container_files" | "alist_container_files" | "upload_container_file" | "aupload_container_file" | "create_sandbox" | "acreate_sandbox" | "delete_sandbox" | "adelete_sandbox" | "run_code" | "arun_code" | "code_interpreter_tool" | "acode_interpreter_tool" | "acancel_fine_tuning_job" | "cancel_fine_tuning_job" | "alist_fine_tuning_jobs" | "list_fine_tuning_jobs" | "aretrieve_fine_tuning_job" | "retrieve_fine_tuning_job" | "responses" | "aresponses" | "alist_input_items" | "llm_passthrough_route" | "allm_passthrough_route" | "generate_content" | "agenerate_content" | "generate_content_stream" | "agenerate_content_stream" | "ocr" | "aocr" | "call_mcp_tool" | "list_mcp_tools" | "asend_message" | "send_message" | "acreate_skill";
/** CallbackDelete */
CallbackDelete: {
/** Callback Name */