diff --git a/litellm/cost_calculator.py b/litellm/cost_calculator.py index a40a8e1389c..8886759d40d 100644 --- a/litellm/cost_calculator.py +++ b/litellm/cost_calculator.py @@ -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 diff --git a/litellm/litellm_core_utils/litellm_logging.py b/litellm/litellm_core_utils/litellm_logging.py index 461ab62b815..36876ce11ff 100644 --- a/litellm/litellm_core_utils/litellm_logging.py +++ b/litellm/litellm_core_utils/litellm_logging.py @@ -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() diff --git a/litellm/litellm_core_utils/llm_cost_calc/usage_object_transformation.py b/litellm/litellm_core_utils/llm_cost_calc/usage_object_transformation.py index 1c6adbec174..a592c5557db 100644 --- a/litellm/litellm_core_utils/llm_cost_calc/usage_object_transformation.py +++ b/litellm/litellm_core_utils/llm_cost_calc/usage_object_transformation.py @@ -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"), + ), + ) diff --git a/litellm/types/utils.py b/litellm/types/utils.py index 90ea99ceb23..0fafd4a27fe 100644 --- a/litellm/types/utils.py +++ b/litellm/types/utils.py @@ -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 ######################################################### diff --git a/ui/litellm-dashboard/src/lib/http/schema.d.ts b/ui/litellm-dashboard/src/lib/http/schema.d.ts index 5629cd6c8d4..59c0532ccbe 100644 --- a/ui/litellm-dashboard/src/lib/http/schema.d.ts +++ b/ui/litellm-dashboard/src/lib/http/schema.d.ts @@ -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 */