diff --git a/litellm/litellm_core_utils/audio_utils/utils.py b/litellm/litellm_core_utils/audio_utils/utils.py index 2f0db4978ff..a7d12841e58 100644 --- a/litellm/litellm_core_utils/audio_utils/utils.py +++ b/litellm/litellm_core_utils/audio_utils/utils.py @@ -2,6 +2,7 @@ Utils used for litellm.transcription() and litellm.atranscription() """ +import hashlib import os from dataclasses import dataclass from typing import Optional @@ -127,6 +128,67 @@ def get_audio_file_name(file_obj: FileTypes) -> str: return repr(file_obj) +def get_audio_file_content_hash(file_obj: FileTypes) -> str: + """ + Compute SHA-256 hash of audio file content for cache keys. + Falls back to filename hash if content extraction fails. + """ + file_content: Optional[bytes] = None + fallback_filename: Optional[str] = None + + if isinstance(file_obj, tuple): + if len(file_obj) < 2: + fallback_filename = str(file_obj[0]) if len(file_obj) > 0 else None + else: + fallback_filename = str(file_obj[0]) if file_obj[0] is not None else None + file_content_obj = file_obj[1] + else: + file_content_obj = file_obj + fallback_filename = get_audio_file_name(file_obj) + + try: + if isinstance(file_content_obj, (bytes, bytearray)): + file_content = bytes(file_content_obj) + elif isinstance(file_content_obj, (str, os.PathLike)): + try: + with open(str(file_content_obj), "rb") as f: + file_content = f.read() + if fallback_filename is None: + fallback_filename = str(file_content_obj) + except (OSError, IOError): + fallback_filename = str(file_content_obj) + file_content = None + elif hasattr(file_content_obj, "read"): + try: + current_position = file_content_obj.tell() if hasattr(file_content_obj, "tell") else None + if hasattr(file_content_obj, "seek"): + file_content_obj.seek(0) + file_content = file_content_obj.read() # type: ignore + if current_position is not None and hasattr(file_content_obj, "seek"): + file_content_obj.seek(current_position) # type: ignore + except (OSError, IOError, AttributeError): + file_content = None + else: + file_content = None + except Exception: + file_content = None + + if file_content is not None and isinstance(file_content, bytes): + try: + hash_object = hashlib.sha256(file_content) + return hash_object.hexdigest() + except Exception: + pass + + if fallback_filename: + hash_object = hashlib.sha256(fallback_filename.encode('utf-8')) + return hash_object.hexdigest() + + file_obj_str = str(file_obj) + hash_object = hashlib.sha256(file_obj_str.encode('utf-8')) + return hash_object.hexdigest() + + def get_audio_file_for_health_check() -> FileTypes: """ Get an audio file for health check diff --git a/litellm/utils.py b/litellm/utils.py index b283d5ae6e1..d58eb28a061 100644 --- a/litellm/utils.py +++ b/litellm/utils.py @@ -790,7 +790,7 @@ def function_setup( # noqa: PLR0915 ): _file_obj: FileTypes = args[1] if len(args) > 1 else kwargs["file"] file_checksum = ( - litellm.litellm_core_utils.audio_utils.utils.get_audio_file_name( + litellm.litellm_core_utils.audio_utils.utils.get_audio_file_content_hash( file_obj=_file_obj ) ) @@ -7346,6 +7346,8 @@ class ProviderConfigManager: return litellm.NvidiaNimRerankConfig() elif litellm.LlmProviders.VERTEX_AI == provider: return litellm.VertexAIRerankConfig() + elif litellm.LlmProviders.FIREWORKS_AI == provider: + return litellm.FireworksAIRerankConfig() return litellm.CohereRerankConfig() @staticmethod diff --git a/tests/test_litellm/litellm_core_utils/test_audio_utils.py b/tests/test_litellm/litellm_core_utils/test_audio_utils.py index 2fbb21e7c90..23c61ce90f0 100644 --- a/tests/test_litellm/litellm_core_utils/test_audio_utils.py +++ b/tests/test_litellm/litellm_core_utils/test_audio_utils.py @@ -12,6 +12,7 @@ import pytest from litellm.litellm_core_utils.audio_utils.utils import ( ProcessedAudioFile, calculate_request_duration, + get_audio_file_content_hash, get_audio_file_for_health_check, get_audio_file_name, process_audio_file, @@ -263,3 +264,51 @@ class TestCalculateRequestDuration: assert file_obj.tell() == len( wav_header ), "File position should be restored to original position" + + +class TestGetAudioFileContentHash: + """Test the get_audio_file_content_hash function for cache key generation""" + + def test_different_content_same_filename_different_hash(self): + """Test that different content with same filename produces different hashes""" + content1 = b"audio content 1" + content2 = b"audio content 2" + filename = "test.mp3" + + hash1 = get_audio_file_content_hash((filename, content1)) + hash2 = get_audio_file_content_hash((filename, content2)) + + assert hash1 != hash2, "Different content should produce different hashes" + + def test_same_content_same_hash(self): + """Test that same content produces same hash""" + content = b"same audio content" + filename1 = "test1.mp3" + filename2 = "test2.mp3" + + hash1 = get_audio_file_content_hash((filename1, content)) + hash2 = get_audio_file_content_hash((filename2, content)) + + assert hash1 == hash2, "Same content should produce same hash regardless of filename" + + def test_bytes_input(self): + """Test that raw bytes input works""" + content = b"raw bytes content" + hash1 = get_audio_file_content_hash(content) + hash2 = get_audio_file_content_hash(content) + + assert hash1 == hash2, "Same bytes should produce same hash" + assert len(hash1) == 64, "SHA-256 hash should be 64 characters" + + def test_fallback_to_filename(self): + """Test that function falls back to filename when content extraction fails""" + # Use a non-readable object that will trigger fallback + class UnreadableFile: + def __init__(self, name): + self.name = name + + file_obj = UnreadableFile("test.mp3") + hash_result = get_audio_file_content_hash(file_obj) + + assert isinstance(hash_result, str) + assert len(hash_result) == 64, "Should return valid hash even on fallback"