From e9328bfa3aa820c4af25e5bae4da9f5cee6557a3 Mon Sep 17 00:00:00 2001 From: Fabricio Ceschin Date: Mon, 19 May 2025 14:48:41 -0400 Subject: [PATCH 01/70] Adding langfuse usage details for cached tokens --- litellm/integrations/langfuse/langfuse.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/litellm/integrations/langfuse/langfuse.py b/litellm/integrations/langfuse/langfuse.py index d0472ee6383..ccc072149cc 100644 --- a/litellm/integrations/langfuse/langfuse.py +++ b/litellm/integrations/langfuse/langfuse.py @@ -638,6 +638,7 @@ class LangFuseLogger: generation_id = None usage = None + usage_details = None if response_obj is not None: if ( hasattr(response_obj, "id") @@ -654,6 +655,13 @@ class LangFuseLogger: "completion_tokens": _usage_obj.completion_tokens, "total_cost": cost if self._supports_costs() else None, } + usage_details = { + "input": _usage_obj.prompt_tokens, + "output": _usage_obj.completion_tokens, + "cache_creation_input_tokens": _usage_obj.get('cache_creation_input_tokens', 0), + "cache_read_input_tokens": _usage_obj.get('cache_read_input_tokens', 0) + } + generation_name = clean_metadata.pop("generation_name", None) if generation_name is None: # if `generation_name` is None, use sensible default values @@ -686,6 +694,7 @@ class LangFuseLogger: "input": input if not mask_input else "redacted-by-litellm", "output": output if not mask_output else "redacted-by-litellm", "usage": usage, + "usage_details": usage_details, "metadata": log_requester_metadata(clean_metadata), "level": level, "version": clean_metadata.pop("version", None), From d47ad69a1b2d006fbc15acea12bc47cc9d330bc1 Mon Sep 17 00:00:00 2001 From: Fabricio Ceschin Date: Wed, 21 May 2025 10:43:08 -0400 Subject: [PATCH 02/70] Added typed dict for LangfuseUsageDetails --- litellm/integrations/langfuse/langfuse.py | 2 +- litellm/types/integrations/langfuse.py | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/litellm/integrations/langfuse/langfuse.py b/litellm/integrations/langfuse/langfuse.py index f4d1a1f79c4..17e15e0b03c 100644 --- a/litellm/integrations/langfuse/langfuse.py +++ b/litellm/integrations/langfuse/langfuse.py @@ -659,7 +659,7 @@ class LangFuseLogger: "completion_tokens": _usage_obj.completion_tokens, "total_cost": cost if self._supports_costs() else None, } - usage_details = { + usage_details : Optional[LangfuseUsageDetails] = { "input": _usage_obj.prompt_tokens, "output": _usage_obj.completion_tokens, "cache_creation_input_tokens": _usage_obj.get('cache_creation_input_tokens', 0), diff --git a/litellm/types/integrations/langfuse.py b/litellm/types/integrations/langfuse.py index ecf42d8cd47..c19cd98d162 100644 --- a/litellm/types/integrations/langfuse.py +++ b/litellm/types/integrations/langfuse.py @@ -5,3 +5,10 @@ class LangfuseLoggingConfig(TypedDict): langfuse_secret: Optional[str] langfuse_public_key: Optional[str] langfuse_host: Optional[str] + + +class LangfuseUsageDetails(TypedDict): + input: Optional[int] + output: Optional[int] + cache_creation_input_tokens: Optional[int] + cache_read_input_tokens: Optional[int] From a30fd5c2ad59682d250493996e55327abd5e645b Mon Sep 17 00:00:00 2001 From: Fabricio Ceschin Date: Wed, 21 May 2025 10:51:59 -0400 Subject: [PATCH 03/70] Fixing lint errors with LangfuseUsageDetails --- litellm/integrations/langfuse/langfuse.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/litellm/integrations/langfuse/langfuse.py b/litellm/integrations/langfuse/langfuse.py index 17e15e0b03c..001e79cf802 100644 --- a/litellm/integrations/langfuse/langfuse.py +++ b/litellm/integrations/langfuse/langfuse.py @@ -642,7 +642,7 @@ class LangFuseLogger: generation_id = None usage = None - usage_details = None + usage_details : Optional[LangfuseUsageDetails] = None if response_obj is not None: if ( hasattr(response_obj, "id") From 90c54d688a4b290312c79a5c50cfbb227aa09670 Mon Sep 17 00:00:00 2001 From: Fabricio Ceschin Date: Wed, 21 May 2025 11:03:14 -0400 Subject: [PATCH 04/70] Fixing lint errors with LangfuseUsageDetails again --- litellm/integrations/langfuse/langfuse.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/litellm/integrations/langfuse/langfuse.py b/litellm/integrations/langfuse/langfuse.py index 001e79cf802..a98771387c0 100644 --- a/litellm/integrations/langfuse/langfuse.py +++ b/litellm/integrations/langfuse/langfuse.py @@ -659,7 +659,7 @@ class LangFuseLogger: "completion_tokens": _usage_obj.completion_tokens, "total_cost": cost if self._supports_costs() else None, } - usage_details : Optional[LangfuseUsageDetails] = { + usage_details = { "input": _usage_obj.prompt_tokens, "output": _usage_obj.completion_tokens, "cache_creation_input_tokens": _usage_obj.get('cache_creation_input_tokens', 0), From bd0369dd887e3fb9f630c6e25d6962f42635a0b5 Mon Sep 17 00:00:00 2001 From: Fabricio Ceschin Date: Wed, 21 May 2025 13:35:21 -0400 Subject: [PATCH 05/70] Using typed dict in usage_details --- litellm/integrations/langfuse/langfuse.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/litellm/integrations/langfuse/langfuse.py b/litellm/integrations/langfuse/langfuse.py index a98771387c0..f116eb8041b 100644 --- a/litellm/integrations/langfuse/langfuse.py +++ b/litellm/integrations/langfuse/langfuse.py @@ -642,7 +642,7 @@ class LangFuseLogger: generation_id = None usage = None - usage_details : Optional[LangfuseUsageDetails] = None + usage_details = None if response_obj is not None: if ( hasattr(response_obj, "id") @@ -659,12 +659,10 @@ class LangFuseLogger: "completion_tokens": _usage_obj.completion_tokens, "total_cost": cost if self._supports_costs() else None, } - usage_details = { - "input": _usage_obj.prompt_tokens, - "output": _usage_obj.completion_tokens, - "cache_creation_input_tokens": _usage_obj.get('cache_creation_input_tokens', 0), - "cache_read_input_tokens": _usage_obj.get('cache_read_input_tokens', 0) - } + usage_details = LangfuseUsageDetails(input=_usage_obj.prompt_tokens, + output=_usage_obj.completion_tokens, + cache_creation_input_tokens=_usage_obj.get('cache_creation_input_tokens', 0), + cache_read_input_tokens=_usage_obj.get('cache_read_input_tokens', 0)) generation_name = clean_metadata.pop("generation_name", None) if generation_name is None: From 9d73f1e607be57c65c6d8061f060c15c7e92a21d Mon Sep 17 00:00:00 2001 From: Fabricio Ceschin Date: Wed, 21 May 2025 21:23:49 -0400 Subject: [PATCH 06/70] Added unit tests for langfuse usage details integration --- tests/litellm/integrations/test_langfuse.py | 218 ++++++++++++++++++++ 1 file changed, 218 insertions(+) create mode 100644 tests/litellm/integrations/test_langfuse.py diff --git a/tests/litellm/integrations/test_langfuse.py b/tests/litellm/integrations/test_langfuse.py new file mode 100644 index 00000000000..26874d9ba21 --- /dev/null +++ b/tests/litellm/integrations/test_langfuse.py @@ -0,0 +1,218 @@ +import unittest +from unittest.mock import patch, MagicMock +import sys +import os +import datetime + +sys.path.insert(0, os.path.abspath("../..")) +from litellm.integrations.langfuse.langfuse import LangFuseLogger +# Import LangfuseUsageDetails directly from the module where it's defined +from litellm.types.integrations.langfuse import * + + + +class TestLangfuseUsageDetails(unittest.TestCase): + + def setUp(self): + # Set up environment variables for testing + self.env_patcher = patch.dict('os.environ', { + 'LANGFUSE_SECRET_KEY': 'test-secret-key', + 'LANGFUSE_PUBLIC_KEY': 'test-public-key', + 'LANGFUSE_HOST': 'https://test.langfuse.com' + }) + self.env_patcher.start() + + # Create mock objects + self.mock_langfuse_client = MagicMock() + self.mock_langfuse_trace = MagicMock() + self.mock_langfuse_generation = MagicMock() + + # Setup the trace and generation chain + self.mock_langfuse_trace.generation.return_value = self.mock_langfuse_generation + self.mock_langfuse_client.trace.return_value = self.mock_langfuse_trace + + # Mock the langfuse module that's imported locally in methods + self.langfuse_module_patcher = patch.dict('sys.modules', {'langfuse': MagicMock()}) + self.mock_langfuse_module = self.langfuse_module_patcher.start() + + # Create a mock for the langfuse module with version + self.mock_langfuse = MagicMock() + self.mock_langfuse.version = MagicMock() + self.mock_langfuse.version.__version__ = "3.0.0" # Set a version that supports all features + + # Mock the Langfuse class + self.mock_langfuse_class = MagicMock() + self.mock_langfuse_class.return_value = self.mock_langfuse_client + + # Set up the sys.modules['langfuse'] mock + sys.modules['langfuse'] = self.mock_langfuse + sys.modules['langfuse'].Langfuse = self.mock_langfuse_class + + # Mock the Langfuse client + self.mock_langfuse_client = MagicMock() + self.mock_langfuse_trace = MagicMock() + self.mock_langfuse_generation = MagicMock() + + # Setup the trace and generation chain + self.mock_langfuse_trace.generation.return_value = self.mock_langfuse_generation + self.mock_langfuse_client.trace.return_value = self.mock_langfuse_trace + + # Mock the Langfuse class + self.mock_langfuse_class = MagicMock() + self.mock_langfuse_class.return_value = self.mock_langfuse_client + self.mock_langfuse.Langfuse = self.mock_langfuse_class + + # Create the logger + self.logger = LangFuseLogger() + + # Add the log_event_on_langfuse method to the instance + def log_event_on_langfuse(self, kwargs, response_obj, start_time=None, end_time=None, user_id=None, level="DEFAULT", status_message=None): + # This implementation calls _log_langfuse_v2 directly + return self._log_langfuse_v2( + user_id=user_id, + metadata=kwargs.get("litellm_params", {}).get("metadata", {}), + litellm_params=kwargs.get("litellm_params", {}), + output=None, + start_time=start_time, + end_time=end_time, + kwargs=kwargs, + optional_params=kwargs.get("optional_params", {}), + input=None, + response_obj=response_obj, + level=level, + litellm_call_id=kwargs.get("litellm_call_id", None), + print_verbose=True # Add the missing parameter + ) + + # Bind the method to the instance + import types + self.logger.log_event_on_langfuse = types.MethodType(log_event_on_langfuse, self.logger) + + # Make sure _is_langfuse_v2 returns True + def mock_is_langfuse_v2(self): + return True + + self.logger._is_langfuse_v2 = types.MethodType(mock_is_langfuse_v2, self.logger) + + def tearDown(self): + self.env_patcher.stop() + self.langfuse_module_patcher.stop() + + def test_langfuse_usage_details_type(self): + """Test that LangfuseUsageDetails TypedDict is properly defined with the correct fields""" + # Create an instance of LangfuseUsageDetails + usage_details: LangfuseUsageDetails = { + "input": 10, + "output": 20, + "cache_creation_input_tokens": 5, + "cache_read_input_tokens": 3 + } + + # Verify all fields are present + self.assertEqual(usage_details["input"], 10) + self.assertEqual(usage_details["output"], 20) + self.assertEqual(usage_details["cache_creation_input_tokens"], 5) + self.assertEqual(usage_details["cache_read_input_tokens"], 3) + + # Test with all fields (all fields are required in TypedDict by default) + minimal_usage_details: LangfuseUsageDetails = { + "input": 10, + "output": 20, + "cache_creation_input_tokens": 0, + "cache_read_input_tokens": 0 + } + + self.assertEqual(minimal_usage_details["input"], 10) + self.assertEqual(minimal_usage_details["output"], 20) + + def test_log_langfuse_v2_usage_details(self): + """Test that usage_details in _log_langfuse_v2 is correctly typed and assigned""" + # Create a mock response object with usage information + response_obj = MagicMock() + response_obj.usage = MagicMock() + response_obj.usage.prompt_tokens = 15 + response_obj.usage.completion_tokens = 25 + + # Add the cache token attributes using get method + def mock_get(key, default=None): + if key == 'cache_creation_input_tokens': + return 7 + elif key == 'cache_read_input_tokens': + return 4 + return default + + response_obj.usage.get = mock_get + + # Create kwargs for the log_event method + kwargs = { + "model": "gpt-4", + "messages": [{"role": "user", "content": "Hello"}], + "litellm_params": {"metadata": {}} + } + + # Create start and end times + start_time = datetime.datetime.now() + end_time = start_time + datetime.timedelta(seconds=1) + + # Call the log_event method + with patch.object(self.logger, '_log_langfuse_v2') as mock_log_langfuse_v2: + self.logger.log_event_on_langfuse( + kwargs=kwargs, + response_obj=response_obj, + start_time=start_time, + end_time=end_time + ) + + # Check if _log_langfuse_v2 was called + mock_log_langfuse_v2.assert_called_once() + + # Get the arguments passed to _log_langfuse_v2 + call_args = mock_log_langfuse_v2.call_args[1] + + # Verify response_obj was passed correctly + self.assertEqual(call_args["response_obj"], response_obj) + + def test_langfuse_usage_details_optional_fields(self): + """Test that LangfuseUsageDetails fields are properly defined as Optional""" + # Create an instance with None values for optional fields + usage_details: LangfuseUsageDetails = { + "input": 10, + "output": 20, + "cache_creation_input_tokens": None, + "cache_read_input_tokens": None + } + + # Verify fields can be None + self.assertEqual(usage_details["input"], 10) + self.assertEqual(usage_details["output"], 20) + self.assertIsNone(usage_details["cache_creation_input_tokens"]) + self.assertIsNone(usage_details["cache_read_input_tokens"]) + + def test_langfuse_usage_details_structure(self): + """Test that LangfuseUsageDetails has the correct structure as defined in the commit""" + # This test directly verifies the structure of the TypedDict + # without relying on the LangFuseLogger class + + # Create a dictionary that matches the LangfuseUsageDetails structure + usage_details = { + "input": 15, + "output": 25, + "cache_creation_input_tokens": 7, + "cache_read_input_tokens": 4 + } + + # Verify the structure matches what we expect + self.assertIn("input", usage_details) + self.assertIn("output", usage_details) + self.assertIn("cache_creation_input_tokens", usage_details) + self.assertIn("cache_read_input_tokens", usage_details) + + # Verify the values + self.assertEqual(usage_details["input"], 15) + self.assertEqual(usage_details["output"], 25) + self.assertEqual(usage_details["cache_creation_input_tokens"], 7) + self.assertEqual(usage_details["cache_read_input_tokens"], 4) + + +if __name__ == "__main__": + unittest.main() From 118d15c1c61e26daf0362bc08e13e28a4eafc4f6 Mon Sep 17 00:00:00 2001 From: Fabricio Ceschin Date: Wed, 21 May 2025 21:37:23 -0400 Subject: [PATCH 07/70] Improving langfuse usage details unit test --- tests/litellm/integrations/test_langfuse.py | 76 ++++++++++----------- 1 file changed, 35 insertions(+), 41 deletions(-) diff --git a/tests/litellm/integrations/test_langfuse.py b/tests/litellm/integrations/test_langfuse.py index 26874d9ba21..2aa363e5f94 100644 --- a/tests/litellm/integrations/test_langfuse.py +++ b/tests/litellm/integrations/test_langfuse.py @@ -9,10 +9,8 @@ from litellm.integrations.langfuse.langfuse import LangFuseLogger # Import LangfuseUsageDetails directly from the module where it's defined from litellm.types.integrations.langfuse import * - - class TestLangfuseUsageDetails(unittest.TestCase): - + def setUp(self): # Set up environment variables for testing self.env_patcher = patch.dict('os.environ', { @@ -21,50 +19,50 @@ class TestLangfuseUsageDetails(unittest.TestCase): 'LANGFUSE_HOST': 'https://test.langfuse.com' }) self.env_patcher.start() - + # Create mock objects self.mock_langfuse_client = MagicMock() self.mock_langfuse_trace = MagicMock() self.mock_langfuse_generation = MagicMock() - + # Setup the trace and generation chain self.mock_langfuse_trace.generation.return_value = self.mock_langfuse_generation self.mock_langfuse_client.trace.return_value = self.mock_langfuse_trace - + # Mock the langfuse module that's imported locally in methods self.langfuse_module_patcher = patch.dict('sys.modules', {'langfuse': MagicMock()}) self.mock_langfuse_module = self.langfuse_module_patcher.start() - + # Create a mock for the langfuse module with version self.mock_langfuse = MagicMock() self.mock_langfuse.version = MagicMock() self.mock_langfuse.version.__version__ = "3.0.0" # Set a version that supports all features - + # Mock the Langfuse class self.mock_langfuse_class = MagicMock() self.mock_langfuse_class.return_value = self.mock_langfuse_client - + # Set up the sys.modules['langfuse'] mock sys.modules['langfuse'] = self.mock_langfuse sys.modules['langfuse'].Langfuse = self.mock_langfuse_class - + # Mock the Langfuse client self.mock_langfuse_client = MagicMock() self.mock_langfuse_trace = MagicMock() self.mock_langfuse_generation = MagicMock() - + # Setup the trace and generation chain self.mock_langfuse_trace.generation.return_value = self.mock_langfuse_generation self.mock_langfuse_client.trace.return_value = self.mock_langfuse_trace - + # Mock the Langfuse class self.mock_langfuse_class = MagicMock() self.mock_langfuse_class.return_value = self.mock_langfuse_client self.mock_langfuse.Langfuse = self.mock_langfuse_class - + # Create the logger self.logger = LangFuseLogger() - + # Add the log_event_on_langfuse method to the instance def log_event_on_langfuse(self, kwargs, response_obj, start_time=None, end_time=None, user_id=None, level="DEFAULT", status_message=None): # This implementation calls _log_langfuse_v2 directly @@ -83,21 +81,21 @@ class TestLangfuseUsageDetails(unittest.TestCase): litellm_call_id=kwargs.get("litellm_call_id", None), print_verbose=True # Add the missing parameter ) - + # Bind the method to the instance import types self.logger.log_event_on_langfuse = types.MethodType(log_event_on_langfuse, self.logger) - + # Make sure _is_langfuse_v2 returns True def mock_is_langfuse_v2(self): return True - + self.logger._is_langfuse_v2 = types.MethodType(mock_is_langfuse_v2, self.logger) - + def tearDown(self): self.env_patcher.stop() self.langfuse_module_patcher.stop() - + def test_langfuse_usage_details_type(self): """Test that LangfuseUsageDetails TypedDict is properly defined with the correct fields""" # Create an instance of LangfuseUsageDetails @@ -107,13 +105,13 @@ class TestLangfuseUsageDetails(unittest.TestCase): "cache_creation_input_tokens": 5, "cache_read_input_tokens": 3 } - + # Verify all fields are present self.assertEqual(usage_details["input"], 10) self.assertEqual(usage_details["output"], 20) self.assertEqual(usage_details["cache_creation_input_tokens"], 5) self.assertEqual(usage_details["cache_read_input_tokens"], 3) - + # Test with all fields (all fields are required in TypedDict by default) minimal_usage_details: LangfuseUsageDetails = { "input": 10, @@ -121,10 +119,10 @@ class TestLangfuseUsageDetails(unittest.TestCase): "cache_creation_input_tokens": 0, "cache_read_input_tokens": 0 } - + self.assertEqual(minimal_usage_details["input"], 10) self.assertEqual(minimal_usage_details["output"], 20) - + def test_log_langfuse_v2_usage_details(self): """Test that usage_details in _log_langfuse_v2 is correctly typed and assigned""" # Create a mock response object with usage information @@ -132,7 +130,7 @@ class TestLangfuseUsageDetails(unittest.TestCase): response_obj.usage = MagicMock() response_obj.usage.prompt_tokens = 15 response_obj.usage.completion_tokens = 25 - + # Add the cache token attributes using get method def mock_get(key, default=None): if key == 'cache_creation_input_tokens': @@ -140,20 +138,20 @@ class TestLangfuseUsageDetails(unittest.TestCase): elif key == 'cache_read_input_tokens': return 4 return default - + response_obj.usage.get = mock_get - + # Create kwargs for the log_event method kwargs = { "model": "gpt-4", "messages": [{"role": "user", "content": "Hello"}], "litellm_params": {"metadata": {}} } - + # Create start and end times start_time = datetime.datetime.now() end_time = start_time + datetime.timedelta(seconds=1) - + # Call the log_event method with patch.object(self.logger, '_log_langfuse_v2') as mock_log_langfuse_v2: self.logger.log_event_on_langfuse( @@ -162,16 +160,16 @@ class TestLangfuseUsageDetails(unittest.TestCase): start_time=start_time, end_time=end_time ) - + # Check if _log_langfuse_v2 was called mock_log_langfuse_v2.assert_called_once() - + # Get the arguments passed to _log_langfuse_v2 call_args = mock_log_langfuse_v2.call_args[1] - + # Verify response_obj was passed correctly self.assertEqual(call_args["response_obj"], response_obj) - + def test_langfuse_usage_details_optional_fields(self): """Test that LangfuseUsageDetails fields are properly defined as Optional""" # Create an instance with None values for optional fields @@ -181,18 +179,18 @@ class TestLangfuseUsageDetails(unittest.TestCase): "cache_creation_input_tokens": None, "cache_read_input_tokens": None } - + # Verify fields can be None self.assertEqual(usage_details["input"], 10) self.assertEqual(usage_details["output"], 20) self.assertIsNone(usage_details["cache_creation_input_tokens"]) self.assertIsNone(usage_details["cache_read_input_tokens"]) - + def test_langfuse_usage_details_structure(self): """Test that LangfuseUsageDetails has the correct structure as defined in the commit""" # This test directly verifies the structure of the TypedDict # without relying on the LangFuseLogger class - + # Create a dictionary that matches the LangfuseUsageDetails structure usage_details = { "input": 15, @@ -200,19 +198,15 @@ class TestLangfuseUsageDetails(unittest.TestCase): "cache_creation_input_tokens": 7, "cache_read_input_tokens": 4 } - + # Verify the structure matches what we expect self.assertIn("input", usage_details) self.assertIn("output", usage_details) self.assertIn("cache_creation_input_tokens", usage_details) self.assertIn("cache_read_input_tokens", usage_details) - + # Verify the values self.assertEqual(usage_details["input"], 15) self.assertEqual(usage_details["output"], 25) self.assertEqual(usage_details["cache_creation_input_tokens"], 7) self.assertEqual(usage_details["cache_read_input_tokens"], 4) - - -if __name__ == "__main__": - unittest.main() From 6d7d5ed07805fe40ec3ebdba2eca6fd769de7cdc Mon Sep 17 00:00:00 2001 From: Fabricio Ceschin Date: Mon, 8 Sep 2025 11:37:24 -0400 Subject: [PATCH 08/70] Updating poetry.lock --- poetry.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/poetry.lock b/poetry.lock index 771bb6d486e..41182d12bff 100644 --- a/poetry.lock +++ b/poetry.lock @@ -9182,4 +9182,4 @@ utils = ["numpydoc"] [metadata] lock-version = "2.1" python-versions = ">=3.8.1,<4.0, !=3.9.7" -content-hash = "8ebdb444c0ff253857f90d3ec9926a58d23d4482c644fd158a047f0c0f892bb9" +content-hash = "54374b906931afe92861b6b9b226751579c0d342f650bc8f7ebaf4c5882d6249" From ab941c588ae28567912cb7a33c4dfc70df01bca9 Mon Sep 17 00:00:00 2001 From: Franklin <5235904+mrFranklin@users.noreply.github.com> Date: Fri, 19 Sep 2025 17:27:51 +0800 Subject: [PATCH 09/70] doc: navigate to the correct location --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 46acf6cef0f..f74889fbb27 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,7 @@ LiteLLM manages: - Retry/fallback logic across multiple deployments (e.g. Azure/OpenAI) - [Router](https://docs.litellm.ai/docs/routing) - Set Budgets & Rate limits per project, api key, model [LiteLLM Proxy Server (LLM Gateway)](https://docs.litellm.ai/docs/simple_proxy) -[**Jump to LiteLLM Proxy (LLM Gateway) Docs**](https://github.com/BerriAI/litellm?tab=readme-ov-file#openai-proxy---docs)
+[**Jump to LiteLLM Proxy (LLM Gateway) Docs**](https://github.com/BerriAI/litellm?tab=readme-ov-file#litellm-proxy-server-llm-gateway---docs)
[**Jump to Supported LLM Providers**](https://github.com/BerriAI/litellm?tab=readme-ov-file#supported-providers-docs) 🚨 **Stable Release:** Use docker images with the `-stable` tag. These have undergone 12 hour load tests, before being published. [More information about the release cycle here](https://docs.litellm.ai/docs/proxy/release_cycle) From 410e4956c4b6a65c517c48c58187435c500638ae Mon Sep 17 00:00:00 2001 From: "oytun.kutrup" Date: Wed, 24 Sep 2025 15:32:23 +0300 Subject: [PATCH 10/70] GPT-3.5-Turbo price updated. --- model_prices_and_context_window.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/model_prices_and_context_window.json b/model_prices_and_context_window.json index 832ff36bfb8..125088becbf 100644 --- a/model_prices_and_context_window.json +++ b/model_prices_and_context_window.json @@ -11261,13 +11261,13 @@ ] }, "gpt-3.5-turbo": { - "input_cost_per_token": 1.5e-06, + "input_cost_per_token": 0.5e-06, "litellm_provider": "openai", "max_input_tokens": 16385, "max_output_tokens": 4096, "max_tokens": 4097, "mode": "chat", - "output_cost_per_token": 2e-06, + "output_cost_per_token": 1.5e-06, "supports_function_calling": true, "supports_prompt_caching": true, "supports_system_messages": true, From c9b4a6a54993beafdf100fc17b39bb7d9be7ce22 Mon Sep 17 00:00:00 2001 From: mrFranklin Date: Wed, 24 Sep 2025 21:15:28 +0800 Subject: [PATCH 11/70] chore: update OpikLogger class for improved configuration handling --- litellm/integrations/opik/opik.py | 46 ++++++++++++++++++++++++++----- 1 file changed, 39 insertions(+), 7 deletions(-) diff --git a/litellm/integrations/opik/opik.py b/litellm/integrations/opik/opik.py index 9f90d2384d8..94fbcfe2016 100644 --- a/litellm/integrations/opik/opik.py +++ b/litellm/integrations/opik/opik.py @@ -192,9 +192,22 @@ class OpikLogger(CustomBatchLogger): # Extract opik metadata litellm_opik_metadata = litellm_params_metadata.get("opik", {}) + + # Use standard_logging_object to create metadata and input/output data + standard_logging_object = kwargs.get("standard_logging_object", None) + if standard_logging_object is None: + verbose_logger.debug( + "OpikLogger skipping event; no standard_logging_object found" + ) + return [] + + # Update litellm_opik_metadata with opik metadata from requester + litellm_opik_metadata.update(standard_logging_object.get("metadata", {}).get("requester_metadata", {}).get("opik", {})) + verbose_logger.debug( f"litellm_opik_metadata - {json.dumps(litellm_opik_metadata, default=str)}" ) + project_name = litellm_opik_metadata.get("project_name", self.opik_project_name) # Extract trace_id and parent_span_id @@ -208,19 +221,36 @@ class OpikLogger(CustomBatchLogger): else: trace_id = None parent_span_id = None + # Create Opik tags opik_tags = litellm_opik_metadata.get("tags", []) if kwargs.get("custom_llm_provider"): opik_tags.append(kwargs["custom_llm_provider"]) + # Create thread id + thread_id = litellm_opik_metadata.get("thread_id", None) - # Use standard_logging_object to create metadata and input/output data - standard_logging_object = kwargs.get("standard_logging_object", None) - if standard_logging_object is None: - verbose_logger.debug( - "OpikLogger skipping event; no standard_logging_object found" - ) - return [] + # Override with any opik_ headers from proxy request + proxy_headers = (_litellm_params.get("proxy_server_request", {}).get("headers", {}) or {}) + for key, value in proxy_headers.items(): + if key.startswith("opik_"): + param_key = key.replace("opik_", "", 1) + if param_key == "project_name" and value: + project_name = value + elif param_key == "thread_id" and value: + thread_id = value + elif param_key == "trace_id" and value: + trace_id = value + elif param_key == "parent_span_id" and value: + parent_span_id = value + elif param_key == "tags" and value: + try: + parsed_tags = json.loads(value) + if isinstance(parsed_tags, list): + opik_tags.extend(parsed_tags) + except (json.JSONDecodeError, TypeError): + pass + # Create input and output data input_data = standard_logging_object.get("messages", {}) output_data = standard_logging_object.get("response", {}) @@ -298,6 +328,7 @@ class OpikLogger(CustomBatchLogger): "output": output_data, "metadata": metadata, "tags": opik_tags, + "thread_id": thread_id, } ) @@ -319,6 +350,7 @@ class OpikLogger(CustomBatchLogger): "output": output_data, "metadata": metadata, "tags": opik_tags, + "thread_id": thread_id, "usage": usage, } ) From e437273ad52e2f1bd93a6d22574f9833e38839b0 Mon Sep 17 00:00:00 2001 From: Franklin <5235904+mrFranklin@users.noreply.github.com> Date: Thu, 25 Sep 2025 00:57:21 +0800 Subject: [PATCH 12/70] doc: make the README document clearer (#14860) * Update README.md * Update README.md --- README.md | 33 +++++++++++++-------------------- 1 file changed, 13 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index f74889fbb27..0918d2b1fa4 100644 --- a/README.md +++ b/README.md @@ -350,13 +350,21 @@ curl 'http://0.0.0.0:4000/key/generate' \ [**Read the Docs**](https://docs.litellm.ai/docs/) -## Contributing +## Run in Developer mode +### Services +1. Setup .env file in root +2. Run dependant services `docker-compose up db prometheus` -Interested in contributing? Contributions to LiteLLM Python SDK, Proxy Server, and LLM integrations are both accepted and highly encouraged! +### Backend +1. (In root) create virtual environment `python -m venv .venv` +2. Activate virtual environment `source .venv/bin/activate` +3. Install dependencies `pip install -e ".[all]"` +4. Start proxy backend `python litellm/proxy_cli.py` -**Quick start:** `git clone` → `make install-dev` → `make format` → `make lint` → `make test-unit` - -See our comprehensive [Contributing Guide (CONTRIBUTING.md)](CONTRIBUTING.md) for detailed instructions. +### Frontend +1. Navigate to `ui/litellm-dashboard` +2. Install dependencies `npm install` +3. Run `npm run dev` to start the dashboard # Enterprise For companies that need better security, user management and professional support @@ -434,18 +442,3 @@ All these checks must pass before your PR can be merged. -## Run in Developer mode -### Services -1. Setup .env file in root -2. Run dependant services `docker-compose up db prometheus` - -### Backend -1. (In root) create virtual environment `python -m venv .venv` -2. Activate virtual environment `source .venv/bin/activate` -3. Install dependencies `pip install -e ".[all]"` -4. Start proxy backend `python3 /path/to/litellm/proxy_cli.py` - -### Frontend -1. Navigate to `ui/litellm-dashboard` -2. Install dependencies `npm install` -3. Run `npm run dev` to start the dashboard From 22eef373eba848b02969f25d5c49b555ade8b57e Mon Sep 17 00:00:00 2001 From: onlylonly Date: Thu, 25 Sep 2025 00:58:23 +0800 Subject: [PATCH 13/70] feat: New model - Add support for Qwen models family & Deepseek 3.1 to Amazon Bedrock (#14845) * New model - Add Bedrock deepseek v3.1 model - "deepseek.v3-v1:0" * New model - Add Bedrock Qwen models - "qwen.qwen3-coder-480b-a35b-v1:0", "qwen.qwen3-235b-a22b-2507-v1:0", "qwen.qwen3-coder-30b-a3b-v1:0", "qwen.qwen3-32b-v1:0" * fix: add "deepseek.v3-v1:0" in litellm/model_prices_and_context_window_backup.json --- litellm/constants.py | 5 ++ ...odel_prices_and_context_window_backup.json | 60 +++++++++++++++++++ model_prices_and_context_window.json | 60 +++++++++++++++++++ 3 files changed, 125 insertions(+) diff --git a/litellm/constants.py b/litellm/constants.py index 005eb2bb6d0..6e70ae0671f 100644 --- a/litellm/constants.py +++ b/litellm/constants.py @@ -812,6 +812,11 @@ BEDROCK_EMBEDDING_PROVIDERS_LITERAL = Literal[ ] BEDROCK_CONVERSE_MODELS = [ + "qwen.qwen3-coder-480b-a35b-v1:0", + "qwen.qwen3-235b-a22b-2507-v1:0", + "qwen.qwen3-coder-30b-a3b-v1:0", + "qwen.qwen3-32b-v1:0", + "deepseek.v3-v1:0", "openai.gpt-oss-20b-1:0", "openai.gpt-oss-120b-1:0", "anthropic.claude-opus-4-1-20250805-v1:0", diff --git a/litellm/model_prices_and_context_window_backup.json b/litellm/model_prices_and_context_window_backup.json index 832ff36bfb8..b0261d1bd43 100644 --- a/litellm/model_prices_and_context_window_backup.json +++ b/litellm/model_prices_and_context_window_backup.json @@ -7203,6 +7203,18 @@ "supports_prompt_caching": true, "supports_tool_choice": true }, + "deepseek.v3-v1:0": { + "input_cost_per_token": 5.8e-07, + "litellm_provider": "bedrock_converse", + "max_input_tokens": 163840, + "max_output_tokens": 81920, + "max_tokens": 163840, + "mode": "chat", + "output_cost_per_token": 1.68e-06, + "supports_function_calling": true, + "supports_reasoning": true, + "supports_tool_choice": true + }, "dolphin": { "input_cost_per_token": 5e-07, "litellm_provider": "nlp_cloud", @@ -17520,6 +17532,54 @@ "mode": "chat", "output_cost_per_token": 2.8e-07 }, + "qwen.qwen3-coder-480b-a35b-v1:0": { + "input_cost_per_token": 2.2e-07, + "litellm_provider": "bedrock_converse", + "max_input_tokens": 262000, + "max_output_tokens": 65536, + "max_tokens": 262144, + "mode": "chat", + "output_cost_per_token": 1.8e-06, + "supports_function_calling": true, + "supports_reasoning": true, + "supports_tool_choice": true + }, + "qwen.qwen3-235b-a22b-2507-v1:0": { + "input_cost_per_token": 2.2e-07, + "litellm_provider": "bedrock_converse", + "max_input_tokens": 262144, + "max_output_tokens": 131072, + "max_tokens": 262144, + "mode": "chat", + "output_cost_per_token": 8.8e-07, + "supports_function_calling": true, + "supports_reasoning": true, + "supports_tool_choice": true + }, + "qwen.qwen3-coder-30b-a3b-v1:0": { + "input_cost_per_token": 1.5e-07, + "litellm_provider": "bedrock_converse", + "max_input_tokens": 262144, + "max_output_tokens": 131072, + "max_tokens": 262144, + "mode": "chat", + "output_cost_per_token": 6.0e-07, + "supports_function_calling": true, + "supports_reasoning": true, + "supports_tool_choice": true + }, + "qwen.qwen3-32b-v1:0": { + "input_cost_per_token": 1.5e-07, + "litellm_provider": "bedrock_converse", + "max_input_tokens": 131072, + "max_output_tokens": 16384, + "max_tokens": 131072, + "mode": "chat", + "output_cost_per_token": 6.0e-07, + "supports_function_calling": true, + "supports_reasoning": true, + "supports_tool_choice": true + }, "recraft/recraftv2": { "litellm_provider": "recraft", "mode": "image_generation", diff --git a/model_prices_and_context_window.json b/model_prices_and_context_window.json index 832ff36bfb8..37be4e283aa 100644 --- a/model_prices_and_context_window.json +++ b/model_prices_and_context_window.json @@ -7203,6 +7203,18 @@ "supports_prompt_caching": true, "supports_tool_choice": true }, + "deepseek.v3-v1:0": { + "input_cost_per_token": 5.8e-07, + "litellm_provider": "bedrock_converse", + "max_input_tokens": 163840, + "max_output_tokens": 81920, + "max_tokens": 163840, + "mode": "chat", + "output_cost_per_token": 1.68e-06, + "supports_function_calling": true, + "supports_reasoning": true, + "supports_tool_choice": true + }, "dolphin": { "input_cost_per_token": 5e-07, "litellm_provider": "nlp_cloud", @@ -17520,6 +17532,54 @@ "mode": "chat", "output_cost_per_token": 2.8e-07 }, + "qwen.qwen3-coder-480b-a35b-v1:0": { + "input_cost_per_token": 2.2e-07, + "litellm_provider": "bedrock_converse", + "max_input_tokens": 262000, + "max_output_tokens": 65536, + "max_tokens": 262144, + "mode": "chat", + "output_cost_per_token": 1.8e-06, + "supports_function_calling": true, + "supports_reasoning": true, + "supports_tool_choice": true + }, + "qwen.qwen3-235b-a22b-2507-v1:0": { + "input_cost_per_token": 2.2e-07, + "litellm_provider": "bedrock_converse", + "max_input_tokens": 262144, + "max_output_tokens": 131072, + "max_tokens": 262144, + "mode": "chat", + "output_cost_per_token": 8.8e-07, + "supports_function_calling": true, + "supports_reasoning": true, + "supports_tool_choice": true + }, + "qwen.qwen3-coder-30b-a3b-v1:0": { + "input_cost_per_token": 1.5e-07, + "litellm_provider": "bedrock_converse", + "max_input_tokens": 262144, + "max_output_tokens": 131072, + "max_tokens": 262144, + "mode": "chat", + "output_cost_per_token": 6.0e-07, + "supports_function_calling": true, + "supports_reasoning": true, + "supports_tool_choice": true + }, + "qwen.qwen3-32b-v1:0": { + "input_cost_per_token": 1.5e-07, + "litellm_provider": "bedrock_converse", + "max_input_tokens": 131072, + "max_output_tokens": 16384, + "max_tokens": 131072, + "mode": "chat", + "output_cost_per_token": 6.0e-07, + "supports_function_calling": true, + "supports_reasoning": true, + "supports_tool_choice": true + }, "recraft/recraftv2": { "litellm_provider": "recraft", "mode": "image_generation", From 0cd91a82d28aebd63a5d62f68204c7877347fdf9 Mon Sep 17 00:00:00 2001 From: Mubashir Osmani Date: Wed, 24 Sep 2025 13:40:00 -0400 Subject: [PATCH 14/70] Added vertex_ai/qwen models and azure/gpt-5-codex (#14844) * added qwen models and gpt-5-codex * fix flaky test * fix failing test --- model_prices_and_context_window.json | 145 ++++++++++++++++++ .../test_bedrock_completion.py | 15 +- .../test_anthropic_passthrough.py | 5 +- 3 files changed, 159 insertions(+), 6 deletions(-) diff --git a/model_prices_and_context_window.json b/model_prices_and_context_window.json index 37be4e283aa..de1f0c5724e 100644 --- a/model_prices_and_context_window.json +++ b/model_prices_and_context_window.json @@ -2032,6 +2032,36 @@ "supports_tool_choice": false, "supports_vision": true }, + "azure/gpt-5-codex": { + "cache_read_input_token_cost": 1.25e-07, + "input_cost_per_token": 1.25e-06, + "litellm_provider": "azure", + "max_input_tokens": 272000, + "max_output_tokens": 128000, + "max_tokens": 128000, + "mode": "responses", + "output_cost_per_token": 1e-05, + "supported_endpoints": [ + "/v1/responses" + ], + "supported_modalities": [ + "text", + "image" + ], + "supported_output_modalities": [ + "text" + ], + "supports_function_calling": true, + "supports_native_streaming": true, + "supports_parallel_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_vision": true + }, "azure/gpt-5-mini": { "cache_read_input_token_cost": 2.5e-08, "input_cost_per_token": 2.5e-07, @@ -5282,6 +5312,49 @@ "supports_tool_choice": true, "supports_vision": true }, + "deepseek-chat": { + "cache_read_input_token_cost": 6e-08, + "input_cost_per_token": 6e-07, + "litellm_provider": "deepseek", + "max_input_tokens": 131072, + "max_output_tokens": 8192, + "max_tokens": 131072, + "mode": "chat", + "output_cost_per_token": 1.7e-06, + "source": "https://api-docs.deepseek.com/quick_start/pricing", + "supported_endpoints": [ + "/v1/chat/completions" + ], + "supports_function_calling": true, + "supports_native_streaming": true, + "supports_parallel_function_calling": true, + "supports_prompt_caching": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true + }, + "deepseek-reasoner": { + "cache_read_input_token_cost": 6e-08, + "input_cost_per_token": 6e-07, + "litellm_provider": "deepseek", + "max_input_tokens": 131072, + "max_output_tokens": 65536, + "max_tokens": 131072, + "mode": "chat", + "output_cost_per_token": 1.7e-06, + "source": "https://api-docs.deepseek.com/quick_start/pricing", + "supported_endpoints": [ + "/v1/chat/completions" + ], + "supports_function_calling": false, + "supports_native_streaming": true, + "supports_parallel_function_calling": false, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": false + }, "dashscope/qwen-coder": { "input_cost_per_token": 3e-07, "litellm_provider": "dashscope", @@ -12427,6 +12500,36 @@ "supports_tool_choice": false, "supports_vision": true }, + "gpt-5-codex": { + "cache_read_input_token_cost": 1.25e-07, + "input_cost_per_token": 1.25e-06, + "litellm_provider": "openai", + "max_input_tokens": 272000, + "max_output_tokens": 128000, + "max_tokens": 128000, + "mode": "responses", + "output_cost_per_token": 1e-05, + "supported_endpoints": [ + "/v1/responses" + ], + "supported_modalities": [ + "text", + "image" + ], + "supported_output_modalities": [ + "text" + ], + "supports_function_calling": true, + "supports_native_streaming": false, + "supports_parallel_function_calling": true, + "supports_pdf_input": false, + "supports_prompt_caching": true, + "supports_reasoning": false, + "supports_response_schema": true, + "supports_system_messages": false, + "supports_tool_choice": true, + "supports_vision": true + }, "gpt-5-mini": { "cache_read_input_token_cost": 2.5e-08, "cache_read_input_token_cost_flex": 1.25e-08, @@ -20527,6 +20630,24 @@ "supports_function_calling": true, "supports_tool_choice": true }, + "vertex_ai/deepseek-ai/deepseek-v3.1-maas": { + "input_cost_per_token": 1.35e-06, + "litellm_provider": "vertex_ai-deepseek_models", + "max_input_tokens": 163840, + "max_output_tokens": 32768, + "max_tokens": 163840, + "mode": "chat", + "output_cost_per_token": 5.4e-06, + "source": "https://cloud.google.com/vertex-ai/generative-ai/pricing#partner-models", + "supported_regions": [ + "us-west2" + ], + "supports_assistant_prefill": true, + "supports_function_calling": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_tool_choice": true + }, "vertex_ai/deepseek-ai/deepseek-r1-0528-maas": { "input_cost_per_token": 1.35e-06, "litellm_provider": "vertex_ai-deepseek_models", @@ -20940,6 +21061,30 @@ "supports_function_calling": true, "supports_tool_choice": true }, + "vertex_ai/qwen/qwen3-next-80b-a3b-instruct-maas": { + "input_cost_per_token": 1.5e-07, + "litellm_provider": "vertex_ai-qwen_models", + "max_input_tokens": 262144, + "max_output_tokens": 262144, + "max_tokens": 262144, + "mode": "chat", + "output_cost_per_token": 1.2e-06, + "source": "https://cloud.google.com/vertex-ai/generative-ai/pricing", + "supports_function_calling": true, + "supports_tool_choice": true + }, + "vertex_ai/qwen/qwen3-next-80b-a3b-thinking-maas": { + "input_cost_per_token": 1.5e-07, + "litellm_provider": "vertex_ai-qwen_models", + "max_input_tokens": 262144, + "max_output_tokens": 262144, + "max_tokens": 262144, + "mode": "chat", + "output_cost_per_token": 1.2e-06, + "source": "https://cloud.google.com/vertex-ai/generative-ai/pricing", + "supports_function_calling": true, + "supports_tool_choice": true + }, "vertex_ai/veo-2.0-generate-001": { "litellm_provider": "vertex_ai-video-models", "max_input_tokens": 1024, diff --git a/tests/llm_translation/test_bedrock_completion.py b/tests/llm_translation/test_bedrock_completion.py index 91dff9f8636..d41448727d5 100644 --- a/tests/llm_translation/test_bedrock_completion.py +++ b/tests/llm_translation/test_bedrock_completion.py @@ -27,6 +27,7 @@ import litellm from litellm import ( ModelResponse, RateLimitError, + ServiceUnavailableError, Timeout, completion, completion_cost, @@ -2020,10 +2021,16 @@ def test_bedrock_context_window_error(): def test_bedrock_converse_route(): litellm.set_verbose = True - litellm.completion( - model="bedrock/converse/us.amazon.nova-pro-v1:0", - messages=[{"role": "user", "content": "Hello, world!"}], - ) + try: + litellm.completion( + model="bedrock/converse/us.amazon.nova-pro-v1:0", + messages=[{"role": "user", "content": "Hello, world!"}], + ) + except ServiceUnavailableError as e: + if "Too many requests" in str(e): + pytest.skip("Skipping test due to AWS Bedrock rate limiting") + else: + raise def test_bedrock_mapped_converse_models(): diff --git a/tests/pass_through_tests/test_anthropic_passthrough.py b/tests/pass_through_tests/test_anthropic_passthrough.py index e0549d17fa2..002fb20e9e8 100644 --- a/tests/pass_through_tests/test_anthropic_passthrough.py +++ b/tests/pass_through_tests/test_anthropic_passthrough.py @@ -43,8 +43,9 @@ async def test_anthropic_basic_completion_with_headers(): json.dumps(response_json, indent=4, default=str), ) reported_usage = response_json.get("usage", None) - anthropic_api_input_tokens = reported_usage.get("input_tokens", None) - anthropic_api_output_tokens = reported_usage.get("output_tokens", None) + # fix null checks for reported_usage + anthropic_api_input_tokens = reported_usage.get("input_tokens", None) if reported_usage else None + anthropic_api_output_tokens = reported_usage.get("output_tokens", None) if reported_usage else None litellm_call_id = response_headers.get("x-litellm-call-id") print(f"LiteLLM Call ID: {litellm_call_id}") From c6cb36186c60fe70f65d0f7ef50c20622cee008c Mon Sep 17 00:00:00 2001 From: Luis Felipe Salazar Ucros <40307832+luisfucros@users.noreply.github.com> Date: Wed, 24 Sep 2025 12:54:22 -0500 Subject: [PATCH 15/70] Add sambanova deepseek v3.1 and gpt-oss-120b (#14866) * add sambanova deepseek v3.1 and gpt-oss-120b * add sambanova deepseek v3.1 and gpt-oss-120b --- ...odel_prices_and_context_window_backup.json | 26 +++++++++++++++++++ model_prices_and_context_window.json | 26 +++++++++++++++++++ 2 files changed, 52 insertions(+) diff --git a/litellm/model_prices_and_context_window_backup.json b/litellm/model_prices_and_context_window_backup.json index b0261d1bd43..6d28cf1235d 100644 --- a/litellm/model_prices_and_context_window_backup.json +++ b/litellm/model_prices_and_context_window_backup.json @@ -18006,6 +18006,32 @@ "supports_reasoning": true, "supports_tool_choice": true }, + "sambanova/DeepSeek-V3.1": { + "max_tokens": 32768, + "max_input_tokens": 32768, + "max_output_tokens": 32768, + "input_cost_per_token": 3e-06, + "output_cost_per_token": 4.5e-06, + "litellm_provider": "sambanova", + "mode": "chat", + "supports_function_calling": true, + "supports_tool_choice": true, + "supports_reasoning": true, + "source": "https://cloud.sambanova.ai/plans/pricing" + }, + "sambanova/gpt-oss-120b": { + "max_tokens": 131072, + "max_input_tokens": 131072, + "max_output_tokens": 131072, + "input_cost_per_token": 3e-06, + "output_cost_per_token": 4.5e-06, + "litellm_provider": "sambanova", + "mode": "chat", + "supports_function_calling": true, + "supports_tool_choice": true, + "supports_reasoning": true, + "source": "https://cloud.sambanova.ai/plans/pricing" + }, "sample_spec": { "code_interpreter_cost_per_session": 0.0, "computer_use_input_cost_per_1k_tokens": 0.0, diff --git a/model_prices_and_context_window.json b/model_prices_and_context_window.json index de1f0c5724e..3742693ce67 100644 --- a/model_prices_and_context_window.json +++ b/model_prices_and_context_window.json @@ -18109,6 +18109,32 @@ "supports_reasoning": true, "supports_tool_choice": true }, + "sambanova/DeepSeek-V3.1": { + "max_tokens": 32768, + "max_input_tokens": 32768, + "max_output_tokens": 32768, + "input_cost_per_token": 3e-06, + "output_cost_per_token": 4.5e-06, + "litellm_provider": "sambanova", + "mode": "chat", + "supports_function_calling": true, + "supports_tool_choice": true, + "supports_reasoning": true, + "source": "https://cloud.sambanova.ai/plans/pricing" + }, + "sambanova/gpt-oss-120b": { + "max_tokens": 131072, + "max_input_tokens": 131072, + "max_output_tokens": 131072, + "input_cost_per_token": 3e-06, + "output_cost_per_token": 4.5e-06, + "litellm_provider": "sambanova", + "mode": "chat", + "supports_function_calling": true, + "supports_tool_choice": true, + "supports_reasoning": true, + "source": "https://cloud.sambanova.ai/plans/pricing" + }, "sample_spec": { "code_interpreter_cost_per_session": 0.0, "computer_use_input_cost_per_1k_tokens": 0.0, From c69bac991be9f8690cd35d11d9bb95c020a37bb3 Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Wed, 24 Sep 2025 10:55:56 -0700 Subject: [PATCH 16/70] [Fix] LakeraAI v2 Guardrail - Ensure exception is raised correctly (#14867) * fix exception lakera * test lakera ai v2 * ruff fix --- .../guardrail_hooks/lakera_ai_v2.py | 47 +++-- litellm/proxy/proxy_config.yaml | 13 ++ tests/guardrails_tests/test_lakera_v2.py | 179 +++++++++++++++++- 3 files changed, 226 insertions(+), 13 deletions(-) diff --git a/litellm/proxy/guardrails/guardrail_hooks/lakera_ai_v2.py b/litellm/proxy/guardrails/guardrail_hooks/lakera_ai_v2.py index e167e73ac4d..b65664e00bd 100644 --- a/litellm/proxy/guardrails/guardrail_hooks/lakera_ai_v2.py +++ b/litellm/proxy/guardrails/guardrail_hooks/lakera_ai_v2.py @@ -3,9 +3,10 @@ import os from datetime import datetime from typing import Dict, List, Literal, Optional, Tuple, Union +from fastapi import HTTPException + import litellm from litellm._logging import verbose_proxy_logger -from litellm.exceptions import GuardrailRaisedException from litellm.integrations.custom_guardrail import CustomGuardrail from litellm.llms.custom_httpx.http_handler import ( get_async_httpx_client, @@ -237,9 +238,8 @@ class LakeraAIGuardrail(CustomGuardrail): ) else: # If there are other violations or not set to mask PII, raise exception - raise GuardrailRaisedException( - guardrail_name=self.guardrail_name, - message="Lakera AI flagged this request. Please review the request and try again.", + raise self._get_http_exception_for_blocked_guardrail( + lakera_guardrail_response ) ######################################################### @@ -304,9 +304,8 @@ class LakeraAIGuardrail(CustomGuardrail): ) else: # If there are other violations or not set to mask PII, raise exception - raise GuardrailRaisedException( - guardrail_name=self.guardrail_name, - message="Lakera AI flagged this request. Please review the request and try again.", + raise self._get_http_exception_for_blocked_guardrail( + lakera_guardrail_response ) ######################################################### @@ -327,8 +326,32 @@ class LakeraAIGuardrail(CustomGuardrail): if not lakera_response: return False - for item in lakera_response.get("payload", []) or []: - detector_type = item.get("detector_type", "") or "" - if not detector_type.startswith("pii/"): - return False - return True + # Check breakdown field for detected violations + breakdown = lakera_response.get("breakdown", []) or [] + if not breakdown: + return False + + has_violations = False + for item in breakdown: + if item.get("detected", False): + has_violations = True + detector_type = item.get("detector_type", "") or "" + if not detector_type.startswith("pii/"): + return False + + # Return True only if there are violations and they are all PII + return has_violations + + def _get_http_exception_for_blocked_guardrail( + self, lakera_response: Optional[LakeraAIResponse] + ) -> HTTPException: + """ + Get the HTTP exception for a blocked guardrail, similar to Bedrock's implementation. + """ + return HTTPException( + status_code=400, + detail={ + "error": "Violated guardrail policy", + "lakera_guardrail_response": lakera_response, + }, + ) diff --git a/litellm/proxy/proxy_config.yaml b/litellm/proxy/proxy_config.yaml index addf2443d36..921b564407f 100644 --- a/litellm/proxy/proxy_config.yaml +++ b/litellm/proxy/proxy_config.yaml @@ -23,3 +23,16 @@ model_list: litellm_params: model: gemini/* api_key: os.environ/GEMINI_API_KEY + + +guardrails: + - guardrail_name: lakera + litellm_params: + guardrail: lakera_v2 + mode: pre_call + api_key: os.environ/LAKERA_API_KEY + default_on: false + project_id: project-9770817088 + breakdown: true + payload: true + dev_info: true diff --git a/tests/guardrails_tests/test_lakera_v2.py b/tests/guardrails_tests/test_lakera_v2.py index 19c4424bee6..f3b2795a275 100644 --- a/tests/guardrails_tests/test_lakera_v2.py +++ b/tests/guardrails_tests/test_lakera_v2.py @@ -11,7 +11,8 @@ from litellm.proxy.guardrails.guardrail_hooks.lakera_ai_v2 import LakeraAIGuardr from litellm.types.guardrails import PiiEntityType, PiiAction from litellm.proxy._types import UserAPIKeyAuth from litellm.caching.caching import DualCache -from litellm.exceptions import BlockedPiiEntityError +from litellm.exceptions import BlockedPiiEntityError, GuardrailRaisedException +from fastapi import HTTPException from litellm.types.utils import CallTypes as LitellmCallTypes @@ -54,3 +55,179 @@ async def test_lakera_pre_call_hook_for_pii_masking(): assert "4111-1111-1111-1111" not in user_message assert "test@example.com" not in user_message + +@pytest.mark.asyncio +async def test_lakera_blocks_non_pii_violations(): + """Test that Lakera guardrail blocks requests with non-PII violations like hate speech, violence, etc.""" + + lakera_guardrail = LakeraAIGuardrail( + api_key="test_key", + ) + + # Mock the call_v2_guard method to return a response similar to the user's example + mock_response = { + 'payload': [], + 'flagged': True, + 'dev_info': {'git_revision': 'f0bc093a', 'git_timestamp': '2025-09-23T15:28:06+00:00', 'model_version': 'lakera-guard-1', 'version': '2.0.281'}, + 'metadata': {'request_uuid': 'b7cd4c8a-28aa-4285-a245-2befee514dbf'}, + 'breakdown': [ + {'project_id': 'project-9770817088', 'policy_id': 'policy-lakera-default', 'detector_id': 'detector-lakera-default-moderated-content', 'detector_type': 'moderated_content/crime', 'detected': True, 'message_id': 0}, + {'project_id': 'project-9770817088', 'policy_id': 'policy-lakera-default', 'detector_id': 'detector-lakera-default-moderated-content', 'detector_type': 'moderated_content/hate', 'detected': True, 'message_id': 0}, + {'project_id': 'project-9770817088', 'policy_id': 'policy-lakera-default', 'detector_id': 'detector-lakera-default-moderated-content', 'detector_type': 'moderated_content/violence', 'detected': True, 'message_id': 0}, + {'project_id': 'project-9770817088', 'policy_id': 'policy-lakera-default', 'detector_id': 'detector-lakera-default-prompt-attack', 'detector_type': 'prompt_attack', 'detected': True, 'message_id': 0}, + {'project_id': 'project-9770817088', 'policy_id': 'policy-lakera-default', 'detector_id': 'detector-lakera-default-pii', 'detector_type': 'pii/email', 'detected': False, 'message_id': 0}, + ] + } + + with patch.object(lakera_guardrail, 'call_v2_guard', new_callable=AsyncMock) as mock_call: + mock_call.return_value = (mock_response, {}) + + # Create a sample request that would trigger violations + data = { + "messages": [ + {"role": "user", "content": "Some harmful content that triggers violations"} + ], + "model": "gpt-3.5-turbo", + "metadata": {} + } + + # Mock objects needed for the pre-call hook + user_api_key_dict = UserAPIKeyAuth(api_key="test_key") + cache = DualCache() + + # The guardrail should raise an HTTPException for non-PII violations + with pytest.raises(HTTPException) as exc_info: + await lakera_guardrail.async_pre_call_hook( + user_api_key_dict=user_api_key_dict, + cache=cache, + data=data, + call_type="completion" + ) + + # Verify the exception details include the Lakera response + assert exc_info.value.status_code == 400 + assert "Violated guardrail policy" in str(exc_info.value.detail) + assert "lakera_guardrail_response" in exc_info.value.detail + + +@pytest.mark.asyncio +async def test_lakera_only_pii_violations_are_masked(): + """Test that Lakera guardrail only masks PII violations and doesn't block the request.""" + + lakera_guardrail = LakeraAIGuardrail( + api_key="test_key", + ) + + # Mock response with only PII violations + mock_response = { + 'payload': [ + {'detector_type': 'pii/email', 'start': 10, 'end': 25, 'message_id': 0} + ], + 'flagged': True, + 'breakdown': [ + {'project_id': 'project-9770817088', 'detector_type': 'pii/email', 'detected': True, 'message_id': 0}, + {'project_id': 'project-9770817088', 'detector_type': 'moderated_content/hate', 'detected': False, 'message_id': 0}, + {'project_id': 'project-9770817088', 'detector_type': 'prompt_attack', 'detected': False, 'message_id': 0}, + ] + } + + with patch.object(lakera_guardrail, 'call_v2_guard', new_callable=AsyncMock) as mock_call: + mock_call.return_value = (mock_response, {}) + + data = { + "messages": [ + {"role": "user", "content": "My email test@example.com here"} + ], + "model": "gpt-3.5-turbo", + "metadata": {} + } + + user_api_key_dict = UserAPIKeyAuth(api_key="test_key") + cache = DualCache() + + # Should not raise an exception, just mask the PII + result = await lakera_guardrail.async_pre_call_hook( + user_api_key_dict=user_api_key_dict, + cache=cache, + data=data, + call_type="completion" + ) + + # Verify the request was not blocked + assert result is not None + assert "messages" in result + + +@pytest.mark.asyncio +async def test_lakera_blocks_flagged_content_with_user_scenario(): + """ + Test the exact user scenario where Lakera flagged content but request went through. + This should now be blocked with the fix to check breakdown field instead of payload. + """ + + lakera_guardrail = LakeraAIGuardrail( + api_key="test_key", + ) + + # Mock response matching the exact user scenario + mock_response = { + 'payload': [], # Empty payload like in user's case + 'flagged': True, + 'dev_info': {'git_revision': 'f0bc093a', 'git_timestamp': '2025-09-23T15:28:06+00:00', 'model_version': 'lakera-guard-1', 'version': '2.0.281'}, + 'metadata': {'request_uuid': 'b7cd4c8a-28aa-4285-a245-2befee514dbf'}, + 'breakdown': [ + {'project_id': 'project-9770817088', 'policy_id': 'policy-lakera-default', 'detector_id': 'detector-lakera-default-moderated-content', 'detector_type': 'moderated_content/crime', 'detected': True, 'message_id': 0}, + {'project_id': 'project-9770817088', 'policy_id': 'policy-lakera-default', 'detector_id': 'detector-lakera-default-moderated-content', 'detector_type': 'moderated_content/hate', 'detected': True, 'message_id': 0}, + {'project_id': 'project-9770817088', 'policy_id': 'policy-lakera-default', 'detector_id': 'detector-lakera-default-moderated-content', 'detector_type': 'moderated_content/profanity', 'detected': False, 'message_id': 0}, + {'project_id': 'project-9770817088', 'policy_id': 'policy-lakera-default', 'detector_id': 'detector-lakera-default-moderated-content', 'detector_type': 'moderated_content/sexual', 'detected': False, 'message_id': 0}, + {'project_id': 'project-9770817088', 'policy_id': 'policy-lakera-default', 'detector_id': 'detector-lakera-default-moderated-content', 'detector_type': 'moderated_content/violence', 'detected': True, 'message_id': 0}, + {'project_id': 'project-9770817088', 'policy_id': 'policy-lakera-default', 'detector_id': 'detector-lakera-default-moderated-content', 'detector_type': 'moderated_content/weapons', 'detected': True, 'message_id': 0}, + {'project_id': 'project-9770817088', 'policy_id': 'policy-lakera-default', 'detector_id': 'detector-lakera-default-pii', 'detector_type': 'pii/address', 'detected': False, 'message_id': 0}, + {'project_id': 'project-9770817088', 'policy_id': 'policy-lakera-default', 'detector_id': 'detector-lakera-default-pii', 'detector_type': 'pii/credit_card', 'detected': False, 'message_id': 0}, + {'project_id': 'project-9770817088', 'policy_id': 'policy-lakera-default', 'detector_id': 'detector-lakera-default-pii', 'detector_type': 'pii/email', 'detected': False, 'message_id': 0}, + {'project_id': 'project-9770817088', 'policy_id': 'policy-lakera-default', 'detector_id': 'detector-lakera-default-pii', 'detector_type': 'pii/iban_code', 'detected': False, 'message_id': 0}, + {'project_id': 'project-9770817088', 'policy_id': 'policy-lakera-default', 'detector_id': 'detector-lakera-default-pii', 'detector_type': 'pii/ip_address', 'detected': False, 'message_id': 0}, + {'project_id': 'project-9770817088', 'policy_id': 'policy-lakera-default', 'detector_id': 'detector-lakera-default-pii', 'detector_type': 'pii/name', 'detected': False, 'message_id': 0}, + {'project_id': 'project-9770817088', 'policy_id': 'policy-lakera-default', 'detector_id': 'detector-lakera-default-pii', 'detector_type': 'pii/phone_number', 'detected': False, 'message_id': 0}, + {'project_id': 'project-9770817088', 'policy_id': 'policy-lakera-default', 'detector_id': 'detector-lakera-default-pii', 'detector_type': 'pii/us_social_security_number', 'detected': False, 'message_id': 0}, + {'project_id': 'project-9770817088', 'policy_id': 'policy-lakera-default', 'detector_id': 'detector-lakera-default-prompt-attack', 'detector_type': 'prompt_attack', 'detected': True, 'message_id': 0}, + {'project_id': 'project-9770817088', 'policy_id': 'policy-lakera-default', 'detector_id': 'detector-lakera-default-unknown-links', 'detector_type': 'unknown_links', 'detected': False, 'message_id': 0} + ] + } + + with patch.object(lakera_guardrail, 'call_v2_guard', new_callable=AsyncMock) as mock_call: + mock_call.return_value = (mock_response, {}) + + # Create a sample request that would trigger violations + data = { + "messages": [ + {"role": "user", "content": "Some harmful content that should be blocked"} + ], + "model": "gpt-3.5-turbo", + "metadata": {} + } + + # Mock objects needed for the pre-call hook + user_api_key_dict = UserAPIKeyAuth(api_key="test_key") + cache = DualCache() + + # With the fix, this should now raise an HTTPException instead of letting the request through + with pytest.raises(HTTPException) as exc_info: + await lakera_guardrail.async_pre_call_hook( + user_api_key_dict=user_api_key_dict, + cache=cache, + data=data, + call_type="completion" + ) + + # Verify the exception details + assert exc_info.value.status_code == 400 + assert "Violated guardrail policy" in str(exc_info.value.detail) + assert "lakera_guardrail_response" in exc_info.value.detail + + # Verify the full response is included in the exception + lakera_response = exc_info.value.detail["lakera_guardrail_response"] + assert lakera_response["flagged"] is True + assert lakera_response["metadata"]["request_uuid"] == "b7cd4c8a-28aa-4285-a245-2befee514dbf" + assert len(lakera_response["breakdown"]) == 16 # All the breakdown items from the user's scenario + From 3f5b4c84eb42228357c6ddaa67c349d77abe6787 Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Wed, 24 Sep 2025 11:21:14 -0700 Subject: [PATCH 17/70] feat: Add /user/list route to LiteLLMRoutes (#14868) Co-authored-by: Cursor Agent Co-authored-by: ishaan --- litellm/proxy/_types.py | 1 + 1 file changed, 1 insertion(+) diff --git a/litellm/proxy/_types.py b/litellm/proxy/_types.py index 9d5298c30f8..a92b7d8ad2e 100644 --- a/litellm/proxy/_types.py +++ b/litellm/proxy/_types.py @@ -422,6 +422,7 @@ class LiteLLMRoutes(enum.Enum): "/user/update", "/user/delete", "/user/info", + "/user/list", # team "/team/new", "/team/update", From 1816176f9d519e742878d5b3f809e913c971fee2 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Wed, 24 Sep 2025 11:54:06 -0700 Subject: [PATCH 18/70] [Memory Leak Fix] Fix InMemoryCache unbounded growth when TTLs are set (#14869) * Initial plan * Fix InMemoryCache unbounded growth issue when TTLs are set Co-authored-by: ishaan-jaff <29436595+ishaan-jaff@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: ishaan-jaff <29436595+ishaan-jaff@users.noreply.github.com> --- litellm/caching/in_memory_cache.py | 24 ++++- .../caching/test_in_memory_cache.py | 98 +++++++++++++++++++ 2 files changed, 118 insertions(+), 4 deletions(-) diff --git a/litellm/caching/in_memory_cache.py b/litellm/caching/in_memory_cache.py index 63869474d47..082cac791f2 100644 --- a/litellm/caching/in_memory_cache.py +++ b/litellm/caching/in_memory_cache.py @@ -36,7 +36,7 @@ class InMemoryCache(BaseCache): max_size_in_memory [int]: Maximum number of items in cache. done to prevent memory leaks. Use 200 items as a default """ self.max_size_in_memory = ( - max_size_in_memory or 200 + max_size_in_memory if max_size_in_memory is not None else 200 ) # set an upper bound of 200 items in-memory self.default_ttl = default_ttl or 600 self.max_size_per_item = ( @@ -103,20 +103,32 @@ class InMemoryCache(BaseCache): def evict_cache(self): """ Eviction policy: - - check if any items in ttl_dict are expired -> remove them from ttl_dict and cache_dict + 1. First, remove expired items from ttl_dict and cache_dict + 2. If cache is still at or above max_size_in_memory, evict items with earliest expiration times This guarantees the following: - - 1. When item ttl not set: At minimumm each item will remain in memory for 5 minutes - - 2. When ttl is set: the item will remain in memory for at least that amount of time + - 1. When item ttl not set: At minimum each item will remain in memory for the default ttl + - 2. When ttl is set: the item will remain in memory for at least that amount of time, unless cache size requires eviction - 3. the size of in-memory cache is bounded """ current_time = time.time() + + # Step 1: Remove expired items expired_keys = [key for key, ttl in self.ttl_dict.items() if current_time > ttl] for key in expired_keys: self._remove_key(key) + # Step 2: If cache is still full, evict items with earliest expiration times + if len(self.cache_dict) >= self.max_size_in_memory: + # Sort by expiration time (earliest first) and evict until we're under the limit + items_by_expiration = sorted(self.ttl_dict.items(), key=lambda x: x[1]) + keys_to_evict = items_by_expiration[:len(self.cache_dict) - self.max_size_in_memory + 1] + + for key, _ in keys_to_evict: + self._remove_key(key) + # de-reference the removed item # https://www.geeksforgeeks.org/diagnosing-and-fixing-memory-leaks-in-python/ # One of the most common causes of memory leaks in Python is the retention of objects that are no longer being used. @@ -135,6 +147,10 @@ class InMemoryCache(BaseCache): return False def set_cache(self, key, value, **kwargs): + # Handle the edge case where max_size_in_memory is 0 + if self.max_size_in_memory == 0: + return # Don't cache anything if max size is 0 + if len(self.cache_dict) >= self.max_size_in_memory: # only evict when cache is full self.evict_cache() diff --git a/tests/test_litellm/caching/test_in_memory_cache.py b/tests/test_litellm/caching/test_in_memory_cache.py index 72f264b8b7d..616c60c74a0 100644 --- a/tests/test_litellm/caching/test_in_memory_cache.py +++ b/tests/test_litellm/caching/test_in_memory_cache.py @@ -88,3 +88,101 @@ def test_in_memory_cache_ttl_allow_override(): new_ttl_time = in_memory_cache.ttl_dict["new-fake-key"] assert new_ttl_time is not None assert new_ttl_time != initial_ttl_time + + +def test_in_memory_cache_max_size_with_ttl(): + """ + Test that max_size_in_memory is respected even when all items have long TTLs. + This tests the fix for the unbounded growth issue. + """ + in_memory_cache = InMemoryCache(max_size_in_memory=3) + long_ttl = 86400 # 1 day + + # Fill the cache to max capacity + for i in range(3): + in_memory_cache.set_cache(key=f"key_{i}", value=f"value_{i}", ttl=long_ttl) + time.sleep(0.01) # Small delay to ensure different timestamps + + assert len(in_memory_cache.cache_dict) == 3 + assert len(in_memory_cache.ttl_dict) == 3 + + # Add another item - should evict the earliest item + in_memory_cache.set_cache(key="key_3", value="value_3", ttl=long_ttl) + + # Cache should still be at max size, not larger + assert len(in_memory_cache.cache_dict) == 3 + assert len(in_memory_cache.ttl_dict) == 3 + + # key_0 should have been evicted (it was added first) + assert "key_0" not in in_memory_cache.cache_dict + assert "key_0" not in in_memory_cache.ttl_dict + + # Other keys should still be present + assert "key_1" in in_memory_cache.cache_dict + assert "key_2" in in_memory_cache.cache_dict + assert "key_3" in in_memory_cache.cache_dict + + +def test_in_memory_cache_expired_items_evicted_first(): + """ + Test that expired items are evicted before non-expired items when cache is full. + """ + in_memory_cache = InMemoryCache(max_size_in_memory=3) + + # Add items with short TTL that will expire + in_memory_cache.set_cache(key="expired_1", value="value_1", ttl=1) + in_memory_cache.set_cache(key="expired_2", value="value_2", ttl=1) + + # Add item with long TTL + in_memory_cache.set_cache(key="long_lived", value="value_long", ttl=86400) + + assert len(in_memory_cache.cache_dict) == 3 + + # Wait for short TTL items to expire + time.sleep(2) + + # Add new item - should evict expired items first, not the long-lived one + in_memory_cache.set_cache(key="new_item", value="new_value", ttl=86400) + + # Long-lived item should still be present + assert "long_lived" in in_memory_cache.cache_dict + assert "new_item" in in_memory_cache.cache_dict + + # Expired items should be gone + assert "expired_1" not in in_memory_cache.cache_dict + assert "expired_2" not in in_memory_cache.cache_dict + assert "expired_1" not in in_memory_cache.ttl_dict + assert "expired_2" not in in_memory_cache.ttl_dict + + +def test_in_memory_cache_eviction_order(): + """ + Test that when non-expired items need to be evicted, those with earliest expiration times are evicted first. + """ + in_memory_cache = InMemoryCache(max_size_in_memory=2) + + # Add items with different TTLs + now = time.time() + in_memory_cache.set_cache(key="early_expire", value="value_1", ttl=100) # expires in 100 seconds + time.sleep(0.01) + in_memory_cache.set_cache(key="late_expire", value="value_2", ttl=200) # expires in 200 seconds + + # Verify TTL order + early_ttl = in_memory_cache.ttl_dict["early_expire"] + late_ttl = in_memory_cache.ttl_dict["late_expire"] + assert early_ttl < late_ttl, "early_expire should have earlier expiration time" + + assert len(in_memory_cache.cache_dict) == 2 + + # Add third item - should evict the one with earliest expiration time + in_memory_cache.set_cache(key="new_item", value="value_3", ttl=300) + + assert len(in_memory_cache.cache_dict) == 2 + + # Item with earliest expiration should be evicted + assert "early_expire" not in in_memory_cache.cache_dict + assert "early_expire" not in in_memory_cache.ttl_dict + + # Items with later expiration should remain + assert "late_expire" in in_memory_cache.cache_dict + assert "new_item" in in_memory_cache.cache_dict From 66a88d3761a3d9e3cef530a867be7bf3431a18b3 Mon Sep 17 00:00:00 2001 From: Ishaan Jaffer Date: Wed, 24 Sep 2025 11:54:44 -0700 Subject: [PATCH 19/70] fix ci/cd --- .circleci/config.yml | 2 +- ...odel_prices_and_context_window_backup.json | 149 +++++++++++++++++- 2 files changed, 148 insertions(+), 3 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 0e53cfc0edb..ef6445ca0ca 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -144,7 +144,7 @@ jobs: name: Linting Testing command: | cd litellm - pip install "cryptography<40.0.0" + pip install "cryptography>=43.0.1" python -m pip install types-requests types-setuptools types-redis types-PyYAML if ! python -m mypy . \ --config-file mypy.ini \ diff --git a/litellm/model_prices_and_context_window_backup.json b/litellm/model_prices_and_context_window_backup.json index 6d28cf1235d..3742693ce67 100644 --- a/litellm/model_prices_and_context_window_backup.json +++ b/litellm/model_prices_and_context_window_backup.json @@ -2032,6 +2032,36 @@ "supports_tool_choice": false, "supports_vision": true }, + "azure/gpt-5-codex": { + "cache_read_input_token_cost": 1.25e-07, + "input_cost_per_token": 1.25e-06, + "litellm_provider": "azure", + "max_input_tokens": 272000, + "max_output_tokens": 128000, + "max_tokens": 128000, + "mode": "responses", + "output_cost_per_token": 1e-05, + "supported_endpoints": [ + "/v1/responses" + ], + "supported_modalities": [ + "text", + "image" + ], + "supported_output_modalities": [ + "text" + ], + "supports_function_calling": true, + "supports_native_streaming": true, + "supports_parallel_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_vision": true + }, "azure/gpt-5-mini": { "cache_read_input_token_cost": 2.5e-08, "input_cost_per_token": 2.5e-07, @@ -5282,6 +5312,49 @@ "supports_tool_choice": true, "supports_vision": true }, + "deepseek-chat": { + "cache_read_input_token_cost": 6e-08, + "input_cost_per_token": 6e-07, + "litellm_provider": "deepseek", + "max_input_tokens": 131072, + "max_output_tokens": 8192, + "max_tokens": 131072, + "mode": "chat", + "output_cost_per_token": 1.7e-06, + "source": "https://api-docs.deepseek.com/quick_start/pricing", + "supported_endpoints": [ + "/v1/chat/completions" + ], + "supports_function_calling": true, + "supports_native_streaming": true, + "supports_parallel_function_calling": true, + "supports_prompt_caching": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true + }, + "deepseek-reasoner": { + "cache_read_input_token_cost": 6e-08, + "input_cost_per_token": 6e-07, + "litellm_provider": "deepseek", + "max_input_tokens": 131072, + "max_output_tokens": 65536, + "max_tokens": 131072, + "mode": "chat", + "output_cost_per_token": 1.7e-06, + "source": "https://api-docs.deepseek.com/quick_start/pricing", + "supported_endpoints": [ + "/v1/chat/completions" + ], + "supports_function_calling": false, + "supports_native_streaming": true, + "supports_parallel_function_calling": false, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": false + }, "dashscope/qwen-coder": { "input_cost_per_token": 3e-07, "litellm_provider": "dashscope", @@ -7214,7 +7287,7 @@ "supports_function_calling": true, "supports_reasoning": true, "supports_tool_choice": true - }, + }, "dolphin": { "input_cost_per_token": 5e-07, "litellm_provider": "nlp_cloud", @@ -12427,6 +12500,36 @@ "supports_tool_choice": false, "supports_vision": true }, + "gpt-5-codex": { + "cache_read_input_token_cost": 1.25e-07, + "input_cost_per_token": 1.25e-06, + "litellm_provider": "openai", + "max_input_tokens": 272000, + "max_output_tokens": 128000, + "max_tokens": 128000, + "mode": "responses", + "output_cost_per_token": 1e-05, + "supported_endpoints": [ + "/v1/responses" + ], + "supported_modalities": [ + "text", + "image" + ], + "supported_output_modalities": [ + "text" + ], + "supports_function_calling": true, + "supports_native_streaming": false, + "supports_parallel_function_calling": true, + "supports_pdf_input": false, + "supports_prompt_caching": true, + "supports_reasoning": false, + "supports_response_schema": true, + "supports_system_messages": false, + "supports_tool_choice": true, + "supports_vision": true + }, "gpt-5-mini": { "cache_read_input_token_cost": 2.5e-08, "cache_read_input_token_cost_flex": 1.25e-08, @@ -17579,7 +17682,7 @@ "supports_function_calling": true, "supports_reasoning": true, "supports_tool_choice": true - }, + }, "recraft/recraftv2": { "litellm_provider": "recraft", "mode": "image_generation", @@ -20553,6 +20656,24 @@ "supports_function_calling": true, "supports_tool_choice": true }, + "vertex_ai/deepseek-ai/deepseek-v3.1-maas": { + "input_cost_per_token": 1.35e-06, + "litellm_provider": "vertex_ai-deepseek_models", + "max_input_tokens": 163840, + "max_output_tokens": 32768, + "max_tokens": 163840, + "mode": "chat", + "output_cost_per_token": 5.4e-06, + "source": "https://cloud.google.com/vertex-ai/generative-ai/pricing#partner-models", + "supported_regions": [ + "us-west2" + ], + "supports_assistant_prefill": true, + "supports_function_calling": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_tool_choice": true + }, "vertex_ai/deepseek-ai/deepseek-r1-0528-maas": { "input_cost_per_token": 1.35e-06, "litellm_provider": "vertex_ai-deepseek_models", @@ -20966,6 +21087,30 @@ "supports_function_calling": true, "supports_tool_choice": true }, + "vertex_ai/qwen/qwen3-next-80b-a3b-instruct-maas": { + "input_cost_per_token": 1.5e-07, + "litellm_provider": "vertex_ai-qwen_models", + "max_input_tokens": 262144, + "max_output_tokens": 262144, + "max_tokens": 262144, + "mode": "chat", + "output_cost_per_token": 1.2e-06, + "source": "https://cloud.google.com/vertex-ai/generative-ai/pricing", + "supports_function_calling": true, + "supports_tool_choice": true + }, + "vertex_ai/qwen/qwen3-next-80b-a3b-thinking-maas": { + "input_cost_per_token": 1.5e-07, + "litellm_provider": "vertex_ai-qwen_models", + "max_input_tokens": 262144, + "max_output_tokens": 262144, + "max_tokens": 262144, + "mode": "chat", + "output_cost_per_token": 1.2e-06, + "source": "https://cloud.google.com/vertex-ai/generative-ai/pricing", + "supports_function_calling": true, + "supports_tool_choice": true + }, "vertex_ai/veo-2.0-generate-001": { "litellm_provider": "vertex_ai-video-models", "max_input_tokens": 1024, From 268b66b7ea826c5b6be79ea090f9d0fa3626b216 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 24 Sep 2025 16:13:13 -0700 Subject: [PATCH 20/70] Bump tar-fs from 3.0.10 to 3.1.1 in /docs/my-website (#14872) Bumps [tar-fs](https://github.com/mafintosh/tar-fs) from 3.0.10 to 3.1.1. - [Commits](https://github.com/mafintosh/tar-fs/compare/v3.0.10...v3.1.1) --- updated-dependencies: - dependency-name: tar-fs dependency-version: 3.1.1 dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- docs/my-website/package-lock.json | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/docs/my-website/package-lock.json b/docs/my-website/package-lock.json index 4b37e2be11d..b71a15cc8e6 100644 --- a/docs/my-website/package-lock.json +++ b/docs/my-website/package-lock.json @@ -17120,9 +17120,10 @@ } }, "node_modules/prebuild-install/node_modules/tar-fs": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.3.tgz", - "integrity": "sha512-090nwYJDmlhwFwEW3QQl+vaNnxsO2yVsd45eTKRBzSzu+hlb1w2K9inVq5b0ngXuLVqQ4ApvsUHHnu/zQNkWAg==", + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz", + "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==", + "license": "MIT", "dependencies": { "chownr": "^1.1.1", "mkdirp-classic": "^0.5.2", @@ -19295,9 +19296,10 @@ } }, "node_modules/tar-fs": { - "version": "3.0.10", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.0.10.tgz", - "integrity": "sha512-C1SwlQGNLe/jPNqapK8epDsXME7CAJR5RL3GcE6KWx1d9OUByzoHVcbu1VPI8tevg9H8Alae0AApHHFGzrD5zA==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.1.1.tgz", + "integrity": "sha512-LZA0oaPOc2fVo82Txf3gw+AkEd38szODlptMYejQUhndHMLQ9M059uXR+AfS7DNo0NpINvSqDsvyaCrBVkptWg==", + "license": "MIT", "dependencies": { "pump": "^3.0.0", "tar-stream": "^3.1.5" From df88b0656e000f48738b745abc89dfec3b5f8380 Mon Sep 17 00:00:00 2001 From: Krrish Dholakia Date: Wed, 24 Sep 2025 16:13:41 -0700 Subject: [PATCH 21/70] test: update .json --- .../langfuse_expected_request_body/completion.json | 9 ++++++++- .../completion_with_bedrock_call.json | 8 +++++++- .../completion_with_complex_metadata.json | 6 ++++++ .../completion_with_langfuse_metadata.json | 6 ++++++ .../completion_with_no_choices.json | 6 ++++++ .../completion_with_router.json | 6 ++++++ .../completion_with_tags.json | 6 ++++++ .../completion_with_tags_stream.json | 6 ++++++ .../completion_with_vertex_call.json | 6 ++++++ .../langfuse_expected_request_body/complex_metadata.json | 6 ++++++ .../complex_metadata_2.json | 6 ++++++ .../langfuse_expected_request_body/empty_metadata.json | 6 ++++++ .../metadata_with_function.json | 6 ++++++ .../metadata_with_lock.json | 6 ++++++ .../langfuse_expected_request_body/nested_metadata.json | 6 ++++++ .../langfuse_expected_request_body/simple_metadata.json | 6 ++++++ .../langfuse_expected_request_body/simple_metadata2.json | 6 ++++++ .../langfuse_expected_request_body/simple_metadata3.json | 6 ++++++ 18 files changed, 111 insertions(+), 2 deletions(-) diff --git a/tests/logging_callback_tests/langfuse_expected_request_body/completion.json b/tests/logging_callback_tests/langfuse_expected_request_body/completion.json index bede5753a2a..b995df2d445 100644 --- a/tests/logging_callback_tests/langfuse_expected_request_body/completion.json +++ b/tests/logging_callback_tests/langfuse_expected_request_body/completion.json @@ -68,7 +68,14 @@ "output": 20, "unit": "TOKENS", "totalCost": 5.4999999999999995e-05 - } + }, + "usageDetails": { + "cache_creation_input_tokens": 0, + "cache_read_input_tokens": 0, + "input": 10, + "output": 20 + }, + "traceId": "litellm-test-6a51ae70-a4e7-499e-afcd-dce2a3b31850" }, "timestamp": "2025-01-16T19:28:55.125258Z" } diff --git a/tests/logging_callback_tests/langfuse_expected_request_body/completion_with_bedrock_call.json b/tests/logging_callback_tests/langfuse_expected_request_body/completion_with_bedrock_call.json index aa96e5949e5..d9f52477fc8 100644 --- a/tests/logging_callback_tests/langfuse_expected_request_body/completion_with_bedrock_call.json +++ b/tests/logging_callback_tests/langfuse_expected_request_body/completion_with_bedrock_call.json @@ -63,7 +63,13 @@ "output": 10, "unit": "TOKENS", "totalCost": 0.00018 - } + }, + "usageDetails": { + "cache_creation_input_tokens": 0, + "cache_read_input_tokens": 0, + "input": 10, + "output": 10 + } }, "timestamp": "2025-05-26T21:13:16.797156Z" } diff --git a/tests/logging_callback_tests/langfuse_expected_request_body/completion_with_complex_metadata.json b/tests/logging_callback_tests/langfuse_expected_request_body/completion_with_complex_metadata.json index 1e60a0479b1..348fe5956da 100644 --- a/tests/logging_callback_tests/langfuse_expected_request_body/completion_with_complex_metadata.json +++ b/tests/logging_callback_tests/langfuse_expected_request_body/completion_with_complex_metadata.json @@ -109,6 +109,12 @@ "output": 20, "unit": "TOKENS", "totalCost": 5.4999999999999995e-05 + }, + "usageDetails": { + "cache_creation_input_tokens": 0, + "cache_read_input_tokens": 0, + "input": 10, + "output": 20 } }, "timestamp": "2025-01-22T17:27:51.703046Z" diff --git a/tests/logging_callback_tests/langfuse_expected_request_body/completion_with_langfuse_metadata.json b/tests/logging_callback_tests/langfuse_expected_request_body/completion_with_langfuse_metadata.json index a217a901285..b63bedf16a2 100644 --- a/tests/logging_callback_tests/langfuse_expected_request_body/completion_with_langfuse_metadata.json +++ b/tests/logging_callback_tests/langfuse_expected_request_body/completion_with_langfuse_metadata.json @@ -87,6 +87,12 @@ "output": 20, "unit": "TOKENS", "totalCost": 5.4999999999999995e-05 + }, + "usageDetails": { + "cache_creation_input_tokens": 0, + "cache_read_input_tokens": 0, + "input": 10, + "output": 20 } }, "timestamp": "2025-01-22T17:19:11.235541Z" diff --git a/tests/logging_callback_tests/langfuse_expected_request_body/completion_with_no_choices.json b/tests/logging_callback_tests/langfuse_expected_request_body/completion_with_no_choices.json index be21c297dcc..f4242d7edc3 100644 --- a/tests/logging_callback_tests/langfuse_expected_request_body/completion_with_no_choices.json +++ b/tests/logging_callback_tests/langfuse_expected_request_body/completion_with_no_choices.json @@ -58,6 +58,12 @@ "output": 10, "unit": "TOKENS", "totalCost": 3.5e-05 + }, + "usageDetails": { + "cache_creation_input_tokens": 0, + "cache_read_input_tokens": 0, + "input": 10, + "output": 10 } }, "timestamp": "2025-02-07T00:23:27.670175Z" diff --git a/tests/logging_callback_tests/langfuse_expected_request_body/completion_with_router.json b/tests/logging_callback_tests/langfuse_expected_request_body/completion_with_router.json index 021c2b1b73c..84ea9768f01 100644 --- a/tests/logging_callback_tests/langfuse_expected_request_body/completion_with_router.json +++ b/tests/logging_callback_tests/langfuse_expected_request_body/completion_with_router.json @@ -73,6 +73,12 @@ "output": 10, "unit": "TOKENS", "totalCost": 3.5e-05 + }, + "usageDetails": { + "cache_creation_input_tokens": 0, + "cache_read_input_tokens": 0, + "input": 10, + "output": 10 } }, "timestamp": "2025-05-24T17:01:19.408586Z" diff --git a/tests/logging_callback_tests/langfuse_expected_request_body/completion_with_tags.json b/tests/logging_callback_tests/langfuse_expected_request_body/completion_with_tags.json index 07fca9daafb..e7442ce0a02 100644 --- a/tests/logging_callback_tests/langfuse_expected_request_body/completion_with_tags.json +++ b/tests/logging_callback_tests/langfuse_expected_request_body/completion_with_tags.json @@ -77,6 +77,12 @@ "output": 20, "unit": "TOKENS", "totalCost": 5.4999999999999995e-05 + }, + "usageDetails": { + "cache_creation_input_tokens": 0, + "cache_read_input_tokens": 0, + "input": 10, + "output": 20 } }, "timestamp": "2025-01-22T15:31:28.964179Z" diff --git a/tests/logging_callback_tests/langfuse_expected_request_body/completion_with_tags_stream.json b/tests/logging_callback_tests/langfuse_expected_request_body/completion_with_tags_stream.json index d7003c3f99e..04c24c8963a 100644 --- a/tests/logging_callback_tests/langfuse_expected_request_body/completion_with_tags_stream.json +++ b/tests/logging_callback_tests/langfuse_expected_request_body/completion_with_tags_stream.json @@ -77,6 +77,12 @@ "output": 20, "unit": "TOKENS", "totalCost": 5.4999999999999995e-05 + }, + "usageDetails": { + "cache_creation_input_tokens": 0, + "cache_read_input_tokens": 0, + "input": 10, + "output": 20 } }, "timestamp": "2025-01-22T16:38:26.017252Z" diff --git a/tests/logging_callback_tests/langfuse_expected_request_body/completion_with_vertex_call.json b/tests/logging_callback_tests/langfuse_expected_request_body/completion_with_vertex_call.json index 51a4fac0579..3e27f5b54b4 100644 --- a/tests/logging_callback_tests/langfuse_expected_request_body/completion_with_vertex_call.json +++ b/tests/logging_callback_tests/langfuse_expected_request_body/completion_with_vertex_call.json @@ -61,6 +61,12 @@ "output": 10, "unit": "TOKENS", "totalCost": 7.5e-06 + }, + "usageDetails": { + "cache_creation_input_tokens": 0, + "cache_read_input_tokens": 0, + "input": 10, + "output": 10 } }, "timestamp": "2025-05-26T21:15:40.610953Z" diff --git a/tests/logging_callback_tests/langfuse_expected_request_body/complex_metadata.json b/tests/logging_callback_tests/langfuse_expected_request_body/complex_metadata.json index e8f01f6d723..a21ab058fcd 100644 --- a/tests/logging_callback_tests/langfuse_expected_request_body/complex_metadata.json +++ b/tests/logging_callback_tests/langfuse_expected_request_body/complex_metadata.json @@ -84,6 +84,12 @@ "output": 20, "unit": "TOKENS", "totalCost": 5.4999999999999995e-05 + }, + "usageDetails": { + "cache_creation_input_tokens": 0, + "cache_read_input_tokens": 0, + "input": 10, + "output": 20 } }, "timestamp": "2025-01-22T17:59:39.368310Z" diff --git a/tests/logging_callback_tests/langfuse_expected_request_body/complex_metadata_2.json b/tests/logging_callback_tests/langfuse_expected_request_body/complex_metadata_2.json index 786fe20e7b4..3f400f65914 100644 --- a/tests/logging_callback_tests/langfuse_expected_request_body/complex_metadata_2.json +++ b/tests/logging_callback_tests/langfuse_expected_request_body/complex_metadata_2.json @@ -76,6 +76,12 @@ "output": 20, "unit": "TOKENS", "totalCost": 5.4999999999999995e-05 + }, + "usageDetails": { + "cache_creation_input_tokens": 0, + "cache_read_input_tokens": 0, + "input": 10, + "output": 20 } }, "timestamp": "2025-01-22T18:06:50.959850Z" diff --git a/tests/logging_callback_tests/langfuse_expected_request_body/empty_metadata.json b/tests/logging_callback_tests/langfuse_expected_request_body/empty_metadata.json index 69db4314f0f..1873d4a6ea1 100644 --- a/tests/logging_callback_tests/langfuse_expected_request_body/empty_metadata.json +++ b/tests/logging_callback_tests/langfuse_expected_request_body/empty_metadata.json @@ -70,6 +70,12 @@ "output": 20, "unit": "TOKENS", "totalCost": 5.4999999999999995e-05 + }, + "usageDetails": { + "cache_creation_input_tokens": 0, + "cache_read_input_tokens": 0, + "input": 10, + "output": 20 } }, "timestamp": "2025-01-22T17:59:32.889548Z" diff --git a/tests/logging_callback_tests/langfuse_expected_request_body/metadata_with_function.json b/tests/logging_callback_tests/langfuse_expected_request_body/metadata_with_function.json index eae3134555e..34e3c9f8daf 100644 --- a/tests/logging_callback_tests/langfuse_expected_request_body/metadata_with_function.json +++ b/tests/logging_callback_tests/langfuse_expected_request_body/metadata_with_function.json @@ -70,6 +70,12 @@ "output": 20, "unit": "TOKENS", "totalCost": 5.4999999999999995e-05 + }, + "usageDetails": { + "cache_creation_input_tokens": 0, + "cache_read_input_tokens": 0, + "input": 10, + "output": 20 } }, "timestamp": "2025-01-22T17:59:36.162997Z" diff --git a/tests/logging_callback_tests/langfuse_expected_request_body/metadata_with_lock.json b/tests/logging_callback_tests/langfuse_expected_request_body/metadata_with_lock.json index 69db4314f0f..1873d4a6ea1 100644 --- a/tests/logging_callback_tests/langfuse_expected_request_body/metadata_with_lock.json +++ b/tests/logging_callback_tests/langfuse_expected_request_body/metadata_with_lock.json @@ -70,6 +70,12 @@ "output": 20, "unit": "TOKENS", "totalCost": 5.4999999999999995e-05 + }, + "usageDetails": { + "cache_creation_input_tokens": 0, + "cache_read_input_tokens": 0, + "input": 10, + "output": 20 } }, "timestamp": "2025-01-22T17:59:32.889548Z" diff --git a/tests/logging_callback_tests/langfuse_expected_request_body/nested_metadata.json b/tests/logging_callback_tests/langfuse_expected_request_body/nested_metadata.json index c4fe594fa14..4d6ce12ec6d 100644 --- a/tests/logging_callback_tests/langfuse_expected_request_body/nested_metadata.json +++ b/tests/logging_callback_tests/langfuse_expected_request_body/nested_metadata.json @@ -76,6 +76,12 @@ "output": 20, "unit": "TOKENS", "totalCost": 5.4999999999999995e-05 + }, + "usageDetails": { + "cache_creation_input_tokens": 0, + "cache_read_input_tokens": 0, + "input": 10, + "output": 20 } }, "timestamp": "2025-01-22T17:55:28.855732Z" diff --git a/tests/logging_callback_tests/langfuse_expected_request_body/simple_metadata.json b/tests/logging_callback_tests/langfuse_expected_request_body/simple_metadata.json index d348ac50392..5e5edc795ec 100644 --- a/tests/logging_callback_tests/langfuse_expected_request_body/simple_metadata.json +++ b/tests/logging_callback_tests/langfuse_expected_request_body/simple_metadata.json @@ -76,6 +76,12 @@ "output": 20, "unit": "TOKENS", "totalCost": 5.4999999999999995e-05 + }, + "usageDetails": { + "cache_creation_input_tokens": 0, + "cache_read_input_tokens": 0, + "input": 10, + "output": 20 } }, "timestamp": "2025-01-22T17:53:53.754511Z" diff --git a/tests/logging_callback_tests/langfuse_expected_request_body/simple_metadata2.json b/tests/logging_callback_tests/langfuse_expected_request_body/simple_metadata2.json index d5468d302c9..7bbf4e4eeee 100644 --- a/tests/logging_callback_tests/langfuse_expected_request_body/simple_metadata2.json +++ b/tests/logging_callback_tests/langfuse_expected_request_body/simple_metadata2.json @@ -80,6 +80,12 @@ "output": 20, "unit": "TOKENS", "totalCost": 5.4999999999999995e-05 + }, + "usageDetails": { + "cache_creation_input_tokens": 0, + "cache_read_input_tokens": 0, + "input": 10, + "output": 20 } }, "timestamp": "2025-01-22T17:56:35.478171Z" diff --git a/tests/logging_callback_tests/langfuse_expected_request_body/simple_metadata3.json b/tests/logging_callback_tests/langfuse_expected_request_body/simple_metadata3.json index 1107766993a..fcdcb47aea5 100644 --- a/tests/logging_callback_tests/langfuse_expected_request_body/simple_metadata3.json +++ b/tests/logging_callback_tests/langfuse_expected_request_body/simple_metadata3.json @@ -84,6 +84,12 @@ "output": 20, "unit": "TOKENS", "totalCost": 5.4999999999999995e-05 + }, + "usageDetails": { + "cache_creation_input_tokens": 0, + "cache_read_input_tokens": 0, + "input": 10, + "output": 20 } }, "timestamp": "2025-01-22T17:56:38.787196Z" From e75d8b711e156d1528d560a53263288238cdba99 Mon Sep 17 00:00:00 2001 From: Alex Shoop Date: Thu, 25 Sep 2025 09:09:30 +0900 Subject: [PATCH 22/70] Fix: make `pondpond` as optional dependency for `proxy` extras, disable object pooling gracefully (#14863) * pondpond optional dep proxy extra * lock --- litellm/litellm_core_utils/object_pooling.py | 38 ++++++++++++++++--- poetry.lock | 10 +++-- pyproject.toml | 3 +- .../test_object_pooling.py | 3 +- 4 files changed, 43 insertions(+), 11 deletions(-) diff --git a/litellm/litellm_core_utils/object_pooling.py b/litellm/litellm_core_utils/object_pooling.py index 846e6536f80..81c3ec2e133 100644 --- a/litellm/litellm_core_utils/object_pooling.py +++ b/litellm/litellm_core_utils/object_pooling.py @@ -16,11 +16,35 @@ Memory Management Strategy: from typing import Any, Callable, Optional, Type, TypeVar -from pond import Pond, PooledObject, PooledObjectFactory +try: + from pond import Pond, PooledObject, PooledObjectFactory # type: ignore + POND_AVAILABLE = True +except ImportError: # pragma: no cover + POND_AVAILABLE = False + class Pond: # type: ignore + def __init__(self, *args: Any, **kwargs: Any) -> None: + pass + + def register(self, *args: Any, **kwargs: Any) -> None: + pass + + def borrow(self, *args: Any, **kwargs: Any) -> Any: + pass + + def recycle(self, *args: Any, **kwargs: Any) -> None: + pass + + class PooledObject: # type: ignore + def __init__(self, keeped_object: Any = None) -> None: + self.keeped_object = keeped_object + + class PooledObjectFactory: # type: ignore + def __init__(self, *args: Any, **kwargs: Any) -> None: + pass T = TypeVar('T') -class GenericPooledObjectFactory(PooledObjectFactory): +class GenericPooledObjectFactory(PooledObjectFactory): # type: ignore[misc] """Generic factory class for creating pooled objects of any type.""" def __init__( @@ -79,7 +103,7 @@ def get_object_pool( time_between_eviction_runs: int = 300, # Less frequent eviction to maintain high reuse ratio eviction_weight: float = 0.3, # Less aggressive eviction for better reuse prewarm_count: int = 5 # Lower pre-warm count to reduce initial memory usage -) -> Pond: +) -> Pond | None: """Get or create a global object pool instance with balanced eviction-based memory control. Memory is controlled through moderate eviction to balance reuse ratio and memory usage: @@ -98,9 +122,13 @@ def get_object_pool( prewarm_count: Number of objects to pre-warm the pool with (default: 5) Returns: - Pond instance for the specified object type + Pond instance for the specified object type or None if pond is not available """ + # If pond is not available, disable pooling gracefully + if not POND_AVAILABLE: + return None + if pool_name in _pools: return _pools[pool_name] @@ -134,4 +162,4 @@ def _prewarm_pool(pond: Pond, pool_name: str, prewarm_count: int = 20) -> None: pond.recycle(pooled_obj, name=f"{pool_name}Factory") except Exception: # If pre-warming fails, just continue - break \ No newline at end of file + break diff --git a/poetry.lock b/poetry.lock index a8ef9ee79df..87599768db6 100644 --- a/poetry.lock +++ b/poetry.lock @@ -2730,9 +2730,10 @@ files = [ name = "madoka" version = "0.7.1" description = "Memory-efficient CountMin Sketch key-value structure (based on Madoka C++ library)" -optional = false +optional = true python-versions = "*" groups = ["main"] +markers = "extra == \"proxy\"" files = [ {file = "madoka-0.7.1-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:7521eee9ace30b376bb54fdcb2cb42bf6b7a0346b0d0b612f25f3299aa4a95af"}, {file = "madoka-0.7.1.tar.gz", hash = "sha256:e258baa84fc0a3764365993b8bf5e1b065383a6ca8c9f862fb3e3e709843fae7"}, @@ -4044,9 +4045,10 @@ xlsxwriter = ["xlsxwriter"] name = "pondpond" version = "1.4.1" description = "Pond is a high performance object-pooling library for Python." -optional = false +optional = true python-versions = ">=3.8" groups = ["main"] +markers = "extra == \"proxy\"" files = [ {file = "pondpond-1.4.1-py3-none-any.whl", hash = "sha256:641028ead4e8018ca6de1220c660ddd6d6fbf62a60e72f410655dd0451d82880"}, {file = "pondpond-1.4.1.tar.gz", hash = "sha256:8afa34b869d1434d21dd2ec12644abc3b1733fcda8fcf355300338a13a79bb7b"}, @@ -6747,11 +6749,11 @@ type = ["pytest-mypy"] caching = ["diskcache"] extra-proxy = ["azure-identity", "azure-keyvault-secrets", "google-cloud-iam", "google-cloud-kms", "prisma", "redisvl", "resend"] mlflow = ["mlflow"] -proxy = ["PyJWT", "apscheduler", "azure-identity", "azure-storage-blob", "backoff", "boto3", "cryptography", "fastapi", "fastapi-sso", "fastuuid", "gunicorn", "litellm-enterprise", "litellm-proxy-extras", "mcp", "orjson", "polars", "pynacl", "python-multipart", "pyyaml", "rich", "rq", "uvicorn", "uvloop", "websockets"] +proxy = ["PyJWT", "apscheduler", "azure-identity", "azure-storage-blob", "backoff", "boto3", "cryptography", "fastapi", "fastapi-sso", "fastuuid", "gunicorn", "litellm-enterprise", "litellm-proxy-extras", "mcp", "orjson", "polars", "pondpond", "pynacl", "python-multipart", "pyyaml", "rich", "rq", "uvicorn", "uvloop", "websockets"] semantic-router = ["semantic-router"] utils = ["numpydoc"] [metadata] lock-version = "2.1" python-versions = ">=3.8.1,<4.0, !=3.9.7" -content-hash = "75004c6a23b70be86622fa417fd0d62fa3843e6e61c8dff8507ae5c967b7205d" +content-hash = "16fdc1044b4bb316803cbf1825bc970895526949a8bef93eab741777b7210ca8" diff --git a/pyproject.toml b/pyproject.toml index b11b1a1c2e2..4113714415c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -31,7 +31,7 @@ jinja2 = "^3.1.2" aiohttp = ">=3.10" pydantic = "^2.5.0" jsonschema = "^4.22.0" -pondpond = "^1.4.1" +pondpond = {version = "^1.4.1", optional = true} numpydoc = {version = "*", optional = true} # used in utils.py fastuuid = {version = ">=0.12.0", optional = true} @@ -94,6 +94,7 @@ proxy = [ "rich", "polars", "fastuuid", + "pondpond", ] extra_proxy = [ diff --git a/tests/litellm_utils_tests/test_object_pooling.py b/tests/litellm_utils_tests/test_object_pooling.py index 5cdc38ee452..8d0272e92b9 100644 --- a/tests/litellm_utils_tests/test_object_pooling.py +++ b/tests/litellm_utils_tests/test_object_pooling.py @@ -3,6 +3,7 @@ Simplified tests for object pooling utilities in litellm. """ import pytest +pytest.importorskip("pond") from litellm.litellm_core_utils.object_pooling import ( get_object_pool, @@ -103,4 +104,4 @@ class TestObjectPooling: if __name__ == "__main__": - pytest.main([__file__]) \ No newline at end of file + pytest.main([__file__]) From 1b95940d18a3c371dd58d007ba1f4943c334eb50 Mon Sep 17 00:00:00 2001 From: huangyafei Date: Thu, 25 Sep 2025 11:02:54 +0800 Subject: [PATCH 23/70] Add gpt-5 to OpenRouter cost map --- ...odel_prices_and_context_window_backup.json | 19 +++++++++++++++++++ model_prices_and_context_window.json | 19 +++++++++++++++++++ 2 files changed, 38 insertions(+) diff --git a/litellm/model_prices_and_context_window_backup.json b/litellm/model_prices_and_context_window_backup.json index 3742693ce67..4106d904ed2 100644 --- a/litellm/model_prices_and_context_window_backup.json +++ b/litellm/model_prices_and_context_window_backup.json @@ -16857,6 +16857,25 @@ "supports_reasoning": true, "supports_tool_choice": true }, + "openrouter/openai/gpt-5": { + "cache_read_input_token_cost": 1.25e-07, + "input_cost_per_token": 1.25e-06, + "litellm_provider": "openrouter", + "max_input_tokens": 400000, + "max_output_tokens": 128000, + "max_tokens": 128000, + "mode": "chat", + "output_cost_per_token": 1e-05, + "supported_modalities": [ + "text", + "image" + ], + "supported_output_modalities": [ + "text" + ], + "supports_reasoning": true, + "supports_tool_choice": true + }, "openrouter/openai/gpt-5-mini": { "cache_read_input_token_cost": 2.5e-08, "input_cost_per_token": 2.5e-07, diff --git a/model_prices_and_context_window.json b/model_prices_and_context_window.json index 3742693ce67..4106d904ed2 100644 --- a/model_prices_and_context_window.json +++ b/model_prices_and_context_window.json @@ -16857,6 +16857,25 @@ "supports_reasoning": true, "supports_tool_choice": true }, + "openrouter/openai/gpt-5": { + "cache_read_input_token_cost": 1.25e-07, + "input_cost_per_token": 1.25e-06, + "litellm_provider": "openrouter", + "max_input_tokens": 400000, + "max_output_tokens": 128000, + "max_tokens": 128000, + "mode": "chat", + "output_cost_per_token": 1e-05, + "supported_modalities": [ + "text", + "image" + ], + "supported_output_modalities": [ + "text" + ], + "supports_reasoning": true, + "supports_tool_choice": true + }, "openrouter/openai/gpt-5-mini": { "cache_read_input_token_cost": 2.5e-08, "input_cost_per_token": 2.5e-07, From 107c45da9dcefd92c723dbee6b9059ef4cbdee31 Mon Sep 17 00:00:00 2001 From: huangyafei Date: Thu, 25 Sep 2025 11:05:56 +0800 Subject: [PATCH 24/70] Add gpt-5-codex to OpenRouter cost map --- ...odel_prices_and_context_window_backup.json | 19 +++++++++++++++++++ model_prices_and_context_window.json | 19 +++++++++++++++++++ 2 files changed, 38 insertions(+) diff --git a/litellm/model_prices_and_context_window_backup.json b/litellm/model_prices_and_context_window_backup.json index 4106d904ed2..100ac8e91b1 100644 --- a/litellm/model_prices_and_context_window_backup.json +++ b/litellm/model_prices_and_context_window_backup.json @@ -16857,6 +16857,25 @@ "supports_reasoning": true, "supports_tool_choice": true }, + "openrouter/openai/gpt-5-codex": { + "cache_read_input_token_cost": 1.25e-07, + "input_cost_per_token": 1.25e-06, + "litellm_provider": "openrouter", + "max_input_tokens": 400000, + "max_output_tokens": 128000, + "max_tokens": 128000, + "mode": "chat", + "output_cost_per_token": 1e-05, + "supported_modalities": [ + "text", + "image" + ], + "supported_output_modalities": [ + "text" + ], + "supports_reasoning": true, + "supports_tool_choice": true + }, "openrouter/openai/gpt-5": { "cache_read_input_token_cost": 1.25e-07, "input_cost_per_token": 1.25e-06, diff --git a/model_prices_and_context_window.json b/model_prices_and_context_window.json index 4106d904ed2..100ac8e91b1 100644 --- a/model_prices_and_context_window.json +++ b/model_prices_and_context_window.json @@ -16857,6 +16857,25 @@ "supports_reasoning": true, "supports_tool_choice": true }, + "openrouter/openai/gpt-5-codex": { + "cache_read_input_token_cost": 1.25e-07, + "input_cost_per_token": 1.25e-06, + "litellm_provider": "openrouter", + "max_input_tokens": 400000, + "max_output_tokens": 128000, + "max_tokens": 128000, + "mode": "chat", + "output_cost_per_token": 1e-05, + "supported_modalities": [ + "text", + "image" + ], + "supported_output_modalities": [ + "text" + ], + "supports_reasoning": true, + "supports_tool_choice": true + }, "openrouter/openai/gpt-5": { "cache_read_input_token_cost": 1.25e-07, "input_cost_per_token": 1.25e-06, From d9bf6a8c53db9480ab33b00a7578535554681afb Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Wed, 24 Sep 2025 21:34:42 -0700 Subject: [PATCH 25/70] =?UTF-8?q?Revert=20"Fix:=20make=20`pondpond`=20as?= =?UTF-8?q?=20optional=20dependency=20for=20`proxy`=20extras,=20disab?= =?UTF-8?q?=E2=80=A6"=20(#14880)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit e75d8b711e156d1528d560a53263288238cdba99. --- litellm/litellm_core_utils/object_pooling.py | 38 +++---------------- poetry.lock | 10 ++--- pyproject.toml | 3 +- .../test_object_pooling.py | 3 +- 4 files changed, 11 insertions(+), 43 deletions(-) diff --git a/litellm/litellm_core_utils/object_pooling.py b/litellm/litellm_core_utils/object_pooling.py index 81c3ec2e133..846e6536f80 100644 --- a/litellm/litellm_core_utils/object_pooling.py +++ b/litellm/litellm_core_utils/object_pooling.py @@ -16,35 +16,11 @@ Memory Management Strategy: from typing import Any, Callable, Optional, Type, TypeVar -try: - from pond import Pond, PooledObject, PooledObjectFactory # type: ignore - POND_AVAILABLE = True -except ImportError: # pragma: no cover - POND_AVAILABLE = False - class Pond: # type: ignore - def __init__(self, *args: Any, **kwargs: Any) -> None: - pass - - def register(self, *args: Any, **kwargs: Any) -> None: - pass - - def borrow(self, *args: Any, **kwargs: Any) -> Any: - pass - - def recycle(self, *args: Any, **kwargs: Any) -> None: - pass - - class PooledObject: # type: ignore - def __init__(self, keeped_object: Any = None) -> None: - self.keeped_object = keeped_object - - class PooledObjectFactory: # type: ignore - def __init__(self, *args: Any, **kwargs: Any) -> None: - pass +from pond import Pond, PooledObject, PooledObjectFactory T = TypeVar('T') -class GenericPooledObjectFactory(PooledObjectFactory): # type: ignore[misc] +class GenericPooledObjectFactory(PooledObjectFactory): """Generic factory class for creating pooled objects of any type.""" def __init__( @@ -103,7 +79,7 @@ def get_object_pool( time_between_eviction_runs: int = 300, # Less frequent eviction to maintain high reuse ratio eviction_weight: float = 0.3, # Less aggressive eviction for better reuse prewarm_count: int = 5 # Lower pre-warm count to reduce initial memory usage -) -> Pond | None: +) -> Pond: """Get or create a global object pool instance with balanced eviction-based memory control. Memory is controlled through moderate eviction to balance reuse ratio and memory usage: @@ -122,13 +98,9 @@ def get_object_pool( prewarm_count: Number of objects to pre-warm the pool with (default: 5) Returns: - Pond instance for the specified object type or None if pond is not available + Pond instance for the specified object type """ - # If pond is not available, disable pooling gracefully - if not POND_AVAILABLE: - return None - if pool_name in _pools: return _pools[pool_name] @@ -162,4 +134,4 @@ def _prewarm_pool(pond: Pond, pool_name: str, prewarm_count: int = 20) -> None: pond.recycle(pooled_obj, name=f"{pool_name}Factory") except Exception: # If pre-warming fails, just continue - break + break \ No newline at end of file diff --git a/poetry.lock b/poetry.lock index 87599768db6..a8ef9ee79df 100644 --- a/poetry.lock +++ b/poetry.lock @@ -2730,10 +2730,9 @@ files = [ name = "madoka" version = "0.7.1" description = "Memory-efficient CountMin Sketch key-value structure (based on Madoka C++ library)" -optional = true +optional = false python-versions = "*" groups = ["main"] -markers = "extra == \"proxy\"" files = [ {file = "madoka-0.7.1-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:7521eee9ace30b376bb54fdcb2cb42bf6b7a0346b0d0b612f25f3299aa4a95af"}, {file = "madoka-0.7.1.tar.gz", hash = "sha256:e258baa84fc0a3764365993b8bf5e1b065383a6ca8c9f862fb3e3e709843fae7"}, @@ -4045,10 +4044,9 @@ xlsxwriter = ["xlsxwriter"] name = "pondpond" version = "1.4.1" description = "Pond is a high performance object-pooling library for Python." -optional = true +optional = false python-versions = ">=3.8" groups = ["main"] -markers = "extra == \"proxy\"" files = [ {file = "pondpond-1.4.1-py3-none-any.whl", hash = "sha256:641028ead4e8018ca6de1220c660ddd6d6fbf62a60e72f410655dd0451d82880"}, {file = "pondpond-1.4.1.tar.gz", hash = "sha256:8afa34b869d1434d21dd2ec12644abc3b1733fcda8fcf355300338a13a79bb7b"}, @@ -6749,11 +6747,11 @@ type = ["pytest-mypy"] caching = ["diskcache"] extra-proxy = ["azure-identity", "azure-keyvault-secrets", "google-cloud-iam", "google-cloud-kms", "prisma", "redisvl", "resend"] mlflow = ["mlflow"] -proxy = ["PyJWT", "apscheduler", "azure-identity", "azure-storage-blob", "backoff", "boto3", "cryptography", "fastapi", "fastapi-sso", "fastuuid", "gunicorn", "litellm-enterprise", "litellm-proxy-extras", "mcp", "orjson", "polars", "pondpond", "pynacl", "python-multipart", "pyyaml", "rich", "rq", "uvicorn", "uvloop", "websockets"] +proxy = ["PyJWT", "apscheduler", "azure-identity", "azure-storage-blob", "backoff", "boto3", "cryptography", "fastapi", "fastapi-sso", "fastuuid", "gunicorn", "litellm-enterprise", "litellm-proxy-extras", "mcp", "orjson", "polars", "pynacl", "python-multipart", "pyyaml", "rich", "rq", "uvicorn", "uvloop", "websockets"] semantic-router = ["semantic-router"] utils = ["numpydoc"] [metadata] lock-version = "2.1" python-versions = ">=3.8.1,<4.0, !=3.9.7" -content-hash = "16fdc1044b4bb316803cbf1825bc970895526949a8bef93eab741777b7210ca8" +content-hash = "75004c6a23b70be86622fa417fd0d62fa3843e6e61c8dff8507ae5c967b7205d" diff --git a/pyproject.toml b/pyproject.toml index 4113714415c..b11b1a1c2e2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -31,7 +31,7 @@ jinja2 = "^3.1.2" aiohttp = ">=3.10" pydantic = "^2.5.0" jsonschema = "^4.22.0" -pondpond = {version = "^1.4.1", optional = true} +pondpond = "^1.4.1" numpydoc = {version = "*", optional = true} # used in utils.py fastuuid = {version = ">=0.12.0", optional = true} @@ -94,7 +94,6 @@ proxy = [ "rich", "polars", "fastuuid", - "pondpond", ] extra_proxy = [ diff --git a/tests/litellm_utils_tests/test_object_pooling.py b/tests/litellm_utils_tests/test_object_pooling.py index 8d0272e92b9..5cdc38ee452 100644 --- a/tests/litellm_utils_tests/test_object_pooling.py +++ b/tests/litellm_utils_tests/test_object_pooling.py @@ -3,7 +3,6 @@ Simplified tests for object pooling utilities in litellm. """ import pytest -pytest.importorskip("pond") from litellm.litellm_core_utils.object_pooling import ( get_object_pool, @@ -104,4 +103,4 @@ class TestObjectPooling: if __name__ == "__main__": - pytest.main([__file__]) + pytest.main([__file__]) \ No newline at end of file From ba1cd3f0d25fc8529958df1584377b75a41c1d0e Mon Sep 17 00:00:00 2001 From: Ishaan Jaffer Date: Wed, 24 Sep 2025 21:35:53 -0700 Subject: [PATCH 26/70] Revert "feature: generic object pool (#14702)" This reverts commit 60800698f27438509693442a1026406595ff9c5d. --- litellm/litellm_core_utils/object_pooling.py | 137 ------------------ .../test_object_pooling.py | 106 -------------- 2 files changed, 243 deletions(-) delete mode 100644 litellm/litellm_core_utils/object_pooling.py delete mode 100644 tests/litellm_utils_tests/test_object_pooling.py diff --git a/litellm/litellm_core_utils/object_pooling.py b/litellm/litellm_core_utils/object_pooling.py deleted file mode 100644 index 846e6536f80..00000000000 --- a/litellm/litellm_core_utils/object_pooling.py +++ /dev/null @@ -1,137 +0,0 @@ -""" -Generic object pooling utilities for LiteLLM. - -This module provides a flexible object pooling system that can be used -to pool any type of object, reducing memory allocation overhead and -improving performance for frequently created/destroyed objects. - -Memory Management Strategy: -- Balanced eviction-based memory control to optimize reuse ratio -- Moderate eviction frequency (300s) to maintain high object reuse -- Conservative eviction weight (0.3) to avoid destroying useful objects -- Lower pre-warm count (5) to reduce initial memory footprint -- Always keeps at least one object available for high availability -- Unlimited pools when maxsize is not specified (eviction controls actual usage) -""" - -from typing import Any, Callable, Optional, Type, TypeVar - -from pond import Pond, PooledObject, PooledObjectFactory - -T = TypeVar('T') - -class GenericPooledObjectFactory(PooledObjectFactory): - """Generic factory class for creating pooled objects of any type.""" - - def __init__( - self, - object_class: Type[T], - pooled_maxsize: Optional[int] = None, # None = unlimited pool with eviction-based memory control - least_one: bool = True, # Always keep at least one for high concurrency - initializer: Optional[Callable[[T], None]] = None - ): - # Only pass maxsize to Pond if user specified it - otherwise let Pond handle unlimited pools - if pooled_maxsize is not None: - super().__init__(pooled_maxsize=pooled_maxsize, least_one=least_one) - else: - super().__init__(least_one=least_one) - self.object_class = object_class - self.initializer = initializer - self._user_maxsize = pooled_maxsize # Store original user preference - - def createInstance(self) -> PooledObject: - """Create a new instance wrapped in a PooledObject.""" - # Create a properly initialized instance - obj = self.object_class() - return PooledObject(obj) - - def destroy(self, pooled_object: PooledObject): - """Destroy the pooled object.""" - if hasattr(pooled_object.keeped_object, '__dict__'): - pooled_object.keeped_object.__dict__.clear() - del pooled_object - - def reset(self, pooled_object: PooledObject, **kwargs: Any) -> PooledObject: - """Reset the pooled object to a clean state.""" - obj = pooled_object.keeped_object - # Reset the object by calling its reset method if it exists - if hasattr(obj, 'reset') and callable(getattr(obj, 'reset')): - obj.reset() - else: - # Fallback: clear all attributes to reset the object - if hasattr(obj, '__dict__'): - obj.__dict__.clear() - return pooled_object - - def validate(self, pooled_object: PooledObject) -> bool: - """Validate if the pooled object is still usable.""" - return pooled_object.keeped_object is not None - -# Global pond instances -_pools: dict[str, Pond] = {} - -def get_object_pool( - pool_name: str, - object_class: Type[T], - pooled_maxsize: Optional[int] = None, # None = unlimited pool with eviction-based memory control - least_one: bool = True, # Always keep at least one - borrowed_timeout: int = 10, # Longer timeout for high concurrency - time_between_eviction_runs: int = 300, # Less frequent eviction to maintain high reuse ratio - eviction_weight: float = 0.3, # Less aggressive eviction for better reuse - prewarm_count: int = 5 # Lower pre-warm count to reduce initial memory usage -) -> Pond: - """Get or create a global object pool instance with balanced eviction-based memory control. - - Memory is controlled through moderate eviction to balance reuse ratio and memory usage: - - Moderate eviction frequency (300s) to maintain high object reuse ratio - - Conservative eviction weight (0.3) to avoid destroying useful objects - - Lower pre-warm count (5) to reduce initial memory footprint - - Args: - pool_name: Unique name for the pool - object_class: The class type to pool - pooled_maxsize: Maximum number of objects in the pool (None = truly unlimited) - least_one: Whether to keep at least one object in the pool (default: True) - borrowed_timeout: Timeout for borrowing objects (seconds, default: 10) - time_between_eviction_runs: Time between eviction runs (seconds, default: 300) - eviction_weight: Weight for eviction algorithm (default: 0.3, conservative) - prewarm_count: Number of objects to pre-warm the pool with (default: 5) - - Returns: - Pond instance for the specified object type - """ - - if pool_name in _pools: - return _pools[pool_name] - - # Create new pond - pond = Pond( - borrowed_timeout=borrowed_timeout, - time_between_eviction_runs=time_between_eviction_runs, - thread_daemon=True, - eviction_weight=eviction_weight - ) - - # Register the factory with user's maxsize preference - factory = GenericPooledObjectFactory( - object_class=object_class, - pooled_maxsize=pooled_maxsize, - least_one=least_one - ) - pond.register(factory, name=f"{pool_name}Factory") - - # Pre-warm the pool - _prewarm_pool(pond, pool_name, prewarm_count) - - _pools[pool_name] = pond - return pond - -def _prewarm_pool(pond: Pond, pool_name: str, prewarm_count: int = 20) -> None: - """Pre-warm the pool with initial objects for high concurrency.""" - for _ in range(prewarm_count): - try: - pooled_obj = pond.borrow(name=f"{pool_name}Factory") - pond.recycle(pooled_obj, name=f"{pool_name}Factory") - except Exception: - # If pre-warming fails, just continue - break \ No newline at end of file diff --git a/tests/litellm_utils_tests/test_object_pooling.py b/tests/litellm_utils_tests/test_object_pooling.py deleted file mode 100644 index 5cdc38ee452..00000000000 --- a/tests/litellm_utils_tests/test_object_pooling.py +++ /dev/null @@ -1,106 +0,0 @@ -""" -Simplified tests for object pooling utilities in litellm. -""" - -import pytest - -from litellm.litellm_core_utils.object_pooling import ( - get_object_pool, - _pools -) - - -class SimpleObject: - """Simple test object with internal reset tracking.""" - def __init__(self): - self.data = {} - self.reset_count = 0 - self.creation_id = id(self) - - def reset(self): - """Reset method that tracks how many times it's called.""" - self.data.clear() - self.reset_count += 1 - - def set_data(self, key, value): - """Set data to verify reset works.""" - self.data[key] = value - - -class SimpleObjectNoReset: - """Test object without reset method.""" - def __init__(self): - self.data = {} - self.creation_id = id(self) - - -class TestObjectPooling: - """Simplified test suite for object pooling.""" - - def setup_method(self): - """Clear pools before each test.""" - _pools.clear() - - def test_reset_method_works(self): - """Test that reset method is called when recycling objects.""" - pool_name = "reset_test" - pool = get_object_pool(pool_name, SimpleObject, pooled_maxsize=1, prewarm_count=0) - - # Get an object and modify it - obj = pool.borrow(name=f"{pool_name}Factory") - obj.keeped_object.set_data("test", "value") - initial_reset_count = obj.keeped_object.reset_count - - # Return to pool (should trigger reset) - pool.recycle(obj, name=f"{pool_name}Factory") - - # Get the same object back - obj2 = pool.borrow(name=f"{pool_name}Factory") - - # Verify reset was called - assert obj2.keeped_object.reset_count == initial_reset_count + 1 - assert obj2.keeped_object.data == {} # Data should be cleared - assert obj.keeped_object.creation_id == obj2.keeped_object.creation_id # Same object - - def test_fallback_reset_works(self): - """Test fallback reset when no reset method exists.""" - pool_name = "fallback_test" - pool = get_object_pool(pool_name, SimpleObjectNoReset, pooled_maxsize=1, prewarm_count=0) - - # Get an object and modify it - obj = pool.borrow(name=f"{pool_name}Factory") - obj.keeped_object.data["test"] = "value" - - # Return to pool (should trigger fallback reset) - pool.recycle(obj, name=f"{pool_name}Factory") - - # Get the same object back - obj2 = pool.borrow(name=f"{pool_name}Factory") - - # Verify fallback reset worked - all attributes should be cleared by __dict__.clear() - assert obj2.keeped_object.__dict__ == {}, "All attributes should be cleared by fallback reset" - assert obj is obj2, "Should be the same pooled object instance" - - def test_pool_reuses_objects(self): - """Test that pool actually reuses objects instead of creating new ones.""" - pool_name = "reuse_test" - pool = get_object_pool(pool_name, SimpleObject, pooled_maxsize=1, prewarm_count=0) - - # Get first object - obj1 = pool.borrow(name=f"{pool_name}Factory") - creation_id1 = obj1.keeped_object.creation_id - - # Return it - pool.recycle(obj1, name=f"{pool_name}Factory") - - # Get second object - obj2 = pool.borrow(name=f"{pool_name}Factory") - creation_id2 = obj2.keeped_object.creation_id - - # Should be the same object (reused) - assert creation_id1 == creation_id2, "Pool should reuse objects" - assert obj1.keeped_object is obj2.keeped_object, "Should be same object instance" - - -if __name__ == "__main__": - pytest.main([__file__]) \ No newline at end of file From a0e47077828b7e4631045f2c483918b9f751fa88 Mon Sep 17 00:00:00 2001 From: Ishaan Jaffer Date: Wed, 24 Sep 2025 21:36:57 -0700 Subject: [PATCH 27/70] Revert "Added missing dependencies (#14706)" This reverts commit b33d52c6e188ea5d115b1c5d4b0d7663d97ca478. --- pyproject.toml | 1 - requirements.txt | 1 - 2 files changed, 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index b11b1a1c2e2..cc45c35cdac 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -31,7 +31,6 @@ jinja2 = "^3.1.2" aiohttp = ">=3.10" pydantic = "^2.5.0" jsonschema = "^4.22.0" -pondpond = "^1.4.1" numpydoc = {version = "*", optional = true} # used in utils.py fastuuid = {version = ">=0.12.0", optional = true} diff --git a/requirements.txt b/requirements.txt index 41f4c646958..3c55f682a0f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -58,7 +58,6 @@ tenacity==8.2.3 # for retrying requests, when litellm.num_retries set pydantic==2.10.2 # proxy + openai req. jsonschema==4.22.0 # validating json schema websockets==13.1.0 # for realtime API -pondpond==1.4.1 # for object pooling ######################## # LITELLM ENTERPRISE DEPENDENCIES From ded823a3a3a3aec4eca4e6663ca3593016250684 Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Wed, 24 Sep 2025 21:37:56 -0700 Subject: [PATCH 28/70] [Feat] Initial support for scheduled key rotations (#14877) * add KeyRotationSettings * add KeyRotationManager * add key rotation manager * add KeyRotationManager * fix _initialize_spend_tracking_background_jobs * fix add_key_rotation_fields * fixes * new fields in schema * update schema * fix fixes for init key rotation manager * fixes for init key rotation manager * fix KeyRotationManager * tests for key rotation * fix key_rotation_settings * fix KeyRotationManager * fix LITELLM_KEY_ROTATION_CHECK_INTERVAL_SECONDS * fix fixes for init key rotation manager * ruff check fix * fix LITELLM_KEY_ROTATION_CHECK_INTERVAL_SECONDS * fix _initialize_spend_tracking_background_jobs * docs fix * docs auto rotate * schema fix * bump proxy extras * fix config.yml --- .circleci/config.yml | 3 +- docs/my-website/docs/proxy/config_settings.md | 2 + ...tellm_proxy_extras-0.2.20-py3-none-any.whl | Bin 0 -> 30470 bytes .../dist/litellm_proxy_extras-0.2.20.tar.gz | Bin 0 -> 15576 bytes .../litellm_proxy_extras/schema.prisma | 4 + litellm-proxy-extras/pyproject.toml | 4 +- litellm/constants.py | 4 + litellm/proxy/_types.py | 29 +++ .../common_utils/key_rotation_manager.py | 146 +++++++++++++ .../key_management_endpoints.py | 15 ++ litellm/proxy/proxy_config.yaml | 2 + litellm/proxy/proxy_server.py | 40 +++- litellm/proxy/schema.prisma | 4 + poetry.lock | 8 +- pyproject.toml | 2 +- requirements.txt | 2 +- schema.prisma | 4 + .../common_utils/test_key_rotation_manager.py | 194 ++++++++++++++++++ .../test_key_management_endpoints.py | 71 +++++++ 19 files changed, 523 insertions(+), 11 deletions(-) create mode 100644 litellm-proxy-extras/dist/litellm_proxy_extras-0.2.20-py3-none-any.whl create mode 100644 litellm-proxy-extras/dist/litellm_proxy_extras-0.2.20.tar.gz create mode 100644 litellm/proxy/common_utils/key_rotation_manager.py create mode 100644 tests/test_litellm/proxy/common_utils/test_key_rotation_manager.py diff --git a/.circleci/config.yml b/.circleci/config.yml index ef6445ca0ca..ba42ccbc362 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -148,7 +148,8 @@ jobs: python -m pip install types-requests types-setuptools types-redis types-PyYAML if ! python -m mypy . \ --config-file mypy.ini \ - --ignore-missing-imports; then + --ignore-missing-imports \ + --no-incremental; then echo "mypy detected errors" exit 1 fi diff --git a/docs/my-website/docs/proxy/config_settings.md b/docs/my-website/docs/proxy/config_settings.md index 974e95a07bd..4cd15e8ae43 100644 --- a/docs/my-website/docs/proxy/config_settings.md +++ b/docs/my-website/docs/proxy/config_settings.md @@ -613,6 +613,8 @@ router_settings: | LITELLM_MIGRATION_DIR | Custom migrations directory for prisma migrations, used for baselining db in read-only file systems. | LITELLM_HOSTED_UI | URL of the hosted UI for LiteLLM | LITELM_ENVIRONMENT | Environment of LiteLLM Instance, used by logging services. Currently only used by DeepEval. +| LITELLM_KEY_ROTATION_ENABLED | Enable auto-key rotation for LiteLLM (boolean). Default is false. +| LITELLM_KEY_ROTATION_CHECK_INTERVAL_SECONDS | Interval in seconds for how often to run job that auto-rotates keys. Default is 86400 (24 hours). | LITELLM_LICENSE | License key for LiteLLM usage | LITELLM_LOCAL_MODEL_COST_MAP | Local configuration for model cost mapping in LiteLLM | LITELLM_LOG | Enable detailed logging for LiteLLM diff --git a/litellm-proxy-extras/dist/litellm_proxy_extras-0.2.20-py3-none-any.whl b/litellm-proxy-extras/dist/litellm_proxy_extras-0.2.20-py3-none-any.whl new file mode 100644 index 0000000000000000000000000000000000000000..0a94ef6ff621531d9f9d3d4e398315f607384e97 GIT binary patch literal 30470 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 WRz|!2$GbVNetfP@emzb9`0XG0pa)+7 literal 0 HcmV?d00001 diff --git a/litellm-proxy-extras/dist/litellm_proxy_extras-0.2.20.tar.gz b/litellm-proxy-extras/dist/litellm_proxy_extras-0.2.20.tar.gz new file mode 100644 index 0000000000000000000000000000000000000000..1562aacc22b73a2db570b62ef49ebfe06c602535 GIT binary patch literal 15576 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 literal 0 HcmV?d00001 diff --git a/litellm-proxy-extras/litellm_proxy_extras/schema.prisma b/litellm-proxy-extras/litellm_proxy_extras/schema.prisma index 2b1e20820f9..9d3726d29d6 100644 --- a/litellm-proxy-extras/litellm_proxy_extras/schema.prisma +++ b/litellm-proxy-extras/litellm_proxy_extras/schema.prisma @@ -221,6 +221,10 @@ model LiteLLM_VerificationToken { created_by String? updated_at DateTime? @default(now()) @updatedAt @map("updated_at") updated_by String? + rotation_count Int? @default(0) // Number of times key has been rotated + auto_rotate Boolean? @default(false) // Whether this key should be auto-rotated + rotation_interval String? // How often to rotate (e.g., "30d", "90d") + last_rotation_at DateTime? // When this key was last rotated litellm_budget_table LiteLLM_BudgetTable? @relation(fields: [budget_id], references: [budget_id]) litellm_organization_table LiteLLM_OrganizationTable? @relation(fields: [organization_id], references: [organization_id]) object_permission LiteLLM_ObjectPermissionTable? @relation(fields: [object_permission_id], references: [object_permission_id]) diff --git a/litellm-proxy-extras/pyproject.toml b/litellm-proxy-extras/pyproject.toml index 1c368d58077..dd0b2138dcf 100644 --- a/litellm-proxy-extras/pyproject.toml +++ b/litellm-proxy-extras/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "litellm-proxy-extras" -version = "0.2.19" +version = "0.2.20" description = "Additional files for the LiteLLM Proxy. Reduces the size of the main litellm package." authors = ["BerriAI"] readme = "README.md" @@ -22,7 +22,7 @@ requires = ["poetry-core"] build-backend = "poetry.core.masonry.api" [tool.commitizen] -version = "0.2.19" +version = "0.2.20" version_files = [ "pyproject.toml:version", "../requirements.txt:litellm-proxy-extras==", diff --git a/litellm/constants.py b/litellm/constants.py index 6e70ae0671f..1ed9f237a29 100644 --- a/litellm/constants.py +++ b/litellm/constants.py @@ -989,7 +989,11 @@ HEALTH_CHECK_TIMEOUT_SECONDS = int( ) # 60 seconds LITTELM_INTERNAL_HEALTH_SERVICE_ACCOUNT_NAME = "litellm-internal-health-check" LITTELM_CLI_SERVICE_ACCOUNT_NAME = "litellm-cli" +LITELLM_INTERNAL_JOBS_SERVICE_ACCOUNT_NAME = "litellm_internal_jobs" +# Key Rotation Constants +LITELLM_KEY_ROTATION_ENABLED = os.getenv("LITELLM_KEY_ROTATION_ENABLED", "false") +LITELLM_KEY_ROTATION_CHECK_INTERVAL_SECONDS = int(os.getenv("LITELLM_KEY_ROTATION_CHECK_INTERVAL_SECONDS", 86400)) # 24 hours default UI_SESSION_TOKEN_TEAM_ID = "litellm-dashboard" LITELLM_PROXY_ADMIN_NAME = "default_user_id" diff --git a/litellm/proxy/_types.py b/litellm/proxy/_types.py index a92b7d8ad2e..eea370fa224 100644 --- a/litellm/proxy/_types.py +++ b/litellm/proxy/_types.py @@ -787,6 +787,14 @@ class GenerateKeyRequest(KeyRequestBase): default=LiteLLMKeyType.DEFAULT, description="Type of key that determines default allowed routes.", ) + auto_rotate: Optional[bool] = Field( + default=False, + description="Whether this key should be automatically rotated" + ) + rotation_interval: Optional[str] = Field( + default=None, + description="How often to rotate this key (e.g., '30d', '90d'). Required if auto_rotate=True" + ) class GenerateKeyResponse(KeyRequestBase): @@ -1802,6 +1810,10 @@ class LiteLLM_VerificationToken(LiteLLMPydanticObjectBase): updated_by: Optional[str] = None object_permission_id: Optional[str] = None object_permission: Optional[LiteLLM_ObjectPermissionTable] = None + rotation_count: Optional[int] = 0 # Number of times key has been rotated + auto_rotate: Optional[bool] = False # Whether this key should be auto-rotated + rotation_interval: Optional[str] = None # How often to rotate (e.g., "30d", "90d") + last_rotation_at: Optional[datetime] = None # When this key was last rotated model_config = ConfigDict(protected_namespaces=()) @@ -1932,6 +1944,23 @@ class UserAPIKeyAuth( key_alias=LITTELM_CLI_SERVICE_ACCOUNT_NAME, team_alias=LITTELM_CLI_SERVICE_ACCOUNT_NAME, ) + + @classmethod + def get_litellm_internal_jobs_user_api_key_auth(cls) -> "UserAPIKeyAuth": + """ + Returns a `UserAPIKeyAuth` object for internal LiteLLM jobs like key rotation. + + This is used to track actions performed by automated system jobs. + """ + from litellm.constants import LITELLM_INTERNAL_JOBS_SERVICE_ACCOUNT_NAME + + return cls( + api_key=LITELLM_INTERNAL_JOBS_SERVICE_ACCOUNT_NAME, + team_id="system", + key_alias=LITELLM_INTERNAL_JOBS_SERVICE_ACCOUNT_NAME, + team_alias="system", + user_id="system", + ) class UserInfoResponse(LiteLLMPydanticObjectBase): diff --git a/litellm/proxy/common_utils/key_rotation_manager.py b/litellm/proxy/common_utils/key_rotation_manager.py new file mode 100644 index 00000000000..319d5ed5233 --- /dev/null +++ b/litellm/proxy/common_utils/key_rotation_manager.py @@ -0,0 +1,146 @@ +""" +Key Rotation Manager - Automated key rotation based on rotation schedules + +Handles finding keys that need rotation based on their individual schedules. +""" + +from datetime import datetime, timedelta, timezone +from typing import List + +from litellm._logging import verbose_proxy_logger +from litellm.constants import LITELLM_INTERNAL_JOBS_SERVICE_ACCOUNT_NAME +from litellm.proxy._types import ( + GenerateKeyResponse, + LiteLLM_VerificationToken, + RegenerateKeyRequest, +) +from litellm.proxy.hooks.key_management_event_hooks import KeyManagementEventHooks +from litellm.proxy.management_endpoints.key_management_endpoints import ( + regenerate_key_fn, +) +from litellm.proxy.utils import PrismaClient + + +class KeyRotationManager: + """ + Manages automated key rotation based on individual key rotation schedules. + """ + + def __init__(self, prisma_client: PrismaClient): + self.prisma_client = prisma_client + + async def process_rotations(self): + """ + Main entry point - find and rotate keys that are due for rotation + """ + try: + verbose_proxy_logger.info("Starting scheduled key rotation check...") + + # Find keys that are due for rotation + keys_to_rotate = await self._find_keys_needing_rotation() + + if not keys_to_rotate: + verbose_proxy_logger.debug("No keys are due for rotation at this time") + return + + verbose_proxy_logger.info(f"Found {len(keys_to_rotate)} keys due for rotation") + + # Rotate each key + for key in keys_to_rotate: + try: + await self._rotate_key(key) + key_identifier = key.key_name or (key.token[:8] + "..." if key.token else "unknown") + verbose_proxy_logger.info(f"Successfully rotated key: {key_identifier}") + except Exception as e: + key_identifier = key.key_name or (key.token[:8] + "..." if key.token else "unknown") + verbose_proxy_logger.error(f"Failed to rotate key {key_identifier}: {e}") + + except Exception as e: + verbose_proxy_logger.error(f"Key rotation process failed: {e}") + + async def _find_keys_needing_rotation(self) -> List[LiteLLM_VerificationToken]: + """ + Find keys that are due for rotation based on their rotation interval. + + Logic: + - Key has auto_rotate = true + - Key has rotation_interval set + - Either: never been rotated (last_rotation_at is null) OR + - Time since last rotation >= rotation_interval + """ + keys_with_rotation = await self.prisma_client.db.litellm_verificationtoken.find_many( + where={ + "auto_rotate": True, # Only keys marked for auto rotation + "rotation_interval": {"not": None} # Must have rotation interval set + } + ) + + # Filter keys that need rotation based on last_rotation_at + interval + keys_needing_rotation = [] + now = datetime.now(timezone.utc) + + for key in keys_with_rotation: + if self._should_rotate_key(key, now): + keys_needing_rotation.append(key) + + return keys_needing_rotation + + def _should_rotate_key(self, key: LiteLLM_VerificationToken, now: datetime) -> bool: + """ + Determine if a key should be rotated based on last rotation time and interval. + """ + if not key.rotation_interval: + return False + + # If never rotated, rotate immediately + if key.last_rotation_at is None: + return True + + # Calculate if enough time has passed since last rotation + from litellm.litellm_core_utils.duration_parser import duration_in_seconds + + interval_seconds = duration_in_seconds(key.rotation_interval) + next_rotation_time = key.last_rotation_at + timedelta(seconds=interval_seconds) + + return now >= next_rotation_time + + async def _rotate_key(self, key: LiteLLM_VerificationToken): + """ + Rotate a single key using existing regenerate_key_fn and call the rotation hook + """ + # Create regenerate request + regenerate_request = RegenerateKeyRequest( + key=key.token or "" + ) + + # Create a system user for key rotation + from litellm.proxy._types import UserAPIKeyAuth + system_user = UserAPIKeyAuth.get_litellm_internal_jobs_user_api_key_auth() + + # Use existing regenerate key function + response = await regenerate_key_fn( + data=regenerate_request, + user_api_key_dict=system_user, + litellm_changed_by=LITELLM_INTERNAL_JOBS_SERVICE_ACCOUNT_NAME + ) + + # Update the NEW key with rotation info (regenerate_key_fn creates a new token) + if isinstance(response, GenerateKeyResponse) and response.token_id: + await self.prisma_client.db.litellm_verificationtoken.update( + where={"token": response.token_id}, + data={ + "rotation_count": (key.rotation_count or 0) + 1, + "last_rotation_at": datetime.now(timezone.utc) + } + ) + + # Call the existing rotation hook for notifications, audit logs, etc. + if isinstance(response, GenerateKeyResponse): + await KeyManagementEventHooks.async_key_rotated_hook( + data=regenerate_request, + existing_key_row=key, + response=response, + user_api_key_dict=system_user, + litellm_changed_by=LITELLM_INTERNAL_JOBS_SERVICE_ACCOUNT_NAME + ) + \ No newline at end of file diff --git a/litellm/proxy/management_endpoints/key_management_endpoints.py b/litellm/proxy/management_endpoints/key_management_endpoints.py index eb389450f91..363cbf7182c 100644 --- a/litellm/proxy/management_endpoints/key_management_endpoints.py +++ b/litellm/proxy/management_endpoints/key_management_endpoints.py @@ -642,6 +642,9 @@ async def generate_key_fn( - object_permission: Optional[LiteLLM_ObjectPermissionBase] - key-specific object permission. Example - {"vector_stores": ["vector_store_1", "vector_store_2"]}. IF null or {} then no object permission. - key_type: Optional[str] - Type of key that determines default allowed routes. Options: "llm_api" (can call LLM API routes), "management" (can call management routes), "read_only" (can only call info/read routes), "default" (uses default allowed routes). Defaults to "default". - prompts: Optional[List[str]] - List of allowed prompts for the key. If specified, the key will only be able to use these specific prompts. + - auto_rotate: Optional[bool] - Whether this key should be automatically rotated (regenerated) + - rotation_interval: Optional[str] - How often to auto-rotate this key (e.g., '30s', '30m', '30h', '30d'). Required if auto_rotate=True. + Examples: 1. Allow users to turn on/off pii masking @@ -1558,6 +1561,8 @@ def _check_model_access_group( return True + + async def generate_key_helper_fn( # noqa: PLR0915 request_type: Literal[ "user", "key" @@ -1611,6 +1616,8 @@ async def generate_key_helper_fn( # noqa: PLR0915 str ] = None, # object_permission_id <-> LiteLLM_ObjectPermissionTable object_permission: Optional[LiteLLM_ObjectPermissionBase] = None, + auto_rotate: Optional[bool] = None, + rotation_interval: Optional[str] = None, ): from litellm.proxy.proxy_server import premium_user, prisma_client @@ -1718,6 +1725,14 @@ async def generate_key_helper_fn( # noqa: PLR0915 "allowed_routes": allowed_routes or [], "object_permission_id": object_permission_id, } + + # Add rotation fields if auto_rotate is enabled + if auto_rotate and rotation_interval: + key_data.update({ + "auto_rotate": auto_rotate, + "rotation_interval": rotation_interval + # last_rotation_at will be null initially - rotation happens on first check + }) if ( get_secret("DISABLE_KEY_NAME", False) is True diff --git a/litellm/proxy/proxy_config.yaml b/litellm/proxy/proxy_config.yaml index 921b564407f..b9cd33175e7 100644 --- a/litellm/proxy/proxy_config.yaml +++ b/litellm/proxy/proxy_config.yaml @@ -36,3 +36,5 @@ guardrails: breakdown: true payload: true dev_info: true + +# proxy_config.yaml \ No newline at end of file diff --git a/litellm/proxy/proxy_server.py b/litellm/proxy/proxy_server.py index 472aeb140cf..399cadac24b 100644 --- a/litellm/proxy/proxy_server.py +++ b/litellm/proxy/proxy_server.py @@ -35,13 +35,13 @@ from litellm.constants import ( LITELLM_SETTINGS_SAFE_DB_OVERRIDES, ) from litellm.litellm_core_utils.safe_json_dumps import safe_dumps -from litellm.utils import load_credentials_from_list from litellm.types.utils import ( ModelResponse, ModelResponseStream, TextCompletionResponse, TokenCountResponse, ) +from litellm.utils import load_credentials_from_list if TYPE_CHECKING: from opentelemetry.trace import Span as _Span @@ -3802,9 +3802,10 @@ class ProxyStartupEvent: cls, scheduler: AsyncIOScheduler ): """ - Initialize the spend tracking background jobs + Initialize the spend tracking and other background jobs 1. CloudZero Background Job 2. Prometheus Background Job + 3. Key Rotation Background Job Args: scheduler: The scheduler to add the background jobs to @@ -3829,6 +3830,41 @@ class ProxyStartupEvent: except Exception: PrometheusLogger = None + ######################################################## + # Key Rotation Background Job + ######################################################## + from litellm.constants import ( + LITELLM_KEY_ROTATION_CHECK_INTERVAL_SECONDS, + LITELLM_KEY_ROTATION_ENABLED, + ) + + key_rotation_enabled: Optional[bool] = str_to_bool(LITELLM_KEY_ROTATION_ENABLED) + verbose_proxy_logger.debug(f"key_rotation_enabled: {key_rotation_enabled}") + + if key_rotation_enabled is True: + try: + from litellm.proxy.common_utils.key_rotation_manager import ( + KeyRotationManager, + ) + + # Get prisma_client from global scope + global prisma_client + if prisma_client is not None: + key_rotation_manager = KeyRotationManager(prisma_client) + verbose_proxy_logger.debug(f"Key rotation background job scheduled every {LITELLM_KEY_ROTATION_CHECK_INTERVAL_SECONDS} seconds (LITELLM_KEY_ROTATION_ENABLED=true)") + scheduler.add_job( + key_rotation_manager.process_rotations, + "interval", + seconds=LITELLM_KEY_ROTATION_CHECK_INTERVAL_SECONDS, + id="key_rotation_job" + ) + else: + verbose_proxy_logger.warning("Key rotation enabled but prisma_client not available") + except Exception as e: + verbose_proxy_logger.warning(f"Failed to setup key rotation job: {e}") + else: + verbose_proxy_logger.debug("Key rotation disabled (set LITELLM_KEY_ROTATION_ENABLED=true to enable)") + @classmethod async def _setup_prisma_client( cls, diff --git a/litellm/proxy/schema.prisma b/litellm/proxy/schema.prisma index 2b1e20820f9..9d3726d29d6 100644 --- a/litellm/proxy/schema.prisma +++ b/litellm/proxy/schema.prisma @@ -221,6 +221,10 @@ model LiteLLM_VerificationToken { created_by String? updated_at DateTime? @default(now()) @updatedAt @map("updated_at") updated_by String? + rotation_count Int? @default(0) // Number of times key has been rotated + auto_rotate Boolean? @default(false) // Whether this key should be auto-rotated + rotation_interval String? // How often to rotate (e.g., "30d", "90d") + last_rotation_at DateTime? // When this key was last rotated litellm_budget_table LiteLLM_BudgetTable? @relation(fields: [budget_id], references: [budget_id]) litellm_organization_table LiteLLM_OrganizationTable? @relation(fields: [organization_id], references: [organization_id]) object_permission LiteLLM_ObjectPermissionTable? @relation(fields: [object_permission_id], references: [object_permission_id]) diff --git a/poetry.lock b/poetry.lock index a8ef9ee79df..5ce6b0af007 100644 --- a/poetry.lock +++ b/poetry.lock @@ -2715,15 +2715,15 @@ files = [ [[package]] name = "litellm-proxy-extras" -version = "0.2.19" +version = "0.2.20" description = "Additional files for the LiteLLM Proxy. Reduces the size of the main litellm package." optional = true python-versions = "!=2.7.*,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,!=3.7.*,>=3.8" groups = ["main"] markers = "extra == \"proxy\"" files = [ - {file = "litellm_proxy_extras-0.2.19-py3-none-any.whl", hash = "sha256:cd4cc5fc639ac24bc99af70b8b56b7cc0480b0e72a2a53638f557beb7e9f64fd"}, - {file = "litellm_proxy_extras-0.2.19.tar.gz", hash = "sha256:e53592e39eb0b9c3b6cd32f29e6a0a53be51d7ad31dedfb69b58c24073b5b3ec"}, + {file = "litellm_proxy_extras-0.2.20-py3-none-any.whl", hash = "sha256:80ebe03be210eed99ddab0bf8e1a14169f24a502ecdaa98f4bb5032ebe65a944"}, + {file = "litellm_proxy_extras-0.2.20.tar.gz", hash = "sha256:8dea657f965122490be480327995ce534e75dc96778cb3e05c562c7996f85cce"}, ] [[package]] @@ -6754,4 +6754,4 @@ utils = ["numpydoc"] [metadata] lock-version = "2.1" python-versions = ">=3.8.1,<4.0, !=3.9.7" -content-hash = "75004c6a23b70be86622fa417fd0d62fa3843e6e61c8dff8507ae5c967b7205d" +content-hash = "9e20685127a280c3a2b42c2626490ee0af32c8716d7d78b72cd86c8c0308a088" diff --git a/pyproject.toml b/pyproject.toml index cc45c35cdac..5d4605ed5b4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -59,7 +59,7 @@ websockets = {version = "^13.1.0", optional = true} boto3 = {version = "1.36.0", optional = true} redisvl = {version = "^0.4.1", optional = true, markers = "python_version >= '3.9' and python_version < '3.14'"} mcp = {version = "^1.10.0", optional = true, python = ">=3.10"} -litellm-proxy-extras = {version = "0.2.19", optional = true} +litellm-proxy-extras = {version = "0.2.20", optional = true} rich = {version = "13.7.1", optional = true} litellm-enterprise = {version = "0.1.20", optional = true} diskcache = {version = "^5.6.1", optional = true} diff --git a/requirements.txt b/requirements.txt index 3c55f682a0f..10929ab819b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -43,7 +43,7 @@ sentry_sdk==2.21.0 # for sentry error handling detect-secrets==1.5.0 # Enterprise - secret detection / masking in LLM requests cryptography==43.0.1 tzdata==2025.1 # IANA time zone database -litellm-proxy-extras==0.2.19 # for proxy extras - e.g. prisma migrations +litellm-proxy-extras==0.2.20 # for proxy extras - e.g. prisma migrations ### LITELLM PACKAGE DEPENDENCIES python-dotenv==1.0.1 # for env tiktoken==0.8.0 # for calculating usage diff --git a/schema.prisma b/schema.prisma index 2b1e20820f9..9d3726d29d6 100644 --- a/schema.prisma +++ b/schema.prisma @@ -221,6 +221,10 @@ model LiteLLM_VerificationToken { created_by String? updated_at DateTime? @default(now()) @updatedAt @map("updated_at") updated_by String? + rotation_count Int? @default(0) // Number of times key has been rotated + auto_rotate Boolean? @default(false) // Whether this key should be auto-rotated + rotation_interval String? // How often to rotate (e.g., "30d", "90d") + last_rotation_at DateTime? // When this key was last rotated litellm_budget_table LiteLLM_BudgetTable? @relation(fields: [budget_id], references: [budget_id]) litellm_organization_table LiteLLM_OrganizationTable? @relation(fields: [organization_id], references: [organization_id]) object_permission LiteLLM_ObjectPermissionTable? @relation(fields: [object_permission_id], references: [object_permission_id]) diff --git a/tests/test_litellm/proxy/common_utils/test_key_rotation_manager.py b/tests/test_litellm/proxy/common_utils/test_key_rotation_manager.py new file mode 100644 index 00000000000..8aab67bf588 --- /dev/null +++ b/tests/test_litellm/proxy/common_utils/test_key_rotation_manager.py @@ -0,0 +1,194 @@ +""" +Test key rotation manager functionality +""" +import os +import sys +from datetime import datetime, timedelta, timezone +from unittest.mock import AsyncMock, MagicMock + +import pytest + +sys.path.insert(0, os.path.abspath("../../../..")) + +from litellm.proxy._types import ( + GenerateKeyResponse, + LiteLLM_VerificationToken, +) +from litellm.proxy.common_utils.key_rotation_manager import KeyRotationManager + + +class TestKeyRotationManager: + """Test the KeyRotationManager class functionality.""" + + @pytest.mark.asyncio + async def test_should_rotate_key_logic(self): + """ + Test the core logic for determining when a key should be rotated. + + This tests: + - Keys with null last_rotation_at should rotate immediately + - Keys with recent rotation should not rotate + - Keys with old rotation should rotate + """ + # Setup + mock_prisma_client = AsyncMock() + manager = KeyRotationManager(mock_prisma_client) + + now = datetime.now(timezone.utc) + + # Test Case 1: Never rotated (last_rotation_at = None) - should rotate + key_never_rotated = LiteLLM_VerificationToken( + token="test-token-1", + auto_rotate=True, + rotation_interval="30s", + last_rotation_at=None, + rotation_count=0 + ) + + assert manager._should_rotate_key(key_never_rotated, now) == True + + # Test Case 2: Recently rotated (10s ago, interval 30s) - should NOT rotate + key_recently_rotated = LiteLLM_VerificationToken( + token="test-token-2", + auto_rotate=True, + rotation_interval="30s", + last_rotation_at=now - timedelta(seconds=10), + rotation_count=1 + ) + + assert manager._should_rotate_key(key_recently_rotated, now) == False + + # Test Case 3: Old rotation (60s ago, interval 30s) - should rotate + key_old_rotation = LiteLLM_VerificationToken( + token="test-token-3", + auto_rotate=True, + rotation_interval="30s", + last_rotation_at=now - timedelta(seconds=60), + rotation_count=2 + ) + + assert manager._should_rotate_key(key_old_rotation, now) == True + + # Test Case 4: No rotation interval - should NOT rotate + key_no_interval = LiteLLM_VerificationToken( + token="test-token-4", + auto_rotate=True, + rotation_interval=None, + last_rotation_at=None, + rotation_count=0 + ) + + assert manager._should_rotate_key(key_no_interval, now) == False + + @pytest.mark.asyncio + async def test_find_keys_needing_rotation(self): + """ + Test finding keys that need rotation from database. + + This tests: + - Only keys with auto_rotate=True and rotation_interval are considered + - Filtering logic works correctly + - Database query is constructed properly + """ + # Setup + mock_prisma_client = AsyncMock() + manager = KeyRotationManager(mock_prisma_client) + + now = datetime.now(timezone.utc) + + # Mock database response + mock_keys = [ + LiteLLM_VerificationToken( + token="token-1", + auto_rotate=True, + rotation_interval="30s", + last_rotation_at=None, # Should rotate + rotation_count=0 + ), + LiteLLM_VerificationToken( + token="token-2", + auto_rotate=True, + rotation_interval="60s", + last_rotation_at=now - timedelta(seconds=30), # Should NOT rotate (30s < 60s) + rotation_count=1 + ), + LiteLLM_VerificationToken( + token="token-3", + auto_rotate=True, + rotation_interval="30s", + last_rotation_at=now - timedelta(seconds=45), # Should rotate (45s > 30s) + rotation_count=2 + ) + ] + + mock_prisma_client.db.litellm_verificationtoken.find_many.return_value = mock_keys + + # Execute + keys_needing_rotation = await manager._find_keys_needing_rotation() + + # Verify database query + mock_prisma_client.db.litellm_verificationtoken.find_many.assert_called_once_with( + where={ + "auto_rotate": True, + "rotation_interval": {"not": None} + } + ) + + # Verify filtering logic + assert len(keys_needing_rotation) == 2 # token-1 and token-3 should need rotation + + tokens_needing_rotation = [key.token for key in keys_needing_rotation] + assert "token-1" in tokens_needing_rotation # Never rotated + assert "token-2" not in tokens_needing_rotation # Recently rotated + assert "token-3" in tokens_needing_rotation # Old rotation + + @pytest.mark.asyncio + async def test_rotate_key_updates_database(self): + """ + Test that key rotation properly updates the database with new rotation info. + + This tests: + - Rotation count is incremented + - last_rotation_at is set to current time + - New key token is updated (not old one) + """ + # Setup + mock_prisma_client = AsyncMock() + manager = KeyRotationManager(mock_prisma_client) + + # Mock key to rotate + key_to_rotate = LiteLLM_VerificationToken( + token="old-token", + auto_rotate=True, + rotation_interval="30s", + last_rotation_at=None, + rotation_count=0 + ) + + # Mock regenerate_key_fn response + mock_response = GenerateKeyResponse( + key="new-api-key", + token_id="new-token-id", + user_id="test-user" + ) + + # Mock the regenerate function + from unittest.mock import patch + with patch('litellm.proxy.common_utils.key_rotation_manager.regenerate_key_fn', return_value=mock_response): + with patch('litellm.proxy.common_utils.key_rotation_manager.KeyManagementEventHooks.async_key_rotated_hook'): + # Execute + await manager._rotate_key(key_to_rotate) + + # Verify database update was called with correct data + mock_prisma_client.db.litellm_verificationtoken.update.assert_called_once() + + call_args = mock_prisma_client.db.litellm_verificationtoken.update.call_args + + # Check the WHERE clause targets the new token + assert call_args[1]["where"]["token"] == "new-token-id" + + # Check the data being updated + update_data = call_args[1]["data"] + assert update_data["rotation_count"] == 1 # Incremented from 0 + assert "last_rotation_at" in update_data + assert isinstance(update_data["last_rotation_at"], datetime) diff --git a/tests/test_litellm/proxy/management_endpoints/test_key_management_endpoints.py b/tests/test_litellm/proxy/management_endpoints/test_key_management_endpoints.py index da64a866d51..14b5833f14f 100644 --- a/tests/test_litellm/proxy/management_endpoints/test_key_management_endpoints.py +++ b/tests/test_litellm/proxy/management_endpoints/test_key_management_endpoints.py @@ -23,6 +23,7 @@ from litellm.proxy.auth.user_api_key_auth import UserAPIKeyAuth from litellm.proxy.management_endpoints.key_management_endpoints import ( _common_key_generation_helper, _list_key_helper, + generate_key_helper_fn, prepare_key_update_data, validate_key_team_change, ) @@ -1091,3 +1092,73 @@ def test_validate_key_team_change_with_member_permissions(): team_table=mock_team, route=KeyManagementRoutes.KEY_UPDATE.value ) + + +def test_key_rotation_fields_helper(): + """ + Test the key data update logic for rotation fields. + + This test focuses on the core logic that adds rotation fields to key_data + when auto_rotate is enabled, without the complexity of full key generation. + """ + # Test Case 1: With rotation enabled + key_data = { + "models": ["gpt-3.5-turbo"], + "user_id": "test-user" + } + + auto_rotate = True + rotation_interval = "30d" + + # Simulate the rotation logic from generate_key_helper_fn + if auto_rotate and rotation_interval: + key_data.update({ + "auto_rotate": auto_rotate, + "rotation_interval": rotation_interval + }) + + # Verify rotation fields are added + assert key_data["auto_rotate"] == True + assert key_data["rotation_interval"] == "30d" + assert key_data["models"] == ["gpt-3.5-turbo"] # Original fields preserved + + # Test Case 2: Without rotation enabled + key_data2 = { + "models": ["gpt-4"], + "user_id": "test-user" + } + + auto_rotate2 = False + rotation_interval2 = None + + # Simulate the rotation logic + if auto_rotate2 and rotation_interval2: + key_data2.update({ + "auto_rotate": auto_rotate2, + "rotation_interval": rotation_interval2 + }) + + # Verify rotation fields are NOT added + assert "auto_rotate" not in key_data2 + assert "rotation_interval" not in key_data2 + assert key_data2["models"] == ["gpt-4"] # Original fields preserved + + # Test Case 3: auto_rotate=True but no interval + key_data3 = { + "models": ["claude-3"], + "user_id": "test-user" + } + + auto_rotate3 = True + rotation_interval3 = None + + # Simulate the rotation logic + if auto_rotate3 and rotation_interval3: + key_data3.update({ + "auto_rotate": auto_rotate3, + "rotation_interval": rotation_interval3 + }) + + # Verify rotation fields are NOT added (missing interval) + assert "auto_rotate" not in key_data3 + assert "rotation_interval" not in key_data3 From c4838e730cd8016fdbd39af5b4c8da44277488a0 Mon Sep 17 00:00:00 2001 From: Ishaan Jaffer Date: Wed, 24 Sep 2025 21:38:58 -0700 Subject: [PATCH 29/70] bump v extras --- poetry.lock | 20 ++++---------------- 1 file changed, 4 insertions(+), 16 deletions(-) diff --git a/poetry.lock b/poetry.lock index 5ce6b0af007..9677775ad22 100644 --- a/poetry.lock +++ b/poetry.lock @@ -2724,6 +2724,7 @@ markers = "extra == \"proxy\"" files = [ {file = "litellm_proxy_extras-0.2.20-py3-none-any.whl", hash = "sha256:80ebe03be210eed99ddab0bf8e1a14169f24a502ecdaa98f4bb5032ebe65a944"}, {file = "litellm_proxy_extras-0.2.20.tar.gz", hash = "sha256:8dea657f965122490be480327995ce534e75dc96778cb3e05c562c7996f85cce"}, +<<<<<<< HEAD ] [[package]] @@ -2736,6 +2737,8 @@ groups = ["main"] files = [ {file = "madoka-0.7.1-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:7521eee9ace30b376bb54fdcb2cb42bf6b7a0346b0d0b612f25f3299aa4a95af"}, {file = "madoka-0.7.1.tar.gz", hash = "sha256:e258baa84fc0a3764365993b8bf5e1b065383a6ca8c9f862fb3e3e709843fae7"}, +======= +>>>>>>> 0cb68da91d (bump v extras) ] [[package]] @@ -4040,21 +4043,6 @@ timezone = ["tzdata ; platform_system == \"Windows\""] xlsx2csv = ["xlsx2csv (>=0.8.0)"] xlsxwriter = ["xlsxwriter"] -[[package]] -name = "pondpond" -version = "1.4.1" -description = "Pond is a high performance object-pooling library for Python." -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "pondpond-1.4.1-py3-none-any.whl", hash = "sha256:641028ead4e8018ca6de1220c660ddd6d6fbf62a60e72f410655dd0451d82880"}, - {file = "pondpond-1.4.1.tar.gz", hash = "sha256:8afa34b869d1434d21dd2ec12644abc3b1733fcda8fcf355300338a13a79bb7b"}, -] - -[package.dependencies] -madoka = ">=0.7.1" - [[package]] name = "priority" version = "2.0.0" @@ -6754,4 +6742,4 @@ utils = ["numpydoc"] [metadata] lock-version = "2.1" python-versions = ">=3.8.1,<4.0, !=3.9.7" -content-hash = "9e20685127a280c3a2b42c2626490ee0af32c8716d7d78b72cd86c8c0308a088" +content-hash = "6ea59a97b3d69dc5fcc45645131e2fc843ec533854dba19852cecf8e10da5e7a" \ No newline at end of file From 1585750c1b225a3b8f45310b45a79b928e502452 Mon Sep 17 00:00:00 2001 From: Ishaan Jaffer Date: Wed, 24 Sep 2025 21:45:42 -0700 Subject: [PATCH 30/70] test fix --- litellm/model_prices_and_context_window_backup.json | 2 +- model_prices_and_context_window.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/litellm/model_prices_and_context_window_backup.json b/litellm/model_prices_and_context_window_backup.json index 3742693ce67..e67dd3d3464 100644 --- a/litellm/model_prices_and_context_window_backup.json +++ b/litellm/model_prices_and_context_window_backup.json @@ -12522,7 +12522,7 @@ "supports_function_calling": true, "supports_native_streaming": false, "supports_parallel_function_calling": true, - "supports_pdf_input": false, + "supports_pdf_input": true, "supports_prompt_caching": true, "supports_reasoning": false, "supports_response_schema": true, diff --git a/model_prices_and_context_window.json b/model_prices_and_context_window.json index 3742693ce67..e67dd3d3464 100644 --- a/model_prices_and_context_window.json +++ b/model_prices_and_context_window.json @@ -12522,7 +12522,7 @@ "supports_function_calling": true, "supports_native_streaming": false, "supports_parallel_function_calling": true, - "supports_pdf_input": false, + "supports_pdf_input": true, "supports_prompt_caching": true, "supports_reasoning": false, "supports_response_schema": true, From b1e56c55add544e8fffa2aba7e4b3dc1b19db2af Mon Sep 17 00:00:00 2001 From: Ishaan Jaffer Date: Wed, 24 Sep 2025 22:11:37 -0700 Subject: [PATCH 31/70] merge --- .circleci/config.yml | 3 +- docs/my-website/docs/proxy/config_settings.md | 2 - ...tellm_proxy_extras-0.2.20-py3-none-any.whl | Bin 30470 -> 0 bytes .../dist/litellm_proxy_extras-0.2.20.tar.gz | Bin 15576 -> 0 bytes .../litellm_proxy_extras/schema.prisma | 4 - litellm-proxy-extras/pyproject.toml | 4 +- litellm/constants.py | 4 - litellm/proxy/_types.py | 29 --- .../common_utils/key_rotation_manager.py | 146 ------------- .../key_management_endpoints.py | 15 -- litellm/proxy/proxy_config.yaml | 2 - litellm/proxy/proxy_server.py | 38 +--- litellm/proxy/schema.prisma | 4 - poetry.lock | 9 +- pyproject.toml | 2 +- requirements.txt | 2 +- schema.prisma | 4 - .../common_utils/test_key_rotation_manager.py | 194 ------------------ .../test_key_management_endpoints.py | 71 ------- 19 files changed, 13 insertions(+), 520 deletions(-) delete mode 100644 litellm-proxy-extras/dist/litellm_proxy_extras-0.2.20-py3-none-any.whl delete mode 100644 litellm-proxy-extras/dist/litellm_proxy_extras-0.2.20.tar.gz delete mode 100644 litellm/proxy/common_utils/key_rotation_manager.py delete mode 100644 tests/test_litellm/proxy/common_utils/test_key_rotation_manager.py diff --git a/.circleci/config.yml b/.circleci/config.yml index ba42ccbc362..ef6445ca0ca 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -148,8 +148,7 @@ jobs: python -m pip install types-requests types-setuptools types-redis types-PyYAML if ! python -m mypy . \ --config-file mypy.ini \ - --ignore-missing-imports \ - --no-incremental; then + --ignore-missing-imports; then echo "mypy detected errors" exit 1 fi diff --git a/docs/my-website/docs/proxy/config_settings.md b/docs/my-website/docs/proxy/config_settings.md index 4cd15e8ae43..974e95a07bd 100644 --- a/docs/my-website/docs/proxy/config_settings.md +++ b/docs/my-website/docs/proxy/config_settings.md @@ -613,8 +613,6 @@ router_settings: | LITELLM_MIGRATION_DIR | Custom migrations directory for prisma migrations, used for baselining db in read-only file systems. | LITELLM_HOSTED_UI | URL of the hosted UI for LiteLLM | LITELM_ENVIRONMENT | Environment of LiteLLM Instance, used by logging services. Currently only used by DeepEval. -| LITELLM_KEY_ROTATION_ENABLED | Enable auto-key rotation for LiteLLM (boolean). Default is false. -| LITELLM_KEY_ROTATION_CHECK_INTERVAL_SECONDS | Interval in seconds for how often to run job that auto-rotates keys. Default is 86400 (24 hours). | LITELLM_LICENSE | License key for LiteLLM usage | LITELLM_LOCAL_MODEL_COST_MAP | Local configuration for model cost mapping in LiteLLM | LITELLM_LOG | Enable detailed logging for LiteLLM diff --git a/litellm-proxy-extras/dist/litellm_proxy_extras-0.2.20-py3-none-any.whl b/litellm-proxy-extras/dist/litellm_proxy_extras-0.2.20-py3-none-any.whl deleted file mode 100644 index 0a94ef6ff621531d9f9d3d4e398315f607384e97..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 30470 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 WRz|!2$GbVNetfP@emzb9`0XG0pa)+7 diff --git a/litellm-proxy-extras/dist/litellm_proxy_extras-0.2.20.tar.gz b/litellm-proxy-extras/dist/litellm_proxy_extras-0.2.20.tar.gz deleted file mode 100644 index 1562aacc22b73a2db570b62ef49ebfe06c602535..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 15576 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 diff --git a/litellm-proxy-extras/litellm_proxy_extras/schema.prisma b/litellm-proxy-extras/litellm_proxy_extras/schema.prisma index 9d3726d29d6..2b1e20820f9 100644 --- a/litellm-proxy-extras/litellm_proxy_extras/schema.prisma +++ b/litellm-proxy-extras/litellm_proxy_extras/schema.prisma @@ -221,10 +221,6 @@ model LiteLLM_VerificationToken { created_by String? updated_at DateTime? @default(now()) @updatedAt @map("updated_at") updated_by String? - rotation_count Int? @default(0) // Number of times key has been rotated - auto_rotate Boolean? @default(false) // Whether this key should be auto-rotated - rotation_interval String? // How often to rotate (e.g., "30d", "90d") - last_rotation_at DateTime? // When this key was last rotated litellm_budget_table LiteLLM_BudgetTable? @relation(fields: [budget_id], references: [budget_id]) litellm_organization_table LiteLLM_OrganizationTable? @relation(fields: [organization_id], references: [organization_id]) object_permission LiteLLM_ObjectPermissionTable? @relation(fields: [object_permission_id], references: [object_permission_id]) diff --git a/litellm-proxy-extras/pyproject.toml b/litellm-proxy-extras/pyproject.toml index dd0b2138dcf..1c368d58077 100644 --- a/litellm-proxy-extras/pyproject.toml +++ b/litellm-proxy-extras/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "litellm-proxy-extras" -version = "0.2.20" +version = "0.2.19" description = "Additional files for the LiteLLM Proxy. Reduces the size of the main litellm package." authors = ["BerriAI"] readme = "README.md" @@ -22,7 +22,7 @@ requires = ["poetry-core"] build-backend = "poetry.core.masonry.api" [tool.commitizen] -version = "0.2.20" +version = "0.2.19" version_files = [ "pyproject.toml:version", "../requirements.txt:litellm-proxy-extras==", diff --git a/litellm/constants.py b/litellm/constants.py index 1ed9f237a29..6e70ae0671f 100644 --- a/litellm/constants.py +++ b/litellm/constants.py @@ -989,11 +989,7 @@ HEALTH_CHECK_TIMEOUT_SECONDS = int( ) # 60 seconds LITTELM_INTERNAL_HEALTH_SERVICE_ACCOUNT_NAME = "litellm-internal-health-check" LITTELM_CLI_SERVICE_ACCOUNT_NAME = "litellm-cli" -LITELLM_INTERNAL_JOBS_SERVICE_ACCOUNT_NAME = "litellm_internal_jobs" -# Key Rotation Constants -LITELLM_KEY_ROTATION_ENABLED = os.getenv("LITELLM_KEY_ROTATION_ENABLED", "false") -LITELLM_KEY_ROTATION_CHECK_INTERVAL_SECONDS = int(os.getenv("LITELLM_KEY_ROTATION_CHECK_INTERVAL_SECONDS", 86400)) # 24 hours default UI_SESSION_TOKEN_TEAM_ID = "litellm-dashboard" LITELLM_PROXY_ADMIN_NAME = "default_user_id" diff --git a/litellm/proxy/_types.py b/litellm/proxy/_types.py index eea370fa224..a92b7d8ad2e 100644 --- a/litellm/proxy/_types.py +++ b/litellm/proxy/_types.py @@ -787,14 +787,6 @@ class GenerateKeyRequest(KeyRequestBase): default=LiteLLMKeyType.DEFAULT, description="Type of key that determines default allowed routes.", ) - auto_rotate: Optional[bool] = Field( - default=False, - description="Whether this key should be automatically rotated" - ) - rotation_interval: Optional[str] = Field( - default=None, - description="How often to rotate this key (e.g., '30d', '90d'). Required if auto_rotate=True" - ) class GenerateKeyResponse(KeyRequestBase): @@ -1810,10 +1802,6 @@ class LiteLLM_VerificationToken(LiteLLMPydanticObjectBase): updated_by: Optional[str] = None object_permission_id: Optional[str] = None object_permission: Optional[LiteLLM_ObjectPermissionTable] = None - rotation_count: Optional[int] = 0 # Number of times key has been rotated - auto_rotate: Optional[bool] = False # Whether this key should be auto-rotated - rotation_interval: Optional[str] = None # How often to rotate (e.g., "30d", "90d") - last_rotation_at: Optional[datetime] = None # When this key was last rotated model_config = ConfigDict(protected_namespaces=()) @@ -1944,23 +1932,6 @@ class UserAPIKeyAuth( key_alias=LITTELM_CLI_SERVICE_ACCOUNT_NAME, team_alias=LITTELM_CLI_SERVICE_ACCOUNT_NAME, ) - - @classmethod - def get_litellm_internal_jobs_user_api_key_auth(cls) -> "UserAPIKeyAuth": - """ - Returns a `UserAPIKeyAuth` object for internal LiteLLM jobs like key rotation. - - This is used to track actions performed by automated system jobs. - """ - from litellm.constants import LITELLM_INTERNAL_JOBS_SERVICE_ACCOUNT_NAME - - return cls( - api_key=LITELLM_INTERNAL_JOBS_SERVICE_ACCOUNT_NAME, - team_id="system", - key_alias=LITELLM_INTERNAL_JOBS_SERVICE_ACCOUNT_NAME, - team_alias="system", - user_id="system", - ) class UserInfoResponse(LiteLLMPydanticObjectBase): diff --git a/litellm/proxy/common_utils/key_rotation_manager.py b/litellm/proxy/common_utils/key_rotation_manager.py deleted file mode 100644 index 319d5ed5233..00000000000 --- a/litellm/proxy/common_utils/key_rotation_manager.py +++ /dev/null @@ -1,146 +0,0 @@ -""" -Key Rotation Manager - Automated key rotation based on rotation schedules - -Handles finding keys that need rotation based on their individual schedules. -""" - -from datetime import datetime, timedelta, timezone -from typing import List - -from litellm._logging import verbose_proxy_logger -from litellm.constants import LITELLM_INTERNAL_JOBS_SERVICE_ACCOUNT_NAME -from litellm.proxy._types import ( - GenerateKeyResponse, - LiteLLM_VerificationToken, - RegenerateKeyRequest, -) -from litellm.proxy.hooks.key_management_event_hooks import KeyManagementEventHooks -from litellm.proxy.management_endpoints.key_management_endpoints import ( - regenerate_key_fn, -) -from litellm.proxy.utils import PrismaClient - - -class KeyRotationManager: - """ - Manages automated key rotation based on individual key rotation schedules. - """ - - def __init__(self, prisma_client: PrismaClient): - self.prisma_client = prisma_client - - async def process_rotations(self): - """ - Main entry point - find and rotate keys that are due for rotation - """ - try: - verbose_proxy_logger.info("Starting scheduled key rotation check...") - - # Find keys that are due for rotation - keys_to_rotate = await self._find_keys_needing_rotation() - - if not keys_to_rotate: - verbose_proxy_logger.debug("No keys are due for rotation at this time") - return - - verbose_proxy_logger.info(f"Found {len(keys_to_rotate)} keys due for rotation") - - # Rotate each key - for key in keys_to_rotate: - try: - await self._rotate_key(key) - key_identifier = key.key_name or (key.token[:8] + "..." if key.token else "unknown") - verbose_proxy_logger.info(f"Successfully rotated key: {key_identifier}") - except Exception as e: - key_identifier = key.key_name or (key.token[:8] + "..." if key.token else "unknown") - verbose_proxy_logger.error(f"Failed to rotate key {key_identifier}: {e}") - - except Exception as e: - verbose_proxy_logger.error(f"Key rotation process failed: {e}") - - async def _find_keys_needing_rotation(self) -> List[LiteLLM_VerificationToken]: - """ - Find keys that are due for rotation based on their rotation interval. - - Logic: - - Key has auto_rotate = true - - Key has rotation_interval set - - Either: never been rotated (last_rotation_at is null) OR - - Time since last rotation >= rotation_interval - """ - keys_with_rotation = await self.prisma_client.db.litellm_verificationtoken.find_many( - where={ - "auto_rotate": True, # Only keys marked for auto rotation - "rotation_interval": {"not": None} # Must have rotation interval set - } - ) - - # Filter keys that need rotation based on last_rotation_at + interval - keys_needing_rotation = [] - now = datetime.now(timezone.utc) - - for key in keys_with_rotation: - if self._should_rotate_key(key, now): - keys_needing_rotation.append(key) - - return keys_needing_rotation - - def _should_rotate_key(self, key: LiteLLM_VerificationToken, now: datetime) -> bool: - """ - Determine if a key should be rotated based on last rotation time and interval. - """ - if not key.rotation_interval: - return False - - # If never rotated, rotate immediately - if key.last_rotation_at is None: - return True - - # Calculate if enough time has passed since last rotation - from litellm.litellm_core_utils.duration_parser import duration_in_seconds - - interval_seconds = duration_in_seconds(key.rotation_interval) - next_rotation_time = key.last_rotation_at + timedelta(seconds=interval_seconds) - - return now >= next_rotation_time - - async def _rotate_key(self, key: LiteLLM_VerificationToken): - """ - Rotate a single key using existing regenerate_key_fn and call the rotation hook - """ - # Create regenerate request - regenerate_request = RegenerateKeyRequest( - key=key.token or "" - ) - - # Create a system user for key rotation - from litellm.proxy._types import UserAPIKeyAuth - system_user = UserAPIKeyAuth.get_litellm_internal_jobs_user_api_key_auth() - - # Use existing regenerate key function - response = await regenerate_key_fn( - data=regenerate_request, - user_api_key_dict=system_user, - litellm_changed_by=LITELLM_INTERNAL_JOBS_SERVICE_ACCOUNT_NAME - ) - - # Update the NEW key with rotation info (regenerate_key_fn creates a new token) - if isinstance(response, GenerateKeyResponse) and response.token_id: - await self.prisma_client.db.litellm_verificationtoken.update( - where={"token": response.token_id}, - data={ - "rotation_count": (key.rotation_count or 0) + 1, - "last_rotation_at": datetime.now(timezone.utc) - } - ) - - # Call the existing rotation hook for notifications, audit logs, etc. - if isinstance(response, GenerateKeyResponse): - await KeyManagementEventHooks.async_key_rotated_hook( - data=regenerate_request, - existing_key_row=key, - response=response, - user_api_key_dict=system_user, - litellm_changed_by=LITELLM_INTERNAL_JOBS_SERVICE_ACCOUNT_NAME - ) - \ No newline at end of file diff --git a/litellm/proxy/management_endpoints/key_management_endpoints.py b/litellm/proxy/management_endpoints/key_management_endpoints.py index 363cbf7182c..eb389450f91 100644 --- a/litellm/proxy/management_endpoints/key_management_endpoints.py +++ b/litellm/proxy/management_endpoints/key_management_endpoints.py @@ -642,9 +642,6 @@ async def generate_key_fn( - object_permission: Optional[LiteLLM_ObjectPermissionBase] - key-specific object permission. Example - {"vector_stores": ["vector_store_1", "vector_store_2"]}. IF null or {} then no object permission. - key_type: Optional[str] - Type of key that determines default allowed routes. Options: "llm_api" (can call LLM API routes), "management" (can call management routes), "read_only" (can only call info/read routes), "default" (uses default allowed routes). Defaults to "default". - prompts: Optional[List[str]] - List of allowed prompts for the key. If specified, the key will only be able to use these specific prompts. - - auto_rotate: Optional[bool] - Whether this key should be automatically rotated (regenerated) - - rotation_interval: Optional[str] - How often to auto-rotate this key (e.g., '30s', '30m', '30h', '30d'). Required if auto_rotate=True. - Examples: 1. Allow users to turn on/off pii masking @@ -1561,8 +1558,6 @@ def _check_model_access_group( return True - - async def generate_key_helper_fn( # noqa: PLR0915 request_type: Literal[ "user", "key" @@ -1616,8 +1611,6 @@ async def generate_key_helper_fn( # noqa: PLR0915 str ] = None, # object_permission_id <-> LiteLLM_ObjectPermissionTable object_permission: Optional[LiteLLM_ObjectPermissionBase] = None, - auto_rotate: Optional[bool] = None, - rotation_interval: Optional[str] = None, ): from litellm.proxy.proxy_server import premium_user, prisma_client @@ -1725,14 +1718,6 @@ async def generate_key_helper_fn( # noqa: PLR0915 "allowed_routes": allowed_routes or [], "object_permission_id": object_permission_id, } - - # Add rotation fields if auto_rotate is enabled - if auto_rotate and rotation_interval: - key_data.update({ - "auto_rotate": auto_rotate, - "rotation_interval": rotation_interval - # last_rotation_at will be null initially - rotation happens on first check - }) if ( get_secret("DISABLE_KEY_NAME", False) is True diff --git a/litellm/proxy/proxy_config.yaml b/litellm/proxy/proxy_config.yaml index b9cd33175e7..921b564407f 100644 --- a/litellm/proxy/proxy_config.yaml +++ b/litellm/proxy/proxy_config.yaml @@ -36,5 +36,3 @@ guardrails: breakdown: true payload: true dev_info: true - -# proxy_config.yaml \ No newline at end of file diff --git a/litellm/proxy/proxy_server.py b/litellm/proxy/proxy_server.py index 399cadac24b..7255161b8bc 100644 --- a/litellm/proxy/proxy_server.py +++ b/litellm/proxy/proxy_server.py @@ -3802,10 +3802,9 @@ class ProxyStartupEvent: cls, scheduler: AsyncIOScheduler ): """ - Initialize the spend tracking and other background jobs + Initialize the spend tracking background jobs 1. CloudZero Background Job 2. Prometheus Background Job - 3. Key Rotation Background Job Args: scheduler: The scheduler to add the background jobs to @@ -3830,41 +3829,6 @@ class ProxyStartupEvent: except Exception: PrometheusLogger = None - ######################################################## - # Key Rotation Background Job - ######################################################## - from litellm.constants import ( - LITELLM_KEY_ROTATION_CHECK_INTERVAL_SECONDS, - LITELLM_KEY_ROTATION_ENABLED, - ) - - key_rotation_enabled: Optional[bool] = str_to_bool(LITELLM_KEY_ROTATION_ENABLED) - verbose_proxy_logger.debug(f"key_rotation_enabled: {key_rotation_enabled}") - - if key_rotation_enabled is True: - try: - from litellm.proxy.common_utils.key_rotation_manager import ( - KeyRotationManager, - ) - - # Get prisma_client from global scope - global prisma_client - if prisma_client is not None: - key_rotation_manager = KeyRotationManager(prisma_client) - verbose_proxy_logger.debug(f"Key rotation background job scheduled every {LITELLM_KEY_ROTATION_CHECK_INTERVAL_SECONDS} seconds (LITELLM_KEY_ROTATION_ENABLED=true)") - scheduler.add_job( - key_rotation_manager.process_rotations, - "interval", - seconds=LITELLM_KEY_ROTATION_CHECK_INTERVAL_SECONDS, - id="key_rotation_job" - ) - else: - verbose_proxy_logger.warning("Key rotation enabled but prisma_client not available") - except Exception as e: - verbose_proxy_logger.warning(f"Failed to setup key rotation job: {e}") - else: - verbose_proxy_logger.debug("Key rotation disabled (set LITELLM_KEY_ROTATION_ENABLED=true to enable)") - @classmethod async def _setup_prisma_client( cls, diff --git a/litellm/proxy/schema.prisma b/litellm/proxy/schema.prisma index 9d3726d29d6..2b1e20820f9 100644 --- a/litellm/proxy/schema.prisma +++ b/litellm/proxy/schema.prisma @@ -221,10 +221,6 @@ model LiteLLM_VerificationToken { created_by String? updated_at DateTime? @default(now()) @updatedAt @map("updated_at") updated_by String? - rotation_count Int? @default(0) // Number of times key has been rotated - auto_rotate Boolean? @default(false) // Whether this key should be auto-rotated - rotation_interval String? // How often to rotate (e.g., "30d", "90d") - last_rotation_at DateTime? // When this key was last rotated litellm_budget_table LiteLLM_BudgetTable? @relation(fields: [budget_id], references: [budget_id]) litellm_organization_table LiteLLM_OrganizationTable? @relation(fields: [organization_id], references: [organization_id]) object_permission LiteLLM_ObjectPermissionTable? @relation(fields: [object_permission_id], references: [object_permission_id]) diff --git a/poetry.lock b/poetry.lock index 9677775ad22..07822390aff 100644 --- a/poetry.lock +++ b/poetry.lock @@ -2715,16 +2715,21 @@ files = [ [[package]] name = "litellm-proxy-extras" -version = "0.2.20" +version = "0.2.19" description = "Additional files for the LiteLLM Proxy. Reduces the size of the main litellm package." optional = true python-versions = "!=2.7.*,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,!=3.7.*,>=3.8" groups = ["main"] markers = "extra == \"proxy\"" files = [ +<<<<<<< HEAD {file = "litellm_proxy_extras-0.2.20-py3-none-any.whl", hash = "sha256:80ebe03be210eed99ddab0bf8e1a14169f24a502ecdaa98f4bb5032ebe65a944"}, {file = "litellm_proxy_extras-0.2.20.tar.gz", hash = "sha256:8dea657f965122490be480327995ce534e75dc96778cb3e05c562c7996f85cce"}, <<<<<<< HEAD +======= + {file = "litellm_proxy_extras-0.2.19-py3-none-any.whl", hash = "sha256:cd4cc5fc639ac24bc99af70b8b56b7cc0480b0e72a2a53638f557beb7e9f64fd"}, + {file = "litellm_proxy_extras-0.2.19.tar.gz", hash = "sha256:e53592e39eb0b9c3b6cd32f29e6a0a53be51d7ad31dedfb69b58c24073b5b3ec"}, +>>>>>>> parent of ded823a3a3 ([Feat] Initial support for scheduled key rotations (#14877)) ] [[package]] @@ -6742,4 +6747,4 @@ utils = ["numpydoc"] [metadata] lock-version = "2.1" python-versions = ">=3.8.1,<4.0, !=3.9.7" -content-hash = "6ea59a97b3d69dc5fcc45645131e2fc843ec533854dba19852cecf8e10da5e7a" \ No newline at end of file +content-hash = "6ea59a97b3d69dc5fcc45645131e2fc843ec533854dba19852cecf8e10da5e7a" diff --git a/pyproject.toml b/pyproject.toml index 5d4605ed5b4..cc45c35cdac 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -59,7 +59,7 @@ websockets = {version = "^13.1.0", optional = true} boto3 = {version = "1.36.0", optional = true} redisvl = {version = "^0.4.1", optional = true, markers = "python_version >= '3.9' and python_version < '3.14'"} mcp = {version = "^1.10.0", optional = true, python = ">=3.10"} -litellm-proxy-extras = {version = "0.2.20", optional = true} +litellm-proxy-extras = {version = "0.2.19", optional = true} rich = {version = "13.7.1", optional = true} litellm-enterprise = {version = "0.1.20", optional = true} diskcache = {version = "^5.6.1", optional = true} diff --git a/requirements.txt b/requirements.txt index 10929ab819b..3c55f682a0f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -43,7 +43,7 @@ sentry_sdk==2.21.0 # for sentry error handling detect-secrets==1.5.0 # Enterprise - secret detection / masking in LLM requests cryptography==43.0.1 tzdata==2025.1 # IANA time zone database -litellm-proxy-extras==0.2.20 # for proxy extras - e.g. prisma migrations +litellm-proxy-extras==0.2.19 # for proxy extras - e.g. prisma migrations ### LITELLM PACKAGE DEPENDENCIES python-dotenv==1.0.1 # for env tiktoken==0.8.0 # for calculating usage diff --git a/schema.prisma b/schema.prisma index 9d3726d29d6..2b1e20820f9 100644 --- a/schema.prisma +++ b/schema.prisma @@ -221,10 +221,6 @@ model LiteLLM_VerificationToken { created_by String? updated_at DateTime? @default(now()) @updatedAt @map("updated_at") updated_by String? - rotation_count Int? @default(0) // Number of times key has been rotated - auto_rotate Boolean? @default(false) // Whether this key should be auto-rotated - rotation_interval String? // How often to rotate (e.g., "30d", "90d") - last_rotation_at DateTime? // When this key was last rotated litellm_budget_table LiteLLM_BudgetTable? @relation(fields: [budget_id], references: [budget_id]) litellm_organization_table LiteLLM_OrganizationTable? @relation(fields: [organization_id], references: [organization_id]) object_permission LiteLLM_ObjectPermissionTable? @relation(fields: [object_permission_id], references: [object_permission_id]) diff --git a/tests/test_litellm/proxy/common_utils/test_key_rotation_manager.py b/tests/test_litellm/proxy/common_utils/test_key_rotation_manager.py deleted file mode 100644 index 8aab67bf588..00000000000 --- a/tests/test_litellm/proxy/common_utils/test_key_rotation_manager.py +++ /dev/null @@ -1,194 +0,0 @@ -""" -Test key rotation manager functionality -""" -import os -import sys -from datetime import datetime, timedelta, timezone -from unittest.mock import AsyncMock, MagicMock - -import pytest - -sys.path.insert(0, os.path.abspath("../../../..")) - -from litellm.proxy._types import ( - GenerateKeyResponse, - LiteLLM_VerificationToken, -) -from litellm.proxy.common_utils.key_rotation_manager import KeyRotationManager - - -class TestKeyRotationManager: - """Test the KeyRotationManager class functionality.""" - - @pytest.mark.asyncio - async def test_should_rotate_key_logic(self): - """ - Test the core logic for determining when a key should be rotated. - - This tests: - - Keys with null last_rotation_at should rotate immediately - - Keys with recent rotation should not rotate - - Keys with old rotation should rotate - """ - # Setup - mock_prisma_client = AsyncMock() - manager = KeyRotationManager(mock_prisma_client) - - now = datetime.now(timezone.utc) - - # Test Case 1: Never rotated (last_rotation_at = None) - should rotate - key_never_rotated = LiteLLM_VerificationToken( - token="test-token-1", - auto_rotate=True, - rotation_interval="30s", - last_rotation_at=None, - rotation_count=0 - ) - - assert manager._should_rotate_key(key_never_rotated, now) == True - - # Test Case 2: Recently rotated (10s ago, interval 30s) - should NOT rotate - key_recently_rotated = LiteLLM_VerificationToken( - token="test-token-2", - auto_rotate=True, - rotation_interval="30s", - last_rotation_at=now - timedelta(seconds=10), - rotation_count=1 - ) - - assert manager._should_rotate_key(key_recently_rotated, now) == False - - # Test Case 3: Old rotation (60s ago, interval 30s) - should rotate - key_old_rotation = LiteLLM_VerificationToken( - token="test-token-3", - auto_rotate=True, - rotation_interval="30s", - last_rotation_at=now - timedelta(seconds=60), - rotation_count=2 - ) - - assert manager._should_rotate_key(key_old_rotation, now) == True - - # Test Case 4: No rotation interval - should NOT rotate - key_no_interval = LiteLLM_VerificationToken( - token="test-token-4", - auto_rotate=True, - rotation_interval=None, - last_rotation_at=None, - rotation_count=0 - ) - - assert manager._should_rotate_key(key_no_interval, now) == False - - @pytest.mark.asyncio - async def test_find_keys_needing_rotation(self): - """ - Test finding keys that need rotation from database. - - This tests: - - Only keys with auto_rotate=True and rotation_interval are considered - - Filtering logic works correctly - - Database query is constructed properly - """ - # Setup - mock_prisma_client = AsyncMock() - manager = KeyRotationManager(mock_prisma_client) - - now = datetime.now(timezone.utc) - - # Mock database response - mock_keys = [ - LiteLLM_VerificationToken( - token="token-1", - auto_rotate=True, - rotation_interval="30s", - last_rotation_at=None, # Should rotate - rotation_count=0 - ), - LiteLLM_VerificationToken( - token="token-2", - auto_rotate=True, - rotation_interval="60s", - last_rotation_at=now - timedelta(seconds=30), # Should NOT rotate (30s < 60s) - rotation_count=1 - ), - LiteLLM_VerificationToken( - token="token-3", - auto_rotate=True, - rotation_interval="30s", - last_rotation_at=now - timedelta(seconds=45), # Should rotate (45s > 30s) - rotation_count=2 - ) - ] - - mock_prisma_client.db.litellm_verificationtoken.find_many.return_value = mock_keys - - # Execute - keys_needing_rotation = await manager._find_keys_needing_rotation() - - # Verify database query - mock_prisma_client.db.litellm_verificationtoken.find_many.assert_called_once_with( - where={ - "auto_rotate": True, - "rotation_interval": {"not": None} - } - ) - - # Verify filtering logic - assert len(keys_needing_rotation) == 2 # token-1 and token-3 should need rotation - - tokens_needing_rotation = [key.token for key in keys_needing_rotation] - assert "token-1" in tokens_needing_rotation # Never rotated - assert "token-2" not in tokens_needing_rotation # Recently rotated - assert "token-3" in tokens_needing_rotation # Old rotation - - @pytest.mark.asyncio - async def test_rotate_key_updates_database(self): - """ - Test that key rotation properly updates the database with new rotation info. - - This tests: - - Rotation count is incremented - - last_rotation_at is set to current time - - New key token is updated (not old one) - """ - # Setup - mock_prisma_client = AsyncMock() - manager = KeyRotationManager(mock_prisma_client) - - # Mock key to rotate - key_to_rotate = LiteLLM_VerificationToken( - token="old-token", - auto_rotate=True, - rotation_interval="30s", - last_rotation_at=None, - rotation_count=0 - ) - - # Mock regenerate_key_fn response - mock_response = GenerateKeyResponse( - key="new-api-key", - token_id="new-token-id", - user_id="test-user" - ) - - # Mock the regenerate function - from unittest.mock import patch - with patch('litellm.proxy.common_utils.key_rotation_manager.regenerate_key_fn', return_value=mock_response): - with patch('litellm.proxy.common_utils.key_rotation_manager.KeyManagementEventHooks.async_key_rotated_hook'): - # Execute - await manager._rotate_key(key_to_rotate) - - # Verify database update was called with correct data - mock_prisma_client.db.litellm_verificationtoken.update.assert_called_once() - - call_args = mock_prisma_client.db.litellm_verificationtoken.update.call_args - - # Check the WHERE clause targets the new token - assert call_args[1]["where"]["token"] == "new-token-id" - - # Check the data being updated - update_data = call_args[1]["data"] - assert update_data["rotation_count"] == 1 # Incremented from 0 - assert "last_rotation_at" in update_data - assert isinstance(update_data["last_rotation_at"], datetime) diff --git a/tests/test_litellm/proxy/management_endpoints/test_key_management_endpoints.py b/tests/test_litellm/proxy/management_endpoints/test_key_management_endpoints.py index 14b5833f14f..da64a866d51 100644 --- a/tests/test_litellm/proxy/management_endpoints/test_key_management_endpoints.py +++ b/tests/test_litellm/proxy/management_endpoints/test_key_management_endpoints.py @@ -23,7 +23,6 @@ from litellm.proxy.auth.user_api_key_auth import UserAPIKeyAuth from litellm.proxy.management_endpoints.key_management_endpoints import ( _common_key_generation_helper, _list_key_helper, - generate_key_helper_fn, prepare_key_update_data, validate_key_team_change, ) @@ -1092,73 +1091,3 @@ def test_validate_key_team_change_with_member_permissions(): team_table=mock_team, route=KeyManagementRoutes.KEY_UPDATE.value ) - - -def test_key_rotation_fields_helper(): - """ - Test the key data update logic for rotation fields. - - This test focuses on the core logic that adds rotation fields to key_data - when auto_rotate is enabled, without the complexity of full key generation. - """ - # Test Case 1: With rotation enabled - key_data = { - "models": ["gpt-3.5-turbo"], - "user_id": "test-user" - } - - auto_rotate = True - rotation_interval = "30d" - - # Simulate the rotation logic from generate_key_helper_fn - if auto_rotate and rotation_interval: - key_data.update({ - "auto_rotate": auto_rotate, - "rotation_interval": rotation_interval - }) - - # Verify rotation fields are added - assert key_data["auto_rotate"] == True - assert key_data["rotation_interval"] == "30d" - assert key_data["models"] == ["gpt-3.5-turbo"] # Original fields preserved - - # Test Case 2: Without rotation enabled - key_data2 = { - "models": ["gpt-4"], - "user_id": "test-user" - } - - auto_rotate2 = False - rotation_interval2 = None - - # Simulate the rotation logic - if auto_rotate2 and rotation_interval2: - key_data2.update({ - "auto_rotate": auto_rotate2, - "rotation_interval": rotation_interval2 - }) - - # Verify rotation fields are NOT added - assert "auto_rotate" not in key_data2 - assert "rotation_interval" not in key_data2 - assert key_data2["models"] == ["gpt-4"] # Original fields preserved - - # Test Case 3: auto_rotate=True but no interval - key_data3 = { - "models": ["claude-3"], - "user_id": "test-user" - } - - auto_rotate3 = True - rotation_interval3 = None - - # Simulate the rotation logic - if auto_rotate3 and rotation_interval3: - key_data3.update({ - "auto_rotate": auto_rotate3, - "rotation_interval": rotation_interval3 - }) - - # Verify rotation fields are NOT added (missing interval) - assert "auto_rotate" not in key_data3 - assert "rotation_interval" not in key_data3 From 31e38efc8a9e66b4430d2bfbcd7b9a5f6d4ee5c2 Mon Sep 17 00:00:00 2001 From: Ishaan Jaffer Date: Wed, 24 Sep 2025 22:12:13 -0700 Subject: [PATCH 32/70] Revert "bump v extras" This reverts commit c4838e730cd8016fdbd39af5b4c8da44277488a0. --- poetry.lock | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/poetry.lock b/poetry.lock index 07822390aff..d0c966834d6 100644 --- a/poetry.lock +++ b/poetry.lock @@ -2726,10 +2726,13 @@ files = [ {file = "litellm_proxy_extras-0.2.20-py3-none-any.whl", hash = "sha256:80ebe03be210eed99ddab0bf8e1a14169f24a502ecdaa98f4bb5032ebe65a944"}, {file = "litellm_proxy_extras-0.2.20.tar.gz", hash = "sha256:8dea657f965122490be480327995ce534e75dc96778cb3e05c562c7996f85cce"}, <<<<<<< HEAD +<<<<<<< HEAD ======= {file = "litellm_proxy_extras-0.2.19-py3-none-any.whl", hash = "sha256:cd4cc5fc639ac24bc99af70b8b56b7cc0480b0e72a2a53638f557beb7e9f64fd"}, {file = "litellm_proxy_extras-0.2.19.tar.gz", hash = "sha256:e53592e39eb0b9c3b6cd32f29e6a0a53be51d7ad31dedfb69b58c24073b5b3ec"}, >>>>>>> parent of ded823a3a3 ([Feat] Initial support for scheduled key rotations (#14877)) +======= +>>>>>>> parent of c4838e730c (bump v extras) ] [[package]] @@ -2742,8 +2745,6 @@ groups = ["main"] files = [ {file = "madoka-0.7.1-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:7521eee9ace30b376bb54fdcb2cb42bf6b7a0346b0d0b612f25f3299aa4a95af"}, {file = "madoka-0.7.1.tar.gz", hash = "sha256:e258baa84fc0a3764365993b8bf5e1b065383a6ca8c9f862fb3e3e709843fae7"}, -======= ->>>>>>> 0cb68da91d (bump v extras) ] [[package]] @@ -4048,6 +4049,21 @@ timezone = ["tzdata ; platform_system == \"Windows\""] xlsx2csv = ["xlsx2csv (>=0.8.0)"] xlsxwriter = ["xlsxwriter"] +[[package]] +name = "pondpond" +version = "1.4.1" +description = "Pond is a high performance object-pooling library for Python." +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "pondpond-1.4.1-py3-none-any.whl", hash = "sha256:641028ead4e8018ca6de1220c660ddd6d6fbf62a60e72f410655dd0451d82880"}, + {file = "pondpond-1.4.1.tar.gz", hash = "sha256:8afa34b869d1434d21dd2ec12644abc3b1733fcda8fcf355300338a13a79bb7b"}, +] + +[package.dependencies] +madoka = ">=0.7.1" + [[package]] name = "priority" version = "2.0.0" From 02a41d76be96663a768459dd2e5274514c836b75 Mon Sep 17 00:00:00 2001 From: mrFranklin <5235904+mrFranklin@users.noreply.github.com> Date: Thu, 25 Sep 2025 14:23:00 +0800 Subject: [PATCH 33/70] fix: add default value to pass the opik test --- litellm/integrations/opik/opik.py | 13 ++++++++----- tests/local_testing/test_opik.py | 2 +- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/litellm/integrations/opik/opik.py b/litellm/integrations/opik/opik.py index 94fbcfe2016..5d98f220f38 100644 --- a/litellm/integrations/opik/opik.py +++ b/litellm/integrations/opik/opik.py @@ -202,7 +202,10 @@ class OpikLogger(CustomBatchLogger): return [] # Update litellm_opik_metadata with opik metadata from requester - litellm_opik_metadata.update(standard_logging_object.get("metadata", {}).get("requester_metadata", {}).get("opik", {})) + standard_logging_metadata = standard_logging_object.get("metadata", {}) or {} + requester_metadata = standard_logging_metadata.get("requester_metadata", {}) or {} + requester_opik_metadata = requester_metadata.get("opik", {}) or {} + litellm_opik_metadata.update(requester_opik_metadata) verbose_logger.debug( f"litellm_opik_metadata - {json.dumps(litellm_opik_metadata, default=str)}" @@ -230,7 +233,8 @@ class OpikLogger(CustomBatchLogger): thread_id = litellm_opik_metadata.get("thread_id", None) # Override with any opik_ headers from proxy request - proxy_headers = (_litellm_params.get("proxy_server_request", {}).get("headers", {}) or {}) + proxy_server_request = _litellm_params.get("proxy_server_request", {}) or {} + proxy_headers = proxy_server_request.get("headers", {}) or {} for key, value in proxy_headers.items(): if key.startswith("opik_"): param_key = key.replace("opik_", "", 1) @@ -249,8 +253,7 @@ class OpikLogger(CustomBatchLogger): opik_tags.extend(parsed_tags) except (json.JSONDecodeError, TypeError): pass - - + # Create input and output data input_data = standard_logging_object.get("messages", {}) output_data = standard_logging_object.get("response", {}) @@ -273,7 +276,7 @@ class OpikLogger(CustomBatchLogger): del metadata["current_span_data"] metadata["created_from"] = "litellm" - metadata.update(standard_logging_object.get("metadata", {})) + metadata.update(standard_logging_metadata) if "call_type" in standard_logging_object: metadata["type"] = standard_logging_object["call_type"] if "status" in standard_logging_object: diff --git a/tests/local_testing/test_opik.py b/tests/local_testing/test_opik.py index 62c17fbb2d5..d43b7558f54 100644 --- a/tests/local_testing/test_opik.py +++ b/tests/local_testing/test_opik.py @@ -122,7 +122,7 @@ def test_sync_opik_logging_http_request(): ) # Need to wait for a short amount of time as the log_success callback is called in a different thread - time.sleep(1) + time.sleep(3) # Check that 5 spans and 5 traces were sent assert mock_post.call_count == 10, f"Expected 10 HTTP requests, but got {mock_post.call_count}" From f221ce58668a4f20f9f3619480730c8527f477e1 Mon Sep 17 00:00:00 2001 From: mrFranklin <5235904+mrFranklin@users.noreply.github.com> Date: Thu, 25 Sep 2025 14:28:49 +0800 Subject: [PATCH 34/70] chore: add a comment explaining why update one second to three second --- tests/local_testing/test_opik.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/local_testing/test_opik.py b/tests/local_testing/test_opik.py index d43b7558f54..7e3fa84df1a 100644 --- a/tests/local_testing/test_opik.py +++ b/tests/local_testing/test_opik.py @@ -120,8 +120,8 @@ def test_sync_opik_logging_http_request(): temperature=0.2, mock_response="This is a mock response", ) - - # Need to wait for a short amount of time as the log_success callback is called in a different thread + + # Need to wait for a short amount of time as the log_success callback is called in a different thread. One or two seconds is often not enough. time.sleep(3) # Check that 5 spans and 5 traces were sent From dd4fa371fa59ef635a86ea3788639c456019fa59 Mon Sep 17 00:00:00 2001 From: mrFranklin <5235904+mrFranklin@users.noreply.github.com> Date: Thu, 25 Sep 2025 15:54:50 +0800 Subject: [PATCH 35/70] fix: trace not pass through when trace_id has settled --- litellm/integrations/opik/opik.py | 38 ++++++++++++++----------------- 1 file changed, 17 insertions(+), 21 deletions(-) diff --git a/litellm/integrations/opik/opik.py b/litellm/integrations/opik/opik.py index 5d98f220f38..9fa3482f663 100644 --- a/litellm/integrations/opik/opik.py +++ b/litellm/integrations/opik/opik.py @@ -224,12 +224,13 @@ class OpikLogger(CustomBatchLogger): else: trace_id = None parent_span_id = None - + # Create Opik tags opik_tags = litellm_opik_metadata.get("tags", []) if kwargs.get("custom_llm_provider"): opik_tags.append(kwargs["custom_llm_provider"]) - # Create thread id + + # Get thread_id if present thread_id = litellm_opik_metadata.get("thread_id", None) # Override with any opik_ headers from proxy request @@ -242,10 +243,6 @@ class OpikLogger(CustomBatchLogger): project_name = value elif param_key == "thread_id" and value: thread_id = value - elif param_key == "trace_id" and value: - trace_id = value - elif param_key == "parent_span_id" and value: - parent_span_id = value elif param_key == "tags" and value: try: parsed_tags = json.loads(value) @@ -319,21 +316,20 @@ class OpikLogger(CustomBatchLogger): verbose_logger.debug( f"OpikLogger creating payload for trace with id {trace_id}" ) - - payload.append( - { - "project_name": project_name, - "id": trace_id, - "name": trace_name, - "start_time": start_time.astimezone(timezone.utc).isoformat().replace("+00:00", "Z"), - "end_time": end_time.astimezone(timezone.utc).isoformat().replace("+00:00", "Z"), - "input": input_data, - "output": output_data, - "metadata": metadata, - "tags": opik_tags, - "thread_id": thread_id, - } - ) + payload.append( + { + "project_name": project_name, + "id": trace_id, + "name": trace_name, + "start_time": start_time.astimezone(timezone.utc).isoformat().replace("+00:00", "Z"), + "end_time": end_time.astimezone(timezone.utc).isoformat().replace("+00:00", "Z"), + "input": input_data, + "output": output_data, + "metadata": metadata, + "tags": opik_tags, + "thread_id": thread_id, + } + ) span_id = create_uuid7() verbose_logger.debug( From 7f06b7bb4e75e889b76e73ffd093c9d5193769a0 Mon Sep 17 00:00:00 2001 From: mrFranklin <5235904+mrFranklin@users.noreply.github.com> Date: Thu, 25 Sep 2025 16:26:34 +0800 Subject: [PATCH 36/70] doc: update opik integration doc --- .../docs/observability/opik_integration.md | 32 ++++++++++++++----- 1 file changed, 24 insertions(+), 8 deletions(-) diff --git a/docs/my-website/docs/observability/opik_integration.md b/docs/my-website/docs/observability/opik_integration.md index b4bcef53937..d30af7d2cb7 100644 --- a/docs/my-website/docs/observability/opik_integration.md +++ b/docs/my-website/docs/observability/opik_integration.md @@ -140,6 +140,7 @@ These can be passed inside metadata with the `opik` key. - `project_name` - Name of the Opik project to send data to. - `current_span_data` - The current span data to be used for tracing. - `tags` - Tags to be used for tracing. +- `thread_id` - The thread id to group together multiple related traces. ### Usage @@ -159,8 +160,10 @@ response = litellm.completion( messages=messages, metadata = { "opik": { + "project_name": "your-opik-project-name", "current_span_data": get_current_span_data(), "tags": ["streaming-test"], + "thread_id": "your-thread-id" }, } ) @@ -174,7 +177,7 @@ curl -L -X POST 'http://0.0.0.0:4000/v1/chat/completions' \ -H 'Content-Type: application/json' \ -H 'Authorization: Bearer sk-1234' \ -d '{ - "model": "gpt-3.5-turbo-testing", + "model": "gpt-3.5-turbo", "messages": [ { "role": "user", @@ -183,8 +186,10 @@ curl -L -X POST 'http://0.0.0.0:4000/v1/chat/completions' \ ], "metadata": { "opik": { + "project_name": "your-opik-project-name", "current_span_data": "...", "tags": ["streaming-test"], + "thread_id": "your-thread-id" }, } }' @@ -193,14 +198,25 @@ curl -L -X POST 'http://0.0.0.0:4000/v1/chat/completions' \ +You can also pass the fields as part of the request header with a `opik_*` prefix: - - - - - - - +```shell +curl --location --request POST 'http://0.0.0.0:4000/chat/completions' \ + --header 'Content-Type: application/json' \ + --header 'Authorization: Bearer sk-1234' \ + --header 'opik_project_name: your-opik-project-name' \ + --header 'opik_thread_id: your-thread-id' \ + --header 'opik_tags: ["streaming-test"]' \ + --data '{ + "model": "gpt-3.5-turbo", + "messages": [ + { + "role": "user", + "content": "What's the weather like in Boston today?" + } + ] +}' +``` From f717e53d3f3a938b4c846e7ec4a4edee68abac6b Mon Sep 17 00:00:00 2001 From: mrFranklin <5235904+mrFranklin@users.noreply.github.com> Date: Thu, 25 Sep 2025 16:28:20 +0800 Subject: [PATCH 37/70] doc: update opik integration doc --- docs/my-website/docs/observability/opik_integration.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/my-website/docs/observability/opik_integration.md b/docs/my-website/docs/observability/opik_integration.md index d30af7d2cb7..1ba1c2de210 100644 --- a/docs/my-website/docs/observability/opik_integration.md +++ b/docs/my-website/docs/observability/opik_integration.md @@ -198,6 +198,8 @@ curl -L -X POST 'http://0.0.0.0:4000/v1/chat/completions' \ + + You can also pass the fields as part of the request header with a `opik_*` prefix: ```shell From 82ec42993f5cf0cb3a06f63b61c517a9c6e8fa4e Mon Sep 17 00:00:00 2001 From: Arshdeep Singh Date: Thu, 25 Sep 2025 11:27:40 -0400 Subject: [PATCH 38/70] fix(presidio): enable custom PII entities with Union type --- .../guardrails/guardrail_hooks/presidio.py | 8 +-- litellm/types/guardrails.py | 2 +- .../test_presidio_union_fix.py | 58 +++++++++++++++++++ 3 files changed, 63 insertions(+), 5 deletions(-) create mode 100644 tests/test_litellm/proxy/guardrails/guardrail_hooks/test_presidio_union_fix.py diff --git a/litellm/proxy/guardrails/guardrail_hooks/presidio.py b/litellm/proxy/guardrails/guardrail_hooks/presidio.py index 8dbcf77a844..c3c6c3d2e03 100644 --- a/litellm/proxy/guardrails/guardrail_hooks/presidio.py +++ b/litellm/proxy/guardrails/guardrail_hooks/presidio.py @@ -82,7 +82,7 @@ class _OPTIONAL_PresidioPIIMasking(CustomGuardrail): ) # mapping of PII token to original text - only used with Presidio `replace` operation self.mock_redacted_text = mock_redacted_text self.output_parse_pii = output_parse_pii or False - self.pii_entities_config: Dict[PiiEntityType, PiiAction] = ( + self.pii_entities_config: Dict[Union[PiiEntityType, str], PiiAction] = ( pii_entities_config or {} ) self.presidio_language = presidio_language or "en" @@ -302,10 +302,10 @@ class _OPTIONAL_PresidioPIIMasking(CustomGuardrail): entity_type = result.get("entity_type") if entity_type: - casted_entity_type: PiiEntityType = cast(PiiEntityType, entity_type) + # Check if entity_type is in config (supports both enum and string) if ( - casted_entity_type in self.pii_entities_config - and self.pii_entities_config[casted_entity_type] == PiiAction.BLOCK + entity_type in self.pii_entities_config + and self.pii_entities_config[entity_type] == PiiAction.BLOCK ): raise BlockedPiiEntityError( entity_type=entity_type, diff --git a/litellm/types/guardrails.py b/litellm/types/guardrails.py index e5ea89325eb..0c3dc662ec4 100644 --- a/litellm/types/guardrails.py +++ b/litellm/types/guardrails.py @@ -253,7 +253,7 @@ class PresidioPresidioConfigModelUserInterface(BaseModel): class PresidioConfigModel(PresidioPresidioConfigModelUserInterface): """Configuration parameters for the Presidio PII masking guardrail""" - pii_entities_config: Optional[Dict[PiiEntityType, PiiAction]] = Field( + pii_entities_config: Optional[Dict[Union[PiiEntityType, str], PiiAction]] = Field( default=None, description="Configuration for PII entity types and actions" ) presidio_ad_hoc_recognizers: Optional[str] = Field( diff --git a/tests/test_litellm/proxy/guardrails/guardrail_hooks/test_presidio_union_fix.py b/tests/test_litellm/proxy/guardrails/guardrail_hooks/test_presidio_union_fix.py new file mode 100644 index 00000000000..549f715ed50 --- /dev/null +++ b/tests/test_litellm/proxy/guardrails/guardrail_hooks/test_presidio_union_fix.py @@ -0,0 +1,58 @@ +""" +Minimal test for Presidio Union[PiiEntityType, str] type fix. +Tests only the core fix without heavy dependencies. +""" +from typing import Union, Dict +from enum import Enum + + +class PiiEntityType(str, Enum): + EMAIL_ADDRESS = "EMAIL_ADDRESS" + + +class PiiAction(str, Enum): + BLOCK = "BLOCK" + MASK = "MASK" + + +def test_presidio_union_type_fix(): + """Test that Union[PiiEntityType, str] allows both enum and string entity types""" + + # Custom recognizers for EMPLOYEE_ID and CUSTOMER_ID + custom_recognizers = [ + { + "name": "Employee ID Recognizer", + "supported_language": "en", + "patterns": [{"name": "employee id", "regex": "EMP-[0-9]{6}", "score": 0.9}], + "context": ["employee", "id"], + "supported_entity": "EMPLOYEE_ID" + }, + { + "name": "Customer ID Recognizer", + "supported_language": "en", + "patterns": [{"name": "customer id", "regex": "CUST-[0-9]{8}", "score": 0.9}], + "context": ["customer", "id"], + "supported_entity": "CUSTOMER_ID" + } + ] + + # This is the core fix - mixed entity types in pii_entities_config + pii_entities_config: Dict[Union[PiiEntityType, str], PiiAction] = { + PiiEntityType.EMAIL_ADDRESS: PiiAction.MASK, + "EMPLOYEE_ID": PiiAction.MASK, + "CUSTOMER_ID": PiiAction.BLOCK, + } + + # Verify entities can be used together (what Presidio needs) + entities_list = list(pii_entities_config.keys()) + assert len(entities_list) == 3 + assert PiiEntityType.EMAIL_ADDRESS in entities_list + assert "EMPLOYEE_ID" in entities_list + assert "CUSTOMER_ID" in entities_list + + print("✅ Union type fix verified: mixed entity types work correctly") + print("✅ Custom recognizers defined for EMPLOYEE_ID and CUSTOMER_ID") + + +if __name__ == "__main__": + test_presidio_union_type_fix() \ No newline at end of file From 771cfd49f0eb59c8eafe7050ea110fe822124bc5 Mon Sep 17 00:00:00 2001 From: Arshdeep Singh Date: Thu, 25 Sep 2025 11:58:24 -0400 Subject: [PATCH 39/70] fix: regenerate poetry.lock to resolve CI dependency issue --- poetry.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/poetry.lock b/poetry.lock index d0c966834d6..220d0ee3fb0 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 2.2.0 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.2.1 and should not be changed by hand. [[package]] name = "aiohappyeyeballs" From 191ff9f9382bcbeebf9cd146ff8de095e3e7c5c0 Mon Sep 17 00:00:00 2001 From: Arshdeep Singh Date: Thu, 25 Sep 2025 12:06:44 -0400 Subject: [PATCH 40/70] fix: regenerate poetry.lock from scratch to resolve dependency issues --- poetry.lock | 3025 +++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 2938 insertions(+), 87 deletions(-) diff --git a/poetry.lock b/poetry.lock index 220d0ee3fb0..3c36e42f839 100644 --- a/poetry.lock +++ b/poetry.lock @@ -7,11 +7,25 @@ description = "Happy Eyeballs for asyncio" optional = false python-versions = ">=3.8" groups = ["main"] +markers = "python_full_version == \"3.8.*\" or platform_python_implementation == \"PyPy\" and python_version < \"3.10\"" files = [ {file = "aiohappyeyeballs-2.4.4-py3-none-any.whl", hash = "sha256:a980909d50efcd44795c4afeca523296716d50cd756ddca6af8c65b996e27de8"}, {file = "aiohappyeyeballs-2.4.4.tar.gz", hash = "sha256:5fdd7d87889c63183afc18ce9271f9b0a7d32c2303e394468dd45d514a757745"}, ] +[[package]] +name = "aiohappyeyeballs" +version = "2.6.1" +description = "Happy Eyeballs for asyncio" +optional = false +python-versions = ">=3.9" +groups = ["main"] +markers = "python_version >= \"3.9\" and platform_python_implementation != \"PyPy\" or python_version >= \"3.10\"" +files = [ + {file = "aiohappyeyeballs-2.6.1-py3-none-any.whl", hash = "sha256:f349ba8f4b75cb25c99c5c2d84e997e485204d2902a9597802b0371f09331fb8"}, + {file = "aiohappyeyeballs-2.6.1.tar.gz", hash = "sha256:c3f9d0113123803ccadfdf3f0faa505bc78e6a72d1cc4806cbd719826e943558"}, +] + [[package]] name = "aiohttp" version = "3.10.11" @@ -19,6 +33,7 @@ description = "Async http client/server framework (asyncio)" optional = false python-versions = ">=3.8" groups = ["main"] +markers = "python_full_version == \"3.8.*\" or platform_python_implementation == \"PyPy\" and python_version < \"3.10\"" files = [ {file = "aiohttp-3.10.11-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:5077b1a5f40ffa3ba1f40d537d3bec4383988ee51fbba6b74aa8fb1bc466599e"}, {file = "aiohttp-3.10.11-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:8d6a14a4d93b5b3c2891fca94fa9d41b2322a68194422bef0dd5ec1e57d7d298"}, @@ -125,6 +140,116 @@ yarl = ">=1.12.0,<2.0" [package.extras] speedups = ["Brotli ; platform_python_implementation == \"CPython\"", "aiodns (>=3.2.0) ; sys_platform == \"linux\" or sys_platform == \"darwin\"", "brotlicffi ; platform_python_implementation != \"CPython\""] +[[package]] +name = "aiohttp" +version = "3.12.15" +description = "Async http client/server framework (asyncio)" +optional = false +python-versions = ">=3.9" +groups = ["main"] +markers = "python_version >= \"3.9\" and platform_python_implementation != \"PyPy\" or python_version >= \"3.10\"" +files = [ + {file = "aiohttp-3.12.15-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:b6fc902bff74d9b1879ad55f5404153e2b33a82e72a95c89cec5eb6cc9e92fbc"}, + {file = "aiohttp-3.12.15-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:098e92835b8119b54c693f2f88a1dec690e20798ca5f5fe5f0520245253ee0af"}, + {file = "aiohttp-3.12.15-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:40b3fee496a47c3b4a39a731954c06f0bd9bd3e8258c059a4beb76ac23f8e421"}, + {file = "aiohttp-3.12.15-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2ce13fcfb0bb2f259fb42106cdc63fa5515fb85b7e87177267d89a771a660b79"}, + {file = "aiohttp-3.12.15-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3beb14f053222b391bf9cf92ae82e0171067cc9c8f52453a0f1ec7c37df12a77"}, + {file = "aiohttp-3.12.15-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4c39e87afe48aa3e814cac5f535bc6199180a53e38d3f51c5e2530f5aa4ec58c"}, + {file = "aiohttp-3.12.15-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d5f1b4ce5bc528a6ee38dbf5f39bbf11dd127048726323b72b8e85769319ffc4"}, + {file = "aiohttp-3.12.15-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1004e67962efabbaf3f03b11b4c43b834081c9e3f9b32b16a7d97d4708a9abe6"}, + {file = "aiohttp-3.12.15-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8faa08fcc2e411f7ab91d1541d9d597d3a90e9004180edb2072238c085eac8c2"}, + {file = "aiohttp-3.12.15-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:fe086edf38b2222328cdf89af0dde2439ee173b8ad7cb659b4e4c6f385b2be3d"}, + {file = "aiohttp-3.12.15-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:79b26fe467219add81d5e47b4a4ba0f2394e8b7c7c3198ed36609f9ba161aecb"}, + {file = "aiohttp-3.12.15-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:b761bac1192ef24e16706d761aefcb581438b34b13a2f069a6d343ec8fb693a5"}, + {file = "aiohttp-3.12.15-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:e153e8adacfe2af562861b72f8bc47f8a5c08e010ac94eebbe33dc21d677cd5b"}, + {file = "aiohttp-3.12.15-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:fc49c4de44977aa8601a00edbf157e9a421f227aa7eb477d9e3df48343311065"}, + {file = "aiohttp-3.12.15-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:2776c7ec89c54a47029940177e75c8c07c29c66f73464784971d6a81904ce9d1"}, + {file = "aiohttp-3.12.15-cp310-cp310-win32.whl", hash = "sha256:2c7d81a277fa78b2203ab626ced1487420e8c11a8e373707ab72d189fcdad20a"}, + {file = "aiohttp-3.12.15-cp310-cp310-win_amd64.whl", hash = "sha256:83603f881e11f0f710f8e2327817c82e79431ec976448839f3cd05d7afe8f830"}, + {file = "aiohttp-3.12.15-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:d3ce17ce0220383a0f9ea07175eeaa6aa13ae5a41f30bc61d84df17f0e9b1117"}, + {file = "aiohttp-3.12.15-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:010cc9bbd06db80fe234d9003f67e97a10fe003bfbedb40da7d71c1008eda0fe"}, + {file = "aiohttp-3.12.15-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3f9d7c55b41ed687b9d7165b17672340187f87a773c98236c987f08c858145a9"}, + {file = "aiohttp-3.12.15-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bc4fbc61bb3548d3b482f9ac7ddd0f18c67e4225aaa4e8552b9f1ac7e6bda9e5"}, + {file = "aiohttp-3.12.15-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7fbc8a7c410bb3ad5d595bb7118147dfbb6449d862cc1125cf8867cb337e8728"}, + {file = "aiohttp-3.12.15-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:74dad41b3458dbb0511e760fb355bb0b6689e0630de8a22b1b62a98777136e16"}, + {file = "aiohttp-3.12.15-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3b6f0af863cf17e6222b1735a756d664159e58855da99cfe965134a3ff63b0b0"}, + {file = "aiohttp-3.12.15-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b5b7fe4972d48a4da367043b8e023fb70a04d1490aa7d68800e465d1b97e493b"}, + {file = "aiohttp-3.12.15-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6443cca89553b7a5485331bc9bedb2342b08d073fa10b8c7d1c60579c4a7b9bd"}, + {file = "aiohttp-3.12.15-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6c5f40ec615e5264f44b4282ee27628cea221fcad52f27405b80abb346d9f3f8"}, + {file = "aiohttp-3.12.15-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:2abbb216a1d3a2fe86dbd2edce20cdc5e9ad0be6378455b05ec7f77361b3ab50"}, + {file = "aiohttp-3.12.15-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:db71ce547012a5420a39c1b744d485cfb823564d01d5d20805977f5ea1345676"}, + {file = "aiohttp-3.12.15-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:ced339d7c9b5030abad5854aa5413a77565e5b6e6248ff927d3e174baf3badf7"}, + {file = "aiohttp-3.12.15-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:7c7dd29c7b5bda137464dc9bfc738d7ceea46ff70309859ffde8c022e9b08ba7"}, + {file = "aiohttp-3.12.15-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:421da6fd326460517873274875c6c5a18ff225b40da2616083c5a34a7570b685"}, + {file = "aiohttp-3.12.15-cp311-cp311-win32.whl", hash = "sha256:4420cf9d179ec8dfe4be10e7d0fe47d6d606485512ea2265b0d8c5113372771b"}, + {file = "aiohttp-3.12.15-cp311-cp311-win_amd64.whl", hash = "sha256:edd533a07da85baa4b423ee8839e3e91681c7bfa19b04260a469ee94b778bf6d"}, + {file = "aiohttp-3.12.15-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:802d3868f5776e28f7bf69d349c26fc0efadb81676d0afa88ed00d98a26340b7"}, + {file = "aiohttp-3.12.15-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f2800614cd560287be05e33a679638e586a2d7401f4ddf99e304d98878c29444"}, + {file = "aiohttp-3.12.15-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8466151554b593909d30a0a125d638b4e5f3836e5aecde85b66b80ded1cb5b0d"}, + {file = "aiohttp-3.12.15-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2e5a495cb1be69dae4b08f35a6c4579c539e9b5706f606632102c0f855bcba7c"}, + {file = "aiohttp-3.12.15-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6404dfc8cdde35c69aaa489bb3542fb86ef215fc70277c892be8af540e5e21c0"}, + {file = "aiohttp-3.12.15-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3ead1c00f8521a5c9070fcb88f02967b1d8a0544e6d85c253f6968b785e1a2ab"}, + {file = "aiohttp-3.12.15-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6990ef617f14450bc6b34941dba4f12d5613cbf4e33805932f853fbd1cf18bfb"}, + {file = "aiohttp-3.12.15-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd736ed420f4db2b8148b52b46b88ed038d0354255f9a73196b7bbce3ea97545"}, + {file = "aiohttp-3.12.15-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c5092ce14361a73086b90c6efb3948ffa5be2f5b6fbcf52e8d8c8b8848bb97c"}, + {file = "aiohttp-3.12.15-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:aaa2234bb60c4dbf82893e934d8ee8dea30446f0647e024074237a56a08c01bd"}, + {file = "aiohttp-3.12.15-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:6d86a2fbdd14192e2f234a92d3b494dd4457e683ba07e5905a0b3ee25389ac9f"}, + {file = "aiohttp-3.12.15-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a041e7e2612041a6ddf1c6a33b883be6a421247c7afd47e885969ee4cc58bd8d"}, + {file = "aiohttp-3.12.15-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5015082477abeafad7203757ae44299a610e89ee82a1503e3d4184e6bafdd519"}, + {file = "aiohttp-3.12.15-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:56822ff5ddfd1b745534e658faba944012346184fbfe732e0d6134b744516eea"}, + {file = "aiohttp-3.12.15-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b2acbbfff69019d9014508c4ba0401822e8bae5a5fdc3b6814285b71231b60f3"}, + {file = "aiohttp-3.12.15-cp312-cp312-win32.whl", hash = "sha256:d849b0901b50f2185874b9a232f38e26b9b3d4810095a7572eacea939132d4e1"}, + {file = "aiohttp-3.12.15-cp312-cp312-win_amd64.whl", hash = "sha256:b390ef5f62bb508a9d67cb3bba9b8356e23b3996da7062f1a57ce1a79d2b3d34"}, + {file = "aiohttp-3.12.15-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:9f922ffd05034d439dde1c77a20461cf4a1b0831e6caa26151fe7aa8aaebc315"}, + {file = "aiohttp-3.12.15-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2ee8a8ac39ce45f3e55663891d4b1d15598c157b4d494a4613e704c8b43112cd"}, + {file = "aiohttp-3.12.15-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3eae49032c29d356b94eee45a3f39fdf4b0814b397638c2f718e96cfadf4c4e4"}, + {file = "aiohttp-3.12.15-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b97752ff12cc12f46a9b20327104448042fce5c33a624f88c18f66f9368091c7"}, + {file = "aiohttp-3.12.15-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:894261472691d6fe76ebb7fcf2e5870a2ac284c7406ddc95823c8598a1390f0d"}, + {file = "aiohttp-3.12.15-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5fa5d9eb82ce98959fc1031c28198b431b4d9396894f385cb63f1e2f3f20ca6b"}, + {file = "aiohttp-3.12.15-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f0fa751efb11a541f57db59c1dd821bec09031e01452b2b6217319b3a1f34f3d"}, + {file = "aiohttp-3.12.15-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5346b93e62ab51ee2a9d68e8f73c7cf96ffb73568a23e683f931e52450e4148d"}, + {file = "aiohttp-3.12.15-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:049ec0360f939cd164ecbfd2873eaa432613d5e77d6b04535e3d1fbae5a9e645"}, + {file = "aiohttp-3.12.15-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b52dcf013b57464b6d1e51b627adfd69a8053e84b7103a7cd49c030f9ca44461"}, + {file = "aiohttp-3.12.15-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:9b2af240143dd2765e0fb661fd0361a1b469cab235039ea57663cda087250ea9"}, + {file = "aiohttp-3.12.15-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ac77f709a2cde2cc71257ab2d8c74dd157c67a0558a0d2799d5d571b4c63d44d"}, + {file = "aiohttp-3.12.15-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:47f6b962246f0a774fbd3b6b7be25d59b06fdb2f164cf2513097998fc6a29693"}, + {file = "aiohttp-3.12.15-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:760fb7db442f284996e39cf9915a94492e1896baac44f06ae551974907922b64"}, + {file = "aiohttp-3.12.15-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ad702e57dc385cae679c39d318def49aef754455f237499d5b99bea4ef582e51"}, + {file = "aiohttp-3.12.15-cp313-cp313-win32.whl", hash = "sha256:f813c3e9032331024de2eb2e32a88d86afb69291fbc37a3a3ae81cc9917fb3d0"}, + {file = "aiohttp-3.12.15-cp313-cp313-win_amd64.whl", hash = "sha256:1a649001580bdb37c6fdb1bebbd7e3bc688e8ec2b5c6f52edbb664662b17dc84"}, + {file = "aiohttp-3.12.15-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:691d203c2bdf4f4637792efbbcdcd157ae11e55eaeb5e9c360c1206fb03d4d98"}, + {file = "aiohttp-3.12.15-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8e995e1abc4ed2a454c731385bf4082be06f875822adc4c6d9eaadf96e20d406"}, + {file = "aiohttp-3.12.15-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:bd44d5936ab3193c617bfd6c9a7d8d1085a8dc8c3f44d5f1dcf554d17d04cf7d"}, + {file = "aiohttp-3.12.15-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:46749be6e89cd78d6068cdf7da51dbcfa4321147ab8e4116ee6678d9a056a0cf"}, + {file = "aiohttp-3.12.15-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0c643f4d75adea39e92c0f01b3fb83d57abdec8c9279b3078b68a3a52b3933b6"}, + {file = "aiohttp-3.12.15-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0a23918fedc05806966a2438489dcffccbdf83e921a1170773b6178d04ade142"}, + {file = "aiohttp-3.12.15-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:74bdd8c864b36c3673741023343565d95bfbd778ffe1eb4d412c135a28a8dc89"}, + {file = "aiohttp-3.12.15-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0a146708808c9b7a988a4af3821379e379e0f0e5e466ca31a73dbdd0325b0263"}, + {file = "aiohttp-3.12.15-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b7011a70b56facde58d6d26da4fec3280cc8e2a78c714c96b7a01a87930a9530"}, + {file = "aiohttp-3.12.15-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:3bdd6e17e16e1dbd3db74d7f989e8af29c4d2e025f9828e6ef45fbdee158ec75"}, + {file = "aiohttp-3.12.15-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:57d16590a351dfc914670bd72530fd78344b885a00b250e992faea565b7fdc05"}, + {file = "aiohttp-3.12.15-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:bc9a0f6569ff990e0bbd75506c8d8fe7214c8f6579cca32f0546e54372a3bb54"}, + {file = "aiohttp-3.12.15-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:536ad7234747a37e50e7b6794ea868833d5220b49c92806ae2d7e8a9d6b5de02"}, + {file = "aiohttp-3.12.15-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:f0adb4177fa748072546fb650d9bd7398caaf0e15b370ed3317280b13f4083b0"}, + {file = "aiohttp-3.12.15-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:14954a2988feae3987f1eb49c706bff39947605f4b6fa4027c1d75743723eb09"}, + {file = "aiohttp-3.12.15-cp39-cp39-win32.whl", hash = "sha256:b784d6ed757f27574dca1c336f968f4e81130b27595e458e69457e6878251f5d"}, + {file = "aiohttp-3.12.15-cp39-cp39-win_amd64.whl", hash = "sha256:86ceded4e78a992f835209e236617bffae649371c4a50d5e5a3987f237db84b8"}, + {file = "aiohttp-3.12.15.tar.gz", hash = "sha256:4fc61385e9c98d72fcdf47e6dd81833f47b2f77c114c29cd64a361be57a763a2"}, +] + +[package.dependencies] +aiohappyeyeballs = ">=2.5.0" +aiosignal = ">=1.4.0" +async-timeout = {version = ">=4.0,<6.0", markers = "python_version < \"3.11\""} +attrs = ">=17.3.0" +frozenlist = ">=1.1.1" +multidict = ">=4.5,<7.0" +propcache = ">=0.2.0" +yarl = ">=1.17.0,<2.0" + +[package.extras] +speedups = ["Brotli ; platform_python_implementation == \"CPython\"", "aiodns (>=3.3.0)", "brotlicffi ; platform_python_implementation != \"CPython\""] + [[package]] name = "aiosignal" version = "1.3.1" @@ -132,6 +257,7 @@ description = "aiosignal: a list of registered asynchronous callbacks" optional = false python-versions = ">=3.7" groups = ["main"] +markers = "python_full_version == \"3.8.*\" or platform_python_implementation == \"PyPy\" and python_version < \"3.10\"" files = [ {file = "aiosignal-1.3.1-py3-none-any.whl", hash = "sha256:f8376fb07dd1e86a584e4fcdec80b36b7f81aac666ebc724e2c090300dd83b17"}, {file = "aiosignal-1.3.1.tar.gz", hash = "sha256:54cd96e15e1649b75d6c87526a6ff0b6c1b0dd3459f43d9ca11d48c339b68cfc"}, @@ -140,6 +266,23 @@ files = [ [package.dependencies] frozenlist = ">=1.1.0" +[[package]] +name = "aiosignal" +version = "1.4.0" +description = "aiosignal: a list of registered asynchronous callbacks" +optional = false +python-versions = ">=3.9" +groups = ["main"] +markers = "python_version >= \"3.9\" and platform_python_implementation != \"PyPy\" or python_version >= \"3.10\"" +files = [ + {file = "aiosignal-1.4.0-py3-none-any.whl", hash = "sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e"}, + {file = "aiosignal-1.4.0.tar.gz", hash = "sha256:f47eecd9468083c2029cc99945502cb7708b082c232f9aca65da147157b251c7"}, +] + +[package.dependencies] +frozenlist = ">=1.1.0" +typing-extensions = {version = ">=4.2", markers = "python_version < \"3.13\""} + [[package]] name = "alabaster" version = "0.7.13" @@ -147,12 +290,38 @@ description = "A configurable sidebar-enabled Sphinx theme" optional = true python-versions = ">=3.6" groups = ["main"] -markers = "extra == \"utils\"" +markers = "(python_full_version == \"3.8.*\" or platform_python_implementation == \"PyPy\") and extra == \"utils\" and python_version < \"3.10\"" files = [ {file = "alabaster-0.7.13-py3-none-any.whl", hash = "sha256:1ee19aca801bbabb5ba3f5f258e4422dfa86f82f3e9cefb0859b283cdd7f62a3"}, {file = "alabaster-0.7.13.tar.gz", hash = "sha256:a27a4a084d5e690e16e01e03ad2b2e552c61a65469419b907243193de1a84ae2"}, ] +[[package]] +name = "alabaster" +version = "0.7.16" +description = "A light, configurable Sphinx theme" +optional = true +python-versions = ">=3.9" +groups = ["main"] +markers = "python_full_version >= \"3.9.0\" and platform_python_implementation != \"PyPy\" and extra == \"utils\" and python_version < \"3.10\"" +files = [ + {file = "alabaster-0.7.16-py3-none-any.whl", hash = "sha256:b46733c07dce03ae4e150330b975c75737fa60f0a7c591b6c8bf4928a28e2c92"}, + {file = "alabaster-0.7.16.tar.gz", hash = "sha256:75a8b99c28a5dad50dd7f8ccdd447a121ddb3892da9e53d1ca5cca3106d58d65"}, +] + +[[package]] +name = "alabaster" +version = "1.0.0" +description = "A light, configurable Sphinx theme" +optional = true +python-versions = ">=3.10" +groups = ["main"] +markers = "python_version >= \"3.10\" and extra == \"utils\"" +files = [ + {file = "alabaster-1.0.0-py3-none-any.whl", hash = "sha256:fc6786402dc3fcb2de3cabd5fe455a2db534b371124f1f21de8731783dec828b"}, + {file = "alabaster-1.0.0.tar.gz", hash = "sha256:c00dca57bca26fa62a6d7d0a9fcce65f3e026e9bfe33e9c538fd3fbb2144fd9e"}, +] + [[package]] name = "alembic" version = "1.16.5" @@ -197,6 +366,7 @@ description = "High level compatibility layer for multiple asynchronous event lo optional = false python-versions = ">=3.8" groups = ["main", "dev", "proxy-dev"] +markers = "python_full_version == \"3.8.*\" or platform_python_implementation == \"PyPy\" and python_version < \"3.10\"" files = [ {file = "anyio-4.5.2-py3-none-any.whl", hash = "sha256:c011ee36bc1e8ba40e5a81cb9df91925c218fe9b778554e0b56a21e1b5d4716f"}, {file = "anyio-4.5.2.tar.gz", hash = "sha256:23009af4ed04ce05991845451e11ef02fc7c5ed29179ac9a420e5ad0ac7ddc5b"}, @@ -213,6 +383,28 @@ doc = ["Sphinx (>=7.4,<8.0)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "truststore (>=0.9.1) ; python_version >= \"3.10\"", "uvloop (>=0.21.0b1) ; platform_python_implementation == \"CPython\" and platform_system != \"Windows\""] trio = ["trio (>=0.26.1)"] +[[package]] +name = "anyio" +version = "4.11.0" +description = "High-level concurrency and networking framework on top of asyncio or Trio" +optional = false +python-versions = ">=3.9" +groups = ["main", "dev", "proxy-dev"] +markers = "python_version >= \"3.9\" and platform_python_implementation != \"PyPy\" or python_version >= \"3.10\"" +files = [ + {file = "anyio-4.11.0-py3-none-any.whl", hash = "sha256:0287e96f4d26d4149305414d4e3bc32f0dcd0862365a4bddea19d7a1ec38c4fc"}, + {file = "anyio-4.11.0.tar.gz", hash = "sha256:82a8d0b81e318cc5ce71a5f1f8b5c4e63619620b63141ef8c995fa0db95a57c4"}, +] + +[package.dependencies] +exceptiongroup = {version = ">=1.0.2", markers = "python_version < \"3.11\""} +idna = ">=2.8" +sniffio = ">=1.1" +typing_extensions = {version = ">=4.5", markers = "python_version < \"3.13\""} + +[package.extras] +trio = ["trio (>=0.31.0)"] + [[package]] name = "apscheduler" version = "3.11.0" @@ -250,7 +442,7 @@ description = "Timeout context manager for asyncio programs" optional = true python-versions = ">=3.8" groups = ["main"] -markers = "python_full_version < \"3.11.3\" and (extra == \"extra-proxy\" or extra == \"proxy\") or python_version <= \"3.10\"" +markers = "python_full_version < \"3.11.3\" and (extra == \"extra-proxy\" or extra == \"proxy\" or python_version <= \"3.10\")" files = [ {file = "async_timeout-5.0.1-py3-none-any.whl", hash = "sha256:39e3809566ff85354557ec2398b55e096c8364bacac9405a7a1fa429e77fe76c"}, {file = "async_timeout-5.0.1.tar.gz", hash = "sha256:d9321a7a3d5a6a5e187e824d2fa0793ce379a202935782d555d6e9d2735677d3"}, @@ -283,6 +475,7 @@ description = "Microsoft Azure Core Library for Python" optional = false python-versions = ">=3.8" groups = ["main", "proxy-dev"] +markers = "python_full_version == \"3.8.*\" or platform_python_implementation == \"PyPy\" and python_version < \"3.10\"" files = [ {file = "azure_core-1.33.0-py3-none-any.whl", hash = "sha256:9b5b6d0223a1d38c37500e6971118c1e0f13f54951e6893968b38910bc9cda8f"}, {file = "azure_core-1.33.0.tar.gz", hash = "sha256:f367aa07b5e3005fec2c1e184b882b0b039910733907d001c20fb08ebb8c0eb9"}, @@ -297,6 +490,28 @@ typing-extensions = ">=4.6.0" aio = ["aiohttp (>=3.0)"] tracing = ["opentelemetry-api (>=1.26,<2.0)"] +[[package]] +name = "azure-core" +version = "1.35.1" +description = "Microsoft Azure Core Library for Python" +optional = false +python-versions = ">=3.9" +groups = ["main", "proxy-dev"] +markers = "python_version >= \"3.9\" and platform_python_implementation != \"PyPy\" or python_version >= \"3.10\"" +files = [ + {file = "azure_core-1.35.1-py3-none-any.whl", hash = "sha256:12da0c9e08e48e198f9158b56ddbe33b421477e1dc98c2e1c8f9e254d92c468b"}, + {file = "azure_core-1.35.1.tar.gz", hash = "sha256:435d05d6df0fff2f73fb3c15493bb4721ede14203f1ff1382aa6b6b2bdd7e562"}, +] + +[package.dependencies] +requests = ">=2.21.0" +six = ">=1.11.0" +typing-extensions = ">=4.6.0" + +[package.extras] +aio = ["aiohttp (>=3.0)"] +tracing = ["opentelemetry-api (>=1.26,<2.0)"] + [[package]] name = "azure-identity" version = "1.21.0" @@ -304,6 +519,7 @@ description = "Microsoft Azure Identity Library for Python" optional = false python-versions = ">=3.8" groups = ["main", "proxy-dev"] +markers = "python_full_version == \"3.8.*\" or platform_python_implementation == \"PyPy\" and python_version < \"3.10\"" files = [ {file = "azure_identity-1.21.0-py3-none-any.whl", hash = "sha256:258ea6325537352440f71b35c3dffe9d240eae4a5126c1b7ce5efd5766bd9fd9"}, {file = "azure_identity-1.21.0.tar.gz", hash = "sha256:ea22ce6e6b0f429bc1b8d9212d5b9f9877bd4c82f1724bfa910760612c07a9a6"}, @@ -316,6 +532,26 @@ msal = ">=1.30.0" msal-extensions = ">=1.2.0" typing-extensions = ">=4.0.0" +[[package]] +name = "azure-identity" +version = "1.25.0" +description = "Microsoft Azure Identity Library for Python" +optional = false +python-versions = ">=3.9" +groups = ["main", "proxy-dev"] +markers = "python_version >= \"3.9\" and platform_python_implementation != \"PyPy\" or python_version >= \"3.10\"" +files = [ + {file = "azure_identity-1.25.0-py3-none-any.whl", hash = "sha256:becaec086bbdf8d1a6aa4fb080c2772a0f824a97d50c29637ec8cc4933f1e82d"}, + {file = "azure_identity-1.25.0.tar.gz", hash = "sha256:4177df34d684cddc026e6cf684e1abb57767aa9d84e7f2129b080ec45eee7733"}, +] + +[package.dependencies] +azure-core = ">=1.31.0" +cryptography = ">=2.5" +msal = ">=1.30.0" +msal-extensions = ">=1.2.0" +typing-extensions = ">=4.0.0" + [[package]] name = "azure-keyvault-secrets" version = "4.9.0" @@ -323,7 +559,7 @@ description = "Microsoft Azure Key Vault Secrets Client Library for Python" optional = true python-versions = ">=3.8" groups = ["main"] -markers = "extra == \"extra-proxy\"" +markers = "(python_full_version == \"3.8.*\" or platform_python_implementation == \"PyPy\") and extra == \"extra-proxy\" and python_version < \"3.10\"" files = [ {file = "azure_keyvault_secrets-4.9.0-py3-none-any.whl", hash = "sha256:33c7e2aca2cc2092cebc8c6e96eca36a5cc30c767e16ea429c5fa21270e9fba6"}, {file = "azure_keyvault_secrets-4.9.0.tar.gz", hash = "sha256:2a03bb2ffd9a0d6c8ad1c330d9d0310113985a9de06607ece378fd72a5889fe1"}, @@ -334,6 +570,24 @@ azure-core = ">=1.31.0" isodate = ">=0.6.1" typing-extensions = ">=4.0.1" +[[package]] +name = "azure-keyvault-secrets" +version = "4.10.0" +description = "Microsoft Corporation Key Vault Secrets Client Library for Python" +optional = true +python-versions = ">=3.9" +groups = ["main"] +markers = "python_version >= \"3.9\" and (python_version >= \"3.10\" or platform_python_implementation != \"PyPy\") and extra == \"extra-proxy\"" +files = [ + {file = "azure_keyvault_secrets-4.10.0-py3-none-any.whl", hash = "sha256:9dbde256077a4ee1a847646671580692e3f9bea36bcfc189c3cf2b9a94eb38b9"}, + {file = "azure_keyvault_secrets-4.10.0.tar.gz", hash = "sha256:666fa42892f9cee749563e551a90f060435ab878977c95265173a8246d546a36"}, +] + +[package.dependencies] +azure-core = ">=1.31.0" +isodate = ">=0.6.1" +typing-extensions = ">=4.6.0" + [[package]] name = "azure-storage-blob" version = "12.26.0" @@ -386,7 +640,7 @@ files = [ {file = "backoff-2.2.1-py3-none-any.whl", hash = "sha256:63579f9a0628e06278f7e47b7d7d5b6ce20dc65c5e96a6f3ca99a6adca0396e8"}, {file = "backoff-2.2.1.tar.gz", hash = "sha256:03f829f5bb1923180821643f8753b0502c3b682293992485b0eef2807afa5cba"}, ] -markers = {main = "python_version >= \"3.9\" and (extra == \"semantic-router\" or extra == \"proxy\") or extra == \"proxy\""} +markers = {main = "extra == \"proxy\" or python_version >= \"3.13\" and (extra == \"proxy\" or extra == \"semantic-router\")"} [[package]] name = "backports-zoneinfo" @@ -395,7 +649,7 @@ description = "Backport of the standard library zoneinfo module" optional = true python-versions = ">=3.6" groups = ["main"] -markers = "extra == \"proxy\" and python_version < \"3.9\"" +markers = "(python_full_version == \"3.8.*\" or platform_python_implementation == \"PyPy\") and python_version < \"3.9\" and extra == \"proxy\"" files = [ {file = "backports.zoneinfo-0.2.1-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:da6013fd84a690242c310d77ddb8441a559e9cb3d3d59ebac9aca1a57b2e18bc"}, {file = "backports.zoneinfo-0.2.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:89a48c0d158a3cc3f654da4c2de1ceba85263fafb861b98b59040a5086259722"}, @@ -449,6 +703,7 @@ files = [ {file = "black-23.12.1-py3-none-any.whl", hash = "sha256:78baad24af0f033958cad29731e27363183e140962595def56423e626f4bee3e"}, {file = "black-23.12.1.tar.gz", hash = "sha256:4ce3ef14ebe8d9509188014d96af1c456a910d5b5cbf434a09fef7e024b3d0d5"}, ] +markers = {main = "python_version >= \"3.13\""} [package.dependencies] click = ">=8.0.0" @@ -530,7 +785,7 @@ description = "Extensible memoizing collections and decorators" optional = true python-versions = ">=3.7" groups = ["main"] -markers = "python_version >= \"3.10\" and (extra == \"mlflow\" or extra == \"extra-proxy\") or extra == \"extra-proxy\"" +markers = "(extra == \"mlflow\" or extra == \"extra-proxy\") and python_version >= \"3.10\" or extra == \"extra-proxy\"" files = [ {file = "cachetools-5.5.2-py3-none-any.whl", hash = "sha256:d26a22bcc62eb95c3beabd9f1ee5e820d3d2704fe2967cbe350e20c8ffcd3f0a"}, {file = "cachetools-5.5.2.tar.gz", hash = "sha256:1a661caa9175d26759571b2e19580f9d6393969e5dfca11fdb1f947a23e640d4"}, @@ -555,7 +810,7 @@ description = "Foreign Function Interface for Python calling C code." optional = false python-versions = ">=3.8" groups = ["main", "dev", "proxy-dev"] -markers = "python_version < \"3.10\" and platform_python_implementation != \"PyPy\"" +markers = "python_full_version == \"3.8.*\" and platform_python_implementation != \"PyPy\"" files = [ {file = "cffi-1.17.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14"}, {file = "cffi-1.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67"}, @@ -636,7 +891,7 @@ description = "Foreign Function Interface for Python calling C code." optional = false python-versions = ">=3.9" groups = ["main", "dev", "proxy-dev"] -markers = "python_version >= \"3.10\" and platform_python_implementation != \"PyPy\"" +markers = "python_version >= \"3.9\" and platform_python_implementation != \"PyPy\"" files = [ {file = "cffi-2.0.0-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:0cf2d91ecc3fcc0625c2c530fe004f82c110405f101548512cce44322fa8ac44"}, {file = "cffi-2.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f73b96c41e3b2adedc34a7356e64c8eb96e03a3782b535e043a986276ce12a49"}, @@ -823,6 +1078,7 @@ description = "Composable command line interface toolkit" optional = false python-versions = ">=3.7" groups = ["main", "dev", "proxy-dev"] +markers = "python_version < \"3.10\"" files = [ {file = "click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2"}, {file = "click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a"}, @@ -831,6 +1087,22 @@ files = [ [package.dependencies] colorama = {version = "*", markers = "platform_system == \"Windows\""} +[[package]] +name = "click" +version = "8.3.0" +description = "Composable command line interface toolkit" +optional = false +python-versions = ">=3.10" +groups = ["main", "dev", "proxy-dev"] +markers = "python_version >= \"3.10\"" +files = [ + {file = "click-8.3.0-py3-none-any.whl", hash = "sha256:9b9f285302c6e3064f4330c05f05b81945b2a39544279343e6e7c5f27a9baddc"}, + {file = "click-8.3.0.tar.gz", hash = "sha256:e7b8232224eba16f4ebe410c25ced9f7875cb5f3263ffc93cc3e8da705e229c4"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} + [[package]] name = "cloudpickle" version = "3.1.1" @@ -851,7 +1123,7 @@ description = "Python SDK for the Cohere API" optional = true python-versions = ">=3.8,<4.0" groups = ["main"] -markers = "python_version >= \"3.9\" and extra == \"semantic-router\"" +markers = "python_version >= \"3.13\" and extra == \"semantic-router\"" files = [ {file = "cohere-4.57-py3-none-any.whl", hash = "sha256:479bdea81ae119e53f671f1ae808fcff9df88211780525d7ef2f7b99dfb32e59"}, {file = "cohere-4.57.tar.gz", hash = "sha256:71ace0204a92d1a2a8d4b949b88b353b4f22fc645486851924284cc5a0eb700d"}, @@ -876,7 +1148,7 @@ files = [ {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, ] -markers = {main = "platform_system == \"Windows\" or sys_platform == \"win32\" and (extra == \"utils\" or extra == \"semantic-router\") and python_version >= \"3.9\" or sys_platform == \"win32\" and extra == \"utils\"", dev = "platform_system == \"Windows\" or sys_platform == \"win32\"", proxy-dev = "platform_system == \"Windows\""} +markers = {main = "platform_system == \"Windows\" or sys_platform == \"win32\" and python_version >= \"3.9\" and (extra == \"utils\" or extra == \"semantic-router\") or sys_platform == \"win32\" and extra == \"utils\" or python_version >= \"3.9\" and python_version <= \"3.12\" and extra == \"semantic-router\"", dev = "platform_system == \"Windows\" or sys_platform == \"win32\"", proxy-dev = "platform_system == \"Windows\""} [[package]] name = "coloredlogs" @@ -923,7 +1195,7 @@ description = "Python library for calculating contours of 2D quadrilateral grids optional = true python-versions = ">=3.10" groups = ["main"] -markers = "python_version >= \"3.10\" and extra == \"mlflow\"" +markers = "python_version == \"3.10\" and extra == \"mlflow\"" files = [ {file = "contourpy-1.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ba38e3f9f330af820c4b27ceb4b9c7feee5fe0493ea53a8720f4792667465934"}, {file = "contourpy-1.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dc41ba0714aa2968d1f8674ec97504a8f7e334f48eeacebcaa6256213acb0989"}, @@ -994,6 +1266,116 @@ mypy = ["bokeh", "contourpy[bokeh,docs]", "docutils-stubs", "mypy (==1.15.0)", " test = ["Pillow", "contourpy[test-no-images]", "matplotlib"] test-no-images = ["pytest", "pytest-cov", "pytest-rerunfailures", "pytest-xdist", "wurlitzer"] +[[package]] +name = "contourpy" +version = "1.3.3" +description = "Python library for calculating contours of 2D quadrilateral grids" +optional = true +python-versions = ">=3.11" +groups = ["main"] +markers = "python_version >= \"3.11\" and extra == \"mlflow\"" +files = [ + {file = "contourpy-1.3.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:709a48ef9a690e1343202916450bc48b9e51c049b089c7f79a267b46cffcdaa1"}, + {file = "contourpy-1.3.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:23416f38bfd74d5d28ab8429cc4d63fa67d5068bd711a85edb1c3fb0c3e2f381"}, + {file = "contourpy-1.3.3-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:929ddf8c4c7f348e4c0a5a3a714b5c8542ffaa8c22954862a46ca1813b667ee7"}, + {file = "contourpy-1.3.3-cp311-cp311-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9e999574eddae35f1312c2b4b717b7885d4edd6cb46700e04f7f02db454e67c1"}, + {file = "contourpy-1.3.3-cp311-cp311-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0bf67e0e3f482cb69779dd3061b534eb35ac9b17f163d851e2a547d56dba0a3a"}, + {file = "contourpy-1.3.3-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:51e79c1f7470158e838808d4a996fa9bac72c498e93d8ebe5119bc1e6becb0db"}, + {file = "contourpy-1.3.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:598c3aaece21c503615fd59c92a3598b428b2f01bfb4b8ca9c4edeecc2438620"}, + {file = "contourpy-1.3.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:322ab1c99b008dad206d406bb61d014cf0174df491ae9d9d0fac6a6fda4f977f"}, + {file = "contourpy-1.3.3-cp311-cp311-win32.whl", hash = "sha256:fd907ae12cd483cd83e414b12941c632a969171bf90fc937d0c9f268a31cafff"}, + {file = "contourpy-1.3.3-cp311-cp311-win_amd64.whl", hash = "sha256:3519428f6be58431c56581f1694ba8e50626f2dd550af225f82fb5f5814d2a42"}, + {file = "contourpy-1.3.3-cp311-cp311-win_arm64.whl", hash = "sha256:15ff10bfada4bf92ec8b31c62bf7c1834c244019b4a33095a68000d7075df470"}, + {file = "contourpy-1.3.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b08a32ea2f8e42cf1d4be3169a98dd4be32bafe4f22b6c4cb4ba810fa9e5d2cb"}, + {file = "contourpy-1.3.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:556dba8fb6f5d8742f2923fe9457dbdd51e1049c4a43fd3986a0b14a1d815fc6"}, + {file = "contourpy-1.3.3-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92d9abc807cf7d0e047b95ca5d957cf4792fcd04e920ca70d48add15c1a90ea7"}, + {file = "contourpy-1.3.3-cp312-cp312-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b2e8faa0ed68cb29af51edd8e24798bb661eac3bd9f65420c1887b6ca89987c8"}, + {file = "contourpy-1.3.3-cp312-cp312-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:626d60935cf668e70a5ce6ff184fd713e9683fb458898e4249b63be9e28286ea"}, + {file = "contourpy-1.3.3-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4d00e655fcef08aba35ec9610536bfe90267d7ab5ba944f7032549c55a146da1"}, + {file = "contourpy-1.3.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:451e71b5a7d597379ef572de31eeb909a87246974d960049a9848c3bc6c41bf7"}, + {file = "contourpy-1.3.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:459c1f020cd59fcfe6650180678a9993932d80d44ccde1fa1868977438f0b411"}, + {file = "contourpy-1.3.3-cp312-cp312-win32.whl", hash = "sha256:023b44101dfe49d7d53932be418477dba359649246075c996866106da069af69"}, + {file = "contourpy-1.3.3-cp312-cp312-win_amd64.whl", hash = "sha256:8153b8bfc11e1e4d75bcb0bff1db232f9e10b274e0929de9d608027e0d34ff8b"}, + {file = "contourpy-1.3.3-cp312-cp312-win_arm64.whl", hash = "sha256:07ce5ed73ecdc4a03ffe3e1b3e3c1166db35ae7584be76f65dbbe28a7791b0cc"}, + {file = "contourpy-1.3.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:177fb367556747a686509d6fef71d221a4b198a3905fe824430e5ea0fda54eb5"}, + {file = "contourpy-1.3.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d002b6f00d73d69333dac9d0b8d5e84d9724ff9ef044fd63c5986e62b7c9e1b1"}, + {file = "contourpy-1.3.3-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:348ac1f5d4f1d66d3322420f01d42e43122f43616e0f194fc1c9f5d830c5b286"}, + {file = "contourpy-1.3.3-cp313-cp313-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:655456777ff65c2c548b7c454af9c6f33f16c8884f11083244b5819cc214f1b5"}, + {file = "contourpy-1.3.3-cp313-cp313-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:644a6853d15b2512d67881586bd03f462c7ab755db95f16f14d7e238f2852c67"}, + {file = "contourpy-1.3.3-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4debd64f124ca62069f313a9cb86656ff087786016d76927ae2cf37846b006c9"}, + {file = "contourpy-1.3.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a15459b0f4615b00bbd1e91f1b9e19b7e63aea7483d03d804186f278c0af2659"}, + {file = "contourpy-1.3.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ca0fdcd73925568ca027e0b17ab07aad764be4706d0a925b89227e447d9737b7"}, + {file = "contourpy-1.3.3-cp313-cp313-win32.whl", hash = "sha256:b20c7c9a3bf701366556e1b1984ed2d0cedf999903c51311417cf5f591d8c78d"}, + {file = "contourpy-1.3.3-cp313-cp313-win_amd64.whl", hash = "sha256:1cadd8b8969f060ba45ed7c1b714fe69185812ab43bd6b86a9123fe8f99c3263"}, + {file = "contourpy-1.3.3-cp313-cp313-win_arm64.whl", hash = "sha256:fd914713266421b7536de2bfa8181aa8c699432b6763a0ea64195ebe28bff6a9"}, + {file = "contourpy-1.3.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:88df9880d507169449d434c293467418b9f6cbe82edd19284aa0409e7fdb933d"}, + {file = "contourpy-1.3.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:d06bb1f751ba5d417047db62bca3c8fde202b8c11fb50742ab3ab962c81e8216"}, + {file = "contourpy-1.3.3-cp313-cp313t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e4e6b05a45525357e382909a4c1600444e2a45b4795163d3b22669285591c1ae"}, + {file = "contourpy-1.3.3-cp313-cp313t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ab3074b48c4e2cf1a960e6bbeb7f04566bf36b1861d5c9d4d8ac04b82e38ba20"}, + {file = "contourpy-1.3.3-cp313-cp313t-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6c3d53c796f8647d6deb1abe867daeb66dcc8a97e8455efa729516b997b8ed99"}, + {file = "contourpy-1.3.3-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:50ed930df7289ff2a8d7afeb9603f8289e5704755c7e5c3bbd929c90c817164b"}, + {file = "contourpy-1.3.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4feffb6537d64b84877da813a5c30f1422ea5739566abf0bd18065ac040e120a"}, + {file = "contourpy-1.3.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:2b7e9480ffe2b0cd2e787e4df64270e3a0440d9db8dc823312e2c940c167df7e"}, + {file = "contourpy-1.3.3-cp313-cp313t-win32.whl", hash = "sha256:283edd842a01e3dcd435b1c5116798d661378d83d36d337b8dde1d16a5fc9ba3"}, + {file = "contourpy-1.3.3-cp313-cp313t-win_amd64.whl", hash = "sha256:87acf5963fc2b34825e5b6b048f40e3635dd547f590b04d2ab317c2619ef7ae8"}, + {file = "contourpy-1.3.3-cp313-cp313t-win_arm64.whl", hash = "sha256:3c30273eb2a55024ff31ba7d052dde990d7d8e5450f4bbb6e913558b3d6c2301"}, + {file = "contourpy-1.3.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fde6c716d51c04b1c25d0b90364d0be954624a0ee9d60e23e850e8d48353d07a"}, + {file = "contourpy-1.3.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:cbedb772ed74ff5be440fa8eee9bd49f64f6e3fc09436d9c7d8f1c287b121d77"}, + {file = "contourpy-1.3.3-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:22e9b1bd7a9b1d652cd77388465dc358dafcd2e217d35552424aa4f996f524f5"}, + {file = "contourpy-1.3.3-cp314-cp314-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a22738912262aa3e254e4f3cb079a95a67132fc5a063890e224393596902f5a4"}, + {file = "contourpy-1.3.3-cp314-cp314-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:afe5a512f31ee6bd7d0dda52ec9864c984ca3d66664444f2d72e0dc4eb832e36"}, + {file = "contourpy-1.3.3-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f64836de09927cba6f79dcd00fdd7d5329f3fccc633468507079c829ca4db4e3"}, + {file = "contourpy-1.3.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:1fd43c3be4c8e5fd6e4f2baeae35ae18176cf2e5cced681cca908addf1cdd53b"}, + {file = "contourpy-1.3.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6afc576f7b33cf00996e5c1102dc2a8f7cc89e39c0b55df93a0b78c1bd992b36"}, + {file = "contourpy-1.3.3-cp314-cp314-win32.whl", hash = "sha256:66c8a43a4f7b8df8b71ee1840e4211a3c8d93b214b213f590e18a1beca458f7d"}, + {file = "contourpy-1.3.3-cp314-cp314-win_amd64.whl", hash = "sha256:cf9022ef053f2694e31d630feaacb21ea24224be1c3ad0520b13d844274614fd"}, + {file = "contourpy-1.3.3-cp314-cp314-win_arm64.whl", hash = "sha256:95b181891b4c71de4bb404c6621e7e2390745f887f2a026b2d99e92c17892339"}, + {file = "contourpy-1.3.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:33c82d0138c0a062380332c861387650c82e4cf1747aaa6938b9b6516762e772"}, + {file = "contourpy-1.3.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ea37e7b45949df430fe649e5de8351c423430046a2af20b1c1961cae3afcda77"}, + {file = "contourpy-1.3.3-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d304906ecc71672e9c89e87c4675dc5c2645e1f4269a5063b99b0bb29f232d13"}, + {file = "contourpy-1.3.3-cp314-cp314t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ca658cd1a680a5c9ea96dc61cdbae1e85c8f25849843aa799dfd3cb370ad4fbe"}, + {file = "contourpy-1.3.3-cp314-cp314t-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ab2fd90904c503739a75b7c8c5c01160130ba67944a7b77bbf36ef8054576e7f"}, + {file = "contourpy-1.3.3-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b7301b89040075c30e5768810bc96a8e8d78085b47d8be6e4c3f5a0b4ed478a0"}, + {file = "contourpy-1.3.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:2a2a8b627d5cc6b7c41a4beff6c5ad5eb848c88255fda4a8745f7e901b32d8e4"}, + {file = "contourpy-1.3.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:fd6ec6be509c787f1caf6b247f0b1ca598bef13f4ddeaa126b7658215529ba0f"}, + {file = "contourpy-1.3.3-cp314-cp314t-win32.whl", hash = "sha256:e74a9a0f5e3fff48fb5a7f2fd2b9b70a3fe014a67522f79b7cca4c0c7e43c9ae"}, + {file = "contourpy-1.3.3-cp314-cp314t-win_amd64.whl", hash = "sha256:13b68d6a62db8eafaebb8039218921399baf6e47bf85006fd8529f2a08ef33fc"}, + {file = "contourpy-1.3.3-cp314-cp314t-win_arm64.whl", hash = "sha256:b7448cb5a725bb1e35ce88771b86fba35ef418952474492cf7c764059933ff8b"}, + {file = "contourpy-1.3.3-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:cd5dfcaeb10f7b7f9dc8941717c6c2ade08f587be2226222c12b25f0483ed497"}, + {file = "contourpy-1.3.3-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:0c1fc238306b35f246d61a1d416a627348b5cf0648648a031e14bb8705fcdfe8"}, + {file = "contourpy-1.3.3-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:70f9aad7de812d6541d29d2bbf8feb22ff7e1c299523db288004e3157ff4674e"}, + {file = "contourpy-1.3.3-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5ed3657edf08512fc3fe81b510e35c2012fbd3081d2e26160f27ca28affec989"}, + {file = "contourpy-1.3.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:3d1a3799d62d45c18bafd41c5fa05120b96a28079f2393af559b843d1a966a77"}, + {file = "contourpy-1.3.3.tar.gz", hash = "sha256:083e12155b210502d0bca491432bb04d56dc3432f95a979b429f2848c3dbe880"}, +] + +[package.dependencies] +numpy = ">=1.25" + +[package.extras] +bokeh = ["bokeh", "selenium"] +docs = ["furo", "sphinx (>=7.2)", "sphinx-copybutton"] +mypy = ["bokeh", "contourpy[bokeh,docs]", "docutils-stubs", "mypy (==1.17.0)", "types-Pillow"] +test = ["Pillow", "contourpy[test-no-images]", "matplotlib"] +test-no-images = ["pytest", "pytest-cov", "pytest-rerunfailures", "pytest-xdist", "wurlitzer"] + +[[package]] +name = "croniter" +version = "6.0.0" +description = "croniter provides iteration for datetime object with cron like format" +optional = true +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.6" +groups = ["main"] +markers = "python_version >= \"3.9\" and (python_version >= \"3.10\" or platform_python_implementation != \"PyPy\") and extra == \"proxy\"" +files = [ + {file = "croniter-6.0.0-py2.py3-none-any.whl", hash = "sha256:2f878c3856f17896979b2a4379ba1f09c83e374931ea15cc835c5dd2eee9b368"}, + {file = "croniter-6.0.0.tar.gz", hash = "sha256:37c504b313956114a983ece2c2b07790b1f1094fe9d81cc94739214748255577"}, +] + +[package.dependencies] +python-dateutil = "*" +pytz = ">2021.1" + [[package]] name = "cryptography" version = "43.0.3" @@ -1001,6 +1383,7 @@ description = "cryptography is a package which provides cryptographic recipes an optional = false python-versions = ">=3.7" groups = ["main", "dev", "proxy-dev"] +markers = "(python_full_version >= \"3.9.0\" or platform_python_implementation == \"PyPy\") and python_version < \"3.10\"" files = [ {file = "cryptography-43.0.3-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:bf7a1932ac4176486eab36a19ed4c0492da5d97123f1406cf15e41b05e787d2e"}, {file = "cryptography-43.0.3-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:63efa177ff54aec6e1c0aefaa1a241232dcd37413835a9b674b6e3f0ae2bfd3e"}, @@ -1044,6 +1427,146 @@ ssh = ["bcrypt (>=3.1.5)"] test = ["certifi", "cryptography-vectors (==43.0.3)", "pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-xdist"] test-randomorder = ["pytest-randomly"] +[[package]] +name = "cryptography" +version = "45.0.7" +description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." +optional = false +python-versions = "!=3.9.0,!=3.9.1,>=3.7" +groups = ["main", "dev", "proxy-dev"] +markers = "python_version >= \"3.10\"" +files = [ + {file = "cryptography-45.0.7-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:3be4f21c6245930688bd9e162829480de027f8bf962ede33d4f8ba7d67a00cee"}, + {file = "cryptography-45.0.7-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:67285f8a611b0ebc0857ced2081e30302909f571a46bfa7a3cc0ad303fe015c6"}, + {file = "cryptography-45.0.7-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:577470e39e60a6cd7780793202e63536026d9b8641de011ed9d8174da9ca5339"}, + {file = "cryptography-45.0.7-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:4bd3e5c4b9682bc112d634f2c6ccc6736ed3635fc3319ac2bb11d768cc5a00d8"}, + {file = "cryptography-45.0.7-cp311-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:465ccac9d70115cd4de7186e60cfe989de73f7bb23e8a7aa45af18f7412e75bf"}, + {file = "cryptography-45.0.7-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:16ede8a4f7929b4b7ff3642eba2bf79aa1d71f24ab6ee443935c0d269b6bc513"}, + {file = "cryptography-45.0.7-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:8978132287a9d3ad6b54fcd1e08548033cc09dc6aacacb6c004c73c3eb5d3ac3"}, + {file = "cryptography-45.0.7-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:b6a0e535baec27b528cb07a119f321ac024592388c5681a5ced167ae98e9fff3"}, + {file = "cryptography-45.0.7-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:a24ee598d10befaec178efdff6054bc4d7e883f615bfbcd08126a0f4931c83a6"}, + {file = "cryptography-45.0.7-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:fa26fa54c0a9384c27fcdc905a2fb7d60ac6e47d14bc2692145f2b3b1e2cfdbd"}, + {file = "cryptography-45.0.7-cp311-abi3-win32.whl", hash = "sha256:bef32a5e327bd8e5af915d3416ffefdbe65ed975b646b3805be81b23580b57b8"}, + {file = "cryptography-45.0.7-cp311-abi3-win_amd64.whl", hash = "sha256:3808e6b2e5f0b46d981c24d79648e5c25c35e59902ea4391a0dcb3e667bf7443"}, + {file = "cryptography-45.0.7-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:bfb4c801f65dd61cedfc61a83732327fafbac55a47282e6f26f073ca7a41c3b2"}, + {file = "cryptography-45.0.7-cp37-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:81823935e2f8d476707e85a78a405953a03ef7b7b4f55f93f7c2d9680e5e0691"}, + {file = "cryptography-45.0.7-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3994c809c17fc570c2af12c9b840d7cea85a9fd3e5c0e0491f4fa3c029216d59"}, + {file = "cryptography-45.0.7-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:dad43797959a74103cb59c5dac71409f9c27d34c8a05921341fb64ea8ccb1dd4"}, + {file = "cryptography-45.0.7-cp37-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:ce7a453385e4c4693985b4a4a3533e041558851eae061a58a5405363b098fcd3"}, + {file = "cryptography-45.0.7-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:b04f85ac3a90c227b6e5890acb0edbaf3140938dbecf07bff618bf3638578cf1"}, + {file = "cryptography-45.0.7-cp37-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:48c41a44ef8b8c2e80ca4527ee81daa4c527df3ecbc9423c41a420a9559d0e27"}, + {file = "cryptography-45.0.7-cp37-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:f3df7b3d0f91b88b2106031fd995802a2e9ae13e02c36c1fc075b43f420f3a17"}, + {file = "cryptography-45.0.7-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:dd342f085542f6eb894ca00ef70236ea46070c8a13824c6bde0dfdcd36065b9b"}, + {file = "cryptography-45.0.7-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:1993a1bb7e4eccfb922b6cd414f072e08ff5816702a0bdb8941c247a6b1b287c"}, + {file = "cryptography-45.0.7-cp37-abi3-win32.whl", hash = "sha256:18fcf70f243fe07252dcb1b268a687f2358025ce32f9f88028ca5c364b123ef5"}, + {file = "cryptography-45.0.7-cp37-abi3-win_amd64.whl", hash = "sha256:7285a89df4900ed3bfaad5679b1e668cb4b38a8de1ccbfc84b05f34512da0a90"}, + {file = "cryptography-45.0.7-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:de58755d723e86175756f463f2f0bddd45cc36fbd62601228a3f8761c9f58252"}, + {file = "cryptography-45.0.7-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:a20e442e917889d1a6b3c570c9e3fa2fdc398c20868abcea268ea33c024c4083"}, + {file = "cryptography-45.0.7-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:258e0dff86d1d891169b5af222d362468a9570e2532923088658aa866eb11130"}, + {file = "cryptography-45.0.7-pp310-pypy310_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:d97cf502abe2ab9eff8bd5e4aca274da8d06dd3ef08b759a8d6143f4ad65d4b4"}, + {file = "cryptography-45.0.7-pp310-pypy310_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:c987dad82e8c65ebc985f5dae5e74a3beda9d0a2a4daf8a1115f3772b59e5141"}, + {file = "cryptography-45.0.7-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:c13b1e3afd29a5b3b2656257f14669ca8fa8d7956d509926f0b130b600b50ab7"}, + {file = "cryptography-45.0.7-pp311-pypy311_pp73-macosx_10_9_x86_64.whl", hash = "sha256:4a862753b36620af6fc54209264f92c716367f2f0ff4624952276a6bbd18cbde"}, + {file = "cryptography-45.0.7-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:06ce84dc14df0bf6ea84666f958e6080cdb6fe1231be2a51f3fc1267d9f3fb34"}, + {file = "cryptography-45.0.7-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:d0c5c6bac22b177bf8da7435d9d27a6834ee130309749d162b26c3105c0795a9"}, + {file = "cryptography-45.0.7-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:2f641b64acc00811da98df63df7d59fd4706c0df449da71cb7ac39a0732b40ae"}, + {file = "cryptography-45.0.7-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:f5414a788ecc6ee6bc58560e85ca624258a55ca434884445440a810796ea0e0b"}, + {file = "cryptography-45.0.7-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:1f3d56f73595376f4244646dd5c5870c14c196949807be39e79e7bd9bac3da63"}, + {file = "cryptography-45.0.7.tar.gz", hash = "sha256:4b1654dfc64ea479c242508eb8c724044f1e964a47d1d1cacc5132292d851971"}, +] + +[package.dependencies] +cffi = {version = ">=1.14", markers = "platform_python_implementation != \"PyPy\""} + +[package.extras] +docs = ["sphinx (>=5.3.0)", "sphinx-inline-tabs ; python_full_version >= \"3.8.0\"", "sphinx-rtd-theme (>=3.0.0) ; python_full_version >= \"3.8.0\""] +docstest = ["pyenchant (>=3)", "readme-renderer (>=30.0)", "sphinxcontrib-spelling (>=7.3.1)"] +nox = ["nox (>=2024.4.15)", "nox[uv] (>=2024.3.2) ; python_full_version >= \"3.8.0\""] +pep8test = ["check-sdist ; python_full_version >= \"3.8.0\"", "click (>=8.0.1)", "mypy (>=1.4)", "ruff (>=0.3.6)"] +sdist = ["build (>=1.0.0)"] +ssh = ["bcrypt (>=3.1.5)"] +test = ["certifi (>=2024)", "cryptography-vectors (==45.0.7)", "pretend (>=0.7)", "pytest (>=7.4.0)", "pytest-benchmark (>=4.0)", "pytest-cov (>=2.10.1)", "pytest-xdist (>=3.5.0)"] +test-randomorder = ["pytest-randomly"] + +[[package]] +name = "cryptography" +version = "46.0.1" +description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." +optional = false +python-versions = "!=3.9.0,!=3.9.1,>=3.8" +groups = ["main", "dev", "proxy-dev"] +markers = "python_full_version == \"3.8.*\" and platform_python_implementation != \"PyPy\"" +files = [ + {file = "cryptography-46.0.1-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:1cd6d50c1a8b79af1a6f703709d8973845f677c8e97b1268f5ff323d38ce8475"}, + {file = "cryptography-46.0.1-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0ff483716be32690c14636e54a1f6e2e1b7bf8e22ca50b989f88fa1b2d287080"}, + {file = "cryptography-46.0.1-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9873bf7c1f2a6330bdfe8621e7ce64b725784f9f0c3a6a55c3047af5849f920e"}, + {file = "cryptography-46.0.1-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:0dfb7c88d4462a0cfdd0d87a3c245a7bc3feb59de101f6ff88194f740f72eda6"}, + {file = "cryptography-46.0.1-cp311-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e22801b61613ebdebf7deb18b507919e107547a1d39a3b57f5f855032dd7cfb8"}, + {file = "cryptography-46.0.1-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:757af4f6341ce7a1e47c326ca2a81f41d236070217e5fbbad61bbfe299d55d28"}, + {file = "cryptography-46.0.1-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:f7a24ea78de345cfa7f6a8d3bde8b242c7fac27f2bd78fa23474ca38dfaeeab9"}, + {file = "cryptography-46.0.1-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:9e8776dac9e660c22241b6587fae51a67b4b0147daa4d176b172c3ff768ad736"}, + {file = "cryptography-46.0.1-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:9f40642a140c0c8649987027867242b801486865277cbabc8c6059ddef16dc8b"}, + {file = "cryptography-46.0.1-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:449ef2b321bec7d97ef2c944173275ebdab78f3abdd005400cc409e27cd159ab"}, + {file = "cryptography-46.0.1-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:2dd339ba3345b908fa3141ddba4025568fa6fd398eabce3ef72a29ac2d73ad75"}, + {file = "cryptography-46.0.1-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:7411c910fb2a412053cf33cfad0153ee20d27e256c6c3f14d7d7d1d9fec59fd5"}, + {file = "cryptography-46.0.1-cp311-abi3-win32.whl", hash = "sha256:cbb8e769d4cac884bb28e3ff620ef1001b75588a5c83c9c9f1fdc9afbe7f29b0"}, + {file = "cryptography-46.0.1-cp311-abi3-win_amd64.whl", hash = "sha256:92e8cfe8bd7dd86eac0a677499894862cd5cc2fd74de917daa881d00871ac8e7"}, + {file = "cryptography-46.0.1-cp311-abi3-win_arm64.whl", hash = "sha256:db5597a4c7353b2e5fb05a8e6cb74b56a4658a2b7bf3cb6b1821ae7e7fd6eaa0"}, + {file = "cryptography-46.0.1-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:4c49eda9a23019e11d32a0eb51a27b3e7ddedde91e099c0ac6373e3aacc0d2ee"}, + {file = "cryptography-46.0.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:9babb7818fdd71394e576cf26c5452df77a355eac1a27ddfa24096665a27f8fd"}, + {file = "cryptography-46.0.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9f2c4cc63be3ef43c0221861177cee5d14b505cd4d4599a89e2cd273c4d3542a"}, + {file = "cryptography-46.0.1-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:41c281a74df173876da1dc9a9b6953d387f06e3d3ed9284e3baae3ab3f40883a"}, + {file = "cryptography-46.0.1-cp314-cp314t-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0a17377fa52563d730248ba1f68185461fff36e8bc75d8787a7dd2e20a802b7a"}, + {file = "cryptography-46.0.1-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:0d1922d9280e08cde90b518a10cd66831f632960a8d08cb3418922d83fce6f12"}, + {file = "cryptography-46.0.1-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:af84e8e99f1a82cea149e253014ea9dc89f75b82c87bb6c7242203186f465129"}, + {file = "cryptography-46.0.1-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:ef648d2c690703501714588b2ba640facd50fd16548133b11b2859e8655a69da"}, + {file = "cryptography-46.0.1-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:e94eb5fa32a8a9f9bf991f424f002913e3dd7c699ef552db9b14ba6a76a6313b"}, + {file = "cryptography-46.0.1-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:534b96c0831855e29fc3b069b085fd185aa5353033631a585d5cd4dd5d40d657"}, + {file = "cryptography-46.0.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:f9b55038b5c6c47559aa33626d8ecd092f354e23de3c6975e4bb205df128a2a0"}, + {file = "cryptography-46.0.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ec13b7105117dbc9afd023300fb9954d72ca855c274fe563e72428ece10191c0"}, + {file = "cryptography-46.0.1-cp314-cp314t-win32.whl", hash = "sha256:504e464944f2c003a0785b81668fe23c06f3b037e9cb9f68a7c672246319f277"}, + {file = "cryptography-46.0.1-cp314-cp314t-win_amd64.whl", hash = "sha256:c52fded6383f7e20eaf70a60aeddd796b3677c3ad2922c801be330db62778e05"}, + {file = "cryptography-46.0.1-cp314-cp314t-win_arm64.whl", hash = "sha256:9495d78f52c804b5ec8878b5b8c7873aa8e63db9cd9ee387ff2db3fffe4df784"}, + {file = "cryptography-46.0.1-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:d84c40bdb8674c29fa192373498b6cb1e84f882889d21a471b45d1f868d8d44b"}, + {file = "cryptography-46.0.1-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:9ed64e5083fa806709e74fc5ea067dfef9090e5b7a2320a49be3c9df3583a2d8"}, + {file = "cryptography-46.0.1-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:341fb7a26bc9d6093c1b124b9f13acc283d2d51da440b98b55ab3f79f2522ead"}, + {file = "cryptography-46.0.1-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:6ef1488967e729948d424d09c94753d0167ce59afba8d0f6c07a22b629c557b2"}, + {file = "cryptography-46.0.1-cp38-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7823bc7cdf0b747ecfb096d004cc41573c2f5c7e3a29861603a2871b43d3ef32"}, + {file = "cryptography-46.0.1-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:f736ab8036796f5a119ff8211deda416f8c15ce03776db704a7a4e17381cb2ef"}, + {file = "cryptography-46.0.1-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:e46710a240a41d594953012213ea8ca398cd2448fbc5d0f1be8160b5511104a0"}, + {file = "cryptography-46.0.1-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:84ef1f145de5aee82ea2447224dc23f065ff4cc5791bb3b506615957a6ba8128"}, + {file = "cryptography-46.0.1-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:9394c7d5a7565ac5f7d9ba38b2617448eba384d7b107b262d63890079fad77ca"}, + {file = "cryptography-46.0.1-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:ed957044e368ed295257ae3d212b95456bd9756df490e1ac4538857f67531fcc"}, + {file = "cryptography-46.0.1-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:f7de12fa0eee6234de9a9ce0ffcfa6ce97361db7a50b09b65c63ac58e5f22fc7"}, + {file = "cryptography-46.0.1-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:7fab1187b6c6b2f11a326f33b036f7168f5b996aedd0c059f9738915e4e8f53a"}, + {file = "cryptography-46.0.1-cp38-abi3-win32.whl", hash = "sha256:45f790934ac1018adeba46a0f7289b2b8fe76ba774a88c7f1922213a56c98bc1"}, + {file = "cryptography-46.0.1-cp38-abi3-win_amd64.whl", hash = "sha256:7176a5ab56fac98d706921f6416a05e5aff7df0e4b91516f450f8627cda22af3"}, + {file = "cryptography-46.0.1-cp38-abi3-win_arm64.whl", hash = "sha256:efc9e51c3e595267ff84adf56e9b357db89ab2279d7e375ffcaf8f678606f3d9"}, + {file = "cryptography-46.0.1-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:fd4b5e2ee4e60425711ec65c33add4e7a626adef79d66f62ba0acfd493af282d"}, + {file = "cryptography-46.0.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:48948940d0ae00483e85e9154bb42997d0b77c21e43a77b7773c8c80de532ac5"}, + {file = "cryptography-46.0.1-pp311-pypy311_pp73-macosx_10_9_x86_64.whl", hash = "sha256:b9c79af2c3058430d911ff1a5b2b96bbfe8da47d5ed961639ce4681886614e70"}, + {file = "cryptography-46.0.1-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:0ca4be2af48c24df689a150d9cd37404f689e2968e247b6b8ff09bff5bcd786f"}, + {file = "cryptography-46.0.1-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:13e67c4d3fb8b6bc4ef778a7ccdd8df4cd15b4bcc18f4239c8440891a11245cc"}, + {file = "cryptography-46.0.1-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:15b5fd9358803b0d1cc42505a18d8bca81dabb35b5cfbfea1505092e13a9d96d"}, + {file = "cryptography-46.0.1-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:e34da95e29daf8a71cb2841fd55df0511539a6cdf33e6f77c1e95e44006b9b46"}, + {file = "cryptography-46.0.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:34f04b7311174469ab3ac2647469743720f8b6c8b046f238e5cb27905695eb2a"}, + {file = "cryptography-46.0.1.tar.gz", hash = "sha256:ed570874e88f213437f5cf758f9ef26cbfc3f336d889b1e592ee11283bb8d1c7"}, +] + +[package.dependencies] +cffi = {version = ">=1.14", markers = "python_full_version == \"3.8.*\" and platform_python_implementation != \"PyPy\""} +typing-extensions = {version = ">=4.13.2", markers = "python_full_version < \"3.11.0\""} + +[package.extras] +docs = ["sphinx (>=5.3.0)", "sphinx-inline-tabs", "sphinx-rtd-theme (>=3.0.0)"] +docstest = ["pyenchant (>=3)", "readme-renderer (>=30.0)", "sphinxcontrib-spelling (>=7.3.1)"] +nox = ["nox[uv] (>=2024.4.15)"] +pep8test = ["check-sdist", "click (>=8.0.1)", "mypy (>=1.14)", "ruff (>=0.11.11)"] +sdist = ["build (>=1.0.0)"] +ssh = ["bcrypt (>=3.1.5)"] +test = ["certifi (>=2024)", "cryptography-vectors (==46.0.1)", "pretend (>=0.7)", "pytest (>=7.4.0)", "pytest-benchmark (>=4.0)", "pytest-cov (>=2.10.1)", "pytest-xdist (>=3.5.0)"] +test-randomorder = ["pytest-randomly"] + [[package]] name = "cycler" version = "0.12.1" @@ -1063,15 +1586,15 @@ tests = ["pytest", "pytest-cov", "pytest-xdist"] [[package]] name = "databricks-sdk" -version = "0.65.0" +version = "0.67.0" description = "Databricks SDK for Python (Beta)" optional = true python-versions = ">=3.7" groups = ["main"] markers = "python_version >= \"3.10\" and extra == \"mlflow\"" files = [ - {file = "databricks_sdk-0.65.0-py3-none-any.whl", hash = "sha256:594e61138071d7ae830412cfd3fbc5bd16aba9b67a423f44f4c13ca70c493a9f"}, - {file = "databricks_sdk-0.65.0.tar.gz", hash = "sha256:be744c844d1e1e9bf1a4ad2982ef2c8b88f2ef8ad36b6ea8b77591fd3b1f1bbb"}, + {file = "databricks_sdk-0.67.0-py3-none-any.whl", hash = "sha256:ef49e49db45ed12c015a32a6f9d4ba395850f25bb3dcffdcaf31a5167fe03ee2"}, + {file = "databricks_sdk-0.67.0.tar.gz", hash = "sha256:f923227babcaad428b0c2eede2755ebe9deb996e2c8654f179eb37f486b37a36"}, ] [package.dependencies] @@ -1134,7 +1657,7 @@ description = "DNS toolkit" optional = true python-versions = ">=3.8" groups = ["main"] -markers = "extra == \"proxy\"" +markers = "(python_full_version == \"3.8.*\" or platform_python_implementation == \"PyPy\") and extra == \"proxy\" and python_version < \"3.10\"" files = [ {file = "dnspython-2.6.1-py3-none-any.whl", hash = "sha256:5ef3b9680161f6fa89daf8ad451b5f1a33b18ae8a1c6778cdf4b43f08c0a6e50"}, {file = "dnspython-2.6.1.tar.gz", hash = "sha256:e8f0f9c23a7b7cb99ded64e6c3a6f3e701d78f50c55e002b839dea7225cff7cc"}, @@ -1149,6 +1672,50 @@ idna = ["idna (>=3.6)"] trio = ["trio (>=0.23)"] wmi = ["wmi (>=1.5.1)"] +[[package]] +name = "dnspython" +version = "2.7.0" +description = "DNS toolkit" +optional = true +python-versions = ">=3.9" +groups = ["main"] +markers = "python_full_version >= \"3.9.0\" and platform_python_implementation != \"PyPy\" and extra == \"proxy\" and python_version < \"3.10\"" +files = [ + {file = "dnspython-2.7.0-py3-none-any.whl", hash = "sha256:b4c34b7d10b51bcc3a5071e7b8dee77939f1e878477eeecc965e9835f63c6c86"}, + {file = "dnspython-2.7.0.tar.gz", hash = "sha256:ce9c432eda0dc91cf618a5cedf1a4e142651196bbcd2c80e89ed5a907e5cfaf1"}, +] + +[package.extras] +dev = ["black (>=23.1.0)", "coverage (>=7.0)", "flake8 (>=7)", "hypercorn (>=0.16.0)", "mypy (>=1.8)", "pylint (>=3)", "pytest (>=7.4)", "pytest-cov (>=4.1.0)", "quart-trio (>=0.11.0)", "sphinx (>=7.2.0)", "sphinx-rtd-theme (>=2.0.0)", "twine (>=4.0.0)", "wheel (>=0.42.0)"] +dnssec = ["cryptography (>=43)"] +doh = ["h2 (>=4.1.0)", "httpcore (>=1.0.0)", "httpx (>=0.26.0)"] +doq = ["aioquic (>=1.0.0)"] +idna = ["idna (>=3.7)"] +trio = ["trio (>=0.23)"] +wmi = ["wmi (>=1.5.1)"] + +[[package]] +name = "dnspython" +version = "2.8.0" +description = "DNS toolkit" +optional = true +python-versions = ">=3.10" +groups = ["main"] +markers = "python_version >= \"3.10\" and extra == \"proxy\"" +files = [ + {file = "dnspython-2.8.0-py3-none-any.whl", hash = "sha256:01d9bbc4a2d76bf0db7c1f729812ded6d912bd318d3b1cf81d30c0f845dbf3af"}, + {file = "dnspython-2.8.0.tar.gz", hash = "sha256:181d3c6996452cb1189c4046c61599b84a5a86e099562ffde77d26984ff26d0f"}, +] + +[package.extras] +dev = ["black (>=25.1.0)", "coverage (>=7.0)", "flake8 (>=7)", "hypercorn (>=0.17.0)", "mypy (>=1.17)", "pylint (>=3)", "pytest (>=8.4)", "pytest-cov (>=6.2.0)", "quart-trio (>=0.12.0)", "sphinx (>=8.2.0)", "sphinx-rtd-theme (>=3.0.0)", "twine (>=6.1.0)", "wheel (>=0.45.0)"] +dnssec = ["cryptography (>=45)"] +doh = ["h2 (>=4.2.0)", "httpcore (>=1.0.0)", "httpx (>=0.28.0)"] +doq = ["aioquic (>=1.2.0)"] +idna = ["idna (>=3.10)"] +trio = ["trio (>=0.30)"] +wmi = ["wmi (>=1.5.1) ; platform_system == \"Windows\""] + [[package]] name = "docker" version = "7.1.0" @@ -1180,12 +1747,25 @@ description = "Docutils -- Python Documentation Utilities" optional = true python-versions = ">=3.7" groups = ["main"] -markers = "extra == \"utils\"" +markers = "(python_full_version == \"3.8.*\" or platform_python_implementation == \"PyPy\") and extra == \"utils\" and python_version < \"3.10\"" files = [ {file = "docutils-0.20.1-py3-none-any.whl", hash = "sha256:96f387a2c5562db4476f09f13bbab2192e764cac08ebbf3a34a95d9b1e4a59d6"}, {file = "docutils-0.20.1.tar.gz", hash = "sha256:f08a4e276c3a1583a86dce3e34aba3fe04d02bba2dd51ed16106244e8a923e3b"}, ] +[[package]] +name = "docutils" +version = "0.21.2" +description = "Docutils -- Python Documentation Utilities" +optional = true +python-versions = ">=3.9" +groups = ["main"] +markers = "python_version >= \"3.9\" and (python_version >= \"3.10\" or platform_python_implementation != \"PyPy\") and extra == \"utils\"" +files = [ + {file = "docutils-0.21.2-py3-none-any.whl", hash = "sha256:dafca5b9e384f0e419294eb4d2ff9fa826435bf15f15b7bd45723e8ad76811b2"}, + {file = "docutils-0.21.2.tar.gz", hash = "sha256:3a6b18732edf182daa3cd12775bbb338cf5691468f91eeeb109deff6ebfa986f"}, +] + [[package]] name = "email-validator" version = "2.3.0" @@ -1233,7 +1813,7 @@ files = [ {file = "fastapi-0.115.14-py3-none-any.whl", hash = "sha256:6c0c8bf9420bd58f565e585036d971872472b4f7d3f6c73b698e10cffdefb3ca"}, {file = "fastapi-0.115.14.tar.gz", hash = "sha256:b1de15cdc1c499a4da47914db35d0e4ef8f1ce62b624e94e0e5824421df99739"}, ] -markers = {main = "python_version >= \"3.10\" and (extra == \"mlflow\" or extra == \"proxy\") or extra == \"proxy\""} +markers = {main = "(extra == \"mlflow\" or extra == \"proxy\") and python_version >= \"3.10\" or extra == \"proxy\""} [package.dependencies] pydantic = ">=1.7.4,<1.8 || >1.8,<1.8.1 || >1.8.1,<2.0.0 || >2.0.0,<2.0.1 || >2.0.1,<2.1.0 || >2.1.0,<3.0.0" @@ -1289,7 +1869,7 @@ description = "Fast read/write of AVRO files" optional = true python-versions = ">=3.9" groups = ["main"] -markers = "python_version >= \"3.9\" and extra == \"semantic-router\"" +markers = "python_version >= \"3.13\" and extra == \"semantic-router\"" files = [ {file = "fastavro-1.12.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e38497bd24136aad2c47376ee958be4f5b775d6f03c11893fc636eea8c1c3b40"}, {file = "fastavro-1.12.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e8d8401b021f4b3dfc05e6f82365f14de8d170a041fbe3345f992c9c13d4f0ff"}, @@ -1378,6 +1958,7 @@ description = "A platform independent file lock." optional = false python-versions = ">=3.8" groups = ["main"] +markers = "python_full_version == \"3.8.*\" or platform_python_implementation == \"PyPy\" and python_version < \"3.10\"" files = [ {file = "filelock-3.16.1-py3-none-any.whl", hash = "sha256:2082e5703d51fbf98ea75855d9d5527e33d8ff23099bec374a134febee6946b0"}, {file = "filelock-3.16.1.tar.gz", hash = "sha256:c249fbfcd5db47e5e2d6d62198e565475ee65e4831e2561c8e313fa7eb961435"}, @@ -1388,6 +1969,19 @@ docs = ["furo (>=2024.8.6)", "sphinx (>=8.0.2)", "sphinx-autodoc-typehints (>=2. testing = ["covdefaults (>=2.3)", "coverage (>=7.6.1)", "diff-cover (>=9.2)", "pytest (>=8.3.3)", "pytest-asyncio (>=0.24)", "pytest-cov (>=5)", "pytest-mock (>=3.14)", "pytest-timeout (>=2.3.1)", "virtualenv (>=20.26.4)"] typing = ["typing-extensions (>=4.12.2) ; python_version < \"3.11\""] +[[package]] +name = "filelock" +version = "3.19.1" +description = "A platform independent file lock." +optional = false +python-versions = ">=3.9" +groups = ["main"] +markers = "python_version >= \"3.9\" and platform_python_implementation != \"PyPy\" or python_version >= \"3.10\"" +files = [ + {file = "filelock-3.19.1-py3-none-any.whl", hash = "sha256:d38e30481def20772f5baf097c122c3babc4fcdb7e14e57049eb9d88c6dc017d"}, + {file = "filelock-3.19.1.tar.gz", hash = "sha256:66eda1888b0171c998b35be2bcc0f6d75c388a7ce20c3f3f37aa8e96c2dddf58"}, +] + [[package]] name = "flake8" version = "6.1.0" @@ -1519,6 +2113,7 @@ description = "A list-like structure which implements collections.abc.MutableSeq optional = false python-versions = ">=3.8" groups = ["main"] +markers = "python_full_version == \"3.8.*\" or platform_python_implementation == \"PyPy\" and python_version < \"3.10\"" files = [ {file = "frozenlist-1.5.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:5b6a66c18b5b9dd261ca98dffcb826a525334b2f29e7caa54e182255c5f6a65a"}, {file = "frozenlist-1.5.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d1b3eb7b05ea246510b43a7e53ed1653e55c2121019a97e60cad7efb881a97bb"}, @@ -1614,6 +2209,121 @@ files = [ {file = "frozenlist-1.5.0.tar.gz", hash = "sha256:81d5af29e61b9c8348e876d442253723928dce6433e0e76cd925cd83f1b4b817"}, ] +[[package]] +name = "frozenlist" +version = "1.7.0" +description = "A list-like structure which implements collections.abc.MutableSequence" +optional = false +python-versions = ">=3.9" +groups = ["main"] +markers = "python_version >= \"3.9\" and platform_python_implementation != \"PyPy\" or python_version >= \"3.10\"" +files = [ + {file = "frozenlist-1.7.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cc4df77d638aa2ed703b878dd093725b72a824c3c546c076e8fdf276f78ee84a"}, + {file = "frozenlist-1.7.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:716a9973a2cc963160394f701964fe25012600f3d311f60c790400b00e568b61"}, + {file = "frozenlist-1.7.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a0fd1bad056a3600047fb9462cff4c5322cebc59ebf5d0a3725e0ee78955001d"}, + {file = "frozenlist-1.7.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3789ebc19cb811163e70fe2bd354cea097254ce6e707ae42e56f45e31e96cb8e"}, + {file = "frozenlist-1.7.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:af369aa35ee34f132fcfad5be45fbfcde0e3a5f6a1ec0712857f286b7d20cca9"}, + {file = "frozenlist-1.7.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ac64b6478722eeb7a3313d494f8342ef3478dff539d17002f849101b212ef97c"}, + {file = "frozenlist-1.7.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f89f65d85774f1797239693cef07ad4c97fdd0639544bad9ac4b869782eb1981"}, + {file = "frozenlist-1.7.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1073557c941395fdfcfac13eb2456cb8aad89f9de27bae29fabca8e563b12615"}, + {file = "frozenlist-1.7.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1ed8d2fa095aae4bdc7fdd80351009a48d286635edffee66bf865e37a9125c50"}, + {file = "frozenlist-1.7.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:24c34bea555fe42d9f928ba0a740c553088500377448febecaa82cc3e88aa1fa"}, + {file = "frozenlist-1.7.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:69cac419ac6a6baad202c85aaf467b65ac860ac2e7f2ac1686dc40dbb52f6577"}, + {file = "frozenlist-1.7.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:960d67d0611f4c87da7e2ae2eacf7ea81a5be967861e0c63cf205215afbfac59"}, + {file = "frozenlist-1.7.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:41be2964bd4b15bf575e5daee5a5ce7ed3115320fb3c2b71fca05582ffa4dc9e"}, + {file = "frozenlist-1.7.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:46d84d49e00c9429238a7ce02dc0be8f6d7cd0cd405abd1bebdc991bf27c15bd"}, + {file = "frozenlist-1.7.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:15900082e886edb37480335d9d518cec978afc69ccbc30bd18610b7c1b22a718"}, + {file = "frozenlist-1.7.0-cp310-cp310-win32.whl", hash = "sha256:400ddd24ab4e55014bba442d917203c73b2846391dd42ca5e38ff52bb18c3c5e"}, + {file = "frozenlist-1.7.0-cp310-cp310-win_amd64.whl", hash = "sha256:6eb93efb8101ef39d32d50bce242c84bcbddb4f7e9febfa7b524532a239b4464"}, + {file = "frozenlist-1.7.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:aa51e147a66b2d74de1e6e2cf5921890de6b0f4820b257465101d7f37b49fb5a"}, + {file = "frozenlist-1.7.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9b35db7ce1cd71d36ba24f80f0c9e7cff73a28d7a74e91fe83e23d27c7828750"}, + {file = "frozenlist-1.7.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:34a69a85e34ff37791e94542065c8416c1afbf820b68f720452f636d5fb990cd"}, + {file = "frozenlist-1.7.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4a646531fa8d82c87fe4bb2e596f23173caec9185bfbca5d583b4ccfb95183e2"}, + {file = "frozenlist-1.7.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:79b2ffbba483f4ed36a0f236ccb85fbb16e670c9238313709638167670ba235f"}, + {file = "frozenlist-1.7.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a26f205c9ca5829cbf82bb2a84b5c36f7184c4316617d7ef1b271a56720d6b30"}, + {file = "frozenlist-1.7.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bcacfad3185a623fa11ea0e0634aac7b691aa925d50a440f39b458e41c561d98"}, + {file = "frozenlist-1.7.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:72c1b0fe8fe451b34f12dce46445ddf14bd2a5bcad7e324987194dc8e3a74c86"}, + {file = "frozenlist-1.7.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:61d1a5baeaac6c0798ff6edfaeaa00e0e412d49946c53fae8d4b8e8b3566c4ae"}, + {file = "frozenlist-1.7.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7edf5c043c062462f09b6820de9854bf28cc6cc5b6714b383149745e287181a8"}, + {file = "frozenlist-1.7.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:d50ac7627b3a1bd2dcef6f9da89a772694ec04d9a61b66cf87f7d9446b4a0c31"}, + {file = "frozenlist-1.7.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:ce48b2fece5aeb45265bb7a58259f45027db0abff478e3077e12b05b17fb9da7"}, + {file = "frozenlist-1.7.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:fe2365ae915a1fafd982c146754e1de6ab3478def8a59c86e1f7242d794f97d5"}, + {file = "frozenlist-1.7.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:45a6f2fdbd10e074e8814eb98b05292f27bad7d1883afbe009d96abdcf3bc898"}, + {file = "frozenlist-1.7.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:21884e23cffabb157a9dd7e353779077bf5b8f9a58e9b262c6caad2ef5f80a56"}, + {file = "frozenlist-1.7.0-cp311-cp311-win32.whl", hash = "sha256:284d233a8953d7b24f9159b8a3496fc1ddc00f4db99c324bd5fb5f22d8698ea7"}, + {file = "frozenlist-1.7.0-cp311-cp311-win_amd64.whl", hash = "sha256:387cbfdcde2f2353f19c2f66bbb52406d06ed77519ac7ee21be0232147c2592d"}, + {file = "frozenlist-1.7.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:3dbf9952c4bb0e90e98aec1bd992b3318685005702656bc6f67c1a32b76787f2"}, + {file = "frozenlist-1.7.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:1f5906d3359300b8a9bb194239491122e6cf1444c2efb88865426f170c262cdb"}, + {file = "frozenlist-1.7.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3dabd5a8f84573c8d10d8859a50ea2dec01eea372031929871368c09fa103478"}, + {file = "frozenlist-1.7.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa57daa5917f1738064f302bf2626281a1cb01920c32f711fbc7bc36111058a8"}, + {file = "frozenlist-1.7.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c193dda2b6d49f4c4398962810fa7d7c78f032bf45572b3e04dd5249dff27e08"}, + {file = "frozenlist-1.7.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bfe2b675cf0aaa6d61bf8fbffd3c274b3c9b7b1623beb3809df8a81399a4a9c4"}, + {file = "frozenlist-1.7.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8fc5d5cda37f62b262405cf9652cf0856839c4be8ee41be0afe8858f17f4c94b"}, + {file = "frozenlist-1.7.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b0d5ce521d1dd7d620198829b87ea002956e4319002ef0bc8d3e6d045cb4646e"}, + {file = "frozenlist-1.7.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:488d0a7d6a0008ca0db273c542098a0fa9e7dfaa7e57f70acef43f32b3f69dca"}, + {file = "frozenlist-1.7.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:15a7eaba63983d22c54d255b854e8108e7e5f3e89f647fc854bd77a237e767df"}, + {file = "frozenlist-1.7.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:1eaa7e9c6d15df825bf255649e05bd8a74b04a4d2baa1ae46d9c2d00b2ca2cb5"}, + {file = "frozenlist-1.7.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:e4389e06714cfa9d47ab87f784a7c5be91d3934cd6e9a7b85beef808297cc025"}, + {file = "frozenlist-1.7.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:73bd45e1488c40b63fe5a7df892baf9e2a4d4bb6409a2b3b78ac1c6236178e01"}, + {file = "frozenlist-1.7.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:99886d98e1643269760e5fe0df31e5ae7050788dd288947f7f007209b8c33f08"}, + {file = "frozenlist-1.7.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:290a172aae5a4c278c6da8a96222e6337744cd9c77313efe33d5670b9f65fc43"}, + {file = "frozenlist-1.7.0-cp312-cp312-win32.whl", hash = "sha256:426c7bc70e07cfebc178bc4c2bf2d861d720c4fff172181eeb4a4c41d4ca2ad3"}, + {file = "frozenlist-1.7.0-cp312-cp312-win_amd64.whl", hash = "sha256:563b72efe5da92e02eb68c59cb37205457c977aa7a449ed1b37e6939e5c47c6a"}, + {file = "frozenlist-1.7.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ee80eeda5e2a4e660651370ebffd1286542b67e268aa1ac8d6dbe973120ef7ee"}, + {file = "frozenlist-1.7.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d1a81c85417b914139e3a9b995d4a1c84559afc839a93cf2cb7f15e6e5f6ed2d"}, + {file = "frozenlist-1.7.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cbb65198a9132ebc334f237d7b0df163e4de83fb4f2bdfe46c1e654bdb0c5d43"}, + {file = "frozenlist-1.7.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dab46c723eeb2c255a64f9dc05b8dd601fde66d6b19cdb82b2e09cc6ff8d8b5d"}, + {file = "frozenlist-1.7.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6aeac207a759d0dedd2e40745575ae32ab30926ff4fa49b1635def65806fddee"}, + {file = "frozenlist-1.7.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bd8c4e58ad14b4fa7802b8be49d47993182fdd4023393899632c88fd8cd994eb"}, + {file = "frozenlist-1.7.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:04fb24d104f425da3540ed83cbfc31388a586a7696142004c577fa61c6298c3f"}, + {file = "frozenlist-1.7.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6a5c505156368e4ea6b53b5ac23c92d7edc864537ff911d2fb24c140bb175e60"}, + {file = "frozenlist-1.7.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8bd7eb96a675f18aa5c553eb7ddc24a43c8c18f22e1f9925528128c052cdbe00"}, + {file = "frozenlist-1.7.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:05579bf020096fe05a764f1f84cd104a12f78eaab68842d036772dc6d4870b4b"}, + {file = "frozenlist-1.7.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:376b6222d114e97eeec13d46c486facd41d4f43bab626b7c3f6a8b4e81a5192c"}, + {file = "frozenlist-1.7.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:0aa7e176ebe115379b5b1c95b4096fb1c17cce0847402e227e712c27bdb5a949"}, + {file = "frozenlist-1.7.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3fbba20e662b9c2130dc771e332a99eff5da078b2b2648153a40669a6d0e36ca"}, + {file = "frozenlist-1.7.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:f3f4410a0a601d349dd406b5713fec59b4cee7e71678d5b17edda7f4655a940b"}, + {file = "frozenlist-1.7.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e2cdfaaec6a2f9327bf43c933c0319a7c429058e8537c508964a133dffee412e"}, + {file = "frozenlist-1.7.0-cp313-cp313-win32.whl", hash = "sha256:5fc4df05a6591c7768459caba1b342d9ec23fa16195e744939ba5914596ae3e1"}, + {file = "frozenlist-1.7.0-cp313-cp313-win_amd64.whl", hash = "sha256:52109052b9791a3e6b5d1b65f4b909703984b770694d3eb64fad124c835d7cba"}, + {file = "frozenlist-1.7.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:a6f86e4193bb0e235ef6ce3dde5cbabed887e0b11f516ce8a0f4d3b33078ec2d"}, + {file = "frozenlist-1.7.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:82d664628865abeb32d90ae497fb93df398a69bb3434463d172b80fc25b0dd7d"}, + {file = "frozenlist-1.7.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:912a7e8375a1c9a68325a902f3953191b7b292aa3c3fb0d71a216221deca460b"}, + {file = "frozenlist-1.7.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9537c2777167488d539bc5de2ad262efc44388230e5118868e172dd4a552b146"}, + {file = "frozenlist-1.7.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:f34560fb1b4c3e30ba35fa9a13894ba39e5acfc5f60f57d8accde65f46cc5e74"}, + {file = "frozenlist-1.7.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:acd03d224b0175f5a850edc104ac19040d35419eddad04e7cf2d5986d98427f1"}, + {file = "frozenlist-1.7.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f2038310bc582f3d6a09b3816ab01737d60bf7b1ec70f5356b09e84fb7408ab1"}, + {file = "frozenlist-1.7.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b8c05e4c8e5f36e5e088caa1bf78a687528f83c043706640a92cb76cd6999384"}, + {file = "frozenlist-1.7.0-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:765bb588c86e47d0b68f23c1bee323d4b703218037765dcf3f25c838c6fecceb"}, + {file = "frozenlist-1.7.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:32dc2e08c67d86d0969714dd484fd60ff08ff81d1a1e40a77dd34a387e6ebc0c"}, + {file = "frozenlist-1.7.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:c0303e597eb5a5321b4de9c68e9845ac8f290d2ab3f3e2c864437d3c5a30cd65"}, + {file = "frozenlist-1.7.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:a47f2abb4e29b3a8d0b530f7c3598badc6b134562b1a5caee867f7c62fee51e3"}, + {file = "frozenlist-1.7.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:3d688126c242a6fabbd92e02633414d40f50bb6002fa4cf995a1d18051525657"}, + {file = "frozenlist-1.7.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:4e7e9652b3d367c7bd449a727dc79d5043f48b88d0cbfd4f9f1060cf2b414104"}, + {file = "frozenlist-1.7.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:1a85e345b4c43db8b842cab1feb41be5cc0b10a1830e6295b69d7310f99becaf"}, + {file = "frozenlist-1.7.0-cp313-cp313t-win32.whl", hash = "sha256:3a14027124ddb70dfcee5148979998066897e79f89f64b13328595c4bdf77c81"}, + {file = "frozenlist-1.7.0-cp313-cp313t-win_amd64.whl", hash = "sha256:3bf8010d71d4507775f658e9823210b7427be36625b387221642725b515dcf3e"}, + {file = "frozenlist-1.7.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:cea3dbd15aea1341ea2de490574a4a37ca080b2ae24e4b4f4b51b9057b4c3630"}, + {file = "frozenlist-1.7.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7d536ee086b23fecc36c2073c371572374ff50ef4db515e4e503925361c24f71"}, + {file = "frozenlist-1.7.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:dfcebf56f703cb2e346315431699f00db126d158455e513bd14089d992101e44"}, + {file = "frozenlist-1.7.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:974c5336e61d6e7eb1ea5b929cb645e882aadab0095c5a6974a111e6479f8878"}, + {file = "frozenlist-1.7.0-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c70db4a0ab5ab20878432c40563573229a7ed9241506181bba12f6b7d0dc41cb"}, + {file = "frozenlist-1.7.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1137b78384eebaf70560a36b7b229f752fb64d463d38d1304939984d5cb887b6"}, + {file = "frozenlist-1.7.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e793a9f01b3e8b5c0bc646fb59140ce0efcc580d22a3468d70766091beb81b35"}, + {file = "frozenlist-1.7.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:74739ba8e4e38221d2c5c03d90a7e542cb8ad681915f4ca8f68d04f810ee0a87"}, + {file = "frozenlist-1.7.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1e63344c4e929b1a01e29bc184bbb5fd82954869033765bfe8d65d09e336a677"}, + {file = "frozenlist-1.7.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:2ea2a7369eb76de2217a842f22087913cdf75f63cf1307b9024ab82dfb525938"}, + {file = "frozenlist-1.7.0-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:836b42f472a0e006e02499cef9352ce8097f33df43baaba3e0a28a964c26c7d2"}, + {file = "frozenlist-1.7.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:e22b9a99741294b2571667c07d9f8cceec07cb92aae5ccda39ea1b6052ed4319"}, + {file = "frozenlist-1.7.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:9a19e85cc503d958abe5218953df722748d87172f71b73cf3c9257a91b999890"}, + {file = "frozenlist-1.7.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:f22dac33bb3ee8fe3e013aa7b91dc12f60d61d05b7fe32191ffa84c3aafe77bd"}, + {file = "frozenlist-1.7.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:9ccec739a99e4ccf664ea0775149f2749b8a6418eb5b8384b4dc0a7d15d304cb"}, + {file = "frozenlist-1.7.0-cp39-cp39-win32.whl", hash = "sha256:b3950f11058310008a87757f3eee16a8e1ca97979833239439586857bc25482e"}, + {file = "frozenlist-1.7.0-cp39-cp39-win_amd64.whl", hash = "sha256:43a82fce6769c70f2f5a06248b614a7d268080a9d20f7457ef10ecee5af82b63"}, + {file = "frozenlist-1.7.0-py3-none-any.whl", hash = "sha256:9a5af342e34f7e97caf8c995864c7a396418ae2859cc6fdf1b1073020d516a7e"}, + {file = "frozenlist-1.7.0.tar.gz", hash = "sha256:2e310d81923c2437ea8670467121cc3e9b0f76d3043cc1d2331d56c7fb7a3a8f"}, +] + [[package]] name = "fsspec" version = "2025.3.0" @@ -1621,6 +2331,7 @@ description = "File-system specification" optional = false python-versions = ">=3.8" groups = ["main"] +markers = "python_full_version == \"3.8.*\" or platform_python_implementation == \"PyPy\" and python_version < \"3.10\"" files = [ {file = "fsspec-2025.3.0-py3-none-any.whl", hash = "sha256:efb87af3efa9103f94ca91a7f8cb7a4df91af9f74fc106c9c7ea0efd7277c1b3"}, {file = "fsspec-2025.3.0.tar.gz", hash = "sha256:a935fd1ea872591f2b5148907d103488fc523295e6c64b835cfad8c3eca44972"}, @@ -1654,6 +2365,47 @@ test-downstream = ["aiobotocore (>=2.5.4,<3.0.0)", "dask[dataframe,test]", "moto test-full = ["adlfs", "aiohttp (!=4.0.0a0,!=4.0.0a1)", "cloudpickle", "dask", "distributed", "dropbox", "dropboxdrivefs", "fastparquet", "fusepy", "gcsfs", "jinja2", "kerchunk", "libarchive-c", "lz4", "notebook", "numpy", "ocifs", "pandas", "panel", "paramiko", "pyarrow", "pyarrow (>=1)", "pyftpdlib", "pygit2", "pytest", "pytest-asyncio (!=0.22.0)", "pytest-benchmark", "pytest-cov", "pytest-mock", "pytest-recording", "pytest-rerunfailures", "python-snappy", "requests", "smbprotocol", "tqdm", "urllib3", "zarr", "zstandard"] tqdm = ["tqdm"] +[[package]] +name = "fsspec" +version = "2025.9.0" +description = "File-system specification" +optional = false +python-versions = ">=3.9" +groups = ["main"] +markers = "python_version >= \"3.9\" and platform_python_implementation != \"PyPy\" or python_version >= \"3.10\"" +files = [ + {file = "fsspec-2025.9.0-py3-none-any.whl", hash = "sha256:530dc2a2af60a414a832059574df4a6e10cce927f6f4a78209390fe38955cfb7"}, + {file = "fsspec-2025.9.0.tar.gz", hash = "sha256:19fd429483d25d28b65ec68f9f4adc16c17ea2c7c7bf54ec61360d478fb19c19"}, +] + +[package.extras] +abfs = ["adlfs"] +adl = ["adlfs"] +arrow = ["pyarrow (>=1)"] +dask = ["dask", "distributed"] +dev = ["pre-commit", "ruff (>=0.5)"] +doc = ["numpydoc", "sphinx", "sphinx-design", "sphinx-rtd-theme", "yarl"] +dropbox = ["dropbox", "dropboxdrivefs", "requests"] +full = ["adlfs", "aiohttp (!=4.0.0a0,!=4.0.0a1)", "dask", "distributed", "dropbox", "dropboxdrivefs", "fusepy", "gcsfs", "libarchive-c", "ocifs", "panel", "paramiko", "pyarrow (>=1)", "pygit2", "requests", "s3fs", "smbprotocol", "tqdm"] +fuse = ["fusepy"] +gcs = ["gcsfs"] +git = ["pygit2"] +github = ["requests"] +gs = ["gcsfs"] +gui = ["panel"] +hdfs = ["pyarrow (>=1)"] +http = ["aiohttp (!=4.0.0a0,!=4.0.0a1)"] +libarchive = ["libarchive-c"] +oci = ["ocifs"] +s3 = ["s3fs"] +sftp = ["paramiko"] +smb = ["smbprotocol"] +ssh = ["paramiko"] +test = ["aiohttp (!=4.0.0a0,!=4.0.0a1)", "numpy", "pytest", "pytest-asyncio (!=0.22.0)", "pytest-benchmark", "pytest-cov", "pytest-mock", "pytest-recording", "pytest-rerunfailures", "requests"] +test-downstream = ["aiobotocore (>=2.5.4,<3.0.0)", "dask[dataframe,test]", "moto[server] (>4,<5)", "pytest-timeout", "xarray"] +test-full = ["adlfs", "aiohttp (!=4.0.0a0,!=4.0.0a1)", "cloudpickle", "dask", "distributed", "dropbox", "dropboxdrivefs", "fastparquet", "fusepy", "gcsfs", "jinja2", "kerchunk", "libarchive-c", "lz4", "notebook", "numpy", "ocifs", "pandas", "panel", "paramiko", "pyarrow", "pyarrow (>=1)", "pyftpdlib", "pygit2", "pytest", "pytest-asyncio (!=0.22.0)", "pytest-benchmark", "pytest-cov", "pytest-mock", "pytest-recording", "pytest-rerunfailures", "python-snappy", "requests", "smbprotocol", "tqdm", "urllib3", "zarr", "zstandard ; python_version < \"3.14\""] +tqdm = ["tqdm"] + [[package]] name = "gitdb" version = "4.0.12" @@ -1734,7 +2486,7 @@ description = "Google Authentication Library" optional = true python-versions = ">=3.7" groups = ["main"] -markers = "python_version >= \"3.10\" and (extra == \"mlflow\" or extra == \"extra-proxy\") or extra == \"extra-proxy\"" +markers = "(extra == \"mlflow\" or extra == \"extra-proxy\") and python_version >= \"3.10\" or extra == \"extra-proxy\"" files = [ {file = "google_auth-2.40.3-py2.py3-none-any.whl", hash = "sha256:1370d4593e86213563547f97a92752fc658456fe4514c809544f330fed45a7ca"}, {file = "google_auth-2.40.3.tar.gz", hash = "sha256:500c3a29adedeb36ea9cf24b8d10858e152f2412e3ca37829b3fa18e33d63b77"}, @@ -2021,11 +2773,79 @@ files = [ {file = "grpcio-1.70.0-cp39-cp39-win_amd64.whl", hash = "sha256:a31d7e3b529c94e930a117b2175b2efd179d96eb3c7a21ccb0289a8ab05b645c"}, {file = "grpcio-1.70.0.tar.gz", hash = "sha256:8d1584a68d5922330025881e63a6c1b54cc8117291d382e4fa69339b6d914c56"}, ] -markers = {main = "extra == \"extra-proxy\""} +markers = {main = "(python_full_version == \"3.8.*\" or platform_python_implementation == \"PyPy\") and extra == \"extra-proxy\" and python_version < \"3.10\"", dev = "python_full_version == \"3.8.*\" or platform_python_implementation == \"PyPy\" and python_version < \"3.10\"", proxy-dev = "python_full_version == \"3.8.*\" or platform_python_implementation == \"PyPy\" and python_version < \"3.10\""} [package.extras] protobuf = ["grpcio-tools (>=1.70.0)"] +[[package]] +name = "grpcio" +version = "1.75.0" +description = "HTTP/2-based RPC framework" +optional = false +python-versions = ">=3.9" +groups = ["main", "dev", "proxy-dev"] +files = [ + {file = "grpcio-1.75.0-cp310-cp310-linux_armv7l.whl", hash = "sha256:1ec9cbaec18d9597c718b1ed452e61748ac0b36ba350d558f9ded1a94cc15ec7"}, + {file = "grpcio-1.75.0-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:7ee5ee42bfae8238b66a275f9ebcf6f295724375f2fa6f3b52188008b6380faf"}, + {file = "grpcio-1.75.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:9146e40378f551eed66c887332afc807fcce593c43c698e21266a4227d4e20d2"}, + {file = "grpcio-1.75.0-cp310-cp310-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:0c40f368541945bb664857ecd7400acb901053a1abbcf9f7896361b2cfa66798"}, + {file = "grpcio-1.75.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:50a6e43a9adc6938e2a16c9d9f8a2da9dd557ddd9284b73b07bd03d0e098d1e9"}, + {file = "grpcio-1.75.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:dce15597ca11913b78e1203c042d5723e3ea7f59e7095a1abd0621be0e05b895"}, + {file = "grpcio-1.75.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:851194eec47755101962da423f575ea223c9dd7f487828fe5693920e8745227e"}, + {file = "grpcio-1.75.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ca123db0813eef80625a4242a0c37563cb30a3edddebe5ee65373854cf187215"}, + {file = "grpcio-1.75.0-cp310-cp310-win32.whl", hash = "sha256:222b0851e20c04900c63f60153503e918b08a5a0fad8198401c0b1be13c6815b"}, + {file = "grpcio-1.75.0-cp310-cp310-win_amd64.whl", hash = "sha256:bb58e38a50baed9b21492c4b3f3263462e4e37270b7ea152fc10124b4bd1c318"}, + {file = "grpcio-1.75.0-cp311-cp311-linux_armv7l.whl", hash = "sha256:7f89d6d0cd43170a80ebb4605cad54c7d462d21dc054f47688912e8bf08164af"}, + {file = "grpcio-1.75.0-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:cb6c5b075c2d092f81138646a755f0dad94e4622300ebef089f94e6308155d82"}, + {file = "grpcio-1.75.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:494dcbade5606128cb9f530ce00331a90ecf5e7c5b243d373aebdb18e503c346"}, + {file = "grpcio-1.75.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:050760fd29c8508844a720f06c5827bb00de8f5e02f58587eb21a4444ad706e5"}, + {file = "grpcio-1.75.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:266fa6209b68a537b2728bb2552f970e7e78c77fe43c6e9cbbe1f476e9e5c35f"}, + {file = "grpcio-1.75.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:06d22e1d8645e37bc110f4c589cb22c283fd3de76523065f821d6e81de33f5d4"}, + {file = "grpcio-1.75.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:9880c323595d851292785966cadb6c708100b34b163cab114e3933f5773cba2d"}, + {file = "grpcio-1.75.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:55a2d5ae79cd0f68783fb6ec95509be23746e3c239290b2ee69c69a38daa961a"}, + {file = "grpcio-1.75.0-cp311-cp311-win32.whl", hash = "sha256:352dbdf25495eef584c8de809db280582093bc3961d95a9d78f0dfb7274023a2"}, + {file = "grpcio-1.75.0-cp311-cp311-win_amd64.whl", hash = "sha256:678b649171f229fb16bda1a2473e820330aa3002500c4f9fd3a74b786578e90f"}, + {file = "grpcio-1.75.0-cp312-cp312-linux_armv7l.whl", hash = "sha256:fa35ccd9501ffdd82b861809cbfc4b5b13f4b4c5dc3434d2d9170b9ed38a9054"}, + {file = "grpcio-1.75.0-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:0fcb77f2d718c1e58cc04ef6d3b51e0fa3b26cf926446e86c7eba105727b6cd4"}, + {file = "grpcio-1.75.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:36764a4ad9dc1eb891042fab51e8cdf7cc014ad82cee807c10796fb708455041"}, + {file = "grpcio-1.75.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:725e67c010f63ef17fc052b261004942763c0b18dcd84841e6578ddacf1f9d10"}, + {file = "grpcio-1.75.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:91fbfc43f605c5ee015c9056d580a70dd35df78a7bad97e05426795ceacdb59f"}, + {file = "grpcio-1.75.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7a9337ac4ce61c388e02019d27fa837496c4b7837cbbcec71b05934337e51531"}, + {file = "grpcio-1.75.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:ee16e232e3d0974750ab5f4da0ab92b59d6473872690b5e40dcec9a22927f22e"}, + {file = "grpcio-1.75.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:55dfb9122973cc69520b23d39867726722cafb32e541435707dc10249a1bdbc6"}, + {file = "grpcio-1.75.0-cp312-cp312-win32.whl", hash = "sha256:fb64dd62face3d687a7b56cd881e2ea39417af80f75e8b36f0f81dfd93071651"}, + {file = "grpcio-1.75.0-cp312-cp312-win_amd64.whl", hash = "sha256:6b365f37a9c9543a9e91c6b4103d68d38d5bcb9965b11d5092b3c157bd6a5ee7"}, + {file = "grpcio-1.75.0-cp313-cp313-linux_armv7l.whl", hash = "sha256:1bb78d052948d8272c820bb928753f16a614bb2c42fbf56ad56636991b427518"}, + {file = "grpcio-1.75.0-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:9dc4a02796394dd04de0b9673cb79a78901b90bb16bf99ed8cb528c61ed9372e"}, + {file = "grpcio-1.75.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:437eeb16091d31498585d73b133b825dc80a8db43311e332c08facf820d36894"}, + {file = "grpcio-1.75.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:c2c39984e846bd5da45c5f7bcea8fafbe47c98e1ff2b6f40e57921b0c23a52d0"}, + {file = "grpcio-1.75.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:38d665f44b980acdbb2f0e1abf67605ba1899f4d2443908df9ec8a6f26d2ed88"}, + {file = "grpcio-1.75.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:2e8e752ab5cc0a9c5b949808c000ca7586223be4f877b729f034b912364c3964"}, + {file = "grpcio-1.75.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:3a6788b30aa8e6f207c417874effe3f79c2aa154e91e78e477c4825e8b431ce0"}, + {file = "grpcio-1.75.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ffc33e67cab6141c54e75d85acd5dec616c5095a957ff997b4330a6395aa9b51"}, + {file = "grpcio-1.75.0-cp313-cp313-win32.whl", hash = "sha256:c8cfc780b7a15e06253aae5f228e1e84c0d3c4daa90faf5bc26b751174da4bf9"}, + {file = "grpcio-1.75.0-cp313-cp313-win_amd64.whl", hash = "sha256:0c91d5b16eff3cbbe76b7a1eaaf3d91e7a954501e9d4f915554f87c470475c3d"}, + {file = "grpcio-1.75.0-cp39-cp39-linux_armv7l.whl", hash = "sha256:0b85f4ebe6b56d2a512201bb0e5f192c273850d349b0a74ac889ab5d38959d16"}, + {file = "grpcio-1.75.0-cp39-cp39-macosx_11_0_universal2.whl", hash = "sha256:68c95b1c1e3bf96ceadf98226e9dfe2bc92155ce352fa0ee32a1603040e61856"}, + {file = "grpcio-1.75.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:153c5a7655022c3626ad70be3d4c2974cb0967f3670ee49ece8b45b7a139665f"}, + {file = "grpcio-1.75.0-cp39-cp39-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:53067c590ac3638ad0c04272f2a5e7e32a99fec8824c31b73bc3ef93160511fa"}, + {file = "grpcio-1.75.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:78dcc025a144319b66df6d088bd0eda69e1719eb6ac6127884a36188f336df19"}, + {file = "grpcio-1.75.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:1ec2937fd92b5b4598cbe65f7e57d66039f82b9e2b7f7a5f9149374057dde77d"}, + {file = "grpcio-1.75.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:597340a41ad4b619aaa5c9b94f7e6ba4067885386342ab0af039eda945c255cd"}, + {file = "grpcio-1.75.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:0aa795198b28807d28570c0a5f07bb04d5facca7d3f27affa6ae247bbd7f312a"}, + {file = "grpcio-1.75.0-cp39-cp39-win32.whl", hash = "sha256:585147859ff4603798e92605db28f4a97c821c69908e7754c44771c27b239bbd"}, + {file = "grpcio-1.75.0-cp39-cp39-win_amd64.whl", hash = "sha256:eafbe3563f9cb378370a3fa87ef4870539cf158124721f3abee9f11cd8162460"}, + {file = "grpcio-1.75.0.tar.gz", hash = "sha256:b989e8b09489478c2d19fecc744a298930f40d8b27c3638afbfe84d22f36ce4e"}, +] +markers = {main = "python_version >= \"3.9\" and (python_version >= \"3.10\" or platform_python_implementation != \"PyPy\") and extra == \"extra-proxy\"", dev = "python_version >= \"3.9\" and platform_python_implementation != \"PyPy\" or python_version >= \"3.10\"", proxy-dev = "python_version >= \"3.9\" and platform_python_implementation != \"PyPy\" or python_version >= \"3.10\""} + +[package.dependencies] +typing-extensions = ">=4.12,<5.0" + +[package.extras] +protobuf = ["grpcio-tools (>=1.75.0)"] + [[package]] name = "grpcio-status" version = "1.62.3" @@ -2051,7 +2871,7 @@ description = "WSGI HTTP Server for UNIX" optional = true python-versions = ">=3.7" groups = ["main"] -markers = "platform_system != \"Windows\" and (extra == \"mlflow\" or extra == \"proxy\") and python_version >= \"3.10\" or extra == \"proxy\"" +markers = "(extra == \"mlflow\" or extra == \"proxy\") and platform_system != \"Windows\" and python_version >= \"3.10\" or extra == \"proxy\"" files = [ {file = "gunicorn-23.0.0-py3-none-any.whl", hash = "sha256:ec400d38950de4dfd418cff8328b2c8faed0edb0d517d3394e457c317908ca4d"}, {file = "gunicorn-23.0.0.tar.gz", hash = "sha256:f014447a0101dc57e294f6c18ca6b40227a4c90e9bdb586042628030cba004ec"}, @@ -2086,6 +2906,7 @@ description = "HTTP/2 State-Machine based protocol implementation" optional = false python-versions = ">=3.6.1" groups = ["proxy-dev"] +markers = "python_full_version == \"3.8.*\" or platform_python_implementation == \"PyPy\" and python_version < \"3.10\"" files = [ {file = "h2-4.1.0-py3-none-any.whl", hash = "sha256:03a46bcf682256c95b5fd9e9a99c1323584c3eec6440d379b9903d709476bc6d"}, {file = "h2-4.1.0.tar.gz", hash = "sha256:a83aca08fbe7aacb79fec788c9c0bac936343560ed9ec18b82a13a12c28d2abb"}, @@ -2095,6 +2916,23 @@ files = [ hpack = ">=4.0,<5" hyperframe = ">=6.0,<7" +[[package]] +name = "h2" +version = "4.3.0" +description = "Pure-Python HTTP/2 protocol implementation" +optional = false +python-versions = ">=3.9" +groups = ["proxy-dev"] +markers = "python_version >= \"3.9\" and platform_python_implementation != \"PyPy\" or python_version >= \"3.10\"" +files = [ + {file = "h2-4.3.0-py3-none-any.whl", hash = "sha256:c438f029a25f7945c69e0ccf0fb951dc3f73a5f6412981daee861431b70e2bdd"}, + {file = "h2-4.3.0.tar.gz", hash = "sha256:6c59efe4323fa18b47a632221a1888bd7fde6249819beda254aeca909f221bf1"}, +] + +[package.dependencies] +hpack = ">=4.1,<5" +hyperframe = ">=6.1,<7" + [[package]] name = "hf-xet" version = "1.1.10" @@ -2124,11 +2962,25 @@ description = "Pure-Python HPACK header compression" optional = false python-versions = ">=3.6.1" groups = ["proxy-dev"] +markers = "python_full_version == \"3.8.*\" or platform_python_implementation == \"PyPy\" and python_version < \"3.10\"" files = [ {file = "hpack-4.0.0-py3-none-any.whl", hash = "sha256:84a076fad3dc9a9f8063ccb8041ef100867b1878b25ef0ee63847a5d53818a6c"}, {file = "hpack-4.0.0.tar.gz", hash = "sha256:fc41de0c63e687ebffde81187a948221294896f6bdc0ae2312708df339430095"}, ] +[[package]] +name = "hpack" +version = "4.1.0" +description = "Pure-Python HPACK header encoding" +optional = false +python-versions = ">=3.9" +groups = ["proxy-dev"] +markers = "python_version >= \"3.9\" and platform_python_implementation != \"PyPy\" or python_version >= \"3.10\"" +files = [ + {file = "hpack-4.1.0-py3-none-any.whl", hash = "sha256:157ac792668d995c657d93111f46b4535ed114f0c9c8d672271bbec7eae1b496"}, + {file = "hpack-4.1.0.tar.gz", hash = "sha256:ec5eca154f7056aa06f196a557655c5b009b382873ac8d1e66e79e87535f1dca"}, +] + [[package]] name = "httpcore" version = "1.0.9" @@ -2191,14 +3043,14 @@ files = [ [[package]] name = "huggingface-hub" -version = "0.35.0" +version = "0.35.1" description = "Client library to download and publish models, datasets and other repos on the huggingface.co hub" optional = false python-versions = ">=3.8.0" groups = ["main"] files = [ - {file = "huggingface_hub-0.35.0-py3-none-any.whl", hash = "sha256:f2e2f693bca9a26530b1c0b9bcd4c1495644dad698e6a0060f90e22e772c31e9"}, - {file = "huggingface_hub-0.35.0.tar.gz", hash = "sha256:ccadd2a78eef75effff184ad89401413629fabc52cefd76f6bbacb9b1c0676ac"}, + {file = "huggingface_hub-0.35.1-py3-none-any.whl", hash = "sha256:2f0e2709c711e3040e31d3e0418341f7092910f1462dd00350c4e97af47280a8"}, + {file = "huggingface_hub-0.35.1.tar.gz", hash = "sha256:3585b88c5169c64b7e4214d0e88163d4a709de6d1a502e0cd0459e9ee2c9c572"}, ] [package.dependencies] @@ -2277,11 +3129,25 @@ description = "HTTP/2 framing layer for Python" optional = false python-versions = ">=3.6.1" groups = ["proxy-dev"] +markers = "python_full_version == \"3.8.*\" or platform_python_implementation == \"PyPy\" and python_version < \"3.10\"" files = [ {file = "hyperframe-6.0.1-py3-none-any.whl", hash = "sha256:0ec6bafd80d8ad2195c4f03aacba3a8265e57bc4cff261e802bf39970ed02a15"}, {file = "hyperframe-6.0.1.tar.gz", hash = "sha256:ae510046231dc8e9ecb1a6586f63d2347bf4c8905914aa84ba585ae85f28a914"}, ] +[[package]] +name = "hyperframe" +version = "6.1.0" +description = "Pure-Python HTTP/2 framing" +optional = false +python-versions = ">=3.9" +groups = ["proxy-dev"] +markers = "python_version >= \"3.9\" and platform_python_implementation != \"PyPy\" or python_version >= \"3.10\"" +files = [ + {file = "hyperframe-6.1.0-py3-none-any.whl", hash = "sha256:b03380493a519fce58ea5af42e4a42317bf9bd425596f7a0835ffce80f1a42e5"}, + {file = "hyperframe-6.1.0.tar.gz", hash = "sha256:f630908a00854a7adeabd6382b43923a4c4cd4b821fcb527e6ab9e15382a3b08"}, +] + [[package]] name = "idna" version = "3.10" @@ -2317,6 +3183,7 @@ description = "Read metadata from Python packages" optional = false python-versions = ">=3.8" groups = ["main", "dev", "proxy-dev"] +markers = "python_version >= \"3.13\"" files = [ {file = "importlib_metadata-6.11.0-py3-none-any.whl", hash = "sha256:f0afba6205ad8f8947c7d338b5342d5db2afbfd82f9cbef7879a9539cc12eb9b"}, {file = "importlib_metadata-6.11.0.tar.gz", hash = "sha256:1231cf92d825c9e03cfc4da076a16de6422c863558229ea0b22b675657463443"}, @@ -2330,6 +3197,27 @@ docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.link perf = ["ipython"] testing = ["flufl.flake8", "importlib-resources (>=1.3) ; python_version < \"3.9\"", "packaging", "pyfakefs", "pytest (>=6)", "pytest-black (>=0.3.7) ; platform_python_implementation != \"PyPy\"", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy (>=0.9.1) ; platform_python_implementation != \"PyPy\"", "pytest-perf (>=0.9.2)", "pytest-ruff"] +[[package]] +name = "importlib-metadata" +version = "7.1.0" +description = "Read metadata from Python packages" +optional = false +python-versions = ">=3.8" +groups = ["main", "dev", "proxy-dev"] +markers = "python_version <= \"3.12\"" +files = [ + {file = "importlib_metadata-7.1.0-py3-none-any.whl", hash = "sha256:30962b96c0c223483ed6cc7280e7f0199feb01a0e40cfae4d4450fc6fab1f570"}, + {file = "importlib_metadata-7.1.0.tar.gz", hash = "sha256:b78938b926ee8d5f020fc4772d487045805a55ddbad2ecf21c6d60938dc7fcd2"}, +] + +[package.dependencies] +zipp = ">=0.5" + +[package.extras] +docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +perf = ["ipython"] +testing = ["flufl.flake8", "importlib-resources (>=1.3) ; python_version < \"3.9\"", "jaraco.test (>=5.4)", "packaging", "pyfakefs", "pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy ; platform_python_implementation != \"PyPy\"", "pytest-perf (>=0.9.2)", "pytest-ruff (>=0.2.1)"] + [[package]] name = "importlib-resources" version = "6.4.5" @@ -2337,7 +3225,7 @@ description = "Read resources from Python packages" optional = false python-versions = ">=3.8" groups = ["main"] -markers = "python_version < \"3.9\"" +markers = "python_full_version == \"3.8.*\" or platform_python_implementation == \"PyPy\" and python_version < \"3.9\"" files = [ {file = "importlib_resources-6.4.5-py3-none-any.whl", hash = "sha256:ac29d5f956f01d5e4bb63102a5a19957f1b9175e45649977264a1416783bb717"}, {file = "importlib_resources-6.4.5.tar.gz", hash = "sha256:980862a1d16c9e147a59603677fa2aa5fd82b87f223b6cb870695bcfce830065"}, @@ -2417,6 +3305,7 @@ description = "Fast iterable JSON parser." optional = false python-versions = ">=3.8" groups = ["main"] +markers = "python_full_version == \"3.8.*\" or platform_python_implementation == \"PyPy\" and python_version < \"3.10\"" files = [ {file = "jiter-0.9.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:c0163baa7ee85860fdc14cc39263014500df901eeffdf94c1eab9a2d713b2a9d"}, {file = "jiter-0.9.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:514d4dd845e0af4da15112502e6fcb952f0721f27f17e530454e379472b90c14"}, @@ -2496,6 +3385,95 @@ files = [ {file = "jiter-0.9.1.tar.gz", hash = "sha256:7852990068b6e06102ecdc44c1619855a2af63347bfb5e7e009928dcacf04fdd"}, ] +[[package]] +name = "jiter" +version = "0.11.0" +description = "Fast iterable JSON parser." +optional = false +python-versions = ">=3.9" +groups = ["main"] +markers = "python_version >= \"3.9\" and platform_python_implementation != \"PyPy\" or python_version >= \"3.10\"" +files = [ + {file = "jiter-0.11.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:3893ce831e1c0094a83eeaf56c635a167d6fa8cc14393cc14298fd6fdc2a2449"}, + {file = "jiter-0.11.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:25c625b9b61b5a8725267fdf867ef2e51b429687f6a4eef211f4612e95607179"}, + {file = "jiter-0.11.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dd4ca85fb6a62cf72e1c7f5e34ddef1b660ce4ed0886ec94a1ef9777d35eaa1f"}, + {file = "jiter-0.11.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:572208127034725e79c28437b82414028c3562335f2b4f451d98136d0fc5f9cd"}, + {file = "jiter-0.11.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:494ba627c7f550ad3dabb21862864b8f2216098dc18ff62f37b37796f2f7c325"}, + {file = "jiter-0.11.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b8da18a99f58bca3ecc2d2bba99cac000a924e115b6c4f0a2b98f752b6fbf39a"}, + {file = "jiter-0.11.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4ffd3b0fff3fabbb02cc09910c08144db6bb5697a98d227a074401e01ee63dd"}, + {file = "jiter-0.11.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8fe6530aa738a4f7d4e4702aa8f9581425d04036a5f9e25af65ebe1f708f23be"}, + {file = "jiter-0.11.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e35d66681c133a03d7e974e7eedae89720fe8ca3bd09f01a4909b86a8adf31f5"}, + {file = "jiter-0.11.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c59459beca2fbc9718b6f1acb7bfb59ebc3eb4294fa4d40e9cb679dafdcc6c60"}, + {file = "jiter-0.11.0-cp310-cp310-win32.whl", hash = "sha256:b7b0178417b0dcfc5f259edbc6db2b1f5896093ed9035ee7bab0f2be8854726d"}, + {file = "jiter-0.11.0-cp310-cp310-win_amd64.whl", hash = "sha256:11df2bf99fb4754abddd7f5d940a48e51f9d11624d6313ca4314145fcad347f0"}, + {file = "jiter-0.11.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:cb5d9db02979c3f49071fce51a48f4b4e4cf574175fb2b11c7a535fa4867b222"}, + {file = "jiter-0.11.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1dc6a123f3471c4730db7ca8ba75f1bb3dcb6faeb8d46dd781083e7dee88b32d"}, + {file = "jiter-0.11.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:09858f8d230f031c7b8e557429102bf050eea29c77ad9c34c8fe253c5329acb7"}, + {file = "jiter-0.11.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:dbe2196c4a0ce760925a74ab4456bf644748ab0979762139626ad138f6dac72d"}, + {file = "jiter-0.11.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5beb56d22b63647bafd0b74979216fdee80c580c0c63410be8c11053860ffd09"}, + {file = "jiter-0.11.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:97025d09ef549795d8dc720a824312cee3253c890ac73c621721ddfc75066789"}, + {file = "jiter-0.11.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d50880a6da65d8c23a2cf53c412847d9757e74cc9a3b95c5704a1d1a24667347"}, + {file = "jiter-0.11.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:452d80a1c86c095a242007bd9fc5d21b8a8442307193378f891cb8727e469648"}, + {file = "jiter-0.11.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e84e58198d4894668eec2da660ffff60e0f3e60afa790ecc50cb12b0e02ca1d4"}, + {file = "jiter-0.11.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:df64edcfc5dd5279a791eea52aa113d432c933119a025b0b5739f90d2e4e75f1"}, + {file = "jiter-0.11.0-cp311-cp311-win32.whl", hash = "sha256:144fc21337d21b1d048f7f44bf70881e1586401d405ed3a98c95a114a9994982"}, + {file = "jiter-0.11.0-cp311-cp311-win_amd64.whl", hash = "sha256:b0f32e644d241293b892b1a6dd8f0b9cc029bfd94c97376b2681c36548aabab7"}, + {file = "jiter-0.11.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:2fb7b377688cc3850bbe5c192a6bd493562a0bc50cbc8b047316428fbae00ada"}, + {file = "jiter-0.11.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a1b7cbe3f25bd0d8abb468ba4302a5d45617ee61b2a7a638f63fee1dc086be99"}, + {file = "jiter-0.11.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c0a7f0ec81d5b7588c5cade1eb1925b91436ae6726dc2df2348524aeabad5de6"}, + {file = "jiter-0.11.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:07630bb46ea2a6b9c6ed986c6e17e35b26148cce2c535454b26ee3f0e8dcaba1"}, + {file = "jiter-0.11.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7764f27d28cd4a9cbc61704dfcd80c903ce3aad106a37902d3270cd6673d17f4"}, + {file = "jiter-0.11.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1d4a6c4a737d486f77f842aeb22807edecb4a9417e6700c7b981e16d34ba7c72"}, + {file = "jiter-0.11.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cf408d2a0abd919b60de8c2e7bc5eeab72d4dafd18784152acc7c9adc3291591"}, + {file = "jiter-0.11.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:cdef53eda7d18e799625023e1e250dbc18fbc275153039b873ec74d7e8883e09"}, + {file = "jiter-0.11.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:53933a38ef7b551dd9c7f1064f9d7bb235bb3168d0fa5f14f0798d1b7ea0d9c5"}, + {file = "jiter-0.11.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:11840d2324c9ab5162fc1abba23bc922124fedcff0d7b7f85fffa291e2f69206"}, + {file = "jiter-0.11.0-cp312-cp312-win32.whl", hash = "sha256:4f01a744d24a5f2bb4a11657a1b27b61dc038ae2e674621a74020406e08f749b"}, + {file = "jiter-0.11.0-cp312-cp312-win_amd64.whl", hash = "sha256:29fff31190ab3a26de026da2f187814f4b9c6695361e20a9ac2123e4d4378a4c"}, + {file = "jiter-0.11.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:4441a91b80a80249f9a6452c14b2c24708f139f64de959943dfeaa6cb915e8eb"}, + {file = "jiter-0.11.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ff85fc6d2a431251ad82dbd1ea953affb5a60376b62e7d6809c5cd058bb39471"}, + {file = "jiter-0.11.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c5e86126d64706fd28dfc46f910d496923c6f95b395138c02d0e252947f452bd"}, + {file = "jiter-0.11.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4ad8bd82165961867a10f52010590ce0b7a8c53da5ddd8bbb62fef68c181b921"}, + {file = "jiter-0.11.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b42c2cd74273455ce439fd9528db0c6e84b5623cb74572305bdd9f2f2961d3df"}, + {file = "jiter-0.11.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f0062dab98172dd0599fcdbf90214d0dcde070b1ff38a00cc1b90e111f071982"}, + {file = "jiter-0.11.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bb948402821bc76d1f6ef0f9e19b816f9b09f8577844ba7140f0b6afe994bc64"}, + {file = "jiter-0.11.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:25a5b1110cca7329fd0daf5060faa1234be5c11e988948e4f1a1923b6a457fe1"}, + {file = "jiter-0.11.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:bf11807e802a214daf6c485037778843fadd3e2ec29377ae17e0706ec1a25758"}, + {file = "jiter-0.11.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:dbb57da40631c267861dd0090461222060960012d70fd6e4c799b0f62d0ba166"}, + {file = "jiter-0.11.0-cp313-cp313-win32.whl", hash = "sha256:8e36924dad32c48d3c5e188d169e71dc6e84d6cb8dedefea089de5739d1d2f80"}, + {file = "jiter-0.11.0-cp313-cp313-win_amd64.whl", hash = "sha256:452d13e4fd59698408087235259cebe67d9d49173b4dacb3e8d35ce4acf385d6"}, + {file = "jiter-0.11.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:089f9df9f69532d1339e83142438668f52c97cd22ee2d1195551c2b1a9e6cf33"}, + {file = "jiter-0.11.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:29ed1fe69a8c69bf0f2a962d8d706c7b89b50f1332cd6b9fbda014f60bd03a03"}, + {file = "jiter-0.11.0-cp313-cp313t-win_amd64.whl", hash = "sha256:a4d71d7ea6ea8786291423fe209acf6f8d398a0759d03e7f24094acb8ab686ba"}, + {file = "jiter-0.11.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:9a6dff27eca70930bdbe4cbb7c1a4ba8526e13b63dc808c0670083d2d51a4a72"}, + {file = "jiter-0.11.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:b1ae2a7593a62132c7d4c2abbee80bbbb94fdc6d157e2c6cc966250c564ef774"}, + {file = "jiter-0.11.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7b13a431dba4b059e9e43019d3022346d009baf5066c24dcdea321a303cde9f0"}, + {file = "jiter-0.11.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:af62e84ca3889604ebb645df3b0a3f3bcf6b92babbff642bd214616f57abb93a"}, + {file = "jiter-0.11.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c6f3b32bb723246e6b351aecace52aba78adb8eeb4b2391630322dc30ff6c773"}, + {file = "jiter-0.11.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:adcab442f4a099a358a7f562eaa54ed6456fb866e922c6545a717be51dbed7d7"}, + {file = "jiter-0.11.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c9967c2ab338ee2b2c0102fd379ec2693c496abf71ffd47e4d791d1f593b68e2"}, + {file = "jiter-0.11.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e7d0bed3b187af8b47a981d9742ddfc1d9b252a7235471ad6078e7e4e5fe75c2"}, + {file = "jiter-0.11.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:f6fe0283e903ebc55f1a6cc569b8c1f3bf4abd026fed85e3ff8598a9e6f982f0"}, + {file = "jiter-0.11.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:4ee5821e3d66606b29ae5b497230b304f1376f38137d69e35f8d2bd5f310ff73"}, + {file = "jiter-0.11.0-cp314-cp314-win32.whl", hash = "sha256:c2d13ba7567ca8799f17c76ed56b1d49be30df996eb7fa33e46b62800562a5e2"}, + {file = "jiter-0.11.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:fb4790497369d134a07fc763cc88888c46f734abdd66f9fdf7865038bf3a8f40"}, + {file = "jiter-0.11.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6e2bbf24f16ba5ad4441a9845e40e4ea0cb9eed00e76ba94050664ef53ef4406"}, + {file = "jiter-0.11.0-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:719891c2fb7628a41adff4f2f54c19380a27e6fdfdb743c24680ef1a54c67bd0"}, + {file = "jiter-0.11.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:df7f1927cbdf34cb91262a5418ca06920fd42f1cf733936d863aeb29b45a14ef"}, + {file = "jiter-0.11.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e71ae6d969d0c9bab336c5e9e2fabad31e74d823f19e3604eaf96d9a97f463df"}, + {file = "jiter-0.11.0-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5661469a7b2be25ade3a4bb6c21ffd1e142e13351a0759f264dfdd3ad99af1ab"}, + {file = "jiter-0.11.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:76c15ef0d3d02f8b389066fa4c410a0b89e9cc6468a1f0674c5925d2f3c3e890"}, + {file = "jiter-0.11.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:63782a1350917a27817030716566ed3d5b3c731500fd42d483cbd7094e2c5b25"}, + {file = "jiter-0.11.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5a7092b699646a1ddc03a7b112622d9c066172627c7382659befb0d2996f1659"}, + {file = "jiter-0.11.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f637b8e818f6d75540f350a6011ce21252573c0998ea1b4365ee54b7672c23c5"}, + {file = "jiter-0.11.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:a624d87719e1b5d09c15286eaee7e1532a40c692a096ea7ca791121365f548c1"}, + {file = "jiter-0.11.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a9d0146d8d9b3995821bb586fc8256636258947c2f39da5bab709f3a28fb1a0b"}, + {file = "jiter-0.11.0-cp39-cp39-win32.whl", hash = "sha256:d067655a7cf0831eb8ec3e39cbd752995e9b69a2206df3535b3a067fac23b032"}, + {file = "jiter-0.11.0-cp39-cp39-win_amd64.whl", hash = "sha256:f05d03775a11aaf132c447436983169958439f1219069abf24662a672851f94e"}, + {file = "jiter-0.11.0-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:902b43386c04739229076bd1c4c69de5d115553d982ab442a8ae82947c72ede7"}, + {file = "jiter-0.11.0.tar.gz", hash = "sha256:1d9637eaf8c1d6a63d6562f2a6e5ab3af946c66037eb1b894e8fad75422266e4"}, +] + [[package]] name = "jmespath" version = "1.0.1" @@ -2529,6 +3507,7 @@ description = "An implementation of JSON Schema validation for Python" optional = false python-versions = ">=3.8" groups = ["main"] +markers = "python_full_version == \"3.8.*\" or platform_python_implementation == \"PyPy\" and python_version < \"3.10\"" files = [ {file = "jsonschema-4.23.0-py3-none-any.whl", hash = "sha256:fbadb6f8b144a8f8cf9f0b89ba94501d143e50411a1278633f56a7acf7fd5566"}, {file = "jsonschema-4.23.0.tar.gz", hash = "sha256:d71497fef26351a33265337fa77ffeb82423f3ea21283cd9467bb03999266bc4"}, @@ -2546,6 +3525,29 @@ rpds-py = ">=0.7.1" format = ["fqdn", "idna", "isoduration", "jsonpointer (>1.13)", "rfc3339-validator", "rfc3987", "uri-template", "webcolors (>=1.11)"] format-nongpl = ["fqdn", "idna", "isoduration", "jsonpointer (>1.13)", "rfc3339-validator", "rfc3986-validator (>0.1.0)", "uri-template", "webcolors (>=24.6.0)"] +[[package]] +name = "jsonschema" +version = "4.25.1" +description = "An implementation of JSON Schema validation for Python" +optional = false +python-versions = ">=3.9" +groups = ["main"] +markers = "python_version >= \"3.9\" and platform_python_implementation != \"PyPy\" or python_version >= \"3.10\"" +files = [ + {file = "jsonschema-4.25.1-py3-none-any.whl", hash = "sha256:3fba0169e345c7175110351d456342c364814cfcf3b964ba4587f22915230a63"}, + {file = "jsonschema-4.25.1.tar.gz", hash = "sha256:e4a9655ce0da0c0b67a085847e00a3a51449e1157f4f75e9fb5aa545e122eb85"}, +] + +[package.dependencies] +attrs = ">=22.2.0" +jsonschema-specifications = ">=2023.03.6" +referencing = ">=0.28.4" +rpds-py = ">=0.7.1" + +[package.extras] +format = ["fqdn", "idna", "isoduration", "jsonpointer (>1.13)", "rfc3339-validator", "rfc3987", "uri-template", "webcolors (>=1.11)"] +format-nongpl = ["fqdn", "idna", "isoduration", "jsonpointer (>1.13)", "rfc3339-validator", "rfc3986-validator (>0.1.0)", "rfc3987-syntax (>=1.1.0)", "uri-template", "webcolors (>=24.6.0)"] + [[package]] name = "jsonschema-specifications" version = "2023.12.1" @@ -2553,6 +3555,7 @@ description = "The JSON Schema meta-schemas and vocabularies, exposed as a Regis optional = false python-versions = ">=3.8" groups = ["main"] +markers = "python_full_version == \"3.8.*\" or platform_python_implementation == \"PyPy\" and python_version < \"3.10\"" files = [ {file = "jsonschema_specifications-2023.12.1-py3-none-any.whl", hash = "sha256:87e4fdf3a94858b8a2ba2778d9ba57d8a9cafca7c7489c46ba0d30a8bc6a9c3c"}, {file = "jsonschema_specifications-2023.12.1.tar.gz", hash = "sha256:48a76787b3e70f5ed53f1160d2b81f586e4ca6d1548c5de7085d1682674764cc"}, @@ -2562,6 +3565,22 @@ files = [ importlib-resources = {version = ">=1.4.0", markers = "python_version < \"3.9\""} referencing = ">=0.31.0" +[[package]] +name = "jsonschema-specifications" +version = "2025.9.1" +description = "The JSON Schema meta-schemas and vocabularies, exposed as a Registry" +optional = false +python-versions = ">=3.9" +groups = ["main"] +markers = "python_version >= \"3.9\" and platform_python_implementation != \"PyPy\" or python_version >= \"3.10\"" +files = [ + {file = "jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe"}, + {file = "jsonschema_specifications-2025.9.1.tar.gz", hash = "sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d"}, +] + +[package.dependencies] +referencing = ">=0.31.0" + [[package]] name = "kiwisolver" version = "1.4.9" @@ -2681,6 +3700,7 @@ description = "A client library for accessing langfuse" optional = false python-versions = "<4.0,>=3.8.1" groups = ["dev"] +markers = "python_full_version == \"3.8.*\" or platform_python_implementation == \"PyPy\" and python_version < \"3.10\"" files = [ {file = "langfuse-2.54.1-py3-none-any.whl", hash = "sha256:1f1261cf763886758c70e192133340ff296169cc0930cde725eee52d467eb661"}, {file = "langfuse-2.54.1.tar.gz", hash = "sha256:7efc70799740ffa0ac7e04066e0596fb6433e8e501fc850c6a4e7967de6de8a7"}, @@ -2700,6 +3720,34 @@ langchain = ["langchain (>=0.0.309)"] llama-index = ["llama-index (>=0.10.12,<2.0.0)"] openai = ["openai (>=0.27.8)"] +[[package]] +name = "langfuse" +version = "2.60.10" +description = "A client library for accessing langfuse" +optional = false +python-versions = "<4.0,>=3.9" +groups = ["dev"] +markers = "python_version >= \"3.9\" and platform_python_implementation != \"PyPy\" or python_version >= \"3.10\"" +files = [ + {file = "langfuse-2.60.10-py3-none-any.whl", hash = "sha256:815c6369194aa5b2a24f88eb9952f7c3fc863272c41e90642a71f3bc76f4a11f"}, + {file = "langfuse-2.60.10.tar.gz", hash = "sha256:a26d0d927a28ee01b2d12bb5b862590b643cc4e60a28de6e2b0c2cfff5dbfc6a"}, +] + +[package.dependencies] +anyio = ">=4.4.0,<5.0.0" +backoff = ">=1.10.0" +httpx = ">=0.15.4,<1.0" +idna = ">=3.7,<4.0" +packaging = ">=23.2,<25.0" +pydantic = ">=1.10.7,<3.0" +requests = ">=2,<3" +wrapt = ">=1.14,<2.0" + +[package.extras] +langchain = ["langchain (>=0.0.309)"] +llama-index = ["llama-index (>=0.10.12,<2.0.0)"] +openai = ["openai (>=0.27.8)"] + [[package]] name = "litellm-enterprise" version = "0.1.20" @@ -2775,7 +3823,7 @@ description = "Python port of markdown-it. Markdown parsing, done right!" optional = true python-versions = ">=3.8" groups = ["main"] -markers = "extra == \"proxy\"" +markers = "extra == \"proxy\" and python_version < \"3.10\"" files = [ {file = "markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb"}, {file = "markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1"}, @@ -2794,6 +3842,31 @@ profiling = ["gprof2dot"] rtd = ["jupyter_sphinx", "mdit-py-plugins", "myst-parser", "pyyaml", "sphinx", "sphinx-copybutton", "sphinx-design", "sphinx_book_theme"] testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"] +[[package]] +name = "markdown-it-py" +version = "4.0.0" +description = "Python port of markdown-it. Markdown parsing, done right!" +optional = true +python-versions = ">=3.10" +groups = ["main"] +markers = "python_version >= \"3.10\" and extra == \"proxy\"" +files = [ + {file = "markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147"}, + {file = "markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3"}, +] + +[package.dependencies] +mdurl = ">=0.1,<1.0" + +[package.extras] +benchmarking = ["psutil", "pytest", "pytest-benchmark"] +compare = ["commonmark (>=0.9,<1.0)", "markdown (>=3.4,<4.0)", "markdown-it-pyrs", "mistletoe (>=1.0,<2.0)", "mistune (>=3.0,<4.0)", "panflute (>=2.3,<3.0)"] +linkify = ["linkify-it-py (>=1,<3)"] +plugins = ["mdit-py-plugins (>=0.5.0)"] +profiling = ["gprof2dot"] +rtd = ["ipykernel", "jupyter_sphinx", "mdit-py-plugins (>=0.5.0)", "myst-parser", "pyyaml", "sphinx", "sphinx-book-theme (>=1.0,<2.0)", "sphinx-copybutton", "sphinx-design"] +testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions", "requests"] + [[package]] name = "markupsafe" version = "2.1.5" @@ -2801,6 +3874,7 @@ description = "Safely add untrusted strings to HTML/XML markup." optional = false python-versions = ">=3.7" groups = ["main", "proxy-dev"] +markers = "python_full_version == \"3.8.*\" or platform_python_implementation == \"PyPy\" and python_version < \"3.10\"" files = [ {file = "MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a17a92de5231666cfbe003f0e4b9b3a7ae3afb1ec2845aadc2bacc93ff85febc"}, {file = "MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:72b6be590cc35924b02c78ef34b467da4ba07e4e0f0454a2c5907f473fc50ce5"}, @@ -2864,6 +3938,78 @@ files = [ {file = "MarkupSafe-2.1.5.tar.gz", hash = "sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b"}, ] +[[package]] +name = "markupsafe" +version = "3.0.2" +description = "Safely add untrusted strings to HTML/XML markup." +optional = false +python-versions = ">=3.9" +groups = ["main", "proxy-dev"] +markers = "python_version >= \"3.9\" and platform_python_implementation != \"PyPy\" or python_version >= \"3.10\"" +files = [ + {file = "MarkupSafe-3.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38a9ef736c01fccdd6600705b09dc574584b89bea478200c5fbf112a6b0d5579"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bbcb445fa71794da8f178f0f6d66789a28d7319071af7a496d4d507ed566270d"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57cb5a3cf367aeb1d316576250f65edec5bb3be939e9247ae594b4bcbc317dfb"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3809ede931876f5b2ec92eef964286840ed3540dadf803dd570c3b7e13141a3b"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e07c3764494e3776c602c1e78e298937c3315ccc9043ead7e685b7f2b8d47b3c"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b424c77b206d63d500bcb69fa55ed8d0e6a3774056bdc4839fc9298a7edca171"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-win32.whl", hash = "sha256:fcabf5ff6eea076f859677f5f0b6b5c1a51e70a376b0579e0eadef8db48c6b50"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:6af100e168aa82a50e186c82875a5893c5597a0c1ccdb0d8b40240b1f28b969a"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-win32.whl", hash = "sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-win32.whl", hash = "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:eaa0a10b7f72326f1372a713e73c3f739b524b3af41feb43e4921cb529f5929a"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:48032821bbdf20f5799ff537c7ac3d1fba0ba032cfc06194faffa8cda8b560ff"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a9d3f5f0901fdec14d8d2f66ef7d035f2157240a433441719ac9a3fba440b13"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88b49a3b9ff31e19998750c38e030fc7bb937398b1f78cfa599aaef92d693144"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cfad01eed2c2e0c01fd0ecd2ef42c492f7f93902e39a42fc9ee1692961443a29"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:1225beacc926f536dc82e45f8a4d68502949dc67eea90eab715dea3a21c1b5f0"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:3169b1eefae027567d1ce6ee7cae382c57fe26e82775f460f0b2778beaad66c0"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:eb7972a85c54febfb25b5c4b4f3af4dcc731994c7da0d8a0b4a6eb0640e1d178"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-win32.whl", hash = "sha256:8c4e8c3ce11e1f92f6536ff07154f9d49677ebaaafc32db9db4620bc11ed480f"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:6e296a513ca3d94054c2c881cc913116e90fd030ad1c656b3869762b754f5f8a"}, + {file = "markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0"}, +] + [[package]] name = "matplotlib" version = "3.10.6" @@ -3150,18 +4296,18 @@ pydantic = ">=1.10.8,<3" [[package]] name = "msal" -version = "1.33.0" +version = "1.34.0" description = "The Microsoft Authentication Library (MSAL) for Python library enables your app to access the Microsoft Cloud by supporting authentication of users with Microsoft Azure Active Directory accounts (AAD) and Microsoft Accounts (MSA) using industry standard OAuth2 and OpenID Connect." optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" groups = ["main", "proxy-dev"] files = [ - {file = "msal-1.33.0-py3-none-any.whl", hash = "sha256:c0cd41cecf8eaed733ee7e3be9e040291eba53b0f262d3ae9c58f38b04244273"}, - {file = "msal-1.33.0.tar.gz", hash = "sha256:836ad80faa3e25a7d71015c990ce61f704a87328b1e73bcbb0623a18cbf17510"}, + {file = "msal-1.34.0-py3-none-any.whl", hash = "sha256:f669b1644e4950115da7a176441b0e13ec2975c29528d8b9e81316023676d6e1"}, + {file = "msal-1.34.0.tar.gz", hash = "sha256:76ba83b716ea5a6d75b0279c0ac353a0e05b820ca1f6682c0eb7f45190c43c2f"}, ] [package.dependencies] -cryptography = ">=2.5,<48" +cryptography = ">=2.5,<49" PyJWT = {version = ">=1.0.0,<3", extras = ["crypto"]} requests = ">=2.0.0,<3" @@ -3175,6 +4321,7 @@ description = "Microsoft Authentication Library extensions (MSAL EX) provides a optional = false python-versions = ">=3.7" groups = ["main", "proxy-dev"] +markers = "python_full_version == \"3.8.*\" or platform_python_implementation == \"PyPy\" and python_version < \"3.10\"" files = [ {file = "msal_extensions-1.3.0-py3-none-any.whl", hash = "sha256:105328ddcbdd342016c9949d8f89e3917554740c8ab26669c0fa0e069e730a0e"}, {file = "msal_extensions-1.3.0.tar.gz", hash = "sha256:96918996642b38c78cd59b55efa0f06fd1373c90e0949be8615697c048fba62c"}, @@ -3186,6 +4333,25 @@ msal = ">=1.29,<2" [package.extras] portalocker = ["portalocker (>=1.4,<4)"] +[[package]] +name = "msal-extensions" +version = "1.3.1" +description = "Microsoft Authentication Library extensions (MSAL EX) provides a persistence API that can save your data on disk, encrypted on Windows, macOS and Linux. Concurrent data access will be coordinated by a file lock mechanism." +optional = false +python-versions = ">=3.9" +groups = ["main", "proxy-dev"] +markers = "python_version >= \"3.9\" and platform_python_implementation != \"PyPy\" or python_version >= \"3.10\"" +files = [ + {file = "msal_extensions-1.3.1-py3-none-any.whl", hash = "sha256:96d3de4d034504e969ac5e85bae8106c8373b5c6568e4c8fa7af2eca9dbe6bca"}, + {file = "msal_extensions-1.3.1.tar.gz", hash = "sha256:c5b0fd10f65ef62b5f1d62f4251d51cbcaf003fcedae8c91b040a488614be1a4"}, +] + +[package.dependencies] +msal = ">=1.29,<2" + +[package.extras] +portalocker = ["portalocker (>=1.4,<4)"] + [[package]] name = "multidict" version = "6.1.0" @@ -3193,6 +4359,7 @@ description = "multidict implementation" optional = false python-versions = ">=3.8" groups = ["main"] +markers = "python_full_version == \"3.8.*\" or platform_python_implementation == \"PyPy\" and python_version < \"3.10\"" files = [ {file = "multidict-6.1.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:3380252550e372e8511d49481bd836264c009adb826b23fefcc5dd3c69692f60"}, {file = "multidict-6.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:99f826cbf970077383d7de805c0681799491cb939c25450b9b5b3ced03ca99f1"}, @@ -3291,6 +4458,130 @@ files = [ [package.dependencies] typing-extensions = {version = ">=4.1.0", markers = "python_version < \"3.11\""} +[[package]] +name = "multidict" +version = "6.6.4" +description = "multidict implementation" +optional = false +python-versions = ">=3.9" +groups = ["main"] +markers = "python_version >= \"3.9\" and platform_python_implementation != \"PyPy\" or python_version >= \"3.10\"" +files = [ + {file = "multidict-6.6.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:b8aa6f0bd8125ddd04a6593437bad6a7e70f300ff4180a531654aa2ab3f6d58f"}, + {file = "multidict-6.6.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b9e5853bbd7264baca42ffc53391b490d65fe62849bf2c690fa3f6273dbcd0cb"}, + {file = "multidict-6.6.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0af5f9dee472371e36d6ae38bde009bd8ce65ac7335f55dcc240379d7bed1495"}, + {file = "multidict-6.6.4-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:d24f351e4d759f5054b641c81e8291e5d122af0fca5c72454ff77f7cbe492de8"}, + {file = "multidict-6.6.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:db6a3810eec08280a172a6cd541ff4a5f6a97b161d93ec94e6c4018917deb6b7"}, + {file = "multidict-6.6.4-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a1b20a9d56b2d81e2ff52ecc0670d583eaabaa55f402e8d16dd062373dbbe796"}, + {file = "multidict-6.6.4-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8c9854df0eaa610a23494c32a6f44a3a550fb398b6b51a56e8c6b9b3689578db"}, + {file = "multidict-6.6.4-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4bb7627fd7a968f41905a4d6343b0d63244a0623f006e9ed989fa2b78f4438a0"}, + {file = "multidict-6.6.4-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:caebafea30ed049c57c673d0b36238b1748683be2593965614d7b0e99125c877"}, + {file = "multidict-6.6.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:ad887a8250eb47d3ab083d2f98db7f48098d13d42eb7a3b67d8a5c795f224ace"}, + {file = "multidict-6.6.4-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:ed8358ae7d94ffb7c397cecb62cbac9578a83ecefc1eba27b9090ee910e2efb6"}, + {file = "multidict-6.6.4-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:ecab51ad2462197a4c000b6d5701fc8585b80eecb90583635d7e327b7b6923eb"}, + {file = "multidict-6.6.4-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:c5c97aa666cf70e667dfa5af945424ba1329af5dd988a437efeb3a09430389fb"}, + {file = "multidict-6.6.4-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:9a950b7cf54099c1209f455ac5970b1ea81410f2af60ed9eb3c3f14f0bfcf987"}, + {file = "multidict-6.6.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:163c7ea522ea9365a8a57832dea7618e6cbdc3cd75f8c627663587459a4e328f"}, + {file = "multidict-6.6.4-cp310-cp310-win32.whl", hash = "sha256:17d2cbbfa6ff20821396b25890f155f40c986f9cfbce5667759696d83504954f"}, + {file = "multidict-6.6.4-cp310-cp310-win_amd64.whl", hash = "sha256:ce9a40fbe52e57e7edf20113a4eaddfacac0561a0879734e636aa6d4bb5e3fb0"}, + {file = "multidict-6.6.4-cp310-cp310-win_arm64.whl", hash = "sha256:01d0959807a451fe9fdd4da3e139cb5b77f7328baf2140feeaf233e1d777b729"}, + {file = "multidict-6.6.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c7a0e9b561e6460484318a7612e725df1145d46b0ef57c6b9866441bf6e27e0c"}, + {file = "multidict-6.6.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6bf2f10f70acc7a2446965ffbc726e5fc0b272c97a90b485857e5c70022213eb"}, + {file = "multidict-6.6.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:66247d72ed62d5dd29752ffc1d3b88f135c6a8de8b5f63b7c14e973ef5bda19e"}, + {file = "multidict-6.6.4-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:105245cc6b76f51e408451a844a54e6823bbd5a490ebfe5bdfc79798511ceded"}, + {file = "multidict-6.6.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cbbc54e58b34c3bae389ef00046be0961f30fef7cb0dd9c7756aee376a4f7683"}, + {file = "multidict-6.6.4-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:56c6b3652f945c9bc3ac6c8178cd93132b8d82dd581fcbc3a00676c51302bc1a"}, + {file = "multidict-6.6.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b95494daf857602eccf4c18ca33337dd2be705bccdb6dddbfc9d513e6addb9d9"}, + {file = "multidict-6.6.4-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e5b1413361cef15340ab9dc61523e653d25723e82d488ef7d60a12878227ed50"}, + {file = "multidict-6.6.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e167bf899c3d724f9662ef00b4f7fef87a19c22b2fead198a6f68b263618df52"}, + {file = "multidict-6.6.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:aaea28ba20a9026dfa77f4b80369e51cb767c61e33a2d4043399c67bd95fb7c6"}, + {file = "multidict-6.6.4-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:8c91cdb30809a96d9ecf442ec9bc45e8cfaa0f7f8bdf534e082c2443a196727e"}, + {file = "multidict-6.6.4-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:1a0ccbfe93ca114c5d65a2471d52d8829e56d467c97b0e341cf5ee45410033b3"}, + {file = "multidict-6.6.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:55624b3f321d84c403cb7d8e6e982f41ae233d85f85db54ba6286f7295dc8a9c"}, + {file = "multidict-6.6.4-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:4a1fb393a2c9d202cb766c76208bd7945bc194eba8ac920ce98c6e458f0b524b"}, + {file = "multidict-6.6.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:43868297a5759a845fa3a483fb4392973a95fb1de891605a3728130c52b8f40f"}, + {file = "multidict-6.6.4-cp311-cp311-win32.whl", hash = "sha256:ed3b94c5e362a8a84d69642dbeac615452e8af9b8eb825b7bc9f31a53a1051e2"}, + {file = "multidict-6.6.4-cp311-cp311-win_amd64.whl", hash = "sha256:d8c112f7a90d8ca5d20213aa41eac690bb50a76da153e3afb3886418e61cb22e"}, + {file = "multidict-6.6.4-cp311-cp311-win_arm64.whl", hash = "sha256:3bb0eae408fa1996d87247ca0d6a57b7fc1dcf83e8a5c47ab82c558c250d4adf"}, + {file = "multidict-6.6.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0ffb87be160942d56d7b87b0fdf098e81ed565add09eaa1294268c7f3caac4c8"}, + {file = "multidict-6.6.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d191de6cbab2aff5de6c5723101705fd044b3e4c7cfd587a1929b5028b9714b3"}, + {file = "multidict-6.6.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:38a0956dd92d918ad5feff3db8fcb4a5eb7dba114da917e1a88475619781b57b"}, + {file = "multidict-6.6.4-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:6865f6d3b7900ae020b495d599fcf3765653bc927951c1abb959017f81ae8287"}, + {file = "multidict-6.6.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0a2088c126b6f72db6c9212ad827d0ba088c01d951cee25e758c450da732c138"}, + {file = "multidict-6.6.4-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0f37bed7319b848097085d7d48116f545985db988e2256b2e6f00563a3416ee6"}, + {file = "multidict-6.6.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:01368e3c94032ba6ca0b78e7ccb099643466cf24f8dc8eefcfdc0571d56e58f9"}, + {file = "multidict-6.6.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8fe323540c255db0bffee79ad7f048c909f2ab0edb87a597e1c17da6a54e493c"}, + {file = "multidict-6.6.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8eb3025f17b0a4c3cd08cda49acf312a19ad6e8a4edd9dbd591e6506d999402"}, + {file = "multidict-6.6.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:bbc14f0365534d35a06970d6a83478b249752e922d662dc24d489af1aa0d1be7"}, + {file = "multidict-6.6.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:75aa52fba2d96bf972e85451b99d8e19cc37ce26fd016f6d4aa60da9ab2b005f"}, + {file = "multidict-6.6.4-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:4fefd4a815e362d4f011919d97d7b4a1e566f1dde83dc4ad8cfb5b41de1df68d"}, + {file = "multidict-6.6.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:db9801fe021f59a5b375ab778973127ca0ac52429a26e2fd86aa9508f4d26eb7"}, + {file = "multidict-6.6.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:a650629970fa21ac1fb06ba25dabfc5b8a2054fcbf6ae97c758aa956b8dba802"}, + {file = "multidict-6.6.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:452ff5da78d4720d7516a3a2abd804957532dd69296cb77319c193e3ffb87e24"}, + {file = "multidict-6.6.4-cp312-cp312-win32.whl", hash = "sha256:8c2fcb12136530ed19572bbba61b407f655e3953ba669b96a35036a11a485793"}, + {file = "multidict-6.6.4-cp312-cp312-win_amd64.whl", hash = "sha256:047d9425860a8c9544fed1b9584f0c8bcd31bcde9568b047c5e567a1025ecd6e"}, + {file = "multidict-6.6.4-cp312-cp312-win_arm64.whl", hash = "sha256:14754eb72feaa1e8ae528468f24250dd997b8e2188c3d2f593f9eba259e4b364"}, + {file = "multidict-6.6.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:f46a6e8597f9bd71b31cc708195d42b634c8527fecbcf93febf1052cacc1f16e"}, + {file = "multidict-6.6.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:22e38b2bc176c5eb9c0a0e379f9d188ae4cd8b28c0f53b52bce7ab0a9e534657"}, + {file = "multidict-6.6.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5df8afd26f162da59e218ac0eefaa01b01b2e6cd606cffa46608f699539246da"}, + {file = "multidict-6.6.4-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:49517449b58d043023720aa58e62b2f74ce9b28f740a0b5d33971149553d72aa"}, + {file = "multidict-6.6.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ae9408439537c5afdca05edd128a63f56a62680f4b3c234301055d7a2000220f"}, + {file = "multidict-6.6.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:87a32d20759dc52a9e850fe1061b6e41ab28e2998d44168a8a341b99ded1dba0"}, + {file = "multidict-6.6.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:52e3c8d43cdfff587ceedce9deb25e6ae77daba560b626e97a56ddcad3756879"}, + {file = "multidict-6.6.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ad8850921d3a8d8ff6fbef790e773cecfc260bbfa0566998980d3fa8f520bc4a"}, + {file = "multidict-6.6.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:497a2954adc25c08daff36f795077f63ad33e13f19bfff7736e72c785391534f"}, + {file = "multidict-6.6.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:024ce601f92d780ca1617ad4be5ac15b501cc2414970ffa2bb2bbc2bd5a68fa5"}, + {file = "multidict-6.6.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:a693fc5ed9bdd1c9e898013e0da4dcc640de7963a371c0bd458e50e046bf6438"}, + {file = "multidict-6.6.4-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:190766dac95aab54cae5b152a56520fd99298f32a1266d66d27fdd1b5ac00f4e"}, + {file = "multidict-6.6.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:34d8f2a5ffdceab9dcd97c7a016deb2308531d5f0fced2bb0c9e1df45b3363d7"}, + {file = "multidict-6.6.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:59e8d40ab1f5a8597abcef00d04845155a5693b5da00d2c93dbe88f2050f2812"}, + {file = "multidict-6.6.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:467fe64138cfac771f0e949b938c2e1ada2b5af22f39692aa9258715e9ea613a"}, + {file = "multidict-6.6.4-cp313-cp313-win32.whl", hash = "sha256:14616a30fe6d0a48d0a48d1a633ab3b8bec4cf293aac65f32ed116f620adfd69"}, + {file = "multidict-6.6.4-cp313-cp313-win_amd64.whl", hash = "sha256:40cd05eaeb39e2bc8939451f033e57feaa2ac99e07dbca8afe2be450a4a3b6cf"}, + {file = "multidict-6.6.4-cp313-cp313-win_arm64.whl", hash = "sha256:f6eb37d511bfae9e13e82cb4d1af36b91150466f24d9b2b8a9785816deb16605"}, + {file = "multidict-6.6.4-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:6c84378acd4f37d1b507dfa0d459b449e2321b3ba5f2338f9b085cf7a7ba95eb"}, + {file = "multidict-6.6.4-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0e0558693063c75f3d952abf645c78f3c5dfdd825a41d8c4d8156fc0b0da6e7e"}, + {file = "multidict-6.6.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3f8e2384cb83ebd23fd07e9eada8ba64afc4c759cd94817433ab8c81ee4b403f"}, + {file = "multidict-6.6.4-cp313-cp313t-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:f996b87b420995a9174b2a7c1a8daf7db4750be6848b03eb5e639674f7963773"}, + {file = "multidict-6.6.4-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cc356250cffd6e78416cf5b40dc6a74f1edf3be8e834cf8862d9ed5265cf9b0e"}, + {file = "multidict-6.6.4-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:dadf95aa862714ea468a49ad1e09fe00fcc9ec67d122f6596a8d40caf6cec7d0"}, + {file = "multidict-6.6.4-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7dd57515bebffd8ebd714d101d4c434063322e4fe24042e90ced41f18b6d3395"}, + {file = "multidict-6.6.4-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:967af5f238ebc2eb1da4e77af5492219fbd9b4b812347da39a7b5f5c72c0fa45"}, + {file = "multidict-6.6.4-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2a4c6875c37aae9794308ec43e3530e4aa0d36579ce38d89979bbf89582002bb"}, + {file = "multidict-6.6.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:7f683a551e92bdb7fac545b9c6f9fa2aebdeefa61d607510b3533286fcab67f5"}, + {file = "multidict-6.6.4-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:3ba5aaf600edaf2a868a391779f7a85d93bed147854925f34edd24cc70a3e141"}, + {file = "multidict-6.6.4-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:580b643b7fd2c295d83cad90d78419081f53fd532d1f1eb67ceb7060f61cff0d"}, + {file = "multidict-6.6.4-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:37b7187197da6af3ee0b044dbc9625afd0c885f2800815b228a0e70f9a7f473d"}, + {file = "multidict-6.6.4-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:e1b93790ed0bc26feb72e2f08299691ceb6da5e9e14a0d13cc74f1869af327a0"}, + {file = "multidict-6.6.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:a506a77ddee1efcca81ecbeae27ade3e09cdf21a8ae854d766c2bb4f14053f92"}, + {file = "multidict-6.6.4-cp313-cp313t-win32.whl", hash = "sha256:f93b2b2279883d1d0a9e1bd01f312d6fc315c5e4c1f09e112e4736e2f650bc4e"}, + {file = "multidict-6.6.4-cp313-cp313t-win_amd64.whl", hash = "sha256:6d46a180acdf6e87cc41dc15d8f5c2986e1e8739dc25dbb7dac826731ef381a4"}, + {file = "multidict-6.6.4-cp313-cp313t-win_arm64.whl", hash = "sha256:756989334015e3335d087a27331659820d53ba432befdef6a718398b0a8493ad"}, + {file = "multidict-6.6.4-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:af7618b591bae552b40dbb6f93f5518328a949dac626ee75927bba1ecdeea9f4"}, + {file = "multidict-6.6.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b6819f83aef06f560cb15482d619d0e623ce9bf155115150a85ab11b8342a665"}, + {file = "multidict-6.6.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4d09384e75788861e046330308e7af54dd306aaf20eb760eb1d0de26b2bea2cb"}, + {file = "multidict-6.6.4-cp39-cp39-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:a59c63061f1a07b861c004e53869eb1211ffd1a4acbca330e3322efa6dd02978"}, + {file = "multidict-6.6.4-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:350f6b0fe1ced61e778037fdc7613f4051c8baf64b1ee19371b42a3acdb016a0"}, + {file = "multidict-6.6.4-cp39-cp39-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0c5cbac6b55ad69cb6aa17ee9343dfbba903118fd530348c330211dc7aa756d1"}, + {file = "multidict-6.6.4-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:630f70c32b8066ddfd920350bc236225814ad94dfa493fe1910ee17fe4365cbb"}, + {file = "multidict-6.6.4-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f8d4916a81697faec6cb724a273bd5457e4c6c43d82b29f9dc02c5542fd21fc9"}, + {file = "multidict-6.6.4-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8e42332cf8276bb7645d310cdecca93a16920256a5b01bebf747365f86a1675b"}, + {file = "multidict-6.6.4-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:f3be27440f7644ab9a13a6fc86f09cdd90b347c3c5e30c6d6d860de822d7cb53"}, + {file = "multidict-6.6.4-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:21f216669109e02ef3e2415ede07f4f8987f00de8cdfa0cc0b3440d42534f9f0"}, + {file = "multidict-6.6.4-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:d9890d68c45d1aeac5178ded1d1cccf3bc8d7accf1f976f79bf63099fb16e4bd"}, + {file = "multidict-6.6.4-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:edfdcae97cdc5d1a89477c436b61f472c4d40971774ac4729c613b4b133163cb"}, + {file = "multidict-6.6.4-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:0b2e886624be5773e69cf32bcb8534aecdeb38943520b240fed3d5596a430f2f"}, + {file = "multidict-6.6.4-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:be5bf4b3224948032a845d12ab0f69f208293742df96dc14c4ff9b09e508fc17"}, + {file = "multidict-6.6.4-cp39-cp39-win32.whl", hash = "sha256:10a68a9191f284fe9d501fef4efe93226e74df92ce7a24e301371293bd4918ae"}, + {file = "multidict-6.6.4-cp39-cp39-win_amd64.whl", hash = "sha256:ee25f82f53262f9ac93bd7e58e47ea1bdcc3393cef815847e397cba17e284210"}, + {file = "multidict-6.6.4-cp39-cp39-win_arm64.whl", hash = "sha256:f9867e55590e0855bcec60d4f9a092b69476db64573c9fe17e92b0c50614c16a"}, + {file = "multidict-6.6.4-py3-none-any.whl", hash = "sha256:27d8f8e125c07cb954e54d75d04905a9bba8a439c1d84aca94949d4d03d8601c"}, + {file = "multidict-6.6.4.tar.gz", hash = "sha256:d2d4e4787672911b48350df02ed3fa3fffdc2f2e8ca06dd6afdf34189b76a9dd"}, +] + +[package.dependencies] +typing-extensions = {version = ">=4.1.0", markers = "python_version < \"3.11\""} + [[package]] name = "mypy" version = "1.14.1" @@ -3298,6 +4589,7 @@ description = "Optional static typing for Python" optional = false python-versions = ">=3.8" groups = ["dev"] +markers = "python_full_version == \"3.8.*\" or platform_python_implementation == \"PyPy\" and python_version < \"3.10\"" files = [ {file = "mypy-1.14.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:52686e37cf13d559f668aa398dd7ddf1f92c5d613e4f8cb262be2fb4fedb0fcb"}, {file = "mypy-1.14.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1fb545ca340537d4b45d3eecdb3def05e913299ca72c290326be19b3804b39c0"}, @@ -3351,6 +4643,68 @@ install-types = ["pip"] mypyc = ["setuptools (>=50)"] reports = ["lxml"] +[[package]] +name = "mypy" +version = "1.18.2" +description = "Optional static typing for Python" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +markers = "python_version >= \"3.9\" and platform_python_implementation != \"PyPy\" or python_version >= \"3.10\"" +files = [ + {file = "mypy-1.18.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c1eab0cf6294dafe397c261a75f96dc2c31bffe3b944faa24db5def4e2b0f77c"}, + {file = "mypy-1.18.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:7a780ca61fc239e4865968ebc5240bb3bf610ef59ac398de9a7421b54e4a207e"}, + {file = "mypy-1.18.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:448acd386266989ef11662ce3c8011fd2a7b632e0ec7d61a98edd8e27472225b"}, + {file = "mypy-1.18.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f9e171c465ad3901dc652643ee4bffa8e9fef4d7d0eece23b428908c77a76a66"}, + {file = "mypy-1.18.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:592ec214750bc00741af1f80cbf96b5013d81486b7bb24cb052382c19e40b428"}, + {file = "mypy-1.18.2-cp310-cp310-win_amd64.whl", hash = "sha256:7fb95f97199ea11769ebe3638c29b550b5221e997c63b14ef93d2e971606ebed"}, + {file = "mypy-1.18.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:807d9315ab9d464125aa9fcf6d84fde6e1dc67da0b6f80e7405506b8ac72bc7f"}, + {file = "mypy-1.18.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:776bb00de1778caf4db739c6e83919c1d85a448f71979b6a0edd774ea8399341"}, + {file = "mypy-1.18.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1379451880512ffce14505493bd9fe469e0697543717298242574882cf8cdb8d"}, + {file = "mypy-1.18.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1331eb7fd110d60c24999893320967594ff84c38ac6d19e0a76c5fd809a84c86"}, + {file = "mypy-1.18.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3ca30b50a51e7ba93b00422e486cbb124f1c56a535e20eff7b2d6ab72b3b2e37"}, + {file = "mypy-1.18.2-cp311-cp311-win_amd64.whl", hash = "sha256:664dc726e67fa54e14536f6e1224bcfce1d9e5ac02426d2326e2bb4e081d1ce8"}, + {file = "mypy-1.18.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:33eca32dd124b29400c31d7cf784e795b050ace0e1f91b8dc035672725617e34"}, + {file = "mypy-1.18.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a3c47adf30d65e89b2dcd2fa32f3aeb5e94ca970d2c15fcb25e297871c8e4764"}, + {file = "mypy-1.18.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5d6c838e831a062f5f29d11c9057c6009f60cb294fea33a98422688181fe2893"}, + {file = "mypy-1.18.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01199871b6110a2ce984bde85acd481232d17413868c9807e95c1b0739a58914"}, + {file = "mypy-1.18.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a2afc0fa0b0e91b4599ddfe0f91e2c26c2b5a5ab263737e998d6817874c5f7c8"}, + {file = "mypy-1.18.2-cp312-cp312-win_amd64.whl", hash = "sha256:d8068d0afe682c7c4897c0f7ce84ea77f6de953262b12d07038f4d296d547074"}, + {file = "mypy-1.18.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:07b8b0f580ca6d289e69209ec9d3911b4a26e5abfde32228a288eb79df129fcc"}, + {file = "mypy-1.18.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ed4482847168439651d3feee5833ccedbf6657e964572706a2adb1f7fa4dfe2e"}, + {file = "mypy-1.18.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c3ad2afadd1e9fea5cf99a45a822346971ede8685cc581ed9cd4d42eaf940986"}, + {file = "mypy-1.18.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a431a6f1ef14cf8c144c6b14793a23ec4eae3db28277c358136e79d7d062f62d"}, + {file = "mypy-1.18.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7ab28cc197f1dd77a67e1c6f35cd1f8e8b73ed2217e4fc005f9e6a504e46e7ba"}, + {file = "mypy-1.18.2-cp313-cp313-win_amd64.whl", hash = "sha256:0e2785a84b34a72ba55fb5daf079a1003a34c05b22238da94fcae2bbe46f3544"}, + {file = "mypy-1.18.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:62f0e1e988ad41c2a110edde6c398383a889d95b36b3e60bcf155f5164c4fdce"}, + {file = "mypy-1.18.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:8795a039bab805ff0c1dfdb8cd3344642c2b99b8e439d057aba30850b8d3423d"}, + {file = "mypy-1.18.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6ca1e64b24a700ab5ce10133f7ccd956a04715463d30498e64ea8715236f9c9c"}, + {file = "mypy-1.18.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d924eef3795cc89fecf6bedc6ed32b33ac13e8321344f6ddbf8ee89f706c05cb"}, + {file = "mypy-1.18.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:20c02215a080e3a2be3aa50506c67242df1c151eaba0dcbc1e4e557922a26075"}, + {file = "mypy-1.18.2-cp314-cp314-win_amd64.whl", hash = "sha256:749b5f83198f1ca64345603118a6f01a4e99ad4bf9d103ddc5a3200cc4614adf"}, + {file = "mypy-1.18.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:25a9c8fb67b00599f839cf472713f54249a62efd53a54b565eb61956a7e3296b"}, + {file = "mypy-1.18.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c2b9c7e284ee20e7598d6f42e13ca40b4928e6957ed6813d1ab6348aa3f47133"}, + {file = "mypy-1.18.2-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d6985ed057513e344e43a26cc1cd815c7a94602fb6a3130a34798625bc2f07b6"}, + {file = "mypy-1.18.2-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:22f27105f1525ec024b5c630c0b9f36d5c1cc4d447d61fe51ff4bd60633f47ac"}, + {file = "mypy-1.18.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:030c52d0ea8144e721e49b1f68391e39553d7451f0c3f8a7565b59e19fcb608b"}, + {file = "mypy-1.18.2-cp39-cp39-win_amd64.whl", hash = "sha256:aa5e07ac1a60a253445797e42b8b2963c9675563a94f11291ab40718b016a7a0"}, + {file = "mypy-1.18.2-py3-none-any.whl", hash = "sha256:22a1748707dd62b58d2ae53562ffc4d7f8bcc727e8ac7cbc69c053ddc874d47e"}, + {file = "mypy-1.18.2.tar.gz", hash = "sha256:06a398102a5f203d7477b2923dda3634c36727fa5c237d8f859ef90c42a9924b"}, +] + +[package.dependencies] +mypy_extensions = ">=1.0.0" +pathspec = ">=0.9.0" +tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} +typing_extensions = ">=4.6.0" + +[package.extras] +dmypy = ["psutil (>=4.0)"] +faster-cache = ["orjson"] +install-types = ["pip"] +mypyc = ["setuptools (>=50)"] +reports = ["lxml"] + [[package]] name = "mypy-extensions" version = "1.1.0" @@ -3362,6 +4716,7 @@ files = [ {file = "mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505"}, {file = "mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558"}, ] +markers = {main = "python_version >= \"3.13\""} [[package]] name = "nodeenv" @@ -3429,7 +4784,7 @@ description = "Sphinx extension to support docstrings in Numpy format" optional = true python-versions = ">=3.8" groups = ["main"] -markers = "extra == \"utils\"" +markers = "(python_full_version == \"3.8.*\" or platform_python_implementation == \"PyPy\") and extra == \"utils\" and python_version < \"3.10\"" files = [ {file = "numpydoc-1.7.0-py3-none-any.whl", hash = "sha256:5a56419d931310d79a06cfc2a126d1558700feeb9b4f3d8dcae1a8134be829c9"}, {file = "numpydoc-1.7.0.tar.gz", hash = "sha256:866e5ae5b6509dcf873fc6381120f5c31acf13b135636c1a81d68c166a95f921"}, @@ -3445,6 +4800,23 @@ developer = ["pre-commit (>=3.3)", "tomli ; python_version < \"3.11\""] doc = ["matplotlib (>=3.5)", "numpy (>=1.22)", "pydata-sphinx-theme (>=0.13.3)", "sphinx (>=7)"] test = ["matplotlib", "pytest", "pytest-cov"] +[[package]] +name = "numpydoc" +version = "1.9.0" +description = "Sphinx extension to support docstrings in Numpy format" +optional = true +python-versions = ">=3.9" +groups = ["main"] +markers = "python_version >= \"3.9\" and (python_version >= \"3.10\" or platform_python_implementation != \"PyPy\") and extra == \"utils\"" +files = [ + {file = "numpydoc-1.9.0-py3-none-any.whl", hash = "sha256:8a2983b2d62bfd0a8c470c7caa25e7e0c3d163875cdec12a8a1034020a9d1135"}, + {file = "numpydoc-1.9.0.tar.gz", hash = "sha256:5fec64908fe041acc4b3afc2a32c49aab1540cf581876f5563d68bb129e27c5b"}, +] + +[package.dependencies] +sphinx = ">=6" +tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} + [[package]] name = "oauthlib" version = "3.3.1" @@ -3465,14 +4837,14 @@ signedtoken = ["cryptography (>=3.0.0)", "pyjwt (>=2.0.0,<3)"] [[package]] name = "openai" -version = "1.108.0" +version = "1.109.1" description = "The official Python library for the openai API" optional = false python-versions = ">=3.8" groups = ["main"] files = [ - {file = "openai-1.108.0-py3-none-any.whl", hash = "sha256:31f2e58230e2703f13ddbb50c285f39dacf7fca64ab19882fd8a7a0b2bccd781"}, - {file = "openai-1.108.0.tar.gz", hash = "sha256:e859c64e4202d7f5956f19280eee92bb281f211c41cdd5be9e63bf51a024ff72"}, + {file = "openai-1.109.1-py3-none-any.whl", hash = "sha256:6bcaf57086cf59159b8e27447e4e7dd019db5d29a438072fbd49c290c7e65315"}, + {file = "openai-1.109.1.tar.gz", hash = "sha256:d173ed8dbca665892a6db099b4a2dfac624f94d20a93f46eb0b56aae940ed869"}, ] [package.dependencies] @@ -3637,7 +5009,7 @@ description = "Fast, correct Python JSON library supporting dataclasses, datetim optional = true python-versions = ">=3.8" groups = ["main"] -markers = "extra == \"proxy\"" +markers = "(python_full_version == \"3.8.*\" or platform_python_implementation == \"PyPy\") and extra == \"proxy\" and python_version < \"3.10\"" files = [ {file = "orjson-3.10.15-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:552c883d03ad185f720d0c09583ebde257e41b9521b74ff40e08b7dec4559c04"}, {file = "orjson-3.10.15-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:616e3e8d438d02e4854f70bfdc03a6bcdb697358dbaa6bcd19cbe24d24ece1f8"}, @@ -3720,6 +5092,100 @@ files = [ {file = "orjson-3.10.15.tar.gz", hash = "sha256:05ca7fe452a2e9d8d9d706a2984c95b9c2ebc5db417ce0b7a49b91d50642a23e"}, ] +[[package]] +name = "orjson" +version = "3.11.3" +description = "Fast, correct Python JSON library supporting dataclasses, datetimes, and numpy" +optional = true +python-versions = ">=3.9" +groups = ["main"] +markers = "python_version >= \"3.9\" and (python_version >= \"3.10\" or platform_python_implementation != \"PyPy\") and extra == \"proxy\"" +files = [ + {file = "orjson-3.11.3-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:29cb1f1b008d936803e2da3d7cba726fc47232c45df531b29edf0b232dd737e7"}, + {file = "orjson-3.11.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:97dceed87ed9139884a55db8722428e27bd8452817fbf1869c58b49fecab1120"}, + {file = "orjson-3.11.3-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:58533f9e8266cb0ac298e259ed7b4d42ed3fa0b78ce76860626164de49e0d467"}, + {file = "orjson-3.11.3-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0c212cfdd90512fe722fa9bd620de4d46cda691415be86b2e02243242ae81873"}, + {file = "orjson-3.11.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5ff835b5d3e67d9207343effb03760c00335f8b5285bfceefd4dc967b0e48f6a"}, + {file = "orjson-3.11.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f5aa4682912a450c2db89cbd92d356fef47e115dffba07992555542f344d301b"}, + {file = "orjson-3.11.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d7d18dd34ea2e860553a579df02041845dee0af8985dff7f8661306f95504ddf"}, + {file = "orjson-3.11.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d8b11701bc43be92ea42bd454910437b355dfb63696c06fe953ffb40b5f763b4"}, + {file = "orjson-3.11.3-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:90368277087d4af32d38bd55f9da2ff466d25325bf6167c8f382d8ee40cb2bbc"}, + {file = "orjson-3.11.3-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:fd7ff459fb393358d3a155d25b275c60b07a2c83dcd7ea962b1923f5a1134569"}, + {file = "orjson-3.11.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f8d902867b699bcd09c176a280b1acdab57f924489033e53d0afe79817da37e6"}, + {file = "orjson-3.11.3-cp310-cp310-win32.whl", hash = "sha256:bb93562146120bb51e6b154962d3dadc678ed0fce96513fa6bc06599bb6f6edc"}, + {file = "orjson-3.11.3-cp310-cp310-win_amd64.whl", hash = "sha256:976c6f1975032cc327161c65d4194c549f2589d88b105a5e3499429a54479770"}, + {file = "orjson-3.11.3-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:9d2ae0cc6aeb669633e0124531f342a17d8e97ea999e42f12a5ad4adaa304c5f"}, + {file = "orjson-3.11.3-cp311-cp311-macosx_15_0_arm64.whl", hash = "sha256:ba21dbb2493e9c653eaffdc38819b004b7b1b246fb77bfc93dc016fe664eac91"}, + {file = "orjson-3.11.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:00f1a271e56d511d1569937c0447d7dce5a99a33ea0dec76673706360a051904"}, + {file = "orjson-3.11.3-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b67e71e47caa6680d1b6f075a396d04fa6ca8ca09aafb428731da9b3ea32a5a6"}, + {file = "orjson-3.11.3-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d7d012ebddffcce8c85734a6d9e5f08180cd3857c5f5a3ac70185b43775d043d"}, + {file = "orjson-3.11.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dd759f75d6b8d1b62012b7f5ef9461d03c804f94d539a5515b454ba3a6588038"}, + {file = "orjson-3.11.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6890ace0809627b0dff19cfad92d69d0fa3f089d3e359a2a532507bb6ba34efb"}, + {file = "orjson-3.11.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f9d4a5e041ae435b815e568537755773d05dac031fee6a57b4ba70897a44d9d2"}, + {file = "orjson-3.11.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2d68bf97a771836687107abfca089743885fb664b90138d8761cce61d5625d55"}, + {file = "orjson-3.11.3-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:bfc27516ec46f4520b18ef645864cee168d2a027dbf32c5537cb1f3e3c22dac1"}, + {file = "orjson-3.11.3-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:f66b001332a017d7945e177e282a40b6997056394e3ed7ddb41fb1813b83e824"}, + {file = "orjson-3.11.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:212e67806525d2561efbfe9e799633b17eb668b8964abed6b5319b2f1cfbae1f"}, + {file = "orjson-3.11.3-cp311-cp311-win32.whl", hash = "sha256:6e8e0c3b85575a32f2ffa59de455f85ce002b8bdc0662d6b9c2ed6d80ab5d204"}, + {file = "orjson-3.11.3-cp311-cp311-win_amd64.whl", hash = "sha256:6be2f1b5d3dc99a5ce5ce162fc741c22ba9f3443d3dd586e6a1211b7bc87bc7b"}, + {file = "orjson-3.11.3-cp311-cp311-win_arm64.whl", hash = "sha256:fafb1a99d740523d964b15c8db4eabbfc86ff29f84898262bf6e3e4c9e97e43e"}, + {file = "orjson-3.11.3-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:8c752089db84333e36d754c4baf19c0e1437012242048439c7e80eb0e6426e3b"}, + {file = "orjson-3.11.3-cp312-cp312-macosx_15_0_arm64.whl", hash = "sha256:9b8761b6cf04a856eb544acdd82fc594b978f12ac3602d6374a7edb9d86fd2c2"}, + {file = "orjson-3.11.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b13974dc8ac6ba22feaa867fc19135a3e01a134b4f7c9c28162fed4d615008a"}, + {file = "orjson-3.11.3-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f83abab5bacb76d9c821fd5c07728ff224ed0e52d7a71b7b3de822f3df04e15c"}, + {file = "orjson-3.11.3-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e6fbaf48a744b94091a56c62897b27c31ee2da93d826aa5b207131a1e13d4064"}, + {file = "orjson-3.11.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bc779b4f4bba2847d0d2940081a7b6f7b5877e05408ffbb74fa1faf4a136c424"}, + {file = "orjson-3.11.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd4b909ce4c50faa2192da6bb684d9848d4510b736b0611b6ab4020ea6fd2d23"}, + {file = "orjson-3.11.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:524b765ad888dc5518bbce12c77c2e83dee1ed6b0992c1790cc5fb49bb4b6667"}, + {file = "orjson-3.11.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:84fd82870b97ae3cdcea9d8746e592b6d40e1e4d4527835fc520c588d2ded04f"}, + {file = "orjson-3.11.3-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:fbecb9709111be913ae6879b07bafd4b0785b44c1eb5cac8ac76da048b3885a1"}, + {file = "orjson-3.11.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:9dba358d55aee552bd868de348f4736ca5a4086d9a62e2bfbbeeb5629fe8b0cc"}, + {file = "orjson-3.11.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:eabcf2e84f1d7105f84580e03012270c7e97ecb1fb1618bda395061b2a84a049"}, + {file = "orjson-3.11.3-cp312-cp312-win32.whl", hash = "sha256:3782d2c60b8116772aea8d9b7905221437fdf53e7277282e8d8b07c220f96cca"}, + {file = "orjson-3.11.3-cp312-cp312-win_amd64.whl", hash = "sha256:79b44319268af2eaa3e315b92298de9a0067ade6e6003ddaef72f8e0bedb94f1"}, + {file = "orjson-3.11.3-cp312-cp312-win_arm64.whl", hash = "sha256:0e92a4e83341ef79d835ca21b8bd13e27c859e4e9e4d7b63defc6e58462a3710"}, + {file = "orjson-3.11.3-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:af40c6612fd2a4b00de648aa26d18186cd1322330bd3a3cc52f87c699e995810"}, + {file = "orjson-3.11.3-cp313-cp313-macosx_15_0_arm64.whl", hash = "sha256:9f1587f26c235894c09e8b5b7636a38091a9e6e7fe4531937534749c04face43"}, + {file = "orjson-3.11.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:61dcdad16da5bb486d7227a37a2e789c429397793a6955227cedbd7252eb5a27"}, + {file = "orjson-3.11.3-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:11c6d71478e2cbea0a709e8a06365fa63da81da6498a53e4c4f065881d21ae8f"}, + {file = "orjson-3.11.3-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ff94112e0098470b665cb0ed06efb187154b63649403b8d5e9aedeb482b4548c"}, + {file = "orjson-3.11.3-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ae8b756575aaa2a855a75192f356bbda11a89169830e1439cfb1a3e1a6dde7be"}, + {file = "orjson-3.11.3-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c9416cc19a349c167ef76135b2fe40d03cea93680428efee8771f3e9fb66079d"}, + {file = "orjson-3.11.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b822caf5b9752bc6f246eb08124c3d12bf2175b66ab74bac2ef3bbf9221ce1b2"}, + {file = "orjson-3.11.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:414f71e3bdd5573893bf5ecdf35c32b213ed20aa15536fe2f588f946c318824f"}, + {file = "orjson-3.11.3-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:828e3149ad8815dc14468f36ab2a4b819237c155ee1370341b91ea4c8672d2ee"}, + {file = "orjson-3.11.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ac9e05f25627ffc714c21f8dfe3a579445a5c392a9c8ae7ba1d0e9fb5333f56e"}, + {file = "orjson-3.11.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e44fbe4000bd321d9f3b648ae46e0196d21577cf66ae684a96ff90b1f7c93633"}, + {file = "orjson-3.11.3-cp313-cp313-win32.whl", hash = "sha256:2039b7847ba3eec1f5886e75e6763a16e18c68a63efc4b029ddf994821e2e66b"}, + {file = "orjson-3.11.3-cp313-cp313-win_amd64.whl", hash = "sha256:29be5ac4164aa8bdcba5fa0700a3c9c316b411d8ed9d39ef8a882541bd452fae"}, + {file = "orjson-3.11.3-cp313-cp313-win_arm64.whl", hash = "sha256:18bd1435cb1f2857ceb59cfb7de6f92593ef7b831ccd1b9bfb28ca530e539dce"}, + {file = "orjson-3.11.3-cp314-cp314-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:cf4b81227ec86935568c7edd78352a92e97af8da7bd70bdfdaa0d2e0011a1ab4"}, + {file = "orjson-3.11.3-cp314-cp314-macosx_15_0_arm64.whl", hash = "sha256:bc8bc85b81b6ac9fc4dae393a8c159b817f4c2c9dee5d12b773bddb3b95fc07e"}, + {file = "orjson-3.11.3-cp314-cp314-manylinux_2_34_aarch64.whl", hash = "sha256:88dcfc514cfd1b0de038443c7b3e6a9797ffb1b3674ef1fd14f701a13397f82d"}, + {file = "orjson-3.11.3-cp314-cp314-manylinux_2_34_x86_64.whl", hash = "sha256:d61cd543d69715d5fc0a690c7c6f8dcc307bc23abef9738957981885f5f38229"}, + {file = "orjson-3.11.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:2b7b153ed90ababadbef5c3eb39549f9476890d339cf47af563aea7e07db2451"}, + {file = "orjson-3.11.3-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:7909ae2460f5f494fecbcd10613beafe40381fd0316e35d6acb5f3a05bfda167"}, + {file = "orjson-3.11.3-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:2030c01cbf77bc67bee7eef1e7e31ecf28649353987775e3583062c752da0077"}, + {file = "orjson-3.11.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:a0169ebd1cbd94b26c7a7ad282cf5c2744fce054133f959e02eb5265deae1872"}, + {file = "orjson-3.11.3-cp314-cp314-win32.whl", hash = "sha256:0c6d7328c200c349e3a4c6d8c83e0a5ad029bdc2d417f234152bf34842d0fc8d"}, + {file = "orjson-3.11.3-cp314-cp314-win_amd64.whl", hash = "sha256:317bbe2c069bbc757b1a2e4105b64aacd3bc78279b66a6b9e51e846e4809f804"}, + {file = "orjson-3.11.3-cp314-cp314-win_arm64.whl", hash = "sha256:e8f6a7a27d7b7bec81bd5924163e9af03d49bbb63013f107b48eb5d16db711bc"}, + {file = "orjson-3.11.3-cp39-cp39-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:56afaf1e9b02302ba636151cfc49929c1bb66b98794291afd0e5f20fecaf757c"}, + {file = "orjson-3.11.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:913f629adef31d2d350d41c051ce7e33cf0fd06a5d1cb28d49b1899b23b903aa"}, + {file = "orjson-3.11.3-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e0a23b41f8f98b4e61150a03f83e4f0d566880fe53519d445a962929a4d21045"}, + {file = "orjson-3.11.3-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3d721fee37380a44f9d9ce6c701b3960239f4fb3d5ceea7f31cbd43882edaa2f"}, + {file = "orjson-3.11.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:73b92a5b69f31b1a58c0c7e31080aeaec49c6e01b9522e71ff38d08f15aa56de"}, + {file = "orjson-3.11.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d2489b241c19582b3f1430cc5d732caefc1aaf378d97e7fb95b9e56bed11725f"}, + {file = "orjson-3.11.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c5189a5dab8b0312eadaf9d58d3049b6a52c454256493a557405e77a3d67ab7f"}, + {file = "orjson-3.11.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:9d8787bdfbb65a85ea76d0e96a3b1bed7bf0fbcb16d40408dc1172ad784a49d2"}, + {file = "orjson-3.11.3-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:8e531abd745f51f8035e207e75e049553a86823d189a51809c078412cefb399a"}, + {file = "orjson-3.11.3-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:8ab962931015f170b97a3dd7bd933399c1bae8ed8ad0fb2a7151a5654b6941c7"}, + {file = "orjson-3.11.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:124d5ba71fee9c9902c4a7baa9425e663f7f0aecf73d31d54fe3dd357d62c1a7"}, + {file = "orjson-3.11.3-cp39-cp39-win32.whl", hash = "sha256:22724d80ee5a815a44fc76274bb7ba2e7464f5564aacb6ecddaa9970a83e3225"}, + {file = "orjson-3.11.3-cp39-cp39-win_amd64.whl", hash = "sha256:215c595c792a87d4407cb72dd5e0f6ee8e694ceeb7f9102b533c5a9bf2a916bb"}, + {file = "orjson-3.11.3.tar.gz", hash = "sha256:1c0603b1d2ffcd43a411d64797a19556ef76958aef1c182f22dc30860152a98a"}, +] + [[package]] name = "packaging" version = "24.2" @@ -3831,6 +5297,7 @@ files = [ {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"}, {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, ] +markers = {main = "python_version >= \"3.13\""} [[package]] name = "pillow" @@ -3965,7 +5432,7 @@ description = "Resolve a name to an object." optional = false python-versions = ">=3.6" groups = ["main"] -markers = "python_version < \"3.9\"" +markers = "python_full_version == \"3.8.*\" or platform_python_implementation == \"PyPy\" and python_version < \"3.9\"" files = [ {file = "pkgutil_resolve_name-1.3.10-py3-none-any.whl", hash = "sha256:ca27cc078d25c5ad71a9de0a7a330146c4e014c2462d9af19c6b828280649c5e"}, {file = "pkgutil_resolve_name-1.3.10.tar.gz", hash = "sha256:357d6c9e6a755653cfd78893817c0853af365dd51ec97f3d358a819373bbd174"}, @@ -3977,7 +5444,8 @@ version = "4.3.6" description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." optional = false python-versions = ">=3.8" -groups = ["main", "dev"] +groups = ["dev"] +markers = "python_full_version == \"3.8.*\" or platform_python_implementation == \"PyPy\" and python_version < \"3.10\"" files = [ {file = "platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb"}, {file = "platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907"}, @@ -3988,6 +5456,24 @@ docs = ["furo (>=2024.8.6)", "proselint (>=0.14)", "sphinx (>=8.0.2)", "sphinx-a test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=8.3.2)", "pytest-cov (>=5)", "pytest-mock (>=3.14)"] type = ["mypy (>=1.11.2)"] +[[package]] +name = "platformdirs" +version = "4.4.0" +description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." +optional = false +python-versions = ">=3.9" +groups = ["main", "dev"] +files = [ + {file = "platformdirs-4.4.0-py3-none-any.whl", hash = "sha256:abd01743f24e5287cd7a5db3752faf1a2d65353f38ec26d98e25a6db65958c85"}, + {file = "platformdirs-4.4.0.tar.gz", hash = "sha256:ca753cf4d81dc309bc67b0ea38fd15dc97bc30ce419a7f58d13eb3bf14c4febf"}, +] +markers = {main = "python_version >= \"3.13\"", dev = "python_version >= \"3.9\" and platform_python_implementation != \"PyPy\" or python_version >= \"3.10\""} + +[package.extras] +docs = ["furo (>=2024.8.6)", "proselint (>=0.14)", "sphinx (>=8.1.3)", "sphinx-autodoc-typehints (>=3)"] +test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=8.3.4)", "pytest-cov (>=6)", "pytest-mock (>=3.14)"] +type = ["mypy (>=1.14.1)"] + [[package]] name = "pluggy" version = "1.5.0" @@ -3995,6 +5481,7 @@ description = "plugin and hook calling mechanisms for python" optional = false python-versions = ">=3.8" groups = ["dev"] +markers = "python_full_version == \"3.8.*\" or platform_python_implementation == \"PyPy\" and python_version < \"3.10\"" files = [ {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, @@ -4004,6 +5491,23 @@ files = [ dev = ["pre-commit", "tox"] testing = ["pytest", "pytest-benchmark"] +[[package]] +name = "pluggy" +version = "1.6.0" +description = "plugin and hook calling mechanisms for python" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +markers = "python_version >= \"3.9\" and platform_python_implementation != \"PyPy\" or python_version >= \"3.10\"" +files = [ + {file = "pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746"}, + {file = "pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3"}, +] + +[package.extras] +dev = ["pre-commit", "tox"] +testing = ["coverage", "pytest", "pytest-benchmark"] + [[package]] name = "polars" version = "1.33.1" @@ -4124,6 +5628,7 @@ description = "Accelerated property cache" optional = false python-versions = ">=3.8" groups = ["main"] +markers = "python_full_version == \"3.8.*\" or platform_python_implementation == \"PyPy\" and python_version < \"3.10\"" files = [ {file = "propcache-0.2.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:c5869b8fd70b81835a6f187c5fdbe67917a04d7e52b6e7cc4e5fe39d55c39d58"}, {file = "propcache-0.2.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:952e0d9d07609d9c5be361f33b0d6d650cd2bae393aabb11d9b719364521984b"}, @@ -4225,6 +5730,115 @@ files = [ {file = "propcache-0.2.0.tar.gz", hash = "sha256:df81779732feb9d01e5d513fad0122efb3d53bbc75f61b2a4f29a020bc985e70"}, ] +[[package]] +name = "propcache" +version = "0.3.2" +description = "Accelerated property cache" +optional = false +python-versions = ">=3.9" +groups = ["main"] +markers = "python_version >= \"3.9\" and platform_python_implementation != \"PyPy\" or python_version >= \"3.10\"" +files = [ + {file = "propcache-0.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:22d9962a358aedbb7a2e36187ff273adeaab9743373a272976d2e348d08c7770"}, + {file = "propcache-0.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0d0fda578d1dc3f77b6b5a5dce3b9ad69a8250a891760a548df850a5e8da87f3"}, + {file = "propcache-0.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:3def3da3ac3ce41562d85db655d18ebac740cb3fa4367f11a52b3da9d03a5cc3"}, + {file = "propcache-0.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9bec58347a5a6cebf239daba9bda37dffec5b8d2ce004d9fe4edef3d2815137e"}, + {file = "propcache-0.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:55ffda449a507e9fbd4aca1a7d9aa6753b07d6166140e5a18d2ac9bc49eac220"}, + {file = "propcache-0.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:64a67fb39229a8a8491dd42f864e5e263155e729c2e7ff723d6e25f596b1e8cb"}, + {file = "propcache-0.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9da1cf97b92b51253d5b68cf5a2b9e0dafca095e36b7f2da335e27dc6172a614"}, + {file = "propcache-0.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5f559e127134b07425134b4065be45b166183fdcb433cb6c24c8e4149056ad50"}, + {file = "propcache-0.3.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:aff2e4e06435d61f11a428360a932138d0ec288b0a31dd9bd78d200bd4a2b339"}, + {file = "propcache-0.3.2-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:4927842833830942a5d0a56e6f4839bc484785b8e1ce8d287359794818633ba0"}, + {file = "propcache-0.3.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:6107ddd08b02654a30fb8ad7a132021759d750a82578b94cd55ee2772b6ebea2"}, + {file = "propcache-0.3.2-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:70bd8b9cd6b519e12859c99f3fc9a93f375ebd22a50296c3a295028bea73b9e7"}, + {file = "propcache-0.3.2-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:2183111651d710d3097338dd1893fcf09c9f54e27ff1a8795495a16a469cc90b"}, + {file = "propcache-0.3.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:fb075ad271405dcad8e2a7ffc9a750a3bf70e533bd86e89f0603e607b93aa64c"}, + {file = "propcache-0.3.2-cp310-cp310-win32.whl", hash = "sha256:404d70768080d3d3bdb41d0771037da19d8340d50b08e104ca0e7f9ce55fce70"}, + {file = "propcache-0.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:7435d766f978b4ede777002e6b3b6641dd229cd1da8d3d3106a45770365f9ad9"}, + {file = "propcache-0.3.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0b8d2f607bd8f80ddc04088bc2a037fdd17884a6fcadc47a96e334d72f3717be"}, + {file = "propcache-0.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:06766d8f34733416e2e34f46fea488ad5d60726bb9481d3cddf89a6fa2d9603f"}, + {file = "propcache-0.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a2dc1f4a1df4fecf4e6f68013575ff4af84ef6f478fe5344317a65d38a8e6dc9"}, + {file = "propcache-0.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:be29c4f4810c5789cf10ddf6af80b041c724e629fa51e308a7a0fb19ed1ef7bf"}, + {file = "propcache-0.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:59d61f6970ecbd8ff2e9360304d5c8876a6abd4530cb752c06586849ac8a9dc9"}, + {file = "propcache-0.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:62180e0b8dbb6b004baec00a7983e4cc52f5ada9cd11f48c3528d8cfa7b96a66"}, + {file = "propcache-0.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c144ca294a204c470f18cf4c9d78887810d04a3e2fbb30eea903575a779159df"}, + {file = "propcache-0.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c5c2a784234c28854878d68978265617aa6dc0780e53d44b4d67f3651a17a9a2"}, + {file = "propcache-0.3.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:5745bc7acdafa978ca1642891b82c19238eadc78ba2aaa293c6863b304e552d7"}, + {file = "propcache-0.3.2-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:c0075bf773d66fa8c9d41f66cc132ecc75e5bb9dd7cce3cfd14adc5ca184cb95"}, + {file = "propcache-0.3.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5f57aa0847730daceff0497f417c9de353c575d8da3579162cc74ac294c5369e"}, + {file = "propcache-0.3.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:eef914c014bf72d18efb55619447e0aecd5fb7c2e3fa7441e2e5d6099bddff7e"}, + {file = "propcache-0.3.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:2a4092e8549031e82facf3decdbc0883755d5bbcc62d3aea9d9e185549936dcf"}, + {file = "propcache-0.3.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:85871b050f174bc0bfb437efbdb68aaf860611953ed12418e4361bc9c392749e"}, + {file = "propcache-0.3.2-cp311-cp311-win32.whl", hash = "sha256:36c8d9b673ec57900c3554264e630d45980fd302458e4ac801802a7fd2ef7897"}, + {file = "propcache-0.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:e53af8cb6a781b02d2ea079b5b853ba9430fcbe18a8e3ce647d5982a3ff69f39"}, + {file = "propcache-0.3.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:8de106b6c84506b31c27168582cd3cb3000a6412c16df14a8628e5871ff83c10"}, + {file = "propcache-0.3.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:28710b0d3975117239c76600ea351934ac7b5ff56e60953474342608dbbb6154"}, + {file = "propcache-0.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce26862344bdf836650ed2487c3d724b00fbfec4233a1013f597b78c1cb73615"}, + {file = "propcache-0.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bca54bd347a253af2cf4544bbec232ab982f4868de0dd684246b67a51bc6b1db"}, + {file = "propcache-0.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:55780d5e9a2ddc59711d727226bb1ba83a22dd32f64ee15594b9392b1f544eb1"}, + {file = "propcache-0.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:035e631be25d6975ed87ab23153db6a73426a48db688070d925aa27e996fe93c"}, + {file = "propcache-0.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ee6f22b6eaa39297c751d0e80c0d3a454f112f5c6481214fcf4c092074cecd67"}, + {file = "propcache-0.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7ca3aee1aa955438c4dba34fc20a9f390e4c79967257d830f137bd5a8a32ed3b"}, + {file = "propcache-0.3.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7a4f30862869fa2b68380d677cc1c5fcf1e0f2b9ea0cf665812895c75d0ca3b8"}, + {file = "propcache-0.3.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:b77ec3c257d7816d9f3700013639db7491a434644c906a2578a11daf13176251"}, + {file = "propcache-0.3.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:cab90ac9d3f14b2d5050928483d3d3b8fb6b4018893fc75710e6aa361ecb2474"}, + {file = "propcache-0.3.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:0b504d29f3c47cf6b9e936c1852246c83d450e8e063d50562115a6be6d3a2535"}, + {file = "propcache-0.3.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:ce2ac2675a6aa41ddb2a0c9cbff53780a617ac3d43e620f8fd77ba1c84dcfc06"}, + {file = "propcache-0.3.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:62b4239611205294cc433845b914131b2a1f03500ff3c1ed093ed216b82621e1"}, + {file = "propcache-0.3.2-cp312-cp312-win32.whl", hash = "sha256:df4a81b9b53449ebc90cc4deefb052c1dd934ba85012aa912c7ea7b7e38b60c1"}, + {file = "propcache-0.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:7046e79b989d7fe457bb755844019e10f693752d169076138abf17f31380800c"}, + {file = "propcache-0.3.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ca592ed634a73ca002967458187109265e980422116c0a107cf93d81f95af945"}, + {file = "propcache-0.3.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9ecb0aad4020e275652ba3975740f241bd12a61f1a784df044cf7477a02bc252"}, + {file = "propcache-0.3.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7f08f1cc28bd2eade7a8a3d2954ccc673bb02062e3e7da09bc75d843386b342f"}, + {file = "propcache-0.3.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d1a342c834734edb4be5ecb1e9fb48cb64b1e2320fccbd8c54bf8da8f2a84c33"}, + {file = "propcache-0.3.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8a544caaae1ac73f1fecfae70ded3e93728831affebd017d53449e3ac052ac1e"}, + {file = "propcache-0.3.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:310d11aa44635298397db47a3ebce7db99a4cc4b9bbdfcf6c98a60c8d5261cf1"}, + {file = "propcache-0.3.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4c1396592321ac83157ac03a2023aa6cc4a3cc3cfdecb71090054c09e5a7cce3"}, + {file = "propcache-0.3.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8cabf5b5902272565e78197edb682017d21cf3b550ba0460ee473753f28d23c1"}, + {file = "propcache-0.3.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0a2f2235ac46a7aa25bdeb03a9e7060f6ecbd213b1f9101c43b3090ffb971ef6"}, + {file = "propcache-0.3.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:92b69e12e34869a6970fd2f3da91669899994b47c98f5d430b781c26f1d9f387"}, + {file = "propcache-0.3.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:54e02207c79968ebbdffc169591009f4474dde3b4679e16634d34c9363ff56b4"}, + {file = "propcache-0.3.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4adfb44cb588001f68c5466579d3f1157ca07f7504fc91ec87862e2b8e556b88"}, + {file = "propcache-0.3.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:fd3e6019dc1261cd0291ee8919dd91fbab7b169bb76aeef6c716833a3f65d206"}, + {file = "propcache-0.3.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4c181cad81158d71c41a2bce88edce078458e2dd5ffee7eddd6b05da85079f43"}, + {file = "propcache-0.3.2-cp313-cp313-win32.whl", hash = "sha256:8a08154613f2249519e549de2330cf8e2071c2887309a7b07fb56098f5170a02"}, + {file = "propcache-0.3.2-cp313-cp313-win_amd64.whl", hash = "sha256:e41671f1594fc4ab0a6dec1351864713cb3a279910ae8b58f884a88a0a632c05"}, + {file = "propcache-0.3.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:9a3cf035bbaf035f109987d9d55dc90e4b0e36e04bbbb95af3055ef17194057b"}, + {file = "propcache-0.3.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:156c03d07dc1323d8dacaa221fbe028c5c70d16709cdd63502778e6c3ccca1b0"}, + {file = "propcache-0.3.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:74413c0ba02ba86f55cf60d18daab219f7e531620c15f1e23d95563f505efe7e"}, + {file = "propcache-0.3.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f066b437bb3fa39c58ff97ab2ca351db465157d68ed0440abecb21715eb24b28"}, + {file = "propcache-0.3.2-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f1304b085c83067914721e7e9d9917d41ad87696bf70f0bc7dee450e9c71ad0a"}, + {file = "propcache-0.3.2-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ab50cef01b372763a13333b4e54021bdcb291fc9a8e2ccb9c2df98be51bcde6c"}, + {file = "propcache-0.3.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fad3b2a085ec259ad2c2842666b2a0a49dea8463579c606426128925af1ed725"}, + {file = "propcache-0.3.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:261fa020c1c14deafd54c76b014956e2f86991af198c51139faf41c4d5e83892"}, + {file = "propcache-0.3.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:46d7f8aa79c927e5f987ee3a80205c987717d3659f035c85cf0c3680526bdb44"}, + {file = "propcache-0.3.2-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:6d8f3f0eebf73e3c0ff0e7853f68be638b4043c65a70517bb575eff54edd8dbe"}, + {file = "propcache-0.3.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:03c89c1b14a5452cf15403e291c0ccd7751d5b9736ecb2c5bab977ad6c5bcd81"}, + {file = "propcache-0.3.2-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:0cc17efde71e12bbaad086d679ce575268d70bc123a5a71ea7ad76f70ba30bba"}, + {file = "propcache-0.3.2-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:acdf05d00696bc0447e278bb53cb04ca72354e562cf88ea6f9107df8e7fd9770"}, + {file = "propcache-0.3.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4445542398bd0b5d32df908031cb1b30d43ac848e20470a878b770ec2dcc6330"}, + {file = "propcache-0.3.2-cp313-cp313t-win32.whl", hash = "sha256:f86e5d7cd03afb3a1db8e9f9f6eff15794e79e791350ac48a8c924e6f439f394"}, + {file = "propcache-0.3.2-cp313-cp313t-win_amd64.whl", hash = "sha256:9704bedf6e7cbe3c65eca4379a9b53ee6a83749f047808cbb5044d40d7d72198"}, + {file = "propcache-0.3.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:a7fad897f14d92086d6b03fdd2eb844777b0c4d7ec5e3bac0fbae2ab0602bbe5"}, + {file = "propcache-0.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:1f43837d4ca000243fd7fd6301947d7cb93360d03cd08369969450cc6b2ce3b4"}, + {file = "propcache-0.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:261df2e9474a5949c46e962065d88eb9b96ce0f2bd30e9d3136bcde84befd8f2"}, + {file = "propcache-0.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e514326b79e51f0a177daab1052bc164d9d9e54133797a3a58d24c9c87a3fe6d"}, + {file = "propcache-0.3.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d4a996adb6904f85894570301939afeee65f072b4fd265ed7e569e8d9058e4ec"}, + {file = "propcache-0.3.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:76cace5d6b2a54e55b137669b30f31aa15977eeed390c7cbfb1dafa8dfe9a701"}, + {file = "propcache-0.3.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:31248e44b81d59d6addbb182c4720f90b44e1efdc19f58112a3c3a1615fb47ef"}, + {file = "propcache-0.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:abb7fa19dbf88d3857363e0493b999b8011eea856b846305d8c0512dfdf8fbb1"}, + {file = "propcache-0.3.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:d81ac3ae39d38588ad0549e321e6f773a4e7cc68e7751524a22885d5bbadf886"}, + {file = "propcache-0.3.2-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:cc2782eb0f7a16462285b6f8394bbbd0e1ee5f928034e941ffc444012224171b"}, + {file = "propcache-0.3.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:db429c19a6c7e8a1c320e6a13c99799450f411b02251fb1b75e6217cf4a14fcb"}, + {file = "propcache-0.3.2-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:21d8759141a9e00a681d35a1f160892a36fb6caa715ba0b832f7747da48fb6ea"}, + {file = "propcache-0.3.2-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:2ca6d378f09adb13837614ad2754fa8afaee330254f404299611bce41a8438cb"}, + {file = "propcache-0.3.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:34a624af06c048946709f4278b4176470073deda88d91342665d95f7c6270fbe"}, + {file = "propcache-0.3.2-cp39-cp39-win32.whl", hash = "sha256:4ba3fef1c30f306b1c274ce0b8baaa2c3cdd91f645c48f06394068f37d3837a1"}, + {file = "propcache-0.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:7a2368eed65fc69a7a7a40b27f22e85e7627b74216f0846b04ba5c116e191ec9"}, + {file = "propcache-0.3.2-py3-none-any.whl", hash = "sha256:98f1ec44fb675f5052cccc8e609c46ed23a35a1cfd18545ad4e29002d858a43f"}, + {file = "propcache-0.3.2.tar.gz", hash = "sha256:20d7d62e4e7ef05f221e0db2856b979540686342e7dd9973b815599c7057e168"}, +] + [[package]] name = "proto-plus" version = "1.26.1" @@ -4264,7 +5878,7 @@ files = [ {file = "protobuf-4.25.8-py3-none-any.whl", hash = "sha256:15a0af558aa3b13efef102ae6e4f3efac06f1eea11afb3a57db2901447d9fb59"}, {file = "protobuf-4.25.8.tar.gz", hash = "sha256:6135cf8affe1fc6f76cced2641e4ea8d3e59518d1f24ae41ba97bcad82d397cd"}, ] -markers = {main = "python_version >= \"3.10\" and (extra == \"mlflow\" or extra == \"extra-proxy\") or extra == \"extra-proxy\""} +markers = {main = "(extra == \"mlflow\" or extra == \"extra-proxy\") and python_version >= \"3.10\" or extra == \"extra-proxy\""} [[package]] name = "pyarrow" @@ -4330,7 +5944,7 @@ description = "Pure-Python implementation of ASN.1 types and DER/BER/CER codecs optional = true python-versions = ">=3.8" groups = ["main"] -markers = "python_version >= \"3.10\" and (extra == \"mlflow\" or extra == \"extra-proxy\") or extra == \"extra-proxy\"" +markers = "(extra == \"mlflow\" or extra == \"extra-proxy\") and python_version >= \"3.10\" or extra == \"extra-proxy\"" files = [ {file = "pyasn1-0.6.1-py3-none-any.whl", hash = "sha256:0d632f46f2ba09143da3a8afe9e33fb6f92fa2320ab7e886e2d0f7672af84629"}, {file = "pyasn1-0.6.1.tar.gz", hash = "sha256:6f580d2bdd84365380830acf45550f2511469f673cb4a5ae3857a3170128b034"}, @@ -4343,7 +5957,7 @@ description = "A collection of ASN.1-based protocols modules" optional = true python-versions = ">=3.8" groups = ["main"] -markers = "python_version >= \"3.10\" and (extra == \"mlflow\" or extra == \"extra-proxy\") or extra == \"extra-proxy\"" +markers = "(extra == \"mlflow\" or extra == \"extra-proxy\") and python_version >= \"3.10\" or extra == \"extra-proxy\"" files = [ {file = "pyasn1_modules-0.4.2-py3-none-any.whl", hash = "sha256:29253a9207ce32b64c3ac6600edc75368f98473906e8fd1043bd6b5b1de2c14a"}, {file = "pyasn1_modules-0.4.2.tar.gz", hash = "sha256:677091de870a80aae844b1ca6134f54652fa2c8c5a52aa396440ac3106e941e6"}, @@ -4371,7 +5985,7 @@ description = "C parser in Python" optional = false python-versions = ">=3.8" groups = ["main", "dev", "proxy-dev"] -markers = "platform_python_implementation != \"PyPy\" and (implementation_name != \"PyPy\" or python_version < \"3.10\")" +markers = "platform_python_implementation != \"PyPy\" and (implementation_name != \"PyPy\" or python_full_version == \"3.8.*\")" files = [ {file = "pycparser-2.23-py3-none-any.whl", hash = "sha256:e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934"}, {file = "pycparser-2.23.tar.gz", hash = "sha256:78816d4f24add8f10a06d6f05b4d424ad9e96cfebf68a4ddc99c65c0720d00c2"}, @@ -4384,6 +5998,7 @@ description = "Data validation using Python type hints" optional = false python-versions = ">=3.8" groups = ["main", "dev", "proxy-dev"] +markers = "python_full_version == \"3.8.*\" or platform_python_implementation == \"PyPy\" and python_version < \"3.10\"" files = [ {file = "pydantic-2.10.6-py3-none-any.whl", hash = "sha256:427d664bf0b8a2b34ff5dd0f5a18df00591adcee7198fbd71981054cef37b584"}, {file = "pydantic-2.10.6.tar.gz", hash = "sha256:ca5daa827cce33de7a42be142548b0096bf05a7e7b365aebfa5f8eeec7128236"}, @@ -4399,6 +6014,30 @@ typing-extensions = ">=4.12.2" email = ["email-validator (>=2.0.0)"] timezone = ["tzdata ; python_version >= \"3.9\" and platform_system == \"Windows\""] +[[package]] +name = "pydantic" +version = "2.11.9" +description = "Data validation using Python type hints" +optional = false +python-versions = ">=3.9" +groups = ["main", "dev", "proxy-dev"] +markers = "python_version >= \"3.9\" and platform_python_implementation != \"PyPy\" or python_version >= \"3.10\"" +files = [ + {file = "pydantic-2.11.9-py3-none-any.whl", hash = "sha256:c42dd626f5cfc1c6950ce6205ea58c93efa406da65f479dcb4029d5934857da2"}, + {file = "pydantic-2.11.9.tar.gz", hash = "sha256:6b8ffda597a14812a7975c90b82a8a2e777d9257aba3453f973acd3c032a18e2"}, +] + +[package.dependencies] +annotated-types = ">=0.6.0" +email-validator = {version = ">=2.0.0", optional = true, markers = "extra == \"email\""} +pydantic-core = "2.33.2" +typing-extensions = ">=4.12.2" +typing-inspection = ">=0.4.0" + +[package.extras] +email = ["email-validator (>=2.0.0)"] +timezone = ["tzdata ; python_version >= \"3.9\" and platform_system == \"Windows\""] + [[package]] name = "pydantic-core" version = "2.27.2" @@ -4406,6 +6045,7 @@ description = "Core functionality for Pydantic validation and serialization" optional = false python-versions = ">=3.8" groups = ["main", "dev", "proxy-dev"] +markers = "python_full_version == \"3.8.*\" or platform_python_implementation == \"PyPy\" and python_version < \"3.10\"" files = [ {file = "pydantic_core-2.27.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2d367ca20b2f14095a8f4fa1210f5a7b78b8a20009ecced6b12818f455b1e9fa"}, {file = "pydantic_core-2.27.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:491a2b73db93fab69731eaee494f320faa4e093dbed776be1a829c2eb222c34c"}, @@ -4512,17 +6152,130 @@ files = [ [package.dependencies] typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" +[[package]] +name = "pydantic-core" +version = "2.33.2" +description = "Core functionality for Pydantic validation and serialization" +optional = false +python-versions = ">=3.9" +groups = ["main", "dev", "proxy-dev"] +markers = "python_version >= \"3.9\" and platform_python_implementation != \"PyPy\" or python_version >= \"3.10\"" +files = [ + {file = "pydantic_core-2.33.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2b3d326aaef0c0399d9afffeb6367d5e26ddc24d351dbc9c636840ac355dc5d8"}, + {file = "pydantic_core-2.33.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0e5b2671f05ba48b94cb90ce55d8bdcaaedb8ba00cc5359f6810fc918713983d"}, + {file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0069c9acc3f3981b9ff4cdfaf088e98d83440a4c7ea1bc07460af3d4dc22e72d"}, + {file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d53b22f2032c42eaaf025f7c40c2e3b94568ae077a606f006d206a463bc69572"}, + {file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0405262705a123b7ce9f0b92f123334d67b70fd1f20a9372b907ce1080c7ba02"}, + {file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4b25d91e288e2c4e0662b8038a28c6a07eaac3e196cfc4ff69de4ea3db992a1b"}, + {file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6bdfe4b3789761f3bcb4b1ddf33355a71079858958e3a552f16d5af19768fef2"}, + {file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:efec8db3266b76ef9607c2c4c419bdb06bf335ae433b80816089ea7585816f6a"}, + {file = "pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:031c57d67ca86902726e0fae2214ce6770bbe2f710dc33063187a68744a5ecac"}, + {file = "pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:f8de619080e944347f5f20de29a975c2d815d9ddd8be9b9b7268e2e3ef68605a"}, + {file = "pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:73662edf539e72a9440129f231ed3757faab89630d291b784ca99237fb94db2b"}, + {file = "pydantic_core-2.33.2-cp310-cp310-win32.whl", hash = "sha256:0a39979dcbb70998b0e505fb1556a1d550a0781463ce84ebf915ba293ccb7e22"}, + {file = "pydantic_core-2.33.2-cp310-cp310-win_amd64.whl", hash = "sha256:b0379a2b24882fef529ec3b4987cb5d003b9cda32256024e6fe1586ac45fc640"}, + {file = "pydantic_core-2.33.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:4c5b0a576fb381edd6d27f0a85915c6daf2f8138dc5c267a57c08a62900758c7"}, + {file = "pydantic_core-2.33.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e799c050df38a639db758c617ec771fd8fb7a5f8eaaa4b27b101f266b216a246"}, + {file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dc46a01bf8d62f227d5ecee74178ffc448ff4e5197c756331f71efcc66dc980f"}, + {file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a144d4f717285c6d9234a66778059f33a89096dfb9b39117663fd8413d582dcc"}, + {file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:73cf6373c21bc80b2e0dc88444f41ae60b2f070ed02095754eb5a01df12256de"}, + {file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3dc625f4aa79713512d1976fe9f0bc99f706a9dee21dfd1810b4bbbf228d0e8a"}, + {file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:881b21b5549499972441da4758d662aeea93f1923f953e9cbaff14b8b9565aef"}, + {file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bdc25f3681f7b78572699569514036afe3c243bc3059d3942624e936ec93450e"}, + {file = "pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:fe5b32187cbc0c862ee201ad66c30cf218e5ed468ec8dc1cf49dec66e160cc4d"}, + {file = "pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:bc7aee6f634a6f4a95676fcb5d6559a2c2a390330098dba5e5a5f28a2e4ada30"}, + {file = "pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:235f45e5dbcccf6bd99f9f472858849f73d11120d76ea8707115415f8e5ebebf"}, + {file = "pydantic_core-2.33.2-cp311-cp311-win32.whl", hash = "sha256:6368900c2d3ef09b69cb0b913f9f8263b03786e5b2a387706c5afb66800efd51"}, + {file = "pydantic_core-2.33.2-cp311-cp311-win_amd64.whl", hash = "sha256:1e063337ef9e9820c77acc768546325ebe04ee38b08703244c1309cccc4f1bab"}, + {file = "pydantic_core-2.33.2-cp311-cp311-win_arm64.whl", hash = "sha256:6b99022f1d19bc32a4c2a0d544fc9a76e3be90f0b3f4af413f87d38749300e65"}, + {file = "pydantic_core-2.33.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a7ec89dc587667f22b6a0b6579c249fca9026ce7c333fc142ba42411fa243cdc"}, + {file = "pydantic_core-2.33.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3c6db6e52c6d70aa0d00d45cdb9b40f0433b96380071ea80b09277dba021ddf7"}, + {file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e61206137cbc65e6d5256e1166f88331d3b6238e082d9f74613b9b765fb9025"}, + {file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eb8c529b2819c37140eb51b914153063d27ed88e3bdc31b71198a198e921e011"}, + {file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c52b02ad8b4e2cf14ca7b3d918f3eb0ee91e63b3167c32591e57c4317e134f8f"}, + {file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:96081f1605125ba0855dfda83f6f3df5ec90c61195421ba72223de35ccfb2f88"}, + {file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f57a69461af2a5fa6e6bbd7a5f60d3b7e6cebb687f55106933188e79ad155c1"}, + {file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:572c7e6c8bb4774d2ac88929e3d1f12bc45714ae5ee6d9a788a9fb35e60bb04b"}, + {file = "pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:db4b41f9bd95fbe5acd76d89920336ba96f03e149097365afe1cb092fceb89a1"}, + {file = "pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:fa854f5cf7e33842a892e5c73f45327760bc7bc516339fda888c75ae60edaeb6"}, + {file = "pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:5f483cfb75ff703095c59e365360cb73e00185e01aaea067cd19acffd2ab20ea"}, + {file = "pydantic_core-2.33.2-cp312-cp312-win32.whl", hash = "sha256:9cb1da0f5a471435a7bc7e439b8a728e8b61e59784b2af70d7c169f8dd8ae290"}, + {file = "pydantic_core-2.33.2-cp312-cp312-win_amd64.whl", hash = "sha256:f941635f2a3d96b2973e867144fde513665c87f13fe0e193c158ac51bfaaa7b2"}, + {file = "pydantic_core-2.33.2-cp312-cp312-win_arm64.whl", hash = "sha256:cca3868ddfaccfbc4bfb1d608e2ccaaebe0ae628e1416aeb9c4d88c001bb45ab"}, + {file = "pydantic_core-2.33.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1082dd3e2d7109ad8b7da48e1d4710c8d06c253cbc4a27c1cff4fbcaa97a9e3f"}, + {file = "pydantic_core-2.33.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f517ca031dfc037a9c07e748cefd8d96235088b83b4f4ba8939105d20fa1dcd6"}, + {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a9f2c9dd19656823cb8250b0724ee9c60a82f3cdf68a080979d13092a3b0fef"}, + {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2b0a451c263b01acebe51895bfb0e1cc842a5c666efe06cdf13846c7418caa9a"}, + {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ea40a64d23faa25e62a70ad163571c0b342b8bf66d5fa612ac0dec4f069d916"}, + {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0fb2d542b4d66f9470e8065c5469ec676978d625a8b7a363f07d9a501a9cb36a"}, + {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdac5d6ffa1b5a83bca06ffe7583f5576555e6c8b3a91fbd25ea7780f825f7d"}, + {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:04a1a413977ab517154eebb2d326da71638271477d6ad87a769102f7c2488c56"}, + {file = "pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c8e7af2f4e0194c22b5b37205bfb293d166a7344a5b0d0eaccebc376546d77d5"}, + {file = "pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:5c92edd15cd58b3c2d34873597a1e20f13094f59cf88068adb18947df5455b4e"}, + {file = "pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:65132b7b4a1c0beded5e057324b7e16e10910c106d43675d9bd87d4f38dde162"}, + {file = "pydantic_core-2.33.2-cp313-cp313-win32.whl", hash = "sha256:52fb90784e0a242bb96ec53f42196a17278855b0f31ac7c3cc6f5c1ec4811849"}, + {file = "pydantic_core-2.33.2-cp313-cp313-win_amd64.whl", hash = "sha256:c083a3bdd5a93dfe480f1125926afcdbf2917ae714bdb80b36d34318b2bec5d9"}, + {file = "pydantic_core-2.33.2-cp313-cp313-win_arm64.whl", hash = "sha256:e80b087132752f6b3d714f041ccf74403799d3b23a72722ea2e6ba2e892555b9"}, + {file = "pydantic_core-2.33.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:61c18fba8e5e9db3ab908620af374db0ac1baa69f0f32df4f61ae23f15e586ac"}, + {file = "pydantic_core-2.33.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95237e53bb015f67b63c91af7518a62a8660376a6a0db19b89acc77a4d6199f5"}, + {file = "pydantic_core-2.33.2-cp313-cp313t-win_amd64.whl", hash = "sha256:c2fc0a768ef76c15ab9238afa6da7f69895bb5d1ee83aeea2e3509af4472d0b9"}, + {file = "pydantic_core-2.33.2-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:a2b911a5b90e0374d03813674bf0a5fbbb7741570dcd4b4e85a2e48d17def29d"}, + {file = "pydantic_core-2.33.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:6fa6dfc3e4d1f734a34710f391ae822e0a8eb8559a85c6979e14e65ee6ba2954"}, + {file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c54c939ee22dc8e2d545da79fc5381f1c020d6d3141d3bd747eab59164dc89fb"}, + {file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:53a57d2ed685940a504248187d5685e49eb5eef0f696853647bf37c418c538f7"}, + {file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:09fb9dd6571aacd023fe6aaca316bd01cf60ab27240d7eb39ebd66a3a15293b4"}, + {file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0e6116757f7959a712db11f3e9c0a99ade00a5bbedae83cb801985aa154f071b"}, + {file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d55ab81c57b8ff8548c3e4947f119551253f4e3787a7bbc0b6b3ca47498a9d3"}, + {file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c20c462aa4434b33a2661701b861604913f912254e441ab8d78d30485736115a"}, + {file = "pydantic_core-2.33.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:44857c3227d3fb5e753d5fe4a3420d6376fa594b07b621e220cd93703fe21782"}, + {file = "pydantic_core-2.33.2-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:eb9b459ca4df0e5c87deb59d37377461a538852765293f9e6ee834f0435a93b9"}, + {file = "pydantic_core-2.33.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:9fcd347d2cc5c23b06de6d3b7b8275be558a0c90549495c699e379a80bf8379e"}, + {file = "pydantic_core-2.33.2-cp39-cp39-win32.whl", hash = "sha256:83aa99b1285bc8f038941ddf598501a86f1536789740991d7d8756e34f1e74d9"}, + {file = "pydantic_core-2.33.2-cp39-cp39-win_amd64.whl", hash = "sha256:f481959862f57f29601ccced557cc2e817bce7533ab8e01a797a48b49c9692b3"}, + {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5c4aa4e82353f65e548c476b37e64189783aa5384903bfea4f41580f255fddfa"}, + {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:d946c8bf0d5c24bf4fe333af284c59a19358aa3ec18cb3dc4370080da1e8ad29"}, + {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:87b31b6846e361ef83fedb187bb5b4372d0da3f7e28d85415efa92d6125d6e6d"}, + {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa9d91b338f2df0508606f7009fde642391425189bba6d8c653afd80fd6bb64e"}, + {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2058a32994f1fde4ca0480ab9d1e75a0e8c87c22b53a3ae66554f9af78f2fe8c"}, + {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:0e03262ab796d986f978f79c943fc5f620381be7287148b8010b4097f79a39ec"}, + {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:1a8695a8d00c73e50bff9dfda4d540b7dee29ff9b8053e38380426a85ef10052"}, + {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:fa754d1850735a0b0e03bcffd9d4b4343eb417e47196e4485d9cca326073a42c"}, + {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:a11c8d26a50bfab49002947d3d237abe4d9e4b5bdc8846a63537b6488e197808"}, + {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:dd14041875d09cc0f9308e37a6f8b65f5585cf2598a53aa0123df8b129d481f8"}, + {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:d87c561733f66531dced0da6e864f44ebf89a8fba55f31407b00c2f7f9449593"}, + {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2f82865531efd18d6e07a04a17331af02cb7a651583c418df8266f17a63c6612"}, + {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bfb5112df54209d820d7bf9317c7a6c9025ea52e49f46b6a2060104bba37de7"}, + {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:64632ff9d614e5eecfb495796ad51b0ed98c453e447a76bcbeeb69615079fc7e"}, + {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:f889f7a40498cc077332c7ab6b4608d296d852182211787d4f3ee377aaae66e8"}, + {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:de4b83bb311557e439b9e186f733f6c645b9417c84e2eb8203f3f820a4b988bf"}, + {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:82f68293f055f51b51ea42fafc74b6aad03e70e191799430b90c13d643059ebb"}, + {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:329467cecfb529c925cf2bbd4d60d2c509bc2fb52a20c1045bf09bb70971a9c1"}, + {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:87acbfcf8e90ca885206e98359d7dca4bcbb35abdc0ff66672a293e1d7a19101"}, + {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:7f92c15cd1e97d4b12acd1cc9004fa092578acfa57b67ad5e43a197175d01a64"}, + {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d3f26877a748dc4251cfcfda9dfb5f13fcb034f5308388066bcfe9031b63ae7d"}, + {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dac89aea9af8cd672fa7b510e7b8c33b0bba9a43186680550ccf23020f32d535"}, + {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:970919794d126ba8645f3837ab6046fb4e72bbc057b3709144066204c19a455d"}, + {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:3eb3fe62804e8f859c49ed20a8451342de53ed764150cb14ca71357c765dc2a6"}, + {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:3abcd9392a36025e3bd55f9bd38d908bd17962cc49bc6da8e7e96285336e2bca"}, + {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:3a1c81334778f9e3af2f8aeb7a960736e5cab1dfebfb26aabca09afd2906c039"}, + {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:2807668ba86cb38c6817ad9bc66215ab8584d1d304030ce4f0887336f28a5e27"}, + {file = "pydantic_core-2.33.2.tar.gz", hash = "sha256:7cb8bc3605c29176e1b105350d2e6474142d7c1bd1d9327c4a9bdb46bf827acc"}, +] + +[package.dependencies] +typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" + [[package]] name = "pydantic-settings" -version = "2.10.1" +version = "2.11.0" description = "Settings management using Pydantic" optional = true python-versions = ">=3.9" groups = ["main"] markers = "python_version >= \"3.10\" and extra == \"proxy\"" files = [ - {file = "pydantic_settings-2.10.1-py3-none-any.whl", hash = "sha256:a60952460b99cf661dc25c29c0ef171721f98bfcb52ef8d9ea4c943d7c8cc796"}, - {file = "pydantic_settings-2.10.1.tar.gz", hash = "sha256:06f0062169818d0f5524420a360d632d5857b83cffd4d42fe29597807a1614ee"}, + {file = "pydantic_settings-2.11.0-py3-none-any.whl", hash = "sha256:fe2cea3413b9530d10f3a5875adffb17ada5c1e1bab0b2885546d7310415207c"}, + {file = "pydantic_settings-2.11.0.tar.gz", hash = "sha256:d0e87a1c7d33593beb7194adb8470fc426e95ba02af83a0f23474a04c9a08180"}, ] [package.dependencies] @@ -4572,6 +6325,7 @@ description = "JSON Web Token implementation in Python" optional = false python-versions = ">=3.8" groups = ["main", "proxy-dev"] +markers = "python_full_version == \"3.8.*\" or platform_python_implementation == \"PyPy\" and python_version < \"3.10\"" files = [ {file = "PyJWT-2.9.0-py3-none-any.whl", hash = "sha256:3b02fb0f44517787776cf48f2ae25d8e14f300e6d7545a4315cee571a415e850"}, {file = "pyjwt-2.9.0.tar.gz", hash = "sha256:7e1e5b56cc735432a7369cbfa0efe50fa113ebecdc04ae6922deba8b84582d0c"}, @@ -4586,6 +6340,28 @@ dev = ["coverage[toml] (==5.0.4)", "cryptography (>=3.4.0)", "pre-commit", "pyte docs = ["sphinx", "sphinx-rtd-theme", "zope.interface"] tests = ["coverage[toml] (==5.0.4)", "pytest (>=6.0.0,<7.0.0)"] +[[package]] +name = "pyjwt" +version = "2.10.1" +description = "JSON Web Token implementation in Python" +optional = false +python-versions = ">=3.9" +groups = ["main", "proxy-dev"] +markers = "python_version >= \"3.9\" and platform_python_implementation != \"PyPy\" or python_version >= \"3.10\"" +files = [ + {file = "PyJWT-2.10.1-py3-none-any.whl", hash = "sha256:dcdd193e30abefd5debf142f9adfcdd2b58004e644f25406ffaebd50bd98dacb"}, + {file = "pyjwt-2.10.1.tar.gz", hash = "sha256:3cc5772eb20009233caf06e9d8a0577824723b44e6648ee0a2aedb6cf9381953"}, +] + +[package.dependencies] +cryptography = {version = ">=3.4.0", optional = true, markers = "extra == \"crypto\""} + +[package.extras] +crypto = ["cryptography (>=3.4.0)"] +dev = ["coverage[toml] (==5.0.4)", "cryptography (>=3.4.0)", "pre-commit", "pytest (>=6.0.0,<7.0.0)", "sphinx", "sphinx-rtd-theme", "zope.interface"] +docs = ["sphinx", "sphinx-rtd-theme", "zope.interface"] +tests = ["coverage[toml] (==5.0.4)", "pytest (>=6.0.0,<7.0.0)"] + [[package]] name = "pynacl" version = "1.6.0" @@ -4636,15 +6412,15 @@ tests = ["hypothesis (>=3.27.0)", "pytest (>=7.4.0)", "pytest-cov (>=2.10.1)", " [[package]] name = "pyparsing" -version = "3.2.4" +version = "3.2.5" description = "pyparsing - Classes and methods to define and execute parsing grammars" optional = true python-versions = ">=3.9" groups = ["main"] markers = "python_version >= \"3.10\" and extra == \"mlflow\"" files = [ - {file = "pyparsing-3.2.4-py3-none-any.whl", hash = "sha256:91d0fcde680d42cd031daf3a6ba20da3107e08a75de50da58360e7d94ab24d36"}, - {file = "pyparsing-3.2.4.tar.gz", hash = "sha256:fff89494f45559d0f2ce46613b419f632bbb6afbdaed49696d322bcf98a58e99"}, + {file = "pyparsing-3.2.5-py3-none-any.whl", hash = "sha256:e38a4f02064cf41fe6593d328d0512495ad1f3d8a91c4f73fc401b3079a59a5e"}, + {file = "pyparsing-3.2.5.tar.gz", hash = "sha256:2df8d5b7b2802ef88e8d016a2eb9c7aeaa923529cd251ed0fe4608275d4105b6"}, ] [package.extras] @@ -4715,6 +6491,7 @@ description = "Thin-wrapper around the mock package for easier use with pytest" optional = false python-versions = ">=3.8" groups = ["dev"] +markers = "python_full_version == \"3.8.*\" or platform_python_implementation == \"PyPy\" and python_version < \"3.10\"" files = [ {file = "pytest_mock-3.14.1-py3-none-any.whl", hash = "sha256:178aefcd11307d874b4cd3100344e7e2d888d9791a6a1d9bfe90fbc1b74fd1d0"}, {file = "pytest_mock-3.14.1.tar.gz", hash = "sha256:159e9edac4c451ce77a5cdb9fc5d1100708d2dd4ba3c3df572f14097351af80e"}, @@ -4726,6 +6503,25 @@ pytest = ">=6.2.5" [package.extras] dev = ["pre-commit", "pytest-asyncio", "tox"] +[[package]] +name = "pytest-mock" +version = "3.15.1" +description = "Thin-wrapper around the mock package for easier use with pytest" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +markers = "python_version >= \"3.9\" and platform_python_implementation != \"PyPy\" or python_version >= \"3.10\"" +files = [ + {file = "pytest_mock-3.15.1-py3-none-any.whl", hash = "sha256:0a25e2eb88fe5168d535041d09a4529a188176ae608a6d249ee65abc0949630d"}, + {file = "pytest_mock-3.15.1.tar.gz", hash = "sha256:1849a238f6f396da19762269de72cb1814ab44416fa73a8686deac10b0d87a0f"}, +] + +[package.dependencies] +pytest = ">=6.2.5" + +[package.extras] +dev = ["pre-commit", "pytest-asyncio", "tox"] + [[package]] name = "python-dateutil" version = "2.9.0.post0" @@ -4733,7 +6529,7 @@ description = "Extensions to the standard Python datetime module" optional = true python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" groups = ["main"] -markers = "python_version >= \"3.10\" and (extra == \"mlflow\" or extra == \"proxy\") or extra == \"proxy\"" +markers = "(extra == \"mlflow\" or extra == \"proxy\") and python_version >= \"3.10\" or extra == \"proxy\"" files = [ {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"}, {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"}, @@ -4749,6 +6545,7 @@ description = "Read key-value pairs from a .env file and set them as environment optional = false python-versions = ">=3.8" groups = ["main", "proxy-dev"] +markers = "python_full_version == \"3.8.*\" or platform_python_implementation == \"PyPy\" and python_version < \"3.10\"" files = [ {file = "python-dotenv-1.0.1.tar.gz", hash = "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca"}, {file = "python_dotenv-1.0.1-py3-none-any.whl", hash = "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a"}, @@ -4757,6 +6554,22 @@ files = [ [package.extras] cli = ["click (>=5.0)"] +[[package]] +name = "python-dotenv" +version = "1.1.1" +description = "Read key-value pairs from a .env file and set them as environment variables" +optional = false +python-versions = ">=3.9" +groups = ["main", "proxy-dev"] +markers = "python_version >= \"3.9\" and platform_python_implementation != \"PyPy\" or python_version >= \"3.10\"" +files = [ + {file = "python_dotenv-1.1.1-py3-none-any.whl", hash = "sha256:31f23644fe2602f88ff55e1f5c79ba497e01224ee7737937930c448e4d0e24dc"}, + {file = "python_dotenv-1.1.1.tar.gz", hash = "sha256:a8a6399716257f45be6a007360200409fce5cda2661e3dec71d23dc15f6189ab"}, +] + +[package.extras] +cli = ["click (>=5.0)"] + [[package]] name = "python-multipart" version = "0.0.18" @@ -4793,7 +6606,7 @@ description = "World timezone definitions, modern and historical" optional = true python-versions = "*" groups = ["main"] -markers = "python_version >= \"3.10\" and extra == \"mlflow\" or python_version < \"3.9\" and extra == \"utils\"" +markers = "python_version >= \"3.10\" and (extra == \"mlflow\" or extra == \"proxy\") or extra == \"utils\" and python_version < \"3.9\" and platform_python_implementation == \"PyPy\" or python_version >= \"3.9\" and platform_python_implementation != \"PyPy\" and extra == \"proxy\" or python_version == \"3.8\" and extra == \"utils\" or extra == \"utils\" and extra == \"proxy\" and platform_python_implementation != \"PyPy\" and (extra == \"mlflow\" or extra == \"proxy\")" files = [ {file = "pytz-2025.2-py2.py3-none-any.whl", hash = "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00"}, {file = "pytz-2025.2.tar.gz", hash = "sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3"}, @@ -4900,7 +6713,7 @@ description = "Python client for Redis database and key-value store" optional = true python-versions = ">=3.8" groups = ["main"] -markers = "python_version >= \"3.9\" and (extra == \"extra-proxy\" or extra == \"proxy\") and python_version < \"3.14\" or extra == \"proxy\"" +markers = "python_version >= \"3.9\" and python_version < \"3.14\" and (extra == \"extra-proxy\" or extra == \"proxy\") or platform_python_implementation == \"PyPy\" and extra == \"proxy\"" files = [ {file = "redis-5.3.1-py3-none-any.whl", hash = "sha256:dc1909bd24669cc31b5f67a039700b16ec30571096c5f1f0d9d2324bff31af97"}, {file = "redis-5.3.1.tar.gz", hash = "sha256:ca49577a531ea64039b5a36db3d6cd1a0c7a60c34124d46924a45b956e8cf14c"}, @@ -4914,6 +6727,45 @@ PyJWT = ">=2.9.0" hiredis = ["hiredis (>=3.0.0)"] ocsp = ["cryptography (>=36.0.1)", "pyopenssl (==23.2.1)", "requests (>=2.31.0)"] +[[package]] +name = "redis" +version = "6.1.1" +description = "Python client for Redis database and key-value store" +optional = true +python-versions = ">=3.8" +groups = ["main"] +markers = "python_full_version == \"3.8.*\" and platform_python_implementation != \"PyPy\" and extra == \"proxy\"" +files = [ + {file = "redis-6.1.1-py3-none-any.whl", hash = "sha256:ed44d53d065bbe04ac6d76864e331cfe5c5353f86f6deccc095f8794fd15bb2e"}, + {file = "redis-6.1.1.tar.gz", hash = "sha256:88c689325b5b41cedcbdbdfd4d937ea86cf6dab2222a83e86d8a466e4b3d2600"}, +] + +[package.dependencies] +async-timeout = {version = ">=4.0.3", markers = "python_full_version < \"3.11.3\""} + +[package.extras] +hiredis = ["hiredis (>=3.0.0)"] +jwt = ["pyjwt (>=2.9.0)"] +ocsp = ["cryptography (>=36.0.1)", "pyopenssl (>=20.0.1)", "requests (>=2.31.0)"] + +[[package]] +name = "redis" +version = "6.4.0" +description = "Python client for Redis database and key-value store" +optional = true +python-versions = ">=3.9" +groups = ["main"] +markers = "platform_python_implementation != \"PyPy\" and python_version >= \"3.14\" and extra == \"proxy\"" +files = [ + {file = "redis-6.4.0-py3-none-any.whl", hash = "sha256:f0544fa9604264e9464cdf4814e7d4830f74b165d52f2a330a760a88dd248b7f"}, + {file = "redis-6.4.0.tar.gz", hash = "sha256:b01bc7282b8444e28ec36b261df5375183bb47a07eb9c603f284e89cbc5ef010"}, +] + +[package.extras] +hiredis = ["hiredis (>=3.2.0)"] +jwt = ["pyjwt (>=2.9.0)"] +ocsp = ["cryptography (>=36.0.1)", "pyopenssl (>=20.0.1)", "requests (>=2.31.0)"] + [[package]] name = "redisvl" version = "0.4.1" @@ -4957,6 +6809,7 @@ description = "JSON Referencing + Python" optional = false python-versions = ">=3.8" groups = ["main"] +markers = "python_full_version == \"3.8.*\" or platform_python_implementation == \"PyPy\" and python_version < \"3.10\"" files = [ {file = "referencing-0.35.1-py3-none-any.whl", hash = "sha256:eda6d3234d62814d1c64e305c1331c9a3a6132da475ab6382eaa997b21ee75de"}, {file = "referencing-0.35.1.tar.gz", hash = "sha256:25b42124a6c8b632a425174f24087783efb348a6f1e0008e63cd4466fedf703c"}, @@ -4966,6 +6819,24 @@ files = [ attrs = ">=22.2.0" rpds-py = ">=0.7.0" +[[package]] +name = "referencing" +version = "0.36.2" +description = "JSON Referencing + Python" +optional = false +python-versions = ">=3.9" +groups = ["main"] +markers = "python_version >= \"3.9\" and platform_python_implementation != \"PyPy\" or python_version >= \"3.10\"" +files = [ + {file = "referencing-0.36.2-py3-none-any.whl", hash = "sha256:e8699adbbf8b5c7de96d8ffa0eb5c158b3beafce084968e2ea8bb08c6794dcd0"}, + {file = "referencing-0.36.2.tar.gz", hash = "sha256:df2e89862cd09deabbdba16944cc3f10feb6b3e6f18e902f7cc25609a34775aa"}, +] + +[package.dependencies] +attrs = ">=22.2.0" +rpds-py = ">=0.7.0" +typing-extensions = {version = ">=4.4.0", markers = "python_version < \"3.13\""} + [[package]] name = "regex" version = "2024.11.6" @@ -4973,6 +6844,7 @@ description = "Alternative regular expression module, to replace re." optional = false python-versions = ">=3.8" groups = ["main"] +markers = "python_full_version == \"3.8.*\" or platform_python_implementation == \"PyPy\" and python_version < \"3.10\"" files = [ {file = "regex-2024.11.6-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ff590880083d60acc0433f9c3f713c51f7ac6ebb9adf889c79a261ecf541aa91"}, {file = "regex-2024.11.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:658f90550f38270639e83ce492f27d2c8d2cd63805c65a13a14d36ca126753f0"}, @@ -5070,6 +6942,132 @@ files = [ {file = "regex-2024.11.6.tar.gz", hash = "sha256:7ab159b063c52a0333c884e4679f8d7a85112ee3078fe3d9004b2dd875585519"}, ] +[[package]] +name = "regex" +version = "2025.9.18" +description = "Alternative regular expression module, to replace re." +optional = false +python-versions = ">=3.9" +groups = ["main"] +markers = "python_version >= \"3.9\" and platform_python_implementation != \"PyPy\" or python_version >= \"3.10\"" +files = [ + {file = "regex-2025.9.18-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:12296202480c201c98a84aecc4d210592b2f55e200a1d193235c4db92b9f6788"}, + {file = "regex-2025.9.18-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:220381f1464a581f2ea988f2220cf2a67927adcef107d47d6897ba5a2f6d51a4"}, + {file = "regex-2025.9.18-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:87f681bfca84ebd265278b5daa1dcb57f4db315da3b5d044add7c30c10442e61"}, + {file = "regex-2025.9.18-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:34d674cbba70c9398074c8a1fcc1a79739d65d1105de2a3c695e2b05ea728251"}, + {file = "regex-2025.9.18-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:385c9b769655cb65ea40b6eea6ff763cbb6d69b3ffef0b0db8208e1833d4e746"}, + {file = "regex-2025.9.18-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8900b3208e022570ae34328712bef6696de0804c122933414014bae791437ab2"}, + {file = "regex-2025.9.18-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c204e93bf32cd7a77151d44b05eb36f469d0898e3fba141c026a26b79d9914a0"}, + {file = "regex-2025.9.18-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3acc471d1dd7e5ff82e6cacb3b286750decd949ecd4ae258696d04f019817ef8"}, + {file = "regex-2025.9.18-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:6479d5555122433728760e5f29edb4c2b79655a8deb681a141beb5c8a025baea"}, + {file = "regex-2025.9.18-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:431bd2a8726b000eb6f12429c9b438a24062a535d06783a93d2bcbad3698f8a8"}, + {file = "regex-2025.9.18-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:0cc3521060162d02bd36927e20690129200e5ac9d2c6d32b70368870b122db25"}, + {file = "regex-2025.9.18-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:a021217b01be2d51632ce056d7a837d3fa37c543ede36e39d14063176a26ae29"}, + {file = "regex-2025.9.18-cp310-cp310-win32.whl", hash = "sha256:4a12a06c268a629cb67cc1d009b7bb0be43e289d00d5111f86a2efd3b1949444"}, + {file = "regex-2025.9.18-cp310-cp310-win_amd64.whl", hash = "sha256:47acd811589301298c49db2c56bde4f9308d6396da92daf99cba781fa74aa450"}, + {file = "regex-2025.9.18-cp310-cp310-win_arm64.whl", hash = "sha256:16bd2944e77522275e5ee36f867e19995bcaa533dcb516753a26726ac7285442"}, + {file = "regex-2025.9.18-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:51076980cd08cd13c88eb7365427ae27f0d94e7cebe9ceb2bb9ffdae8fc4d82a"}, + {file = "regex-2025.9.18-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:828446870bd7dee4e0cbeed767f07961aa07f0ea3129f38b3ccecebc9742e0b8"}, + {file = "regex-2025.9.18-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c28821d5637866479ec4cc23b8c990f5bc6dd24e5e4384ba4a11d38a526e1414"}, + {file = "regex-2025.9.18-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:726177ade8e481db669e76bf99de0b278783be8acd11cef71165327abd1f170a"}, + {file = "regex-2025.9.18-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f5cca697da89b9f8ea44115ce3130f6c54c22f541943ac8e9900461edc2b8bd4"}, + {file = "regex-2025.9.18-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:dfbde38f38004703c35666a1e1c088b778e35d55348da2b7b278914491698d6a"}, + {file = "regex-2025.9.18-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f2f422214a03fab16bfa495cfec72bee4aaa5731843b771860a471282f1bf74f"}, + {file = "regex-2025.9.18-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a295916890f4df0902e4286bc7223ee7f9e925daa6dcdec4192364255b70561a"}, + {file = "regex-2025.9.18-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:5db95ff632dbabc8c38c4e82bf545ab78d902e81160e6e455598014f0abe66b9"}, + {file = "regex-2025.9.18-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:fb967eb441b0f15ae610b7069bdb760b929f267efbf522e814bbbfffdf125ce2"}, + {file = "regex-2025.9.18-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f04d2f20da4053d96c08f7fde6e1419b7ec9dbcee89c96e3d731fca77f411b95"}, + {file = "regex-2025.9.18-cp311-cp311-win32.whl", hash = "sha256:895197241fccf18c0cea7550c80e75f185b8bd55b6924fcae269a1a92c614a07"}, + {file = "regex-2025.9.18-cp311-cp311-win_amd64.whl", hash = "sha256:7e2b414deae99166e22c005e154a5513ac31493db178d8aec92b3269c9cce8c9"}, + {file = "regex-2025.9.18-cp311-cp311-win_arm64.whl", hash = "sha256:fb137ec7c5c54f34a25ff9b31f6b7b0c2757be80176435bf367111e3f71d72df"}, + {file = "regex-2025.9.18-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:436e1b31d7efd4dcd52091d076482031c611dde58bf9c46ca6d0a26e33053a7e"}, + {file = "regex-2025.9.18-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c190af81e5576b9c5fdc708f781a52ff20f8b96386c6e2e0557a78402b029f4a"}, + {file = "regex-2025.9.18-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e4121f1ce2b2b5eec4b397cc1b277686e577e658d8f5870b7eb2d726bd2300ab"}, + {file = "regex-2025.9.18-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:300e25dbbf8299d87205e821a201057f2ef9aa3deb29caa01cd2cac669e508d5"}, + {file = "regex-2025.9.18-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7b47fcf9f5316c0bdaf449e879407e1b9937a23c3b369135ca94ebc8d74b1742"}, + {file = "regex-2025.9.18-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:57a161bd3acaa4b513220b49949b07e252165e6b6dc910ee7617a37ff4f5b425"}, + {file = "regex-2025.9.18-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4f130c3a7845ba42de42f380fff3c8aebe89a810747d91bcf56d40a069f15352"}, + {file = "regex-2025.9.18-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5f96fa342b6f54dcba928dd452e8d8cb9f0d63e711d1721cd765bb9f73bb048d"}, + {file = "regex-2025.9.18-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:0f0d676522d68c207828dcd01fb6f214f63f238c283d9f01d85fc664c7c85b56"}, + {file = "regex-2025.9.18-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:40532bff8a1a0621e7903ae57fce88feb2e8a9a9116d341701302c9302aef06e"}, + {file = "regex-2025.9.18-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:039f11b618ce8d71a1c364fdee37da1012f5a3e79b1b2819a9f389cd82fd6282"}, + {file = "regex-2025.9.18-cp312-cp312-win32.whl", hash = "sha256:e1dd06f981eb226edf87c55d523131ade7285137fbde837c34dc9d1bf309f459"}, + {file = "regex-2025.9.18-cp312-cp312-win_amd64.whl", hash = "sha256:3d86b5247bf25fa3715e385aa9ff272c307e0636ce0c9595f64568b41f0a9c77"}, + {file = "regex-2025.9.18-cp312-cp312-win_arm64.whl", hash = "sha256:032720248cbeeae6444c269b78cb15664458b7bb9ed02401d3da59fe4d68c3a5"}, + {file = "regex-2025.9.18-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:2a40f929cd907c7e8ac7566ac76225a77701a6221bca937bdb70d56cb61f57b2"}, + {file = "regex-2025.9.18-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c90471671c2cdf914e58b6af62420ea9ecd06d1554d7474d50133ff26ae88feb"}, + {file = "regex-2025.9.18-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1a351aff9e07a2dabb5022ead6380cff17a4f10e4feb15f9100ee56c4d6d06af"}, + {file = "regex-2025.9.18-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bc4b8e9d16e20ddfe16430c23468a8707ccad3365b06d4536142e71823f3ca29"}, + {file = "regex-2025.9.18-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4b8cdbddf2db1c5e80338ba2daa3cfa3dec73a46fff2a7dda087c8efbf12d62f"}, + {file = "regex-2025.9.18-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a276937d9d75085b2c91fb48244349c6954f05ee97bba0963ce24a9d915b8b68"}, + {file = "regex-2025.9.18-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:92a8e375ccdc1256401c90e9dc02b8642894443d549ff5e25e36d7cf8a80c783"}, + {file = "regex-2025.9.18-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0dc6893b1f502d73037cf807a321cdc9be29ef3d6219f7970f842475873712ac"}, + {file = "regex-2025.9.18-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:a61e85bfc63d232ac14b015af1261f826260c8deb19401c0597dbb87a864361e"}, + {file = "regex-2025.9.18-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:1ef86a9ebc53f379d921fb9a7e42b92059ad3ee800fcd9e0fe6181090e9f6c23"}, + {file = "regex-2025.9.18-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d3bc882119764ba3a119fbf2bd4f1b47bc56c1da5d42df4ed54ae1e8e66fdf8f"}, + {file = "regex-2025.9.18-cp313-cp313-win32.whl", hash = "sha256:3810a65675845c3bdfa58c3c7d88624356dd6ee2fc186628295e0969005f928d"}, + {file = "regex-2025.9.18-cp313-cp313-win_amd64.whl", hash = "sha256:16eaf74b3c4180ede88f620f299e474913ab6924d5c4b89b3833bc2345d83b3d"}, + {file = "regex-2025.9.18-cp313-cp313-win_arm64.whl", hash = "sha256:4dc98ba7dd66bd1261927a9f49bd5ee2bcb3660f7962f1ec02617280fc00f5eb"}, + {file = "regex-2025.9.18-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:fe5d50572bc885a0a799410a717c42b1a6b50e2f45872e2b40f4f288f9bce8a2"}, + {file = "regex-2025.9.18-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:1b9d9a2d6cda6621551ca8cf7a06f103adf72831153f3c0d982386110870c4d3"}, + {file = "regex-2025.9.18-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:13202e4c4ac0ef9a317fff817674b293c8f7e8c68d3190377d8d8b749f566e12"}, + {file = "regex-2025.9.18-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:874ff523b0fecffb090f80ae53dc93538f8db954c8bb5505f05b7787ab3402a0"}, + {file = "regex-2025.9.18-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d13ab0490128f2bb45d596f754148cd750411afc97e813e4b3a61cf278a23bb6"}, + {file = "regex-2025.9.18-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:05440bc172bc4b4b37fb9667e796597419404dbba62e171e1f826d7d2a9ebcef"}, + {file = "regex-2025.9.18-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5514b8e4031fdfaa3d27e92c75719cbe7f379e28cacd939807289bce76d0e35a"}, + {file = "regex-2025.9.18-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:65d3c38c39efce73e0d9dc019697b39903ba25b1ad45ebbd730d2cf32741f40d"}, + {file = "regex-2025.9.18-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:ae77e447ebc144d5a26d50055c6ddba1d6ad4a865a560ec7200b8b06bc529368"}, + {file = "regex-2025.9.18-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:e3ef8cf53dc8df49d7e28a356cf824e3623764e9833348b655cfed4524ab8a90"}, + {file = "regex-2025.9.18-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:9feb29817df349c976da9a0debf775c5c33fc1c8ad7b9f025825da99374770b7"}, + {file = "regex-2025.9.18-cp313-cp313t-win32.whl", hash = "sha256:168be0d2f9b9d13076940b1ed774f98595b4e3c7fc54584bba81b3cc4181742e"}, + {file = "regex-2025.9.18-cp313-cp313t-win_amd64.whl", hash = "sha256:d59ecf3bb549e491c8104fea7313f3563c7b048e01287db0a90485734a70a730"}, + {file = "regex-2025.9.18-cp313-cp313t-win_arm64.whl", hash = "sha256:dbef80defe9fb21310948a2595420b36c6d641d9bea4c991175829b2cc4bc06a"}, + {file = "regex-2025.9.18-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:c6db75b51acf277997f3adcd0ad89045d856190d13359f15ab5dda21581d9129"}, + {file = "regex-2025.9.18-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8f9698b6f6895d6db810e0bda5364f9ceb9e5b11328700a90cae573574f61eea"}, + {file = "regex-2025.9.18-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:29cd86aa7cb13a37d0f0d7c21d8d949fe402ffa0ea697e635afedd97ab4b69f1"}, + {file = "regex-2025.9.18-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7c9f285a071ee55cd9583ba24dde006e53e17780bb309baa8e4289cd472bcc47"}, + {file = "regex-2025.9.18-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5adf266f730431e3be9021d3e5b8d5ee65e563fec2883ea8093944d21863b379"}, + {file = "regex-2025.9.18-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1137cabc0f38807de79e28d3f6e3e3f2cc8cfb26bead754d02e6d1de5f679203"}, + {file = "regex-2025.9.18-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7cc9e5525cada99699ca9223cce2d52e88c52a3d2a0e842bd53de5497c604164"}, + {file = "regex-2025.9.18-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:bbb9246568f72dce29bcd433517c2be22c7791784b223a810225af3b50d1aafb"}, + {file = "regex-2025.9.18-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:6a52219a93dd3d92c675383efff6ae18c982e2d7651c792b1e6d121055808743"}, + {file = "regex-2025.9.18-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:ae9b3840c5bd456780e3ddf2f737ab55a79b790f6409182012718a35c6d43282"}, + {file = "regex-2025.9.18-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d488c236ac497c46a5ac2005a952c1a0e22a07be9f10c3e735bc7d1209a34773"}, + {file = "regex-2025.9.18-cp314-cp314-win32.whl", hash = "sha256:0c3506682ea19beefe627a38872d8da65cc01ffa25ed3f2e422dffa1474f0788"}, + {file = "regex-2025.9.18-cp314-cp314-win_amd64.whl", hash = "sha256:57929d0f92bebb2d1a83af372cd0ffba2263f13f376e19b1e4fa32aec4efddc3"}, + {file = "regex-2025.9.18-cp314-cp314-win_arm64.whl", hash = "sha256:6a4b44df31d34fa51aa5c995d3aa3c999cec4d69b9bd414a8be51984d859f06d"}, + {file = "regex-2025.9.18-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:b176326bcd544b5e9b17d6943f807697c0cb7351f6cfb45bf5637c95ff7e6306"}, + {file = "regex-2025.9.18-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:0ffd9e230b826b15b369391bec167baed57c7ce39efc35835448618860995946"}, + {file = "regex-2025.9.18-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ec46332c41add73f2b57e2f5b642f991f6b15e50e9f86285e08ffe3a512ac39f"}, + {file = "regex-2025.9.18-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b80fa342ed1ea095168a3f116637bd1030d39c9ff38dc04e54ef7c521e01fc95"}, + {file = "regex-2025.9.18-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f4d97071c0ba40f0cf2a93ed76e660654c399a0a04ab7d85472239460f3da84b"}, + {file = "regex-2025.9.18-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0ac936537ad87cef9e0e66c5144484206c1354224ee811ab1519a32373e411f3"}, + {file = "regex-2025.9.18-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dec57f96d4def58c422d212d414efe28218d58537b5445cf0c33afb1b4768571"}, + {file = "regex-2025.9.18-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:48317233294648bf7cd068857f248e3a57222259a5304d32c7552e2284a1b2ad"}, + {file = "regex-2025.9.18-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:274687e62ea3cf54846a9b25fc48a04459de50af30a7bd0b61a9e38015983494"}, + {file = "regex-2025.9.18-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:a78722c86a3e7e6aadf9579e3b0ad78d955f2d1f1a8ca4f67d7ca258e8719d4b"}, + {file = "regex-2025.9.18-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:06104cd203cdef3ade989a1c45b6215bf42f8b9dd705ecc220c173233f7cba41"}, + {file = "regex-2025.9.18-cp314-cp314t-win32.whl", hash = "sha256:2e1eddc06eeaffd249c0adb6fafc19e2118e6308c60df9db27919e96b5656096"}, + {file = "regex-2025.9.18-cp314-cp314t-win_amd64.whl", hash = "sha256:8620d247fb8c0683ade51217b459cb4a1081c0405a3072235ba43a40d355c09a"}, + {file = "regex-2025.9.18-cp314-cp314t-win_arm64.whl", hash = "sha256:b7531a8ef61de2c647cdf68b3229b071e46ec326b3138b2180acb4275f470b01"}, + {file = "regex-2025.9.18-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:3dbcfcaa18e9480669030d07371713c10b4f1a41f791ffa5cb1a99f24e777f40"}, + {file = "regex-2025.9.18-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:1e85f73ef7095f0380208269055ae20524bfde3f27c5384126ddccf20382a638"}, + {file = "regex-2025.9.18-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:9098e29b3ea4ffffeade423f6779665e2a4f8db64e699c0ed737ef0db6ba7b12"}, + {file = "regex-2025.9.18-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:90b6b7a2d0f45b7ecaaee1aec6b362184d6596ba2092dd583ffba1b78dd0231c"}, + {file = "regex-2025.9.18-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c81b892af4a38286101502eae7aec69f7cd749a893d9987a92776954f3943408"}, + {file = "regex-2025.9.18-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3b524d010973f2e1929aeb635418d468d869a5f77b52084d9f74c272189c251d"}, + {file = "regex-2025.9.18-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6b498437c026a3d5d0be0020023ff76d70ae4d77118e92f6f26c9d0423452446"}, + {file = "regex-2025.9.18-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0716e4d6e58853d83f6563f3cf25c281ff46cf7107e5f11879e32cb0b59797d9"}, + {file = "regex-2025.9.18-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:065b6956749379d41db2625f880b637d4acc14c0a4de0d25d609a62850e96d36"}, + {file = "regex-2025.9.18-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:d4a691494439287c08ddb9b5793da605ee80299dd31e95fa3f323fac3c33d9d4"}, + {file = "regex-2025.9.18-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:ef8d10cc0989565bcbe45fb4439f044594d5c2b8919d3d229ea2c4238f1d55b0"}, + {file = "regex-2025.9.18-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:4baeb1b16735ac969a7eeecc216f1f8b7caf60431f38a2671ae601f716a32d25"}, + {file = "regex-2025.9.18-cp39-cp39-win32.whl", hash = "sha256:8e5f41ad24a1e0b5dfcf4c4e5d9f5bd54c895feb5708dd0c1d0d35693b24d478"}, + {file = "regex-2025.9.18-cp39-cp39-win_amd64.whl", hash = "sha256:50e8290707f2fb8e314ab3831e594da71e062f1d623b05266f8cfe4db4949afd"}, + {file = "regex-2025.9.18-cp39-cp39-win_arm64.whl", hash = "sha256:039a9d7195fd88c943d7c777d4941e8ef736731947becce773c31a1009cb3c35"}, + {file = "regex-2025.9.18.tar.gz", hash = "sha256:c5ba23274c61c6fef447ba6a39333297d0c247f53059dba0bca415cac511edc4"}, +] + [[package]] name = "requests" version = "2.31.0" @@ -5098,11 +7096,12 @@ version = "1.12.1" description = "Mock out responses from the requests package" optional = false python-versions = ">=3.5" -groups = ["dev"] +groups = ["main", "dev"] files = [ {file = "requests-mock-1.12.1.tar.gz", hash = "sha256:e9e12e333b525156e82a3c852f22016b9158220d2f47454de9cae8a77d371401"}, {file = "requests_mock-1.12.1-py2.py3-none-any.whl", hash = "sha256:b1e37054004cdd5e56c84454cc7df12b25f90f382159087f4b6915aaeef39563"}, ] +markers = {main = "(python_version >= \"3.9\" or platform_python_implementation == \"PyPy\") and python_version <= \"3.12\""} [package.dependencies] requests = ">=2.22,<3" @@ -5182,6 +7181,23 @@ typing-extensions = {version = ">=4.0.0,<5.0", markers = "python_version < \"3.9 [package.extras] jupyter = ["ipywidgets (>=7.5.1,<9)"] +[[package]] +name = "roman-numerals-py" +version = "3.1.0" +description = "Manipulate well-formed Roman numerals" +optional = true +python-versions = ">=3.9" +groups = ["main"] +markers = "python_version >= \"3.11\" and extra == \"utils\"" +files = [ + {file = "roman_numerals_py-3.1.0-py3-none-any.whl", hash = "sha256:9da2ad2fb670bcf24e81070ceb3be72f6c11c440d73bd579fbeca1e9f330954c"}, + {file = "roman_numerals_py-3.1.0.tar.gz", hash = "sha256:be4bf804f083a4ce001b5eb7e3c0862479d10f94c936f6c4e5f250aa5ff5bd2d"}, +] + +[package.extras] +lint = ["mypy (==1.15.0)", "pyright (==1.1.394)", "ruff (==0.9.7)"] +test = ["pytest (>=8)"] + [[package]] name = "rpds-py" version = "0.20.1" @@ -5189,6 +7205,7 @@ description = "Python bindings to Rust's persistent data structures (rpds)" optional = false python-versions = ">=3.8" groups = ["main"] +markers = "python_full_version == \"3.8.*\" or platform_python_implementation == \"PyPy\" and python_version < \"3.10\"" files = [ {file = "rpds_py-0.20.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:a649dfd735fff086e8a9d0503a9f0c7d01b7912a333c7ae77e1515c08c146dad"}, {file = "rpds_py-0.20.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f16bc1334853e91ddaaa1217045dd7be166170beec337576818461268a3de67f"}, @@ -5295,6 +7312,172 @@ files = [ {file = "rpds_py-0.20.1.tar.gz", hash = "sha256:e1791c4aabd117653530dccd24108fa03cc6baf21f58b950d0a73c3b3b29a350"}, ] +[[package]] +name = "rpds-py" +version = "0.27.1" +description = "Python bindings to Rust's persistent data structures (rpds)" +optional = false +python-versions = ">=3.9" +groups = ["main"] +markers = "python_version >= \"3.9\" and platform_python_implementation != \"PyPy\" or python_version >= \"3.10\"" +files = [ + {file = "rpds_py-0.27.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:68afeec26d42ab3b47e541b272166a0b4400313946871cba3ed3a4fc0cab1cef"}, + {file = "rpds_py-0.27.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:74e5b2f7bb6fa38b1b10546d27acbacf2a022a8b5543efb06cfebc72a59c85be"}, + {file = "rpds_py-0.27.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9024de74731df54546fab0bfbcdb49fae19159ecaecfc8f37c18d2c7e2c0bd61"}, + {file = "rpds_py-0.27.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:31d3ebadefcd73b73928ed0b2fd696f7fefda8629229f81929ac9c1854d0cffb"}, + {file = "rpds_py-0.27.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b2e7f8f169d775dd9092a1743768d771f1d1300453ddfe6325ae3ab5332b4657"}, + {file = "rpds_py-0.27.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3d905d16f77eb6ab2e324e09bfa277b4c8e5e6b8a78a3e7ff8f3cdf773b4c013"}, + {file = "rpds_py-0.27.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:50c946f048209e6362e22576baea09193809f87687a95a8db24e5fbdb307b93a"}, + {file = "rpds_py-0.27.1-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:3deab27804d65cd8289eb814c2c0e807c4b9d9916c9225e363cb0cf875eb67c1"}, + {file = "rpds_py-0.27.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8b61097f7488de4be8244c89915da8ed212832ccf1e7c7753a25a394bf9b1f10"}, + {file = "rpds_py-0.27.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:8a3f29aba6e2d7d90528d3c792555a93497fe6538aa65eb675b44505be747808"}, + {file = "rpds_py-0.27.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:dd6cd0485b7d347304067153a6dc1d73f7d4fd995a396ef32a24d24b8ac63ac8"}, + {file = "rpds_py-0.27.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:6f4461bf931108c9fa226ffb0e257c1b18dc2d44cd72b125bec50ee0ab1248a9"}, + {file = "rpds_py-0.27.1-cp310-cp310-win32.whl", hash = "sha256:ee5422d7fb21f6a00c1901bf6559c49fee13a5159d0288320737bbf6585bd3e4"}, + {file = "rpds_py-0.27.1-cp310-cp310-win_amd64.whl", hash = "sha256:3e039aabf6d5f83c745d5f9a0a381d031e9ed871967c0a5c38d201aca41f3ba1"}, + {file = "rpds_py-0.27.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:be898f271f851f68b318872ce6ebebbc62f303b654e43bf72683dbdc25b7c881"}, + {file = "rpds_py-0.27.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:62ac3d4e3e07b58ee0ddecd71d6ce3b1637de2d373501412df395a0ec5f9beb5"}, + {file = "rpds_py-0.27.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4708c5c0ceb2d034f9991623631d3d23cb16e65c83736ea020cdbe28d57c0a0e"}, + {file = "rpds_py-0.27.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:abfa1171a9952d2e0002aba2ad3780820b00cc3d9c98c6630f2e93271501f66c"}, + {file = "rpds_py-0.27.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4b507d19f817ebaca79574b16eb2ae412e5c0835542c93fe9983f1e432aca195"}, + {file = "rpds_py-0.27.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:168b025f8fd8d8d10957405f3fdcef3dc20f5982d398f90851f4abc58c566c52"}, + {file = "rpds_py-0.27.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cb56c6210ef77caa58e16e8c17d35c63fe3f5b60fd9ba9d424470c3400bcf9ed"}, + {file = "rpds_py-0.27.1-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:d252f2d8ca0195faa707f8eb9368955760880b2b42a8ee16d382bf5dd807f89a"}, + {file = "rpds_py-0.27.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6e5e54da1e74b91dbc7996b56640f79b195d5925c2b78efaa8c5d53e1d88edde"}, + {file = "rpds_py-0.27.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ffce0481cc6e95e5b3f0a47ee17ffbd234399e6d532f394c8dce320c3b089c21"}, + {file = "rpds_py-0.27.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:a205fdfe55c90c2cd8e540ca9ceba65cbe6629b443bc05db1f590a3db8189ff9"}, + {file = "rpds_py-0.27.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:689fb5200a749db0415b092972e8eba85847c23885c8543a8b0f5c009b1a5948"}, + {file = "rpds_py-0.27.1-cp311-cp311-win32.whl", hash = "sha256:3182af66048c00a075010bc7f4860f33913528a4b6fc09094a6e7598e462fe39"}, + {file = "rpds_py-0.27.1-cp311-cp311-win_amd64.whl", hash = "sha256:b4938466c6b257b2f5c4ff98acd8128ec36b5059e5c8f8372d79316b1c36bb15"}, + {file = "rpds_py-0.27.1-cp311-cp311-win_arm64.whl", hash = "sha256:2f57af9b4d0793e53266ee4325535a31ba48e2f875da81a9177c9926dfa60746"}, + {file = "rpds_py-0.27.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:ae2775c1973e3c30316892737b91f9283f9908e3cc7625b9331271eaaed7dc90"}, + {file = "rpds_py-0.27.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2643400120f55c8a96f7c9d858f7be0c88d383cd4653ae2cf0d0c88f668073e5"}, + {file = "rpds_py-0.27.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:16323f674c089b0360674a4abd28d5042947d54ba620f72514d69be4ff64845e"}, + {file = "rpds_py-0.27.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9a1f4814b65eacac94a00fc9a526e3fdafd78e439469644032032d0d63de4881"}, + {file = "rpds_py-0.27.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7ba32c16b064267b22f1850a34051121d423b6f7338a12b9459550eb2096e7ec"}, + {file = "rpds_py-0.27.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e5c20f33fd10485b80f65e800bbe5f6785af510b9f4056c5a3c612ebc83ba6cb"}, + {file = "rpds_py-0.27.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:466bfe65bd932da36ff279ddd92de56b042f2266d752719beb97b08526268ec5"}, + {file = "rpds_py-0.27.1-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:41e532bbdcb57c92ba3be62c42e9f096431b4cf478da9bc3bc6ce5c38ab7ba7a"}, + {file = "rpds_py-0.27.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f149826d742b406579466283769a8ea448eed82a789af0ed17b0cd5770433444"}, + {file = "rpds_py-0.27.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:80c60cfb5310677bd67cb1e85a1e8eb52e12529545441b43e6f14d90b878775a"}, + {file = "rpds_py-0.27.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:7ee6521b9baf06085f62ba9c7a3e5becffbc32480d2f1b351559c001c38ce4c1"}, + {file = "rpds_py-0.27.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a512c8263249a9d68cac08b05dd59d2b3f2061d99b322813cbcc14c3c7421998"}, + {file = "rpds_py-0.27.1-cp312-cp312-win32.whl", hash = "sha256:819064fa048ba01b6dadc5116f3ac48610435ac9a0058bbde98e569f9e785c39"}, + {file = "rpds_py-0.27.1-cp312-cp312-win_amd64.whl", hash = "sha256:d9199717881f13c32c4046a15f024971a3b78ad4ea029e8da6b86e5aa9cf4594"}, + {file = "rpds_py-0.27.1-cp312-cp312-win_arm64.whl", hash = "sha256:33aa65b97826a0e885ef6e278fbd934e98cdcfed80b63946025f01e2f5b29502"}, + {file = "rpds_py-0.27.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:e4b9fcfbc021633863a37e92571d6f91851fa656f0180246e84cbd8b3f6b329b"}, + {file = "rpds_py-0.27.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1441811a96eadca93c517d08df75de45e5ffe68aa3089924f963c782c4b898cf"}, + {file = "rpds_py-0.27.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:55266dafa22e672f5a4f65019015f90336ed31c6383bd53f5e7826d21a0e0b83"}, + {file = "rpds_py-0.27.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d78827d7ac08627ea2c8e02c9e5b41180ea5ea1f747e9db0915e3adf36b62dcf"}, + {file = "rpds_py-0.27.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ae92443798a40a92dc5f0b01d8a7c93adde0c4dc965310a29ae7c64d72b9fad2"}, + {file = "rpds_py-0.27.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c46c9dd2403b66a2a3b9720ec4b74d4ab49d4fabf9f03dfdce2d42af913fe8d0"}, + {file = "rpds_py-0.27.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2efe4eb1d01b7f5f1939f4ef30ecea6c6b3521eec451fb93191bf84b2a522418"}, + {file = "rpds_py-0.27.1-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:15d3b4d83582d10c601f481eca29c3f138d44c92187d197aff663a269197c02d"}, + {file = "rpds_py-0.27.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4ed2e16abbc982a169d30d1a420274a709949e2cbdef119fe2ec9d870b42f274"}, + {file = "rpds_py-0.27.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a75f305c9b013289121ec0f1181931975df78738cdf650093e6b86d74aa7d8dd"}, + {file = "rpds_py-0.27.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:67ce7620704745881a3d4b0ada80ab4d99df390838839921f99e63c474f82cf2"}, + {file = "rpds_py-0.27.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9d992ac10eb86d9b6f369647b6a3f412fc0075cfd5d799530e84d335e440a002"}, + {file = "rpds_py-0.27.1-cp313-cp313-win32.whl", hash = "sha256:4f75e4bd8ab8db624e02c8e2fc4063021b58becdbe6df793a8111d9343aec1e3"}, + {file = "rpds_py-0.27.1-cp313-cp313-win_amd64.whl", hash = "sha256:f9025faafc62ed0b75a53e541895ca272815bec18abe2249ff6501c8f2e12b83"}, + {file = "rpds_py-0.27.1-cp313-cp313-win_arm64.whl", hash = "sha256:ed10dc32829e7d222b7d3b93136d25a406ba9788f6a7ebf6809092da1f4d279d"}, + {file = "rpds_py-0.27.1-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:92022bbbad0d4426e616815b16bc4127f83c9a74940e1ccf3cfe0b387aba0228"}, + {file = "rpds_py-0.27.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:47162fdab9407ec3f160805ac3e154df042e577dd53341745fc7fb3f625e6d92"}, + {file = "rpds_py-0.27.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb89bec23fddc489e5d78b550a7b773557c9ab58b7946154a10a6f7a214a48b2"}, + {file = "rpds_py-0.27.1-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e48af21883ded2b3e9eb48cb7880ad8598b31ab752ff3be6457001d78f416723"}, + {file = "rpds_py-0.27.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6f5b7bd8e219ed50299e58551a410b64daafb5017d54bbe822e003856f06a802"}, + {file = "rpds_py-0.27.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:08f1e20bccf73b08d12d804d6e1c22ca5530e71659e6673bce31a6bb71c1e73f"}, + {file = "rpds_py-0.27.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0dc5dceeaefcc96dc192e3a80bbe1d6c410c469e97bdd47494a7d930987f18b2"}, + {file = "rpds_py-0.27.1-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:d76f9cc8665acdc0c9177043746775aa7babbf479b5520b78ae4002d889f5c21"}, + {file = "rpds_py-0.27.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:134fae0e36022edad8290a6661edf40c023562964efea0cc0ec7f5d392d2aaef"}, + {file = "rpds_py-0.27.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:eb11a4f1b2b63337cfd3b4d110af778a59aae51c81d195768e353d8b52f88081"}, + {file = "rpds_py-0.27.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:13e608ac9f50a0ed4faec0e90ece76ae33b34c0e8656e3dceb9a7db994c692cd"}, + {file = "rpds_py-0.27.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dd2135527aa40f061350c3f8f89da2644de26cd73e4de458e79606384f4f68e7"}, + {file = "rpds_py-0.27.1-cp313-cp313t-win32.whl", hash = "sha256:3020724ade63fe320a972e2ffd93b5623227e684315adce194941167fee02688"}, + {file = "rpds_py-0.27.1-cp313-cp313t-win_amd64.whl", hash = "sha256:8ee50c3e41739886606388ba3ab3ee2aae9f35fb23f833091833255a31740797"}, + {file = "rpds_py-0.27.1-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:acb9aafccaae278f449d9c713b64a9e68662e7799dbd5859e2c6b3c67b56d334"}, + {file = "rpds_py-0.27.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:b7fb801aa7f845ddf601c49630deeeccde7ce10065561d92729bfe81bd21fb33"}, + {file = "rpds_py-0.27.1-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fe0dd05afb46597b9a2e11c351e5e4283c741237e7f617ffb3252780cca9336a"}, + {file = "rpds_py-0.27.1-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b6dfb0e058adb12d8b1d1b25f686e94ffa65d9995a5157afe99743bf7369d62b"}, + {file = "rpds_py-0.27.1-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ed090ccd235f6fa8bb5861684567f0a83e04f52dfc2e5c05f2e4b1309fcf85e7"}, + {file = "rpds_py-0.27.1-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bf876e79763eecf3e7356f157540d6a093cef395b65514f17a356f62af6cc136"}, + {file = "rpds_py-0.27.1-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:12ed005216a51b1d6e2b02a7bd31885fe317e45897de81d86dcce7d74618ffff"}, + {file = "rpds_py-0.27.1-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:ee4308f409a40e50593c7e3bb8cbe0b4d4c66d1674a316324f0c2f5383b486f9"}, + {file = "rpds_py-0.27.1-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0b08d152555acf1f455154d498ca855618c1378ec810646fcd7c76416ac6dc60"}, + {file = "rpds_py-0.27.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:dce51c828941973a5684d458214d3a36fcd28da3e1875d659388f4f9f12cc33e"}, + {file = "rpds_py-0.27.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:c1476d6f29eb81aa4151c9a31219b03f1f798dc43d8af1250a870735516a1212"}, + {file = "rpds_py-0.27.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:3ce0cac322b0d69b63c9cdb895ee1b65805ec9ffad37639f291dd79467bee675"}, + {file = "rpds_py-0.27.1-cp314-cp314-win32.whl", hash = "sha256:dfbfac137d2a3d0725758cd141f878bf4329ba25e34979797c89474a89a8a3a3"}, + {file = "rpds_py-0.27.1-cp314-cp314-win_amd64.whl", hash = "sha256:a6e57b0abfe7cc513450fcf529eb486b6e4d3f8aee83e92eb5f1ef848218d456"}, + {file = "rpds_py-0.27.1-cp314-cp314-win_arm64.whl", hash = "sha256:faf8d146f3d476abfee026c4ae3bdd9ca14236ae4e4c310cbd1cf75ba33d24a3"}, + {file = "rpds_py-0.27.1-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:ba81d2b56b6d4911ce735aad0a1d4495e808b8ee4dc58715998741a26874e7c2"}, + {file = "rpds_py-0.27.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:84f7d509870098de0e864cad0102711c1e24e9b1a50ee713b65928adb22269e4"}, + {file = "rpds_py-0.27.1-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a9e960fc78fecd1100539f14132425e1d5fe44ecb9239f8f27f079962021523e"}, + {file = "rpds_py-0.27.1-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:62f85b665cedab1a503747617393573995dac4600ff51869d69ad2f39eb5e817"}, + {file = "rpds_py-0.27.1-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fed467af29776f6556250c9ed85ea5a4dd121ab56a5f8b206e3e7a4c551e48ec"}, + {file = "rpds_py-0.27.1-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f2729615f9d430af0ae6b36cf042cb55c0936408d543fb691e1a9e36648fd35a"}, + {file = "rpds_py-0.27.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1b207d881a9aef7ba753d69c123a35d96ca7cb808056998f6b9e8747321f03b8"}, + {file = "rpds_py-0.27.1-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:639fd5efec029f99b79ae47e5d7e00ad8a773da899b6309f6786ecaf22948c48"}, + {file = "rpds_py-0.27.1-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fecc80cb2a90e28af8a9b366edacf33d7a91cbfe4c2c4544ea1246e949cfebeb"}, + {file = "rpds_py-0.27.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:42a89282d711711d0a62d6f57d81aa43a1368686c45bc1c46b7f079d55692734"}, + {file = "rpds_py-0.27.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:cf9931f14223de59551ab9d38ed18d92f14f055a5f78c1d8ad6493f735021bbb"}, + {file = "rpds_py-0.27.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f39f58a27cc6e59f432b568ed8429c7e1641324fbe38131de852cd77b2d534b0"}, + {file = "rpds_py-0.27.1-cp314-cp314t-win32.whl", hash = "sha256:d5fa0ee122dc09e23607a28e6d7b150da16c662e66409bbe85230e4c85bb528a"}, + {file = "rpds_py-0.27.1-cp314-cp314t-win_amd64.whl", hash = "sha256:6567d2bb951e21232c2f660c24cf3470bb96de56cdcb3f071a83feeaff8a2772"}, + {file = "rpds_py-0.27.1-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:c918c65ec2e42c2a78d19f18c553d77319119bf43aa9e2edf7fb78d624355527"}, + {file = "rpds_py-0.27.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1fea2b1a922c47c51fd07d656324531adc787e415c8b116530a1d29c0516c62d"}, + {file = "rpds_py-0.27.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bbf94c58e8e0cd6b6f38d8de67acae41b3a515c26169366ab58bdca4a6883bb8"}, + {file = "rpds_py-0.27.1-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c2a8fed130ce946d5c585eddc7c8eeef0051f58ac80a8ee43bd17835c144c2cc"}, + {file = "rpds_py-0.27.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:037a2361db72ee98d829bc2c5b7cc55598ae0a5e0ec1823a56ea99374cfd73c1"}, + {file = "rpds_py-0.27.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5281ed1cc1d49882f9997981c88df1a22e140ab41df19071222f7e5fc4e72125"}, + {file = "rpds_py-0.27.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2fd50659a069c15eef8aa3d64bbef0d69fd27bb4a50c9ab4f17f83a16cbf8905"}, + {file = "rpds_py-0.27.1-cp39-cp39-manylinux_2_31_riscv64.whl", hash = "sha256:c4b676c4ae3921649a15d28ed10025548e9b561ded473aa413af749503c6737e"}, + {file = "rpds_py-0.27.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:079bc583a26db831a985c5257797b2b5d3affb0386e7ff886256762f82113b5e"}, + {file = "rpds_py-0.27.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:4e44099bd522cba71a2c6b97f68e19f40e7d85399de899d66cdb67b32d7cb786"}, + {file = "rpds_py-0.27.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:e202e6d4188e53c6661af813b46c37ca2c45e497fc558bacc1a7630ec2695aec"}, + {file = "rpds_py-0.27.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:f41f814b8eaa48768d1bb551591f6ba45f87ac76899453e8ccd41dba1289b04b"}, + {file = "rpds_py-0.27.1-cp39-cp39-win32.whl", hash = "sha256:9e71f5a087ead99563c11fdaceee83ee982fd39cf67601f4fd66cb386336ee52"}, + {file = "rpds_py-0.27.1-cp39-cp39-win_amd64.whl", hash = "sha256:71108900c9c3c8590697244b9519017a400d9ba26a36c48381b3f64743a44aab"}, + {file = "rpds_py-0.27.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:7ba22cb9693df986033b91ae1d7a979bc399237d45fccf875b76f62bb9e52ddf"}, + {file = "rpds_py-0.27.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:5b640501be9288c77738b5492b3fd3abc4ba95c50c2e41273c8a1459f08298d3"}, + {file = "rpds_py-0.27.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb08b65b93e0c6dd70aac7f7890a9c0938d5ec71d5cb32d45cf844fb8ae47636"}, + {file = "rpds_py-0.27.1-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d7ff07d696a7a38152ebdb8212ca9e5baab56656749f3d6004b34ab726b550b8"}, + {file = "rpds_py-0.27.1-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fb7c72262deae25366e3b6c0c0ba46007967aea15d1eea746e44ddba8ec58dcc"}, + {file = "rpds_py-0.27.1-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7b002cab05d6339716b03a4a3a2ce26737f6231d7b523f339fa061d53368c9d8"}, + {file = "rpds_py-0.27.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:23f6b69d1c26c4704fec01311963a41d7de3ee0570a84ebde4d544e5a1859ffc"}, + {file = "rpds_py-0.27.1-pp310-pypy310_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:530064db9146b247351f2a0250b8f00b289accea4596a033e94be2389977de71"}, + {file = "rpds_py-0.27.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7b90b0496570bd6b0321724a330d8b545827c4df2034b6ddfc5f5275f55da2ad"}, + {file = "rpds_py-0.27.1-pp310-pypy310_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:879b0e14a2da6a1102a3fc8af580fc1ead37e6d6692a781bd8c83da37429b5ab"}, + {file = "rpds_py-0.27.1-pp310-pypy310_pp73-musllinux_1_2_i686.whl", hash = "sha256:0d807710df3b5faa66c731afa162ea29717ab3be17bdc15f90f2d9f183da4059"}, + {file = "rpds_py-0.27.1-pp310-pypy310_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:3adc388fc3afb6540aec081fa59e6e0d3908722771aa1e37ffe22b220a436f0b"}, + {file = "rpds_py-0.27.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:c796c0c1cc68cb08b0284db4229f5af76168172670c74908fdbd4b7d7f515819"}, + {file = "rpds_py-0.27.1-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:cdfe4bb2f9fe7458b7453ad3c33e726d6d1c7c0a72960bcc23800d77384e42df"}, + {file = "rpds_py-0.27.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:8fabb8fd848a5f75a2324e4a84501ee3a5e3c78d8603f83475441866e60b94a3"}, + {file = "rpds_py-0.27.1-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eda8719d598f2f7f3e0f885cba8646644b55a187762bec091fa14a2b819746a9"}, + {file = "rpds_py-0.27.1-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3c64d07e95606ec402a0a1c511fe003873fa6af630bda59bac77fac8b4318ebc"}, + {file = "rpds_py-0.27.1-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:93a2ed40de81bcff59aabebb626562d48332f3d028ca2036f1d23cbb52750be4"}, + {file = "rpds_py-0.27.1-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:387ce8c44ae94e0ec50532d9cb0edce17311024c9794eb196b90e1058aadeb66"}, + {file = "rpds_py-0.27.1-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aaf94f812c95b5e60ebaf8bfb1898a7d7cb9c1af5744d4a67fa47796e0465d4e"}, + {file = "rpds_py-0.27.1-pp311-pypy311_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:4848ca84d6ded9b58e474dfdbad4b8bfb450344c0551ddc8d958bf4b36aa837c"}, + {file = "rpds_py-0.27.1-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2bde09cbcf2248b73c7c323be49b280180ff39fadcfe04e7b6f54a678d02a7cf"}, + {file = "rpds_py-0.27.1-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:94c44ee01fd21c9058f124d2d4f0c9dc7634bec93cd4b38eefc385dabe71acbf"}, + {file = "rpds_py-0.27.1-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:df8b74962e35c9249425d90144e721eed198e6555a0e22a563d29fe4486b51f6"}, + {file = "rpds_py-0.27.1-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:dc23e6820e3b40847e2f4a7726462ba0cf53089512abe9ee16318c366494c17a"}, + {file = "rpds_py-0.27.1-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:aa8933159edc50be265ed22b401125c9eebff3171f570258854dbce3ecd55475"}, + {file = "rpds_py-0.27.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:a50431bf02583e21bf273c71b89d710e7a710ad5e39c725b14e685610555926f"}, + {file = "rpds_py-0.27.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:78af06ddc7fe5cc0e967085a9115accee665fb912c22a3f54bad70cc65b05fe6"}, + {file = "rpds_py-0.27.1-pp39-pypy39_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:70d0738ef8fee13c003b100c2fbd667ec4f133468109b3472d249231108283a3"}, + {file = "rpds_py-0.27.1-pp39-pypy39_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e2f6fd8a1cea5bbe599b6e78a6e5ee08db434fc8ffea51ff201c8765679698b3"}, + {file = "rpds_py-0.27.1-pp39-pypy39_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8177002868d1426305bb5de1e138161c2ec9eb2d939be38291d7c431c4712df8"}, + {file = "rpds_py-0.27.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:008b839781d6c9bf3b6a8984d1d8e56f0ec46dc56df61fd669c49b58ae800400"}, + {file = "rpds_py-0.27.1-pp39-pypy39_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:a55b9132bb1ade6c734ddd2759c8dc132aa63687d259e725221f106b83a0e485"}, + {file = "rpds_py-0.27.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a46fdec0083a26415f11d5f236b79fa1291c32aaa4a17684d82f7017a1f818b1"}, + {file = "rpds_py-0.27.1-pp39-pypy39_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:8a63b640a7845f2bdd232eb0d0a4a2dd939bcdd6c57e6bb134526487f3160ec5"}, + {file = "rpds_py-0.27.1-pp39-pypy39_pp73-musllinux_1_2_i686.whl", hash = "sha256:7e32721e5d4922deaaf963469d795d5bde6093207c52fec719bd22e5d1bedbc4"}, + {file = "rpds_py-0.27.1-pp39-pypy39_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:2c426b99a068601b5f4623573df7a7c3d72e87533a2dd2253353a03e7502566c"}, + {file = "rpds_py-0.27.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:4fc9b7fe29478824361ead6e14e4f5aed570d477e06088826537e202d25fe859"}, + {file = "rpds_py-0.27.1.tar.gz", hash = "sha256:26a1c73171d10b7acccbded82bf6a586ab8203601e565badc74bbbf8bc5a10f8"}, +] + [[package]] name = "rq" version = "2.3.3" @@ -5302,7 +7485,7 @@ description = "RQ is a simple, lightweight, library for creating background jobs optional = true python-versions = ">=3.8" groups = ["main"] -markers = "extra == \"proxy\"" +markers = "(python_full_version == \"3.8.*\" or platform_python_implementation == \"PyPy\") and extra == \"proxy\" and python_version < \"3.10\"" files = [ {file = "rq-2.3.3-py3-none-any.whl", hash = "sha256:2202c4409c4c527ac4bee409867d6c02515dd110030499eb0de54c7374aee0ce"}, {file = "rq-2.3.3.tar.gz", hash = "sha256:20c41c977b6f27c852a41bd855893717402bae7b8d9607dca21fe9dd55453e22"}, @@ -5312,6 +7495,24 @@ files = [ click = ">=5" redis = ">=3.5,<6.0.0 || >6.0.0" +[[package]] +name = "rq" +version = "2.6.0" +description = "RQ is a simple, lightweight, library for creating background jobs, and processing them." +optional = true +python-versions = ">=3.9" +groups = ["main"] +markers = "python_version >= \"3.9\" and (python_version >= \"3.10\" or platform_python_implementation != \"PyPy\") and extra == \"proxy\"" +files = [ + {file = "rq-2.6.0-py3-none-any.whl", hash = "sha256:be5ccc0f0fc5f32da0999648340e31476368f08067f0c3fce6768d00064edbb5"}, + {file = "rq-2.6.0.tar.gz", hash = "sha256:92ad55676cda14512c4eea5782f398a102dc3af108bea197c868c4c50c5d3e81"}, +] + +[package.dependencies] +click = ">=5" +croniter = "*" +redis = ">=3.5,<6 || >6" + [[package]] name = "rsa" version = "4.9.1" @@ -5319,7 +7520,7 @@ description = "Pure-Python RSA implementation" optional = true python-versions = "<4,>=3.6" groups = ["main"] -markers = "python_version >= \"3.10\" and (extra == \"mlflow\" or extra == \"extra-proxy\") or extra == \"extra-proxy\"" +markers = "(extra == \"mlflow\" or extra == \"extra-proxy\") and python_version >= \"3.10\" or extra == \"extra-proxy\"" files = [ {file = "rsa-4.9.1-py3-none-any.whl", hash = "sha256:68635866661c6836b8d39430f97a996acbd61bfa49406748ea243539fe239762"}, {file = "rsa-4.9.1.tar.gz", hash = "sha256:e7bdbfdb5497da4c07dfd35530e1a902659db6ff241e39d9953cad06ebd0ae75"}, @@ -5438,7 +7639,7 @@ description = "Fundamental algorithms for scientific computing in Python" optional = true python-versions = ">=3.10" groups = ["main"] -markers = "python_version >= \"3.10\" and extra == \"mlflow\"" +markers = "python_version == \"3.10\" and extra == \"mlflow\"" files = [ {file = "scipy-1.15.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:a345928c86d535060c9c2b25e71e87c39ab2f22fc96e9636bd74d1dbf9de448c"}, {file = "scipy-1.15.3-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:ad3432cb0f9ed87477a8d97f03b763fd1d57709f1bbde3c9369b1dff5503b253"}, @@ -5496,6 +7697,86 @@ dev = ["cython-lint (>=0.12.2)", "doit (>=0.36.0)", "mypy (==1.10.0)", "pycodest doc = ["intersphinx_registry", "jupyterlite-pyodide-kernel", "jupyterlite-sphinx (>=0.19.1)", "jupytext", "matplotlib (>=3.5)", "myst-nb", "numpydoc", "pooch", "pydata-sphinx-theme (>=0.15.2)", "sphinx (>=5.0.0,<8.0.0)", "sphinx-copybutton", "sphinx-design (>=0.4.0)"] test = ["Cython", "array-api-strict (>=2.0,<2.1.1)", "asv", "gmpy2", "hypothesis (>=6.30)", "meson", "mpmath", "ninja ; sys_platform != \"emscripten\"", "pooch", "pytest", "pytest-cov", "pytest-timeout", "pytest-xdist", "scikit-umfpack", "threadpoolctl"] +[[package]] +name = "scipy" +version = "1.16.2" +description = "Fundamental algorithms for scientific computing in Python" +optional = true +python-versions = ">=3.11" +groups = ["main"] +markers = "python_version >= \"3.11\" and extra == \"mlflow\"" +files = [ + {file = "scipy-1.16.2-cp311-cp311-macosx_10_14_x86_64.whl", hash = "sha256:6ab88ea43a57da1af33292ebd04b417e8e2eaf9d5aa05700be8d6e1b6501cd92"}, + {file = "scipy-1.16.2-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:c95e96c7305c96ede73a7389f46ccd6c659c4da5ef1b2789466baeaed3622b6e"}, + {file = "scipy-1.16.2-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:87eb178db04ece7c698220d523c170125dbffebb7af0345e66c3554f6f60c173"}, + {file = "scipy-1.16.2-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:4e409eac067dcee96a57fbcf424c13f428037827ec7ee3cb671ff525ca4fc34d"}, + {file = "scipy-1.16.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e574be127bb760f0dad24ff6e217c80213d153058372362ccb9555a10fc5e8d2"}, + {file = "scipy-1.16.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f5db5ba6188d698ba7abab982ad6973265b74bb40a1efe1821b58c87f73892b9"}, + {file = "scipy-1.16.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ec6e74c4e884104ae006d34110677bfe0098203a3fec2f3faf349f4cb05165e3"}, + {file = "scipy-1.16.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:912f46667d2d3834bc3d57361f854226475f695eb08c08a904aadb1c936b6a88"}, + {file = "scipy-1.16.2-cp311-cp311-win_amd64.whl", hash = "sha256:91e9e8a37befa5a69e9cacbe0bcb79ae5afb4a0b130fd6db6ee6cc0d491695fa"}, + {file = "scipy-1.16.2-cp311-cp311-win_arm64.whl", hash = "sha256:f3bf75a6dcecab62afde4d1f973f1692be013110cad5338007927db8da73249c"}, + {file = "scipy-1.16.2-cp312-cp312-macosx_10_14_x86_64.whl", hash = "sha256:89d6c100fa5c48472047632e06f0876b3c4931aac1f4291afc81a3644316bb0d"}, + {file = "scipy-1.16.2-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:ca748936cd579d3f01928b30a17dc474550b01272d8046e3e1ee593f23620371"}, + {file = "scipy-1.16.2-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:fac4f8ce2ddb40e2e3d0f7ec36d2a1e7f92559a2471e59aec37bd8d9de01fec0"}, + {file = "scipy-1.16.2-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:033570f1dcefd79547a88e18bccacff025c8c647a330381064f561d43b821232"}, + {file = "scipy-1.16.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ea3421209bf00c8a5ef2227de496601087d8f638a2363ee09af059bd70976dc1"}, + {file = "scipy-1.16.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f66bd07ba6f84cd4a380b41d1bf3c59ea488b590a2ff96744845163309ee8e2f"}, + {file = "scipy-1.16.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5e9feab931bd2aea4a23388c962df6468af3d808ddf2d40f94a81c5dc38f32ef"}, + {file = "scipy-1.16.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:03dfc75e52f72cf23ec2ced468645321407faad8f0fe7b1f5b49264adbc29cb1"}, + {file = "scipy-1.16.2-cp312-cp312-win_amd64.whl", hash = "sha256:0ce54e07bbb394b417457409a64fd015be623f36e330ac49306433ffe04bc97e"}, + {file = "scipy-1.16.2-cp312-cp312-win_arm64.whl", hash = "sha256:2a8ffaa4ac0df81a0b94577b18ee079f13fecdb924df3328fc44a7dc5ac46851"}, + {file = "scipy-1.16.2-cp313-cp313-macosx_10_14_x86_64.whl", hash = "sha256:84f7bf944b43e20b8a894f5fe593976926744f6c185bacfcbdfbb62736b5cc70"}, + {file = "scipy-1.16.2-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:5c39026d12edc826a1ef2ad35ad1e6d7f087f934bb868fc43fa3049c8b8508f9"}, + {file = "scipy-1.16.2-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:e52729ffd45b68777c5319560014d6fd251294200625d9d70fd8626516fc49f5"}, + {file = "scipy-1.16.2-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:024dd4a118cccec09ca3209b7e8e614931a6ffb804b2a601839499cb88bdf925"}, + {file = "scipy-1.16.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7a5dc7ee9c33019973a470556081b0fd3c9f4c44019191039f9769183141a4d9"}, + {file = "scipy-1.16.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c2275ff105e508942f99d4e3bc56b6ef5e4b3c0af970386ca56b777608ce95b7"}, + {file = "scipy-1.16.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:af80196eaa84f033e48444d2e0786ec47d328ba00c71e4299b602235ffef9acb"}, + {file = "scipy-1.16.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9fb1eb735fe3d6ed1f89918224e3385fbf6f9e23757cacc35f9c78d3b712dd6e"}, + {file = "scipy-1.16.2-cp313-cp313-win_amd64.whl", hash = "sha256:fda714cf45ba43c9d3bae8f2585c777f64e3f89a2e073b668b32ede412d8f52c"}, + {file = "scipy-1.16.2-cp313-cp313-win_arm64.whl", hash = "sha256:2f5350da923ccfd0b00e07c3e5cfb316c1c0d6c1d864c07a72d092e9f20db104"}, + {file = "scipy-1.16.2-cp313-cp313t-macosx_10_14_x86_64.whl", hash = "sha256:53d8d2ee29b925344c13bda64ab51785f016b1b9617849dac10897f0701b20c1"}, + {file = "scipy-1.16.2-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:9e05e33657efb4c6a9d23bd8300101536abd99c85cca82da0bffff8d8764d08a"}, + {file = "scipy-1.16.2-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:7fe65b36036357003b3ef9d37547abeefaa353b237e989c21027b8ed62b12d4f"}, + {file = "scipy-1.16.2-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:6406d2ac6d40b861cccf57f49592f9779071655e9f75cd4f977fa0bdd09cb2e4"}, + {file = "scipy-1.16.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ff4dc42bd321991fbf611c23fc35912d690f731c9914bf3af8f417e64aca0f21"}, + {file = "scipy-1.16.2-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:654324826654d4d9133e10675325708fb954bc84dae6e9ad0a52e75c6b1a01d7"}, + {file = "scipy-1.16.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:63870a84cd15c44e65220eaed2dac0e8f8b26bbb991456a033c1d9abfe8a94f8"}, + {file = "scipy-1.16.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:fa01f0f6a3050fa6a9771a95d5faccc8e2f5a92b4a2e5440a0fa7264a2398472"}, + {file = "scipy-1.16.2-cp313-cp313t-win_amd64.whl", hash = "sha256:116296e89fba96f76353a8579820c2512f6e55835d3fad7780fece04367de351"}, + {file = "scipy-1.16.2-cp313-cp313t-win_arm64.whl", hash = "sha256:98e22834650be81d42982360382b43b17f7ba95e0e6993e2a4f5b9ad9283a94d"}, + {file = "scipy-1.16.2-cp314-cp314-macosx_10_14_x86_64.whl", hash = "sha256:567e77755019bb7461513c87f02bb73fb65b11f049aaaa8ca17cfaa5a5c45d77"}, + {file = "scipy-1.16.2-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:17d9bb346194e8967296621208fcdfd39b55498ef7d2f376884d5ac47cec1a70"}, + {file = "scipy-1.16.2-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:0a17541827a9b78b777d33b623a6dcfe2ef4a25806204d08ead0768f4e529a88"}, + {file = "scipy-1.16.2-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:d7d4c6ba016ffc0f9568d012f5f1eb77ddd99412aea121e6fa8b4c3b7cbad91f"}, + {file = "scipy-1.16.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:9702c4c023227785c779cba2e1d6f7635dbb5b2e0936cdd3a4ecb98d78fd41eb"}, + {file = "scipy-1.16.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d1cdf0ac28948d225decdefcc45ad7dd91716c29ab56ef32f8e0d50657dffcc7"}, + {file = "scipy-1.16.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:70327d6aa572a17c2941cdfb20673f82e536e91850a2e4cb0c5b858b690e1548"}, + {file = "scipy-1.16.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5221c0b2a4b58aa7c4ed0387d360fd90ee9086d383bb34d9f2789fafddc8a936"}, + {file = "scipy-1.16.2-cp314-cp314-win_amd64.whl", hash = "sha256:f5a85d7b2b708025af08f060a496dd261055b617d776fc05a1a1cc69e09fe9ff"}, + {file = "scipy-1.16.2-cp314-cp314-win_arm64.whl", hash = "sha256:2cc73a33305b4b24556957d5857d6253ce1e2dcd67fa0ff46d87d1670b3e1e1d"}, + {file = "scipy-1.16.2-cp314-cp314t-macosx_10_14_x86_64.whl", hash = "sha256:9ea2a3fed83065d77367775d689401a703d0f697420719ee10c0780bcab594d8"}, + {file = "scipy-1.16.2-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:7280d926f11ca945c3ef92ba960fa924e1465f8d07ce3a9923080363390624c4"}, + {file = "scipy-1.16.2-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:8afae1756f6a1fe04636407ef7dbece33d826a5d462b74f3d0eb82deabefd831"}, + {file = "scipy-1.16.2-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:5c66511f29aa8d233388e7416a3f20d5cae7a2744d5cee2ecd38c081f4e861b3"}, + {file = "scipy-1.16.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:efe6305aeaa0e96b0ccca5ff647a43737d9a092064a3894e46c414db84bc54ac"}, + {file = "scipy-1.16.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7f3a337d9ae06a1e8d655ee9d8ecb835ea5ddcdcbd8d23012afa055ab014f374"}, + {file = "scipy-1.16.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:bab3605795d269067d8ce78a910220262711b753de8913d3deeaedb5dded3bb6"}, + {file = "scipy-1.16.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b0348d8ddb55be2a844c518cd8cc8deeeb8aeba707cf834db5758fc89b476a2c"}, + {file = "scipy-1.16.2-cp314-cp314t-win_amd64.whl", hash = "sha256:26284797e38b8a75e14ea6631d29bda11e76ceaa6ddb6fdebbfe4c4d90faf2f9"}, + {file = "scipy-1.16.2-cp314-cp314t-win_arm64.whl", hash = "sha256:d2a4472c231328d4de38d5f1f68fdd6d28a615138f842580a8a321b5845cf779"}, + {file = "scipy-1.16.2.tar.gz", hash = "sha256:af029b153d243a80afb6eabe40b0a07f8e35c9adc269c019f364ad747f826a6b"}, +] + +[package.dependencies] +numpy = ">=1.25.2,<2.6" + +[package.extras] +dev = ["cython-lint (>=0.12.2)", "doit (>=0.36.0)", "mypy (==1.10.0)", "pycodestyle", "pydevtool", "rich-click", "ruff (>=0.0.292)", "types-psutil", "typing_extensions"] +doc = ["intersphinx_registry", "jupyterlite-pyodide-kernel", "jupyterlite-sphinx (>=0.19.1)", "jupytext", "linkify-it-py", "matplotlib (>=3.5)", "myst-nb (>=1.2.0)", "numpydoc", "pooch", "pydata-sphinx-theme (>=0.15.2)", "sphinx (>=5.0.0,<8.2.0)", "sphinx-copybutton", "sphinx-design (>=0.4.0)"] +test = ["Cython", "array-api-strict (>=2.3.1)", "asv", "gmpy2", "hypothesis (>=6.30)", "meson", "mpmath", "ninja ; sys_platform != \"emscripten\"", "pooch", "pytest (>=8.0.0)", "pytest-cov", "pytest-timeout", "pytest-xdist", "scikit-umfpack", "threadpoolctl"] + [[package]] name = "semantic-router" version = "0.0.20" @@ -5503,7 +7784,7 @@ description = "Super fast semantic router for AI decision making" optional = true python-versions = ">=3.9,<4.0" groups = ["main"] -markers = "python_version >= \"3.9\" and extra == \"semantic-router\"" +markers = "python_version >= \"3.13\" and extra == \"semantic-router\"" files = [ {file = "semantic_router-0.0.20-py3-none-any.whl", hash = "sha256:7a713401564fb6cf22b566046ad32a4224e4f357be8de6583ca3b9ee328c8f95"}, {file = "semantic_router-0.0.20.tar.gz", hash = "sha256:26119a4628ca72b2fa9eacd446ea763b6f1925a661a34e26945433d2601efac7"}, @@ -5523,6 +7804,46 @@ fastembed = ["fastembed (>=0.1.3,<0.2.0) ; python_version < \"3.12\""] hybrid = ["pinecone-text (>=0.7.1,<0.8.0)"] local = ["llama-cpp-python (>=0.2.28,<0.3.0)", "torch (>=2.1.0,<3.0.0)", "transformers (>=4.36.2,<5.0.0)"] +[[package]] +name = "semantic-router" +version = "0.0.72" +description = "Super fast semantic router for AI decision making" +optional = true +python-versions = "<3.13,>=3.9" +groups = ["main"] +markers = "python_version >= \"3.9\" and extra == \"semantic-router\" and python_version < \"3.13\"" +files = [ + {file = "semantic_router-0.0.72-py3-none-any.whl", hash = "sha256:4973869859a514f3d94d8c82ef02f4822d833443151611eeb4c732d6111e716b"}, + {file = "semantic_router-0.0.72.tar.gz", hash = "sha256:60c72d61ef7091f6ee70c73dc6416524ca7fb037b7245fc3348d055ee6f141b8"}, +] + +[package.dependencies] +aiohttp = ">=3.9.5,<4.0.0" +colorama = ">=0.4.6,<0.5.0" +colorlog = ">=6.8.0,<7.0.0" +numpy = ">=1.25.2,<2.0.0" +openai = ">=1.10.0,<2.0.0" +pydantic = ">=2.5.3,<3.0.0" +pyyaml = ">=6.0.1,<7.0.0" +regex = ">=2023.12.25" +requests-mock = ">=1.12.1,<2.0.0" +tiktoken = ">=0.6.0,<1.0.0" + +[package.extras] +bedrock = ["boto3 (>=1.34.98,<2.0.0)", "botocore (>=1.34.110,<2.0.0)"] +cohere = ["cohere (>=5.9.4,<6.00)"] +docs = ["sphinx (>=7.0.0,<8.0.0)", "sphinxawesome-theme (>=5.2.0,<6.0.0)"] +fastembed = ["fastembed (>=0.3.0,<0.4.0)"] +google = ["google-cloud-aiplatform (>=1.45.0,<2.0.0)"] +hybrid = ["pinecone-text (>=0.7.1,<0.10.0)"] +local = ["llama-cpp-python (>=0.2.28,<0.2.86)", "tokenizers (>=0.19)", "torch (>=2.1.0,<2.6.0)", "transformers (>=4.36.2)"] +mistralai = ["mistralai (>=0.0.12,<0.1.0)"] +pinecone = ["pinecone (>=5.0.0)"] +postgres = ["psycopg2 (>=2.9.9,<3.0.0)"] +processing = ["matplotlib (>=3.8.3,<4.0.0)"] +qdrant = ["qdrant-client (>=1.11.1,<2.0.0)"] +vision = ["pillow (>=10.2.0,<11.0.0)", "torch (>=2.1.0,<2.6.0)", "torchvision (>=0.17.0,<0.18.0)", "transformers (>=4.36.2)"] + [[package]] name = "six" version = "1.17.0" @@ -5580,7 +7901,7 @@ description = "Python documentation generator" optional = true python-versions = ">=3.8" groups = ["main"] -markers = "extra == \"utils\"" +markers = "(python_full_version == \"3.8.*\" or platform_python_implementation == \"PyPy\") and extra == \"utils\" and python_version < \"3.10\"" files = [ {file = "sphinx-7.1.2-py3-none-any.whl", hash = "sha256:d170a81825b2fcacb6dfd5a0d7f578a053e45d3f2b153fecc948c37344eb4cbe"}, {file = "sphinx-7.1.2.tar.gz", hash = "sha256:780f4d32f1d7d1126576e0e5ecc19dc32ab76cd24e950228dcf7b1f6d3d9e22f"}, @@ -5610,6 +7931,118 @@ docs = ["sphinxcontrib-websupport"] lint = ["docutils-stubs", "flake8 (>=3.5.0)", "flake8-simplify", "isort", "mypy (>=0.990)", "ruff", "sphinx-lint", "types-requests"] test = ["cython", "filelock", "html5lib", "pytest (>=4.6)"] +[[package]] +name = "sphinx" +version = "7.4.7" +description = "Python documentation generator" +optional = true +python-versions = ">=3.9" +groups = ["main"] +markers = "python_full_version >= \"3.9.0\" and platform_python_implementation != \"PyPy\" and extra == \"utils\" and python_version < \"3.10\"" +files = [ + {file = "sphinx-7.4.7-py3-none-any.whl", hash = "sha256:c2419e2135d11f1951cd994d6eb18a1835bd8fdd8429f9ca375dc1f3281bd239"}, + {file = "sphinx-7.4.7.tar.gz", hash = "sha256:242f92a7ea7e6c5b406fdc2615413890ba9f699114a9c09192d7dfead2ee9cfe"}, +] + +[package.dependencies] +alabaster = ">=0.7.14,<0.8.0" +babel = ">=2.13" +colorama = {version = ">=0.4.6", markers = "sys_platform == \"win32\""} +docutils = ">=0.20,<0.22" +imagesize = ">=1.3" +importlib-metadata = {version = ">=6.0", markers = "python_version < \"3.10\""} +Jinja2 = ">=3.1" +packaging = ">=23.0" +Pygments = ">=2.17" +requests = ">=2.30.0" +snowballstemmer = ">=2.2" +sphinxcontrib-applehelp = "*" +sphinxcontrib-devhelp = "*" +sphinxcontrib-htmlhelp = ">=2.0.0" +sphinxcontrib-jsmath = "*" +sphinxcontrib-qthelp = "*" +sphinxcontrib-serializinghtml = ">=1.1.9" +tomli = {version = ">=2", markers = "python_version < \"3.11\""} + +[package.extras] +docs = ["sphinxcontrib-websupport"] +lint = ["flake8 (>=6.0)", "importlib-metadata (>=6.0)", "mypy (==1.10.1)", "pytest (>=6.0)", "ruff (==0.5.2)", "sphinx-lint (>=0.9)", "tomli (>=2)", "types-docutils (==0.21.0.20240711)", "types-requests (>=2.30.0)"] +test = ["cython (>=3.0)", "defusedxml (>=0.7.1)", "pytest (>=8.0)", "setuptools (>=70.0)", "typing_extensions (>=4.9)"] + +[[package]] +name = "sphinx" +version = "8.1.3" +description = "Python documentation generator" +optional = true +python-versions = ">=3.10" +groups = ["main"] +markers = "python_version == \"3.10\" and extra == \"utils\"" +files = [ + {file = "sphinx-8.1.3-py3-none-any.whl", hash = "sha256:09719015511837b76bf6e03e42eb7595ac8c2e41eeb9c29c5b755c6b677992a2"}, + {file = "sphinx-8.1.3.tar.gz", hash = "sha256:43c1911eecb0d3e161ad78611bc905d1ad0e523e4ddc202a58a821773dc4c927"}, +] + +[package.dependencies] +alabaster = ">=0.7.14" +babel = ">=2.13" +colorama = {version = ">=0.4.6", markers = "sys_platform == \"win32\""} +docutils = ">=0.20,<0.22" +imagesize = ">=1.3" +Jinja2 = ">=3.1" +packaging = ">=23.0" +Pygments = ">=2.17" +requests = ">=2.30.0" +snowballstemmer = ">=2.2" +sphinxcontrib-applehelp = ">=1.0.7" +sphinxcontrib-devhelp = ">=1.0.6" +sphinxcontrib-htmlhelp = ">=2.0.6" +sphinxcontrib-jsmath = ">=1.0.1" +sphinxcontrib-qthelp = ">=1.0.6" +sphinxcontrib-serializinghtml = ">=1.1.9" +tomli = {version = ">=2", markers = "python_version < \"3.11\""} + +[package.extras] +docs = ["sphinxcontrib-websupport"] +lint = ["flake8 (>=6.0)", "mypy (==1.11.1)", "pyright (==1.1.384)", "pytest (>=6.0)", "ruff (==0.6.9)", "sphinx-lint (>=0.9)", "tomli (>=2)", "types-Pillow (==10.2.0.20240822)", "types-Pygments (==2.18.0.20240506)", "types-colorama (==0.4.15.20240311)", "types-defusedxml (==0.7.0.20240218)", "types-docutils (==0.21.0.20241005)", "types-requests (==2.32.0.20240914)", "types-urllib3 (==1.26.25.14)"] +test = ["cython (>=3.0)", "defusedxml (>=0.7.1)", "pytest (>=8.0)", "setuptools (>=70.0)", "typing_extensions (>=4.9)"] + +[[package]] +name = "sphinx" +version = "8.2.3" +description = "Python documentation generator" +optional = true +python-versions = ">=3.11" +groups = ["main"] +markers = "python_version >= \"3.11\" and extra == \"utils\"" +files = [ + {file = "sphinx-8.2.3-py3-none-any.whl", hash = "sha256:4405915165f13521d875a8c29c8970800a0141c14cc5416a38feca4ea5d9b9c3"}, + {file = "sphinx-8.2.3.tar.gz", hash = "sha256:398ad29dee7f63a75888314e9424d40f52ce5a6a87ae88e7071e80af296ec348"}, +] + +[package.dependencies] +alabaster = ">=0.7.14" +babel = ">=2.13" +colorama = {version = ">=0.4.6", markers = "sys_platform == \"win32\""} +docutils = ">=0.20,<0.22" +imagesize = ">=1.3" +Jinja2 = ">=3.1" +packaging = ">=23.0" +Pygments = ">=2.17" +requests = ">=2.30.0" +roman-numerals-py = ">=1.0.0" +snowballstemmer = ">=2.2" +sphinxcontrib-applehelp = ">=1.0.7" +sphinxcontrib-devhelp = ">=1.0.6" +sphinxcontrib-htmlhelp = ">=2.0.6" +sphinxcontrib-jsmath = ">=1.0.1" +sphinxcontrib-qthelp = ">=1.0.6" +sphinxcontrib-serializinghtml = ">=1.1.9" + +[package.extras] +docs = ["sphinxcontrib-websupport"] +lint = ["betterproto (==2.0.0b6)", "mypy (==1.15.0)", "pypi-attestations (==0.0.21)", "pyright (==1.1.395)", "pytest (>=8.0)", "ruff (==0.9.9)", "sphinx-lint (>=0.9)", "types-Pillow (==10.2.0.20240822)", "types-Pygments (==2.19.0.20250219)", "types-colorama (==0.4.15.20240311)", "types-defusedxml (==0.7.0.20240218)", "types-docutils (==0.21.0.20241128)", "types-requests (==2.32.0.20241016)", "types-urllib3 (==1.26.25.14)"] +test = ["cython (>=3.0)", "defusedxml (>=0.7.1)", "pytest (>=8.0)", "pytest-xdist[psutil] (>=3.4)", "setuptools (>=70.0)", "typing_extensions (>=4.9)"] + [[package]] name = "sphinxcontrib-applehelp" version = "1.0.4" @@ -5617,7 +8050,7 @@ description = "sphinxcontrib-applehelp is a Sphinx extension which outputs Apple optional = true python-versions = ">=3.8" groups = ["main"] -markers = "extra == \"utils\"" +markers = "(python_full_version == \"3.8.*\" or platform_python_implementation == \"PyPy\") and extra == \"utils\" and python_version < \"3.10\"" files = [ {file = "sphinxcontrib-applehelp-1.0.4.tar.gz", hash = "sha256:828f867945bbe39817c210a1abfd1bc4895c8b73fcaade56d45357a348a07d7e"}, {file = "sphinxcontrib_applehelp-1.0.4-py3-none-any.whl", hash = "sha256:29d341f67fb0f6f586b23ad80e072c8e6ad0b48417db2bde114a4c9746feb228"}, @@ -5627,6 +8060,24 @@ files = [ lint = ["docutils-stubs", "flake8", "mypy"] test = ["pytest"] +[[package]] +name = "sphinxcontrib-applehelp" +version = "2.0.0" +description = "sphinxcontrib-applehelp is a Sphinx extension which outputs Apple help books" +optional = true +python-versions = ">=3.9" +groups = ["main"] +markers = "python_version >= \"3.9\" and (python_version >= \"3.10\" or platform_python_implementation != \"PyPy\") and extra == \"utils\"" +files = [ + {file = "sphinxcontrib_applehelp-2.0.0-py3-none-any.whl", hash = "sha256:4cd3f0ec4ac5dd9c17ec65e9ab272c9b867ea77425228e68ecf08d6b28ddbdb5"}, + {file = "sphinxcontrib_applehelp-2.0.0.tar.gz", hash = "sha256:2f29ef331735ce958efa4734873f084941970894c6090408b079c61b2e1c06d1"}, +] + +[package.extras] +lint = ["mypy", "ruff (==0.5.5)", "types-docutils"] +standalone = ["Sphinx (>=5)"] +test = ["pytest"] + [[package]] name = "sphinxcontrib-devhelp" version = "1.0.2" @@ -5634,7 +8085,7 @@ description = "sphinxcontrib-devhelp is a sphinx extension which outputs Devhelp optional = true python-versions = ">=3.5" groups = ["main"] -markers = "extra == \"utils\"" +markers = "(python_full_version == \"3.8.*\" or platform_python_implementation == \"PyPy\") and extra == \"utils\" and python_version < \"3.10\"" files = [ {file = "sphinxcontrib-devhelp-1.0.2.tar.gz", hash = "sha256:ff7f1afa7b9642e7060379360a67e9c41e8f3121f2ce9164266f61b9f4b338e4"}, {file = "sphinxcontrib_devhelp-1.0.2-py2.py3-none-any.whl", hash = "sha256:8165223f9a335cc1af7ffe1ed31d2871f325254c0423bc0c4c7cd1c1e4734a2e"}, @@ -5644,6 +8095,24 @@ files = [ lint = ["docutils-stubs", "flake8", "mypy"] test = ["pytest"] +[[package]] +name = "sphinxcontrib-devhelp" +version = "2.0.0" +description = "sphinxcontrib-devhelp is a sphinx extension which outputs Devhelp documents" +optional = true +python-versions = ">=3.9" +groups = ["main"] +markers = "python_version >= \"3.9\" and (python_version >= \"3.10\" or platform_python_implementation != \"PyPy\") and extra == \"utils\"" +files = [ + {file = "sphinxcontrib_devhelp-2.0.0-py3-none-any.whl", hash = "sha256:aefb8b83854e4b0998877524d1029fd3e6879210422ee3780459e28a1f03a8a2"}, + {file = "sphinxcontrib_devhelp-2.0.0.tar.gz", hash = "sha256:411f5d96d445d1d73bb5d52133377b4248ec79db5c793ce7dbe59e074b4dd1ad"}, +] + +[package.extras] +lint = ["mypy", "ruff (==0.5.5)", "types-docutils"] +standalone = ["Sphinx (>=5)"] +test = ["pytest"] + [[package]] name = "sphinxcontrib-htmlhelp" version = "2.0.1" @@ -5651,7 +8120,7 @@ description = "sphinxcontrib-htmlhelp is a sphinx extension which renders HTML h optional = true python-versions = ">=3.8" groups = ["main"] -markers = "extra == \"utils\"" +markers = "(python_full_version == \"3.8.*\" or platform_python_implementation == \"PyPy\") and extra == \"utils\" and python_version < \"3.10\"" files = [ {file = "sphinxcontrib-htmlhelp-2.0.1.tar.gz", hash = "sha256:0cbdd302815330058422b98a113195c9249825d681e18f11e8b1f78a2f11efff"}, {file = "sphinxcontrib_htmlhelp-2.0.1-py3-none-any.whl", hash = "sha256:c38cb46dccf316c79de6e5515e1770414b797162b23cd3d06e67020e1d2a6903"}, @@ -5661,6 +8130,24 @@ files = [ lint = ["docutils-stubs", "flake8", "mypy"] test = ["html5lib", "pytest"] +[[package]] +name = "sphinxcontrib-htmlhelp" +version = "2.1.0" +description = "sphinxcontrib-htmlhelp is a sphinx extension which renders HTML help files" +optional = true +python-versions = ">=3.9" +groups = ["main"] +markers = "python_version >= \"3.9\" and (python_version >= \"3.10\" or platform_python_implementation != \"PyPy\") and extra == \"utils\"" +files = [ + {file = "sphinxcontrib_htmlhelp-2.1.0-py3-none-any.whl", hash = "sha256:166759820b47002d22914d64a075ce08f4c46818e17cfc9470a9786b759b19f8"}, + {file = "sphinxcontrib_htmlhelp-2.1.0.tar.gz", hash = "sha256:c9e2916ace8aad64cc13a0d233ee22317f2b9025b9cf3295249fa985cc7082e9"}, +] + +[package.extras] +lint = ["mypy", "ruff (==0.5.5)", "types-docutils"] +standalone = ["Sphinx (>=5)"] +test = ["html5lib", "pytest"] + [[package]] name = "sphinxcontrib-jsmath" version = "1.0.1" @@ -5684,7 +8171,7 @@ description = "sphinxcontrib-qthelp is a sphinx extension which outputs QtHelp d optional = true python-versions = ">=3.5" groups = ["main"] -markers = "extra == \"utils\"" +markers = "(python_full_version == \"3.8.*\" or platform_python_implementation == \"PyPy\") and extra == \"utils\" and python_version < \"3.10\"" files = [ {file = "sphinxcontrib-qthelp-1.0.3.tar.gz", hash = "sha256:4c33767ee058b70dba89a6fc5c1892c0d57a54be67ddd3e7875a18d14cba5a72"}, {file = "sphinxcontrib_qthelp-1.0.3-py2.py3-none-any.whl", hash = "sha256:bd9fc24bcb748a8d51fd4ecaade681350aa63009a347a8c14e637895444dfab6"}, @@ -5694,6 +8181,24 @@ files = [ lint = ["docutils-stubs", "flake8", "mypy"] test = ["pytest"] +[[package]] +name = "sphinxcontrib-qthelp" +version = "2.0.0" +description = "sphinxcontrib-qthelp is a sphinx extension which outputs QtHelp documents" +optional = true +python-versions = ">=3.9" +groups = ["main"] +markers = "python_version >= \"3.9\" and (python_version >= \"3.10\" or platform_python_implementation != \"PyPy\") and extra == \"utils\"" +files = [ + {file = "sphinxcontrib_qthelp-2.0.0-py3-none-any.whl", hash = "sha256:b18a828cdba941ccd6ee8445dbe72ffa3ef8cbe7505d8cd1fa0d42d3f2d5f3eb"}, + {file = "sphinxcontrib_qthelp-2.0.0.tar.gz", hash = "sha256:4fe7d0ac8fc171045be623aba3e2a8f613f8682731f9153bb2e40ece16b9bbab"}, +] + +[package.extras] +lint = ["mypy", "ruff (==0.5.5)", "types-docutils"] +standalone = ["Sphinx (>=5)"] +test = ["defusedxml (>=0.7.1)", "pytest"] + [[package]] name = "sphinxcontrib-serializinghtml" version = "1.1.5" @@ -5701,7 +8206,7 @@ description = "sphinxcontrib-serializinghtml is a sphinx extension which outputs optional = true python-versions = ">=3.5" groups = ["main"] -markers = "extra == \"utils\"" +markers = "(python_full_version == \"3.8.*\" or platform_python_implementation == \"PyPy\") and extra == \"utils\" and python_version < \"3.10\"" files = [ {file = "sphinxcontrib-serializinghtml-1.1.5.tar.gz", hash = "sha256:aa5f6de5dfdf809ef505c4895e51ef5c9eac17d0f287933eb49ec495280b6952"}, {file = "sphinxcontrib_serializinghtml-1.1.5-py2.py3-none-any.whl", hash = "sha256:352a9a00ae864471d3a7ead8d7d79f5fc0b57e8b3f95e9867eb9eb28999b92fd"}, @@ -5711,6 +8216,24 @@ files = [ lint = ["docutils-stubs", "flake8", "mypy"] test = ["pytest"] +[[package]] +name = "sphinxcontrib-serializinghtml" +version = "2.0.0" +description = "sphinxcontrib-serializinghtml is a sphinx extension which outputs \"serialized\" HTML files (json and pickle)" +optional = true +python-versions = ">=3.9" +groups = ["main"] +markers = "python_version >= \"3.9\" and (python_version >= \"3.10\" or platform_python_implementation != \"PyPy\") and extra == \"utils\"" +files = [ + {file = "sphinxcontrib_serializinghtml-2.0.0-py3-none-any.whl", hash = "sha256:6e2cb0eef194e10c27ec0023bfeb25badbbb5868244cf5bc5bdc04e4464bf331"}, + {file = "sphinxcontrib_serializinghtml-2.0.0.tar.gz", hash = "sha256:e9d912827f872c029017a53f0ef2180b327c3f7fd23c87229f7a8e8b70031d4d"}, +] + +[package.extras] +lint = ["mypy", "ruff (==0.5.5)", "types-docutils"] +standalone = ["Sphinx (>=5)"] +test = ["pytest"] + [[package]] name = "sqlalchemy" version = "2.0.43" @@ -5827,24 +8350,25 @@ doc = ["sphinx"] [[package]] name = "sse-starlette" -version = "2.1.3" +version = "3.0.2" description = "SSE plugin for Starlette" optional = true -python-versions = ">=3.8" +python-versions = ">=3.9" groups = ["main"] markers = "python_version >= \"3.10\" and extra == \"proxy\"" files = [ - {file = "sse_starlette-2.1.3-py3-none-any.whl", hash = "sha256:8ec846438b4665b9e8c560fcdea6bc8081a3abf7942faa95e5a744999d219772"}, - {file = "sse_starlette-2.1.3.tar.gz", hash = "sha256:9cd27eb35319e1414e3d2558ee7414487f9529ce3b3cf9b21434fd110e017169"}, + {file = "sse_starlette-3.0.2-py3-none-any.whl", hash = "sha256:16b7cbfddbcd4eaca11f7b586f3b8a080f1afe952c15813455b162edea619e5a"}, + {file = "sse_starlette-3.0.2.tar.gz", hash = "sha256:ccd60b5765ebb3584d0de2d7a6e4f745672581de4f5005ab31c3a25d10b52b3a"}, ] [package.dependencies] -anyio = "*" -starlette = "*" -uvicorn = "*" +anyio = ">=4.7.0" [package.extras] -examples = ["fastapi"] +daphne = ["daphne (>=4.2.0)"] +examples = ["aiosqlite (>=0.21.0)", "fastapi (>=0.115.12)", "sqlalchemy[asyncio] (>=2.0.41)", "starlette (>=0.41.3)", "uvicorn (>=0.34.0)"] +granian = ["granian (>=2.3.1)"] +uvicorn = ["uvicorn (>=0.34.0)"] [[package]] name = "starlette" @@ -5857,10 +8381,31 @@ files = [ {file = "starlette-0.44.0-py3-none-any.whl", hash = "sha256:19edeb75844c16dcd4f9dd72f22f9108c1539f3fc9c4c88885654fef64f85aea"}, {file = "starlette-0.44.0.tar.gz", hash = "sha256:e35166950a3ccccc701962fe0711db0bc14f2ecd37c6f9fe5e3eae0cbaea8715"}, ] -markers = {main = "python_version >= \"3.10\" and (extra == \"mlflow\" or extra == \"proxy\") or extra == \"proxy\""} +markers = {main = "(python_full_version == \"3.8.*\" or platform_python_implementation == \"PyPy\") and extra == \"proxy\" and python_version < \"3.10\"", dev = "python_full_version == \"3.8.*\" or platform_python_implementation == \"PyPy\" and python_version < \"3.10\""} [package.dependencies] anyio = ">=3.4.0,<5" +typing-extensions = {version = ">=3.10.0", markers = "python_version < \"3.10\""} + +[package.extras] +full = ["httpx (>=0.27.0,<0.29.0)", "itsdangerous", "jinja2", "python-multipart (>=0.0.18)", "pyyaml"] + +[[package]] +name = "starlette" +version = "0.46.2" +description = "The little ASGI library that shines." +optional = false +python-versions = ">=3.9" +groups = ["main", "dev"] +files = [ + {file = "starlette-0.46.2-py3-none-any.whl", hash = "sha256:595633ce89f8ffa71a015caed34a5b2dc1c0cdb3f0f1fbd1e69339cf2abeec35"}, + {file = "starlette-0.46.2.tar.gz", hash = "sha256:7f7361f34eed179294600af672f565727419830b54b7b084efe44bb82d2fccd5"}, +] +markers = {main = "(extra == \"mlflow\" or extra == \"proxy\") and python_version >= \"3.10\" or extra == \"proxy\" and platform_python_implementation != \"PyPy\" and python_version >= \"3.9\" or python_version >= \"3.12\" and extra == \"proxy\"", dev = "python_version >= \"3.9\" and platform_python_implementation != \"PyPy\" or python_version >= \"3.10\""} + +[package.dependencies] +anyio = ">=3.6.2,<5" +typing-extensions = {version = ">=3.10.0", markers = "python_version < \"3.10\""} [package.extras] full = ["httpx (>=0.27.0,<0.29.0)", "itsdangerous", "jinja2", "python-multipart (>=0.0.18)", "pyyaml"] @@ -5872,7 +8417,7 @@ description = "Pretty-print tabular data" optional = true python-versions = ">=3.7" groups = ["main"] -markers = "python_version >= \"3.9\" and (extra == \"extra-proxy\" or extra == \"utils\") and python_version < \"3.14\" or extra == \"utils\"" +markers = "(python_version >= \"3.9\" or extra == \"utils\") and python_version < \"3.14\" and (python_version < \"3.10\" or extra == \"extra-proxy\") and (python_full_version == \"3.8.*\" or platform_python_implementation == \"PyPy\" or extra == \"extra-proxy\") and (extra == \"extra-proxy\" or extra == \"utils\")" files = [ {file = "tabulate-0.9.0-py3-none-any.whl", hash = "sha256:024ca478df22e9340661486f85298cff5f6dcdba14f3813e8830015b9ed1948f"}, {file = "tabulate-0.9.0.tar.gz", hash = "sha256:0095b12bf5966de529c0feb1fa08671671b3368eec77d7ef7ab114be2c068b3c"}, @@ -5935,6 +8480,7 @@ description = "tiktoken is a fast BPE tokeniser for use with OpenAI's models" optional = false python-versions = ">=3.8" groups = ["main"] +markers = "python_full_version == \"3.8.*\" or platform_python_implementation == \"PyPy\" and python_version < \"3.10\"" files = [ {file = "tiktoken-0.7.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:485f3cc6aba7c6b6ce388ba634fbba656d9ee27f766216f45146beb4ac18b25f"}, {file = "tiktoken-0.7.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e54be9a2cd2f6d6ffa3517b064983fb695c9a9d8aa7d574d1ef3c3f931a99225"}, @@ -5981,6 +8527,55 @@ requests = ">=2.26.0" [package.extras] blobfile = ["blobfile (>=2)"] +[[package]] +name = "tiktoken" +version = "0.11.0" +description = "tiktoken is a fast BPE tokeniser for use with OpenAI's models" +optional = false +python-versions = ">=3.9" +groups = ["main"] +markers = "python_version >= \"3.9\" and platform_python_implementation != \"PyPy\" or python_version >= \"3.10\"" +files = [ + {file = "tiktoken-0.11.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:8a9b517d6331d7103f8bef29ef93b3cca95fa766e293147fe7bacddf310d5917"}, + {file = "tiktoken-0.11.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b4ddb1849e6bf0afa6cc1c5d809fb980ca240a5fffe585a04e119519758788c0"}, + {file = "tiktoken-0.11.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:10331d08b5ecf7a780b4fe4d0281328b23ab22cdb4ff65e68d56caeda9940ecc"}, + {file = "tiktoken-0.11.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b062c82300341dc87e0258c69f79bed725f87e753c21887aea90d272816be882"}, + {file = "tiktoken-0.11.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:195d84bec46169af3b1349a1495c151d37a0ff4cba73fd08282736be7f92cc6c"}, + {file = "tiktoken-0.11.0-cp310-cp310-win_amd64.whl", hash = "sha256:fe91581b0ecdd8783ce8cb6e3178f2260a3912e8724d2f2d49552b98714641a1"}, + {file = "tiktoken-0.11.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:4ae374c46afadad0f501046db3da1b36cd4dfbfa52af23c998773682446097cf"}, + {file = "tiktoken-0.11.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:25a512ff25dc6c85b58f5dd4f3d8c674dc05f96b02d66cdacf628d26a4e4866b"}, + {file = "tiktoken-0.11.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2130127471e293d385179c1f3f9cd445070c0772be73cdafb7cec9a3684c0458"}, + {file = "tiktoken-0.11.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21e43022bf2c33f733ea9b54f6a3f6b4354b909f5a73388fb1b9347ca54a069c"}, + {file = "tiktoken-0.11.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:adb4e308eb64380dc70fa30493e21c93475eaa11669dea313b6bbf8210bfd013"}, + {file = "tiktoken-0.11.0-cp311-cp311-win_amd64.whl", hash = "sha256:ece6b76bfeeb61a125c44bbefdfccc279b5288e6007fbedc0d32bfec602df2f2"}, + {file = "tiktoken-0.11.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fd9e6b23e860973cf9526544e220b223c60badf5b62e80a33509d6d40e6c8f5d"}, + {file = "tiktoken-0.11.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6a76d53cee2da71ee2731c9caa747398762bda19d7f92665e882fef229cb0b5b"}, + {file = "tiktoken-0.11.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ef72aab3ea240646e642413cb363b73869fed4e604dcfd69eec63dc54d603e8"}, + {file = "tiktoken-0.11.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f929255c705efec7a28bf515e29dc74220b2f07544a8c81b8d69e8efc4578bd"}, + {file = "tiktoken-0.11.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:61f1d15822e4404953d499fd1dcc62817a12ae9fb1e4898033ec8fe3915fdf8e"}, + {file = "tiktoken-0.11.0-cp312-cp312-win_amd64.whl", hash = "sha256:45927a71ab6643dfd3ef57d515a5db3d199137adf551f66453be098502838b0f"}, + {file = "tiktoken-0.11.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a5f3f25ffb152ee7fec78e90a5e5ea5b03b4ea240beed03305615847f7a6ace2"}, + {file = "tiktoken-0.11.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7dc6e9ad16a2a75b4c4be7208055a1f707c9510541d94d9cc31f7fbdc8db41d8"}, + {file = "tiktoken-0.11.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5a0517634d67a8a48fd4a4ad73930c3022629a85a217d256a6e9b8b47439d1e4"}, + {file = "tiktoken-0.11.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7fb4effe60574675118b73c6fbfd3b5868e5d7a1f570d6cc0d18724b09ecf318"}, + {file = "tiktoken-0.11.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:94f984c9831fd32688aef4348803b0905d4ae9c432303087bae370dc1381a2b8"}, + {file = "tiktoken-0.11.0-cp313-cp313-win_amd64.whl", hash = "sha256:2177ffda31dec4023356a441793fed82f7af5291120751dee4d696414f54db0c"}, + {file = "tiktoken-0.11.0-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:13220f12c9e82e399377e768640ddfe28bea962739cc3a869cad98f42c419a89"}, + {file = "tiktoken-0.11.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:7f2db627f5c74477c0404b4089fd8a28ae22fa982a6f7d9c7d4c305c375218f3"}, + {file = "tiktoken-0.11.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2302772f035dceb2bcf8e55a735e4604a0b51a6dd50f38218ff664d46ec43807"}, + {file = "tiktoken-0.11.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:20b977989afe44c94bcc50db1f76971bb26dca44218bd203ba95925ef56f8e7a"}, + {file = "tiktoken-0.11.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:669a1aa1ad6ebf1b3c26b45deb346f345da7680f845b5ea700bba45c20dea24c"}, + {file = "tiktoken-0.11.0-cp39-cp39-win_amd64.whl", hash = "sha256:e363f33c720a055586f730c00e330df4c7ea0024bf1c83a8a9a9dbc054c4f304"}, + {file = "tiktoken-0.11.0.tar.gz", hash = "sha256:3c518641aee1c52247c2b97e74d8d07d780092af79d5911a6ab5e79359d9b06a"}, +] + +[package.dependencies] +regex = ">=2022.1.18" +requests = ">=2.26.0" + +[package.extras] +blobfile = ["blobfile (>=2)"] + [[package]] name = "tokenizers" version = "0.21.0" @@ -5988,6 +8583,7 @@ description = "" optional = false python-versions = ">=3.7" groups = ["main"] +markers = "python_full_version == \"3.8.*\" or platform_python_implementation == \"PyPy\" and python_version < \"3.10\"" files = [ {file = "tokenizers-0.21.0-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:3c4c93eae637e7d2aaae3d376f06085164e1660f89304c0ab2b1d08a406636b2"}, {file = "tokenizers-0.21.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:f53ea537c925422a2e0e92a24cce96f6bc5046bbef24a1652a5edc8ba975f62e"}, @@ -6014,6 +8610,40 @@ dev = ["tokenizers[testing]"] docs = ["setuptools-rust", "sphinx", "sphinx-rtd-theme"] testing = ["black (==22.3)", "datasets", "numpy", "pytest", "requests", "ruff"] +[[package]] +name = "tokenizers" +version = "0.22.1" +description = "" +optional = false +python-versions = ">=3.9" +groups = ["main"] +markers = "python_version >= \"3.9\" and platform_python_implementation != \"PyPy\" or python_version >= \"3.10\"" +files = [ + {file = "tokenizers-0.22.1-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:59fdb013df17455e5f950b4b834a7b3ee2e0271e6378ccb33aa74d178b513c73"}, + {file = "tokenizers-0.22.1-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:8d4e484f7b0827021ac5f9f71d4794aaef62b979ab7608593da22b1d2e3c4edc"}, + {file = "tokenizers-0.22.1-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:19d2962dd28bc67c1f205ab180578a78eef89ac60ca7ef7cbe9635a46a56422a"}, + {file = "tokenizers-0.22.1-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:38201f15cdb1f8a6843e6563e6e79f4abd053394992b9bbdf5213ea3469b4ae7"}, + {file = "tokenizers-0.22.1-cp39-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d1cbe5454c9a15df1b3443c726063d930c16f047a3cc724b9e6e1a91140e5a21"}, + {file = "tokenizers-0.22.1-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e7d094ae6312d69cc2a872b54b91b309f4f6fbce871ef28eb27b52a98e4d0214"}, + {file = "tokenizers-0.22.1-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:afd7594a56656ace95cdd6df4cca2e4059d294c5cfb1679c57824b605556cb2f"}, + {file = "tokenizers-0.22.1-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e2ef6063d7a84994129732b47e7915e8710f27f99f3a3260b8a38fc7ccd083f4"}, + {file = "tokenizers-0.22.1-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ba0a64f450b9ef412c98f6bcd2a50c6df6e2443b560024a09fa6a03189726879"}, + {file = "tokenizers-0.22.1-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:331d6d149fa9c7d632cde4490fb8bbb12337fa3a0232e77892be656464f4b446"}, + {file = "tokenizers-0.22.1-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:607989f2ea68a46cb1dfbaf3e3aabdf3f21d8748312dbeb6263d1b3b66c5010a"}, + {file = "tokenizers-0.22.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a0f307d490295717726598ef6fa4f24af9d484809223bbc253b201c740a06390"}, + {file = "tokenizers-0.22.1-cp39-abi3-win32.whl", hash = "sha256:b5120eed1442765cd90b903bb6cfef781fd8fe64e34ccaecbae4c619b7b12a82"}, + {file = "tokenizers-0.22.1-cp39-abi3-win_amd64.whl", hash = "sha256:65fd6e3fb11ca1e78a6a93602490f134d1fdeb13bcef99389d5102ea318ed138"}, + {file = "tokenizers-0.22.1.tar.gz", hash = "sha256:61de6522785310a309b3407bac22d99c4db5dba349935e99e4d15ea2226af2d9"}, +] + +[package.dependencies] +huggingface-hub = ">=0.16.4,<2.0" + +[package.extras] +dev = ["tokenizers[testing]"] +docs = ["setuptools-rust", "sphinx", "sphinx-rtd-theme"] +testing = ["black (==22.3)", "datasets", "numpy", "pytest", "pytest-asyncio", "requests", "ruff"] + [[package]] name = "tomli" version = "2.2.1" @@ -6021,7 +8651,6 @@ description = "A lil' TOML parser" optional = false python-versions = ">=3.8" groups = ["main", "dev", "proxy-dev"] -markers = "python_version <= \"3.10\"" files = [ {file = "tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249"}, {file = "tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6"}, @@ -6056,6 +8685,7 @@ files = [ {file = "tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc"}, {file = "tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff"}, ] +markers = {main = "(extra == \"utils\" or python_version == \"3.10\") and python_version <= \"3.10\" and (extra == \"utils\" or extra == \"mlflow\")", dev = "python_version <= \"3.10\"", proxy-dev = "python_version <= \"3.10\""} [[package]] name = "tomlkit" @@ -6098,6 +8728,7 @@ description = "Typing stubs for cffi" optional = false python-versions = ">=3.8" groups = ["dev"] +markers = "python_full_version == \"3.8.*\" or platform_python_implementation == \"PyPy\" and python_version < \"3.10\"" files = [ {file = "types_cffi-1.16.0.20241221-py3-none-any.whl", hash = "sha256:e5b76b4211d7a9185f6ab8d06a106d56c7eb80af7cdb8bfcb4186ade10fb112f"}, {file = "types_cffi-1.16.0.20241221.tar.gz", hash = "sha256:1c96649618f4b6145f58231acb976e0b448be6b847f7ab733dabe62dfbff6591"}, @@ -6106,6 +8737,22 @@ files = [ [package.dependencies] types-setuptools = "*" +[[package]] +name = "types-cffi" +version = "1.17.0.20250915" +description = "Typing stubs for cffi" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +markers = "python_version >= \"3.9\" and platform_python_implementation != \"PyPy\" or python_version >= \"3.10\"" +files = [ + {file = "types_cffi-1.17.0.20250915-py3-none-any.whl", hash = "sha256:cef4af1116c83359c11bb4269283c50f0688e9fc1d7f0eeb390f3661546da52c"}, + {file = "types_cffi-1.17.0.20250915.tar.gz", hash = "sha256:4362e20368f78dabd5c56bca8004752cc890e07a71605d9e0d9e069dbaac8c06"}, +] + +[package.dependencies] +types-setuptools = "*" + [[package]] name = "types-pyopenssl" version = "24.1.0.20240722" @@ -6129,11 +8776,25 @@ description = "Typing stubs for PyYAML" optional = false python-versions = ">=3.8" groups = ["dev"] +markers = "python_full_version == \"3.8.*\" or platform_python_implementation == \"PyPy\" and python_version < \"3.10\"" files = [ {file = "types_PyYAML-6.0.12.20241230-py3-none-any.whl", hash = "sha256:fa4d32565219b68e6dee5f67534c722e53c00d1cfc09c435ef04d7353e1e96e6"}, {file = "types_pyyaml-6.0.12.20241230.tar.gz", hash = "sha256:7f07622dbd34bb9c8b264fe860a17e0efcad00d50b5f27e93984909d9363498c"}, ] +[[package]] +name = "types-pyyaml" +version = "6.0.12.20250915" +description = "Typing stubs for PyYAML" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +markers = "python_version >= \"3.9\" and platform_python_implementation != \"PyPy\" or python_version >= \"3.10\"" +files = [ + {file = "types_pyyaml-6.0.12.20250915-py3-none-any.whl", hash = "sha256:e7d4d9e064e89a3b3cae120b4990cd370874d2bf12fa5f46c97018dd5d3c9ab6"}, + {file = "types_pyyaml-6.0.12.20250915.tar.gz", hash = "sha256:0f8b54a528c303f0e6f7165687dd33fafa81c807fcac23f632b63aa624ced1d3"}, +] + [[package]] name = "types-redis" version = "4.6.0.20241004" @@ -6168,15 +8829,15 @@ types-urllib3 = "*" [[package]] name = "types-requests" -version = "2.32.0.20241016" +version = "2.32.4.20250913" description = "Typing stubs for requests" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" groups = ["dev"] markers = "python_version >= \"3.10\"" files = [ - {file = "types-requests-2.32.0.20241016.tar.gz", hash = "sha256:0d9cad2f27515d0e3e3da7134a1b6f28fb97129d86b867f24d9c726452634d95"}, - {file = "types_requests-2.32.0.20241016-py3-none-any.whl", hash = "sha256:4195d62d6d3e043a4eaaf08ff8a62184584d2e8684e9d2aa178c7915a7da3747"}, + {file = "types_requests-2.32.4.20250913-py3-none-any.whl", hash = "sha256:78c9c1fffebbe0fa487a418e0fa5252017e9c60d1a2da394077f1780f655d7e1"}, + {file = "types_requests-2.32.4.20250913.tar.gz", hash = "sha256:abd6d4f9ce3a9383f269775a9835a4c24e5cd6b9f647d64f88aa4613c33def5d"}, ] [package.dependencies] @@ -6189,11 +8850,25 @@ description = "Typing stubs for setuptools" optional = false python-versions = ">=3.8" groups = ["dev"] +markers = "python_full_version == \"3.8.*\" or platform_python_implementation == \"PyPy\" and python_version < \"3.10\"" files = [ {file = "types_setuptools-75.8.0.20250110-py3-none-any.whl", hash = "sha256:a9f12980bbf9bcdc23ecd80755789085bad6bfce4060c2275bc2b4ca9f2bc480"}, {file = "types_setuptools-75.8.0.20250110.tar.gz", hash = "sha256:96f7ec8bbd6e0a54ea180d66ad68ad7a1d7954e7281a710ea2de75e355545271"}, ] +[[package]] +name = "types-setuptools" +version = "80.9.0.20250822" +description = "Typing stubs for setuptools" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +markers = "python_version >= \"3.9\" and platform_python_implementation != \"PyPy\" or python_version >= \"3.10\"" +files = [ + {file = "types_setuptools-80.9.0.20250822-py3-none-any.whl", hash = "sha256:53bf881cb9d7e46ed12c76ef76c0aaf28cfe6211d3fab12e0b83620b1a8642c3"}, + {file = "types_setuptools-80.9.0.20250822.tar.gz", hash = "sha256:070ea7716968ec67a84c7f7768d9952ff24d28b65b6594797a464f1b3066f965"}, +] + [[package]] name = "types-urllib3" version = "1.26.25.14" @@ -6214,19 +8889,33 @@ description = "Backported and Experimental Type Hints for Python 3.8+" optional = false python-versions = ">=3.8" groups = ["main", "dev", "proxy-dev"] +markers = "python_full_version == \"3.8.*\" or platform_python_implementation == \"PyPy\" and python_version < \"3.10\"" files = [ {file = "typing_extensions-4.13.2-py3-none-any.whl", hash = "sha256:a439e7c04b49fec3e5d3e2beaa21755cadbbdc391694e28ccdd36ca4a1408f8c"}, {file = "typing_extensions-4.13.2.tar.gz", hash = "sha256:e6c81219bd689f51865d9e372991c540bda33a0379d5573cddb9a3a23f7caaef"}, ] +[[package]] +name = "typing-extensions" +version = "4.15.0" +description = "Backported and Experimental Type Hints for Python 3.9+" +optional = false +python-versions = ">=3.9" +groups = ["main", "dev", "proxy-dev"] +markers = "python_version >= \"3.9\" and platform_python_implementation != \"PyPy\" or python_version >= \"3.10\"" +files = [ + {file = "typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548"}, + {file = "typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466"}, +] + [[package]] name = "typing-inspection" version = "0.4.1" description = "Runtime typing introspection tools" -optional = true +optional = false python-versions = ">=3.9" -groups = ["main"] -markers = "python_version >= \"3.10\" and extra == \"proxy\"" +groups = ["main", "dev", "proxy-dev"] +markers = "python_version >= \"3.9\" and platform_python_implementation != \"PyPy\" or python_version >= \"3.10\"" files = [ {file = "typing_inspection-0.4.1-py3-none-any.whl", hash = "sha256:389055682238f53b04f7badcb49b989835495a96700ced5dab2d8feae4b26f51"}, {file = "typing_inspection-0.4.1.tar.gz", hash = "sha256:6ae134cc0203c33377d43188d4064e9b357dba58cff3185f22924610e70a9d28"}, @@ -6242,7 +8931,7 @@ description = "Provider of IANA time zone data" optional = true python-versions = ">=2" groups = ["main"] -markers = "python_version >= \"3.10\" and platform_system == \"Windows\" and (extra == \"proxy\" or extra == \"mlflow\") or python_version >= \"3.10\" and extra == \"mlflow\" or platform_system == \"Windows\" and extra == \"proxy\"" +markers = "(python_version >= \"3.10\" or platform_system == \"Windows\") and (python_version >= \"3.10\" or extra == \"proxy\") and (extra == \"proxy\" or extra == \"mlflow\") and (platform_system == \"Windows\" or extra == \"mlflow\")" files = [ {file = "tzdata-2025.2-py2.py3-none-any.whl", hash = "sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8"}, {file = "tzdata-2025.2.tar.gz", hash = "sha256:b60a638fcc0daffadf82fe0f57e53d06bdec2f36c4df66280ae79bce6bd6f2b9"}, @@ -6255,7 +8944,7 @@ description = "tzinfo object for the local timezone" optional = true python-versions = ">=3.8" groups = ["main"] -markers = "extra == \"proxy\"" +markers = "(python_full_version == \"3.8.*\" or platform_python_implementation == \"PyPy\") and extra == \"proxy\" and python_version < \"3.10\"" files = [ {file = "tzlocal-5.2-py3-none-any.whl", hash = "sha256:49816ef2fe65ea8ac19d19aa7a1ae0551c834303d5014c6d5a62e4cbda8047b8"}, {file = "tzlocal-5.2.tar.gz", hash = "sha256:8d399205578f1a9342816409cc1e46a93ebd5755e39ea2d85334bea911bf0e6e"}, @@ -6268,6 +8957,25 @@ tzdata = {version = "*", markers = "platform_system == \"Windows\""} [package.extras] devenv = ["check-manifest", "pytest (>=4.3)", "pytest-cov", "pytest-mock (>=3.3)", "zest.releaser"] +[[package]] +name = "tzlocal" +version = "5.3.1" +description = "tzinfo object for the local timezone" +optional = true +python-versions = ">=3.9" +groups = ["main"] +markers = "python_version >= \"3.9\" and (python_version >= \"3.10\" or platform_python_implementation != \"PyPy\") and extra == \"proxy\"" +files = [ + {file = "tzlocal-5.3.1-py3-none-any.whl", hash = "sha256:eb1a66c3ef5847adf7a834f1be0800581b683b5608e74f86ecbcef8ab91bb85d"}, + {file = "tzlocal-5.3.1.tar.gz", hash = "sha256:cceffc7edecefea1f595541dbd6e990cb1ea3d19bf01b2809f362a03dd7921fd"}, +] + +[package.dependencies] +tzdata = {version = "*", markers = "platform_system == \"Windows\""} + +[package.extras] +devenv = ["check-manifest", "pytest (>=4.3)", "pytest-cov", "pytest-mock (>=3.3)", "zest.releaser"] + [[package]] name = "urllib3" version = "1.26.20" @@ -6288,15 +8996,15 @@ socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] [[package]] name = "urllib3" -version = "2.2.3" +version = "2.5.0" description = "HTTP library with thread-safe connection pooling, file post, and more." optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" groups = ["main", "dev", "proxy-dev"] markers = "python_version >= \"3.10\"" files = [ - {file = "urllib3-2.2.3-py3-none-any.whl", hash = "sha256:ca899ca043dcb1bafa3e262d73aa25c465bfb49e0bd9dd5d59f1d0acba2f8fac"}, - {file = "urllib3-2.2.3.tar.gz", hash = "sha256:e7d814a81dad81e6caf2ec9fdedb284ecc9c73076b62654547cc64ccdcae26e9"}, + {file = "urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc"}, + {file = "urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760"}, ] [package.extras] @@ -6312,7 +9020,7 @@ description = "The lightning-fast ASGI server." optional = true python-versions = ">=3.8" groups = ["main"] -markers = "python_version >= \"3.10\" and (extra == \"mlflow\" or extra == \"proxy\") or extra == \"proxy\"" +markers = "(extra == \"mlflow\" or extra == \"proxy\") and python_version >= \"3.10\" or extra == \"proxy\"" files = [ {file = "uvicorn-0.29.0-py3-none-any.whl", hash = "sha256:2c2aac7ff4f4365c206fd773a39bf4ebd1047c238f8b8268ad996829323473de"}, {file = "uvicorn-0.29.0.tar.gz", hash = "sha256:6a69214c0b6a087462412670b3ef21224fa48cae0e452b5883e8e8bdfdd11dd0"}, @@ -6626,6 +9334,7 @@ description = "Yet another URL library" optional = false python-versions = ">=3.8" groups = ["main"] +markers = "python_full_version == \"3.8.*\" or platform_python_implementation == \"PyPy\" and python_version < \"3.10\"" files = [ {file = "yarl-1.15.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e4ee8b8639070ff246ad3649294336b06db37a94bdea0d09ea491603e0be73b8"}, {file = "yarl-1.15.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a7cf963a357c5f00cb55b1955df8bbe68d2f2f65de065160a1c26b85a1e44172"}, @@ -6732,6 +9441,126 @@ idna = ">=2.0" multidict = ">=4.0" propcache = ">=0.2.0" +[[package]] +name = "yarl" +version = "1.20.1" +description = "Yet another URL library" +optional = false +python-versions = ">=3.9" +groups = ["main"] +markers = "python_version >= \"3.9\" and platform_python_implementation != \"PyPy\" or python_version >= \"3.10\"" +files = [ + {file = "yarl-1.20.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:6032e6da6abd41e4acda34d75a816012717000fa6839f37124a47fcefc49bec4"}, + {file = "yarl-1.20.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2c7b34d804b8cf9b214f05015c4fee2ebe7ed05cf581e7192c06555c71f4446a"}, + {file = "yarl-1.20.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0c869f2651cc77465f6cd01d938d91a11d9ea5d798738c1dc077f3de0b5e5fed"}, + {file = "yarl-1.20.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:62915e6688eb4d180d93840cda4110995ad50c459bf931b8b3775b37c264af1e"}, + {file = "yarl-1.20.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:41ebd28167bc6af8abb97fec1a399f412eec5fd61a3ccbe2305a18b84fb4ca73"}, + {file = "yarl-1.20.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:21242b4288a6d56f04ea193adde174b7e347ac46ce6bc84989ff7c1b1ecea84e"}, + {file = "yarl-1.20.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bea21cdae6c7eb02ba02a475f37463abfe0a01f5d7200121b03e605d6a0439f8"}, + {file = "yarl-1.20.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1f8a891e4a22a89f5dde7862994485e19db246b70bb288d3ce73a34422e55b23"}, + {file = "yarl-1.20.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dd803820d44c8853a109a34e3660e5a61beae12970da479cf44aa2954019bf70"}, + {file = "yarl-1.20.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b982fa7f74c80d5c0c7b5b38f908971e513380a10fecea528091405f519b9ebb"}, + {file = "yarl-1.20.1-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:33f29ecfe0330c570d997bcf1afd304377f2e48f61447f37e846a6058a4d33b2"}, + {file = "yarl-1.20.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:835ab2cfc74d5eb4a6a528c57f05688099da41cf4957cf08cad38647e4a83b30"}, + {file = "yarl-1.20.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:46b5e0ccf1943a9a6e766b2c2b8c732c55b34e28be57d8daa2b3c1d1d4009309"}, + {file = "yarl-1.20.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:df47c55f7d74127d1b11251fe6397d84afdde0d53b90bedb46a23c0e534f9d24"}, + {file = "yarl-1.20.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:76d12524d05841276b0e22573f28d5fbcb67589836772ae9244d90dd7d66aa13"}, + {file = "yarl-1.20.1-cp310-cp310-win32.whl", hash = "sha256:6c4fbf6b02d70e512d7ade4b1f998f237137f1417ab07ec06358ea04f69134f8"}, + {file = "yarl-1.20.1-cp310-cp310-win_amd64.whl", hash = "sha256:aef6c4d69554d44b7f9d923245f8ad9a707d971e6209d51279196d8e8fe1ae16"}, + {file = "yarl-1.20.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:47ee6188fea634bdfaeb2cc420f5b3b17332e6225ce88149a17c413c77ff269e"}, + {file = "yarl-1.20.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d0f6500f69e8402d513e5eedb77a4e1818691e8f45e6b687147963514d84b44b"}, + {file = "yarl-1.20.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7a8900a42fcdaad568de58887c7b2f602962356908eedb7628eaf6021a6e435b"}, + {file = "yarl-1.20.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bad6d131fda8ef508b36be3ece16d0902e80b88ea7200f030a0f6c11d9e508d4"}, + {file = "yarl-1.20.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:df018d92fe22aaebb679a7f89fe0c0f368ec497e3dda6cb81a567610f04501f1"}, + {file = "yarl-1.20.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8f969afbb0a9b63c18d0feecf0db09d164b7a44a053e78a7d05f5df163e43833"}, + {file = "yarl-1.20.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:812303eb4aa98e302886ccda58d6b099e3576b1b9276161469c25803a8db277d"}, + {file = "yarl-1.20.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98c4a7d166635147924aa0bf9bfe8d8abad6fffa6102de9c99ea04a1376f91e8"}, + {file = "yarl-1.20.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:12e768f966538e81e6e7550f9086a6236b16e26cd964cf4df35349970f3551cf"}, + {file = "yarl-1.20.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:fe41919b9d899661c5c28a8b4b0acf704510b88f27f0934ac7a7bebdd8938d5e"}, + {file = "yarl-1.20.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:8601bc010d1d7780592f3fc1bdc6c72e2b6466ea34569778422943e1a1f3c389"}, + {file = "yarl-1.20.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:daadbdc1f2a9033a2399c42646fbd46da7992e868a5fe9513860122d7fe7a73f"}, + {file = "yarl-1.20.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:03aa1e041727cb438ca762628109ef1333498b122e4c76dd858d186a37cec845"}, + {file = "yarl-1.20.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:642980ef5e0fa1de5fa96d905c7e00cb2c47cb468bfcac5a18c58e27dbf8d8d1"}, + {file = "yarl-1.20.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:86971e2795584fe8c002356d3b97ef6c61862720eeff03db2a7c86b678d85b3e"}, + {file = "yarl-1.20.1-cp311-cp311-win32.whl", hash = "sha256:597f40615b8d25812f14562699e287f0dcc035d25eb74da72cae043bb884d773"}, + {file = "yarl-1.20.1-cp311-cp311-win_amd64.whl", hash = "sha256:26ef53a9e726e61e9cd1cda6b478f17e350fb5800b4bd1cd9fe81c4d91cfeb2e"}, + {file = "yarl-1.20.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:bdcc4cd244e58593a4379fe60fdee5ac0331f8eb70320a24d591a3be197b94a9"}, + {file = "yarl-1.20.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b29a2c385a5f5b9c7d9347e5812b6f7ab267193c62d282a540b4fc528c8a9d2a"}, + {file = "yarl-1.20.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1112ae8154186dfe2de4732197f59c05a83dc814849a5ced892b708033f40dc2"}, + {file = "yarl-1.20.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:90bbd29c4fe234233f7fa2b9b121fb63c321830e5d05b45153a2ca68f7d310ee"}, + {file = "yarl-1.20.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:680e19c7ce3710ac4cd964e90dad99bf9b5029372ba0c7cbfcd55e54d90ea819"}, + {file = "yarl-1.20.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4a979218c1fdb4246a05efc2cc23859d47c89af463a90b99b7c56094daf25a16"}, + {file = "yarl-1.20.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:255b468adf57b4a7b65d8aad5b5138dce6a0752c139965711bdcb81bc370e1b6"}, + {file = "yarl-1.20.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a97d67108e79cfe22e2b430d80d7571ae57d19f17cda8bb967057ca8a7bf5bfd"}, + {file = "yarl-1.20.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8570d998db4ddbfb9a590b185a0a33dbf8aafb831d07a5257b4ec9948df9cb0a"}, + {file = "yarl-1.20.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:97c75596019baae7c71ccf1d8cc4738bc08134060d0adfcbe5642f778d1dca38"}, + {file = "yarl-1.20.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:1c48912653e63aef91ff988c5432832692ac5a1d8f0fb8a33091520b5bbe19ef"}, + {file = "yarl-1.20.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:4c3ae28f3ae1563c50f3d37f064ddb1511ecc1d5584e88c6b7c63cf7702a6d5f"}, + {file = "yarl-1.20.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c5e9642f27036283550f5f57dc6156c51084b458570b9d0d96100c8bebb186a8"}, + {file = "yarl-1.20.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:2c26b0c49220d5799f7b22c6838409ee9bc58ee5c95361a4d7831f03cc225b5a"}, + {file = "yarl-1.20.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:564ab3d517e3d01c408c67f2e5247aad4019dcf1969982aba3974b4093279004"}, + {file = "yarl-1.20.1-cp312-cp312-win32.whl", hash = "sha256:daea0d313868da1cf2fac6b2d3a25c6e3a9e879483244be38c8e6a41f1d876a5"}, + {file = "yarl-1.20.1-cp312-cp312-win_amd64.whl", hash = "sha256:48ea7d7f9be0487339828a4de0360d7ce0efc06524a48e1810f945c45b813698"}, + {file = "yarl-1.20.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:0b5ff0fbb7c9f1b1b5ab53330acbfc5247893069e7716840c8e7d5bb7355038a"}, + {file = "yarl-1.20.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:14f326acd845c2b2e2eb38fb1346c94f7f3b01a4f5c788f8144f9b630bfff9a3"}, + {file = "yarl-1.20.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f60e4ad5db23f0b96e49c018596707c3ae89f5d0bd97f0ad3684bcbad899f1e7"}, + {file = "yarl-1.20.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:49bdd1b8e00ce57e68ba51916e4bb04461746e794e7c4d4bbc42ba2f18297691"}, + {file = "yarl-1.20.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:66252d780b45189975abfed839616e8fd2dbacbdc262105ad7742c6ae58f3e31"}, + {file = "yarl-1.20.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:59174e7332f5d153d8f7452a102b103e2e74035ad085f404df2e40e663a22b28"}, + {file = "yarl-1.20.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e3968ec7d92a0c0f9ac34d5ecfd03869ec0cab0697c91a45db3fbbd95fe1b653"}, + {file = "yarl-1.20.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d1a4fbb50e14396ba3d375f68bfe02215d8e7bc3ec49da8341fe3157f59d2ff5"}, + {file = "yarl-1.20.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:11a62c839c3a8eac2410e951301309426f368388ff2f33799052787035793b02"}, + {file = "yarl-1.20.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:041eaa14f73ff5a8986b4388ac6bb43a77f2ea09bf1913df7a35d4646db69e53"}, + {file = "yarl-1.20.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:377fae2fef158e8fd9d60b4c8751387b8d1fb121d3d0b8e9b0be07d1b41e83dc"}, + {file = "yarl-1.20.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:1c92f4390e407513f619d49319023664643d3339bd5e5a56a3bebe01bc67ec04"}, + {file = "yarl-1.20.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:d25ddcf954df1754ab0f86bb696af765c5bfaba39b74095f27eececa049ef9a4"}, + {file = "yarl-1.20.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:909313577e9619dcff8c31a0ea2aa0a2a828341d92673015456b3ae492e7317b"}, + {file = "yarl-1.20.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:793fd0580cb9664548c6b83c63b43c477212c0260891ddf86809e1c06c8b08f1"}, + {file = "yarl-1.20.1-cp313-cp313-win32.whl", hash = "sha256:468f6e40285de5a5b3c44981ca3a319a4b208ccc07d526b20b12aeedcfa654b7"}, + {file = "yarl-1.20.1-cp313-cp313-win_amd64.whl", hash = "sha256:495b4ef2fea40596bfc0affe3837411d6aa3371abcf31aac0ccc4bdd64d4ef5c"}, + {file = "yarl-1.20.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:f60233b98423aab21d249a30eb27c389c14929f47be8430efa7dbd91493a729d"}, + {file = "yarl-1.20.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:6f3eff4cc3f03d650d8755c6eefc844edde99d641d0dcf4da3ab27141a5f8ddf"}, + {file = "yarl-1.20.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:69ff8439d8ba832d6bed88af2c2b3445977eba9a4588b787b32945871c2444e3"}, + {file = "yarl-1.20.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3cf34efa60eb81dd2645a2e13e00bb98b76c35ab5061a3989c7a70f78c85006d"}, + {file = "yarl-1.20.1-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:8e0fe9364ad0fddab2688ce72cb7a8e61ea42eff3c7caeeb83874a5d479c896c"}, + {file = "yarl-1.20.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8f64fbf81878ba914562c672024089e3401974a39767747691c65080a67b18c1"}, + {file = "yarl-1.20.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f6342d643bf9a1de97e512e45e4b9560a043347e779a173250824f8b254bd5ce"}, + {file = "yarl-1.20.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:56dac5f452ed25eef0f6e3c6a066c6ab68971d96a9fb441791cad0efba6140d3"}, + {file = "yarl-1.20.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c7d7f497126d65e2cad8dc5f97d34c27b19199b6414a40cb36b52f41b79014be"}, + {file = "yarl-1.20.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:67e708dfb8e78d8a19169818eeb5c7a80717562de9051bf2413aca8e3696bf16"}, + {file = "yarl-1.20.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:595c07bc79af2494365cc96ddeb772f76272364ef7c80fb892ef9d0649586513"}, + {file = "yarl-1.20.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:7bdd2f80f4a7df852ab9ab49484a4dee8030023aa536df41f2d922fd57bf023f"}, + {file = "yarl-1.20.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:c03bfebc4ae8d862f853a9757199677ab74ec25424d0ebd68a0027e9c639a390"}, + {file = "yarl-1.20.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:344d1103e9c1523f32a5ed704d576172d2cabed3122ea90b1d4e11fe17c66458"}, + {file = "yarl-1.20.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:88cab98aa4e13e1ade8c141daeedd300a4603b7132819c484841bb7af3edce9e"}, + {file = "yarl-1.20.1-cp313-cp313t-win32.whl", hash = "sha256:b121ff6a7cbd4abc28985b6028235491941b9fe8fe226e6fdc539c977ea1739d"}, + {file = "yarl-1.20.1-cp313-cp313t-win_amd64.whl", hash = "sha256:541d050a355bbbc27e55d906bc91cb6fe42f96c01413dd0f4ed5a5240513874f"}, + {file = "yarl-1.20.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:e42ba79e2efb6845ebab49c7bf20306c4edf74a0b20fc6b2ccdd1a219d12fad3"}, + {file = "yarl-1.20.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:41493b9b7c312ac448b7f0a42a089dffe1d6e6e981a2d76205801a023ed26a2b"}, + {file = "yarl-1.20.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f5a5928ff5eb13408c62a968ac90d43f8322fd56d87008b8f9dabf3c0f6ee983"}, + {file = "yarl-1.20.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:30c41ad5d717b3961b2dd785593b67d386b73feca30522048d37298fee981805"}, + {file = "yarl-1.20.1-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:59febc3969b0781682b469d4aca1a5cab7505a4f7b85acf6db01fa500fa3f6ba"}, + {file = "yarl-1.20.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d2b6fb3622b7e5bf7a6e5b679a69326b4279e805ed1699d749739a61d242449e"}, + {file = "yarl-1.20.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:749d73611db8d26a6281086f859ea7ec08f9c4c56cec864e52028c8b328db723"}, + {file = "yarl-1.20.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9427925776096e664c39e131447aa20ec738bdd77c049c48ea5200db2237e000"}, + {file = "yarl-1.20.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ff70f32aa316393eaf8222d518ce9118148eddb8a53073c2403863b41033eed5"}, + {file = "yarl-1.20.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:c7ddf7a09f38667aea38801da8b8d6bfe81df767d9dfc8c88eb45827b195cd1c"}, + {file = "yarl-1.20.1-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:57edc88517d7fc62b174fcfb2e939fbc486a68315d648d7e74d07fac42cec240"}, + {file = "yarl-1.20.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:dab096ce479d5894d62c26ff4f699ec9072269d514b4edd630a393223f45a0ee"}, + {file = "yarl-1.20.1-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:14a85f3bd2d7bb255be7183e5d7d6e70add151a98edf56a770d6140f5d5f4010"}, + {file = "yarl-1.20.1-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:2c89b5c792685dd9cd3fa9761c1b9f46fc240c2a3265483acc1565769996a3f8"}, + {file = "yarl-1.20.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:69e9b141de5511021942a6866990aea6d111c9042235de90e08f94cf972ca03d"}, + {file = "yarl-1.20.1-cp39-cp39-win32.whl", hash = "sha256:b5f307337819cdfdbb40193cad84978a029f847b0a357fbe49f712063cfc4f06"}, + {file = "yarl-1.20.1-cp39-cp39-win_amd64.whl", hash = "sha256:eae7bfe2069f9c1c5b05fc7fe5d612e5bbc089a39309904ee8b829e322dcad00"}, + {file = "yarl-1.20.1-py3-none-any.whl", hash = "sha256:83b8eb083fe4683c6115795d9fc1cfaf2cbbefb19b3a1cb68f6527460f483a77"}, + {file = "yarl-1.20.1.tar.gz", hash = "sha256:d017a4997ee50c91fd5466cef416231bb82177b93b029906cefc542ce14c35ac"}, +] + +[package.dependencies] +idna = ">=2.0" +multidict = ">=4.0" +propcache = ">=0.2.1" + [[package]] name = "zipp" version = "3.20.2" @@ -6739,6 +9568,7 @@ description = "Backport of pathlib-compatible object wrapper for zip files" optional = false python-versions = ">=3.8" groups = ["main", "dev", "proxy-dev"] +markers = "python_full_version == \"3.8.*\" or platform_python_implementation == \"PyPy\" and python_version < \"3.10\"" files = [ {file = "zipp-3.20.2-py3-none-any.whl", hash = "sha256:a817ac80d6cf4b23bf7f2828b7cabf326f15a001bea8b1f9b49631780ba28350"}, {file = "zipp-3.20.2.tar.gz", hash = "sha256:bc9eb26f4506fda01b81bcde0ca78103b6e62f991b381fec825435c836edbc29"}, @@ -6752,6 +9582,27 @@ enabler = ["pytest-enabler (>=2.2)"] test = ["big-O", "importlib-resources ; python_version < \"3.9\"", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more-itertools", "pytest (>=6,!=8.1.*)", "pytest-ignore-flaky"] type = ["pytest-mypy"] +[[package]] +name = "zipp" +version = "3.23.0" +description = "Backport of pathlib-compatible object wrapper for zip files" +optional = false +python-versions = ">=3.9" +groups = ["main", "dev", "proxy-dev"] +markers = "python_version >= \"3.9\" and platform_python_implementation != \"PyPy\" or python_version >= \"3.10\"" +files = [ + {file = "zipp-3.23.0-py3-none-any.whl", hash = "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e"}, + {file = "zipp-3.23.0.tar.gz", hash = "sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166"}, +] + +[package.extras] +check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\""] +cover = ["pytest-cov"] +doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +enabler = ["pytest-enabler (>=2.2)"] +test = ["big-O", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more_itertools", "pytest (>=6,!=8.1.*)", "pytest-ignore-flaky"] +type = ["pytest-mypy"] + [extras] caching = ["diskcache"] extra-proxy = ["azure-identity", "azure-keyvault-secrets", "google-cloud-iam", "google-cloud-kms", "prisma", "redisvl", "resend"] From 3abb66f080aec07b535c1828950807928bd79a7a Mon Sep 17 00:00:00 2001 From: Arshdeep Singh Date: Thu, 25 Sep 2025 12:21:23 -0400 Subject: [PATCH 41/70] fix: regenerate poetry.lock to resolve line 3773 corruption --- poetry.lock | 38 +------------------------------------- 1 file changed, 1 insertion(+), 37 deletions(-) diff --git a/poetry.lock b/poetry.lock index 3c36e42f839..9a9409664b0 100644 --- a/poetry.lock +++ b/poetry.lock @@ -3770,29 +3770,8 @@ python-versions = "!=2.7.*,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6 groups = ["main"] markers = "extra == \"proxy\"" files = [ -<<<<<<< HEAD - {file = "litellm_proxy_extras-0.2.20-py3-none-any.whl", hash = "sha256:80ebe03be210eed99ddab0bf8e1a14169f24a502ecdaa98f4bb5032ebe65a944"}, - {file = "litellm_proxy_extras-0.2.20.tar.gz", hash = "sha256:8dea657f965122490be480327995ce534e75dc96778cb3e05c562c7996f85cce"}, -<<<<<<< HEAD -<<<<<<< HEAD -======= {file = "litellm_proxy_extras-0.2.19-py3-none-any.whl", hash = "sha256:cd4cc5fc639ac24bc99af70b8b56b7cc0480b0e72a2a53638f557beb7e9f64fd"}, {file = "litellm_proxy_extras-0.2.19.tar.gz", hash = "sha256:e53592e39eb0b9c3b6cd32f29e6a0a53be51d7ad31dedfb69b58c24073b5b3ec"}, ->>>>>>> parent of ded823a3a3 ([Feat] Initial support for scheduled key rotations (#14877)) -======= ->>>>>>> parent of c4838e730c (bump v extras) -] - -[[package]] -name = "madoka" -version = "0.7.1" -description = "Memory-efficient CountMin Sketch key-value structure (based on Madoka C++ library)" -optional = false -python-versions = "*" -groups = ["main"] -files = [ - {file = "madoka-0.7.1-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:7521eee9ace30b376bb54fdcb2cb42bf6b7a0346b0d0b612f25f3299aa4a95af"}, - {file = "madoka-0.7.1.tar.gz", hash = "sha256:e258baa84fc0a3764365993b8bf5e1b065383a6ca8c9f862fb3e3e709843fae7"}, ] [[package]] @@ -5553,21 +5532,6 @@ timezone = ["tzdata ; platform_system == \"Windows\""] xlsx2csv = ["xlsx2csv (>=0.8.0)"] xlsxwriter = ["xlsxwriter"] -[[package]] -name = "pondpond" -version = "1.4.1" -description = "Pond is a high performance object-pooling library for Python." -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "pondpond-1.4.1-py3-none-any.whl", hash = "sha256:641028ead4e8018ca6de1220c660ddd6d6fbf62a60e72f410655dd0451d82880"}, - {file = "pondpond-1.4.1.tar.gz", hash = "sha256:8afa34b869d1434d21dd2ec12644abc3b1733fcda8fcf355300338a13a79bb7b"}, -] - -[package.dependencies] -madoka = ">=0.7.1" - [[package]] name = "priority" version = "2.0.0" @@ -9614,4 +9578,4 @@ utils = ["numpydoc"] [metadata] lock-version = "2.1" python-versions = ">=3.8.1,<4.0, !=3.9.7" -content-hash = "6ea59a97b3d69dc5fcc45645131e2fc843ec533854dba19852cecf8e10da5e7a" +content-hash = "6ba83d170d58367d0888a0bdd320a5a348d49411cb4c4661af81c36e9cf23420" From 8364da014fe8053a9c0c017ad353422fe9392f8b Mon Sep 17 00:00:00 2001 From: Arshdeep Singh Date: Thu, 25 Sep 2025 12:23:44 -0400 Subject: [PATCH 42/70] style: format code with black --- .../guardrails/guardrail_hooks/presidio.py | 12 ++++++---- litellm/types/guardrails.py | 1 - .../test_presidio_union_fix.py | 24 +++++++++++-------- 3 files changed, 22 insertions(+), 15 deletions(-) diff --git a/litellm/proxy/guardrails/guardrail_hooks/presidio.py b/litellm/proxy/guardrails/guardrail_hooks/presidio.py index c3c6c3d2e03..78ade173bf9 100644 --- a/litellm/proxy/guardrails/guardrail_hooks/presidio.py +++ b/litellm/proxy/guardrails/guardrail_hooks/presidio.py @@ -402,10 +402,14 @@ class _OPTIONAL_PresidioPIIMasking(CustomGuardrail): content_safety = data.get("content_safety", None) verbose_proxy_logger.debug("content_safety: %s", content_safety) presidio_config = self.get_presidio_settings_from_request_data(data) - if call_type in [ - LitellmCallTypes.completion.value, - LitellmCallTypes.acompletion.value, - ] or call_type == "mcp_call": + if ( + call_type + in [ + LitellmCallTypes.completion.value, + LitellmCallTypes.acompletion.value, + ] + or call_type == "mcp_call" + ): messages = data["messages"] tasks = [] for m in messages: diff --git a/litellm/types/guardrails.py b/litellm/types/guardrails.py index 0c3dc662ec4..3f9f7cec0e0 100644 --- a/litellm/types/guardrails.py +++ b/litellm/types/guardrails.py @@ -40,7 +40,6 @@ class SupportedGuardrailIntegrations(Enum): TOOL_PERMISSION = "tool_permission" - class Role(Enum): SYSTEM = "system" ASSISTANT = "assistant" diff --git a/tests/test_litellm/proxy/guardrails/guardrail_hooks/test_presidio_union_fix.py b/tests/test_litellm/proxy/guardrails/guardrail_hooks/test_presidio_union_fix.py index 549f715ed50..a062bc1a896 100644 --- a/tests/test_litellm/proxy/guardrails/guardrail_hooks/test_presidio_union_fix.py +++ b/tests/test_litellm/proxy/guardrails/guardrail_hooks/test_presidio_union_fix.py @@ -17,42 +17,46 @@ class PiiAction(str, Enum): def test_presidio_union_type_fix(): """Test that Union[PiiEntityType, str] allows both enum and string entity types""" - + # Custom recognizers for EMPLOYEE_ID and CUSTOMER_ID custom_recognizers = [ { "name": "Employee ID Recognizer", "supported_language": "en", - "patterns": [{"name": "employee id", "regex": "EMP-[0-9]{6}", "score": 0.9}], + "patterns": [ + {"name": "employee id", "regex": "EMP-[0-9]{6}", "score": 0.9} + ], "context": ["employee", "id"], - "supported_entity": "EMPLOYEE_ID" + "supported_entity": "EMPLOYEE_ID", }, { "name": "Customer ID Recognizer", "supported_language": "en", - "patterns": [{"name": "customer id", "regex": "CUST-[0-9]{8}", "score": 0.9}], + "patterns": [ + {"name": "customer id", "regex": "CUST-[0-9]{8}", "score": 0.9} + ], "context": ["customer", "id"], - "supported_entity": "CUSTOMER_ID" - } + "supported_entity": "CUSTOMER_ID", + }, ] - + # This is the core fix - mixed entity types in pii_entities_config pii_entities_config: Dict[Union[PiiEntityType, str], PiiAction] = { PiiEntityType.EMAIL_ADDRESS: PiiAction.MASK, "EMPLOYEE_ID": PiiAction.MASK, "CUSTOMER_ID": PiiAction.BLOCK, } - + # Verify entities can be used together (what Presidio needs) entities_list = list(pii_entities_config.keys()) assert len(entities_list) == 3 assert PiiEntityType.EMAIL_ADDRESS in entities_list assert "EMPLOYEE_ID" in entities_list assert "CUSTOMER_ID" in entities_list - + print("✅ Union type fix verified: mixed entity types work correctly") print("✅ Custom recognizers defined for EMPLOYEE_ID and CUSTOMER_ID") if __name__ == "__main__": - test_presidio_union_type_fix() \ No newline at end of file + test_presidio_union_type_fix() From 39683cc3d8712b6321b20e545f4a830c62231f50 Mon Sep 17 00:00:00 2001 From: Arshdeep Singh Date: Thu, 25 Sep 2025 12:31:08 -0400 Subject: [PATCH 43/70] fix: remove unused custom_recognizers variable to pass linting --- .../test_presidio_union_fix.py | 23 ------------------- 1 file changed, 23 deletions(-) diff --git a/tests/test_litellm/proxy/guardrails/guardrail_hooks/test_presidio_union_fix.py b/tests/test_litellm/proxy/guardrails/guardrail_hooks/test_presidio_union_fix.py index a062bc1a896..2de324c7e50 100644 --- a/tests/test_litellm/proxy/guardrails/guardrail_hooks/test_presidio_union_fix.py +++ b/tests/test_litellm/proxy/guardrails/guardrail_hooks/test_presidio_union_fix.py @@ -18,28 +18,6 @@ class PiiAction(str, Enum): def test_presidio_union_type_fix(): """Test that Union[PiiEntityType, str] allows both enum and string entity types""" - # Custom recognizers for EMPLOYEE_ID and CUSTOMER_ID - custom_recognizers = [ - { - "name": "Employee ID Recognizer", - "supported_language": "en", - "patterns": [ - {"name": "employee id", "regex": "EMP-[0-9]{6}", "score": 0.9} - ], - "context": ["employee", "id"], - "supported_entity": "EMPLOYEE_ID", - }, - { - "name": "Customer ID Recognizer", - "supported_language": "en", - "patterns": [ - {"name": "customer id", "regex": "CUST-[0-9]{8}", "score": 0.9} - ], - "context": ["customer", "id"], - "supported_entity": "CUSTOMER_ID", - }, - ] - # This is the core fix - mixed entity types in pii_entities_config pii_entities_config: Dict[Union[PiiEntityType, str], PiiAction] = { PiiEntityType.EMAIL_ADDRESS: PiiAction.MASK, @@ -55,7 +33,6 @@ def test_presidio_union_type_fix(): assert "CUSTOMER_ID" in entities_list print("✅ Union type fix verified: mixed entity types work correctly") - print("✅ Custom recognizers defined for EMPLOYEE_ID and CUSTOMER_ID") if __name__ == "__main__": From 8193f56876c72d8a4d4c0d8268efc051fea0b452 Mon Sep 17 00:00:00 2001 From: Arshdeep Singh Date: Thu, 25 Sep 2025 12:39:49 -0400 Subject: [PATCH 44/70] fix: remove print statement to pass ruff linting checks --- .../guardrails/guardrail_hooks/test_presidio_union_fix.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_litellm/proxy/guardrails/guardrail_hooks/test_presidio_union_fix.py b/tests/test_litellm/proxy/guardrails/guardrail_hooks/test_presidio_union_fix.py index 2de324c7e50..ff31249665b 100644 --- a/tests/test_litellm/proxy/guardrails/guardrail_hooks/test_presidio_union_fix.py +++ b/tests/test_litellm/proxy/guardrails/guardrail_hooks/test_presidio_union_fix.py @@ -2,8 +2,8 @@ Minimal test for Presidio Union[PiiEntityType, str] type fix. Tests only the core fix without heavy dependencies. """ -from typing import Union, Dict from enum import Enum +from typing import Dict, Union class PiiEntityType(str, Enum): @@ -32,7 +32,7 @@ def test_presidio_union_type_fix(): assert "EMPLOYEE_ID" in entities_list assert "CUSTOMER_ID" in entities_list - print("✅ Union type fix verified: mixed entity types work correctly") + # Union type fix verified: mixed entity types work correctly if __name__ == "__main__": From a9f39396b24ae35a278ef4d31f6aac77e962ef16 Mon Sep 17 00:00:00 2001 From: Arshdeep Singh Date: Thu, 25 Sep 2025 12:47:15 -0400 Subject: [PATCH 45/70] fix: resolve mypy type checking errors for Union type --- litellm/proxy/guardrails/guardrail_hooks/presidio.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/litellm/proxy/guardrails/guardrail_hooks/presidio.py b/litellm/proxy/guardrails/guardrail_hooks/presidio.py index 78ade173bf9..d2ec0471e78 100644 --- a/litellm/proxy/guardrails/guardrail_hooks/presidio.py +++ b/litellm/proxy/guardrails/guardrail_hooks/presidio.py @@ -68,7 +68,7 @@ class _OPTIONAL_PresidioPIIMasking(CustomGuardrail): output_parse_pii: Optional[bool] = False, presidio_ad_hoc_recognizers: Optional[str] = None, logging_only: Optional[bool] = None, - pii_entities_config: Optional[Dict[PiiEntityType, PiiAction]] = None, + pii_entities_config: Optional[Dict[Union[PiiEntityType, str], PiiAction]] = None, presidio_language: Optional[str] = None, **kwargs, ): @@ -173,7 +173,7 @@ class _OPTIONAL_PresidioPIIMasking(CustomGuardrail): analyze_payload["ad_hoc_recognizers"] = self.ad_hoc_recognizers if self.pii_entities_config: - analyze_payload["entities"] = list(self.pii_entities_config.keys()) + analyze_payload["entities"] = list(self.pii_entities_config.keys()) # type: ignore ################################################################## ######### End of adding config params From 471f011ae50424482b0bbce012323bb5c8d8b7e0 Mon Sep 17 00:00:00 2001 From: Arshdeep Singh Date: Thu, 25 Sep 2025 12:52:12 -0400 Subject: [PATCH 46/70] fix: add return type annotation for full mypy compliance --- .../proxy/guardrails/guardrail_hooks/test_presidio_union_fix.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_litellm/proxy/guardrails/guardrail_hooks/test_presidio_union_fix.py b/tests/test_litellm/proxy/guardrails/guardrail_hooks/test_presidio_union_fix.py index ff31249665b..841288948d5 100644 --- a/tests/test_litellm/proxy/guardrails/guardrail_hooks/test_presidio_union_fix.py +++ b/tests/test_litellm/proxy/guardrails/guardrail_hooks/test_presidio_union_fix.py @@ -15,7 +15,7 @@ class PiiAction(str, Enum): MASK = "MASK" -def test_presidio_union_type_fix(): +def test_presidio_union_type_fix() -> None: """Test that Union[PiiEntityType, str] allows both enum and string entity types""" # This is the core fix - mixed entity types in pii_entities_config From cb14bb341d4caadbe7904b03f1b7e33f0c424554 Mon Sep 17 00:00:00 2001 From: Arshdeep Singh Date: Thu, 25 Sep 2025 13:00:05 -0400 Subject: [PATCH 47/70] fix: update PresidioAnalyzeRequest type to support Union entities --- litellm/proxy/guardrails/guardrail_hooks/presidio.py | 2 +- litellm/types/proxy/guardrails/guardrail_hooks/presidio.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/litellm/proxy/guardrails/guardrail_hooks/presidio.py b/litellm/proxy/guardrails/guardrail_hooks/presidio.py index d2ec0471e78..e3f114c746f 100644 --- a/litellm/proxy/guardrails/guardrail_hooks/presidio.py +++ b/litellm/proxy/guardrails/guardrail_hooks/presidio.py @@ -173,7 +173,7 @@ class _OPTIONAL_PresidioPIIMasking(CustomGuardrail): analyze_payload["ad_hoc_recognizers"] = self.ad_hoc_recognizers if self.pii_entities_config: - analyze_payload["entities"] = list(self.pii_entities_config.keys()) # type: ignore + analyze_payload["entities"] = list(self.pii_entities_config.keys()) ################################################################## ######### End of adding config params diff --git a/litellm/types/proxy/guardrails/guardrail_hooks/presidio.py b/litellm/types/proxy/guardrails/guardrail_hooks/presidio.py index 5e480db4826..89e0f39c438 100644 --- a/litellm/types/proxy/guardrails/guardrail_hooks/presidio.py +++ b/litellm/types/proxy/guardrails/guardrail_hooks/presidio.py @@ -9,7 +9,7 @@ class PresidioAnalyzeRequest(TypedDict, total=False): text: str language: Optional[str] ad_hoc_recognizers: Optional[List[str]] - entities: Optional[List[PiiEntityType]] + entities: Optional[List[Union[PiiEntityType, str]]] class PresidioAnalyzeResponseItem(TypedDict, total=False): From 6916bf15de43fd93d5288e97acd934bde06c07af Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Thu, 25 Sep 2025 15:15:46 -0700 Subject: [PATCH 48/70] Remove 'Useful Links' tab from admin settings UI (#14918) - Removed 'Useful Links' tab from TabList in admin panel - Removed corresponding TabPanel containing UsefulLinksManagement component - Removed unused import for UsefulLinksManagement component - Fixes issue where useful links section was displaying blank content Co-authored-by: Cursor Agent --- ui/litellm-dashboard/src/components/admins.tsx | 8 -------- 1 file changed, 8 deletions(-) diff --git a/ui/litellm-dashboard/src/components/admins.tsx b/ui/litellm-dashboard/src/components/admins.tsx index 0d93b3645ee..ccd0e4e3049 100644 --- a/ui/litellm-dashboard/src/components/admins.tsx +++ b/ui/litellm-dashboard/src/components/admins.tsx @@ -45,7 +45,6 @@ import SSOModals from "./SSOModals"; import { ssoProviderConfigs } from './SSOModals'; import SCIMConfig from "./SCIM"; import UIAccessControlForm from "./UIAccessControlForm"; -import UsefulLinksManagement from "./useful_links_management"; import NotificationsManager from "./molecules/notifications_manager"; interface AdminPanelProps { @@ -555,7 +554,6 @@ const AdminPanel: React.FC = ({ Security Settings SCIM - Useful Links @@ -710,12 +708,6 @@ const AdminPanel: React.FC = ({ proxySettings={proxySettings} /> - - - From be44735de4c53087cffab6400a0075cf6aa9632a Mon Sep 17 00:00:00 2001 From: Teddy Amkie Date: Thu, 25 Sep 2025 15:45:13 -0700 Subject: [PATCH 49/70] Update docs to reflect session management for all models (#14914) Co-authored-by: Cursor Agent --- docs/my-website/docs/response_api.md | 4 ++-- docs/my-website/release_notes/v1.67.4-stable/index.md | 2 +- docs/my-website/static/llms-full.txt | 8 ++++---- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/my-website/docs/response_api.md b/docs/my-website/docs/response_api.md index 94d7c73be05..3e9d6683c2b 100644 --- a/docs/my-website/docs/response_api.md +++ b/docs/my-website/docs/response_api.md @@ -795,9 +795,9 @@ curl http://localhost:4000/v1/responses \ -## Session Management - Non-OpenAI Models +## Session Management -LiteLLM Proxy supports session management for non-OpenAI models. This allows you to store and fetch conversation history (state) in LiteLLM Proxy. +LiteLLM Proxy supports session management for all supported models. This allows you to store and fetch conversation history (state) in LiteLLM Proxy. #### Usage diff --git a/docs/my-website/release_notes/v1.67.4-stable/index.md b/docs/my-website/release_notes/v1.67.4-stable/index.md index 6750ced47c7..93a27155d2b 100644 --- a/docs/my-website/release_notes/v1.67.4-stable/index.md +++ b/docs/my-website/release_notes/v1.67.4-stable/index.md @@ -106,7 +106,7 @@ This release allow you to group requests to LiteLLM proxy into a session. If you 1. Added support for max_completion_tokens parameter [Get Started](https://docs.litellm.ai/docs/providers/sagemaker), [PR](https://github.com/BerriAI/litellm/pull/10300) - **Responses API** 1. Added support for GET and DELETE operations - `/v1/responses/{response_id}` [Get Started](../../docs/response_api) - 2. Added session management support for non-OpenAI models [PR](https://github.com/BerriAI/litellm/pull/10321) + 2. Added session management support for all supported models [PR](https://github.com/BerriAI/litellm/pull/10321) 3. Added routing affinity to maintain model consistency within sessions [Get Started](https://docs.litellm.ai/docs/response_api#load-balancing-with-routing-affinity), [PR](https://github.com/BerriAI/litellm/pull/10193) diff --git a/docs/my-website/static/llms-full.txt b/docs/my-website/static/llms-full.txt index c64d4170968..203dfd12bab 100644 --- a/docs/my-website/static/llms-full.txt +++ b/docs/my-website/static/llms-full.txt @@ -1699,7 +1699,7 @@ This release allow you to group requests to LiteLLM proxy into a session. If you 1. Added support for max\_completion\_tokens parameter [Get Started](https://docs.litellm.ai/docs/providers/sagemaker), [PR](https://github.com/BerriAI/litellm/pull/10300) - **Responses API** 1. Added support for GET and DELETE operations - `/v1/responses/{response_id}` [Get Started](https://docs.litellm.ai/docs/response_api) -2. Added session management support for non-OpenAI models [PR](https://github.com/BerriAI/litellm/pull/10321) +2. Added session management support for all supported models [PR](https://github.com/BerriAI/litellm/pull/10321) 3. Added routing affinity to maintain model consistency within sessions [Get Started](https://docs.litellm.ai/docs/response_api#load-balancing-with-routing-affinity), [PR](https://github.com/BerriAI/litellm/pull/10193) ## Spend Tracking Improvements [​](https://docs.litellm.ai/release_notes\#spend-tracking-improvements "Direct link to Spend Tracking Improvements") @@ -7736,7 +7736,7 @@ This release allow you to group requests to LiteLLM proxy into a session. If you 1. Added support for max\_completion\_tokens parameter [Get Started](https://docs.litellm.ai/docs/providers/sagemaker), [PR](https://github.com/BerriAI/litellm/pull/10300) - **Responses API** 1. Added support for GET and DELETE operations - `/v1/responses/{response_id}` [Get Started](https://docs.litellm.ai/docs/response_api) -2. Added session management support for non-OpenAI models [PR](https://github.com/BerriAI/litellm/pull/10321) +2. Added session management support for all supported models [PR](https://github.com/BerriAI/litellm/pull/10321) 3. Added routing affinity to maintain model consistency within sessions [Get Started](https://docs.litellm.ai/docs/response_api#load-balancing-with-routing-affinity), [PR](https://github.com/BerriAI/litellm/pull/10193) ## Spend Tracking Improvements [​](https://docs.litellm.ai/release_notes/tags/responses-api\#spend-tracking-improvements "Direct link to Spend Tracking Improvements") @@ -8295,7 +8295,7 @@ This release allow you to group requests to LiteLLM proxy into a session. If you 1. Added support for max\_completion\_tokens parameter [Get Started](https://docs.litellm.ai/docs/providers/sagemaker), [PR](https://github.com/BerriAI/litellm/pull/10300) - **Responses API** 1. Added support for GET and DELETE operations - `/v1/responses/{response_id}` [Get Started](https://docs.litellm.ai/docs/response_api) -2. Added session management support for non-OpenAI models [PR](https://github.com/BerriAI/litellm/pull/10321) +2. Added session management support for all supported models [PR](https://github.com/BerriAI/litellm/pull/10321) 3. Added routing affinity to maintain model consistency within sessions [Get Started](https://docs.litellm.ai/docs/response_api#load-balancing-with-routing-affinity), [PR](https://github.com/BerriAI/litellm/pull/10193) ## Spend Tracking Improvements [​](https://docs.litellm.ai/release_notes/tags/security\#spend-tracking-improvements "Direct link to Spend Tracking Improvements") @@ -8821,7 +8821,7 @@ This release allow you to group requests to LiteLLM proxy into a session. If you 1. Added support for max\_completion\_tokens parameter [Get Started](https://docs.litellm.ai/docs/providers/sagemaker), [PR](https://github.com/BerriAI/litellm/pull/10300) - **Responses API** 1. Added support for GET and DELETE operations - `/v1/responses/{response_id}` [Get Started](https://docs.litellm.ai/docs/response_api) -2. Added session management support for non-OpenAI models [PR](https://github.com/BerriAI/litellm/pull/10321) +2. Added session management support for all supported models [PR](https://github.com/BerriAI/litellm/pull/10321) 3. Added routing affinity to maintain model consistency within sessions [Get Started](https://docs.litellm.ai/docs/response_api#load-balancing-with-routing-affinity), [PR](https://github.com/BerriAI/litellm/pull/10193) ## Spend Tracking Improvements [​](https://docs.litellm.ai/release_notes/tags/session-management\#spend-tracking-improvements "Direct link to Spend Tracking Improvements") From ab5fb704c2aef7cb324984a39324f9bd90f37707 Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Thu, 25 Sep 2025 15:46:22 -0700 Subject: [PATCH 50/70] [Feat] Logging - `datadog` callback Log message content w/o sending to datadog (#14909) * add DatadogInitParams * fix _get_datadog_params * test_datadog_message_redaction --- litellm/__init__.py | 2 + litellm/integrations/datadog/datadog.py | 25 ++++++++- litellm/types/integrations/datadog.py | 10 ++++ tests/logging_callback_tests/test_datadog.py | 54 +++++++++++++++++++- 4 files changed, 89 insertions(+), 2 deletions(-) diff --git a/litellm/__init__.py b/litellm/__init__.py index 523ee36b865..69e5f78c997 100644 --- a/litellm/__init__.py +++ b/litellm/__init__.py @@ -17,6 +17,7 @@ from typing import ( TYPE_CHECKING, ) from litellm.types.integrations.datadog_llm_obs import DatadogLLMObsInitParams +from litellm.types.integrations.datadog import DatadogInitParams from litellm.llms.custom_httpx.http_handler import AsyncHTTPHandler, HTTPHandler from litellm.caching.caching import Cache, DualCache, RedisCache, InMemoryCache from litellm.caching.llm_caching_handler import LLMClientCache @@ -343,6 +344,7 @@ suppress_debug_info = False dynamodb_table_name: Optional[str] = None s3_callback_params: Optional[Dict] = None datadog_llm_observability_params: Optional[Union[DatadogLLMObsInitParams, Dict]] = None +datadog_params: Optional[Union[DatadogInitParams, Dict]] = None aws_sqs_callback_params: Optional[Dict] = None generic_logger_headers: Optional[Dict] = None default_key_generate_params: Optional[Dict] = None diff --git a/litellm/integrations/datadog/datadog.py b/litellm/integrations/datadog/datadog.py index 1fa651ec71c..f5a0883bea4 100644 --- a/litellm/integrations/datadog/datadog.py +++ b/litellm/integrations/datadog/datadog.py @@ -19,7 +19,7 @@ import os import traceback import uuid from datetime import datetime as datetimeObj -from typing import Any, List, Optional, Union +from typing import Any, Dict, List, Optional, Union import httpx from httpx import Response @@ -71,6 +71,13 @@ class DataDogLogger( raise Exception("DD_API_KEY is not set, set 'DD_API_KEY=<>") if os.getenv("DD_SITE", None) is None: raise Exception("DD_SITE is not set in .env, set 'DD_SITE=<>") + + ######################################################### + # Handle datadog_params set as litellm.datadog_params + ######################################################### + dict_datadog_params = self._get_datadog_params() + kwargs.update(dict_datadog_params) + self.async_client = get_async_httpx_client( llm_provider=httpxSpecialProvider.LoggingCallback ) @@ -101,6 +108,21 @@ class DataDogLogger( ) raise e + def _get_datadog_params(self) -> Dict: + """ + Get the datadog_params from litellm.datadog_params + + These are params specific to initializing the DataDogLogger e.g. turn_off_message_logging + """ + dict_datadog_params: Dict = {} + if litellm.datadog_params is not None: + if isinstance(litellm.datadog_params, DatadogInitParams): + dict_datadog_params = litellm.datadog_params.model_dump() + elif isinstance(litellm.datadog_params, Dict): + # only allow params that are of DatadogInitParams + dict_datadog_params = DatadogInitParams(**litellm.datadog_params).model_dump() + return dict_datadog_params + async def async_log_success_event(self, kwargs, response_obj, start_time, end_time): """ Async Log success events to Datadog @@ -458,6 +480,7 @@ class DataDogLogger( else: clean_metadata[key] = value + # Build the initial payload payload = { "id": id, diff --git a/litellm/types/integrations/datadog.py b/litellm/types/integrations/datadog.py index fce60b9c4b5..b7411843947 100644 --- a/litellm/types/integrations/datadog.py +++ b/litellm/types/integrations/datadog.py @@ -3,6 +3,8 @@ from typing import Optional from typing_extensions import TypedDict +from litellm.types.integrations.custom_logger import StandardCustomLoggerInitParams + DD_MAX_BATCH_SIZE = 1000 @@ -25,6 +27,14 @@ class DD_ERRORS(Enum): DATADOG_413_ERROR = "Datadog API Error - Payload too large (batch is above 5MB uncompressed). If you want this logged either disable request/response logging or set `DD_BATCH_SIZE=50`" +class DatadogInitParams(StandardCustomLoggerInitParams): + """ + Params for initializing a DataDog logger on litellm + """ + + pass + + class DatadogProxyFailureHookJsonMessage(TypedDict, total=False): exception: str error_class: str diff --git a/tests/logging_callback_tests/test_datadog.py b/tests/logging_callback_tests/test_datadog.py index 4b56684ade3..db6f193327c 100644 --- a/tests/logging_callback_tests/test_datadog.py +++ b/tests/logging_callback_tests/test_datadog.py @@ -24,7 +24,9 @@ from litellm.types.utils import ( StandardLoggingModelInformation, StandardLoggingMetadata, StandardLoggingHiddenParams, + LiteLLMCommonStrings, ) +from litellm.types.integrations.datadog import DatadogInitParams verbose_logger.setLevel(logging.DEBUG) @@ -576,4 +578,54 @@ def test_get_datadog_tags(): # Test with None request_tags standard_logging_obj["request_tags"] = None tags_none_request = DataDogLogger._get_datadog_tags(standard_logging_obj) - assert "request_tag:" not in tags_none_request \ No newline at end of file + assert "request_tag:" not in tags_none_request + + +@pytest.mark.asyncio +async def test_datadog_message_redaction(): + """ + Test that DataDog logger correctly initializes with turn_off_message_logging=True + from litellm.datadog_params + """ + try: + # Test using litellm.datadog_params pattern + litellm.datadog_params = DatadogInitParams(turn_off_message_logging=True) + + os.environ["DD_SITE"] = "https://fake.datadoghq.com" + os.environ["DD_API_KEY"] = "anything" + + # Mock the periodic flush to avoid async issues + with patch("asyncio.create_task"): + dd_logger = DataDogLogger() + + # Verify that turn_off_message_logging was set correctly from litellm.datadog_params + assert hasattr(dd_logger, 'turn_off_message_logging'), "DataDogLogger should have turn_off_message_logging attribute" + assert dd_logger.turn_off_message_logging is True, f"Expected turn_off_message_logging=True, got {dd_logger.turn_off_message_logging}" + + # Test the redaction method inherited from CustomLogger + model_call_details = { + "standard_logging_object": { + "messages": [{"role": "user", "content": "This is sensitive information that should be redacted"}], + "response": {"choices": [{"message": {"content": "This is a sensitive response that should be redacted"}}]} + } + } + + # Apply redaction using the inherited method + redacted_details = dd_logger.redact_standard_logging_payload_from_model_call_details(model_call_details) + redacted_str = LiteLLMCommonStrings.redacted_by_litellm.value + + # Verify that messages are redacted + redacted_standard_obj = redacted_details["standard_logging_object"] + assert redacted_standard_obj["messages"][0]["content"] == redacted_str, f"Messages not redacted. Got: {redacted_standard_obj['messages'][0]['content']}" + + # Verify that response is redacted + assert redacted_standard_obj["response"]["choices"][0]["message"]["content"] == redacted_str, f"Response not redacted. Got: {redacted_standard_obj['response']['choices'][0]['message']['content']}" + + print("✅ DataDog message redaction test passed") + + except Exception as e: + pytest.fail(f"Test failed with exception: {str(e)}") + finally: + # Clean up + litellm.datadog_params = None + litellm.callbacks = [] \ No newline at end of file From eaa04cd8ce29da739b32d37513d426e832209a2b Mon Sep 17 00:00:00 2001 From: Alexsander Hamir Date: Thu, 25 Sep 2025 15:47:01 -0700 Subject: [PATCH 51/70] fix: use fastuuid helper (#14903) * fix: use fastuuid helper across the codebase First batch of changes, simple drop in replacement. * second batch of changes * fixed: script mistake on helper file --- .../memory_usage/router_endpoint.py | 2 +- .../memory_usage/router_memory_usage copy.py | 2 +- .../memory_usage/router_memory_usage.py | 2 +- .../enterprise_callbacks/generic_api_callback.py | 2 +- .../proxy/common_utils/check_batch_cost.py | 2 +- .../proxy/hooks/managed_files.py | 2 +- litellm/caching/qdrant_semantic_cache.py | 4 ++-- .../integrations/azure_storage/azure_storage.py | 2 +- litellm/integrations/datadog/datadog.py | 2 +- litellm/integrations/datadog/datadog_llm_obs.py | 2 +- litellm/integrations/deepeval/deepeval.py | 2 +- litellm/integrations/dynamodb.py | 2 +- litellm/integrations/gcs_bucket/gcs_bucket.py | 2 +- litellm/integrations/lago.py | 2 +- litellm/integrations/langsmith.py | 2 +- litellm/integrations/literal_ai.py | 2 +- litellm/integrations/logfire_logger.py | 2 +- litellm/integrations/posthog.py | 2 +- litellm/litellm_core_utils/fallback_utils.py | 2 +- .../convert_dict_to_response.py | 2 +- .../prompt_templates/factory.py | 2 +- litellm/litellm_core_utils/streaming_handler.py | 2 +- .../adapters/streaming_iterator.py | 2 +- .../adapters/transformation.py | 2 +- litellm/llms/azure/audio_transcriptions.py | 2 +- .../bedrock/chat/invoke_agent/transformation.py | 2 +- litellm/llms/bedrock/chat/invoke_handler.py | 2 +- litellm/llms/bedrock/common_utils.py | 2 +- litellm/llms/bedrock/files/transformation.py | 2 +- litellm/llms/bedrock/rerank/transformation.py | 2 +- litellm/llms/deepinfra/rerank/transformation.py | 2 +- litellm/llms/fireworks_ai/chat/transformation.py | 2 +- litellm/llms/gemini/realtime/transformation.py | 2 +- .../llms/hosted_vllm/rerank/transformation.py | 2 +- .../llms/huggingface/rerank/transformation.py | 2 +- litellm/llms/infinity/rerank/transformation.py | 2 +- litellm/llms/jina_ai/rerank/transformation.py | 2 +- litellm/llms/ollama/chat/transformation.py | 2 +- litellm/llms/ollama/completion/transformation.py | 2 +- litellm/llms/ollama_chat.py | 2 +- .../llms/together_ai/rerank/transformation.py | 2 +- litellm/llms/vertex_ai/batches/transformation.py | 2 +- litellm/llms/vertex_ai/files/transformation.py | 2 +- .../gemini/vertex_and_google_ai_studio_gemini.py | 2 +- litellm/main.py | 2 +- litellm/proxy/_experimental/mcp_server/db.py | 2 +- litellm/proxy/_types.py | 2 +- litellm/proxy/client/cli/commands/auth.py | 2 +- .../db/db_transaction_queue/pod_lock_manager.py | 2 +- .../panw_prisma_airs/panw_prisma_airs.py | 2 +- .../proxy/guardrails/guardrail_hooks/presidio.py | 2 +- litellm/proxy/guardrails/guardrail_registry.py | 2 +- .../proxy/hooks/key_management_event_hooks.py | 2 +- .../proxy/hooks/user_management_event_hooks.py | 2 +- .../internal_user_endpoints.py | 2 +- .../key_management_endpoints.py | 2 +- .../model_management_endpoints.py | 2 +- .../organization_endpoints.py | 2 +- .../proxy/management_endpoints/scim/scim_v2.py | 2 +- .../proxy/management_endpoints/team_endpoints.py | 2 +- litellm/proxy/management_endpoints/ui_sso.py | 2 +- litellm/proxy/management_helpers/audit_logs.py | 2 +- .../object_permission_utils.py | 2 +- litellm/proxy/management_helpers/utils.py | 2 +- .../pass_through_endpoints.py | 8 ++++---- litellm/proxy/proxy_server.py | 2 +- .../proxy/spend_tracking/spend_tracking_utils.py | 2 +- .../ui_crud_endpoints/proxy_setting_endpoints.py | 2 +- litellm/proxy/utils.py | 2 +- litellm/responses/main.py | 2 +- .../responses/mcp/litellm_proxy_mcp_handler.py | 4 ++-- litellm/responses/mcp/mcp_streaming_iterator.py | 5 +---- litellm/router.py | 2 +- litellm/secret_managers/aws_secret_manager_v2.py | 2 +- litellm/types/router.py | 2 +- litellm/types/services.py | 2 +- .../test_prometheus_logging_callbacks.py | 2 +- tests/guardrails_tests/test_guardrails_config.py | 2 +- .../test_aws_secret_manager.py | 2 +- tests/litellm_utils_tests/test_hashicorp.py | 2 +- tests/litellm_utils_tests/test_secret_manager.py | 2 +- .../base_responses_api.py | 2 +- .../base_audio_transcription_unit_tests.py | 2 +- tests/llm_translation/base_llm_unit_tests.py | 2 +- tests/local_testing/cache_unit_tests.py | 2 +- tests/local_testing/test_add_update_models.py | 8 ++++---- tests/local_testing/test_alangfuse.py | 10 +++++----- .../test_amazing_vertex_completion.py | 2 +- tests/local_testing/test_audio_speech.py | 2 +- tests/local_testing/test_caching.py | 16 ++++++++-------- tests/local_testing/test_caching_handler.py | 2 +- .../local_testing/test_custom_callback_input.py | 2 +- tests/local_testing/test_dual_cache.py | 2 +- .../test_dynamic_rate_limit_handler.py | 2 +- tests/local_testing/test_gcs_bucket.py | 2 +- tests/local_testing/test_helicone_integration.py | 2 +- tests/local_testing/test_langsmith.py | 2 +- tests/local_testing/test_mem_usage.py | 2 +- .../local_testing/test_pass_through_endpoints.py | 2 +- tests/local_testing/test_router_timeout.py | 2 +- tests/local_testing/test_streaming.py | 4 ++-- tests/local_testing/test_timeout.py | 2 +- tests/local_testing/test_unit_test_caching.py | 2 +- tests/logging_callback_tests/test_alerting.py | 2 +- .../test_built_in_tools_cost_tracking.py | 4 ++-- .../test_langfuse_e2e_test.py | 2 +- .../test_moderations_api_logging.py | 2 +- tests/logging_callback_tests/test_spend_logs.py | 2 +- .../test_token_counting.py | 2 +- .../test_view_request_resp_logs.py | 2 +- .../test_update_team_e2e.py | 2 +- .../tests/bursty_load_test_completion.py | 2 +- .../tests/load_test_completion.py | 2 +- .../tests/load_test_embedding_100.py | 2 +- .../tests/test_simple_traceparent_openai.py | 2 +- tests/otel_tests/test_e2e_budgeting.py | 2 +- tests/otel_tests/test_e2e_model_access.py | 2 +- tests/otel_tests/test_guardrails.py | 2 +- tests/otel_tests/test_moderations.py | 2 +- tests/otel_tests/test_otel.py | 2 +- tests/otel_tests/test_prometheus.py | 2 +- tests/otel_tests/test_rerank.py | 2 +- tests/otel_tests/test_team_member_permissions.py | 2 +- tests/otel_tests/test_team_tag_routing.py | 2 +- .../proxy_admin_ui_tests/test_key_management.py | 8 ++++---- .../test_role_based_access.py | 2 +- .../test_route_check_unit_tests.py | 2 +- tests/proxy_admin_ui_tests/test_sso_sign_in.py | 4 ++-- .../proxy_admin_ui_tests/test_usage_endpoints.py | 2 +- tests/proxy_unit_tests/test_audit_logs_proxy.py | 4 ++-- .../test_custom_callback_input.py | 2 +- .../test_e2e_pod_lock_manager.py | 4 ++-- tests/proxy_unit_tests/test_jwt.py | 12 ++++++------ .../proxy_unit_tests/test_key_generate_prisma.py | 16 ++++++++-------- tests/proxy_unit_tests/test_proxy_server.py | 2 +- .../test_spend_accuracy_tests.py | 4 ++-- .../store_model_in_db_tests/test_mcp_servers.py | 2 +- .../store_model_in_db_tests/test_team_models.py | 4 ++-- tests/test_budget_management.py | 2 +- tests/test_callbacks_on_proxy.py | 4 ++-- tests/test_config.py | 2 +- tests/test_end_users.py | 2 +- tests/test_litellm/integrations/test_deepeval.py | 2 +- .../test_bedrock_agent_transformation.py | 2 +- .../test_ollama_completion_transformation.py | 2 +- .../scim/test_scim_transformations.py | 2 +- .../test_mcp_management_endpoints.py | 2 +- .../test_model_management_endpoints.py | 2 +- .../test_organization_endpoints.py | 2 +- .../management_endpoints/test_team_endpoints.py | 2 +- .../proxy/management_endpoints/test_ui_sso.py | 2 +- .../test_management_helpers_utils.py | 2 +- .../proxy/test_common_request_processing.py | 2 +- tests/test_models.py | 6 +++--- tests/test_openai_endpoints.py | 2 +- tests/test_team_logging.py | 4 ++-- tests/test_team_members.py | 2 +- tests/test_users.py | 2 +- .../vector_store_tests/base_vector_store_test.py | 2 +- 159 files changed, 204 insertions(+), 207 deletions(-) diff --git a/cookbook/litellm_router_load_test/memory_usage/router_endpoint.py b/cookbook/litellm_router_load_test/memory_usage/router_endpoint.py index 689f105bc5f..1dc2d914857 100644 --- a/cookbook/litellm_router_load_test/memory_usage/router_endpoint.py +++ b/cookbook/litellm_router_load_test/memory_usage/router_endpoint.py @@ -5,7 +5,7 @@ import os import litellm from litellm import Router from dotenv import load_dotenv -import uuid +from litellm._uuid import uuid load_dotenv() diff --git a/cookbook/litellm_router_load_test/memory_usage/router_memory_usage copy.py b/cookbook/litellm_router_load_test/memory_usage/router_memory_usage copy.py index a8aa506e8a2..76d5d3913f5 100644 --- a/cookbook/litellm_router_load_test/memory_usage/router_memory_usage copy.py +++ b/cookbook/litellm_router_load_test/memory_usage/router_memory_usage copy.py @@ -12,7 +12,7 @@ sys.path.insert( import litellm from litellm import Router from dotenv import load_dotenv -import uuid +from litellm._uuid import uuid load_dotenv() diff --git a/cookbook/litellm_router_load_test/memory_usage/router_memory_usage.py b/cookbook/litellm_router_load_test/memory_usage/router_memory_usage.py index a8aa506e8a2..76d5d3913f5 100644 --- a/cookbook/litellm_router_load_test/memory_usage/router_memory_usage.py +++ b/cookbook/litellm_router_load_test/memory_usage/router_memory_usage.py @@ -12,7 +12,7 @@ sys.path.insert( import litellm from litellm import Router from dotenv import load_dotenv -import uuid +from litellm._uuid import uuid load_dotenv() diff --git a/enterprise/litellm_enterprise/enterprise_callbacks/generic_api_callback.py b/enterprise/litellm_enterprise/enterprise_callbacks/generic_api_callback.py index d239be41257..7e259d4e19d 100644 --- a/enterprise/litellm_enterprise/enterprise_callbacks/generic_api_callback.py +++ b/enterprise/litellm_enterprise/enterprise_callbacks/generic_api_callback.py @@ -9,7 +9,7 @@ Callback to log events to a Generic API Endpoint import asyncio import os import traceback -import uuid +from litellm._uuid import uuid from typing import Dict, List, Optional, Union import litellm diff --git a/enterprise/litellm_enterprise/proxy/common_utils/check_batch_cost.py b/enterprise/litellm_enterprise/proxy/common_utils/check_batch_cost.py index 6edd198cd8e..4b1bb024ac6 100644 --- a/enterprise/litellm_enterprise/proxy/common_utils/check_batch_cost.py +++ b/enterprise/litellm_enterprise/proxy/common_utils/check_batch_cost.py @@ -2,7 +2,7 @@ Polls LiteLLM_ManagedObjectTable to check if the batch job is complete, and if the cost has been tracked. """ -import uuid +from litellm._uuid import uuid from datetime import datetime from typing import TYPE_CHECKING, Optional, cast diff --git a/enterprise/litellm_enterprise/proxy/hooks/managed_files.py b/enterprise/litellm_enterprise/proxy/hooks/managed_files.py index e069a89b9c5..e2963f8fb87 100644 --- a/enterprise/litellm_enterprise/proxy/hooks/managed_files.py +++ b/enterprise/litellm_enterprise/proxy/hooks/managed_files.py @@ -4,7 +4,7 @@ import asyncio import base64 import json -import uuid +from litellm._uuid import uuid from typing import TYPE_CHECKING, Any, Dict, List, Literal, Optional, Union, cast from fastapi import HTTPException diff --git a/litellm/caching/qdrant_semantic_cache.py b/litellm/caching/qdrant_semantic_cache.py index 32d4d8b0fdc..0e77b5a6c21 100644 --- a/litellm/caching/qdrant_semantic_cache.py +++ b/litellm/caching/qdrant_semantic_cache.py @@ -168,7 +168,7 @@ class QdrantSemanticCache(BaseCache): def set_cache(self, key, value, **kwargs): print_verbose(f"qdrant semantic-cache set_cache, kwargs: {kwargs}") - import uuid + from litellm._uuid import uuid # get the prompt messages = kwargs["messages"] @@ -279,7 +279,7 @@ class QdrantSemanticCache(BaseCache): pass async def async_set_cache(self, key, value, **kwargs): - import uuid + from litellm._uuid import uuid from litellm.proxy.proxy_server import llm_model_list, llm_router diff --git a/litellm/integrations/azure_storage/azure_storage.py b/litellm/integrations/azure_storage/azure_storage.py index 6ffb1e542fc..b4362665a4c 100644 --- a/litellm/integrations/azure_storage/azure_storage.py +++ b/litellm/integrations/azure_storage/azure_storage.py @@ -2,7 +2,7 @@ import asyncio import json import os import time -import uuid +from litellm._uuid import uuid from datetime import datetime, timedelta from typing import List, Optional diff --git a/litellm/integrations/datadog/datadog.py b/litellm/integrations/datadog/datadog.py index f5a0883bea4..0c62667f749 100644 --- a/litellm/integrations/datadog/datadog.py +++ b/litellm/integrations/datadog/datadog.py @@ -17,7 +17,7 @@ import asyncio import datetime import os import traceback -import uuid +from litellm._uuid import uuid from datetime import datetime as datetimeObj from typing import Any, Dict, List, Optional, Union diff --git a/litellm/integrations/datadog/datadog_llm_obs.py b/litellm/integrations/datadog/datadog_llm_obs.py index 2702192f637..fc3cf4b9ff2 100644 --- a/litellm/integrations/datadog/datadog_llm_obs.py +++ b/litellm/integrations/datadog/datadog_llm_obs.py @@ -9,7 +9,7 @@ API Reference: https://docs.datadoghq.com/llm_observability/setup/api/?tab=examp import asyncio import json import os -import uuid +from litellm._uuid import uuid from datetime import datetime from typing import Any, Dict, List, Literal, Optional, Union diff --git a/litellm/integrations/deepeval/deepeval.py b/litellm/integrations/deepeval/deepeval.py index f548ff50d73..972843e120a 100644 --- a/litellm/integrations/deepeval/deepeval.py +++ b/litellm/integrations/deepeval/deepeval.py @@ -1,5 +1,5 @@ import os -import uuid +from litellm._uuid import uuid from litellm.integrations.custom_logger import CustomLogger from litellm.integrations.deepeval.api import Api, Endpoints, HttpMethods from litellm.integrations.deepeval.types import ( diff --git a/litellm/integrations/dynamodb.py b/litellm/integrations/dynamodb.py index 2c527ea8aa9..dfc05ae1f32 100644 --- a/litellm/integrations/dynamodb.py +++ b/litellm/integrations/dynamodb.py @@ -3,7 +3,7 @@ import os import traceback -import uuid +from litellm._uuid import uuid from typing import Any import litellm diff --git a/litellm/integrations/gcs_bucket/gcs_bucket.py b/litellm/integrations/gcs_bucket/gcs_bucket.py index 972a0236666..9190f921d50 100644 --- a/litellm/integrations/gcs_bucket/gcs_bucket.py +++ b/litellm/integrations/gcs_bucket/gcs_bucket.py @@ -1,7 +1,7 @@ import asyncio import json import os -import uuid +from litellm._uuid import uuid from datetime import datetime, timedelta, timezone from typing import TYPE_CHECKING, Any, Dict, List, Optional from urllib.parse import quote diff --git a/litellm/integrations/lago.py b/litellm/integrations/lago.py index 5dfb1ce097d..b881193e869 100644 --- a/litellm/integrations/lago.py +++ b/litellm/integrations/lago.py @@ -3,7 +3,7 @@ import json import os -import uuid +from litellm._uuid import uuid from typing import Literal, Optional import httpx diff --git a/litellm/integrations/langsmith.py b/litellm/integrations/langsmith.py index 7783b704b46..cc9b361b69d 100644 --- a/litellm/integrations/langsmith.py +++ b/litellm/integrations/langsmith.py @@ -5,7 +5,7 @@ import os import random import traceback import types -import uuid +from litellm._uuid import uuid from datetime import datetime, timezone from typing import Any, Dict, List, Optional diff --git a/litellm/integrations/literal_ai.py b/litellm/integrations/literal_ai.py index 5bf9afd7eb4..042779ba844 100644 --- a/litellm/integrations/literal_ai.py +++ b/litellm/integrations/literal_ai.py @@ -2,7 +2,7 @@ # This file contains the LiteralAILogger class which is used to log steps to the LiteralAI observability platform. import asyncio import os -import uuid +from litellm._uuid import uuid from typing import List, Optional import httpx diff --git a/litellm/integrations/logfire_logger.py b/litellm/integrations/logfire_logger.py index 516bd4a8e28..2345dc869c6 100644 --- a/litellm/integrations/logfire_logger.py +++ b/litellm/integrations/logfire_logger.py @@ -3,7 +3,7 @@ import os import traceback -import uuid +from litellm._uuid import uuid from enum import Enum from typing import Any, Dict, NamedTuple diff --git a/litellm/integrations/posthog.py b/litellm/integrations/posthog.py index d321135f289..5298e538a72 100644 --- a/litellm/integrations/posthog.py +++ b/litellm/integrations/posthog.py @@ -11,7 +11,7 @@ For batching specific details see CustomBatchLogger class import asyncio import os -import uuid +from litellm._uuid import uuid from typing import Any, Dict, Optional diff --git a/litellm/litellm_core_utils/fallback_utils.py b/litellm/litellm_core_utils/fallback_utils.py index a5b0c85c816..7ce53862089 100644 --- a/litellm/litellm_core_utils/fallback_utils.py +++ b/litellm/litellm_core_utils/fallback_utils.py @@ -1,4 +1,4 @@ -import uuid +from litellm._uuid import uuid from typing import Optional import litellm diff --git a/litellm/litellm_core_utils/llm_response_utils/convert_dict_to_response.py b/litellm/litellm_core_utils/llm_response_utils/convert_dict_to_response.py index ce054b91cc9..2def52ed7e6 100644 --- a/litellm/litellm_core_utils/llm_response_utils/convert_dict_to_response.py +++ b/litellm/litellm_core_utils/llm_response_utils/convert_dict_to_response.py @@ -2,7 +2,7 @@ import asyncio import json import time import traceback -import uuid +from litellm._uuid import uuid from typing import Dict, Iterable, List, Literal, Optional, Tuple, Union import litellm diff --git a/litellm/litellm_core_utils/prompt_templates/factory.py b/litellm/litellm_core_utils/prompt_templates/factory.py index b9cc5e50c3b..c1d8a1cd41b 100644 --- a/litellm/litellm_core_utils/prompt_templates/factory.py +++ b/litellm/litellm_core_utils/prompt_templates/factory.py @@ -2,7 +2,7 @@ import copy import json import mimetypes import re -import uuid +from litellm._uuid import uuid import xml.etree.ElementTree as ET from enum import Enum from typing import Any, List, Optional, Tuple, cast, overload diff --git a/litellm/litellm_core_utils/streaming_handler.py b/litellm/litellm_core_utils/streaming_handler.py index bc0fbdf5c11..b2f6c621407 100644 --- a/litellm/litellm_core_utils/streaming_handler.py +++ b/litellm/litellm_core_utils/streaming_handler.py @@ -5,7 +5,7 @@ import json import threading import time import traceback -import uuid +from litellm._uuid import uuid from typing import Any, Callable, Dict, List, Optional, Union, cast import httpx diff --git a/litellm/llms/anthropic/experimental_pass_through/adapters/streaming_iterator.py b/litellm/llms/anthropic/experimental_pass_through/adapters/streaming_iterator.py index e4191a945f3..306bcd9bb2c 100644 --- a/litellm/llms/anthropic/experimental_pass_through/adapters/streaming_iterator.py +++ b/litellm/llms/anthropic/experimental_pass_through/adapters/streaming_iterator.py @@ -2,7 +2,7 @@ ## Translates OpenAI call to Anthropic `/v1/messages` format import json import traceback -import uuid +from litellm._uuid import uuid from collections import deque from typing import TYPE_CHECKING, Any, AsyncIterator, Iterator, Literal, Optional diff --git a/litellm/llms/anthropic/experimental_pass_through/adapters/transformation.py b/litellm/llms/anthropic/experimental_pass_through/adapters/transformation.py index d38e7adc231..7de2a1e1c66 100644 --- a/litellm/llms/anthropic/experimental_pass_through/adapters/transformation.py +++ b/litellm/llms/anthropic/experimental_pass_through/adapters/transformation.py @@ -458,7 +458,7 @@ class LiteLLMAnthropicMessagesAdapter: Literal["text", "tool_use"], "ContentBlockContentBlockDict", ]: - import uuid + from litellm._uuid import uuid from litellm.types.llms.anthropic import TextBlock, ToolUseBlock diff --git a/litellm/llms/azure/audio_transcriptions.py b/litellm/llms/azure/audio_transcriptions.py index 1f09ac7574a..8519b1c35a5 100644 --- a/litellm/llms/azure/audio_transcriptions.py +++ b/litellm/llms/azure/audio_transcriptions.py @@ -1,4 +1,4 @@ -import uuid +from litellm._uuid import uuid from typing import Any, Coroutine, Optional, Union from openai import AsyncAzureOpenAI, AzureOpenAI diff --git a/litellm/llms/bedrock/chat/invoke_agent/transformation.py b/litellm/llms/bedrock/chat/invoke_agent/transformation.py index e4ff6d398ea..4a7c33ed204 100644 --- a/litellm/llms/bedrock/chat/invoke_agent/transformation.py +++ b/litellm/llms/bedrock/chat/invoke_agent/transformation.py @@ -5,7 +5,7 @@ https://docs.aws.amazon.com/bedrock/latest/APIReference/API_agent-runtime_Invoke """ import base64 import json -import uuid +from litellm._uuid import uuid from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple, Union import httpx diff --git a/litellm/llms/bedrock/chat/invoke_handler.py b/litellm/llms/bedrock/chat/invoke_handler.py index 42cdb34fc1a..0d1b4102c94 100644 --- a/litellm/llms/bedrock/chat/invoke_handler.py +++ b/litellm/llms/bedrock/chat/invoke_handler.py @@ -7,7 +7,7 @@ import json import time import types import urllib.parse -import uuid +from litellm._uuid import uuid from functools import partial from typing import ( Any, diff --git a/litellm/llms/bedrock/common_utils.py b/litellm/llms/bedrock/common_utils.py index 63c366c9480..2b111cde600 100644 --- a/litellm/llms/bedrock/common_utils.py +++ b/litellm/llms/bedrock/common_utils.py @@ -802,7 +802,7 @@ class CommonBatchFilesUtils: Tuple of (bucket_name, object_key) """ import time - import uuid + from litellm._uuid import uuid # Get bucket name bucket_name = ( diff --git a/litellm/llms/bedrock/files/transformation.py b/litellm/llms/bedrock/files/transformation.py index d493deddb62..0a95cf9168f 100644 --- a/litellm/llms/bedrock/files/transformation.py +++ b/litellm/llms/bedrock/files/transformation.py @@ -1,7 +1,7 @@ import json import os import time -import uuid +from litellm._uuid import uuid from typing import Any, Dict, List, Optional, Tuple, Union from httpx import Headers, Response diff --git a/litellm/llms/bedrock/rerank/transformation.py b/litellm/llms/bedrock/rerank/transformation.py index be8250a9671..b5d33eda49f 100644 --- a/litellm/llms/bedrock/rerank/transformation.py +++ b/litellm/llms/bedrock/rerank/transformation.py @@ -4,7 +4,7 @@ Translates from Cohere's `/v1/rerank` input format to Bedrock's `/rerank` input Why separate file? Make it easy to see how transformation works """ -import uuid +from litellm._uuid import uuid from typing import List, Optional, Union from litellm.types.llms.bedrock import ( diff --git a/litellm/llms/deepinfra/rerank/transformation.py b/litellm/llms/deepinfra/rerank/transformation.py index 8259c6075bb..6a3244a3c88 100644 --- a/litellm/llms/deepinfra/rerank/transformation.py +++ b/litellm/llms/deepinfra/rerank/transformation.py @@ -2,7 +2,7 @@ Translate between Cohere's `/rerank` format and Deepinfra's `/rerank` format. """ -import uuid +from litellm._uuid import uuid from typing import Any, Dict, List, Optional, Union import httpx diff --git a/litellm/llms/fireworks_ai/chat/transformation.py b/litellm/llms/fireworks_ai/chat/transformation.py index 31d749032b4..524b1c97145 100644 --- a/litellm/llms/fireworks_ai/chat/transformation.py +++ b/litellm/llms/fireworks_ai/chat/transformation.py @@ -1,5 +1,5 @@ import json -import uuid +from litellm._uuid import uuid from typing import Any, List, Literal, Optional, Tuple, Union, cast import httpx diff --git a/litellm/llms/gemini/realtime/transformation.py b/litellm/llms/gemini/realtime/transformation.py index f32a404c9e8..e1dd6f146f3 100644 --- a/litellm/llms/gemini/realtime/transformation.py +++ b/litellm/llms/gemini/realtime/transformation.py @@ -3,7 +3,7 @@ This file contains the transformation logic for the Gemini realtime API. """ import json -import uuid +from litellm._uuid import uuid from typing import Any, Dict, List, Optional, Union, cast from litellm import verbose_logger diff --git a/litellm/llms/hosted_vllm/rerank/transformation.py b/litellm/llms/hosted_vllm/rerank/transformation.py index 419327d9d5c..c0a57c43534 100644 --- a/litellm/llms/hosted_vllm/rerank/transformation.py +++ b/litellm/llms/hosted_vllm/rerank/transformation.py @@ -2,7 +2,7 @@ Transformation logic for Hosted VLLM rerank """ -import uuid +from litellm._uuid import uuid from typing import Any, Dict, List, Optional, Union from litellm.types.rerank import ( diff --git a/litellm/llms/huggingface/rerank/transformation.py b/litellm/llms/huggingface/rerank/transformation.py index 9c3234c3512..aa0f37bc6ba 100644 --- a/litellm/llms/huggingface/rerank/transformation.py +++ b/litellm/llms/huggingface/rerank/transformation.py @@ -1,5 +1,5 @@ import os -import uuid +from litellm._uuid import uuid from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple, Union import httpx diff --git a/litellm/llms/infinity/rerank/transformation.py b/litellm/llms/infinity/rerank/transformation.py index 4b75fa121b2..6259d445aec 100644 --- a/litellm/llms/infinity/rerank/transformation.py +++ b/litellm/llms/infinity/rerank/transformation.py @@ -4,7 +4,7 @@ Transformation logic from Cohere's /v1/rerank format to Infinity's `/v1/rerank` Why separate file? Make it easy to see how transformation works """ -import uuid +from litellm._uuid import uuid from typing import List, Optional import httpx diff --git a/litellm/llms/jina_ai/rerank/transformation.py b/litellm/llms/jina_ai/rerank/transformation.py index 8d0a9b1431c..d8569b01b83 100644 --- a/litellm/llms/jina_ai/rerank/transformation.py +++ b/litellm/llms/jina_ai/rerank/transformation.py @@ -6,7 +6,7 @@ Why separate file? Make it easy to see how transformation works Docs - https://jina.ai/reranker """ -import uuid +from litellm._uuid import uuid from typing import Any, Dict, List, Optional, Tuple, Union from httpx import URL, Response diff --git a/litellm/llms/ollama/chat/transformation.py b/litellm/llms/ollama/chat/transformation.py index 3527a579218..3b755e79330 100644 --- a/litellm/llms/ollama/chat/transformation.py +++ b/litellm/llms/ollama/chat/transformation.py @@ -1,6 +1,6 @@ import json import time -import uuid +from litellm._uuid import uuid from typing import ( TYPE_CHECKING, Any, diff --git a/litellm/llms/ollama/completion/transformation.py b/litellm/llms/ollama/completion/transformation.py index bfb0b7f1877..981a987ec91 100644 --- a/litellm/llms/ollama/completion/transformation.py +++ b/litellm/llms/ollama/completion/transformation.py @@ -1,6 +1,6 @@ import json import time -import uuid +from litellm._uuid import uuid from typing import TYPE_CHECKING, Any, AsyncIterator, Iterator, List, Optional, Union from httpx._models import Headers, Response diff --git a/litellm/llms/ollama_chat.py b/litellm/llms/ollama_chat.py index d46e7145194..082312d28f2 100644 --- a/litellm/llms/ollama_chat.py +++ b/litellm/llms/ollama_chat.py @@ -1,6 +1,6 @@ import json import time -import uuid +from litellm._uuid import uuid from typing import Any, List, Optional, Union import aiohttp diff --git a/litellm/llms/together_ai/rerank/transformation.py b/litellm/llms/together_ai/rerank/transformation.py index 1fdb772adde..63b593dfe42 100644 --- a/litellm/llms/together_ai/rerank/transformation.py +++ b/litellm/llms/together_ai/rerank/transformation.py @@ -4,7 +4,7 @@ Transformation logic from Cohere's /v1/rerank format to Together AI's `/v1/rera Why separate file? Make it easy to see how transformation works """ -import uuid +from litellm._uuid import uuid from typing import List, Optional from litellm.types.rerank import ( diff --git a/litellm/llms/vertex_ai/batches/transformation.py b/litellm/llms/vertex_ai/batches/transformation.py index 5b6d21b5948..22cd0bd402a 100644 --- a/litellm/llms/vertex_ai/batches/transformation.py +++ b/litellm/llms/vertex_ai/batches/transformation.py @@ -1,4 +1,4 @@ -import uuid +from litellm._uuid import uuid from typing import Dict from litellm.llms.vertex_ai.common_utils import ( diff --git a/litellm/llms/vertex_ai/files/transformation.py b/litellm/llms/vertex_ai/files/transformation.py index f2e5a5b5d25..01f6c86fd4d 100644 --- a/litellm/llms/vertex_ai/files/transformation.py +++ b/litellm/llms/vertex_ai/files/transformation.py @@ -1,7 +1,7 @@ import json import os import time -import uuid +from litellm._uuid import uuid from typing import Any, Dict, List, Optional, Tuple, Union from httpx import Headers, Response diff --git a/litellm/llms/vertex_ai/gemini/vertex_and_google_ai_studio_gemini.py b/litellm/llms/vertex_ai/gemini/vertex_and_google_ai_studio_gemini.py index 9376b28cbec..dc3a6cf15e5 100644 --- a/litellm/llms/vertex_ai/gemini/vertex_and_google_ai_studio_gemini.py +++ b/litellm/llms/vertex_ai/gemini/vertex_and_google_ai_studio_gemini.py @@ -3,7 +3,7 @@ ## Initial implementation - covers gemini + image gen calls import json import time -import uuid +from litellm._uuid import uuid from copy import deepcopy from functools import partial from typing import ( diff --git a/litellm/main.py b/litellm/main.py index 50de09c3dbf..f0b1820cbb5 100644 --- a/litellm/main.py +++ b/litellm/main.py @@ -17,7 +17,7 @@ import random import sys import time import traceback -import uuid +from litellm._uuid import uuid from concurrent import futures from concurrent.futures import FIRST_COMPLETED, ThreadPoolExecutor, wait from copy import deepcopy diff --git a/litellm/proxy/_experimental/mcp_server/db.py b/litellm/proxy/_experimental/mcp_server/db.py index d5d9f978908..79e3b0f7623 100644 --- a/litellm/proxy/_experimental/mcp_server/db.py +++ b/litellm/proxy/_experimental/mcp_server/db.py @@ -1,4 +1,4 @@ -import uuid +from litellm._uuid import uuid from typing import Any, Dict, Iterable, List, Optional, Set, Union from litellm._logging import verbose_proxy_logger diff --git a/litellm/proxy/_types.py b/litellm/proxy/_types.py index a92b7d8ad2e..6f2732af371 100644 --- a/litellm/proxy/_types.py +++ b/litellm/proxy/_types.py @@ -1,6 +1,6 @@ import enum import json -import uuid +from litellm._uuid import uuid from datetime import datetime from typing import ( TYPE_CHECKING, diff --git a/litellm/proxy/client/cli/commands/auth.py b/litellm/proxy/client/cli/commands/auth.py index 78cbb5ecf82..ff9f05fd32a 100644 --- a/litellm/proxy/client/cli/commands/auth.py +++ b/litellm/proxy/client/cli/commands/auth.py @@ -336,7 +336,7 @@ def _handle_team_assignment(base_url: str, api_key: str, user_id: str) -> None: @click.pass_context def login(ctx: click.Context): """Login to LiteLLM proxy using SSO authentication""" - import uuid + from litellm._uuid import uuid from litellm.constants import LITELLM_CLI_SOURCE_IDENTIFIER from litellm.proxy.client.cli.interface import show_commands diff --git a/litellm/proxy/db/db_transaction_queue/pod_lock_manager.py b/litellm/proxy/db/db_transaction_queue/pod_lock_manager.py index 2dab963b02a..bb5424b0e90 100644 --- a/litellm/proxy/db/db_transaction_queue/pod_lock_manager.py +++ b/litellm/proxy/db/db_transaction_queue/pod_lock_manager.py @@ -1,5 +1,5 @@ import asyncio -import uuid +from litellm._uuid import uuid from typing import TYPE_CHECKING, Any, Optional from litellm._logging import verbose_proxy_logger diff --git a/litellm/proxy/guardrails/guardrail_hooks/panw_prisma_airs/panw_prisma_airs.py b/litellm/proxy/guardrails/guardrail_hooks/panw_prisma_airs/panw_prisma_airs.py index db72a9e9d29..8ca29506771 100644 --- a/litellm/proxy/guardrails/guardrail_hooks/panw_prisma_airs/panw_prisma_airs.py +++ b/litellm/proxy/guardrails/guardrail_hooks/panw_prisma_airs/panw_prisma_airs.py @@ -5,7 +5,7 @@ PANW Prisma AIRS Built-in Guardrail for LiteLLM """ import os -import uuid +from litellm._uuid import uuid from typing import TYPE_CHECKING, Any, Dict, List, Literal, Optional, Type, cast from fastapi import HTTPException diff --git a/litellm/proxy/guardrails/guardrail_hooks/presidio.py b/litellm/proxy/guardrails/guardrail_hooks/presidio.py index e3f114c746f..38a17595c46 100644 --- a/litellm/proxy/guardrails/guardrail_hooks/presidio.py +++ b/litellm/proxy/guardrails/guardrail_hooks/presidio.py @@ -10,7 +10,7 @@ import asyncio import json -import uuid +from litellm._uuid import uuid from datetime import datetime from typing import ( Any, diff --git a/litellm/proxy/guardrails/guardrail_registry.py b/litellm/proxy/guardrails/guardrail_registry.py index 69c8fd8084e..b5ae8437d86 100644 --- a/litellm/proxy/guardrails/guardrail_registry.py +++ b/litellm/proxy/guardrails/guardrail_registry.py @@ -2,7 +2,7 @@ import importlib import os -import uuid +from litellm._uuid import uuid from datetime import datetime, timezone from typing import Dict, List, Optional, Type, cast diff --git a/litellm/proxy/hooks/key_management_event_hooks.py b/litellm/proxy/hooks/key_management_event_hooks.py index 0289a929613..7ad1aaffc10 100644 --- a/litellm/proxy/hooks/key_management_event_hooks.py +++ b/litellm/proxy/hooks/key_management_event_hooks.py @@ -1,6 +1,6 @@ import asyncio import json -import uuid +from litellm._uuid import uuid from datetime import datetime, timezone from typing import Any, List, Optional diff --git a/litellm/proxy/hooks/user_management_event_hooks.py b/litellm/proxy/hooks/user_management_event_hooks.py index cc33f63837c..9579298e5c7 100644 --- a/litellm/proxy/hooks/user_management_event_hooks.py +++ b/litellm/proxy/hooks/user_management_event_hooks.py @@ -3,7 +3,7 @@ Hooks that are triggered when a litellm user event occurs """ import asyncio -import uuid +from litellm._uuid import uuid from datetime import datetime, timezone from typing import Optional diff --git a/litellm/proxy/management_endpoints/internal_user_endpoints.py b/litellm/proxy/management_endpoints/internal_user_endpoints.py index 68e6342d6b6..66085b69b3d 100644 --- a/litellm/proxy/management_endpoints/internal_user_endpoints.py +++ b/litellm/proxy/management_endpoints/internal_user_endpoints.py @@ -14,7 +14,7 @@ These are members of a Team on LiteLLM import asyncio import json import traceback -import uuid +from litellm._uuid import uuid from datetime import datetime, timezone from typing import Any, Dict, List, Optional, Union, cast diff --git a/litellm/proxy/management_endpoints/key_management_endpoints.py b/litellm/proxy/management_endpoints/key_management_endpoints.py index eb389450f91..aa56cc68a8a 100644 --- a/litellm/proxy/management_endpoints/key_management_endpoints.py +++ b/litellm/proxy/management_endpoints/key_management_endpoints.py @@ -14,7 +14,7 @@ import copy import json import secrets import traceback -import uuid +from litellm._uuid import uuid from datetime import datetime, timedelta, timezone from typing import List, Literal, Optional, Tuple, cast diff --git a/litellm/proxy/management_endpoints/model_management_endpoints.py b/litellm/proxy/management_endpoints/model_management_endpoints.py index 9180e6100be..8f9c6edd2b7 100644 --- a/litellm/proxy/management_endpoints/model_management_endpoints.py +++ b/litellm/proxy/management_endpoints/model_management_endpoints.py @@ -13,7 +13,7 @@ model/{model_id}/update - PATCH endpoint for model update. import asyncio import datetime import json -import uuid +from litellm._uuid import uuid from typing import Dict, List, Literal, Optional, Tuple, Union, cast from fastapi import APIRouter, Depends, HTTPException, Request, status diff --git a/litellm/proxy/management_endpoints/organization_endpoints.py b/litellm/proxy/management_endpoints/organization_endpoints.py index c3fca196d57..2b52b6fc51c 100644 --- a/litellm/proxy/management_endpoints/organization_endpoints.py +++ b/litellm/proxy/management_endpoints/organization_endpoints.py @@ -11,7 +11,7 @@ Endpoints for /organization operations #### ORGANIZATION MANAGEMENT #### -import uuid +from litellm._uuid import uuid from typing import List, Optional, Tuple from fastapi import APIRouter, Depends, HTTPException, Request, status diff --git a/litellm/proxy/management_endpoints/scim/scim_v2.py b/litellm/proxy/management_endpoints/scim/scim_v2.py index 6720f0c3b71..fd7d150a7f4 100644 --- a/litellm/proxy/management_endpoints/scim/scim_v2.py +++ b/litellm/proxy/management_endpoints/scim/scim_v2.py @@ -4,7 +4,7 @@ This is an enterprise feature and requires a premium license. """ -import uuid +from litellm._uuid import uuid from typing import Any, Dict, List, Optional, Set, Tuple from fastapi import ( diff --git a/litellm/proxy/management_endpoints/team_endpoints.py b/litellm/proxy/management_endpoints/team_endpoints.py index 97663863446..cbea3da1744 100644 --- a/litellm/proxy/management_endpoints/team_endpoints.py +++ b/litellm/proxy/management_endpoints/team_endpoints.py @@ -12,7 +12,7 @@ All /team management endpoints import asyncio import json import traceback -import uuid +from litellm._uuid import uuid from datetime import datetime, timezone from typing import Any, Dict, List, Optional, Tuple, Union, cast diff --git a/litellm/proxy/management_endpoints/ui_sso.py b/litellm/proxy/management_endpoints/ui_sso.py index 2af940ec654..dc8b96656c4 100644 --- a/litellm/proxy/management_endpoints/ui_sso.py +++ b/litellm/proxy/management_endpoints/ui_sso.py @@ -10,7 +10,7 @@ Has all /sso/* routes import asyncio import os -import uuid +from litellm._uuid import uuid from copy import deepcopy from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple, Union, cast diff --git a/litellm/proxy/management_helpers/audit_logs.py b/litellm/proxy/management_helpers/audit_logs.py index b1b2fdf1ca9..ea082f468ae 100644 --- a/litellm/proxy/management_helpers/audit_logs.py +++ b/litellm/proxy/management_helpers/audit_logs.py @@ -3,7 +3,7 @@ Functions to create audit logs for LiteLLM Proxy """ import json -import uuid +from litellm._uuid import uuid from datetime import datetime, timezone import litellm diff --git a/litellm/proxy/management_helpers/object_permission_utils.py b/litellm/proxy/management_helpers/object_permission_utils.py index 3b55974b7bf..5299bc9209d 100644 --- a/litellm/proxy/management_helpers/object_permission_utils.py +++ b/litellm/proxy/management_helpers/object_permission_utils.py @@ -4,7 +4,7 @@ organizations, teams, and keys. """ import json -import uuid +from litellm._uuid import uuid from typing import Dict, Optional, Union from litellm._logging import verbose_proxy_logger diff --git a/litellm/proxy/management_helpers/utils.py b/litellm/proxy/management_helpers/utils.py index 246fa0b6390..e25208a216a 100644 --- a/litellm/proxy/management_helpers/utils.py +++ b/litellm/proxy/management_helpers/utils.py @@ -1,6 +1,6 @@ # What is this? ## Helper utils for the management endpoints (keys/users/teams) -import uuid +from litellm._uuid import uuid from datetime import datetime from functools import wraps from typing import Optional, Tuple diff --git a/litellm/proxy/pass_through_endpoints/pass_through_endpoints.py b/litellm/proxy/pass_through_endpoints/pass_through_endpoints.py index a1f43d0ca50..628784f6f1f 100644 --- a/litellm/proxy/pass_through_endpoints/pass_through_endpoints.py +++ b/litellm/proxy/pass_through_endpoints/pass_through_endpoints.py @@ -3,7 +3,7 @@ import asyncio import copy import json import traceback -import uuid +from litellm._uuid import uuid from base64 import b64encode from datetime import datetime from typing import Dict, List, Optional, Tuple, Union @@ -922,7 +922,7 @@ def create_pass_through_route( cost_per_request: Optional[float] = None, ): # check if target is an adapter.py or a url - import uuid + from litellm._uuid import uuid from litellm.proxy.types_utils.utils import get_instance_fn @@ -1123,7 +1123,7 @@ async def initialize_pass_through_endpoints( Returns: None """ - import uuid + from litellm._uuid import uuid verbose_proxy_logger.debug("initializing pass through endpoints") from litellm.proxy._types import CommonProxyErrors, LiteLLMRoutes @@ -1366,7 +1366,7 @@ async def create_pass_through_endpoints( """ Create new pass-through endpoint """ - import uuid + from litellm._uuid import uuid from litellm.proxy.proxy_server import ( get_config_general_settings, diff --git a/litellm/proxy/proxy_server.py b/litellm/proxy/proxy_server.py index 7255161b8bc..9fff2478804 100644 --- a/litellm/proxy/proxy_server.py +++ b/litellm/proxy/proxy_server.py @@ -9,7 +9,7 @@ import subprocess import sys import time import traceback -import uuid +from litellm._uuid import uuid import warnings from datetime import datetime, timedelta from typing import ( diff --git a/litellm/proxy/spend_tracking/spend_tracking_utils.py b/litellm/proxy/spend_tracking/spend_tracking_utils.py index 8c59020bc24..7b78200b043 100644 --- a/litellm/proxy/spend_tracking/spend_tracking_utils.py +++ b/litellm/proxy/spend_tracking/spend_tracking_utils.py @@ -370,7 +370,7 @@ def _get_session_id_for_spend_log( This ensures each spend log is associated with a unique session id. """ - import uuid + from litellm._uuid import uuid if ( standard_logging_payload is not None diff --git a/litellm/proxy/ui_crud_endpoints/proxy_setting_endpoints.py b/litellm/proxy/ui_crud_endpoints/proxy_setting_endpoints.py index e2ca3078384..8c11760904c 100644 --- a/litellm/proxy/ui_crud_endpoints/proxy_setting_endpoints.py +++ b/litellm/proxy/ui_crud_endpoints/proxy_setting_endpoints.py @@ -671,7 +671,7 @@ async def upload_logo(file: UploadFile = File(...)): os.makedirs(upload_dir, exist_ok=True) # Generate unique filename - import uuid + from litellm._uuid import uuid unique_filename = f"logo_{uuid.uuid4().hex}{file_extension}" file_path = os.path.join(upload_dir, unique_filename) diff --git a/litellm/proxy/utils.py b/litellm/proxy/utils.py index f3affa707ae..036b3174b63 100644 --- a/litellm/proxy/utils.py +++ b/litellm/proxy/utils.py @@ -1308,7 +1308,7 @@ class ProxyLogging: "litellm_logging_obj", None ) if litellm_logging_obj is None: - import uuid + from litellm._uuid import uuid request_data["litellm_call_id"] = str(uuid.uuid4()) user_api_key_logged_metadata = ( diff --git a/litellm/responses/main.py b/litellm/responses/main.py index d3cb5a7de2a..c190d8e2d21 100644 --- a/litellm/responses/main.py +++ b/litellm/responses/main.py @@ -211,7 +211,7 @@ async def aresponses_api_with_mcp( # Handle MCP streaming if requested if stream and mcp_tools_with_litellm_proxy: # Generate MCP discovery events using the already processed tools - import uuid + from litellm._uuid import uuid from litellm.responses.mcp.mcp_streaming_iterator import ( create_mcp_list_tools_events, diff --git a/litellm/responses/mcp/litellm_proxy_mcp_handler.py b/litellm/responses/mcp/litellm_proxy_mcp_handler.py index 3e5ab38af2f..2600d53a171 100644 --- a/litellm/responses/mcp/litellm_proxy_mcp_handler.py +++ b/litellm/responses/mcp/litellm_proxy_mcp_handler.py @@ -630,7 +630,7 @@ class LiteLLM_Proxy_MCP_Handler: Returns: List of MCP tool execution events for streaming """ - import uuid + from litellm._uuid import uuid from litellm.responses.mcp.mcp_streaming_iterator import create_mcp_call_events @@ -714,7 +714,7 @@ class LiteLLM_Proxy_MCP_Handler: """Add custom output elements to the final response for MCP tool execution.""" # Import the required classes for creating output items import json - import uuid + from litellm._uuid import uuid from litellm.types.responses.main import GenericResponseOutputItem, OutputText diff --git a/litellm/responses/mcp/mcp_streaming_iterator.py b/litellm/responses/mcp/mcp_streaming_iterator.py index 8045d1c1dca..7d53452c1c0 100644 --- a/litellm/responses/mcp/mcp_streaming_iterator.py +++ b/litellm/responses/mcp/mcp_streaming_iterator.py @@ -1,4 +1,4 @@ -import uuid +from litellm._uuid import uuid from typing import ( TYPE_CHECKING, Any, @@ -444,9 +444,6 @@ class MCPEnhancedStreamingIterator(BaseResponsesAPIStreamingIterator): """Generate tool execution events and execute tools""" if not self.collected_response: return - - import uuid - from litellm.responses.mcp.litellm_proxy_mcp_handler import ( LiteLLM_Proxy_MCP_Handler, ) diff --git a/litellm/router.py b/litellm/router.py index 1d6b4f5dd9b..9c87f0bb029 100644 --- a/litellm/router.py +++ b/litellm/router.py @@ -17,7 +17,7 @@ import logging import threading import time import traceback -import uuid +from litellm._uuid import uuid from collections import defaultdict from functools import lru_cache from typing import ( diff --git a/litellm/secret_managers/aws_secret_manager_v2.py b/litellm/secret_managers/aws_secret_manager_v2.py index 327dbf3d196..ff9921ebcb5 100644 --- a/litellm/secret_managers/aws_secret_manager_v2.py +++ b/litellm/secret_managers/aws_secret_manager_v2.py @@ -215,7 +215,7 @@ class AWSSecretsManagerV2(BaseAWSLLM, BaseSecretManager): optional_params: Additional AWS parameters timeout: Request timeout """ - import uuid + from litellm._uuid import uuid # Prepare the request data data = {"Name": secret_name, "SecretString": secret_value} diff --git a/litellm/types/router.py b/litellm/types/router.py index 8bed2bdccda..62f1c13eb4b 100644 --- a/litellm/types/router.py +++ b/litellm/types/router.py @@ -4,7 +4,7 @@ litellm.Router Types - includes RouterConfig, UpdateRouterConfig, ModelInfo etc import datetime import enum -import uuid +from litellm._uuid import uuid from dataclasses import dataclass from typing import Any, Dict, List, Literal, Optional, Tuple, Union, get_type_hints diff --git a/litellm/types/services.py b/litellm/types/services.py index e038100e279..78cb0cb3ceb 100644 --- a/litellm/types/services.py +++ b/litellm/types/services.py @@ -1,5 +1,5 @@ import enum -import uuid +from litellm._uuid import uuid from typing import List, Optional from pydantic import BaseModel, Field diff --git a/tests/enterprise/litellm_enterprise/enterprise_callbacks/test_prometheus_logging_callbacks.py b/tests/enterprise/litellm_enterprise/enterprise_callbacks/test_prometheus_logging_callbacks.py index a18af7a54d2..e2abf9c4c75 100644 --- a/tests/enterprise/litellm_enterprise/enterprise_callbacks/test_prometheus_logging_callbacks.py +++ b/tests/enterprise/litellm_enterprise/enterprise_callbacks/test_prometheus_logging_callbacks.py @@ -6,7 +6,7 @@ sys.path.insert(0, os.path.abspath("../..")) import asyncio import logging -import uuid +from litellm._uuid import uuid from datetime import datetime, timedelta, timezone from unittest.mock import MagicMock, call, patch diff --git a/tests/guardrails_tests/test_guardrails_config.py b/tests/guardrails_tests/test_guardrails_config.py index 110c0a30a9e..80b408a2dd1 100644 --- a/tests/guardrails_tests/test_guardrails_config.py +++ b/tests/guardrails_tests/test_guardrails_config.py @@ -6,7 +6,7 @@ import os import sys import time import traceback -import uuid +from litellm._uuid import uuid from datetime import datetime import pytest diff --git a/tests/litellm_utils_tests/test_aws_secret_manager.py b/tests/litellm_utils_tests/test_aws_secret_manager.py index e64c42b26c6..5aa608acb86 100644 --- a/tests/litellm_utils_tests/test_aws_secret_manager.py +++ b/tests/litellm_utils_tests/test_aws_secret_manager.py @@ -28,7 +28,7 @@ from typing import Optional from unittest.mock import MagicMock, patch import pytest -import uuid +from litellm._uuid import uuid import json from litellm.secret_managers.aws_secret_manager_v2 import AWSSecretsManagerV2 diff --git a/tests/litellm_utils_tests/test_hashicorp.py b/tests/litellm_utils_tests/test_hashicorp.py index c61168c99dd..055112d88c0 100644 --- a/tests/litellm_utils_tests/test_hashicorp.py +++ b/tests/litellm_utils_tests/test_hashicorp.py @@ -13,7 +13,7 @@ sys.path.insert( from unittest.mock import patch, MagicMock import logging from litellm._logging import verbose_logger -import uuid +from litellm._uuid import uuid verbose_logger.setLevel(logging.DEBUG) diff --git a/tests/litellm_utils_tests/test_secret_manager.py b/tests/litellm_utils_tests/test_secret_manager.py index cc0977cc519..8c604fd2e68 100644 --- a/tests/litellm_utils_tests/test_secret_manager.py +++ b/tests/litellm_utils_tests/test_secret_manager.py @@ -2,7 +2,7 @@ import os import sys import time import traceback -import uuid +from litellm._uuid import uuid from dotenv import load_dotenv import json diff --git a/tests/llm_responses_api_testing/base_responses_api.py b/tests/llm_responses_api_testing/base_responses_api.py index 8436f130e1a..855aeff246c 100644 --- a/tests/llm_responses_api_testing/base_responses_api.py +++ b/tests/llm_responses_api_testing/base_responses_api.py @@ -5,7 +5,7 @@ import sys from typing import Any, Dict, List from unittest.mock import MagicMock, Mock, patch import os -import uuid +from litellm._uuid import uuid import time import base64 diff --git a/tests/llm_translation/base_audio_transcription_unit_tests.py b/tests/llm_translation/base_audio_transcription_unit_tests.py index 958a02d9f46..4ee00fd4e9f 100644 --- a/tests/llm_translation/base_audio_transcription_unit_tests.py +++ b/tests/llm_translation/base_audio_transcription_unit_tests.py @@ -5,7 +5,7 @@ import sys from typing import Any, Dict, List from unittest.mock import MagicMock, Mock, patch import os -import uuid +from litellm._uuid import uuid sys.path.insert( 0, os.path.abspath("../..") diff --git a/tests/llm_translation/base_llm_unit_tests.py b/tests/llm_translation/base_llm_unit_tests.py index e7f60b3caae..1419edaea42 100644 --- a/tests/llm_translation/base_llm_unit_tests.py +++ b/tests/llm_translation/base_llm_unit_tests.py @@ -5,7 +5,7 @@ import sys from typing import Any, Dict, List from unittest.mock import MagicMock, Mock, patch import os -import uuid +from litellm._uuid import uuid import time import base64 import inspect diff --git a/tests/local_testing/cache_unit_tests.py b/tests/local_testing/cache_unit_tests.py index 8b82bbdfe66..d29eed33687 100644 --- a/tests/local_testing/cache_unit_tests.py +++ b/tests/local_testing/cache_unit_tests.py @@ -4,7 +4,7 @@ import os import sys import time import traceback -import uuid +from litellm._uuid import uuid from dotenv import load_dotenv diff --git a/tests/local_testing/test_add_update_models.py b/tests/local_testing/test_add_update_models.py index 163687c1c21..6d7ae597b54 100644 --- a/tests/local_testing/test_add_update_models.py +++ b/tests/local_testing/test_add_update_models.py @@ -1,7 +1,7 @@ import sys, os import traceback import json -import uuid +from litellm._uuid import uuid from dotenv import load_dotenv from fastapi import Request from datetime import datetime @@ -72,7 +72,7 @@ async def test_add_new_model(prisma_client): await litellm.proxy.proxy_server.prisma_client.connect() from litellm.proxy.proxy_server import user_api_key_cache - import uuid + from litellm._uuid import uuid _new_model_id = f"local-test-{uuid.uuid4().hex}" @@ -121,7 +121,7 @@ async def test_add_update_model(prisma_client): await litellm.proxy.proxy_server.prisma_client.connect() from litellm.proxy.proxy_server import user_api_key_cache - import uuid + from litellm._uuid import uuid _new_model_id = f"local-test-{uuid.uuid4().hex}" @@ -230,7 +230,7 @@ async def test_add_team_model_to_db(prisma_client): from litellm.proxy.management_endpoints.model_management_endpoints import ( _add_team_model_to_db, ) - import uuid + from litellm._uuid import uuid new_team = await _create_new_team(prisma_client) team_id = new_team.team_id diff --git a/tests/local_testing/test_alangfuse.py b/tests/local_testing/test_alangfuse.py index 2eea4264786..75dc07f9d5c 100644 --- a/tests/local_testing/test_alangfuse.py +++ b/tests/local_testing/test_alangfuse.py @@ -212,7 +212,7 @@ def create_async_task(**completion_kwargs): @pytest.mark.flaky(retries=12, delay=2) async def test_langfuse_logging_without_request_response(stream, langfuse_client): try: - import uuid + from litellm._uuid import uuid _unique_trace_name = f"litellm-test-{str(uuid.uuid4())}" litellm.set_verbose = True @@ -276,7 +276,7 @@ async def test_langfuse_logging_audio_transcriptions(langfuse_client): """ Test that creates a trace with masked input and output """ - import uuid + from litellm._uuid import uuid _unique_trace_name = f"litellm-test-{str(uuid.uuid4())}" litellm.set_verbose = True @@ -314,7 +314,7 @@ async def test_langfuse_masked_input_output(langfuse_client): """ Test that creates a trace with masked input and output """ - import uuid + from litellm._uuid import uuid for mask_value in [True, False]: _unique_trace_name = f"litellm-test-{str(uuid.uuid4())}" @@ -364,7 +364,7 @@ async def test_aaalangfuse_logging_metadata(langfuse_client): Release is just set for the trace Tags is just set for the trace """ - import uuid + from litellm._uuid import uuid litellm.set_verbose = True litellm.success_callback = ["langfuse"] @@ -939,7 +939,7 @@ def test_aaalangfuse_dynamic_logging(): Covers the team-logging scenario. """ - import uuid + from litellm._uuid import uuid import langfuse diff --git a/tests/local_testing/test_amazing_vertex_completion.py b/tests/local_testing/test_amazing_vertex_completion.py index 2aae51110d3..ebaafcf43ba 100644 --- a/tests/local_testing/test_amazing_vertex_completion.py +++ b/tests/local_testing/test_amazing_vertex_completion.py @@ -2489,7 +2489,7 @@ def mock_gemini_list_request(*args, **kwargs): return mock_response -import uuid +from litellm._uuid import uuid @pytest.mark.parametrize( diff --git a/tests/local_testing/test_audio_speech.py b/tests/local_testing/test_audio_speech.py index 9e8817d6e62..1d9247f9981 100644 --- a/tests/local_testing/test_audio_speech.py +++ b/tests/local_testing/test_audio_speech.py @@ -7,7 +7,7 @@ import random import sys import time import traceback -import uuid +from litellm._uuid import uuid from dotenv import load_dotenv diff --git a/tests/local_testing/test_caching.py b/tests/local_testing/test_caching.py index d1abec5ff70..39019ef35ac 100644 --- a/tests/local_testing/test_caching.py +++ b/tests/local_testing/test_caching.py @@ -2,7 +2,7 @@ import os import sys import time import traceback -import uuid +from litellm._uuid import uuid from dotenv import load_dotenv @@ -659,7 +659,7 @@ async def test_embedding_caching_base_64(): host=os.environ["REDIS_HOST"], port=os.environ["REDIS_PORT"], ) - import uuid + from litellm._uuid import uuid inputs = [ f"{uuid.uuid4()} hello this is ishaan", @@ -788,7 +788,7 @@ async def test_redis_batch_cache_write(): - read from client """ litellm.set_verbose = True - import uuid + from litellm._uuid import uuid messages = [ {"role": "user", "content": f"write a one sentence poem about: {uuid.uuid4()}"}, @@ -1479,7 +1479,7 @@ async def test_cache_control_overrides(): ) print("Testing cache override") litellm.set_verbose = True - import uuid + from litellm._uuid import uuid unique_num = str(uuid.uuid4()) @@ -1527,7 +1527,7 @@ def test_sync_cache_control_overrides(): ) print("Testing cache override") litellm.set_verbose = True - import uuid + from litellm._uuid import uuid unique_num = str(uuid.uuid4()) @@ -2050,7 +2050,7 @@ async def test_redis_proxy_batch_redis_get_cache(): user_api_key_cache = DualCache() - import uuid + from litellm._uuid import uuid batch_redis_get_obj.in_memory_cache = user_api_key_cache.in_memory_cache @@ -2493,7 +2493,7 @@ def test_redis_caching_multiple_namespaces(): The same request with different namespaces should not be cached under the same key """ - import uuid + from litellm._uuid import uuid from unittest.mock import patch, MagicMock import litellm from litellm.caching import Cache @@ -2639,7 +2639,7 @@ def test_caching_with_reasoning_content(): Test that reasoning content is cached """ - import uuid + from litellm._uuid import uuid messages = [{"role": "user", "content": f"what is litellm? {uuid.uuid4()}"}] litellm.cache = Cache() diff --git a/tests/local_testing/test_caching_handler.py b/tests/local_testing/test_caching_handler.py index 1006d23df32..70217257a75 100644 --- a/tests/local_testing/test_caching_handler.py +++ b/tests/local_testing/test_caching_handler.py @@ -2,7 +2,7 @@ import os import sys import time import traceback -import uuid +from litellm._uuid import uuid from dotenv import load_dotenv diff --git a/tests/local_testing/test_custom_callback_input.py b/tests/local_testing/test_custom_callback_input.py index 90d1d15c5a9..fb7ca510404 100644 --- a/tests/local_testing/test_custom_callback_input.py +++ b/tests/local_testing/test_custom_callback_input.py @@ -6,7 +6,7 @@ import os import sys import time import traceback -import uuid +from litellm._uuid import uuid from datetime import datetime import pytest diff --git a/tests/local_testing/test_dual_cache.py b/tests/local_testing/test_dual_cache.py index 657e00dae19..2da3df2783c 100644 --- a/tests/local_testing/test_dual_cache.py +++ b/tests/local_testing/test_dual_cache.py @@ -2,7 +2,7 @@ import os import sys import time import traceback -import uuid +from litellm._uuid import uuid from dotenv import load_dotenv diff --git a/tests/local_testing/test_dynamic_rate_limit_handler.py b/tests/local_testing/test_dynamic_rate_limit_handler.py index 3f52e25389a..ff540e22e7a 100644 --- a/tests/local_testing/test_dynamic_rate_limit_handler.py +++ b/tests/local_testing/test_dynamic_rate_limit_handler.py @@ -6,7 +6,7 @@ import random import sys import time import traceback -import uuid +from litellm._uuid import uuid from datetime import datetime from typing import Optional, Tuple diff --git a/tests/local_testing/test_gcs_bucket.py b/tests/local_testing/test_gcs_bucket.py index 0004fae7c48..5a9c99108c3 100644 --- a/tests/local_testing/test_gcs_bucket.py +++ b/tests/local_testing/test_gcs_bucket.py @@ -8,7 +8,7 @@ import asyncio import json import logging import tempfile -import uuid +from litellm._uuid import uuid from datetime import datetime import pytest diff --git a/tests/local_testing/test_helicone_integration.py b/tests/local_testing/test_helicone_integration.py index 3a6fa0309b1..4d77058598f 100644 --- a/tests/local_testing/test_helicone_integration.py +++ b/tests/local_testing/test_helicone_integration.py @@ -97,7 +97,7 @@ def create_async_task(**completion_kwargs): reason="Authentication missing for openai", ) async def test_helicone_logging_metadata(): - import uuid + from litellm._uuid import uuid litellm.success_callback = ["helicone"] diff --git a/tests/local_testing/test_langsmith.py b/tests/local_testing/test_langsmith.py index ff4c677d164..af7ac46a1cf 100644 --- a/tests/local_testing/test_langsmith.py +++ b/tests/local_testing/test_langsmith.py @@ -6,7 +6,7 @@ sys.path.insert(0, os.path.abspath("../..")) import asyncio import logging -import uuid +from litellm._uuid import uuid import pytest diff --git a/tests/local_testing/test_mem_usage.py b/tests/local_testing/test_mem_usage.py index 9f18fb1e2d6..f91852899ad 100644 --- a/tests/local_testing/test_mem_usage.py +++ b/tests/local_testing/test_mem_usage.py @@ -13,7 +13,7 @@ # from concurrent.futures import ThreadPoolExecutor # from collections import defaultdict # from dotenv import load_dotenv -# import uuid +# from litellm._uuid import uuid # import tracemalloc # import objgraph diff --git a/tests/local_testing/test_pass_through_endpoints.py b/tests/local_testing/test_pass_through_endpoints.py index 6cc6a66007f..9c5f5995218 100644 --- a/tests/local_testing/test_pass_through_endpoints.py +++ b/tests/local_testing/test_pass_through_endpoints.py @@ -1,6 +1,6 @@ import os import sys -import uuid +from litellm._uuid import uuid from functools import partial from typing import Optional diff --git a/tests/local_testing/test_router_timeout.py b/tests/local_testing/test_router_timeout.py index 8ca58f84455..882c1684356 100644 --- a/tests/local_testing/test_router_timeout.py +++ b/tests/local_testing/test_router_timeout.py @@ -96,7 +96,7 @@ def test_router_timeouts(): @pytest.mark.asyncio async def test_router_timeouts_bedrock(): - import uuid + from litellm._uuid import uuid import openai diff --git a/tests/local_testing/test_streaming.py b/tests/local_testing/test_streaming.py index 5ce437a49d4..630ae2b184f 100644 --- a/tests/local_testing/test_streaming.py +++ b/tests/local_testing/test_streaming.py @@ -7,7 +7,7 @@ import os import sys import time import traceback -import uuid +from litellm._uuid import uuid from typing import Tuple from unittest.mock import AsyncMock, MagicMock, patch @@ -2784,7 +2784,7 @@ def test_azure_streaming_and_function_calling(): @pytest.mark.asyncio async def test_azure_astreaming_and_function_calling(): - import uuid + from litellm._uuid import uuid tools = [ { diff --git a/tests/local_testing/test_timeout.py b/tests/local_testing/test_timeout.py index 2f8050affed..007c32e4476 100644 --- a/tests/local_testing/test_timeout.py +++ b/tests/local_testing/test_timeout.py @@ -9,7 +9,7 @@ sys.path.insert( 0, os.path.abspath("../..") ) # Adds the parent directory to the system path import time -import uuid +from litellm._uuid import uuid import httpx import openai diff --git a/tests/local_testing/test_unit_test_caching.py b/tests/local_testing/test_unit_test_caching.py index 033fb774f0c..fa5cf802546 100644 --- a/tests/local_testing/test_unit_test_caching.py +++ b/tests/local_testing/test_unit_test_caching.py @@ -2,7 +2,7 @@ import os import sys import time import traceback -import uuid +from litellm._uuid import uuid from dotenv import load_dotenv diff --git a/tests/logging_callback_tests/test_alerting.py b/tests/logging_callback_tests/test_alerting.py index e431f99129f..b9ecfaeb3f0 100644 --- a/tests/logging_callback_tests/test_alerting.py +++ b/tests/logging_callback_tests/test_alerting.py @@ -8,7 +8,7 @@ import os import random import sys import time -import uuid +from litellm._uuid import uuid from datetime import datetime, timedelta from typing import Optional diff --git a/tests/logging_callback_tests/test_built_in_tools_cost_tracking.py b/tests/logging_callback_tests/test_built_in_tools_cost_tracking.py index 4d0f1accaba..bd1465f1564 100644 --- a/tests/logging_callback_tests/test_built_in_tools_cost_tracking.py +++ b/tests/logging_callback_tests/test_built_in_tools_cost_tracking.py @@ -1,7 +1,7 @@ import os import sys import traceback -import uuid +from litellm._uuid import uuid import pytest from dotenv import load_dotenv from fastapi import Request @@ -98,7 +98,7 @@ async def test_openai_web_search_logging_cost_tracking( ): """Test web search cost tracking with different search context sizes""" test_custom_logger = await _setup_web_search_test() - import uuid + from litellm._uuid import uuid diff --git a/tests/logging_callback_tests/test_langfuse_e2e_test.py b/tests/logging_callback_tests/test_langfuse_e2e_test.py index 8d5e63fcdc0..3e76a685630 100644 --- a/tests/logging_callback_tests/test_langfuse_e2e_test.py +++ b/tests/logging_callback_tests/test_langfuse_e2e_test.py @@ -119,7 +119,7 @@ class TestLangfuseLogging: @pytest.fixture async def mock_setup(self): """Common setup for Langfuse logging tests""" - import uuid + from litellm._uuid import uuid from unittest.mock import AsyncMock, patch import httpx diff --git a/tests/logging_callback_tests/test_moderations_api_logging.py b/tests/logging_callback_tests/test_moderations_api_logging.py index 1d3b5356b0f..5e2011afbba 100644 --- a/tests/logging_callback_tests/test_moderations_api_logging.py +++ b/tests/logging_callback_tests/test_moderations_api_logging.py @@ -1,7 +1,7 @@ import os import sys import traceback -import uuid +from litellm._uuid import uuid import pytest from dotenv import load_dotenv from fastapi import Request diff --git a/tests/logging_callback_tests/test_spend_logs.py b/tests/logging_callback_tests/test_spend_logs.py index 881e170f8aa..500419eb04d 100644 --- a/tests/logging_callback_tests/test_spend_logs.py +++ b/tests/logging_callback_tests/test_spend_logs.py @@ -1,7 +1,7 @@ import os import sys import traceback -import uuid +from litellm._uuid import uuid from dotenv import load_dotenv from fastapi import Request diff --git a/tests/logging_callback_tests/test_token_counting.py b/tests/logging_callback_tests/test_token_counting.py index 1a44229140a..6f84b7c18fe 100644 --- a/tests/logging_callback_tests/test_token_counting.py +++ b/tests/logging_callback_tests/test_token_counting.py @@ -1,7 +1,7 @@ import os import sys import traceback -import uuid +from litellm._uuid import uuid import pytest from dotenv import load_dotenv from fastapi import Request diff --git a/tests/logging_callback_tests/test_view_request_resp_logs.py b/tests/logging_callback_tests/test_view_request_resp_logs.py index a8018b62ee2..34e8d01303a 100644 --- a/tests/logging_callback_tests/test_view_request_resp_logs.py +++ b/tests/logging_callback_tests/test_view_request_resp_logs.py @@ -8,7 +8,7 @@ import asyncio import json import logging import tempfile -import uuid +from litellm._uuid import uuid import json from datetime import datetime, timedelta, timezone diff --git a/tests/multi_instance_e2e_tests/test_update_team_e2e.py b/tests/multi_instance_e2e_tests/test_update_team_e2e.py index c4769aa6a57..dfbfbd310ee 100644 --- a/tests/multi_instance_e2e_tests/test_update_team_e2e.py +++ b/tests/multi_instance_e2e_tests/test_update_team_e2e.py @@ -52,7 +52,7 @@ async def chat_completion_on_port( Accepts an optional prompt string. """ from openai import AsyncOpenAI - import uuid + from litellm._uuid import uuid if prompt is None: prompt = f"Say hello! {uuid.uuid4()}" * 100 diff --git a/tests/old_proxy_tests/tests/bursty_load_test_completion.py b/tests/old_proxy_tests/tests/bursty_load_test_completion.py index 529f2ce9f69..642bd9f14d4 100644 --- a/tests/old_proxy_tests/tests/bursty_load_test_completion.py +++ b/tests/old_proxy_tests/tests/bursty_load_test_completion.py @@ -1,6 +1,6 @@ import time, asyncio from openai import AsyncOpenAI -import uuid +from litellm._uuid import uuid import traceback diff --git a/tests/old_proxy_tests/tests/load_test_completion.py b/tests/old_proxy_tests/tests/load_test_completion.py index 29d8924ab5f..afbd74a7900 100644 --- a/tests/old_proxy_tests/tests/load_test_completion.py +++ b/tests/old_proxy_tests/tests/load_test_completion.py @@ -2,7 +2,7 @@ import time import asyncio import os from openai import AsyncOpenAI, AsyncAzureOpenAI -import uuid +from litellm._uuid import uuid import traceback from large_text import text from dotenv import load_dotenv diff --git a/tests/old_proxy_tests/tests/load_test_embedding_100.py b/tests/old_proxy_tests/tests/load_test_embedding_100.py index 38ae8990aaa..8cd4d250249 100644 --- a/tests/old_proxy_tests/tests/load_test_embedding_100.py +++ b/tests/old_proxy_tests/tests/load_test_embedding_100.py @@ -1,6 +1,6 @@ import time, asyncio from openai import AsyncOpenAI -import uuid +from litellm._uuid import uuid import traceback diff --git a/tests/old_proxy_tests/tests/test_simple_traceparent_openai.py b/tests/old_proxy_tests/tests/test_simple_traceparent_openai.py index caa10051abf..d4c36029948 100644 --- a/tests/old_proxy_tests/tests/test_simple_traceparent_openai.py +++ b/tests/old_proxy_tests/tests/test_simple_traceparent_openai.py @@ -1,5 +1,5 @@ # mypy: ignore-errors -import uuid +from litellm._uuid import uuid import openai diff --git a/tests/otel_tests/test_e2e_budgeting.py b/tests/otel_tests/test_e2e_budgeting.py index 8af9fa9cbd4..e3b4c8b2b55 100644 --- a/tests/otel_tests/test_e2e_budgeting.py +++ b/tests/otel_tests/test_e2e_budgeting.py @@ -61,7 +61,7 @@ async def generate_key( async def chat_completion(session, key: str, model: str): """Make a chat completion request using OpenAI SDK""" from openai import AsyncOpenAI - import uuid + from litellm._uuid import uuid client = AsyncOpenAI( api_key=key, base_url="http://0.0.0.0:4000/v1" # Point to our local proxy diff --git a/tests/otel_tests/test_e2e_model_access.py b/tests/otel_tests/test_e2e_model_access.py index 4628dc7e9c8..ade319c2d43 100644 --- a/tests/otel_tests/test_e2e_model_access.py +++ b/tests/otel_tests/test_e2e_model_access.py @@ -35,7 +35,7 @@ async def generate_team(session, models: Optional[List[str]] = None): async def mock_chat_completion(session, key: str, model: str): """Make a chat completion request using OpenAI SDK""" from openai import AsyncOpenAI - import uuid + from litellm._uuid import uuid client = AsyncOpenAI(api_key=key, base_url="http://0.0.0.0:4000/v1") diff --git a/tests/otel_tests/test_guardrails.py b/tests/otel_tests/test_guardrails.py index cfad6985a0f..5345944bcb1 100644 --- a/tests/otel_tests/test_guardrails.py +++ b/tests/otel_tests/test_guardrails.py @@ -4,7 +4,7 @@ import aiohttp, openai from openai import OpenAI, AsyncOpenAI from typing import Optional, List, Union import json -import uuid +from litellm._uuid import uuid async def chat_completion( diff --git a/tests/otel_tests/test_moderations.py b/tests/otel_tests/test_moderations.py index 21abf748922..a0218b039fa 100644 --- a/tests/otel_tests/test_moderations.py +++ b/tests/otel_tests/test_moderations.py @@ -3,7 +3,7 @@ import asyncio import aiohttp, openai from openai import OpenAI, AsyncOpenAI from typing import Optional, List, Union -import uuid +from litellm._uuid import uuid async def make_moderations_curl_request( diff --git a/tests/otel_tests/test_otel.py b/tests/otel_tests/test_otel.py index b252fcb5b57..e546b7d4a9d 100644 --- a/tests/otel_tests/test_otel.py +++ b/tests/otel_tests/test_otel.py @@ -5,7 +5,7 @@ import asyncio import aiohttp, openai from openai import OpenAI, AsyncOpenAI from typing import Optional, List, Union -import uuid +from litellm._uuid import uuid async def generate_key( diff --git a/tests/otel_tests/test_prometheus.py b/tests/otel_tests/test_prometheus.py index 4e459508cc8..69c1f9413e3 100644 --- a/tests/otel_tests/test_prometheus.py +++ b/tests/otel_tests/test_prometheus.py @@ -5,7 +5,7 @@ Unit tests for prometheus metrics import pytest import aiohttp import asyncio -import uuid +from litellm._uuid import uuid import os import sys from openai import AsyncOpenAI diff --git a/tests/otel_tests/test_rerank.py b/tests/otel_tests/test_rerank.py index 47fe109e7f6..f04d82442a3 100644 --- a/tests/otel_tests/test_rerank.py +++ b/tests/otel_tests/test_rerank.py @@ -3,7 +3,7 @@ import asyncio import aiohttp, openai from openai import OpenAI, AsyncOpenAI from typing import Optional, List, Union -import uuid +from litellm._uuid import uuid async def make_rerank_curl_request( diff --git a/tests/otel_tests/test_team_member_permissions.py b/tests/otel_tests/test_team_member_permissions.py index 3b266a9e68d..d8187e2bc15 100644 --- a/tests/otel_tests/test_team_member_permissions.py +++ b/tests/otel_tests/test_team_member_permissions.py @@ -45,7 +45,7 @@ import pytest import asyncio import aiohttp, openai -import uuid +from litellm._uuid import uuid import json from litellm.proxy._types import ProxyErrorTypes from typing import Optional diff --git a/tests/otel_tests/test_team_tag_routing.py b/tests/otel_tests/test_team_tag_routing.py index 842b76d946e..17570e7363c 100644 --- a/tests/otel_tests/test_team_tag_routing.py +++ b/tests/otel_tests/test_team_tag_routing.py @@ -5,7 +5,7 @@ import asyncio import aiohttp, openai from openai import OpenAI, AsyncOpenAI from typing import Optional, List, Union -import uuid +from litellm._uuid import uuid LITELLM_MASTER_KEY = "sk-1234" diff --git a/tests/proxy_admin_ui_tests/test_key_management.py b/tests/proxy_admin_ui_tests/test_key_management.py index b3522c88164..589a394cbbc 100644 --- a/tests/proxy_admin_ui_tests/test_key_management.py +++ b/tests/proxy_admin_ui_tests/test_key_management.py @@ -1,7 +1,7 @@ import os import sys import traceback -import uuid +from litellm._uuid import uuid import datetime as dt from datetime import datetime @@ -228,7 +228,7 @@ async def test_regenerate_api_key_with_new_alias_and_expiration(prisma_client): setattr(litellm.proxy.proxy_server, "prisma_client", prisma_client) setattr(litellm.proxy.proxy_server, "master_key", "sk-1234") await litellm.proxy.proxy_server.prisma_client.connect() - import uuid + from litellm._uuid import uuid # generate new key key_alias = f"test_alias_regenerate_key-{uuid.uuid4()}" @@ -279,7 +279,7 @@ async def test_regenerate_key_ui(prisma_client): setattr(litellm.proxy.proxy_server, "prisma_client", prisma_client) setattr(litellm.proxy.proxy_server, "master_key", "sk-1234") await litellm.proxy.proxy_server.prisma_client.connect() - import uuid + from litellm._uuid import uuid # generate new key key_alias = f"test_alias_regenerate_key-{uuid.uuid4()}" @@ -1076,7 +1076,7 @@ async def test_list_key_helper_team_filtering(prisma_client): from litellm.proxy.management_endpoints.key_management_endpoints import ( _list_key_helper, ) - import uuid + from litellm._uuid import uuid # Setup setattr(litellm.proxy.proxy_server, "prisma_client", prisma_client) diff --git a/tests/proxy_admin_ui_tests/test_role_based_access.py b/tests/proxy_admin_ui_tests/test_role_based_access.py index a1c4e9a399a..6d3fb357499 100644 --- a/tests/proxy_admin_ui_tests/test_role_based_access.py +++ b/tests/proxy_admin_ui_tests/test_role_based_access.py @@ -5,7 +5,7 @@ RBAC tests import os import sys import traceback -import uuid +from litellm._uuid import uuid from datetime import datetime from dotenv import load_dotenv diff --git a/tests/proxy_admin_ui_tests/test_route_check_unit_tests.py b/tests/proxy_admin_ui_tests/test_route_check_unit_tests.py index 8f758884683..dacda602c01 100644 --- a/tests/proxy_admin_ui_tests/test_route_check_unit_tests.py +++ b/tests/proxy_admin_ui_tests/test_route_check_unit_tests.py @@ -1,7 +1,7 @@ import os import sys import traceback -import uuid +from litellm._uuid import uuid import datetime as dt from datetime import datetime diff --git a/tests/proxy_admin_ui_tests/test_sso_sign_in.py b/tests/proxy_admin_ui_tests/test_sso_sign_in.py index 28e0cbf8cc5..83bddf5b8b2 100644 --- a/tests/proxy_admin_ui_tests/test_sso_sign_in.py +++ b/tests/proxy_admin_ui_tests/test_sso_sign_in.py @@ -60,7 +60,7 @@ async def test_auth_callback_new_user(mock_google_sso, mock_env_vars, prisma_cli """ Tests that a new SSO Sign In user is by default given an 'INTERNAL_USER_VIEW_ONLY' role """ - import uuid + from litellm._uuid import uuid import litellm litellm._turn_on_debug() @@ -133,7 +133,7 @@ async def test_auth_callback_new_user_with_sso_default( Tests that a new SSO Sign In user is by default given an 'INTERNAL_USER' role """ - import uuid + from litellm._uuid import uuid # Generate a unique user ID unique_user_id = str(uuid.uuid4()) diff --git a/tests/proxy_admin_ui_tests/test_usage_endpoints.py b/tests/proxy_admin_ui_tests/test_usage_endpoints.py index 61d4e944936..3d3360b8ff6 100644 --- a/tests/proxy_admin_ui_tests/test_usage_endpoints.py +++ b/tests/proxy_admin_ui_tests/test_usage_endpoints.py @@ -16,7 +16,7 @@ For all tests - test the following: import os import sys import traceback -import uuid +from litellm._uuid import uuid from datetime import datetime from dotenv import load_dotenv diff --git a/tests/proxy_unit_tests/test_audit_logs_proxy.py b/tests/proxy_unit_tests/test_audit_logs_proxy.py index a4506950dd1..ec7d42805d2 100644 --- a/tests/proxy_unit_tests/test_audit_logs_proxy.py +++ b/tests/proxy_unit_tests/test_audit_logs_proxy.py @@ -1,7 +1,7 @@ import os import sys import traceback -import uuid +from litellm._uuid import uuid from datetime import datetime from dotenv import load_dotenv @@ -24,7 +24,7 @@ import logging load_dotenv() import pytest -import uuid +from litellm._uuid import uuid import litellm from litellm._logging import verbose_proxy_logger diff --git a/tests/proxy_unit_tests/test_custom_callback_input.py b/tests/proxy_unit_tests/test_custom_callback_input.py index d98c7619e32..71a7e94b180 100644 --- a/tests/proxy_unit_tests/test_custom_callback_input.py +++ b/tests/proxy_unit_tests/test_custom_callback_input.py @@ -6,7 +6,7 @@ import os import sys import time import traceback -import uuid +from litellm._uuid import uuid from datetime import datetime import pytest diff --git a/tests/proxy_unit_tests/test_e2e_pod_lock_manager.py b/tests/proxy_unit_tests/test_e2e_pod_lock_manager.py index 5f4a16f4a0e..e52e8816a30 100644 --- a/tests/proxy_unit_tests/test_e2e_pod_lock_manager.py +++ b/tests/proxy_unit_tests/test_e2e_pod_lock_manager.py @@ -1,7 +1,7 @@ import os import sys import traceback -import uuid +from litellm._uuid import uuid from typing import List from datetime import datetime, timezone, timedelta @@ -379,7 +379,7 @@ async def test_e2e_size_of_redis_buffer(): from litellm.proxy.db.db_spend_update_writer import DBSpendUpdateWriter from litellm.proxy.db.db_transaction_queue.base_update_queue import BaseUpdateQueue from litellm.caching import RedisCache - import uuid + from litellm._uuid import uuid redis_cache = RedisCache(host=os.getenv("REDIS_HOST"), port=os.getenv("REDIS_PORT"), password=os.getenv("REDIS_PASSWORD")) diff --git a/tests/proxy_unit_tests/test_jwt.py b/tests/proxy_unit_tests/test_jwt.py index 57514817e88..2e54ade688f 100644 --- a/tests/proxy_unit_tests/test_jwt.py +++ b/tests/proxy_unit_tests/test_jwt.py @@ -7,7 +7,7 @@ import random import sys import time import traceback -import uuid +from litellm._uuid import uuid from dotenv import load_dotenv @@ -240,7 +240,7 @@ def prisma_client(): @pytest.fixture def team_token_tuple(): import json - import uuid + from litellm._uuid import uuid import jwt from cryptography.hazmat.backends import default_backend @@ -305,7 +305,7 @@ def team_token_tuple(): @pytest.mark.asyncio async def test_team_token_output(prisma_client, audience, monkeypatch): import json - import uuid + from litellm._uuid import uuid import jwt from cryptography.hazmat.backends import default_backend @@ -478,7 +478,7 @@ async def test_team_token_output(prisma_client, audience, monkeypatch): async def aaaatest_user_token_output( prisma_client, audience, team_id_set, default_team_id, user_id_upsert, monkeypatch ): - import uuid + from litellm._uuid import uuid args = locals() print(f"received args - {args}") @@ -491,7 +491,7 @@ async def aaaatest_user_token_output( - retry -> it should pass now """ import json - import uuid + from litellm._uuid import uuid import jwt from cryptography.hazmat.backends import default_backend @@ -726,7 +726,7 @@ async def test_allowed_routes_admin( - check if admin passes user_api_key_auth for them """ import json - import uuid + from litellm._uuid import uuid import jwt from cryptography.hazmat.backends import default_backend diff --git a/tests/proxy_unit_tests/test_key_generate_prisma.py b/tests/proxy_unit_tests/test_key_generate_prisma.py index be29ce1e343..6eb4efa7d63 100644 --- a/tests/proxy_unit_tests/test_key_generate_prisma.py +++ b/tests/proxy_unit_tests/test_key_generate_prisma.py @@ -22,7 +22,7 @@ import os import sys import traceback -import uuid +from litellm._uuid import uuid from datetime import datetime, timezone from dotenv import load_dotenv @@ -1236,7 +1236,7 @@ def test_generate_and_update_key(prisma_client): # 11. Generate a Key, cal key/info, call key/update, call key/info # Check if data gets updated # Check if untouched data does not get updated - import uuid + from litellm._uuid import uuid print("prisma client=", prisma_client) @@ -1554,7 +1554,7 @@ def test_call_with_key_over_budget(prisma_client): litellm.cache = Cache() import time - import uuid + from litellm._uuid import uuid request_id = f"chatcmpl-e41836bb-bb8b-4df2-8e70-8f3e160155ac{uuid.uuid4()}" @@ -1676,7 +1676,7 @@ def test_call_with_key_over_budget_no_cache(prisma_client): litellm.cache = Cache() import time - import uuid + from litellm._uuid import uuid request_id = f"chatcmpl-e41836bb-bb8b-4df2-8e70-8f3e160155ac{uuid.uuid4()}" @@ -1900,7 +1900,7 @@ async def test_call_with_key_never_over_budget(prisma_client): # update spend using track_cost callback, make 2nd request, it should fail import time - import uuid + from litellm._uuid import uuid from litellm import Choices, Message, ModelResponse, Usage from litellm.proxy.proxy_server import _ProxyDBLogger @@ -1991,7 +1991,7 @@ async def test_call_with_key_over_budget_stream(prisma_client): # update spend using track_cost callback, make 2nd request, it should fail import time - import uuid + from litellm._uuid import uuid from litellm import Choices, Message, ModelResponse, Usage from litellm.proxy.proxy_server import _ProxyDBLogger @@ -2440,7 +2440,7 @@ async def test_key_with_no_permissions(prisma_client): async def track_cost_callback_helper_fn(generated_key: str, user_id: str): - import uuid + from litellm._uuid import uuid from litellm import Choices, Message, ModelResponse, Usage from litellm.proxy.proxy_server import _ProxyDBLogger @@ -2549,7 +2549,7 @@ async def test_proxy_load_test_db(prisma_client): @pytest.mark.asyncio() async def test_master_key_hashing(prisma_client): try: - import uuid + from litellm._uuid import uuid print("prisma client=", prisma_client) diff --git a/tests/proxy_unit_tests/test_proxy_server.py b/tests/proxy_unit_tests/test_proxy_server.py index 139c90fd3cb..4e72580b8be 100644 --- a/tests/proxy_unit_tests/test_proxy_server.py +++ b/tests/proxy_unit_tests/test_proxy_server.py @@ -928,7 +928,7 @@ async def test_get_team_redis(client_no_auth): import random -import uuid +from litellm._uuid import uuid from unittest.mock import AsyncMock, MagicMock, PropertyMock, patch from litellm.proxy._types import ( diff --git a/tests/spend_tracking_tests/test_spend_accuracy_tests.py b/tests/spend_tracking_tests/test_spend_accuracy_tests.py index 93228c2d06a..821709250da 100644 --- a/tests/spend_tracking_tests/test_spend_accuracy_tests.py +++ b/tests/spend_tracking_tests/test_spend_accuracy_tests.py @@ -4,7 +4,7 @@ import aiohttp import json from httpx import AsyncClient from typing import Any, Optional -import uuid +from litellm._uuid import uuid """ Tests to run @@ -89,7 +89,7 @@ async def generate_key(session, user_id: str, team_id: str): async def chat_completion(session, key: str): """Make a chat completion request""" from openai import AsyncOpenAI - import uuid + from litellm._uuid import uuid client = AsyncOpenAI(api_key=key, base_url="http://0.0.0.0:4000/v1") diff --git a/tests/store_model_in_db_tests/test_mcp_servers.py b/tests/store_model_in_db_tests/test_mcp_servers.py index 7a524c16d22..a396c37832a 100644 --- a/tests/store_model_in_db_tests/test_mcp_servers.py +++ b/tests/store_model_in_db_tests/test_mcp_servers.py @@ -1,7 +1,7 @@ from datetime import datetime from typing import List, Optional import pytest -import uuid +from litellm._uuid import uuid import os import asyncio from unittest import mock diff --git a/tests/store_model_in_db_tests/test_team_models.py b/tests/store_model_in_db_tests/test_team_models.py index 0faa01c8ee7..83822433a63 100644 --- a/tests/store_model_in_db_tests/test_team_models.py +++ b/tests/store_model_in_db_tests/test_team_models.py @@ -3,9 +3,9 @@ import asyncio import aiohttp import json from openai import AsyncOpenAI -import uuid +from litellm._uuid import uuid from httpx import AsyncClient -import uuid +from litellm._uuid import uuid import os TEST_MASTER_KEY = "sk-1234" diff --git a/tests/test_budget_management.py b/tests/test_budget_management.py index 8175a9b1df4..5863cea9d3b 100644 --- a/tests/test_budget_management.py +++ b/tests/test_budget_management.py @@ -1,6 +1,6 @@ # What is this? ## Unit tests for the /budget/* endpoints -import uuid +from litellm._uuid import uuid from datetime import datetime, timedelta import aiohttp diff --git a/tests/test_callbacks_on_proxy.py b/tests/test_callbacks_on_proxy.py index 42665c35bce..831ca449f83 100644 --- a/tests/test_callbacks_on_proxy.py +++ b/tests/test_callbacks_on_proxy.py @@ -108,7 +108,7 @@ async def test_check_num_callbacks(): -> sleep for 30 seconds -> check current callbacks """ - import uuid + from litellm._uuid import uuid async with aiohttp.ClientSession() as session: await asyncio.sleep(30) @@ -158,7 +158,7 @@ async def test_check_num_callbacks_on_lowest_latency(): -> check current callbacks -> update back to original routing-strategy """ - import uuid + from litellm._uuid import uuid async with aiohttp.ClientSession() as session: await asyncio.sleep(30) diff --git a/tests/test_config.py b/tests/test_config.py index 76316368f6c..8ec65341963 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -83,7 +83,7 @@ async def test_team_logging(): await config_update(session) # 2. Call /chat/completions with a specific trace id - import uuid + from litellm._uuid import uuid _trace_id = f"trace-{uuid.uuid4()}" _request_metadata = { diff --git a/tests/test_end_users.py b/tests/test_end_users.py index 0ce1694147b..c175bb371e2 100644 --- a/tests/test_end_users.py +++ b/tests/test_end_users.py @@ -4,7 +4,7 @@ import pytest import asyncio import aiohttp import time -import uuid +from litellm._uuid import uuid from openai import AsyncOpenAI from typing import Optional diff --git a/tests/test_litellm/integrations/test_deepeval.py b/tests/test_litellm/integrations/test_deepeval.py index 26e79585c91..6dd3aef0a03 100644 --- a/tests/test_litellm/integrations/test_deepeval.py +++ b/tests/test_litellm/integrations/test_deepeval.py @@ -1,6 +1,6 @@ import os import unittest -import uuid +from litellm._uuid import uuid from datetime import datetime, timezone from unittest.mock import MagicMock, patch diff --git a/tests/test_litellm/llms/bedrock/invoke_agent/test_bedrock_agent_transformation.py b/tests/test_litellm/llms/bedrock/invoke_agent/test_bedrock_agent_transformation.py index ef2bb3757fc..ffd3526698a 100644 --- a/tests/test_litellm/llms/bedrock/invoke_agent/test_bedrock_agent_transformation.py +++ b/tests/test_litellm/llms/bedrock/invoke_agent/test_bedrock_agent_transformation.py @@ -2,7 +2,7 @@ import base64 import json import os import sys -import uuid +from litellm._uuid import uuid from unittest.mock import MagicMock, Mock, patch import pytest diff --git a/tests/test_litellm/llms/ollama/test_ollama_completion_transformation.py b/tests/test_litellm/llms/ollama/test_ollama_completion_transformation.py index 452f5a94024..39386fe4fd2 100644 --- a/tests/test_litellm/llms/ollama/test_ollama_completion_transformation.py +++ b/tests/test_litellm/llms/ollama/test_ollama_completion_transformation.py @@ -1,7 +1,7 @@ import json import os import sys -import uuid +from litellm._uuid import uuid from unittest.mock import MagicMock, patch import pytest diff --git a/tests/test_litellm/proxy/management_endpoints/scim/test_scim_transformations.py b/tests/test_litellm/proxy/management_endpoints/scim/test_scim_transformations.py index be24444afaf..bacdfb225fb 100644 --- a/tests/test_litellm/proxy/management_endpoints/scim/test_scim_transformations.py +++ b/tests/test_litellm/proxy/management_endpoints/scim/test_scim_transformations.py @@ -2,7 +2,7 @@ import asyncio import json import os import sys -import uuid +from litellm._uuid import uuid from typing import Optional, cast from unittest.mock import AsyncMock, MagicMock, patch diff --git a/tests/test_litellm/proxy/management_endpoints/test_mcp_management_endpoints.py b/tests/test_litellm/proxy/management_endpoints/test_mcp_management_endpoints.py index f1594c36e7e..215e0d050a3 100644 --- a/tests/test_litellm/proxy/management_endpoints/test_mcp_management_endpoints.py +++ b/tests/test_litellm/proxy/management_endpoints/test_mcp_management_endpoints.py @@ -1,7 +1,7 @@ import json import os import sys -import uuid +from litellm._uuid import uuid from datetime import datetime from typing import List from unittest.mock import AsyncMock, MagicMock, patch diff --git a/tests/test_litellm/proxy/management_endpoints/test_model_management_endpoints.py b/tests/test_litellm/proxy/management_endpoints/test_model_management_endpoints.py index bd37e9cbe41..2f736e4e1d1 100644 --- a/tests/test_litellm/proxy/management_endpoints/test_model_management_endpoints.py +++ b/tests/test_litellm/proxy/management_endpoints/test_model_management_endpoints.py @@ -1,7 +1,7 @@ import json import os import sys -import uuid +from litellm._uuid import uuid from typing import Dict, Optional from unittest.mock import AsyncMock, MagicMock, patch diff --git a/tests/test_litellm/proxy/management_endpoints/test_organization_endpoints.py b/tests/test_litellm/proxy/management_endpoints/test_organization_endpoints.py index c05e528846d..981c26d6db9 100644 --- a/tests/test_litellm/proxy/management_endpoints/test_organization_endpoints.py +++ b/tests/test_litellm/proxy/management_endpoints/test_organization_endpoints.py @@ -2,7 +2,7 @@ import asyncio import json import os import sys -import uuid +from litellm._uuid import uuid from typing import Optional, cast from unittest.mock import AsyncMock, MagicMock, patch diff --git a/tests/test_litellm/proxy/management_endpoints/test_team_endpoints.py b/tests/test_litellm/proxy/management_endpoints/test_team_endpoints.py index 84454160572..c895e016e3e 100644 --- a/tests/test_litellm/proxy/management_endpoints/test_team_endpoints.py +++ b/tests/test_litellm/proxy/management_endpoints/test_team_endpoints.py @@ -2,7 +2,7 @@ import asyncio import json import os import sys -import uuid +from litellm._uuid import uuid from typing import Optional, cast from unittest.mock import AsyncMock, MagicMock, patch diff --git a/tests/test_litellm/proxy/management_endpoints/test_ui_sso.py b/tests/test_litellm/proxy/management_endpoints/test_ui_sso.py index 639b4756533..86339c0d227 100644 --- a/tests/test_litellm/proxy/management_endpoints/test_ui_sso.py +++ b/tests/test_litellm/proxy/management_endpoints/test_ui_sso.py @@ -2,7 +2,7 @@ import asyncio import json import os import sys -import uuid +from litellm._uuid import uuid from typing import Optional, cast from unittest.mock import AsyncMock, MagicMock, patch diff --git a/tests/test_litellm/proxy/management_helpers/test_management_helpers_utils.py b/tests/test_litellm/proxy/management_helpers/test_management_helpers_utils.py index 14306197603..5f46d469ddb 100644 --- a/tests/test_litellm/proxy/management_helpers/test_management_helpers_utils.py +++ b/tests/test_litellm/proxy/management_helpers/test_management_helpers_utils.py @@ -1,7 +1,7 @@ import json import os import sys -import uuid +from litellm._uuid import uuid from unittest.mock import AsyncMock, MagicMock import pytest diff --git a/tests/test_litellm/proxy/test_common_request_processing.py b/tests/test_litellm/proxy/test_common_request_processing.py index 6a8d1ae1339..ef1aec2e764 100644 --- a/tests/test_litellm/proxy/test_common_request_processing.py +++ b/tests/test_litellm/proxy/test_common_request_processing.py @@ -1,5 +1,5 @@ import copy -import uuid +from litellm._uuid import uuid from unittest.mock import AsyncMock, MagicMock import pytest diff --git a/tests/test_models.py b/tests/test_models.py index 5a61d2eafd8..a0fa0525273 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -280,7 +280,7 @@ async def test_add_and_delete_models(): - Delete model - Call model -> expect to fail """ - import uuid + from litellm._uuid import uuid async with aiohttp.ClientSession() as session: key_gen = await generate_key(session=session) @@ -404,7 +404,7 @@ async def test_add_model_run_health(): Call /health -> Ensure the health check for the endpoint is working as expected """ - import uuid + from litellm._uuid import uuid async with aiohttp.ClientSession() as session: key_gen = await generate_key(session=session) @@ -528,7 +528,7 @@ async def test_team_model_e2e(): """ from tests.test_users import new_user from tests.test_team import new_team - import uuid + from litellm._uuid import uuid async with aiohttp.ClientSession() as session: # Creat a user diff --git a/tests/test_openai_endpoints.py b/tests/test_openai_endpoints.py index c90fd91a5d7..fd997ebb71c 100644 --- a/tests/test_openai_endpoints.py +++ b/tests/test_openai_endpoints.py @@ -5,7 +5,7 @@ import asyncio import aiohttp, openai from openai import OpenAI, AsyncOpenAI, AzureOpenAI, AsyncAzureOpenAI from typing import Optional, List, Union -import uuid +from litellm._uuid import uuid LITELLM_MASTER_KEY = "sk-1234" diff --git a/tests/test_team_logging.py b/tests/test_team_logging.py index 516b6fa13b4..913b1e19496 100644 --- a/tests/test_team_logging.py +++ b/tests/test_team_logging.py @@ -78,7 +78,7 @@ async def test_aaateam_logging(): session, models=["fake-openai-endpoint"], team_id="team-1" ) # team-1 logs to project 1 - import uuid + from litellm._uuid import uuid _trace_id = f"trace-{uuid.uuid4()}" _request_metadata = { @@ -144,7 +144,7 @@ async def test_team_2logging(): session, models=["fake-openai-endpoint"], team_id="team-2" ) # team-1 logs to project 1 - import uuid + from litellm._uuid import uuid _trace_id = f"trace-{uuid.uuid4()}" _request_metadata = { diff --git a/tests/test_team_members.py b/tests/test_team_members.py index d8981f84a62..a3d64eae803 100644 --- a/tests/test_team_members.py +++ b/tests/test_team_members.py @@ -3,7 +3,7 @@ import requests import time from typing import Dict, List import logging -import uuid +from litellm._uuid import uuid # Configure logging logging.basicConfig(level=logging.INFO) diff --git a/tests/test_users.py b/tests/test_users.py index 08cbf9f1035..9b8eca789b2 100644 --- a/tests/test_users.py +++ b/tests/test_users.py @@ -318,7 +318,7 @@ async def test_user_model_access(): import json -import uuid +from litellm._uuid import uuid import pytest import aiohttp from typing import Dict, Tuple diff --git a/tests/vector_store_tests/base_vector_store_test.py b/tests/vector_store_tests/base_vector_store_test.py index d3b58d8338d..414b5ee3334 100644 --- a/tests/vector_store_tests/base_vector_store_test.py +++ b/tests/vector_store_tests/base_vector_store_test.py @@ -5,7 +5,7 @@ import sys from typing import Any, Dict, List from unittest.mock import MagicMock, Mock, patch import os -import uuid +from litellm._uuid import uuid import time import base64 From 2dd38420a73d3458fa0df3baf9815090bf97d8ab Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Thu, 25 Sep 2025 15:48:22 -0700 Subject: [PATCH 52/70] [Feat] - Cost Tracking - show input, output, tool call cost breakdown in StandardLoggingPayload (#14921) * add new CostBreakdown typed dict * fix CostBreakdown type * fix fix _store_cost_breakdown_in_logging_obj * fix CostBreakdown * test_cost_breakdown_in_standard_logging_payload --- docs/my-website/docs/proxy/logging_spec.md | 24 +++ litellm/cost_calculator.py | 49 +++++- litellm/litellm_core_utils/litellm_logging.py | 32 ++++ litellm/proxy/proxy_config.yaml | 7 + litellm/types/utils.py | 11 ++ .../test_standard_logging_payload.py | 140 ++++++++++++++++++ 6 files changed, 262 insertions(+), 1 deletion(-) diff --git a/docs/my-website/docs/proxy/logging_spec.md b/docs/my-website/docs/proxy/logging_spec.md index 5166b86ae17..205282428ee 100644 --- a/docs/my-website/docs/proxy/logging_spec.md +++ b/docs/my-website/docs/proxy/logging_spec.md @@ -11,6 +11,7 @@ Found under `kwargs["standard_logging_object"]`. This is a standard payload, log | `trace_id` | `str` | Trace multiple LLM calls belonging to same overall request | | `call_type` | `str` | Type of call | | `response_cost` | `float` | Cost of the response in USD ($) | +| `cost_breakdown` | `Optional[CostBreakdown]` | Detailed cost breakdown object | | `response_cost_failure_debug_info` | `StandardLoggingModelCostFailureDebugInformation` | Debug information if cost tracking fails | | `status` | `StandardLoggingPayloadStatus` | Status of the payload | | `total_tokens` | `int` | Total number of tokens | @@ -39,6 +40,29 @@ Found under `kwargs["standard_logging_object"]`. This is a standard payload, log | `model_parameters` | `dict` | Model parameters | | `hidden_params` | `StandardLoggingHiddenParams` | Hidden parameters | +## Cost Breakdown + +The `cost_breakdown` field provides detailed cost breakdown for completion requests as a `CostBreakdown` object containing: + +- **`input_cost`**: Cost of input/prompt tokens including cache creation tokens +- **`output_cost`**: Cost of output/completion tokens (including reasoning tokens if applicable) +- **`tool_usage_cost`**: Cost of built-in tools usage (e.g., web search, code interpreter) +- **`total_cost`**: Total cost of input + output + tool usage + +**Note**: This field is populated for all call types. For non-completion calls, `input_cost` and `output_cost` may be 0. + +The total cost relationship is: `response_cost = cost_breakdown.total_cost` + +### CostBreakdown Type + +```python +class CostBreakdown(TypedDict, total=False): + input_cost: float # Cost of input/prompt tokens in USD + output_cost: float # Cost of output/completion tokens in USD (includes reasoning) + tool_usage_cost: float # Cost of built-in tools usage in USD + total_cost: float # Total cost in USD +``` + ## StandardLoggingUserAPIKeyMetadata | Field | Type | Description | diff --git a/litellm/cost_calculator.py b/litellm/cost_calculator.py index 36a562b3574..94a9523facd 100644 --- a/litellm/cost_calculator.py +++ b/litellm/cost_calculator.py @@ -584,6 +584,42 @@ def _infer_call_type( return call_type +def _store_cost_breakdown_in_logging_obj( + litellm_logging_obj: Optional[LitellmLoggingObject], + prompt_tokens_cost_usd_dollar: float, + completion_tokens_cost_usd_dollar: float, + cost_for_built_in_tools_cost_usd_dollar: float, + total_cost_usd_dollar: float, +) -> None: + """ + Helper function to store cost breakdown in the logging object. + + Args: + litellm_logging_obj: The logging object to store breakdown in + call_type: Type of call (completion, etc.) + prompt_tokens_cost_usd_dollar: Cost of input tokens + completion_tokens_cost_usd_dollar: Cost of completion tokens (includes reasoning if applicable) + cost_for_built_in_tools_cost_usd_dollar: Cost of built-in tools + total_cost_usd_dollar: Total cost of request + """ + if (litellm_logging_obj is None): + return + + try: + # Store the cost breakdown - reasoning cost is 0 since it's already included in completion cost + litellm_logging_obj.set_cost_breakdown( + input_cost=prompt_tokens_cost_usd_dollar, + output_cost=completion_tokens_cost_usd_dollar, + total_cost=total_cost_usd_dollar, + cost_for_built_in_tools_cost_usd_dollar=cost_for_built_in_tools_cost_usd_dollar + ) + + except Exception as breakdown_error: + verbose_logger.debug(f"Error storing cost breakdown: {str(breakdown_error)}") + # Don't fail the main cost calculation if breakdown storage fails + pass + + def completion_cost( # noqa: PLR0915 completion_response=None, model: Optional[str] = None, @@ -923,7 +959,7 @@ def completion_cost( # noqa: PLR0915 _final_cost = ( prompt_tokens_cost_usd_dollar + completion_tokens_cost_usd_dollar ) - _final_cost += ( + cost_for_built_in_tools = ( StandardBuiltInToolCostTracking.get_cost_for_built_in_tools( model=model, response_object=completion_response, @@ -932,6 +968,17 @@ def completion_cost( # noqa: PLR0915 custom_llm_provider=custom_llm_provider, ) ) + _final_cost += cost_for_built_in_tools + + # Store cost breakdown in logging object if available + _store_cost_breakdown_in_logging_obj( + litellm_logging_obj=litellm_logging_obj, + prompt_tokens_cost_usd_dollar=prompt_tokens_cost_usd_dollar, + completion_tokens_cost_usd_dollar=completion_tokens_cost_usd_dollar, + cost_for_built_in_tools_cost_usd_dollar=cost_for_built_in_tools, + total_cost_usd_dollar=_final_cost + ) + return _final_cost except Exception as e: verbose_logger.debug( diff --git a/litellm/litellm_core_utils/litellm_logging.py b/litellm/litellm_core_utils/litellm_logging.py index 64986970d00..d7d82d0440e 100644 --- a/litellm/litellm_core_utils/litellm_logging.py +++ b/litellm/litellm_core_utils/litellm_logging.py @@ -84,6 +84,7 @@ from litellm.types.rerank import RerankResponse from litellm.types.router import CustomPricingLiteLLMParams from litellm.types.utils import ( CallTypes, + CostBreakdown, CostResponseTypes, DynamicPromptManagementParamLiteral, EmbeddingResponse, @@ -343,6 +344,9 @@ class Logging(LiteLLMLoggingBaseClass): litellm_params = scrub_sensitive_keys_in_metadata(litellm_params) self.litellm_params = litellm_params + + # Initialize cost breakdown field + self.cost_breakdown: Optional[CostBreakdown] = None self.model_call_details: Dict[str, Any] = { "litellm_trace_id": litellm_trace_id, @@ -1155,6 +1159,33 @@ class Logging(LiteLLMLoggingBaseClass): - self.model_call_details.get("start_time", datetime.datetime.now()) ).total_seconds() * 1000 + def set_cost_breakdown( + self, + input_cost: float, + output_cost: float, + total_cost: float, + cost_for_built_in_tools_cost_usd_dollar: float, + ) -> None: + """ + Helper method to store cost breakdown in the logging object. + + Args: + input_cost: Cost of input/prompt tokens + output_cost: Cost of output/completion tokens + cost_for_built_in_tools_cost_usd_dollar: Cost of built-in tools + total_cost: Total cost of request + """ + + self.cost_breakdown = CostBreakdown( + input_cost=input_cost, + output_cost=output_cost, + total_cost=total_cost, + tool_usage_cost=cost_for_built_in_tools_cost_usd_dollar + ) + verbose_logger.debug( + f"Cost breakdown set - input: {input_cost}, output: {output_cost}, cost_for_built_in_tools_cost_usd_dollar: {cost_for_built_in_tools_cost_usd_dollar}, total: {total_cost}" + ) + def _response_cost_calculator( self, result: Union[ @@ -4533,6 +4564,7 @@ def get_standard_logging_object_payload( metadata=clean_metadata, cache_key=clean_hidden_params["cache_key"], response_cost=response_cost, + cost_breakdown=logging_obj.cost_breakdown, total_tokens=usage.total_tokens, prompt_tokens=usage.prompt_tokens, completion_tokens=usage.completion_tokens, diff --git a/litellm/proxy/proxy_config.yaml b/litellm/proxy/proxy_config.yaml index 921b564407f..9897e17d29a 100644 --- a/litellm/proxy/proxy_config.yaml +++ b/litellm/proxy/proxy_config.yaml @@ -36,3 +36,10 @@ guardrails: breakdown: true payload: true dev_info: true + +litellm_settings: + callbacks: ["datadog"] + datadog_params: + turn_off_message_logging: true + datadog_llm_observability_params: + turn_off_message_logging: true \ No newline at end of file diff --git a/litellm/types/utils.py b/litellm/types/utils.py index d37978f3e21..16a69d049c6 100644 --- a/litellm/types/utils.py +++ b/litellm/types/utils.py @@ -2060,12 +2060,23 @@ class StandardLoggingGuardrailInformation(TypedDict, total=False): StandardLoggingPayloadStatus = Literal["success", "failure"] +class CostBreakdown(TypedDict): + """ + Detailed cost breakdown for a request + """ + input_cost: float # Cost of input/prompt tokens + output_cost: float # Cost of output/completion tokens (includes reasoning if applicable) + total_cost: float # Total cost (input + output + tool usage) + tool_usage_cost: float # Cost of usage of built-in tools + + class StandardLoggingPayload(TypedDict): id: str trace_id: str # Trace multiple LLM calls belonging to same overall request (e.g. fallbacks/retries) call_type: str stream: Optional[bool] response_cost: float + cost_breakdown: Optional[CostBreakdown] # Detailed cost breakdown response_cost_failure_debug_info: Optional[ StandardLoggingModelCostFailureDebugInformation ] diff --git a/tests/logging_callback_tests/test_standard_logging_payload.py b/tests/logging_callback_tests/test_standard_logging_payload.py index 0e6fc41221a..1c01ab91938 100644 --- a/tests/logging_callback_tests/test_standard_logging_payload.py +++ b/tests/logging_callback_tests/test_standard_logging_payload.py @@ -516,3 +516,143 @@ def test_standard_logging_metadata_requester_metadata( ): result = StandardLoggingPayloadSetup.get_standard_logging_metadata(metadata) assert result["requester_metadata"] == expected_requester_metadata + + +def test_cost_breakdown_in_standard_logging_payload(): + """ + Test that cost breakdown fields are properly included in StandardLoggingPayload. + Tests input_cost, output_cost, tool_usage_cost, and total_cost fields. + """ + from litellm.litellm_core_utils.litellm_logging import get_standard_logging_object_payload, Logging + from litellm.types.utils import Usage + from datetime import datetime + import time + + # Create a mock logging object with cost breakdown + logging_obj = Logging( + model="gpt-4o", + messages=[{"role": "user", "content": "Hello"}], + stream=False, + call_type="completion", + start_time=datetime.now(), + litellm_call_id="test-123", + function_id="test-function" + ) + + # Simulate cost breakdown being stored during cost calculation + logging_obj.set_cost_breakdown( + input_cost=0.001, + output_cost=0.002, + total_cost=0.0035, + cost_for_built_in_tools_cost_usd_dollar=0.0005 + ) + + # Mock response object + mock_response = { + "id": "chatcmpl-123", + "object": "chat.completion", + "model": "gpt-4o", + "usage": { + "prompt_tokens": 10, + "completion_tokens": 20, + "total_tokens": 30, + }, + "choices": [ + { + "index": 0, + "message": { + "role": "assistant", + "content": "Hello! How can I help you today?" + }, + "finish_reason": "stop" + } + ] + } + + # Create kwargs + kwargs = { + "model": "gpt-4o", + "messages": [{"role": "user", "content": "Hello"}], + "response_cost": 0.0035, + "custom_llm_provider": "openai", + } + + start_time = datetime.now() + end_time = datetime.now() + + # Get the standard logging payload + payload = get_standard_logging_object_payload( + kwargs=kwargs, + init_response_obj=mock_response, + start_time=start_time, + end_time=end_time, + logging_obj=logging_obj, + status="success" + ) + + # Verify the cost breakdown field is present + assert payload is not None + assert payload["cost_breakdown"] is not None + assert payload["cost_breakdown"]["input_cost"] == 0.001 + assert payload["cost_breakdown"]["output_cost"] == 0.002 + assert payload["cost_breakdown"]["tool_usage_cost"] == 0.0005 + assert payload["cost_breakdown"]["total_cost"] == 0.0035 + assert payload["response_cost"] == 0.0035 + + print("✅ Cost breakdown test passed!") + + +def test_cost_breakdown_missing_in_standard_logging_payload(): + """ + Test that cost breakdown field is None when not available (e.g., for embedding calls) + """ + from litellm.litellm_core_utils.litellm_logging import get_standard_logging_object_payload, Logging + from datetime import datetime + + # Create a mock logging object without cost breakdown + logging_obj = Logging( + model="gpt-4o", + messages=[{"role": "user", "content": "Hello"}], + stream=False, + call_type="embedding", # Non-completion call type + start_time=datetime.now(), + litellm_call_id="test-123", + function_id="test-function" + ) + + # No cost breakdown stored + + # Mock response object + mock_response = { + "object": "list", + "data": [{"embedding": [0.1, 0.2, 0.3]}], + "model": "text-embedding-ada-002", + "usage": {"prompt_tokens": 10, "total_tokens": 10} + } + + kwargs = { + "model": "text-embedding-ada-002", + "input": ["Hello"], + "response_cost": 0.0001, + "custom_llm_provider": "openai", + } + + start_time = datetime.now() + end_time = datetime.now() + + # Get the standard logging payload + payload = get_standard_logging_object_payload( + kwargs=kwargs, + init_response_obj=mock_response, + start_time=start_time, + end_time=end_time, + logging_obj=logging_obj, + status="success" + ) + + # Verify the cost breakdown field is None for non-completion calls + assert payload is not None + assert payload["cost_breakdown"] is None + assert payload["response_cost"] == 0.0001 + + print("✅ Cost breakdown missing test passed!") From dcbccd1feac87ea14ed34a7bad63c28689ffafcf Mon Sep 17 00:00:00 2001 From: Teddy Amkie Date: Thu, 25 Sep 2025 15:49:19 -0700 Subject: [PATCH 53/70] Corrected docs updates sept 2025 (#14916) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * docs: Corrected documentation updates from Sept 2025 This PR contains the actual intended documentation changes, properly synced with main: ✅ Real changes applied: - Added AWS authentication link to bedrock guardrails documentation - Updated Vertex AI with Gemini API alternative configuration - Added async_post_call_success_hook code snippet to custom callback docs - Added SSO free for up to 5 users information to enterprise and custom_sso docs - Added SSO free information block to security.md - Added cancel response API usage and curl example to response_api.md - Added image for modifying default user budget via admin UI - Re-ordered sidebars in documentation ❌ Sync issues resolved: - Kept all upstream changes that were added to main after branch diverged - Preserved Provider-Specific Metadata Parameters section that was added upstream - Maintained proper curl parameter formatting (-d instead of -D) This corrects the sync issues from the original PR #14769. * docs: Restore missing files from original PR Added back ~16 missing documentation files that were part of the original PR: ✅ Restored files: - docs/my-website/docs/completion/usage.md - docs/my-website/docs/fine_tuning.md - docs/my-website/docs/getting_started.md - docs/my-website/docs/image_edits.md - docs/my-website/docs/image_generation.md - docs/my-website/docs/index.md - docs/my-website/docs/moderation.md - docs/my-website/docs/observability/callbacks.md - docs/my-website/docs/providers/bedrock.md - docs/my-website/docs/proxy/caching.md - docs/my-website/docs/proxy/config_settings.md - docs/my-website/docs/proxy/db_deadlocks.md - docs/my-website/docs/proxy/load_balancing.md - docs/my-website/docs/proxy_api.md - docs/my-website/docs/rerank.md ✅ Fixed context-caching issue: - Restored provider_specific_params.md to main version (preserving Provider-Specific Metadata Parameters section) - Your original PR didn't intend to modify this file - it was just a sync issue Now includes all ~26 documentation files from the original PR #14769. * docs: Remove files that were deleted in original PR - Removed docs/my-website/docs/providers/azure_ai_img_edit.md (was deleted in original PR) - sdk/headers.md was already not present Now matches the complete intended changes from original PR #14769. * docs: Restore azure_ai_img_edit.md from main - Restored docs/my-website/docs/providers/azure_ai_img_edit.md from main branch - This file should not have been deleted as it was a newer commit - SDK headers file doesn't exist in main (was reverted) and wasn't part of your original changes Fixes the file restoration issues. * docs: Fix vertex.md - preserve context caching from newer commit - Restored vertex.md to main version to preserve context caching content (lines 817-887) - Added back only your intended change: alternative gemini config example - Context caching content from newer commit is now preserved Fixes the vertex.md sync issue where newer content was incorrectly deleted. * docs: Fix providers/bedrock.md - restore deleted content from newer commit - Restored providers/bedrock.md to main version - Preserves 'Usage - Request Metadata' section that was added in newer commit - Your actual intended change was to proxy/guardrails/bedrock.md (authentication tip) which is preserved - Now only has additions, no subtractions as intended Fixes the bedrock.md sync issue. * docs: Restore missing IAM policy section in bedrock.md Added back your intended IAM policy documentation that was lost when restoring main version: ✅ Added IAM AssumeRole Policy section: - Explains requirement for sts:AssumeRole permission - Shows error message example when permission missing - Provides complete IAM policy JSON example - Links to AWS AssumeRole documentation - Clarifies trust policy requirements Now bedrock.md has both: - All newer content preserved (Request Metadata section) - Your intended IAM policy addition restored --------- Co-authored-by: Cursor Agent --- docs/my-website/docs/completion/usage.md | 1 + docs/my-website/docs/enterprise.md | 5 + docs/my-website/docs/fine_tuning.md | 2 + docs/my-website/docs/getting_started.md | 3 +- docs/my-website/docs/image_edits.md | 3 + docs/my-website/docs/image_generation.md | 2 + docs/my-website/docs/index.md | 9 + docs/my-website/docs/moderation.md | 2 + .../docs/observability/callbacks.md | 21 +- .../docs/observability/custom_callback.md | 17 + docs/my-website/docs/providers/bedrock.md | 33 ++ docs/my-website/docs/providers/vertex.md | 13 + docs/my-website/docs/proxy/caching.md | 14 + docs/my-website/docs/proxy/config_settings.md | 1 + docs/my-website/docs/proxy/custom_sso.md | 4 +- docs/my-website/docs/proxy/db_deadlocks.md | 26 ++ .../docs/proxy/guardrails/bedrock.md | 4 + docs/my-website/docs/proxy/load_balancing.md | 3 + docs/my-website/docs/proxy/self_serve.md | 6 +- docs/my-website/docs/proxy_api.md | 2 +- docs/my-website/docs/rerank.md | 2 + docs/my-website/docs/response_api.md | 40 +++ .../img/default_user_settings_admin_ui.png | Bin 0 -> 239138 bytes docs/my-website/sidebars.js | 315 +++++++++--------- security.md | 5 + 25 files changed, 371 insertions(+), 162 deletions(-) create mode 100644 docs/my-website/img/default_user_settings_admin_ui.png diff --git a/docs/my-website/docs/completion/usage.md b/docs/my-website/docs/completion/usage.md index 2a9eab941ea..c388e5bfee1 100644 --- a/docs/my-website/docs/completion/usage.md +++ b/docs/my-website/docs/completion/usage.md @@ -26,6 +26,7 @@ response = completion( print(response.usage) ``` +> **Note:** LiteLLM supports endpoint bridging—if a model does not natively support a requested endpoint, LiteLLM will automatically route the call to the correct supported endpoint (such as bridging `/chat/completions` to `/responses` or vice versa) based on the model's `mode`set in `model_prices_and_context_window`. ## Streaming Usage diff --git a/docs/my-website/docs/enterprise.md b/docs/my-website/docs/enterprise.md index 9101d8e3751..cc3466fc103 100644 --- a/docs/my-website/docs/enterprise.md +++ b/docs/my-website/docs/enterprise.md @@ -1,6 +1,11 @@ import Image from '@theme/IdealImage'; # Enterprise + +:::info +✨ SSO is free for up to 5 users. After that, an enterprise license is required. [Get Started with Enterprise here](https://www.litellm.ai/enterprise) +::: + For companies that need SSO, user management and professional support for LiteLLM Proxy :::info diff --git a/docs/my-website/docs/fine_tuning.md b/docs/my-website/docs/fine_tuning.md index f9a9297e062..f3f955cb01d 100644 --- a/docs/my-website/docs/fine_tuning.md +++ b/docs/my-website/docs/fine_tuning.md @@ -13,6 +13,8 @@ This is an Enterprise only endpoint [Get Started with Enterprise here](https://c | Feature | Supported | Notes | |-------|-------|-------| | Supported Providers | OpenAI, Azure OpenAI, Vertex AI | - | + +#### ⚡️See an exhaustive list of supported models and providers at [models.litellm.ai](https://models.litellm.ai/) | Cost Tracking | 🟡 | [Let us know if you need this](https://github.com/BerriAI/litellm/issues) | | Logging | ✅ | Works across all logging integrations | diff --git a/docs/my-website/docs/getting_started.md b/docs/my-website/docs/getting_started.md index 15ee00a7273..6b2c1fd531e 100644 --- a/docs/my-website/docs/getting_started.md +++ b/docs/my-website/docs/getting_started.md @@ -32,7 +32,8 @@ Next Steps 👉 [Call all supported models - e.g. Claude-2, Llama2-70b, etc.](./ More details 👉 - [Completion() function details](./completion/) -- [All supported models / providers on LiteLLM](./providers/) +- [Overview of supported models / providers on LiteLLM](./providers/) +- [Search all models / providers](https://models.litellm.ai/) - [Build your own OpenAI proxy](https://github.com/BerriAI/liteLLM-proxy/tree/main) ## streaming diff --git a/docs/my-website/docs/image_edits.md b/docs/my-website/docs/image_edits.md index 246e1c70f0e..84dddd5e4ad 100644 --- a/docs/my-website/docs/image_edits.md +++ b/docs/my-website/docs/image_edits.md @@ -18,6 +18,9 @@ LiteLLM provides image editing functionality that maps to OpenAI's `/images/edit | Supported LiteLLM Proxy Versions | 1.71.1+ | | | Supported LLM providers | **OpenAI** | Currently only `openai` is supported | + #### ⚡️See all supported models and providers at [models.litellm.ai](https://models.litellm.ai/) + + ## Usage ### LiteLLM Python SDK diff --git a/docs/my-website/docs/image_generation.md b/docs/my-website/docs/image_generation.md index 7e7ff9922d6..8cd5803aa6c 100644 --- a/docs/my-website/docs/image_generation.md +++ b/docs/my-website/docs/image_generation.md @@ -279,6 +279,8 @@ print(f"response: {response}") ## Supported Providers +#### ⚡️See all supported models and providers at [models.litellm.ai](https://models.litellm.ai/) + | Provider | Documentation Link | |----------|-------------------| | OpenAI | [OpenAI Image Generation →](./providers/openai) | diff --git a/docs/my-website/docs/index.md b/docs/my-website/docs/index.md index 3f5e1b479c3..11d2963b7a3 100644 --- a/docs/my-website/docs/index.md +++ b/docs/my-website/docs/index.md @@ -524,6 +524,15 @@ try: except OpenAIError as e: print(e) ``` +### See How LiteLLM Transforms Your Requests + +Want to understand how LiteLLM parses and normalizes your LLM API requests? Use the `/utils/transform_request` endpoint to see exactly how your request is transformed internally. + +You can try it out now directly on our Demo App! +Go to the [LiteLLM API docs for transform_request](https://litellm-api.up.railway.app/#/llm%20utils/transform_request_utils_transform_request_post) + +LiteLLM will show you the normalized, provider-agnostic version of your request. This is useful for debugging, learning, and understanding how LiteLLM handles different providers and options. + ### Logging Observability - Log LLM Input/Output ([Docs](https://docs.litellm.ai/docs/observability/callbacks)) LiteLLM exposes pre defined callbacks to send data to Lunary, MLflow, Langfuse, Helicone, Promptlayer, Traceloop, Slack diff --git a/docs/my-website/docs/moderation.md b/docs/my-website/docs/moderation.md index 95fe8b2856d..f9c2810bc8a 100644 --- a/docs/my-website/docs/moderation.md +++ b/docs/my-website/docs/moderation.md @@ -130,6 +130,8 @@ Here's the exact json output and type you can expect from all moderation calls: ## **Supported Providers** +#### ⚡️See all supported models and providers at [models.litellm.ai](https://models.litellm.ai/) + | Provider | |-------------| | OpenAI | diff --git a/docs/my-website/docs/observability/callbacks.md b/docs/my-website/docs/observability/callbacks.md index 040d83697d3..b752bdc2764 100644 --- a/docs/my-website/docs/observability/callbacks.md +++ b/docs/my-website/docs/observability/callbacks.md @@ -5,13 +5,15 @@ liteLLM provides `input_callbacks`, `success_callbacks` and `failure_callbacks`, making it easy for you to send data to a particular provider depending on the status of your responses. :::tip -**New to LiteLLM Callbacks?** Check out our comprehensive [Callback Management Guide](./callback_management.md) to understand when to use different callback hooks like `async_log_success_event` vs `async_post_call_success_hook`. +**New to LiteLLM Callbacks?** + +- For proxy/server logging and observability, see the [Proxy Logging Guide](https://docs.litellm.ai/docs/proxy/logging). +- To write your own callback logic, see the [Custom Callbacks Guide](https://docs.litellm.ai/docs/observability/custom_callback). ::: -liteLLM supports: -- [Custom Callback Functions](https://docs.litellm.ai/docs/observability/custom_callback) -- [Callback Management Guide](./callback_management.md) - **Comprehensive guide for choosing the right hooks** +### Supported Callback Integrations + - [Lunary](https://lunary.ai/docs) - [Langfuse](https://langfuse.com/docs) - [LangSmith](https://www.langchain.com/langsmith) @@ -21,9 +23,20 @@ liteLLM supports: - [Sentry](https://docs.sentry.io/platforms/python/) - [PostHog](https://posthog.com/docs/libraries/python) - [Slack](https://slack.dev/bolt-python/concepts) +- [Arize](https://docs.arize.com/) +- [PromptLayer](https://docs.promptlayer.com/) This is **not** an extensive list. Please check the dropdown for all logging integrations. +### Related Cookbooks +Try out our cookbooks for code snippets and interactive demos: + +- [Langfuse Callback Example (Colab)](https://colab.research.google.com/github/BerriAI/litellm/blob/main/cookbook/logging_observability/LiteLLM_Langfuse.ipynb) +- [Lunary Callback Example (Colab)](https://colab.research.google.com/github/BerriAI/litellm/blob/main/cookbook/logging_observability/LiteLLM_Lunary.ipynb) +- [Arize Callback Example (Colab)](https://colab.research.google.com/github/BerriAI/litellm/blob/main/cookbook/logging_observability/LiteLLM_Arize.ipynb) +- [Proxy + Langfuse Callback Example (Colab)](https://colab.research.google.com/github/BerriAI/litellm/blob/main/cookbook/logging_observability/LiteLLM_Proxy_Langfuse.ipynb) +- [PromptLayer Callback Example (Colab)](https://colab.research.google.com/github/BerriAI/litellm/blob/main/cookbook/LiteLLM_PromptLayer.ipynb) + ### Quick Start ```python diff --git a/docs/my-website/docs/observability/custom_callback.md b/docs/my-website/docs/observability/custom_callback.md index c206c23d0f4..cfe97ca42c0 100644 --- a/docs/my-website/docs/observability/custom_callback.md +++ b/docs/my-website/docs/observability/custom_callback.md @@ -67,6 +67,23 @@ asyncio.run(completion()) - `async_post_call_success_hook` - Access user data + modify responses - `async_pre_call_hook` - Modify requests before sending +### Example: Modifying the Response in async_post_call_success_hook + +You can use `async_post_call_success_hook` to add custom headers or metadata to the response before it is returned to the client. For example: + +```python +async def async_post_call_success_hook(data, user_api_key_dict, response): + # Add a custom header to the response + additional_headers = getattr(response, "_hidden_params", {}).get("additional_headers", {}) or {} + additional_headers["x-litellm-custom-header"] = "my-value" + if not hasattr(response, "_hidden_params"): + response._hidden_params = {} + response._hidden_params["additional_headers"] = additional_headers + return response +``` + +This allows you to inject custom metadata or headers into the response for downstream consumers. You can use this pattern to pass information to clients, proxies, or observability tools. + ## Callback Functions If you just want to log on a specific event (e.g. on input) - you can use callback functions. diff --git a/docs/my-website/docs/providers/bedrock.md b/docs/my-website/docs/providers/bedrock.md index 86e9ac5e3e6..fe996099145 100644 --- a/docs/my-website/docs/providers/bedrock.md +++ b/docs/my-website/docs/providers/bedrock.md @@ -2340,6 +2340,39 @@ response = completion( Make the bedrock completion call +--- + +### Required AWS IAM Policy for AssumeRole + +To use `aws_role_name` (STS AssumeRole) with LiteLLM, your IAM user or role **must** have permission to call `sts:AssumeRole` on the target role. If you see an error like: + +``` +An error occurred (AccessDenied) when calling the AssumeRole operation: User: arn:aws:sts::...:assumed-role/litellm-ecs-task-role/... is not authorized to perform: sts:AssumeRole on resource: arn:aws:iam::...:role/Enterprise/BedrockCrossAccountConsumer +``` + +This means the IAM identity running LiteLLM does **not** have permission to assume the target role. You must update your IAM policy to allow this action. + +#### Example IAM Policy + +Replace `` with the ARN of the role you want to assume (e.g., `arn:aws:iam::123456789012:role/Enterprise/BedrockCrossAccountConsumer`). + +```json +{ + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": "sts:AssumeRole", + "Resource": "" + } + ] +} +``` + +**Note:** The target role itself must also trust the calling IAM identity (via its trust policy) for AssumeRole to succeed. See [AWS AssumeRole docs](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_use_switch-role-api.html) for more details. + +--- + diff --git a/docs/my-website/docs/providers/vertex.md b/docs/my-website/docs/providers/vertex.md index 3f4d1068958..78b6abd84af 100644 --- a/docs/my-website/docs/providers/vertex.md +++ b/docs/my-website/docs/providers/vertex.md @@ -196,6 +196,19 @@ model_list: vertex_location: "us-central1" vertex_credentials: "/path/to/service_account.json" # [OPTIONAL] Do this OR `!gcloud auth application-default login` - run this to add vertex credentials to your env ``` +or +```yaml +model_list: + - model_name: gemini-pro + litellm_params: + model: vertex_ai/gemini-1.5-pro + litellm_credential_name: vertex-global + vertex_project: project-name-here + vertex_location: global + base_model: gemini + model_info: + provider: Vertex +``` 2. Start Proxy diff --git a/docs/my-website/docs/proxy/caching.md b/docs/my-website/docs/proxy/caching.md index 1fb7385f689..617609cf08a 100644 --- a/docs/my-website/docs/proxy/caching.md +++ b/docs/my-website/docs/proxy/caching.md @@ -958,6 +958,19 @@ curl http://localhost:4000/v1/chat/completions \ + +## Redis max_connections + +You can set the `max_connections` parameter in your `cache_params` for Redis. This is passed directly to the Redis client and controls the maximum number of simultaneous connections in the pool. If you see errors like `No connection available`, try increasing this value: + +```yaml +litellm_settings: + cache: true + cache_params: + type: redis + max_connections: 100 +``` + ## Supported `cache_params` on proxy config.yaml ```yaml @@ -966,6 +979,7 @@ cache_params: ttl: Optional[float] default_in_memory_ttl: Optional[float] default_in_redis_ttl: Optional[float] + max_connections: Optional[Int] # Type of cache (options: "local", "redis", "s3") type: s3 diff --git a/docs/my-website/docs/proxy/config_settings.md b/docs/my-website/docs/proxy/config_settings.md index 974e95a07bd..f70701886b5 100644 --- a/docs/my-website/docs/proxy/config_settings.md +++ b/docs/my-website/docs/proxy/config_settings.md @@ -50,6 +50,7 @@ litellm_settings: port: 6379 # The port number for the Redis cache. Required if type is "redis". password: "your_password" # The password for the Redis cache. Required if type is "redis". namespace: "litellm.caching.caching" # namespace for redis cache + max_connections: 100 # [OPTIONAL] Set Maximum number of Redis connections. Passed directly to redis-py. # Optional - Redis Cluster Settings redis_startup_nodes: [{"host": "127.0.0.1", "port": "7001"}] diff --git a/docs/my-website/docs/proxy/custom_sso.md b/docs/my-website/docs/proxy/custom_sso.md index 8e869a11393..bbd7f41bee1 100644 --- a/docs/my-website/docs/proxy/custom_sso.md +++ b/docs/my-website/docs/proxy/custom_sso.md @@ -1,9 +1,7 @@ # ✨ Event Hooks for SSO Login :::info - -✨ This is an Enterprise only feature [Get Started with Enterprise here](https://www.litellm.ai/enterprise) - +✨ SSO is free for up to 5 users. After that, an enterprise license is required. [Get Started with Enterprise here](https://www.litellm.ai/enterprise) ::: ## Overview diff --git a/docs/my-website/docs/proxy/db_deadlocks.md b/docs/my-website/docs/proxy/db_deadlocks.md index 0eee928fa64..ef9d31d6232 100644 --- a/docs/my-website/docs/proxy/db_deadlocks.md +++ b/docs/my-website/docs/proxy/db_deadlocks.md @@ -84,3 +84,29 @@ LiteLLM emits the following prometheus metrics to monitor the health/status of t | `litellm_in_memory_spend_update_queue_size` | In-memory aggregate spend values for keys, users, teams, team members, etc.| In-Memory | | `litellm_redis_spend_update_queue_size` | Redis aggregate spend values for keys, users, teams, etc. | Redis | + +## Troubleshooting: Redis Connection Errors + +You may see errors like: + +``` +LiteLLM Redis Caching: async async_increment() - Got exception from REDIS No connection available., Writing value=21 +LiteLLM Redis Caching: async set_cache_pipeline() - Got exception from REDIS No connection available., Writing value=None +``` + +This means all available Redis connections are in use, and LiteLLM cannot obtain a new connection from the pool. This can happen under high load or with many concurrent proxy requests. + +**Solution:** + +- Increase the `max_connections` parameter in your Redis config section in `proxy_config.yaml` to allow more simultaneous connections. For example: + +```yaml +litellm_settings: + cache: True + cache_params: + type: redis + max_connections: 100 # Increase as needed for your traffic +``` + +Adjust this value based on your expected concurrency and Redis server capacity. + diff --git a/docs/my-website/docs/proxy/guardrails/bedrock.md b/docs/my-website/docs/proxy/guardrails/bedrock.md index 6725acf1f25..4a1a0a246f8 100644 --- a/docs/my-website/docs/proxy/guardrails/bedrock.md +++ b/docs/my-website/docs/proxy/guardrails/bedrock.md @@ -4,6 +4,10 @@ import TabItem from '@theme/TabItem'; # Bedrock Guardrails +:::tip ⚡️ +If you haven't set up or authenticated your Bedrock provider yet, see the [Bedrock Provider Setup & Authentication Guide](../../providers/bedrock.md). +::: + LiteLLM supports Bedrock guardrails via the [Bedrock ApplyGuardrail API](https://docs.aws.amazon.com/bedrock/latest/APIReference/API_runtime_ApplyGuardrail.html). ## Quick Start diff --git a/docs/my-website/docs/proxy/load_balancing.md b/docs/my-website/docs/proxy/load_balancing.md index bcbc4e93651..54c917bbbca 100644 --- a/docs/my-website/docs/proxy/load_balancing.md +++ b/docs/my-website/docs/proxy/load_balancing.md @@ -172,6 +172,9 @@ router_settings: redis_host: redis_password: redis_port: 1992 + cache_params: + type: redis + max_connections: 100 # maximum Redis connections in the pool; tune based on expected concurrency/load ``` ## Router settings on config - routing_strategy, model_group_alias diff --git a/docs/my-website/docs/proxy/self_serve.md b/docs/my-website/docs/proxy/self_serve.md index dff55a8ac04..b54344c1d05 100644 --- a/docs/my-website/docs/proxy/self_serve.md +++ b/docs/my-website/docs/proxy/self_serve.md @@ -227,7 +227,7 @@ export PROXY_LOGOUT_URL="https://www.google.com" -### Set max budget for internal users +### Set default max budget for internal users Automatically apply budget per internal user when they sign up. By default the table will be checked every 10 minutes, for users to reset. To modify this, [see this](./users.md#reset-budgets) @@ -239,6 +239,10 @@ litellm_settings: This sets a max budget of $10 USD for internal users when they sign up. +You can also manage these settings visually in the UI: + + + This budget only applies to personal keys created by that user - seen under `Default Team` on the UI. diff --git a/docs/my-website/docs/proxy_api.md b/docs/my-website/docs/proxy_api.md index 89bfacbe19f..7612645fb54 100644 --- a/docs/my-website/docs/proxy_api.md +++ b/docs/my-website/docs/proxy_api.md @@ -27,7 +27,7 @@ Email us @ krrish@berri.ai ## Supported Models for LiteLLM Key These are the models that currently work with the "sk-litellm-.." keys. -For a complete list of models/providers that you can call with LiteLLM, [check out our provider list](./providers/) +For a complete list of models/providers that you can call with LiteLLM, [check out our provider list](./providers/) or check out [models.litellm.ai](https://models.litellm.ai/) * OpenAI models - [OpenAI docs](./providers/openai.md) * gpt-4 diff --git a/docs/my-website/docs/rerank.md b/docs/my-website/docs/rerank.md index c57eacbb224..cad64718384 100644 --- a/docs/my-website/docs/rerank.md +++ b/docs/my-website/docs/rerank.md @@ -109,6 +109,8 @@ curl http://0.0.0.0:4000/rerank \ ## **Supported Providers** +#### ⚡️See all supported models and providers at [models.litellm.ai](https://models.litellm.ai/) + | Provider | Link to Usage | |-------------|--------------------| | Cohere (v1 + v2 clients) | [Usage](#quick-start) | diff --git a/docs/my-website/docs/response_api.md b/docs/my-website/docs/response_api.md index 3e9d6683c2b..8bb10bbe36a 100644 --- a/docs/my-website/docs/response_api.md +++ b/docs/my-website/docs/response_api.md @@ -3,8 +3,11 @@ import TabItem from '@theme/TabItem'; # /responses [Beta] + LiteLLM provides a BETA endpoint in the spec of [OpenAI's `/responses` API](https://platform.openai.com/docs/api-reference/responses) +Requests to /chat/completions may be bridged here automatically when the provider lacks support for that endpoint. The model’s default `mode` determines how bridging works.(see `model_prices_and_context_window`) + | Feature | Supported | Notes | |---------|-----------|--------| | Cost Tracking | ✅ | Works with all supported models | @@ -78,6 +81,43 @@ print(retrieved_response) # retrieved_response = await litellm.aget_responses(response_id=response_id) ``` +#### CANCEL a Response +You can cancel an in-progress response (if supported by the provider): + +```python showLineNumbers title="Cancel Response by ID" +import litellm + +# First, create a response +response = litellm.responses( + model="openai/o1-pro", + input="Tell me a three sentence bedtime story about a unicorn.", + max_output_tokens=100 +) + +# Get the response ID +response_id = response.id + +# Cancel the response by ID +cancel_response = litellm.cancel_responses( + response_id=response_id +) + +print(cancel_response) + +# For async usage +# cancel_response = await litellm.acancel_responses(response_id=response_id) +``` + + +**REST API:** +```bash +curl -X POST http://localhost:4000/v1/responses/response_id/cancel \ + -H "Authorization: Bearer sk-1234" +``` + +This will attempt to cancel the in-progress response with the given ID. +**Note:** Not all providers support response cancellation. If unsupported, an error will be raised. + #### DELETE a Response ```python showLineNumbers title="Delete Response by ID" import litellm diff --git a/docs/my-website/img/default_user_settings_admin_ui.png b/docs/my-website/img/default_user_settings_admin_ui.png new file mode 100644 index 0000000000000000000000000000000000000000..5910154cd51f4914fc7db293183e6fa39a264920 GIT binary patch literal 239138 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

3Y|kZ*?uZg2 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 Date: Fri, 26 Sep 2025 04:21:00 +0530 Subject: [PATCH 54/70] (Feat) Add BitBucket Integration for Prompt Management (#14882) * Add bitbucket integration for prompt management * remove not needed file * remove not needed file * Add the correct readme * Add the correct readme * fix test for bitbucket * fix test for bitbucket --- .../docs/proxy/native_litellm_prompt.md | 90 ++- litellm/__init__.py | 9 + litellm/integrations/bitbucket/README.md | 317 +++++++++++ litellm/integrations/bitbucket/__init__.py | 66 +++ .../bitbucket/bitbucket_client.py | 241 ++++++++ .../bitbucket/bitbucket_prompt_manager.py | 508 +++++++++++++++++ .../custom_logger_registry.py | 3 +- litellm/litellm_core_utils/litellm_logging.py | 39 +- litellm/proxy/proxy_server.py | 50 +- litellm/router.py | 56 +- litellm/types/prompts/init_prompts.py | 3 +- .../bitbucket/test_bitbucket_integration.py | 366 +++++++++++++ .../test_bitbucket_prompt_manager.py | 517 ++++++++++++++++++ 13 files changed, 2214 insertions(+), 51 deletions(-) create mode 100644 litellm/integrations/bitbucket/README.md create mode 100644 litellm/integrations/bitbucket/__init__.py create mode 100644 litellm/integrations/bitbucket/bitbucket_client.py create mode 100644 litellm/integrations/bitbucket/bitbucket_prompt_manager.py create mode 100644 tests/test_litellm/integrations/bitbucket/test_bitbucket_integration.py create mode 100644 tests/test_litellm/integrations/bitbucket/test_bitbucket_prompt_manager.py diff --git a/docs/my-website/docs/proxy/native_litellm_prompt.md b/docs/my-website/docs/proxy/native_litellm_prompt.md index 1e1df999db9..0a4f1ccf35c 100644 --- a/docs/my-website/docs/proxy/native_litellm_prompt.md +++ b/docs/my-website/docs/proxy/native_litellm_prompt.md @@ -1,11 +1,19 @@ -import Image from '@theme/IdealImage'; -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; +import '@theme/IdealImage' +import '@theme/TabItem' +import '@theme/Tabs' +import Image +import TabItem +import Tabs # LiteLLM Prompt Management (GitOps) Store prompts as `.prompt` files in your repository and use them directly with LiteLLM. No external services required. +## Supported Integrations + +- **File System**: Store `.prompt` files locally +- **BitBucket**: Store `.prompt` files in BitBucket repositories with team-based access control + ## Quick Start @@ -41,6 +49,50 @@ response = litellm.completion( ) ``` + + + +**1. Create a .prompt file in BitBucket** + +Create `prompts/hello.prompt` in your BitBucket repository: + +```yaml +--- +model: gpt-4 +temperature: 0.7 +--- +System: You are a helpful assistant. + +User: {{user_message}} +``` + +**2. Configure BitBucket access** + +```python +import litellm + +# Configure BitBucket access +bitbucket_config = { + "workspace": "your-workspace", + "repository": "your-repo", + "access_token": "your-access-token", + "branch": "main" +} + +# Set global BitBucket configuration +litellm.set_global_bitbucket_config(bitbucket_config) +``` + +**3. Use with LiteLLM** + +```python +response = litellm.completion( + model="bitbucket/gpt-4", + prompt_id="hello", + prompt_variables={"user_message": "What is the capital of France?"} +) +``` + @@ -70,6 +122,12 @@ model_list: litellm_settings: global_prompt_directory: "./prompts" + # Or use BitBucket for team-based prompt management + global_bitbucket_config: + workspace: "your-workspace" + repository: "your-repo" + access_token: "your-access-token" + branch: "main" ``` **3. Start the proxy** @@ -142,21 +200,43 @@ User: {{user_message}} ### API Reference -For dotprompt integration, use these parameters: +For prompt integrations, use these parameters: +**File System (dotprompt):** ``` model: dotprompt/ # required (e.g., dotprompt/gpt-4) prompt_id: str # required - the .prompt filename without extension prompt_variables: Optional[dict] # optional - variables for template rendering ``` -**Example API call:** +**BitBucket:** +``` +model: bitbucket/ # required (e.g., bitbucket/gpt-4) +prompt_id: str # required - the .prompt filename without extension +prompt_variables: Optional[dict] # optional - variables for template rendering +bitbucket_config: Optional[dict] # optional - BitBucket configuration (if not set globally) +``` + +**Example API calls:** ```python +# File system integration response = litellm.completion( model="dotprompt/gpt-4", prompt_id="hello", prompt_variables={"user_message": "Hello world"}, messages=[{"role": "user", "content": "This will be ignored"}] ) + +# BitBucket integration +response = litellm.completion( + model="bitbucket/gpt-4", + prompt_id="hello", + prompt_variables={"user_message": "Hello world"}, + bitbucket_config={ + "workspace": "your-workspace", + "repository": "your-repo", + "access_token": "your-token" + } +) ``` diff --git a/litellm/__init__.py b/litellm/__init__.py index 69e5f78c997..6df9d0780a4 100644 --- a/litellm/__init__.py +++ b/litellm/__init__.py @@ -151,6 +151,7 @@ _custom_logger_compatible_callbacks_literal = Literal[ "aws_sqs", "vector_store_pre_call_hook", "dotprompt", + "bitbucket", "cloudzero", "posthog", ] @@ -1352,3 +1353,11 @@ from litellm.litellm_core_utils.cli_token_utils import get_litellm_gateway_api_k ### PASSTHROUGH ### from .passthrough import allm_passthrough_route, llm_passthrough_route + +### GLOBAL CONFIG ### +global_bitbucket_config: Optional[Dict[str, Any]] = None + +def set_global_bitbucket_config(config: Dict[str, Any]) -> None: + """Set global BitBucket configuration for prompt management.""" + global global_bitbucket_config + global_bitbucket_config = config diff --git a/litellm/integrations/bitbucket/README.md b/litellm/integrations/bitbucket/README.md new file mode 100644 index 00000000000..473beeea9e0 --- /dev/null +++ b/litellm/integrations/bitbucket/README.md @@ -0,0 +1,317 @@ +# LiteLLM BitBucket Prompt Management + +A powerful prompt management system for LiteLLM that fetches `.prompt` files from BitBucket repositories. This enables team-based prompt management with BitBucket's built-in access control and version control capabilities. + +## Features + +- **🏢 Team-based access control**: Leverage BitBucket's workspace and repository permissions +- **📁 Repository-based prompt storage**: Store prompts in BitBucket repositories +- **🔐 Multiple authentication methods**: Support for access tokens and basic auth +- **🎯 YAML frontmatter**: Define model, parameters, and schemas in file headers +- **🔧 Handlebars templating**: Use `{{variable}}` syntax with Jinja2 backend +- **✅ Input validation**: Automatic validation against defined schemas +- **🔗 LiteLLM integration**: Works seamlessly with `litellm.completion()` +- **💬 Smart message parsing**: Converts prompts to proper chat messages +- **⚙️ Parameter extraction**: Automatically applies model settings from prompts + +## Quick Start + +### 1. Set up BitBucket Repository + +Create a repository in your BitBucket workspace and add `.prompt` files: + +``` +your-repo/ +├── prompts/ +│ ├── chat_assistant.prompt +│ ├── code_reviewer.prompt +│ └── data_analyst.prompt +``` + +### 2. Create a `.prompt` file + +Create a file called `prompts/chat_assistant.prompt`: + +```yaml +--- +model: gpt-4 +temperature: 0.7 +max_tokens: 150 +input: + schema: + user_message: string + system_context?: string +--- + +{% if system_context %}System: {{system_context}} + +{% endif %}User: {{user_message}} +``` + +### 3. Configure BitBucket Access + +#### Option A: Access Token (Recommended) + +```python +import litellm + +# Configure BitBucket access +bitbucket_config = { + "workspace": "your-workspace", + "repository": "your-repo", + "access_token": "your-access-token", + "branch": "main" # optional, defaults to main +} + +# Set global BitBucket configuration +litellm.set_global_bitbucket_config(bitbucket_config) +``` + +#### Option B: Basic Authentication + +```python +import litellm + +# Configure BitBucket access with basic auth +bitbucket_config = { + "workspace": "your-workspace", + "repository": "your-repo", + "username": "your-username", + "access_token": "your-app-password", # Use app password for basic auth + "auth_method": "basic", + "branch": "main" +} + +litellm.set_global_bitbucket_config(bitbucket_config) +``` + +### 4. Use with LiteLLM + +```python +# Use with completion - the model prefix 'bitbucket/' tells LiteLLM to use BitBucket prompt management +response = litellm.completion( + model="bitbucket/gpt-4", # The actual model comes from the .prompt file + prompt_id="prompts/chat_assistant", # Location of the prompt file + prompt_variables={ + "user_message": "What is machine learning?", + "system_context": "You are a helpful AI tutor." + }, + # Any additional messages will be appended after the prompt + messages=[{"role": "user", "content": "Please explain it simply."}] +) + +print(response.choices[0].message.content) +``` + +## Proxy Server Configuration + +### 1. Create a `.prompt` file + +Create `prompts/hello.prompt`: + +```yaml +--- +model: gpt-4 +temperature: 0.7 +--- +System: You are a helpful assistant. + +User: {{user_message}} +``` + +### 2. Setup config.yaml + +```yaml +model_list: + - model_name: my-bitbucket-model + litellm_params: + model: bitbucket/gpt-4 + prompt_id: "prompts/hello" + api_key: os.environ/OPENAI_API_KEY + +litellm_settings: + global_bitbucket_config: + workspace: "your-workspace" + repository: "your-repo" + access_token: "your-access-token" + branch: "main" +``` + +### 3. Start the proxy + +```bash +litellm --config config.yaml --detailed_debug +``` + +### 4. Test it! + +```bash +curl -L -X POST 'http://0.0.0.0:4000/v1/chat/completions' \ +-H 'Content-Type: application/json' \ +-H 'Authorization: Bearer sk-1234' \ +-d '{ + "model": "my-bitbucket-model", + "messages": [{"role": "user", "content": "IGNORED"}], + "prompt_variables": { + "user_message": "What is the capital of France?" + } +}' +``` + +## Prompt File Format + +### Basic Structure + +```yaml +--- +# Model configuration +model: gpt-4 +temperature: 0.7 +max_tokens: 500 + +# Input schema (optional) +input: + schema: + user_message: string + system_context?: string +--- + +System: You are a helpful {{role}} assistant. + +User: {{user_message}} +``` + +### Advanced Features + +**Multi-role conversations:** + +```yaml +--- +model: gpt-4 +temperature: 0.3 +--- +System: You are a helpful coding assistant. + +User: {{user_question}} +``` + +**Dynamic model selection:** + +```yaml +--- +model: "{{preferred_model}}" # Model can be a variable +temperature: 0.7 +--- +System: You are a helpful assistant specialized in {{domain}}. + +User: {{user_message}} +``` + +## Team-Based Access Control + +BitBucket's built-in permission system provides team-based access control: + +1. **Workspace-level permissions**: Control access to entire workspaces +2. **Repository-level permissions**: Control access to specific repositories +3. **Branch-level permissions**: Control access to specific branches +4. **User and group management**: Manage team members and their access levels + +### Setting up Team Access + +1. **Create workspaces for each team**: + ``` + team-a-prompts/ + team-b-prompts/ + team-c-prompts/ + ``` + +2. **Configure repository permissions**: + - Grant read access to team members + - Grant write access to prompt maintainers + - Use branch protection rules for production prompts + +3. **Use different access tokens**: + - Each team can have their own access token + - Tokens can be scoped to specific repositories + - Use app passwords for additional security + +## API Reference + +### BitBucket Configuration + +```python +bitbucket_config = { + "workspace": str, # Required: BitBucket workspace name + "repository": str, # Required: Repository name + "access_token": str, # Required: BitBucket access token or app password + "branch": str, # Optional: Branch to fetch from (default: "main") + "base_url": str, # Optional: Custom BitBucket API URL + "auth_method": str, # Optional: "token" or "basic" (default: "token") + "username": str, # Optional: Username for basic auth + "base_url" : str # Optional: Incase where the base url is not https://api.bitbucket.org/2.0 +} +``` + +### LiteLLM Integration + +```python +response = litellm.completion( + model="bitbucket/", # required (e.g., bitbucket/gpt-4) + prompt_id=str, # required - the .prompt filename without extension + prompt_variables=dict, # optional - variables for template rendering + bitbucket_config=dict, # optional - BitBucket configuration (if not set globally) + messages=list, # optional - additional messages +) +``` + +## Error Handling + +The BitBucket integration provides detailed error messages for common issues: + +- **Authentication errors**: Invalid access tokens or credentials +- **Permission errors**: Insufficient access to workspace/repository +- **File not found**: Missing .prompt files +- **Network errors**: Connection issues with BitBucket API + +## Security Considerations + +1. **Access Token Security**: Store access tokens securely using environment variables or secret management systems +2. **Repository Permissions**: Use BitBucket's permission system to control access +3. **Branch Protection**: Protect main branches from unauthorized changes +4. **Audit Logging**: BitBucket provides audit logs for all repository access + +## Troubleshooting + +### Common Issues + +1. **"Access denied" errors**: Check your BitBucket permissions for the workspace and repository +2. **"Authentication failed" errors**: Verify your access token or credentials +3. **"File not found" errors**: Ensure the .prompt file exists in the specified branch +4. **Template rendering errors**: Check your Handlebars syntax in the .prompt file + +### Debug Mode + +Enable debug logging to troubleshoot issues: + +```python +import litellm +litellm.set_verbose = True + +# Your BitBucket prompt calls will now show detailed logs +response = litellm.completion( + model="bitbucket/gpt-4", + prompt_id="your_prompt", + prompt_variables={"key": "value"} +) +``` + +## Migration from File-Based Prompts + +If you're currently using file-based prompts with the dotprompt integration, you can easily migrate to BitBucket: + +1. **Upload your .prompt files** to a BitBucket repository +2. **Update your configuration** to use BitBucket instead of local files +3. **Set up team access** using BitBucket's permission system +4. **Update your code** to use `bitbucket/` model prefix instead of `dotprompt/` + +This provides better collaboration, version control, and team-based access control for your prompts. diff --git a/litellm/integrations/bitbucket/__init__.py b/litellm/integrations/bitbucket/__init__.py new file mode 100644 index 00000000000..111d38f78a4 --- /dev/null +++ b/litellm/integrations/bitbucket/__init__.py @@ -0,0 +1,66 @@ +from typing import TYPE_CHECKING, Optional + +if TYPE_CHECKING: + from .bitbucket_prompt_manager import BitBucketPromptManager + from litellm.types.prompts.init_prompts import PromptLiteLLMParams, PromptSpec + from litellm.integrations.custom_prompt_management import CustomPromptManagement + +from litellm.types.prompts.init_prompts import SupportedPromptIntegrations + +from .bitbucket_prompt_manager import BitBucketPromptManager + +# Global instances +global_bitbucket_config: Optional[dict] = None + + +def set_global_bitbucket_config(config: dict) -> None: + """ + Set the global BitBucket configuration for prompt management. + + Args: + config: Dictionary containing BitBucket configuration + - workspace: BitBucket workspace name + - repository: Repository name + - access_token: BitBucket access token + - branch: Branch to fetch prompts from (default: main) + """ + import litellm + + litellm.global_bitbucket_config = config # type: ignore + + +def prompt_initializer( + litellm_params: "PromptLiteLLMParams", prompt_spec: "PromptSpec" +) -> "CustomPromptManagement": + """ + Initialize a prompt from a BitBucket repository. + """ + bitbucket_config = getattr(litellm_params, "bitbucket_config", None) + prompt_id = getattr(litellm_params, "prompt_id", None) + + if not bitbucket_config: + raise ValueError( + "bitbucket_config is required for BitBucket prompt integration" + ) + + try: + bitbucket_prompt_manager = BitBucketPromptManager( + bitbucket_config=bitbucket_config, + prompt_id=prompt_id, + ) + + return bitbucket_prompt_manager + except Exception as e: + raise e + + +prompt_initializer_registry = { + SupportedPromptIntegrations.BITBUCKET.value: prompt_initializer, +} + +# Export public API +__all__ = [ + "BitBucketPromptManager", + "set_global_bitbucket_config", + "global_bitbucket_config", +] diff --git a/litellm/integrations/bitbucket/bitbucket_client.py b/litellm/integrations/bitbucket/bitbucket_client.py new file mode 100644 index 00000000000..0502422cf8b --- /dev/null +++ b/litellm/integrations/bitbucket/bitbucket_client.py @@ -0,0 +1,241 @@ +""" +BitBucket API client for fetching .prompt files from BitBucket repositories. +""" + +import base64 +from typing import Any, Dict, List, Optional + +from litellm.llms.custom_httpx.http_handler import HTTPHandler + + +class BitBucketClient: + """ + Client for interacting with BitBucket API to fetch .prompt files. + + Supports: + - Authentication with access tokens + - Fetching file contents from repositories + - Team-based access control through BitBucket permissions + - Branch-specific file fetching + """ + + def __init__(self, config: Dict[str, Any]): + """ + Initialize the BitBucket client. + + Args: + config: Dictionary containing: + - workspace: BitBucket workspace name + - repository: Repository name + - access_token: BitBucket access token (or app password) + - branch: Branch to fetch from (default: main) + - base_url: Custom BitBucket API base URL (optional) + - auth_method: Authentication method ('token' or 'basic', default: 'token') + - username: Username for basic auth (optional) + """ + self.workspace = config.get("workspace") + self.repository = config.get("repository") + self.access_token = config.get("access_token") + self.branch = config.get("branch", "main") + self.base_url = config.get("", "https://api.bitbucket.org/2.0") + self.auth_method = config.get("auth_method", "token") + self.username = config.get("username") + + if not all([self.workspace, self.repository, self.access_token]): + raise ValueError("workspace, repository, and access_token are required") + + # Set up authentication headers + self.headers = { + "Accept": "application/json", + "Content-Type": "application/json", + } + + if self.auth_method == "basic" and self.username: + # Use basic auth with username and app password + credentials = f"{self.username}:{self.access_token}" + encoded_credentials = base64.b64encode(credentials.encode()).decode() + self.headers["Authorization"] = f"Basic {encoded_credentials}" + else: + # Use token-based authentication (default) + self.headers["Authorization"] = f"Bearer {self.access_token}" + + # Initialize HTTPHandler + self.http_handler = HTTPHandler() + + def get_file_content(self, file_path: str) -> Optional[str]: + """ + Fetch the content of a file from the BitBucket repository. + + Args: + file_path: Path to the file in the repository + + Returns: + File content as string, or None if file not found + """ + url = f"{self.base_url}/repositories/{self.workspace}/{self.repository}/src/{self.branch}/{file_path}" + + try: + response = self.http_handler.get(url, headers=self.headers) + response.raise_for_status() + + # BitBucket returns file content as base64 encoded + if response.headers.get("content-type", "").startswith("text/"): + return response.text + else: + # For binary files or when content-type is not text, try to decode as base64 + try: + return base64.b64decode(response.content).decode("utf-8") + except Exception: + return response.text + + except Exception as e: + # Check if it's an HTTP error + if hasattr(e, "response") and hasattr(e.response, "status_code"): + if e.response.status_code == 404: + return None + elif e.response.status_code == 403: + raise Exception( + f"Access denied to file '{file_path}'. Check your BitBucket permissions for workspace '{self.workspace}' and repository '{self.repository}'." + ) + elif e.response.status_code == 401: + raise Exception( + "Authentication failed. Check your BitBucket access token and permissions." + ) + else: + raise Exception(f"Failed to fetch file '{file_path}': {e}") + else: + raise Exception(f"Error fetching file '{file_path}': {e}") + + def list_files( + self, directory_path: str = "", file_extension: str = ".prompt" + ) -> List[str]: + """ + List files in a directory with a specific extension. + + Args: + directory_path: Directory path in the repository (empty for root) + file_extension: File extension to filter by (default: .prompt) + + Returns: + List of file paths + """ + url = f"{self.base_url}/repositories/{self.workspace}/{self.repository}/src/{self.branch}/{directory_path}" + + try: + response = self.http_handler.get(url, headers=self.headers) + response.raise_for_status() + + data = response.json() + files = [] + + for item in data.get("values", []): + if item.get("type") == "commit_file": + file_path = item.get("path", "") + if file_path.endswith(file_extension): + files.append(file_path) + + return files + + except Exception as e: + # Check if it's an HTTP error + if hasattr(e, "response") and hasattr(e.response, "status_code"): + if e.response.status_code == 404: + return [] + elif e.response.status_code == 403: + raise Exception( + f"Access denied to directory '{directory_path}'. Check your BitBucket permissions for workspace '{self.workspace}' and repository '{self.repository}'." + ) + elif e.response.status_code == 401: + raise Exception( + "Authentication failed. Check your BitBucket access token and permissions." + ) + else: + raise Exception(f"Failed to list files in '{directory_path}': {e}") + else: + raise Exception(f"Error listing files in '{directory_path}': {e}") + + def get_repository_info(self) -> Dict[str, Any]: + """ + Get information about the repository. + + Returns: + Dictionary containing repository information + """ + url = f"{self.base_url}/repositories/{self.workspace}/{self.repository}" + + try: + response = self.http_handler.get(url, headers=self.headers) + response.raise_for_status() + return response.json() + except Exception as e: + raise Exception(f"Failed to get repository info: {e}") + + def test_connection(self) -> bool: + """ + Test the connection to the BitBucket repository. + + Returns: + True if connection is successful, False otherwise + """ + try: + self.get_repository_info() + return True + except Exception: + return False + + def get_branches(self) -> List[Dict[str, Any]]: + """ + Get list of branches in the repository. + + Returns: + List of branch information dictionaries + """ + url = f"{self.base_url}/repositories/{self.workspace}/{self.repository}/refs/branches" + + try: + response = self.http_handler.get(url, headers=self.headers) + response.raise_for_status() + + data = response.json() + return data.get("values", []) + except Exception as e: + raise Exception(f"Failed to get branches: {e}") + + def get_file_metadata(self, file_path: str) -> Optional[Dict[str, Any]]: + """ + Get metadata about a file (size, last modified, etc.). + + Args: + file_path: Path to the file in the repository + + Returns: + Dictionary containing file metadata, or None if file not found + """ + url = f"{self.base_url}/repositories/{self.workspace}/{self.repository}/src/{self.branch}/{file_path}" + + try: + # Use GET with Range header to get just the headers (HEAD equivalent) + headers = self.headers.copy() + headers["Range"] = "bytes=0-0" # Request only first byte to get headers + + response = self.http_handler.get(url, headers=headers) + response.raise_for_status() + + return { + "content_type": response.headers.get("content-type"), + "content_length": response.headers.get("content-length"), + "last_modified": response.headers.get("last-modified"), + } + except Exception as e: + # Check if it's an HTTP error + if hasattr(e, "response") and hasattr(e.response, "status_code"): + if e.response.status_code == 404: + return None + raise Exception(f"Failed to get file metadata for '{file_path}': {e}") + else: + raise Exception(f"Error getting file metadata for '{file_path}': {e}") + + def close(self): + """Close the HTTP handler to free resources.""" + if hasattr(self, "http_handler"): + self.http_handler.close() diff --git a/litellm/integrations/bitbucket/bitbucket_prompt_manager.py b/litellm/integrations/bitbucket/bitbucket_prompt_manager.py new file mode 100644 index 00000000000..96cab6d9ccb --- /dev/null +++ b/litellm/integrations/bitbucket/bitbucket_prompt_manager.py @@ -0,0 +1,508 @@ +""" +BitBucket prompt manager that integrates with LiteLLM's prompt management system. +Fetches .prompt files from BitBucket repositories and provides team-based access control. +""" + +from typing import Any, Dict, List, Optional, Tuple, Union + +from jinja2 import DictLoader, Environment, select_autoescape + +from litellm.integrations.custom_prompt_management import CustomPromptManagement +from litellm.integrations.prompt_management_base import ( + PromptManagementBase, + PromptManagementClient, +) +from litellm.types.llms.openai import AllMessageValues +from litellm.types.utils import StandardCallbackDynamicParams + +from .bitbucket_client import BitBucketClient + + +class BitBucketPromptTemplate: + """ + Represents a prompt template loaded from BitBucket. + """ + + def __init__( + self, + template_id: str, + content: str, + metadata: Dict[str, Any], + model: Optional[str] = None, + ): + self.template_id = template_id + self.content = content + self.metadata = metadata + self.model = model or metadata.get("model") + self.temperature = metadata.get("temperature") + self.max_tokens = metadata.get("max_tokens") + self.input_schema = metadata.get("input", {}).get("schema", {}) + self.optional_params = { + k: v for k, v in metadata.items() if k not in ["model", "input", "content"] + } + + def __repr__(self): + return f"BitBucketPromptTemplate(id='{self.template_id}', model='{self.model}')" + + +class BitBucketTemplateManager: + """ + Manager for loading and rendering .prompt files from BitBucket repositories. + + Supports: + - Fetching .prompt files from BitBucket repositories + - Team-based access control through BitBucket permissions + - YAML frontmatter for metadata + - Handlebars-style templating (using Jinja2) + - Input/output schema validation + - Model configuration + """ + + def __init__( + self, + bitbucket_config: Dict[str, Any], + prompt_id: Optional[str] = None, + ): + self.bitbucket_config = bitbucket_config + self.prompt_id = prompt_id + self.prompts: Dict[str, BitBucketPromptTemplate] = {} + self.bitbucket_client = BitBucketClient(bitbucket_config) + + self.jinja_env = Environment( + loader=DictLoader({}), + autoescape=select_autoescape(["html", "xml"]), + # Use Handlebars-style delimiters to match Dotprompt spec + variable_start_string="{{", + variable_end_string="}}", + block_start_string="{%", + block_end_string="%}", + comment_start_string="{#", + comment_end_string="#}", + ) + + # Load prompts from BitBucket if prompt_id is provided + if self.prompt_id: + self._load_prompt_from_bitbucket(self.prompt_id) + + def _load_prompt_from_bitbucket(self, prompt_id: str) -> None: + """Load a specific .prompt file from BitBucket.""" + try: + # Fetch the .prompt file from BitBucket + prompt_content = self.bitbucket_client.get_file_content( + f"{prompt_id}.prompt" + ) + + if prompt_content: + template = self._parse_prompt_file(prompt_content, prompt_id) + self.prompts[prompt_id] = template + except Exception as e: + raise Exception(f"Failed to load prompt '{prompt_id}' from BitBucket: {e}") + + def _parse_prompt_file( + self, content: str, prompt_id: str + ) -> BitBucketPromptTemplate: + """Parse a .prompt file content and extract metadata and template.""" + # Split frontmatter and content + if content.startswith("---"): + parts = content.split("---", 2) + if len(parts) >= 3: + frontmatter_str = parts[1].strip() + template_content = parts[2].strip() + else: + frontmatter_str = "" + template_content = content + else: + frontmatter_str = "" + template_content = content + + # Parse YAML frontmatter + metadata = {} + if frontmatter_str: + try: + import yaml + + metadata = yaml.safe_load(frontmatter_str) or {} + except ImportError: + # Fallback to basic parsing if PyYAML is not available + metadata = self._parse_yaml_basic(frontmatter_str) + except Exception: + metadata = {} + + return BitBucketPromptTemplate( + template_id=prompt_id, + content=template_content, + metadata=metadata, + ) + + def _parse_yaml_basic(self, yaml_str: str) -> Dict[str, Any]: + """Basic YAML parser for simple cases when PyYAML is not available.""" + result = {} + for line in yaml_str.split("\n"): + line = line.strip() + if ":" in line and not line.startswith("#"): + key, value = line.split(":", 1) + key = key.strip() + value = value.strip() + + # Try to parse value as appropriate type + if value.lower() in ["true", "false"]: + result[key] = value.lower() == "true" + elif value.isdigit(): + result[key] = int(value) + elif value.replace(".", "").isdigit(): + result[key] = float(value) + else: + result[key] = value.strip("\"'") + return result + + def render_template( + self, template_id: str, variables: Dict[str, Any] = None + ) -> str: + """Render a template with the given variables.""" + if template_id not in self.prompts: + raise ValueError(f"Template '{template_id}' not found") + + template = self.prompts[template_id] + jinja_template = self.jinja_env.from_string(template.content) + + return jinja_template.render(**(variables or {})) + + def get_template(self, template_id: str) -> Optional[BitBucketPromptTemplate]: + """Get a template by ID.""" + return self.prompts.get(template_id) + + def list_templates(self) -> List[str]: + """List all available template IDs.""" + return list(self.prompts.keys()) + + +class BitBucketPromptManager(CustomPromptManagement): + """ + BitBucket prompt manager that integrates with LiteLLM's prompt management system. + + This class enables using .prompt files from BitBucket repositories with the + litellm completion() function by implementing the PromptManagementBase interface. + + Usage: + # Configure BitBucket access + bitbucket_config = { + "workspace": "your-workspace", + "repository": "your-repo", + "access_token": "your-token", + "branch": "main" # optional, defaults to main + } + + # Use with completion + response = litellm.completion( + model="bitbucket/gpt-4", + prompt_id="my_prompt", + prompt_variables={"variable": "value"}, + bitbucket_config=bitbucket_config, + messages=[{"role": "user", "content": "This will be combined with the prompt"}] + ) + """ + + def __init__( + self, + bitbucket_config: Dict[str, Any], + prompt_id: Optional[str] = None, + ): + self.bitbucket_config = bitbucket_config + self.prompt_id = prompt_id + self._prompt_manager: Optional[BitBucketPromptManager] = None + + @property + def integration_name(self) -> str: + """Integration name used in model names like 'bitbucket/gpt-4'.""" + return "bitbucket" + + @property + def prompt_manager(self) -> BitBucketTemplateManager: + """Get or create the prompt manager instance.""" + if self._prompt_manager is None: + self._prompt_manager = BitBucketTemplateManager( + bitbucket_config=self.bitbucket_config, + prompt_id=self.prompt_id, + ) + return self._prompt_manager + + def get_prompt_template( + self, + prompt_id: str, + prompt_variables: Optional[Dict[str, Any]] = None, + ) -> Tuple[str, Dict[str, Any]]: + """ + Get a prompt template and render it with variables. + + Args: + prompt_id: The ID of the prompt template + prompt_variables: Variables to substitute in the template + + Returns: + Tuple of (rendered_prompt, metadata) + """ + template = self.prompt_manager.get_template(prompt_id) + if not template: + raise ValueError(f"Prompt template '{prompt_id}' not found") + + # Render the template + rendered_prompt = self.prompt_manager.render_template( + prompt_id, prompt_variables or {} + ) + + # Extract metadata + metadata = { + "model": template.model, + "temperature": template.temperature, + "max_tokens": template.max_tokens, + **template.optional_params, + } + + return rendered_prompt, metadata + + def pre_call_hook( + self, + user_id: Optional[str], + messages: List[AllMessageValues], + function_call: Optional[Union[Dict[str, Any], str]] = None, + litellm_params: Optional[Dict[str, Any]] = None, + prompt_id: Optional[str] = None, + prompt_variables: Optional[Dict[str, Any]] = None, + **kwargs, + ) -> Tuple[List[AllMessageValues], Optional[Dict[str, Any]]]: + """ + Pre-call hook that processes the prompt template before making the LLM call. + """ + if not prompt_id: + return messages, litellm_params + + try: + # Get the rendered prompt and metadata + rendered_prompt, prompt_metadata = self.get_prompt_template( + prompt_id, prompt_variables + ) + + # Parse the rendered prompt into messages + parsed_messages = self._parse_prompt_to_messages(rendered_prompt) + + # Merge with existing messages + if parsed_messages: + # If we have parsed messages, use them instead of the original messages + final_messages = parsed_messages + else: + # If no messages were parsed, prepend the prompt to existing messages + final_messages = [ + {"role": "user", "content": rendered_prompt} + ] + messages + + # Update litellm_params with prompt metadata + if litellm_params is None: + litellm_params = {} + + # Apply model and parameters from prompt metadata + if prompt_metadata.get("model"): + litellm_params["model"] = prompt_metadata["model"] + + for param in [ + "temperature", + "max_tokens", + "top_p", + "frequency_penalty", + "presence_penalty", + ]: + if param in prompt_metadata: + litellm_params[param] = prompt_metadata[param] + + return final_messages, litellm_params + + except Exception as e: + # Log error but don't fail the call + import litellm + + litellm._logging.verbose_proxy_logger.error( + f"Error in BitBucket prompt pre_call_hook: {e}" + ) + return messages, litellm_params + + def _parse_prompt_to_messages(self, prompt_content: str) -> List[AllMessageValues]: + """ + Parse prompt content into a list of messages. + Handles both simple prompts and multi-role conversations. + """ + messages = [] + lines = prompt_content.strip().split("\n") + current_role = None + current_content = [] + + for line in lines: + line = line.strip() + if not line: + continue + + # Check for role indicators + if line.lower().startswith("system:"): + if current_role and current_content: + messages.append( + { + "role": current_role, + "content": "\n".join(current_content).strip(), + } + ) + current_role = "system" + current_content = [line[7:].strip()] # Remove "System:" prefix + elif line.lower().startswith("user:"): + if current_role and current_content: + messages.append( + { + "role": current_role, + "content": "\n".join(current_content).strip(), + } + ) + current_role = "user" + current_content = [line[5:].strip()] # Remove "User:" prefix + elif line.lower().startswith("assistant:"): + if current_role and current_content: + messages.append( + { + "role": current_role, + "content": "\n".join(current_content).strip(), + } + ) + current_role = "assistant" + current_content = [line[10:].strip()] # Remove "Assistant:" prefix + else: + # Continue building current message + current_content.append(line) + + # Add the last message + if current_role and current_content: + messages.append( + {"role": current_role, "content": "\n".join(current_content).strip()} + ) + + # If no role indicators found, treat as a single user message + if not messages and prompt_content.strip(): + messages = [{"role": "user", "content": prompt_content.strip()}] + + return messages + + def post_call_hook( + self, + user_id: Optional[str], + response: Any, + input_messages: List[AllMessageValues], + function_call: Optional[Union[Dict[str, Any], str]] = None, + litellm_params: Optional[Dict[str, Any]] = None, + prompt_id: Optional[str] = None, + prompt_variables: Optional[Dict[str, Any]] = None, + **kwargs, + ) -> Any: + """ + Post-call hook for any post-processing after the LLM call. + """ + return response + + def get_available_prompts(self) -> List[str]: + """Get list of available prompt IDs.""" + return self.prompt_manager.list_templates() + + def reload_prompts(self) -> None: + """Reload prompts from BitBucket.""" + if self.prompt_id: + self._prompt_manager = None # Reset to force reload + self.prompt_manager # This will trigger reload + + def should_run_prompt_management( + self, + prompt_id: str, + dynamic_callback_params: StandardCallbackDynamicParams, + ) -> bool: + """ + Determine if prompt management should run based on the prompt_id. + + For BitBucket, we always return True and handle the prompt loading + in the _compile_prompt_helper method. + """ + return True + + def _compile_prompt_helper( + self, + prompt_id: str, + prompt_variables: Optional[dict], + dynamic_callback_params: StandardCallbackDynamicParams, + prompt_label: Optional[str] = None, + prompt_version: Optional[int] = None, + ) -> PromptManagementClient: + """ + Compile a BitBucket prompt template into a PromptManagementClient structure. + + This method: + 1. Loads the prompt template from BitBucket + 2. Renders it with the provided variables + 3. Converts the rendered text into chat messages + 4. Extracts model and optional parameters from metadata + """ + try: + # Load the prompt from BitBucket if not already loaded + if prompt_id not in self.prompt_manager.prompts: + self.prompt_manager._load_prompt_from_bitbucket(prompt_id) + + # Get the rendered prompt and metadata + rendered_prompt, prompt_metadata = self.get_prompt_template( + prompt_id, prompt_variables + ) + + # Convert rendered content to chat messages + messages = self._parse_prompt_to_messages(rendered_prompt) + + # Extract model from metadata (if specified) + template_model = prompt_metadata.get("model") + + # Extract optional parameters from metadata + optional_params = {} + for param in [ + "temperature", + "max_tokens", + "top_p", + "frequency_penalty", + "presence_penalty", + ]: + if param in prompt_metadata: + optional_params[param] = prompt_metadata[param] + + return PromptManagementClient( + prompt_id=prompt_id, + prompt_template=messages, + prompt_template_model=template_model, + prompt_template_optional_params=optional_params, + completed_messages=None, + ) + + except Exception as e: + raise ValueError(f"Error compiling prompt '{prompt_id}': {e}") + + def get_chat_completion_prompt( + self, + model: str, + messages: List[AllMessageValues], + non_default_params: dict, + prompt_id: Optional[str], + prompt_variables: Optional[dict], + dynamic_callback_params: StandardCallbackDynamicParams, + prompt_label: Optional[str] = None, + prompt_version: Optional[int] = None, + ) -> Tuple[str, List[AllMessageValues], dict]: + """ + Get chat completion prompt from BitBucket and return processed model, messages, and parameters. + """ + return PromptManagementBase.get_chat_completion_prompt( + self, + model, + messages, + non_default_params, + prompt_id, + prompt_variables, + dynamic_callback_params, + prompt_label, + prompt_version, + ) diff --git a/litellm/litellm_core_utils/custom_logger_registry.py b/litellm/litellm_core_utils/custom_logger_registry.py index bb8fa580e3c..fbf685c3700 100644 --- a/litellm/litellm_core_utils/custom_logger_registry.py +++ b/litellm/litellm_core_utils/custom_logger_registry.py @@ -39,6 +39,7 @@ try: from litellm_enterprise.integrations.prometheus import PrometheusLogger except Exception: PrometheusLogger = None +from litellm.integrations.bitbucket import BitBucketPromptManager from litellm.integrations.cloudzero.cloudzero import CloudZeroLogger from litellm.integrations.dotprompt import DotpromptManager from litellm.integrations.s3_v2 import S3Logger @@ -90,6 +91,7 @@ class CustomLoggerRegistry: "dynamic_rate_limiter_v3": _PROXY_DynamicRateLimitHandlerV3, "vector_store_pre_call_hook": VectorStorePreCallHook, "dotprompt": DotpromptManager, + "bitbucket": BitBucketPromptManager, "cloudzero": CloudZeroLogger, "posthog": PostHogLogger, } @@ -157,7 +159,6 @@ class CustomLoggerRegistry: if callback_class == class_type: callback_strs.append(callback_str) return callback_strs - @classmethod def get_class_type_for_custom_logger_name( diff --git a/litellm/litellm_core_utils/litellm_logging.py b/litellm/litellm_core_utils/litellm_logging.py index d7d82d0440e..5c22bb4ad98 100644 --- a/litellm/litellm_core_utils/litellm_logging.py +++ b/litellm/litellm_core_utils/litellm_logging.py @@ -1259,7 +1259,9 @@ class Logging(LiteLLMLoggingBaseClass): "standard_built_in_tools_params": self.standard_built_in_tools_params, "router_model_id": router_model_id, "litellm_logging_obj": self, - "service_tier": self.optional_params.get("service_tier") if self.optional_params else None, + "service_tier": self.optional_params.get("service_tier") + if self.optional_params + else None, } except Exception as e: # error creating kwargs for cost calculation debug_info = StandardLoggingModelCostFailureDebugInformation( @@ -3637,6 +3639,27 @@ def _init_custom_logger_compatible_class( # noqa: PLR0915 dotprompt_logger = DotpromptManager() _in_memory_loggers.append(dotprompt_logger) return dotprompt_logger # type: ignore + elif logging_integration == "bitbucket": + from litellm.integrations.bitbucket.bitbucket_prompt_manager import ( + BitBucketPromptManager, + ) + + for callback in _in_memory_loggers: + if isinstance(callback, BitBucketPromptManager): + return callback + + # Get global BitBucket config + import litellm + + bitbucket_config = getattr(litellm, "global_bitbucket_config", None) + if bitbucket_config is None: + raise ValueError( + "BitBucket configuration not found. Please set litellm.global_bitbucket_config first." + ) + + bitbucket_logger = BitBucketPromptManager(bitbucket_config=bitbucket_config) + _in_memory_loggers.append(bitbucket_logger) + return bitbucket_logger # type: ignore return None except Exception as e: verbose_logger.exception( @@ -4222,16 +4245,22 @@ class StandardLoggingPayloadSetup: # Get the actual s3_path from the configured cold storage logger instance s3_path = "" # default value - + # Try to get the actual logger instance from the logger name try: - custom_logger = litellm.logging_callback_manager.get_active_custom_logger_for_callback_name(configured_cold_storage_logger) - if custom_logger and hasattr(custom_logger, 's3_path') and custom_logger.s3_path: + custom_logger = litellm.logging_callback_manager.get_active_custom_logger_for_callback_name( + configured_cold_storage_logger + ) + if ( + custom_logger + and hasattr(custom_logger, "s3_path") + and custom_logger.s3_path + ): s3_path = custom_logger.s3_path except Exception: # If any error occurs in getting the logger instance, use default empty s3_path pass - + s3_object_key = get_s3_object_key( s3_path=s3_path, # Use actual s3_path from logger configuration team_alias_prefix="", # Don't split by team alias for cold storage diff --git a/litellm/proxy/proxy_server.py b/litellm/proxy/proxy_server.py index 9fff2478804..23d9acfc1e5 100644 --- a/litellm/proxy/proxy_server.py +++ b/litellm/proxy/proxy_server.py @@ -458,9 +458,9 @@ except ImportError: server_root_path = os.getenv("SERVER_ROOT_PATH", "") _license_check = LicenseCheck() premium_user: bool = _license_check.is_premium() -premium_user_data: Optional["EnterpriseLicenseData"] = ( - _license_check.airgapped_license_data -) +premium_user_data: Optional[ + "EnterpriseLicenseData" +] = _license_check.airgapped_license_data global_max_parallel_request_retries_env: Optional[str] = os.getenv( "LITELLM_GLOBAL_MAX_PARALLEL_REQUEST_RETRIES" ) @@ -954,9 +954,9 @@ model_max_budget_limiter = _PROXY_VirtualKeyModelMaxBudgetLimiter( dual_cache=user_api_key_cache ) litellm.logging_callback_manager.add_litellm_callback(model_max_budget_limiter) -redis_usage_cache: Optional[RedisCache] = ( - None # redis cache used for tracking spend, tpm/rpm limits -) +redis_usage_cache: Optional[ + RedisCache +] = None # redis cache used for tracking spend, tpm/rpm limits user_custom_auth = None user_custom_key_generate = None user_custom_sso = None @@ -1287,9 +1287,9 @@ async def update_cache( # noqa: PLR0915 _id = "team_id:{}".format(team_id) try: # Fetch the existing cost for the given user - existing_spend_obj: Optional[LiteLLM_TeamTable] = ( - await user_api_key_cache.async_get_cache(key=_id) - ) + existing_spend_obj: Optional[ + LiteLLM_TeamTable + ] = await user_api_key_cache.async_get_cache(key=_id) if existing_spend_obj is None: # do nothing if team not in api key cache return @@ -1856,6 +1856,15 @@ class ProxyConfig: verbose_proxy_logger.info( f"{blue_color_code}Set Global Prompt Directory on LiteLLM Proxy{reset_color_code}" ) + elif key == "global_bitbucket_config": + from litellm.integrations.bitbucket import ( + set_global_bitbucket_config, + ) + + set_global_bitbucket_config(value) + verbose_proxy_logger.info( + f"{blue_color_code}Set Global BitBucket Config on LiteLLM Proxy{reset_color_code}" + ) elif key == "callbacks": initialize_callbacks_on_proxy( value=value, @@ -3102,10 +3111,10 @@ class ProxyConfig: ) try: - guardrails_in_db: List[Guardrail] = ( - await GuardrailRegistry.get_all_guardrails_from_db( - prisma_client=prisma_client - ) + guardrails_in_db: List[ + Guardrail + ] = await GuardrailRegistry.get_all_guardrails_from_db( + prisma_client=prisma_client ) verbose_proxy_logger.debug( "guardrails from the DB %s", str(guardrails_in_db) @@ -3335,9 +3344,9 @@ async def initialize( # noqa: PLR0915 user_api_base = api_base dynamic_config[user_model]["api_base"] = api_base if api_version: - os.environ["AZURE_API_VERSION"] = ( - api_version # set this for azure - litellm can read this from the env - ) + os.environ[ + "AZURE_API_VERSION" + ] = api_version # set this for azure - litellm can read this from the env if max_tokens: # model-specific param dynamic_config[user_model]["max_tokens"] = max_tokens if temperature: # model-specific param @@ -6109,12 +6118,10 @@ def _add_team_models_to_all_models( team_models: Dict[str, Set[str]] = {} for team_object in team_db_objects_typed: - if ( len(team_object.models) == 0 # empty list = all model access or SpecialModelNames.all_proxy_models.value in team_object.models ): - model_list = llm_router.get_model_list() if model_list is not None: for model in model_list: @@ -6265,7 +6272,6 @@ async def get_all_team_and_direct_access_models( for _model in all_models: model_id = _model.get("model_info", {}).get("id", None) if model_id is not None and model_id in direct_access_models: - _model["model_info"]["direct_access"] = True ## FILTER OUT MODELS THAT ARE NOT IN DIRECT_ACCESS_MODELS OR ACCESS_VIA_TEAM_IDS - only show user models they can call @@ -8625,9 +8631,9 @@ async def get_config_list( hasattr(sub_field_info, "description") and sub_field_info.description is not None ): - nested_fields[idx].field_description = ( - sub_field_info.description - ) + nested_fields[ + idx + ].field_description = sub_field_info.description idx += 1 _stored_in_db = None diff --git a/litellm/router.py b/litellm/router.py index 9c87f0bb029..f093096f848 100644 --- a/litellm/router.py +++ b/litellm/router.py @@ -2003,7 +2003,14 @@ class Router: prompt_label=prompt_label, ) - kwargs = {**data, **kwargs, **optional_params} + # Filter out prompt management specific parameters from data before merging + prompt_management_params = { + "bitbucket_config", "dotprompt_config", "prompt_id", + "prompt_variables", "prompt_label", "prompt_version" + } + filtered_data = {k: v for k, v in data.items() if k not in prompt_management_params} + + kwargs = {**filtered_data, **kwargs, **optional_params} kwargs["model"] = model kwargs["messages"] = messages kwargs["litellm_logging_obj"] = litellm_logging_object @@ -5124,21 +5131,38 @@ class Router: import os #### VALIDATE MODEL ######## - # check if model provider in supported providers - ( - _model, - custom_llm_provider, - dynamic_api_key, - api_base, - ) = litellm.get_llm_provider( - model=deployment.litellm_params.model, - custom_llm_provider=deployment.litellm_params.get( - "custom_llm_provider", None - ), - ) - # done reading model["litellm_params"] - if custom_llm_provider not in litellm.provider_list: - raise Exception(f"Unsupported provider - {custom_llm_provider}") + # Check if this is a prompt management model before validating as LLM provider + litellm_model = deployment.litellm_params.model + is_prompt_management_model = False + + if "/" in litellm_model: + split_litellm_model = litellm_model.split("/")[0] + if split_litellm_model in litellm._known_custom_logger_compatible_callbacks: + is_prompt_management_model = True + + if is_prompt_management_model: + # For prompt management models, skip LLM provider validation + # The actual model will be resolved at runtime from the prompt file + _model = litellm_model + custom_llm_provider = None + dynamic_api_key = None + api_base = None + else: + # check if model provider in supported providers + ( + _model, + custom_llm_provider, + dynamic_api_key, + api_base, + ) = litellm.get_llm_provider( + model=deployment.litellm_params.model, + custom_llm_provider=deployment.litellm_params.get( + "custom_llm_provider", None + ), + ) + # done reading model["litellm_params"] + if custom_llm_provider not in litellm.provider_list: + raise Exception(f"Unsupported provider - {custom_llm_provider}") #### DEPLOYMENT NAMES INIT ######## self.deployment_names.append(deployment.litellm_params.model) diff --git a/litellm/types/prompts/init_prompts.py b/litellm/types/prompts/init_prompts.py index c168bf31289..3c48cd131c3 100644 --- a/litellm/types/prompts/init_prompts.py +++ b/litellm/types/prompts/init_prompts.py @@ -3,13 +3,13 @@ from enum import Enum from typing import Any, Dict, List, Literal, Optional from pydantic import BaseModel, ConfigDict -from typing_extensions import Required, TypedDict class SupportedPromptIntegrations(str, Enum): DOT_PROMPT = "dotprompt" LANGFUSE = "langfuse" CUSTOM = "custom" + BITBUCKET = "bitbucket" class PromptInfo(BaseModel): @@ -55,6 +55,5 @@ class PromptInfoResponse(BaseModel): raw_prompt_template: Optional[PromptTemplateBase] = None - class ListPromptsResponse(BaseModel): prompts: List[PromptSpec] diff --git a/tests/test_litellm/integrations/bitbucket/test_bitbucket_integration.py b/tests/test_litellm/integrations/bitbucket/test_bitbucket_integration.py new file mode 100644 index 00000000000..254c468d766 --- /dev/null +++ b/tests/test_litellm/integrations/bitbucket/test_bitbucket_integration.py @@ -0,0 +1,366 @@ +import json +import os +import sys +from unittest.mock import MagicMock, patch + +import pytest + +sys.path.insert( + 0, os.path.abspath("../../..") +) # Adds the parent directory to the system path + +import litellm +from litellm.integrations.bitbucket import BitBucketPromptManager + + +@patch("litellm.integrations.bitbucket.bitbucket_prompt_manager.BitBucketClient") +def test_bitbucket_prompt_integration_with_litellm(mock_client_class): + """Test BitBucket prompt integration with LiteLLM completion.""" + # Mock the BitBucket client + mock_client = MagicMock() + mock_client.get_file_content.return_value = """--- +model: gpt-4 +temperature: 0.7 +max_tokens: 150 +--- +System: You are a helpful assistant. + +User: {{user_message}}""" + mock_client_class.return_value = mock_client + + # Configure BitBucket + bitbucket_config = { + "workspace": "test-workspace", + "repository": "test-repo", + "access_token": "test-token", + } + + # Set global BitBucket configuration + litellm.set_global_bitbucket_config(bitbucket_config) + + # Test that the configuration was set + assert litellm.global_bitbucket_config == bitbucket_config + + +@patch("litellm.integrations.bitbucket.bitbucket_prompt_manager.BitBucketClient") +def test_bitbucket_prompt_manager_initialization(mock_client_class): + """Test BitBucketPromptManager initialization.""" + # Mock the BitBucket client + mock_client = MagicMock() + mock_client.get_file_content.return_value = """--- +model: gpt-4 +temperature: 0.7 +--- +Hello {{name}}!""" + mock_client_class.return_value = mock_client + + config = { + "workspace": "test-workspace", + "repository": "test-repo", + "access_token": "test-token", + } + + manager = BitBucketPromptManager(config, prompt_id="test_prompt") + + # Should have loaded the prompt + assert "test_prompt" in manager.prompt_manager.prompts + template = manager.prompt_manager.prompts["test_prompt"] + assert template.model == "gpt-4" + assert template.temperature == 0.7 + + # Test rendering + rendered = manager.prompt_manager.render_template("test_prompt", {"name": "World"}) + assert rendered == "Hello World!" + + +@patch("litellm.integrations.bitbucket.bitbucket_prompt_manager.BitBucketClient") +def test_bitbucket_prompt_manager_error_handling(mock_client_class): + """Test BitBucketPromptManager error handling.""" + # Mock the BitBucket client to raise an error + mock_client = MagicMock() + mock_client.get_file_content.side_effect = Exception("BitBucket API error") + mock_client_class.return_value = mock_client + + config = { + "workspace": "test-workspace", + "repository": "test-repo", + "access_token": "test-token", + } + + with pytest.raises(Exception, match="Failed to load prompt 'test_prompt' from BitBucket"): + manager = BitBucketPromptManager(config, prompt_id="test_prompt") + _ = manager.prompt_manager # This triggers the error + + +def test_bitbucket_prompt_manager_config_validation(): + """Test BitBucketPromptManager configuration validation.""" + # Test missing required fields - validation happens when prompt_manager is accessed + with pytest.raises(ValueError, match="workspace, repository, and access_token are required"): + manager = BitBucketPromptManager({}) + _ = manager.prompt_manager # This triggers validation + + with pytest.raises(ValueError, match="workspace, repository, and access_token are required"): + manager = BitBucketPromptManager({"workspace": "test"}) + _ = manager.prompt_manager # This triggers validation + + with pytest.raises(ValueError, match="workspace, repository, and access_token are required"): + manager = BitBucketPromptManager({"repository": "test"}) + _ = manager.prompt_manager # This triggers validation + + with pytest.raises(ValueError, match="workspace, repository, and access_token are required"): + manager = BitBucketPromptManager({"access_token": "test"}) + _ = manager.prompt_manager # This triggers validation + + +@patch("litellm.integrations.bitbucket.bitbucket_prompt_manager.BitBucketClient") +def test_bitbucket_prompt_manager_complex_prompt(mock_client_class): + """Test BitBucketPromptManager with complex prompt structure.""" + # Mock the BitBucket client + mock_client = MagicMock() + mock_client.get_file_content.return_value = """--- +model: gpt-4 +temperature: 0.3 +max_tokens: 500 +input: + schema: + user_question: string + context?: string + language: string +--- +System: You are a helpful {{language}} programming assistant. + +{% if context %}Context: {{context}} + +{% endif %}User: {{user_question}} + +Please provide a detailed response in {{language}}.""" + mock_client_class.return_value = mock_client + + config = { + "workspace": "test-workspace", + "repository": "test-repo", + "access_token": "test-token", + } + + manager = BitBucketPromptManager(config, prompt_id="complex_prompt") + + # Should have loaded the prompt + assert "complex_prompt" in manager.prompt_manager.prompts + template = manager.prompt_manager.prompts["complex_prompt"] + assert template.model == "gpt-4" + assert template.temperature == 0.3 + assert template.max_tokens == 500 + assert template.input_schema == { + "user_question": "string", + "context?": "string", + "language": "string" + } + + # Test rendering with all variables + rendered = manager.prompt_manager.render_template( + "complex_prompt", + { + "user_question": "How do I create a class?", + "context": "Python programming", + "language": "Python" + } + ) + + assert "You are a helpful Python programming assistant." in rendered + assert "Context: Python programming" in rendered + assert "How do I create a class?" in rendered + assert "Please provide a detailed response in Python." in rendered + + # Test rendering without optional context + rendered_no_context = manager.prompt_manager.render_template( + "complex_prompt", + { + "user_question": "What is inheritance?", + "language": "Java" + } + ) + + assert "You are a helpful Java programming assistant." in rendered_no_context + assert "Context:" not in rendered_no_context + assert "What is inheritance?" in rendered_no_context + + +@patch("litellm.integrations.bitbucket.bitbucket_prompt_manager.BitBucketClient") +def test_bitbucket_prompt_manager_message_parsing(mock_client_class): + """Test BitBucketPromptManager message parsing for different prompt formats.""" + # Mock the BitBucket client + mock_client = MagicMock() + mock_client.get_file_content.return_value = """--- +model: gpt-4 +--- +System: You are a helpful assistant. + +User: {{user_message}} + +Assistant: I'll help you with that.""" + mock_client_class.return_value = mock_client + + config = { + "workspace": "test-workspace", + "repository": "test-repo", + "access_token": "test-token", + } + + manager = BitBucketPromptManager(config, prompt_id="conversation_prompt") + + # Test message parsing + messages = manager._parse_prompt_to_messages( + "System: You are a helpful assistant.\n\nUser: Hello!\n\nAssistant: Hi there!" + ) + + assert len(messages) == 3 + assert messages[0]["role"] == "system" + assert messages[0]["content"] == "You are a helpful assistant." + assert messages[1]["role"] == "user" + assert messages[1]["content"] == "Hello!" + assert messages[2]["role"] == "assistant" + assert messages[2]["content"] == "Hi there!" + + +@patch("litellm.integrations.bitbucket.bitbucket_prompt_manager.BitBucketClient") +def test_bitbucket_prompt_manager_pre_call_hook_integration(mock_client_class): + """Test BitBucketPromptManager pre_call_hook integration.""" + # Mock the BitBucket client + mock_client = MagicMock() + mock_client.get_file_content.return_value = """--- +model: gpt-4 +temperature: 0.8 +max_tokens: 200 +--- +System: You are a helpful assistant. + +User: {{user_message}}""" + mock_client_class.return_value = mock_client + + config = { + "workspace": "test-workspace", + "repository": "test-repo", + "access_token": "test-token", + } + + manager = BitBucketPromptManager(config, prompt_id="test_prompt") + + # Test pre_call_hook + original_messages = [{"role": "user", "content": "This will be ignored"}] + litellm_params = {"api_key": "test-key"} + + result_messages, result_params = manager.pre_call_hook( + user_id="test_user", + messages=original_messages, + litellm_params=litellm_params, + prompt_id="test_prompt", + prompt_variables={"user_message": "What is AI?"} + ) + + # Should have parsed the prompt into messages + assert len(result_messages) == 2 + assert result_messages[0]["role"] == "system" + assert result_messages[0]["content"] == "You are a helpful assistant." + assert result_messages[1]["role"] == "user" + assert result_messages[1]["content"] == "What is AI?" + + # Should have updated litellm_params + assert result_params["model"] == "gpt-4" + assert result_params["temperature"] == 0.8 + assert result_params["max_tokens"] == 200 + assert result_params["api_key"] == "test-key" # Original params preserved + + +@patch("litellm.integrations.bitbucket.bitbucket_prompt_manager.BitBucketClient") +def test_bitbucket_prompt_manager_post_call_hook(mock_client_class): + """Test BitBucketPromptManager post_call_hook.""" + # Mock the BitBucket client + mock_client = MagicMock() + mock_client.get_file_content.return_value = "Simple prompt: {{message}}" + mock_client_class.return_value = mock_client + + config = { + "workspace": "test-workspace", + "repository": "test-repo", + "access_token": "test-token", + } + + manager = BitBucketPromptManager(config, prompt_id="test_prompt") + + # Mock response + mock_response = MagicMock() + mock_response.choices = [MagicMock()] + mock_response.choices[0].message = MagicMock() + mock_response.choices[0].message.content = "Test response" + + # Test post_call_hook + result = manager.post_call_hook( + user_id="test_user", + response=mock_response, + input_messages=[{"role": "user", "content": "test"}], + litellm_params={}, + prompt_id="test_prompt" + ) + + # Should return the response unchanged + assert result == mock_response + + +def test_bitbucket_prompt_manager_integration_name(): + """Test BitBucketPromptManager integration name.""" + config = { + "workspace": "test-workspace", + "repository": "test-repo", + "access_token": "test-token", + } + + manager = BitBucketPromptManager(config) + assert manager.integration_name == "bitbucket" + + +@patch("litellm.integrations.bitbucket.bitbucket_prompt_manager.BitBucketClient") +def test_bitbucket_prompt_manager_get_template(mock_client_class): + """Test BitBucketPromptManager get_template method.""" + # Mock the BitBucket client + mock_client = MagicMock() + mock_client.get_file_content.return_value = "Test content" + mock_client_class.return_value = mock_client + + config = { + "workspace": "test-workspace", + "repository": "test-repo", + "access_token": "test-token", + } + + manager = BitBucketPromptManager(config, prompt_id="test_prompt") + + # Test getting existing template + template = manager.prompt_manager.get_template("test_prompt") + assert template is not None + assert template.template_id == "test_prompt" + + # Test getting non-existing template + template = manager.prompt_manager.get_template("nonexistent") + assert template is None + + +@patch("litellm.integrations.bitbucket.bitbucket_prompt_manager.BitBucketClient") +def test_bitbucket_prompt_manager_list_templates(mock_client_class): + """Test BitBucketPromptManager list_templates method.""" + # Mock the BitBucket client + mock_client = MagicMock() + mock_client.get_file_content.return_value = "Test content" + mock_client_class.return_value = mock_client + + config = { + "workspace": "test-workspace", + "repository": "test-repo", + "access_token": "test-token", + } + + manager = BitBucketPromptManager(config, prompt_id="test_prompt") + + # Test listing templates + templates = manager.prompt_manager.list_templates() + assert isinstance(templates, list) + assert "test_prompt" in templates diff --git a/tests/test_litellm/integrations/bitbucket/test_bitbucket_prompt_manager.py b/tests/test_litellm/integrations/bitbucket/test_bitbucket_prompt_manager.py new file mode 100644 index 00000000000..75d7a94c5e4 --- /dev/null +++ b/tests/test_litellm/integrations/bitbucket/test_bitbucket_prompt_manager.py @@ -0,0 +1,517 @@ +import json +import os +import sys +from unittest.mock import MagicMock, patch + +import pytest + +sys.path.insert( + 0, os.path.abspath("../../..") +) # Adds the parent directory to the system path + +from litellm.integrations.bitbucket.bitbucket_client import BitBucketClient +from litellm.integrations.bitbucket.bitbucket_prompt_manager import ( + BitBucketPromptManager, + BitBucketPromptTemplate, +) + + +def test_bitbucket_prompt_template_creation(): + """Test BitBucketPromptTemplate creation and metadata extraction.""" + metadata = { + "model": "gpt-4", + "temperature": 0.7, + "input": {"schema": {"text": "string"}}, + "output": {"format": "json"}, + } + + template = BitBucketPromptTemplate( + template_id="test_template", + content="Hello {{name}}!", + metadata=metadata, + ) + + assert template.template_id == "test_template" + assert template.content == "Hello {{name}}!" + assert template.model == "gpt-4" + assert template.optional_params["temperature"] == 0.7 + assert template.input_schema == {"text": "string"} + + +def test_bitbucket_client_initialization(): + """Test BitBucketClient initialization with different auth methods.""" + # Test token-based auth + config_token = { + "workspace": "test-workspace", + "repository": "test-repo", + "access_token": "test-token", + "branch": "main", + } + + client = BitBucketClient(config_token) + assert client.workspace == "test-workspace" + assert client.repository == "test-repo" + assert client.access_token == "test-token" + assert client.branch == "main" + assert client.auth_method == "token" + + # Test basic auth + config_basic = { + "workspace": "test-workspace", + "repository": "test-repo", + "access_token": "test-password", + "username": "test-user", + "auth_method": "basic", + } + + client_basic = BitBucketClient(config_basic) + assert client_basic.auth_method == "basic" + assert client_basic.username == "test-user" + + +def test_bitbucket_client_missing_required_fields(): + """Test BitBucketClient initialization with missing required fields.""" + with pytest.raises(ValueError, match="workspace, repository, and access_token are required"): + BitBucketClient({"workspace": "test"}) + + with pytest.raises(ValueError, match="workspace, repository, and access_token are required"): + BitBucketClient({"repository": "test"}) + + with pytest.raises(ValueError, match="workspace, repository, and access_token are required"): + BitBucketClient({"access_token": "test"}) + + +@patch("litellm.llms.custom_httpx.http_handler.HTTPHandler.get") +def test_bitbucket_client_get_file_content_success(mock_get): + """Test successful file content retrieval from BitBucket.""" + # Mock successful response + mock_response = MagicMock() + mock_response.text = "file content" + mock_response.headers = {"content-type": "text/plain"} + mock_response.raise_for_status.return_value = None + mock_get.return_value = mock_response + + config = { + "workspace": "test-workspace", + "repository": "test-repo", + "access_token": "test-token", + } + + client = BitBucketClient(config) + content = client.get_file_content("test.prompt") + + assert content == "file content" + mock_get.assert_called_once() + + +@patch("litellm.llms.custom_httpx.http_handler.HTTPHandler.get") +def test_bitbucket_client_get_file_content_not_found(mock_get): + """Test file content retrieval when file doesn't exist.""" + # Mock 404 response + import httpx + mock_response = MagicMock() + mock_response.raise_for_status.side_effect = httpx.HTTPStatusError("404 Not Found", request=MagicMock(), response=mock_response) + mock_response.status_code = 404 + mock_response.response = mock_response + mock_get.return_value = mock_response + + config = { + "workspace": "test-workspace", + "repository": "test-repo", + "access_token": "test-token", + } + + client = BitBucketClient(config) + content = client.get_file_content("nonexistent.prompt") + + assert content is None + + +@patch("litellm.llms.custom_httpx.http_handler.HTTPHandler.get") +def test_bitbucket_client_get_file_content_access_denied(mock_get): + """Test file content retrieval with access denied error.""" + # Mock 403 response + import httpx + mock_response = MagicMock() + mock_response.raise_for_status.side_effect = httpx.HTTPStatusError("403 Forbidden", request=MagicMock(), response=mock_response) + mock_response.status_code = 403 + mock_response.response = mock_response + mock_get.return_value = mock_response + + config = { + "workspace": "test-workspace", + "repository": "test-repo", + "access_token": "test-token", + } + + client = BitBucketClient(config) + + with pytest.raises(Exception, match="Access denied to file 'test.prompt'"): + client.get_file_content("test.prompt") + + +@patch("litellm.llms.custom_httpx.http_handler.HTTPHandler.get") +def test_bitbucket_client_get_file_content_auth_failed(mock_get): + """Test file content retrieval with authentication failure.""" + # Mock 401 response + import httpx + mock_response = MagicMock() + mock_response.raise_for_status.side_effect = httpx.HTTPStatusError("401 Unauthorized", request=MagicMock(), response=mock_response) + mock_response.status_code = 401 + mock_response.response = mock_response + mock_get.return_value = mock_response + + config = { + "workspace": "test-workspace", + "repository": "test-repo", + "access_token": "test-token", + } + + client = BitBucketClient(config) + + with pytest.raises(Exception, match="Authentication failed"): + client.get_file_content("test.prompt") + + +@patch("litellm.llms.custom_httpx.http_handler.HTTPHandler.get") +def test_bitbucket_client_list_files_success(mock_get): + """Test successful file listing from BitBucket.""" + # Mock successful response + mock_response = MagicMock() + mock_response.json.return_value = { + "values": [ + {"type": "commit_file", "path": "prompts/test1.prompt"}, + {"type": "commit_file", "path": "prompts/test2.prompt"}, + {"type": "commit_file", "path": "prompts/other.txt"}, + ] + } + mock_response.raise_for_status.return_value = None + mock_get.return_value = mock_response + + config = { + "workspace": "test-workspace", + "repository": "test-repo", + "access_token": "test-token", + } + + client = BitBucketClient(config) + files = client.list_files("prompts", ".prompt") + + assert len(files) == 2 + assert "prompts/test1.prompt" in files + assert "prompts/test2.prompt" in files + + +def test_bitbucket_prompt_manager_parse_prompt_file(): + """Test parsing .prompt file content with YAML frontmatter.""" + prompt_content = """--- +model: gpt-4 +temperature: 0.7 +max_tokens: 150 +input: + schema: + user_message: string + system_context?: string +--- + +{% if system_context %}System: {{system_context}} + +{% endif %}User: {{user_message}}""" + + config = { + "workspace": "test-workspace", + "repository": "test-repo", + "access_token": "test-token", + } + + manager = BitBucketPromptManager(config) + template = manager.prompt_manager._parse_prompt_file(prompt_content, "test_prompt") + + assert template.template_id == "test_prompt" + assert template.model == "gpt-4" + assert template.temperature == 0.7 + assert template.max_tokens == 150 + assert template.input_schema == {"user_message": "string", "system_context?": "string"} + assert "{% if system_context %}" in template.content + + +def test_bitbucket_prompt_manager_parse_prompt_file_no_frontmatter(): + """Test parsing .prompt file content without YAML frontmatter.""" + prompt_content = "Simple prompt: {{message}}" + + config = { + "workspace": "test-workspace", + "repository": "test-repo", + "access_token": "test-token", + } + + manager = BitBucketPromptManager(config) + template = manager.prompt_manager._parse_prompt_file(prompt_content, "simple_prompt") + + assert template.template_id == "simple_prompt" + assert template.content == "Simple prompt: {{message}}" + assert template.metadata == {} + + +def test_bitbucket_prompt_manager_render_template(): + """Test template rendering with variables.""" + config = { + "workspace": "test-workspace", + "repository": "test-repo", + "access_token": "test-token", + } + + manager = BitBucketPromptManager(config) + + # Add a test template + template = BitBucketPromptTemplate( + template_id="test_template", + content="Hello {{name}}! Welcome to {{place}}.", + metadata={"model": "gpt-4"}, + ) + manager.prompt_manager.prompts["test_template"] = template + + rendered = manager.prompt_manager.render_template("test_template", {"name": "World", "place": "Earth"}) + assert rendered == "Hello World! Welcome to Earth." + + +def test_bitbucket_prompt_manager_render_template_not_found(): + """Test template rendering when template doesn't exist.""" + config = { + "workspace": "test-workspace", + "repository": "test-repo", + "access_token": "test-token", + } + + manager = BitBucketPromptManager(config) + + with pytest.raises(ValueError, match="Template 'nonexistent' not found"): + manager.prompt_manager.render_template("nonexistent", {"some": "variable"}) + + +@patch("litellm.integrations.bitbucket.bitbucket_prompt_manager.BitBucketClient") +def test_bitbucket_prompt_manager_integration(mock_client_class): + """Test BitBucketPromptManager integration with BitBucketClient.""" + # Mock the BitBucket client + mock_client = MagicMock() + mock_client.get_file_content.return_value = """--- +model: gpt-4 +temperature: 0.7 +--- +Hello {{name}}!""" + mock_client_class.return_value = mock_client + + config = { + "workspace": "test-workspace", + "repository": "test-repo", + "access_token": "test-token", + } + + manager = BitBucketPromptManager(config, prompt_id="test_prompt") + + # Should have loaded the prompt + assert "test_prompt" in manager.prompt_manager.prompts + template = manager.prompt_manager.prompts["test_prompt"] + assert template.model == "gpt-4" + assert template.temperature == 0.7 + + # Test rendering + rendered = manager.prompt_manager.render_template("test_prompt", {"name": "World"}) + assert rendered == "Hello World!" + + +def test_bitbucket_prompt_manager_parse_prompt_to_messages(): + """Test parsing prompt content into messages.""" + config = { + "workspace": "test-workspace", + "repository": "test-repo", + "access_token": "test-token", + } + + manager = BitBucketPromptManager(config) + + # Test simple user message + simple_prompt = "Hello, how can I help you?" + messages = manager._parse_prompt_to_messages(simple_prompt) + assert len(messages) == 1 + assert messages[0]["role"] == "user" + assert messages[0]["content"] == "Hello, how can I help you?" + + # Test multi-role conversation + multi_role_prompt = """System: You are a helpful assistant. + +User: What is the capital of France? + +Assistant: The capital of France is Paris.""" + + messages = manager._parse_prompt_to_messages(multi_role_prompt) + assert len(messages) == 3 + assert messages[0]["role"] == "system" + assert messages[0]["content"] == "You are a helpful assistant." + assert messages[1]["role"] == "user" + assert messages[1]["content"] == "What is the capital of France?" + assert messages[2]["role"] == "assistant" + assert messages[2]["content"] == "The capital of France is Paris." + + +def test_bitbucket_prompt_manager_pre_call_hook(): + """Test the pre_call_hook method.""" + config = { + "workspace": "test-workspace", + "repository": "test-repo", + "access_token": "test-token", + } + + manager = BitBucketPromptManager(config) + + # Add a test template + template = BitBucketPromptTemplate( + template_id="test_prompt", + content="System: You are a helpful assistant.\n\nUser: {{user_message}}", + metadata={"model": "gpt-4", "temperature": 0.7}, + ) + manager.prompt_manager.prompts["test_prompt"] = template + + # Test pre_call_hook + messages = [{"role": "user", "content": "This will be ignored"}] + litellm_params = {} + + result_messages, result_params = manager.pre_call_hook( + user_id="test_user", + messages=messages, + litellm_params=litellm_params, + prompt_id="test_prompt", + prompt_variables={"user_message": "Hello!"} + ) + + # Should have parsed the prompt into messages + assert len(result_messages) == 2 + assert result_messages[0]["role"] == "system" + assert result_messages[0]["content"] == "You are a helpful assistant." + assert result_messages[1]["role"] == "user" + assert result_messages[1]["content"] == "Hello!" + + # Should have updated litellm_params + assert result_params["model"] == "gpt-4" + assert result_params["temperature"] == 0.7 + + +def test_bitbucket_prompt_manager_pre_call_hook_no_prompt_id(): + """Test pre_call_hook when no prompt_id is provided.""" + config = { + "workspace": "test-workspace", + "repository": "test-repo", + "access_token": "test-token", + } + + manager = BitBucketPromptManager(config) + + messages = [{"role": "user", "content": "Hello"}] + litellm_params = {} + + result_messages, result_params = manager.pre_call_hook( + user_id="test_user", + messages=messages, + litellm_params=litellm_params, + prompt_id=None, + ) + + # Should return original messages and params unchanged + assert result_messages == messages + assert result_params == litellm_params + + +def test_bitbucket_prompt_manager_get_available_prompts(): + """Test getting list of available prompts.""" + config = { + "workspace": "test-workspace", + "repository": "test-repo", + "access_token": "test-token", + } + + manager = BitBucketPromptManager(config) + + # Add some test templates + template1 = BitBucketPromptTemplate("prompt1", "content1", {}) + template2 = BitBucketPromptTemplate("prompt2", "content2", {}) + manager.prompt_manager.prompts["prompt1"] = template1 + manager.prompt_manager.prompts["prompt2"] = template2 + + available_prompts = manager.get_available_prompts() + assert set(available_prompts) == {"prompt1", "prompt2"} + + +@patch("litellm.integrations.bitbucket.bitbucket_prompt_manager.BitBucketClient") +def test_bitbucket_prompt_manager_reload_prompts(mock_client_class): + """Test reloading prompts from BitBucket.""" + # Mock the BitBucket client + mock_client = MagicMock() + mock_client.get_file_content.return_value = """--- +model: gpt-4 +temperature: 0.7 +--- +Hello {{name}}!""" + mock_client_class.return_value = mock_client + + config = { + "workspace": "test-workspace", + "repository": "test-repo", + "access_token": "test-token", + } + + manager = BitBucketPromptManager(config, prompt_id="test_prompt") + + # Mock the prompt manager to test reload + with patch.object(manager, '_prompt_manager', None): + manager.reload_prompts() + # Should trigger reload by accessing prompt_manager property + _ = manager.prompt_manager + + +def test_bitbucket_prompt_manager_yaml_parsing_fallback(): + """Test YAML parsing fallback when PyYAML is not available.""" + config = { + "workspace": "test-workspace", + "repository": "test-repo", + "access_token": "test-token", + } + + manager = BitBucketPromptManager(config) + + # Test basic YAML parsing fallback + yaml_content = """model: gpt-4 +temperature: 0.7 +max_tokens: 150""" + + parsed = manager.prompt_manager._parse_yaml_basic(yaml_content) + assert parsed["model"] == "gpt-4" + assert parsed["temperature"] == 0.7 + assert parsed["max_tokens"] == 150 + + +def test_bitbucket_prompt_manager_yaml_parsing_with_types(): + """Test YAML parsing with different data types.""" + config = { + "workspace": "test-workspace", + "repository": "test-repo", + "access_token": "test-token", + } + + manager = BitBucketPromptManager(config) + + yaml_content = """model: gpt-4 +temperature: 0.7 +max_tokens: 150 +enabled: true +disabled: false +count: 42 +rate: 0.5""" + + parsed = manager.prompt_manager._parse_yaml_basic(yaml_content) + assert parsed["model"] == "gpt-4" + assert parsed["temperature"] == 0.7 + assert parsed["max_tokens"] == 150 + assert parsed["enabled"] is True + assert parsed["disabled"] is False + assert parsed["count"] == 42 + assert parsed["rate"] == 0.5 From dcfe6dd3d9fc005a45a35e25654cb1f5a37f74d7 Mon Sep 17 00:00:00 2001 From: Ishaan Jaffer Date: Thu, 25 Sep 2025 16:07:02 -0700 Subject: [PATCH 55/70] linting fix --- litellm/litellm_core_utils/litellm_logging.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/litellm/litellm_core_utils/litellm_logging.py b/litellm/litellm_core_utils/litellm_logging.py index 5c22bb4ad98..ea45f45d1c9 100644 --- a/litellm/litellm_core_utils/litellm_logging.py +++ b/litellm/litellm_core_utils/litellm_logging.py @@ -3649,8 +3649,6 @@ def _init_custom_logger_compatible_class( # noqa: PLR0915 return callback # Get global BitBucket config - import litellm - bitbucket_config = getattr(litellm, "global_bitbucket_config", None) if bitbucket_config is None: raise ValueError( From 0e8f60fe67e6742a01487d9105b18062e339fff0 Mon Sep 17 00:00:00 2001 From: Teddy Amkie Date: Thu, 25 Sep 2025 16:09:41 -0700 Subject: [PATCH 56/70] Remove aiohttp_ prefix from config (#14920) * Fix: Correct model name in load test advanced docs Co-authored-by: teddy * Fix: Update load testing provider to openai/fake Co-authored-by: teddy --------- Co-authored-by: Cursor Agent --- docs/my-website/docs/load_test_advanced.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/my-website/docs/load_test_advanced.md b/docs/my-website/docs/load_test_advanced.md index 0b3d38f3fcc..3171bc33594 100644 --- a/docs/my-website/docs/load_test_advanced.md +++ b/docs/my-website/docs/load_test_advanced.md @@ -27,13 +27,13 @@ Tutorial on how to get to 1K+ RPS with LiteLLM Proxy on locust **Use this config for testing:** -**Note:** we're currently migrating to aiohttp which has 10x higher throughput. We recommend using the `aiohttp_openai/` provider for load testing. +**Note:** we're currently migrating to aiohttp which has 10x higher throughput. We recommend using the `openai/` provider for load testing. ```yaml model_list: - model_name: "fake-openai-endpoint" litellm_params: - model: aiohttp_openai/any + model: openai/any api_base: https://your-fake-openai-endpoint.com/chat/completions api_key: "test" ``` @@ -58,7 +58,7 @@ litellm provides a hosted `fake-openai-endpoint` you can load test against model_list: - model_name: fake-openai-endpoint litellm_params: - model: aiohttp_openai/fake + model: openai/fake api_key: fake-key api_base: https://exampleopenaiendpoint-production.up.railway.app/ From fb9e2c93a0f054717eed76bb57989c8cea93e315 Mon Sep 17 00:00:00 2001 From: Vishnu Patibandla <66695281+vpbill@users.noreply.github.com> Date: Thu, 25 Sep 2025 21:53:15 -0700 Subject: [PATCH 57/70] add noma guardrail provider to ui (#14415) * add noma guardrail provider to ui * resolved linting issues * fix return value --- .../guardrails/guardrail_hooks/noma/noma.py | 14 ++++++++++- .../proxy/guardrails/guardrail_hooks/noma.py | 23 +++++++++++++++++++ 2 files changed, 36 insertions(+), 1 deletion(-) create mode 100644 litellm/types/proxy/guardrails/guardrail_hooks/noma.py diff --git a/litellm/proxy/guardrails/guardrail_hooks/noma/noma.py b/litellm/proxy/guardrails/guardrail_hooks/noma/noma.py index 22dffe8b069..06d0af681a0 100644 --- a/litellm/proxy/guardrails/guardrail_hooks/noma/noma.py +++ b/litellm/proxy/guardrails/guardrail_hooks/noma/noma.py @@ -8,7 +8,7 @@ import asyncio import copy import os -from typing import Any, Dict, Final, Literal, Optional, Union +from typing import Any, Dict, Final, Literal, Optional, Union, Type, TYPE_CHECKING from urllib.parse import urljoin from fastapi import HTTPException @@ -34,7 +34,10 @@ SENSITIVE_DATA_DETECTOR_KEYS: Final[list[str]] = ["sensitiveData", "dataDetector MessageRole = Literal["user", "assistant"] LLMResponse = Union[Any, ModelResponse, EmbeddingResponse, ImageResponse] +if TYPE_CHECKING: + from litellm.types.proxy.guardrails.guardrail_hooks.base import GuardrailConfigModel + class NomaBlockedMessage(HTTPException): """Exception raised when Noma guardrail blocks a message""" @@ -730,3 +733,12 @@ class NomaGuardrail(CustomGuardrail): verbose_proxy_logger.info(msg) else: verbose_proxy_logger.debug(msg) + + @staticmethod + def get_config_model() -> Optional[Type["GuardrailConfigModel"]]: + from litellm.types.proxy.guardrails.guardrail_hooks.noma import ( + NomaGuardrailConfigModel, + ) + + return NomaGuardrailConfigModel + diff --git a/litellm/types/proxy/guardrails/guardrail_hooks/noma.py b/litellm/types/proxy/guardrails/guardrail_hooks/noma.py new file mode 100644 index 00000000000..2d6d4a45124 --- /dev/null +++ b/litellm/types/proxy/guardrails/guardrail_hooks/noma.py @@ -0,0 +1,23 @@ +from typing import Optional + +from pydantic import BaseModel, Field + +from .base import GuardrailConfigModel + +class NomaGuardrailConfigModel(GuardrailConfigModel): + api_key: Optional[str] = Field( + default=None, + description="The Noma API key. Reads from NOMA_API_KEY env var if None.", + ) + api_base: Optional[str] = Field( + default=None, + description="The Noma API base URL. Defaults to https://api.noma.security. Also checks if the NOMA_API_KEY env var is set.", + ) + application_id: Optional[str] = Field( + default=None, + description="The Noma Application ID. Reads from NOMA_APPLICATION_ID env var if None.", + ) + + @staticmethod + def ui_friendly_name() -> str: + return "Noma Security" From 625ed3f8cfd6288df7661682e110ed0a9ffb856b Mon Sep 17 00:00:00 2001 From: Mubashir Osmani Date: Fri, 26 Sep 2025 00:54:00 -0400 Subject: [PATCH 58/70] fix: prisma client state retries (#14925) * added qwen models and gpt-5-codex * fix flaky test * fix failing test * Added retries to prisma client state * fix: prisma client state retries in pods * Revert "fix failing test" This reverts commit dbec4988a2627257fd05b905e216225664517f32. * Revert "fix flaky test" This reverts commit b0ac2f2dc35ca433af0c82f3cda770d6981caff4. * Revert "added qwen models and gpt-5-codex" This reverts commit 9a8a8f2d47ab4dc8aecb0cd9a6a4f82ed81bb056. * Revert "fix: prisma client state retries in pods" This reverts commit 04e58e5ca1a489916e3b49e9b674f5c6713fd7cd. * fix lint * Revert "fix lint" This reverts commit 5303d52a5e3bee7e131dcabd098e94f0613a7bb9. * fixed lint --- litellm/proxy/utils.py | 30 +- .../test_prisma_client_backoff_retry.py | 374 ++++++++++++++++++ 2 files changed, 403 insertions(+), 1 deletion(-) create mode 100644 tests/proxy_unit_tests/test_prisma_client_backoff_retry.py diff --git a/litellm/proxy/utils.py b/litellm/proxy/utils.py index 036b3174b63..5b11c25b2bf 100644 --- a/litellm/proxy/utils.py +++ b/litellm/proxy/utils.py @@ -2817,6 +2817,13 @@ class PrismaClient: ) raise e + @backoff.on_exception( + backoff.expo, + Exception, + max_tries=3, + max_time=10, + on_backoff=on_backoff, + ) async def health_check(self): """ Health check endpoint for the prisma client @@ -2848,7 +2855,18 @@ class PrismaClient: raise e async def _get_spend_logs_row_count(self) -> int: - try: + """ + Get the row count from LiteLLM_SpendLogs table using PostgreSQL system statistics. + """ + + @backoff.on_exception( + backoff.expo, + Exception, + max_tries=3, + max_time=10, + on_backoff=on_backoff, + ) + async def _fetch_row_count() -> int: sql_query = """ SELECT reltuples::BIGINT FROM pg_class @@ -2856,12 +2874,22 @@ class PrismaClient: """ result = await self.db.query_raw(query=sql_query) return result[0]["reltuples"] + + try: + return await _fetch_row_count() except Exception as e: verbose_proxy_logger.error( f"Error getting LiteLLM_SpendLogs row count: {e}" ) return 0 + @backoff.on_exception( + backoff.expo, + Exception, + max_tries=3, + max_time=10, + on_backoff=on_backoff, + ) async def _set_spend_logs_row_count_in_proxy_state(self) -> None: """ Set the `LiteLLM_SpendLogs`row count in proxy state. diff --git a/tests/proxy_unit_tests/test_prisma_client_backoff_retry.py b/tests/proxy_unit_tests/test_prisma_client_backoff_retry.py new file mode 100644 index 00000000000..ca0519f0d96 --- /dev/null +++ b/tests/proxy_unit_tests/test_prisma_client_backoff_retry.py @@ -0,0 +1,374 @@ +""" +Test backoff retry mechanisms for PrismaClient methods during _setup_prisma_client state. + +This test validates that intermittent database connection issues are handled correctly +with exponential backoff retries for critical startup operations. +""" + +import asyncio +import pytest +import time +from unittest.mock import AsyncMock, MagicMock, patch, call +from unittest.mock import Mock +import sys +import os + +# Add project root to path +sys.path.insert(0, os.path.abspath("../..")) + +from litellm.proxy.utils import PrismaClient, ProxyLogging +from prisma.errors import PrismaError, ClientNotConnectedError +import httpx +import backoff + + +@pytest.fixture +def mock_prisma_client(): + """Create a mock PrismaClient with necessary attributes""" + client = MagicMock(spec=PrismaClient) + + # Mock database connection + client.db = AsyncMock() + client.db.query_raw = AsyncMock() + + # Mock proxy logging object + client.proxy_logging_obj = AsyncMock() + client.proxy_logging_obj.failure_handler = AsyncMock() + + return client + + +@pytest.fixture +def mock_proxy_logging(): + """Create a mock ProxyLogging object""" + proxy_logging = AsyncMock(spec=ProxyLogging) + proxy_logging.failure_handler = AsyncMock() + return proxy_logging + + +@pytest.fixture +def connection_errors(): + """Common database connection errors that should trigger retries""" + return [ + httpx.ConnectError("Connection failed"), + httpx.TimeoutException("Request timeout"), + ClientNotConnectedError(), + PrismaError("Database connection lost"), + ConnectionError("Network connection failed"), + OSError("Connection refused"), + ] + + +class TestPrismaClientBackoffRetry: + """Test suite for PrismaClient backoff retry mechanisms""" + + @pytest.mark.asyncio + async def test_health_check_success_no_retry( + self, mock_prisma_client, mock_proxy_logging + ): + """Test health_check succeeds immediately without retries""" + # Mock successful query response + mock_prisma_client.db.query_raw.return_value = [{"result": 1}] + + # Create real PrismaClient instance with mocked db + client = PrismaClient( + database_url="mock://test", proxy_logging_obj=mock_proxy_logging + ) + client.db = mock_prisma_client.db + client.proxy_logging_obj = mock_prisma_client.proxy_logging_obj + + # Call health_check + result = await client.health_check() + + # Verify success + assert result == [{"result": 1}] + mock_prisma_client.db.query_raw.assert_called_once_with("SELECT 1") + + @pytest.mark.asyncio + async def test_health_check_retry_then_success( + self, mock_prisma_client, mock_proxy_logging, connection_errors + ): + """Test health_check retries on connection errors and eventually succeeds""" + # Mock first two calls to fail, third to succeed + mock_prisma_client.db.query_raw.side_effect = [ + connection_errors[0], # First call fails + connection_errors[1], # Second call fails + [{"result": 1}], # Third call succeeds + ] + + # Create real PrismaClient instance + client = PrismaClient( + database_url="mock://test", proxy_logging_obj=mock_proxy_logging + ) + client.db = mock_prisma_client.db + client.proxy_logging_obj = mock_prisma_client.proxy_logging_obj + + # Measure execution time to verify backoff delay + start_time = time.time() + result = await client.health_check() + end_time = time.time() + + # Verify eventual success + assert result == [{"result": 1}] + + # Verify retry attempts (3 calls total) + assert mock_prisma_client.db.query_raw.call_count == 3 + + # Verify backoff delay occurred (should be at least a few milliseconds) + assert end_time - start_time > 0.01 + + @pytest.mark.asyncio + async def test_health_check_max_retries_exceeded( + self, mock_prisma_client, mock_proxy_logging, connection_errors + ): + """Test health_check fails after max retries (3) are exceeded""" + # Mock all calls to fail + mock_prisma_client.db.query_raw.side_effect = connection_errors[0] + + # Create real PrismaClient instance + client = PrismaClient( + database_url="mock://test", proxy_logging_obj=mock_proxy_logging + ) + client.db = mock_prisma_client.db + client.proxy_logging_obj = mock_prisma_client.proxy_logging_obj + + # Expect final exception after retries + with pytest.raises(httpx.ConnectError): + await client.health_check() + + # Verify max retries attempted (3 attempts) + assert mock_prisma_client.db.query_raw.call_count == 3 + + @pytest.mark.asyncio + async def test_get_spend_logs_row_count_success_no_retry( + self, mock_prisma_client, mock_proxy_logging + ): + """Test _get_spend_logs_row_count succeeds immediately""" + # Mock successful query response + mock_prisma_client.db.query_raw.return_value = [{"reltuples": 1000}] + + # Create real PrismaClient instance + client = PrismaClient( + database_url="mock://test", proxy_logging_obj=mock_proxy_logging + ) + client.db = mock_prisma_client.db + client.proxy_logging_obj = mock_prisma_client.proxy_logging_obj + + result = await client._get_spend_logs_row_count() + + assert result == 1000 + mock_prisma_client.db.query_raw.assert_called_once() + + @pytest.mark.asyncio + async def test_get_spend_logs_row_count_retry_then_success( + self, mock_prisma_client, mock_proxy_logging, connection_errors + ): + """Test _get_spend_logs_row_count retries and eventually succeeds""" + # Mock first call fails, second succeeds + mock_prisma_client.db.query_raw.side_effect = [ + connection_errors[2], # First call fails (ClientNotConnectedError) + [{"reltuples": 500}], # Second call succeeds + ] + + client = PrismaClient( + database_url="mock://test", proxy_logging_obj=mock_proxy_logging + ) + client.db = mock_prisma_client.db + client.proxy_logging_obj = mock_prisma_client.proxy_logging_obj + + result = await client._get_spend_logs_row_count() + + assert mock_prisma_client.db.query_raw.call_count == 2 + + @pytest.mark.asyncio + async def test_get_spend_logs_row_count_handles_errors_gracefully( + self, mock_prisma_client, mock_proxy_logging + ): + """Test _get_spend_logs_row_count returns 0 on persistent errors""" + # Mock all calls to fail + mock_prisma_client.db.query_raw.side_effect = PrismaError("Persistent DB error") + + client = PrismaClient( + database_url="mock://test", proxy_logging_obj=mock_proxy_logging + ) + client.db = mock_prisma_client.db + client.proxy_logging_obj = mock_prisma_client.proxy_logging_obj + + result = await client._get_spend_logs_row_count() + + assert result == 0 + assert mock_prisma_client.db.query_raw.call_count == 3 # Max retries attempted + + @pytest.mark.asyncio + async def test_set_spend_logs_row_count_in_proxy_state_success( + self, mock_prisma_client, mock_proxy_logging + ): + """Test _set_spend_logs_row_count_in_proxy_state succeeds""" + # Mock successful query response + mock_prisma_client.db.query_raw.return_value = [{"reltuples": 2000}] + + client = PrismaClient( + database_url="mock://test", proxy_logging_obj=mock_proxy_logging + ) + client.db = mock_prisma_client.db + client.proxy_logging_obj = mock_prisma_client.proxy_logging_obj + + # Mock proxy_state + with patch("litellm.proxy.proxy_server.proxy_state") as mock_proxy_state: + mock_proxy_state.set_proxy_state_variable = Mock() + + await client._set_spend_logs_row_count_in_proxy_state() + + # Verify proxy state was updated + mock_proxy_state.set_proxy_state_variable.assert_called_once_with( + variable_name="spend_logs_row_count", value=2000 + ) + + @pytest.mark.asyncio + async def test_set_spend_logs_row_count_retry_behavior( + self, mock_prisma_client, mock_proxy_logging, connection_errors + ): + """Test _set_spend_logs_row_count_in_proxy_state retries on database errors""" + mock_prisma_client.db.query_raw.side_effect = [ + connection_errors[3], # First call fails (PrismaError) + [{"reltuples": 1500}], # Second call succeeds + ] + + client = PrismaClient( + database_url="mock://test", proxy_logging_obj=mock_proxy_logging + ) + client.db = mock_prisma_client.db + client.proxy_logging_obj = mock_prisma_client.proxy_logging_obj + + with patch("litellm.proxy.proxy_server.proxy_state") as mock_proxy_state: + mock_proxy_state.set_proxy_state_variable = Mock() + + await client._set_spend_logs_row_count_in_proxy_state() + + assert mock_prisma_client.db.query_raw.call_count == 2 + mock_proxy_state.set_proxy_state_variable.assert_called_once_with( + variable_name="spend_logs_row_count", value=1500 + ) + + @pytest.mark.asyncio + async def test_backoff_configuration_parameters(self, mock_proxy_logging): + """Test that backoff decorators are configured with correct parameters""" + client = PrismaClient( + database_url="mock://test", proxy_logging_obj=mock_proxy_logging + ) + + # Check that methods have backoff decorators + assert hasattr(client.health_check, "__wrapped__") + assert hasattr(client._set_spend_logs_row_count_in_proxy_state, "__wrapped__") + + # Verify backoff configuration exists (methods should have retry behavior) + # This is implicit verification - the decorators are applied in the source code + + @pytest.mark.asyncio + async def test_multiple_connection_error_types( + self, mock_prisma_client, mock_proxy_logging + ): + """Test that different types of connection errors all trigger retries""" + error_types = [ + httpx.ConnectError("Connection error"), + httpx.TimeoutException("Timeout error"), + ClientNotConnectedError(), + PrismaError("Database error"), + ConnectionError("Network error"), + OSError("OS-level connection error"), + ] + + for error_type in error_types: + # Reset mock for each test + mock_prisma_client.db.query_raw.reset_mock() + mock_prisma_client.db.query_raw.side_effect = [ + error_type, # First call fails + [{"result": 1}], # Second call succeeds + ] + + client = PrismaClient( + database_url="mock://test", proxy_logging_obj=mock_proxy_logging + ) + client.db = mock_prisma_client.db + client.proxy_logging_obj = mock_prisma_client.proxy_logging_obj + + # Should succeed after retry + result = await client.health_check() + assert result == [{"result": 1}] + assert mock_prisma_client.db.query_raw.call_count == 2 + + @pytest.mark.asyncio + async def test_setup_prisma_client_integration( + self, mock_prisma_client, mock_proxy_logging + ): + """Test simulated _setup_prisma_client flow with intermittent failures""" + # This simulates the actual flow that happens in proxy_server.py _setup_prisma_client + + client = PrismaClient( + database_url="mock://test", proxy_logging_obj=mock_proxy_logging + ) + client.db = mock_prisma_client.db + client.proxy_logging_obj = mock_prisma_client.proxy_logging_obj + + # Simulate intermittent failures followed by success + call_count = 0 + + def mock_query_side_effect(*args, **kwargs): + nonlocal call_count + call_count += 1 + if call_count <= 2: # First two calls fail + raise httpx.ConnectError("Intermittent connection issue") + else: + if "SELECT 1" in str(args): + return [{"result": 1}] + else: + return [{"reltuples": 1000}] + + mock_prisma_client.db.query_raw.side_effect = mock_query_side_effect + + with patch("litellm.proxy.proxy_server.proxy_state") as mock_proxy_state: + mock_proxy_state.set_proxy_state_variable = Mock() + + # Execute the two critical calls from _setup_prisma_client + health_result = await client.health_check() + await client._set_spend_logs_row_count_in_proxy_state() + + # Verify both operations eventually succeeded despite initial failures + assert health_result == [{"result": 1}] + mock_proxy_state.set_proxy_state_variable.assert_called_once() + + # Verify retries occurred (should have more than 2 total query calls) + assert mock_prisma_client.db.query_raw.call_count >= 4 + + @pytest.mark.asyncio + async def test_backoff_timing_constraints( + self, mock_prisma_client, mock_proxy_logging, connection_errors + ): + """Test that backoff respects max_time constraint (10 seconds)""" + # Mock all calls to fail to test max_time + mock_prisma_client.db.query_raw.side_effect = connection_errors[0] + + client = PrismaClient( + database_url="mock://test", proxy_logging_obj=mock_proxy_logging + ) + client.db = mock_prisma_client.db + client.proxy_logging_obj = mock_prisma_client.proxy_logging_obj + + start_time = time.time() + + with pytest.raises(httpx.ConnectError): + await client.health_check() + + end_time = time.time() + duration = end_time - start_time + + # Should not exceed max_time of 10 seconds significantly + # Adding small buffer for test execution overhead + assert duration < 12.0 + + # Should have attempted max_tries (3) retries + assert mock_prisma_client.db.query_raw.call_count == 3 + + +if __name__ == "__main__": + pytest.main([__file__, "-v"]) From af0cb7c277c3534fe6160105739c84982169bdd3 Mon Sep 17 00:00:00 2001 From: Ritesh Kadmawala Date: Thu, 25 Sep 2025 17:55:43 +0530 Subject: [PATCH 59/70] :bug: Fix a bug where openai image edit siltently ignores multiple images --- .../llms/openai/image_edit/transformation.py | 32 ++- .../openai/test_o_series_transformation.py | 1 - .../test_openai_image_edit_transformation.py | 254 ++++++++++++++++++ 3 files changed, 269 insertions(+), 18 deletions(-) create mode 100644 tests/test_litellm/llms/openai/test_openai_image_edit_transformation.py diff --git a/litellm/llms/openai/image_edit/transformation.py b/litellm/llms/openai/image_edit/transformation.py index be1aeb1b8a4..f6898c3a762 100644 --- a/litellm/llms/openai/image_edit/transformation.py +++ b/litellm/llms/openai/image_edit/transformation.py @@ -82,7 +82,7 @@ class OpenAIImageEditConfig(BaseImageEditConfig): ######################################################### # Separate images and masks as `files` and send other parameters as `data` ######################################################### - _image = request_dict.get("image") + _image_list = request_dict.get("image") _mask = request_dict.get("mask") data_without_files = { k: v for k, v in request_dict.items() if k not in ["image", "mask"] @@ -90,23 +90,21 @@ class OpenAIImageEditConfig(BaseImageEditConfig): files_list: List[Tuple[str, Any]] = [] # Handle image parameter - if _image is not None: - # Handle case where image can be a list (extract first image) - if isinstance(_image, list): - _image = _image[0] if _image else None - - if _image is not None: - image_content_type: str = ImageEditRequestUtils.get_image_content_type( - _image - ) - if isinstance(_image, BufferedReader): - files_list.append( - ("image", (_image.name, _image, image_content_type)) - ) - else: - files_list.append( - ("image", ("image.png", _image, image_content_type)) + if _image_list is not None: + image_list = [_image_list] if not isinstance(_image_list, list) else _image_list + for _image in image_list: + if _image is not None: + image_content_type: str = ImageEditRequestUtils.get_image_content_type( + _image ) + if isinstance(_image, BufferedReader): + files_list.append( + ("image", (_image.name, _image, image_content_type)) + ) + else: + files_list.append( + ("image", ("image.png", _image, image_content_type)) + ) # Handle mask parameter if provided if _mask is not None: diff --git a/tests/test_litellm/llms/openai/test_o_series_transformation.py b/tests/test_litellm/llms/openai/test_o_series_transformation.py index c82d5878dfd..d4b3343dc7e 100644 --- a/tests/test_litellm/llms/openai/test_o_series_transformation.py +++ b/tests/test_litellm/llms/openai/test_o_series_transformation.py @@ -2,7 +2,6 @@ import pytest from litellm.llms.openai.chat.o_series_transformation import OpenAIOSeriesConfig - @pytest.mark.parametrize( "model_name,expected", [ diff --git a/tests/test_litellm/llms/openai/test_openai_image_edit_transformation.py b/tests/test_litellm/llms/openai/test_openai_image_edit_transformation.py new file mode 100644 index 00000000000..d0d378ddfd7 --- /dev/null +++ b/tests/test_litellm/llms/openai/test_openai_image_edit_transformation.py @@ -0,0 +1,254 @@ +from io import BufferedReader, BytesIO +from typing import Dict +import pytest +from litellm import image_edit +from litellm.llms.openai.image_edit.transformation import OpenAIImageEditConfig +from litellm.types.router import GenericLiteLLMParams + + +@pytest.fixture +def image_edit_config() -> OpenAIImageEditConfig: + return OpenAIImageEditConfig() + + +def test_transform_image_edit_request_basic(image_edit_config: OpenAIImageEditConfig): + """Test basic image edit request transformation with image and prompt""" + model = "dall-e-2" + prompt = "Make the background blue" + image = b"fake_image_data" + image_edit_optional_request_params = {} + litellm_params = GenericLiteLLMParams() + headers = {} + + data, files = image_edit_config.transform_image_edit_request( + model=model, + prompt=prompt, + image=image, + image_edit_optional_request_params=image_edit_optional_request_params, + litellm_params=litellm_params, + headers=headers, + ) + + # Check that data contains model and prompt but not image + assert data["model"] == model + assert data["prompt"] == prompt + assert "image" not in data + assert "mask" not in data + + # Check that files contains the image + assert len(files) == 1 + assert files[0][0] == "image" # field name + assert files[0][1][0] == "image.png" # filename + assert files[0][1][1] == image # image data + assert "image/png" in files[0][1][2] # content type + + +def test_transform_image_edit_request_with_mask(image_edit_config: OpenAIImageEditConfig): + """Test transformation with mask parameter""" + model = "dall-e-2" + prompt = "Make the background blue" + image = b"fake_image_data" + mask = b"fake_mask_data" + image_edit_optional_request_params = {"mask": mask, "size": "1024x1024"} + litellm_params = GenericLiteLLMParams() + headers = {} + + data, files = image_edit_config.transform_image_edit_request( + model=model, + prompt=prompt, + image=image, + image_edit_optional_request_params=image_edit_optional_request_params, + litellm_params=litellm_params, + headers=headers, + ) + + # Check that data contains model, prompt, and size but not image or mask + assert data["model"] == model + assert data["prompt"] == prompt + assert data["size"] == "1024x1024" + assert "image" not in data + assert "mask" not in data + + # Check that files contains both image and mask + assert len(files) == 2 + + # Find image and mask in files + image_file = next(f for f in files if f[0] == "image") + mask_file = next(f for f in files if f[0] == "mask") + + assert image_file[1][0] == "image.png" + assert image_file[1][1] == image + assert "image/png" in image_file[1][2] + + assert mask_file[1][0] == "mask.png" + assert mask_file[1][1] == mask + assert "image/png" in mask_file[1][2] + + +def test_transform_image_edit_request_with_buffered_reader(image_edit_config: OpenAIImageEditConfig): + """Test transformation with BufferedReader as image input""" + import tempfile + import os + + model = "dall-e-2" + prompt = "Make the background blue" + + # Create a real file to get a proper BufferedReader + image_data = b"fake_image_data" + with tempfile.NamedTemporaryFile(suffix=".jpg", delete=False) as temp_file: + temp_file.write(image_data) + temp_file_path = temp_file.name + + try: + # Open the file as BufferedReader + with open(temp_file_path, 'rb') as image_buffer: + image_edit_optional_request_params = {} + litellm_params = GenericLiteLLMParams() + headers = {} + + data, files = image_edit_config.transform_image_edit_request( + model=model, + prompt=prompt, + image=image_buffer, + image_edit_optional_request_params=image_edit_optional_request_params, + litellm_params=litellm_params, + headers=headers, + ) + + # Check that data contains model and prompt but not image + assert data["model"] == model + assert data["prompt"] == prompt + assert "image" not in data + + # Check that files contains the image with the original filename + assert len(files) == 1 + assert files[0][0] == "image" + # Should use the buffer's name (full path from the BufferedReader.name) + assert files[0][1][0] == temp_file_path # Uses full path from buffer.name + assert files[0][1][1] == image_buffer # Should be the buffer object + # Content type detection defaults to PNG for fake data without image headers + assert files[0][1][2].startswith("image/") # Should detect some image type + finally: + # Clean up the temp file + os.unlink(temp_file_path) + + +def test_transform_image_edit_request_with_optional_params(image_edit_config: OpenAIImageEditConfig): + """Test transformation with optional parameters like size, quality, etc.""" + model = "dall-e-2" + prompt = "Make the background blue" + image = b"fake_image_data" + image_edit_optional_request_params = { + "size": "512x512", + "response_format": "b64_json", + "n": 2, + "user": "test_user" + } + litellm_params = GenericLiteLLMParams() + headers = {} + + data, files = image_edit_config.transform_image_edit_request( + model=model, + prompt=prompt, + image=image, + image_edit_optional_request_params=image_edit_optional_request_params, + litellm_params=litellm_params, + headers=headers, + ) + + # Check that data contains all the optional parameters + assert data["model"] == model + assert data["prompt"] == prompt + assert data["size"] == "512x512" + assert data["response_format"] == "b64_json" + assert data["n"] == 2 + assert data["user"] == "test_user" + assert "image" not in data + assert "mask" not in data + + # Check that files contains only the image + assert len(files) == 1 + assert files[0][0] == "image" + assert files[0][1][1] == image + + +def test_transform_image_edit_request_with_multiple_images(image_edit_config: OpenAIImageEditConfig): + """Test transformation with multiple images and no mask""" + model = "dall-e-2" + prompt = "Make the background blue" + image1 = b"fake_image_data_1" + image2 = b"fake_image_data_2" + image3 = b"fake_image_data_3" + images = [image1, image2, image3] + image_edit_optional_request_params = {"size": "1024x1024", "n": 1} + litellm_params = GenericLiteLLMParams() + headers = {} + + data, files = image_edit_config.transform_image_edit_request( + model=model, + prompt=prompt, + image=images, + image_edit_optional_request_params=image_edit_optional_request_params, + litellm_params=litellm_params, + headers=headers, + ) + + # Check that data contains model, prompt, and optional params but not image or mask + assert data["model"] == model + assert data["prompt"] == prompt + assert data["size"] == "1024x1024" + assert data["n"] == 1 + assert "image" not in data + assert "mask" not in data + + # Check that files contains all three images and no mask + assert len(files) == 3 + + # All files should be image entries + image_files = [f for f in files if f[0] == "image"] + assert len(image_files) == 3 + + # Check that all image data is present + image_data_in_files = [f[1][1] for f in image_files] + assert image1 in image_data_in_files + assert image2 in image_data_in_files + assert image3 in image_data_in_files + + # Check that all files have proper content type + for file_entry in image_files: + assert file_entry[1][0] == "image.png" # filename + assert file_entry[1][2].startswith("image/") # content type + + +def test_transform_image_edit_request_with_mask_list(image_edit_config: OpenAIImageEditConfig): + """Test transformation with mask as list (should take first element)""" + model = "dall-e-2" + prompt = "Make the background blue" + image = b"fake_image_data" + mask1 = b"fake_mask_data_1" + mask2 = b"fake_mask_data_2" + image_edit_optional_request_params = {"mask": [mask1, mask2]} + litellm_params = GenericLiteLLMParams() + headers = {} + + data, files = image_edit_config.transform_image_edit_request( + model=model, + prompt=prompt, + image=image, + image_edit_optional_request_params=image_edit_optional_request_params, + litellm_params=litellm_params, + headers=headers, + ) + + # Check that data contains model and prompt but not image or mask + assert data["model"] == model + assert data["prompt"] == prompt + assert "image" not in data + assert "mask" not in data + + # Check that files contains image and only the first mask + assert len(files) == 2 + + mask_file = next(f for f in files if f[0] == "mask") + assert mask_file[1][1] == mask1 # Should be the first mask, not the second + From 914844122cc45f8982d99d5883258089a819742e Mon Sep 17 00:00:00 2001 From: Alex Shoop Date: Sat, 27 Sep 2025 01:14:20 +0900 Subject: [PATCH 60/70] Fix: revert `fastuuid` optional dependency, always use `fastuuid` in `.__uid` helper (#14941) * always use fastuuid * rm from proxy extras since its now default * poetry lock --- litellm/_uuid.py | 11 +- poetry.lock | 124 ++++++++++-------- pyproject.toml | 3 +- ...t_uuid_fallback.py => test_uuid_helper.py} | 7 +- 4 files changed, 80 insertions(+), 65 deletions(-) rename tests/test_litellm/{test_uuid_fallback.py => test_uuid_helper.py} (60%) diff --git a/litellm/_uuid.py b/litellm/_uuid.py index 05b1adbf75b..52acf647dd8 100644 --- a/litellm/_uuid.py +++ b/litellm/_uuid.py @@ -1,17 +1,10 @@ """ Internal unified UUID helper. -Tries to use fastuuid (performance) and falls back to stdlib uuid if unavailable. +Always uses fastuuid for performance. """ -FASTUUID_AVAILABLE = False - -try: - import fastuuid as _uuid # type: ignore - - FASTUUID_AVAILABLE = True -except Exception: # pragma: no cover - fallback path - import uuid as _uuid # type: ignore +import fastuuid as _uuid # type: ignore # Expose a module-like alias so callers can use: uuid.uuid4() diff --git a/poetry.lock b/poetry.lock index 9a9409664b0..b6ed7801bed 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1917,38 +1917,79 @@ zstandard = ["zstandard"] [[package]] name = "fastuuid" -version = "0.12.0" +version = "0.13.5" description = "Python bindings to Rust's UUID library." -optional = true +optional = false python-versions = ">=3.8" groups = ["main"] -markers = "extra == \"proxy\"" files = [ - {file = "fastuuid-0.12.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:22a900ef0956aacf862b460e20541fdae2d7c340594fe1bd6fdcb10d5f0791a9"}, - {file = "fastuuid-0.12.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0302f5acf54dc75de30103025c5a95db06d6c2be36829043a0aa16fc170076bc"}, - {file = "fastuuid-0.12.0-cp310-cp310-manylinux_2_34_x86_64.whl", hash = "sha256:7946b4a310cfc2d597dcba658019d72a2851612a2cebb949d809c0e2474cf0a6"}, - {file = "fastuuid-0.12.0-cp310-cp310-win_amd64.whl", hash = "sha256:a1b6764dd42bf0c46c858fb5ade7b7a3d93b7a27485a7a5c184909026694cd88"}, - {file = "fastuuid-0.12.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2bced35269315d16fe0c41003f8c9d63f2ee16a59295d90922cad5e6a67d0418"}, - {file = "fastuuid-0.12.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:82106e4b0a24f4f2f73c88f89dadbc1533bb808900740ca5db9bbb17d3b0c824"}, - {file = "fastuuid-0.12.0-cp311-cp311-manylinux_2_34_x86_64.whl", hash = "sha256:4db1bc7b8caa1d7412e1bea29b016d23a8d219131cff825b933eb3428f044dca"}, - {file = "fastuuid-0.12.0-cp311-cp311-win_amd64.whl", hash = "sha256:07afc8e674e67ac3d35a608c68f6809da5fab470fb4ef4469094fdb32ba36c51"}, - {file = "fastuuid-0.12.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:328694a573fe9dce556b0b70c9d03776786801e028d82f0b6d9db1cb0521b4d1"}, - {file = "fastuuid-0.12.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:02acaea2c955bb2035a7d8e7b3fba8bd623b03746ae278e5fa932ef54c702f9f"}, - {file = "fastuuid-0.12.0-cp312-cp312-manylinux_2_34_x86_64.whl", hash = "sha256:ed9f449cba8cf16cced252521aee06e633d50ec48c807683f21cc1d89e193eb0"}, - {file = "fastuuid-0.12.0-cp312-cp312-win_amd64.whl", hash = "sha256:0df2ea4c9db96fd8f4fa38d0e88e309b3e56f8fd03675a2f6958a5b082a0c1e4"}, - {file = "fastuuid-0.12.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7fe2407316a04ee8f06d3dbc7eae396d0a86591d92bafe2ca32fce23b1145786"}, - {file = "fastuuid-0.12.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b9b31dd488d0778c36f8279b306dc92a42f16904cba54acca71e107d65b60b0c"}, - {file = "fastuuid-0.12.0-cp313-cp313-manylinux_2_34_x86_64.whl", hash = "sha256:b19361ee649365eefc717ec08005972d3d1eb9ee39908022d98e3bfa9da59e37"}, - {file = "fastuuid-0.12.0-cp313-cp313-win_amd64.whl", hash = "sha256:8fc66b11423e6f3e1937385f655bedd67aebe56a3dcec0cb835351cfe7d358c9"}, - {file = "fastuuid-0.12.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7b15c54d300279ab20a9cc0579ada9c9f80d1bc92997fc61fb7bf3103d7cb26b"}, - {file = "fastuuid-0.12.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:458f1bc3ebbd76fdb89ad83e6b81ccd3b2a99fa6707cd3650b27606745cfb170"}, - {file = "fastuuid-0.12.0-cp38-cp38-manylinux_2_34_x86_64.whl", hash = "sha256:a8f0f83fbba6dc44271a11b22e15838641b8c45612cdf541b4822a5930f6893c"}, - {file = "fastuuid-0.12.0-cp38-cp38-win_amd64.whl", hash = "sha256:7cfd2092253d3441f6a8c66feff3c3c009da25a5b3da82bc73737558543632be"}, - {file = "fastuuid-0.12.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:9303617e887429c193d036d47d0b32b774ed3618431123e9106f610d601eb57e"}, - {file = "fastuuid-0.12.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8790221325b376e1122e95f865753ebf456a9fb8faf0dca4f9bf7a3ff620e413"}, - {file = "fastuuid-0.12.0-cp39-cp39-manylinux_2_34_x86_64.whl", hash = "sha256:e4b12d3e23515e29773fa61644daa660ceb7725e05397a986c2109f512579a48"}, - {file = "fastuuid-0.12.0-cp39-cp39-win_amd64.whl", hash = "sha256:e41656457c34b5dcb784729537ea64c7d9bbaf7047b480c6c6a64c53379f455a"}, - {file = "fastuuid-0.12.0.tar.gz", hash = "sha256:d0bd4e5b35aad2826403f4411937c89e7c88857b1513fe10f696544c03e9bd8e"}, + {file = "fastuuid-0.13.5-cp310-cp310-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:b9edf8ee30718aee787cdd2e9e1ff3d4a3ec6ddb32fba0a23fa04956df69ab07"}, + {file = "fastuuid-0.13.5-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:f67ea1e25c5e782f7fb5aaa5208f157d950401dd9321ce56bcc6d4dc3d72ed60"}, + {file = "fastuuid-0.13.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9ff3fc87e1f19603dd53c38f42c2ea8d5d5462554deab69e9cf1800574e4756c"}, + {file = "fastuuid-0.13.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a6e5337fa7698dc52bc724da7e9239e93c5b24a09f6904b8660dfb8c41ce3dee"}, + {file = "fastuuid-0.13.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f9db596023c10dabb12489a88c51b75297c3a2478cb2be645e06905934e7b9fc"}, + {file = "fastuuid-0.13.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:191ff6192fe53c5fc9d4d241ee1156b30a7ed6f1677b1cc2423e7ecdbc26222b"}, + {file = "fastuuid-0.13.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:348ce9f296dda701ba46d8dceeff309f90dbc75dd85080bbed2b299aa908890a"}, + {file = "fastuuid-0.13.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:46954fb644995d7fc8bbd710fbd4c65cedaa48c921c86fdbafef0229168a8c96"}, + {file = "fastuuid-0.13.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:22da0f66041e1c10c7d465b495cc6cd8e17e080dda34b4bd5ff5240b860fbb82"}, + {file = "fastuuid-0.13.5-cp310-cp310-win32.whl", hash = "sha256:3e6b548f06c1ed7bad951a17a09eef69d6f24eb2b874cb4833e26b886d82990f"}, + {file = "fastuuid-0.13.5-cp310-cp310-win_amd64.whl", hash = "sha256:c82838e52189d16b1307631179cb2cd37778dd8f4ddc00e9ce3c26f920b3b2f7"}, + {file = "fastuuid-0.13.5-cp311-cp311-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:c122558ca4b5487e2bd0863467e4ccfe636afd1274803741487d48f2e32ea0e1"}, + {file = "fastuuid-0.13.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:d7abd42a03a17a681abddd19aa4d44ca2747138cf8a48373b395cf1341a10de2"}, + {file = "fastuuid-0.13.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2705cf7c2d6f7c03053404b75a4c44f872a73f6f9d5ea34f1dc6bba400c4a97c"}, + {file = "fastuuid-0.13.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d220a056fcbad25932c1f25304261198612f271f4d150b2a84e81adb877daf7"}, + {file = "fastuuid-0.13.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2f29f93b5a0c5f5579f97f77d5319e9bfefd61d8678ec59d850201544faf33bf"}, + {file = "fastuuid-0.13.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:399d86623fb806151b1feb9fdd818ebfc1d50387199a35f7264f98dfc1540af5"}, + {file = "fastuuid-0.13.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:689e8795a1edd573b2c9a455024e4edf605a9690339bba29709857f7180894ea"}, + {file = "fastuuid-0.13.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:25e82c4a1734da168b36f7308e397afbe9c9b353799a9c69563a605f11dd4641"}, + {file = "fastuuid-0.13.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:f62299e3cca69aad6a6fb37e26e45055587954d498ad98903fea24382377ea0e"}, + {file = "fastuuid-0.13.5-cp311-cp311-win32.whl", hash = "sha256:68227f2230381b89fb1ad362ca6e433de85c6c11c36312b41757cad47b8a8e32"}, + {file = "fastuuid-0.13.5-cp311-cp311-win_amd64.whl", hash = "sha256:4a32306982bd031cb20d5d1a726b7b958a55babebd2300ce6c8e352d3496e931"}, + {file = "fastuuid-0.13.5-cp312-cp312-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:35fe8045e866bc6846f8de6fa05acb1de0c32478048484a995e96d31e21dff2a"}, + {file = "fastuuid-0.13.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:02a460333f52d731a006d18a52ef6fcb2d295a1f5b1a5938d30744191b2f77b7"}, + {file = "fastuuid-0.13.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:74b0e4f8c307b9f477a5d7284db4431ce53a3c1e3f4173db7a97db18564a6202"}, + {file = "fastuuid-0.13.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6955a99ef455c2986f3851f4e0ccc35dec56ac1a7720f2b92e88a75d6684512e"}, + {file = "fastuuid-0.13.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f10c77b826738c1a27dcdaa92ea4dc1ec9d869748a99e1fde54f1379553d4854"}, + {file = "fastuuid-0.13.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bb25dccbeb249d16d5e664f65f17ebec05136821d5ef462c4110e3f76b86fb86"}, + {file = "fastuuid-0.13.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:a5becc646a3eeafb76ce0a6783ba190cd182e3790a8b2c78ca9db2b5e87af952"}, + {file = "fastuuid-0.13.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:69b34363752d06e9bb0dbdf02ae391ec56ac948c6f2eb00be90dad68e80774b9"}, + {file = "fastuuid-0.13.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:57d0768afcad0eab8770c9b8cf904716bd3c547e8b9a4e755ee8a673b060a3a3"}, + {file = "fastuuid-0.13.5-cp312-cp312-win32.whl", hash = "sha256:8ac6c6f5129d52eaa6ef9ea4b6e2f7c69468a053f3ab8e439661186b9c06bb85"}, + {file = "fastuuid-0.13.5-cp312-cp312-win_amd64.whl", hash = "sha256:ad630e97715beefef07ec37c9c162336e500400774e2c1cbe1a0df6f80d15b9a"}, + {file = "fastuuid-0.13.5-cp313-cp313-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:ea17dfd35e0e91920a35d91e65e5f9c9d1985db55ac4ff2f1667a0f61189cefa"}, + {file = "fastuuid-0.13.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:be6ad91e5fefbcc2a4b478858a2715e386d405834ea3ae337c3b6b95cc0e47d6"}, + {file = "fastuuid-0.13.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ea6df13a306aab3e0439d58c312ff1e6f4f07f09f667579679239b4a6121f64a"}, + {file = "fastuuid-0.13.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2354c1996d3cf12dc2ba3752e2c4d6edc46e1a38c63893146777b1939f3062d4"}, + {file = "fastuuid-0.13.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a6cf9b7469fc26d1f9b1c43ac4b192e219e85b88fdf81d71aa755a6c08c8a817"}, + {file = "fastuuid-0.13.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:92ba539170097b9047551375f1ca09d8d2b4aefcc79eeae3e1c43fe49b42072e"}, + {file = "fastuuid-0.13.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:dbb81d05617bc2970765c1ad82db7e8716f6a2b7a361a14b83de5b9240ade448"}, + {file = "fastuuid-0.13.5-cp313-cp313-musllinux_1_1_i686.whl", hash = "sha256:d973bd6bf9d754d3cca874714ac0a6b22a47f239fb3d3c8687569db05aac3471"}, + {file = "fastuuid-0.13.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:e725ceef79486423f05ee657634d4b4c1ca5fb2c8a94e0708f5d6356a83f2a83"}, + {file = "fastuuid-0.13.5-cp313-cp313-win32.whl", hash = "sha256:a1c430a332ead0b2674f1ef71b17f43b8139ec5a4201182766a21f131a31e021"}, + {file = "fastuuid-0.13.5-cp313-cp313-win_amd64.whl", hash = "sha256:241fdd362fd96e6b337db62a65dd7cb3dfac20adf854573247a47510e192db6f"}, + {file = "fastuuid-0.13.5-cp38-cp38-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:e353c0a0d978a5ecd97171ac4fb7f55a6bd6cbae90f1ec4e828e5317f11b995e"}, + {file = "fastuuid-0.13.5-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:904ac3eb37f4742e23f6a51be0d0451d1d3aceb50df8dac7afc6bf5209793650"}, + {file = "fastuuid-0.13.5-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:c6b070e0dc1965d53b9e07c291537095ececf7d7e36e60aed9b22400fa6c5c7f"}, + {file = "fastuuid-0.13.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0225f8bc78071a191cb458b3b0e23b04a7f03013575b8a3083da2a84c450e200"}, + {file = "fastuuid-0.13.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a3f3f8f10b962cf2e11d3affc0cf2697ac5c9accc0d282dce981ed555a44ce15"}, + {file = "fastuuid-0.13.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c1ae87968614fe6d3029a8198671b5893341aac9459289e93d201027be9ea7e8"}, + {file = "fastuuid-0.13.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:6ca01f5e614530a1a858bf185dd5556805a4c11b6eba0a2536890b68ed954922"}, + {file = "fastuuid-0.13.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:6d3b6b10d78b9f7056445ac377612443980349da7221a3dd3e3f382f7c437be3"}, + {file = "fastuuid-0.13.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:9ae95e4dcf94775d948ebb843f4443d33cd224bb31174030e106ee3cab66527c"}, + {file = "fastuuid-0.13.5-cp38-cp38-win32.whl", hash = "sha256:5d753bc9ba8de6dd9caa8bbac045578c2fbe1c6ae40c2026b614676776fbe9dc"}, + {file = "fastuuid-0.13.5-cp38-cp38-win_amd64.whl", hash = "sha256:f9530f1328b05b80c6fa111e7f2a5d55fa30fbbd72d708326d0c7b55b67ed772"}, + {file = "fastuuid-0.13.5-cp39-cp39-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:ed78153c589e5efb34faaa216836a5bf8a0b9d34e82183203166011238d9ed13"}, + {file = "fastuuid-0.13.5-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:a67e18c2d7fba8be6ea4aed8ca5a20fcf273f003efa01c1f33a096b72537e69e"}, + {file = "fastuuid-0.13.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4e362a3d66874d3d11a1ee9a8e717e32c2817cdb5d7a4e913290bf6e0f2a7fd8"}, + {file = "fastuuid-0.13.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b9baa9c33848ec0926231e7ecfef9e02faa0f6d24265b64108ea41f7a0bb3f48"}, + {file = "fastuuid-0.13.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b3ec6dab282162c19ec2172f33bafd467cffe26b92345789278adcbec19428d1"}, + {file = "fastuuid-0.13.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d2424a90688dbc44f119686fa452ff21aa106c9da258214f577816462ad606d5"}, + {file = "fastuuid-0.13.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:f8f2cc6972941ab030f3776961ed8454772c3acad88781fc262d71514df89973"}, + {file = "fastuuid-0.13.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:a41257ea172b5de199c3cfa71cc6c574dcf22367fe51e26cba0d359107f11f30"}, + {file = "fastuuid-0.13.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:cade5f3b8023dbba5a006e5685a7baf0d7a30c43cea17113768aa9ef9582d799"}, + {file = "fastuuid-0.13.5-cp39-cp39-win32.whl", hash = "sha256:880f0d03ad2518b96757ca422cba6ff76cea5464db2b3ad75c32acf1890e058f"}, + {file = "fastuuid-0.13.5-cp39-cp39-win_amd64.whl", hash = "sha256:ebe95b730f81808eabc90247ac3d412b96d9fae1c406760b163bb9f134b7af69"}, + {file = "fastuuid-0.13.5.tar.gz", hash = "sha256:d4976821ab424d41542e1ea39bc828a9d454c3f8a04067c06fca123c5b95a1a1"}, ] [[package]] @@ -8334,26 +8375,6 @@ examples = ["aiosqlite (>=0.21.0)", "fastapi (>=0.115.12)", "sqlalchemy[asyncio] granian = ["granian (>=2.3.1)"] uvicorn = ["uvicorn (>=0.34.0)"] -[[package]] -name = "starlette" -version = "0.44.0" -description = "The little ASGI library that shines." -optional = false -python-versions = ">=3.8" -groups = ["main", "dev"] -files = [ - {file = "starlette-0.44.0-py3-none-any.whl", hash = "sha256:19edeb75844c16dcd4f9dd72f22f9108c1539f3fc9c4c88885654fef64f85aea"}, - {file = "starlette-0.44.0.tar.gz", hash = "sha256:e35166950a3ccccc701962fe0711db0bc14f2ecd37c6f9fe5e3eae0cbaea8715"}, -] -markers = {main = "(python_full_version == \"3.8.*\" or platform_python_implementation == \"PyPy\") and extra == \"proxy\" and python_version < \"3.10\"", dev = "python_full_version == \"3.8.*\" or platform_python_implementation == \"PyPy\" and python_version < \"3.10\""} - -[package.dependencies] -anyio = ">=3.4.0,<5" -typing-extensions = {version = ">=3.10.0", markers = "python_version < \"3.10\""} - -[package.extras] -full = ["httpx (>=0.27.0,<0.29.0)", "itsdangerous", "jinja2", "python-multipart (>=0.0.18)", "pyyaml"] - [[package]] name = "starlette" version = "0.46.2" @@ -8365,11 +8386,10 @@ files = [ {file = "starlette-0.46.2-py3-none-any.whl", hash = "sha256:595633ce89f8ffa71a015caed34a5b2dc1c0cdb3f0f1fbd1e69339cf2abeec35"}, {file = "starlette-0.46.2.tar.gz", hash = "sha256:7f7361f34eed179294600af672f565727419830b54b7b084efe44bb82d2fccd5"}, ] -markers = {main = "(extra == \"mlflow\" or extra == \"proxy\") and python_version >= \"3.10\" or extra == \"proxy\" and platform_python_implementation != \"PyPy\" and python_version >= \"3.9\" or python_version >= \"3.12\" and extra == \"proxy\"", dev = "python_version >= \"3.9\" and platform_python_implementation != \"PyPy\" or python_version >= \"3.10\""} +markers = {main = "(extra == \"mlflow\" or extra == \"proxy\") and python_version >= \"3.10\" or extra == \"proxy\""} [package.dependencies] anyio = ">=3.6.2,<5" -typing-extensions = {version = ">=3.10.0", markers = "python_version < \"3.10\""} [package.extras] full = ["httpx (>=0.27.0,<0.29.0)", "itsdangerous", "jinja2", "python-multipart (>=0.0.18)", "pyyaml"] @@ -9571,11 +9591,11 @@ type = ["pytest-mypy"] caching = ["diskcache"] extra-proxy = ["azure-identity", "azure-keyvault-secrets", "google-cloud-iam", "google-cloud-kms", "prisma", "redisvl", "resend"] mlflow = ["mlflow"] -proxy = ["PyJWT", "apscheduler", "azure-identity", "azure-storage-blob", "backoff", "boto3", "cryptography", "fastapi", "fastapi-sso", "fastuuid", "gunicorn", "litellm-enterprise", "litellm-proxy-extras", "mcp", "orjson", "polars", "pynacl", "python-multipart", "pyyaml", "rich", "rq", "uvicorn", "uvloop", "websockets"] +proxy = ["PyJWT", "apscheduler", "azure-identity", "azure-storage-blob", "backoff", "boto3", "cryptography", "fastapi", "fastapi-sso", "gunicorn", "litellm-enterprise", "litellm-proxy-extras", "mcp", "orjson", "polars", "pynacl", "python-multipart", "pyyaml", "rich", "rq", "uvicorn", "uvloop", "websockets"] semantic-router = ["semantic-router"] utils = ["numpydoc"] [metadata] lock-version = "2.1" python-versions = ">=3.8.1,<4.0, !=3.9.7" -content-hash = "6ba83d170d58367d0888a0bdd320a5a348d49411cb4c4661af81c36e9cf23420" +content-hash = "b775339256b9eb6ae6ffc72cbe7d17d379897a3d72f762cb0df58e4044cded0c" diff --git a/pyproject.toml b/pyproject.toml index cc45c35cdac..77d80732b45 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -20,6 +20,7 @@ Documentation = "https://docs.litellm.ai" [tool.poetry.dependencies] python = ">=3.8.1,<4.0, !=3.9.7" +fastuuid = ">=0.13.0" httpx = ">=0.23.0" openai = ">=1.99.5" python-dotenv = ">=0.2.0" @@ -32,7 +33,6 @@ aiohttp = ">=3.10" pydantic = "^2.5.0" jsonschema = "^4.22.0" numpydoc = {version = "*", optional = true} # used in utils.py -fastuuid = {version = ">=0.12.0", optional = true} uvicorn = {version = "^0.29.0", optional = true} uvloop = {version = "^0.21.0", optional = true, markers="sys_platform != 'win32'"} @@ -92,7 +92,6 @@ proxy = [ "litellm-enterprise", "rich", "polars", - "fastuuid", ] extra_proxy = [ diff --git a/tests/test_litellm/test_uuid_fallback.py b/tests/test_litellm/test_uuid_helper.py similarity index 60% rename from tests/test_litellm/test_uuid_fallback.py rename to tests/test_litellm/test_uuid_helper.py index e18bdabda8a..589d3f96a17 100644 --- a/tests/test_litellm/test_uuid_fallback.py +++ b/tests/test_litellm/test_uuid_helper.py @@ -1,10 +1,13 @@ import importlib -def test_fastuuid_flag_exposed(): +def test_uses_fastuuid_and_uuid4_works(): mod = importlib.import_module("litellm._uuid") - assert hasattr(mod, "FASTUUID_AVAILABLE") + fastuuid_mod = importlib.import_module("fastuuid") assert hasattr(mod, "uuid4") + assert hasattr(mod, "uuid") + assert mod.uuid is fastuuid_mod + # Ensure uuid4 returns something that looks like a UUID string val = str(mod.uuid4()) assert isinstance(val, str) From 360befa216d9d15affefd90e00f8226adbf5f1ea Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Fri, 26 Sep 2025 09:51:38 -0700 Subject: [PATCH 61/70] [Feat] Add support for Gemini 2.5 Flash and Flash-lite preview models (09-2025 release) (#14948) * add gemini-2.5-flash-preview-09-2025 * docs add gemini-2.5-flash-preview-09-2025 model family --- docs/my-website/docs/providers/gemini.md | 4 + docs/my-website/docs/providers/vertex.md | 4 + ...odel_prices_and_context_window_backup.json | 372 +++++++++++++++++- model_prices_and_context_window.json | 368 +++++++++++++++++ 4 files changed, 746 insertions(+), 2 deletions(-) diff --git a/docs/my-website/docs/providers/gemini.md b/docs/my-website/docs/providers/gemini.md index 9376144cc85..40d64656528 100644 --- a/docs/my-website/docs/providers/gemini.md +++ b/docs/my-website/docs/providers/gemini.md @@ -1199,6 +1199,10 @@ response = litellm.completion( | gemini-2.0-flash | `completion(model='gemini/gemini-2.0-flash', messages)` | `os.environ['GEMINI_API_KEY']` | | gemini-2.0-flash-exp | `completion(model='gemini/gemini-2.0-flash-exp', messages)` | `os.environ['GEMINI_API_KEY']` | | gemini-2.0-flash-lite-preview-02-05 | `completion(model='gemini/gemini-2.0-flash-lite-preview-02-05', messages)` | `os.environ['GEMINI_API_KEY']` | +| gemini-2.5-flash-preview-09-2025 | `completion(model='gemini/gemini-2.5-flash-preview-09-2025', messages)` | `os.environ['GEMINI_API_KEY']` | +| gemini-2.5-flash-lite-preview-09-2025 | `completion(model='gemini/gemini-2.5-flash-lite-preview-09-2025', messages)` | `os.environ['GEMINI_API_KEY']` | +| gemini-flash-latest | `completion(model='gemini/gemini-flash-latest', messages)` | `os.environ['GEMINI_API_KEY']` | +| gemini-flash-lite-latest | `completion(model='gemini/gemini-flash-lite-latest', messages)` | `os.environ['GEMINI_API_KEY']` | diff --git a/docs/my-website/docs/providers/vertex.md b/docs/my-website/docs/providers/vertex.md index 78b6abd84af..f7cdf867af1 100644 --- a/docs/my-website/docs/providers/vertex.md +++ b/docs/my-website/docs/providers/vertex.md @@ -1297,6 +1297,10 @@ litellm.vertex_location = "us-central1 # Your Location | Model Name | Function Call | |------------------|--------------------------------------| | gemini-2.5-pro | `completion('gemini-2.5-pro', messages)`, `completion('vertex_ai/gemini-2.5-pro', messages)` | +| gemini-2.5-flash-preview-09-2025 | `completion('gemini-2.5-flash-preview-09-2025', messages)`, `completion('vertex_ai/gemini-2.5-flash-preview-09-2025', messages)` | +| gemini-2.5-flash-lite-preview-09-2025 | `completion('gemini-2.5-flash-lite-preview-09-2025', messages)`, `completion('vertex_ai/gemini-2.5-flash-lite-preview-09-2025', messages)` | +| gemini-flash-latest | `completion('gemini-flash-latest', messages)`, `completion('vertex_ai/gemini-flash-latest', messages)` | +| gemini-flash-lite-latest | `completion('gemini-flash-lite-latest', messages)`, `completion('vertex_ai/gemini-flash-lite-latest', messages)` | ## Fine-tuned Models diff --git a/litellm/model_prices_and_context_window_backup.json b/litellm/model_prices_and_context_window_backup.json index c3f312beb6a..48a88f7afcb 100644 --- a/litellm/model_prices_and_context_window_backup.json +++ b/litellm/model_prices_and_context_window_backup.json @@ -9306,6 +9306,186 @@ "supports_vision": true, "supports_web_search": true }, + "gemini-2.5-flash-lite-preview-09-2025": { + "cache_read_input_token_cost": 2.5e-08, + "input_cost_per_audio_token": 3e-07, + "input_cost_per_token": 1e-07, + "litellm_provider": "vertex_ai-language-models", + "max_audio_length_hours": 8.4, + "max_audio_per_prompt": 1, + "max_images_per_prompt": 3000, + "max_input_tokens": 1048576, + "max_output_tokens": 65535, + "max_pdf_size_mb": 30, + "max_tokens": 65535, + "max_video_length": 1, + "max_videos_per_prompt": 10, + "mode": "chat", + "output_cost_per_reasoning_token": 4e-07, + "output_cost_per_token": 4e-07, + "source": "https://developers.googleblog.com/en/continuing-to-bring-you-our-latest-models-with-an-improved-gemini-2-5-flash-and-flash-lite-release/", + "supported_endpoints": [ + "/v1/chat/completions", + "/v1/completions", + "/v1/batch" + ], + "supported_modalities": [ + "text", + "image", + "audio", + "video" + ], + "supported_output_modalities": [ + "text" + ], + "supports_audio_output": false, + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_url_context": true, + "supports_vision": true, + "supports_web_search": true + }, + "gemini-2.5-flash-preview-09-2025": { + "cache_read_input_token_cost": 7.5e-08, + "input_cost_per_audio_token": 1e-06, + "input_cost_per_token": 3e-07, + "litellm_provider": "vertex_ai-language-models", + "max_audio_length_hours": 8.4, + "max_audio_per_prompt": 1, + "max_images_per_prompt": 3000, + "max_input_tokens": 1048576, + "max_output_tokens": 65535, + "max_pdf_size_mb": 30, + "max_tokens": 65535, + "max_video_length": 1, + "max_videos_per_prompt": 10, + "mode": "chat", + "output_cost_per_reasoning_token": 2.5e-06, + "output_cost_per_token": 2.5e-06, + "source": "https://developers.googleblog.com/en/continuing-to-bring-you-our-latest-models-with-an-improved-gemini-2-5-flash-and-flash-lite-release/", + "supported_endpoints": [ + "/v1/chat/completions", + "/v1/completions", + "/v1/batch" + ], + "supported_modalities": [ + "text", + "image", + "audio", + "video" + ], + "supported_output_modalities": [ + "text" + ], + "supports_audio_output": false, + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_url_context": true, + "supports_vision": true, + "supports_web_search": true + }, + "gemini-flash-latest": { + "cache_read_input_token_cost": 7.5e-08, + "input_cost_per_audio_token": 1e-06, + "input_cost_per_token": 3e-07, + "litellm_provider": "vertex_ai-language-models", + "max_audio_length_hours": 8.4, + "max_audio_per_prompt": 1, + "max_images_per_prompt": 3000, + "max_input_tokens": 1048576, + "max_output_tokens": 65535, + "max_pdf_size_mb": 30, + "max_tokens": 65535, + "max_video_length": 1, + "max_videos_per_prompt": 10, + "mode": "chat", + "output_cost_per_reasoning_token": 2.5e-06, + "output_cost_per_token": 2.5e-06, + "source": "https://developers.googleblog.com/en/continuing-to-bring-you-our-latest-models-with-an-improved-gemini-2-5-flash-and-flash-lite-release/", + "supported_endpoints": [ + "/v1/chat/completions", + "/v1/completions", + "/v1/batch" + ], + "supported_modalities": [ + "text", + "image", + "audio", + "video" + ], + "supported_output_modalities": [ + "text" + ], + "supports_audio_output": false, + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_url_context": true, + "supports_vision": true, + "supports_web_search": true + }, + "gemini-flash-lite-latest": { + "cache_read_input_token_cost": 2.5e-08, + "input_cost_per_audio_token": 3e-07, + "input_cost_per_token": 1e-07, + "litellm_provider": "vertex_ai-language-models", + "max_audio_length_hours": 8.4, + "max_audio_per_prompt": 1, + "max_images_per_prompt": 3000, + "max_input_tokens": 1048576, + "max_output_tokens": 65535, + "max_pdf_size_mb": 30, + "max_tokens": 65535, + "max_video_length": 1, + "max_videos_per_prompt": 10, + "mode": "chat", + "output_cost_per_reasoning_token": 4e-07, + "output_cost_per_token": 4e-07, + "source": "https://developers.googleblog.com/en/continuing-to-bring-you-our-latest-models-with-an-improved-gemini-2-5-flash-and-flash-lite-release/", + "supported_endpoints": [ + "/v1/chat/completions", + "/v1/completions", + "/v1/batch" + ], + "supported_modalities": [ + "text", + "image", + "audio", + "video" + ], + "supported_output_modalities": [ + "text" + ], + "supports_audio_output": false, + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_url_context": true, + "supports_vision": true, + "supports_web_search": true + }, "gemini-2.5-flash-lite-preview-06-17": { "cache_read_input_token_cost": 2.5e-08, "input_cost_per_audio_token": 5e-07, @@ -10671,6 +10851,194 @@ "supports_web_search": true, "tpm": 250000 }, + "gemini/gemini-2.5-flash-lite-preview-09-2025": { + "cache_read_input_token_cost": 2.5e-08, + "input_cost_per_audio_token": 3e-07, + "input_cost_per_token": 1e-07, + "litellm_provider": "gemini", + "max_audio_length_hours": 8.4, + "max_audio_per_prompt": 1, + "max_images_per_prompt": 3000, + "max_input_tokens": 1048576, + "max_output_tokens": 65535, + "max_pdf_size_mb": 30, + "max_tokens": 65535, + "max_video_length": 1, + "max_videos_per_prompt": 10, + "mode": "chat", + "output_cost_per_reasoning_token": 4e-07, + "output_cost_per_token": 4e-07, + "rpm": 15, + "source": "https://developers.googleblog.com/en/continuing-to-bring-you-our-latest-models-with-an-improved-gemini-2-5-flash-and-flash-lite-release/", + "supported_endpoints": [ + "/v1/chat/completions", + "/v1/completions", + "/v1/batch" + ], + "supported_modalities": [ + "text", + "image", + "audio", + "video" + ], + "supported_output_modalities": [ + "text" + ], + "supports_audio_output": false, + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_url_context": true, + "supports_vision": true, + "supports_web_search": true, + "tpm": 250000 + }, + "gemini/gemini-2.5-flash-preview-09-2025": { + "cache_read_input_token_cost": 7.5e-08, + "input_cost_per_audio_token": 1e-06, + "input_cost_per_token": 3e-07, + "litellm_provider": "gemini", + "max_audio_length_hours": 8.4, + "max_audio_per_prompt": 1, + "max_images_per_prompt": 3000, + "max_input_tokens": 1048576, + "max_output_tokens": 65535, + "max_pdf_size_mb": 30, + "max_tokens": 65535, + "max_video_length": 1, + "max_videos_per_prompt": 10, + "mode": "chat", + "output_cost_per_reasoning_token": 2.5e-06, + "output_cost_per_token": 2.5e-06, + "rpm": 15, + "source": "https://developers.googleblog.com/en/continuing-to-bring-you-our-latest-models-with-an-improved-gemini-2-5-flash-and-flash-lite-release/", + "supported_endpoints": [ + "/v1/chat/completions", + "/v1/completions", + "/v1/batch" + ], + "supported_modalities": [ + "text", + "image", + "audio", + "video" + ], + "supported_output_modalities": [ + "text" + ], + "supports_audio_output": false, + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_url_context": true, + "supports_vision": true, + "supports_web_search": true, + "tpm": 250000 + }, + "gemini/gemini-flash-latest": { + "cache_read_input_token_cost": 7.5e-08, + "input_cost_per_audio_token": 1e-06, + "input_cost_per_token": 3e-07, + "litellm_provider": "gemini", + "max_audio_length_hours": 8.4, + "max_audio_per_prompt": 1, + "max_images_per_prompt": 3000, + "max_input_tokens": 1048576, + "max_output_tokens": 65535, + "max_pdf_size_mb": 30, + "max_tokens": 65535, + "max_video_length": 1, + "max_videos_per_prompt": 10, + "mode": "chat", + "output_cost_per_reasoning_token": 2.5e-06, + "output_cost_per_token": 2.5e-06, + "rpm": 15, + "source": "https://developers.googleblog.com/en/continuing-to-bring-you-our-latest-models-with-an-improved-gemini-2-5-flash-and-flash-lite-release/", + "supported_endpoints": [ + "/v1/chat/completions", + "/v1/completions", + "/v1/batch" + ], + "supported_modalities": [ + "text", + "image", + "audio", + "video" + ], + "supported_output_modalities": [ + "text" + ], + "supports_audio_output": false, + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_url_context": true, + "supports_vision": true, + "supports_web_search": true, + "tpm": 250000 + }, + "gemini/gemini-flash-lite-latest": { + "cache_read_input_token_cost": 2.5e-08, + "input_cost_per_audio_token": 3e-07, + "input_cost_per_token": 1e-07, + "litellm_provider": "gemini", + "max_audio_length_hours": 8.4, + "max_audio_per_prompt": 1, + "max_images_per_prompt": 3000, + "max_input_tokens": 1048576, + "max_output_tokens": 65535, + "max_pdf_size_mb": 30, + "max_tokens": 65535, + "max_video_length": 1, + "max_videos_per_prompt": 10, + "mode": "chat", + "output_cost_per_reasoning_token": 4e-07, + "output_cost_per_token": 4e-07, + "rpm": 15, + "source": "https://developers.googleblog.com/en/continuing-to-bring-you-our-latest-models-with-an-improved-gemini-2-5-flash-and-flash-lite-release/", + "supported_endpoints": [ + "/v1/chat/completions", + "/v1/completions", + "/v1/batch" + ], + "supported_modalities": [ + "text", + "image", + "audio", + "video" + ], + "supported_output_modalities": [ + "text" + ], + "supports_audio_output": false, + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_url_context": true, + "supports_vision": true, + "supports_web_search": true, + "tpm": 250000 + }, "gemini/gemini-2.5-flash-lite-preview-06-17": { "cache_read_input_token_cost": 2.5e-08, "input_cost_per_audio_token": 5e-07, @@ -11346,13 +11714,13 @@ ] }, "gpt-3.5-turbo": { - "input_cost_per_token": 1.5e-06, + "input_cost_per_token": 0.5e-06, "litellm_provider": "openai", "max_input_tokens": 16385, "max_output_tokens": 4096, "max_tokens": 4097, "mode": "chat", - "output_cost_per_token": 2e-06, + "output_cost_per_token": 1.5e-06, "supports_function_calling": true, "supports_prompt_caching": true, "supports_system_messages": true, diff --git a/model_prices_and_context_window.json b/model_prices_and_context_window.json index 755318159b0..48a88f7afcb 100644 --- a/model_prices_and_context_window.json +++ b/model_prices_and_context_window.json @@ -9306,6 +9306,186 @@ "supports_vision": true, "supports_web_search": true }, + "gemini-2.5-flash-lite-preview-09-2025": { + "cache_read_input_token_cost": 2.5e-08, + "input_cost_per_audio_token": 3e-07, + "input_cost_per_token": 1e-07, + "litellm_provider": "vertex_ai-language-models", + "max_audio_length_hours": 8.4, + "max_audio_per_prompt": 1, + "max_images_per_prompt": 3000, + "max_input_tokens": 1048576, + "max_output_tokens": 65535, + "max_pdf_size_mb": 30, + "max_tokens": 65535, + "max_video_length": 1, + "max_videos_per_prompt": 10, + "mode": "chat", + "output_cost_per_reasoning_token": 4e-07, + "output_cost_per_token": 4e-07, + "source": "https://developers.googleblog.com/en/continuing-to-bring-you-our-latest-models-with-an-improved-gemini-2-5-flash-and-flash-lite-release/", + "supported_endpoints": [ + "/v1/chat/completions", + "/v1/completions", + "/v1/batch" + ], + "supported_modalities": [ + "text", + "image", + "audio", + "video" + ], + "supported_output_modalities": [ + "text" + ], + "supports_audio_output": false, + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_url_context": true, + "supports_vision": true, + "supports_web_search": true + }, + "gemini-2.5-flash-preview-09-2025": { + "cache_read_input_token_cost": 7.5e-08, + "input_cost_per_audio_token": 1e-06, + "input_cost_per_token": 3e-07, + "litellm_provider": "vertex_ai-language-models", + "max_audio_length_hours": 8.4, + "max_audio_per_prompt": 1, + "max_images_per_prompt": 3000, + "max_input_tokens": 1048576, + "max_output_tokens": 65535, + "max_pdf_size_mb": 30, + "max_tokens": 65535, + "max_video_length": 1, + "max_videos_per_prompt": 10, + "mode": "chat", + "output_cost_per_reasoning_token": 2.5e-06, + "output_cost_per_token": 2.5e-06, + "source": "https://developers.googleblog.com/en/continuing-to-bring-you-our-latest-models-with-an-improved-gemini-2-5-flash-and-flash-lite-release/", + "supported_endpoints": [ + "/v1/chat/completions", + "/v1/completions", + "/v1/batch" + ], + "supported_modalities": [ + "text", + "image", + "audio", + "video" + ], + "supported_output_modalities": [ + "text" + ], + "supports_audio_output": false, + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_url_context": true, + "supports_vision": true, + "supports_web_search": true + }, + "gemini-flash-latest": { + "cache_read_input_token_cost": 7.5e-08, + "input_cost_per_audio_token": 1e-06, + "input_cost_per_token": 3e-07, + "litellm_provider": "vertex_ai-language-models", + "max_audio_length_hours": 8.4, + "max_audio_per_prompt": 1, + "max_images_per_prompt": 3000, + "max_input_tokens": 1048576, + "max_output_tokens": 65535, + "max_pdf_size_mb": 30, + "max_tokens": 65535, + "max_video_length": 1, + "max_videos_per_prompt": 10, + "mode": "chat", + "output_cost_per_reasoning_token": 2.5e-06, + "output_cost_per_token": 2.5e-06, + "source": "https://developers.googleblog.com/en/continuing-to-bring-you-our-latest-models-with-an-improved-gemini-2-5-flash-and-flash-lite-release/", + "supported_endpoints": [ + "/v1/chat/completions", + "/v1/completions", + "/v1/batch" + ], + "supported_modalities": [ + "text", + "image", + "audio", + "video" + ], + "supported_output_modalities": [ + "text" + ], + "supports_audio_output": false, + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_url_context": true, + "supports_vision": true, + "supports_web_search": true + }, + "gemini-flash-lite-latest": { + "cache_read_input_token_cost": 2.5e-08, + "input_cost_per_audio_token": 3e-07, + "input_cost_per_token": 1e-07, + "litellm_provider": "vertex_ai-language-models", + "max_audio_length_hours": 8.4, + "max_audio_per_prompt": 1, + "max_images_per_prompt": 3000, + "max_input_tokens": 1048576, + "max_output_tokens": 65535, + "max_pdf_size_mb": 30, + "max_tokens": 65535, + "max_video_length": 1, + "max_videos_per_prompt": 10, + "mode": "chat", + "output_cost_per_reasoning_token": 4e-07, + "output_cost_per_token": 4e-07, + "source": "https://developers.googleblog.com/en/continuing-to-bring-you-our-latest-models-with-an-improved-gemini-2-5-flash-and-flash-lite-release/", + "supported_endpoints": [ + "/v1/chat/completions", + "/v1/completions", + "/v1/batch" + ], + "supported_modalities": [ + "text", + "image", + "audio", + "video" + ], + "supported_output_modalities": [ + "text" + ], + "supports_audio_output": false, + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_url_context": true, + "supports_vision": true, + "supports_web_search": true + }, "gemini-2.5-flash-lite-preview-06-17": { "cache_read_input_token_cost": 2.5e-08, "input_cost_per_audio_token": 5e-07, @@ -10671,6 +10851,194 @@ "supports_web_search": true, "tpm": 250000 }, + "gemini/gemini-2.5-flash-lite-preview-09-2025": { + "cache_read_input_token_cost": 2.5e-08, + "input_cost_per_audio_token": 3e-07, + "input_cost_per_token": 1e-07, + "litellm_provider": "gemini", + "max_audio_length_hours": 8.4, + "max_audio_per_prompt": 1, + "max_images_per_prompt": 3000, + "max_input_tokens": 1048576, + "max_output_tokens": 65535, + "max_pdf_size_mb": 30, + "max_tokens": 65535, + "max_video_length": 1, + "max_videos_per_prompt": 10, + "mode": "chat", + "output_cost_per_reasoning_token": 4e-07, + "output_cost_per_token": 4e-07, + "rpm": 15, + "source": "https://developers.googleblog.com/en/continuing-to-bring-you-our-latest-models-with-an-improved-gemini-2-5-flash-and-flash-lite-release/", + "supported_endpoints": [ + "/v1/chat/completions", + "/v1/completions", + "/v1/batch" + ], + "supported_modalities": [ + "text", + "image", + "audio", + "video" + ], + "supported_output_modalities": [ + "text" + ], + "supports_audio_output": false, + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_url_context": true, + "supports_vision": true, + "supports_web_search": true, + "tpm": 250000 + }, + "gemini/gemini-2.5-flash-preview-09-2025": { + "cache_read_input_token_cost": 7.5e-08, + "input_cost_per_audio_token": 1e-06, + "input_cost_per_token": 3e-07, + "litellm_provider": "gemini", + "max_audio_length_hours": 8.4, + "max_audio_per_prompt": 1, + "max_images_per_prompt": 3000, + "max_input_tokens": 1048576, + "max_output_tokens": 65535, + "max_pdf_size_mb": 30, + "max_tokens": 65535, + "max_video_length": 1, + "max_videos_per_prompt": 10, + "mode": "chat", + "output_cost_per_reasoning_token": 2.5e-06, + "output_cost_per_token": 2.5e-06, + "rpm": 15, + "source": "https://developers.googleblog.com/en/continuing-to-bring-you-our-latest-models-with-an-improved-gemini-2-5-flash-and-flash-lite-release/", + "supported_endpoints": [ + "/v1/chat/completions", + "/v1/completions", + "/v1/batch" + ], + "supported_modalities": [ + "text", + "image", + "audio", + "video" + ], + "supported_output_modalities": [ + "text" + ], + "supports_audio_output": false, + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_url_context": true, + "supports_vision": true, + "supports_web_search": true, + "tpm": 250000 + }, + "gemini/gemini-flash-latest": { + "cache_read_input_token_cost": 7.5e-08, + "input_cost_per_audio_token": 1e-06, + "input_cost_per_token": 3e-07, + "litellm_provider": "gemini", + "max_audio_length_hours": 8.4, + "max_audio_per_prompt": 1, + "max_images_per_prompt": 3000, + "max_input_tokens": 1048576, + "max_output_tokens": 65535, + "max_pdf_size_mb": 30, + "max_tokens": 65535, + "max_video_length": 1, + "max_videos_per_prompt": 10, + "mode": "chat", + "output_cost_per_reasoning_token": 2.5e-06, + "output_cost_per_token": 2.5e-06, + "rpm": 15, + "source": "https://developers.googleblog.com/en/continuing-to-bring-you-our-latest-models-with-an-improved-gemini-2-5-flash-and-flash-lite-release/", + "supported_endpoints": [ + "/v1/chat/completions", + "/v1/completions", + "/v1/batch" + ], + "supported_modalities": [ + "text", + "image", + "audio", + "video" + ], + "supported_output_modalities": [ + "text" + ], + "supports_audio_output": false, + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_url_context": true, + "supports_vision": true, + "supports_web_search": true, + "tpm": 250000 + }, + "gemini/gemini-flash-lite-latest": { + "cache_read_input_token_cost": 2.5e-08, + "input_cost_per_audio_token": 3e-07, + "input_cost_per_token": 1e-07, + "litellm_provider": "gemini", + "max_audio_length_hours": 8.4, + "max_audio_per_prompt": 1, + "max_images_per_prompt": 3000, + "max_input_tokens": 1048576, + "max_output_tokens": 65535, + "max_pdf_size_mb": 30, + "max_tokens": 65535, + "max_video_length": 1, + "max_videos_per_prompt": 10, + "mode": "chat", + "output_cost_per_reasoning_token": 4e-07, + "output_cost_per_token": 4e-07, + "rpm": 15, + "source": "https://developers.googleblog.com/en/continuing-to-bring-you-our-latest-models-with-an-improved-gemini-2-5-flash-and-flash-lite-release/", + "supported_endpoints": [ + "/v1/chat/completions", + "/v1/completions", + "/v1/batch" + ], + "supported_modalities": [ + "text", + "image", + "audio", + "video" + ], + "supported_output_modalities": [ + "text" + ], + "supports_audio_output": false, + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_url_context": true, + "supports_vision": true, + "supports_web_search": true, + "tpm": 250000 + }, "gemini/gemini-2.5-flash-lite-preview-06-17": { "cache_read_input_token_cost": 2.5e-08, "input_cost_per_audio_token": 5e-07, From 6fe8c33448e20aa4d5e34ce604c64418126f838c Mon Sep 17 00:00:00 2001 From: Olivier Cornelis <26625900+thiswillbeyourgithub@users.noreply.github.com> Date: Fri, 26 Sep 2025 19:32:30 +0200 Subject: [PATCH 62/70] docs: add documentation for additional cost-related keys in custom pricing (#14949) Co-authored-by: aider (openrouter/anthropic/claude-sonnet-4) --- docs/my-website/docs/proxy/custom_pricing.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/docs/my-website/docs/proxy/custom_pricing.md b/docs/my-website/docs/proxy/custom_pricing.md index e2df7721bfb..fc7312b92ac 100644 --- a/docs/my-website/docs/proxy/custom_pricing.md +++ b/docs/my-website/docs/proxy/custom_pricing.md @@ -83,6 +83,24 @@ model_list: cache_read_input_token_cost: 0.0000006 ``` +### Additional Cost Keys + +There are other keys you can use to specify costs for different scenarios and modalities: + +- `input_cost_per_token_above_200k_tokens` - Cost for input tokens when context exceeds 200k tokens +- `output_cost_per_token_above_200k_tokens` - Cost for output tokens when context exceeds 200k tokens +- `cache_creation_input_token_cost_above_200k_tokens` - Cache creation cost for large contexts +- `cache_read_input_token_cost_above_200k_token` - Cache read cost for large contexts +- `input_cost_per_image` - Cost per image in multimodal requests +- `output_cost_per_reasoning_token` - Cost for reasoning tokens (e.g., OpenAI o1 models) +- `input_cost_per_audio_token` - Cost for audio input tokens +- `output_cost_per_audio_token` - Cost for audio output tokens +- `input_cost_per_video_per_second` - Cost per second of video input +- `input_cost_per_video_per_second_above_128k_tokens` - Video cost for large contexts +- `input_cost_per_character` - Character-based pricing for some providers + +These keys evolve based on how new models handle multimodality. The latest version can be found at [https://github.com/BerriAI/litellm/blob/main/model_prices_and_context_window.json](https://github.com/BerriAI/litellm/blob/main/model_prices_and_context_window.json). + ## Set 'base_model' for Cost Tracking (e.g. Azure deployments) **Problem**: Azure returns `gpt-4` in the response when `azure/gpt-4-1106-preview` is used. This leads to inaccurate cost tracking From d04c6d4eea26db97d112eb55f33674238df5a968 Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Fri, 26 Sep 2025 11:04:11 -0700 Subject: [PATCH 63/70] [Feat] Add new anthropic web fetch tool support (#14951) * add web_fetch tool for ANTHROPIC_HOSTED_TOOLS * add ANTHROPIC_BETA_HEADER_VALUES, ANTHROPIC_HOSTED_TOOLS * feat: add web fetch tool anthropic * test_anthropic_tool_use * docs web fetch * docs fix * docs fix --- docs/my-website/docs/completion/web_fetch.md | 294 ++++++++++++++++++ docs/my-website/docs/completion/web_search.md | 2 +- docs/my-website/docs/providers/vertex.md | 2 +- .../docs/proxy/native_litellm_prompt.md | 8 +- docs/my-website/sidebars.js | 15 +- litellm/llms/anthropic/chat/transformation.py | 20 +- litellm/types/llms/anthropic.py | 14 + .../test_anthropic_completion.py | 51 ++- 8 files changed, 374 insertions(+), 32 deletions(-) create mode 100644 docs/my-website/docs/completion/web_fetch.md diff --git a/docs/my-website/docs/completion/web_fetch.md b/docs/my-website/docs/completion/web_fetch.md new file mode 100644 index 00000000000..30a15e44495 --- /dev/null +++ b/docs/my-website/docs/completion/web_fetch.md @@ -0,0 +1,294 @@ +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +# Web Fetch + +The web fetch tool allows LLMs to retrieve full content from specified web pages and PDF documents. This enables AI models to access real-time information from the internet and incorporate web content into their responses. + +## Web Fetch vs Web Search + +**Web Fetch** retrieves the full content from specific web pages that you provide URLs for, while **Web Search** performs internet searches to find relevant information based on your queries. + +| Feature | Web Fetch | Web Search | +|---------|-----------|------------| +| **Purpose** | Retrieve content from specific URLs | Search the internet for information | +| **Input** | You provide exact URLs to fetch | You provide search queries/questions | +| **Output** | Full page content from specified URLs | Search results with relevant information | +| **Use Cases** | - Analyzing specific articles
- Comparing content from known websites
- Extracting data from particular pages | - Finding current news/events
- Researching topics
- Getting real-time information | + + +**Example Web Fetch**: "Fetch the content from https://example.com/pricing and summarize it" +**Example Web Search**: "What are the latest AI developments this week?" + +**Supported Providers:** +- Anthropic API (`anthropic/`) + +**Supported Tool Types:** +- `web_fetch_20250910` - Web content retrieval tool with usage limits, domain filtering, and citation support + + +## Quick Start + +### LiteLLM Python SDK + +```python +import os +from litellm import completion + +os.environ["ANTHROPIC_API_KEY"] = "your-api-key" + +# Web fetch tool +tools = [ + { + "type": "web_fetch_20250910", + "name": "web_fetch", + "max_uses": 5, + } +] + +messages = [ + { + "role": "user", + "content": "Please analyze the content at https://example.com/article and summarize the main points" + } +] + +response = completion( + model="anthropic/claude-3-5-sonnet-latest", + messages=messages, + tools=tools, +) + +print(response) +``` + +### LiteLLM Proxy + +1. Define web fetch models on config.yaml + +```yaml +model_list: + - model_name: claude-3-5-sonnet-latest # Anthropic claude-3-5-sonnet-latest + litellm_params: + model: anthropic/claude-3-5-sonnet-latest + api_key: os.environ/ANTHROPIC_API_KEY +``` + +2. Run proxy server + +```bash +litellm --config config.yaml +``` + +3. Test it using the OpenAI Python SDK + +```python +import os +from openai import OpenAI + +client = OpenAI( + api_key="sk-1234", # your litellm proxy api key + base_url="http://0.0.0.0:4000" +) + +response = client.chat.completions.create( + model="claude-3-5-sonnet-latest", + messages=[ + { + "role": "user", + "content": "Please fetch and analyze the content from https://news.ycombinator.com and tell me about the top stories" + } + ], + tools=[ + { + "type": "web_fetch_20250910", + "name": "web_fetch", + "max_uses": 5, + } + ] +) + +print(response) +``` + +## Supported Models + +Web fetch is available on the following Anthropic API models: + +- `claude-opus-4-1-20250805` (Claude Opus 4.1) +- `claude-opus-4-20250514` (Claude Opus 4) +- `claude-sonnet-4-20250514` (Claude Sonnet 4) +- `claude-3-7-sonnet-20250219` (Claude Sonnet 3.7) +- `claude-3-5-sonnet-latest` (Claude Sonnet 3.5 v2 - deprecated) +- `claude-3-5-haiku-latest` (Claude Haiku 3.5) + +:::note +The web fetch tool currently does not support websites dynamically rendered via JavaScript. +::: + +## Usage Examples + +### Basic Web Content Retrieval + +```python +import os +from litellm import completion + +os.environ["ANTHROPIC_API_KEY"] = "your-api-key" + +tools = [ + { + "type": "web_fetch_20250910", + "name": "web_fetch", + "max_uses": 3, + } +] + +messages = [ + { + "role": "user", + "content": "Fetch the latest news from https://techcrunch.com and summarize the top 3 articles" + } +] + +response = completion( + model="anthropic/claude-3-5-sonnet-latest", + messages=messages, + tools=tools, +) + +print(response) +``` + +### Research and Analysis + +```python +import os +from litellm import completion + +os.environ["ANTHROPIC_API_KEY"] = "your-api-key" + +tools = [ + { + "type": "web_fetch_20250910", + "name": "web_fetch", + "max_uses": 10, + } +] + +messages = [ + { + "role": "user", + "content": "Research the latest developments in AI by fetching content from multiple tech news websites and provide a comprehensive analysis" + } +] + +response = completion( + model="anthropic/claude-3-5-sonnet-latest", + messages=messages, + tools=tools, +) + +print(response) +``` + +### Content Comparison + +```python +import os +from litellm import completion + +os.environ["ANTHROPIC_API_KEY"] = "your-api-key" + +tools = [ + { + "type": "web_fetch_20250910", + "name": "web_fetch", + "max_uses": 5, + } +] + +messages = [ + { + "role": "user", + "content": "Compare the pricing information from https://openai.com/pricing and https://anthropic.com/pricing and create a comparison table" + } +] + +response = completion( + model="anthropic/claude-3-5-sonnet-latest", + messages=messages, + tools=tools, +) + +print(response) +``` + +## Advanced Usage with Multiple Tools + +You can combine web fetch with other tools like computer use or text editor: + +```python +import os +from litellm import completion + +os.environ["ANTHROPIC_API_KEY"] = "your-api-key" + +tools = [ + { + "type": "web_fetch_20250910", + "name": "web_fetch", + "max_uses": 5, + }, + { + "type": "text_editor_20250124", + "name": "str_replace_editor" + } +] + +messages = [ + { + "role": "user", + "content": "Fetch the latest AI research papers from arXiv, analyze them, and create a detailed report file with your findings" + } +] + +response = completion( + model="anthropic/claude-3-5-sonnet-latest", + messages=messages, + tools=tools, +) + +print(response) +``` + +## Spec + +### Web Fetch Tool (`web_fetch_20250910`) + +The web fetch tool supports the following parameters: + +```json +{ + "type": "web_fetch_20250910", + "name": "web_fetch", + + // Optional: Limit the number of fetches per request + "max_uses": 10, + + // Optional: Only fetch from these domains + "allowed_domains": ["example.com", "docs.example.com"], + + // Optional: Never fetch from these domains + "blocked_domains": ["private.example.com"], + + // Optional: Enable citations for fetched content + "citations": { + "enabled": true + }, + + // Optional: Maximum content length in tokens + "max_content_tokens": 100000 +} +``` + diff --git a/docs/my-website/docs/completion/web_search.md b/docs/my-website/docs/completion/web_search.md index 262e3fc4f9c..b0d8fcdf4c0 100644 --- a/docs/my-website/docs/completion/web_search.md +++ b/docs/my-website/docs/completion/web_search.md @@ -1,7 +1,7 @@ import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; -# Using Web Search +# Web Search Use web search with litellm diff --git a/docs/my-website/docs/providers/vertex.md b/docs/my-website/docs/providers/vertex.md index f7cdf867af1..9a969876432 100644 --- a/docs/my-website/docs/providers/vertex.md +++ b/docs/my-website/docs/providers/vertex.md @@ -898,7 +898,7 @@ curl http://0.0.0.0:4000/chat/completions \ ```
- +
## Pre-requisites * `pip install google-cloud-aiplatform` (pre-installed on proxy docker image) diff --git a/docs/my-website/docs/proxy/native_litellm_prompt.md b/docs/my-website/docs/proxy/native_litellm_prompt.md index 0a4f1ccf35c..ea326d00690 100644 --- a/docs/my-website/docs/proxy/native_litellm_prompt.md +++ b/docs/my-website/docs/proxy/native_litellm_prompt.md @@ -1,9 +1,5 @@ -import '@theme/IdealImage' -import '@theme/TabItem' -import '@theme/Tabs' -import Image -import TabItem -import Tabs +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; # LiteLLM Prompt Management (GitOps) diff --git a/docs/my-website/sidebars.js b/docs/my-website/sidebars.js index 3750915b191..cb68e9024d6 100644 --- a/docs/my-website/sidebars.js +++ b/docs/my-website/sidebars.js @@ -518,12 +518,19 @@ const sidebars = { type: "category", label: "Guides", items: [ + { + type: "category", + label: "Tools", + items: [ + "completion/computer_use", + "completion/web_search", + "completion/web_fetch", + "completion/function_call", + ] + }, "completion/audio", - "completion/batching", - "completion/computer_use", "completion/document_understanding", "completion/drop_params", - "completion/function_call", "completion/image_generation_chat", "completion/json_mode", "completion/knowledgebase", @@ -538,8 +545,8 @@ const sidebars = { "completion/stream", "completion/provider_specific_params", "completion/vision", - "completion/web_search", "exception_mapping", + "completion/batching", "guides/finetuned_models", "guides/security_settings", "proxy/veo_video_generation", diff --git a/litellm/llms/anthropic/chat/transformation.py b/litellm/llms/anthropic/chat/transformation.py index 6d41655674c..c7f0a6da61e 100644 --- a/litellm/llms/anthropic/chat/transformation.py +++ b/litellm/llms/anthropic/chat/transformation.py @@ -18,6 +18,8 @@ from litellm.litellm_core_utils.core_helpers import map_finish_reason from litellm.llms.base_llm.base_utils import type_to_response_format_param from litellm.llms.base_llm.chat.transformation import BaseConfig, BaseLLMException from litellm.types.llms.anthropic import ( + ANTHROPIC_BETA_HEADER_VALUES, + ANTHROPIC_HOSTED_TOOLS, AllAnthropicMessageValues, AllAnthropicToolsValues, AnthropicCodeExecutionTool, @@ -50,7 +52,10 @@ from litellm.types.utils import ( CompletionTokensDetailsWrapper, ) from litellm.types.utils import Message as LitellmMessage -from litellm.types.utils import PromptTokensDetailsWrapper, ServerToolUse +from litellm.types.utils import ( + PromptTokensDetailsWrapper, + ServerToolUse, +) from litellm.utils import ( ModelResponse, Usage, @@ -70,9 +75,6 @@ else: LoggingClass = Any -ANTHROPIC_HOSTED_TOOLS = ["web_search", "bash", "text_editor", "code_execution"] - - class AnthropicConfig(AnthropicModelInfo, BaseConfig): """ Reference: https://docs.anthropic.com/claude/reference/messages_post @@ -639,6 +641,14 @@ class AnthropicConfig(AnthropicModelInfo, BaseConfig): ) ) return tools + + def update_headers_with_optional_anthropic_beta(self, headers: dict, optional_params: dict) -> dict: + """Update headers with optional anthropic beta.""" + _tools = optional_params.get("tools", []) + for tool in _tools: + if tool.get("type", None) and tool.get("type").startswith(ANTHROPIC_HOSTED_TOOLS.WEB_FETCH.value): + headers["anthropic-beta"] = ANTHROPIC_BETA_HEADER_VALUES.WEB_FETCH_2025_09_10.value + return headers def transform_request( self, @@ -675,6 +685,8 @@ class AnthropicConfig(AnthropicModelInfo, BaseConfig): llm_provider="anthropic", ) + headers = self.update_headers_with_optional_anthropic_beta(headers=headers, optional_params=optional_params) + # Separate system prompt from rest of message anthropic_system_message_list = self.translate_system_message(messages=messages) # Handling anthropic API Prompt Caching diff --git a/litellm/types/llms/anthropic.py b/litellm/types/llms/anthropic.py index 837f4bf9a8f..02c6f2cf8cf 100644 --- a/litellm/types/llms/anthropic.py +++ b/litellm/types/llms/anthropic.py @@ -1,3 +1,4 @@ +from enum import Enum from typing import Any, Dict, Iterable, List, Optional, Union from pydantic import BaseModel, validator @@ -440,3 +441,16 @@ ANTHROPIC_API_ONLY_HEADERS = { # fails if calling anthropic on vertex ai / bedr class AnthropicThinkingParam(TypedDict, total=False): type: Literal["enabled"] budget_tokens: int + +class ANTHROPIC_HOSTED_TOOLS(str, Enum): + WEB_SEARCH = "web_search" + BASH = "bash" + TEXT_EDITOR = "text_editor" + CODE_EXECUTION = "code_execution" + WEB_FETCH = "web_fetch" + +class ANTHROPIC_BETA_HEADER_VALUES(str, Enum): + """ + Known beta header values for Anthropic. + """ + WEB_FETCH_2025_09_10 = "web-fetch-2025-09-10" \ No newline at end of file diff --git a/tests/llm_translation/test_anthropic_completion.py b/tests/llm_translation/test_anthropic_completion.py index 307c429fc62..4eeb80c4191 100644 --- a/tests/llm_translation/test_anthropic_completion.py +++ b/tests/llm_translation/test_anthropic_completion.py @@ -329,32 +329,51 @@ def test_process_anthropic_headers_with_no_matching_headers(): assert result == expected_output, "Unexpected output for non-matching headers" -def test_anthropic_computer_tool_use(): - from litellm import completion - - tools = [ - { - "type": "computer_20241022", - "function": { - "name": "computer", - "parameters": { - "display_height_px": 100, - "display_width_px": 100, - "display_number": 1, +@pytest.mark.parametrize( + "tool_type, tool_config, message_content", + [ + ( + "computer_20241022", + { + "type": "computer_20241022", + "function": { + "name": "computer", + "parameters": { + "display_height_px": 100, + "display_width_px": 100, + "display_number": 1, + }, }, }, - } - ] + "Save a picture of a cat to my desktop.", + ), + ( + "web_fetch_20250910", + { + "type": "web_fetch_20250910", + "name": "web_fetch", + "max_uses": 5, + }, + "Please analyze the content at https://example.com/article", + ), + ], +) +def test_anthropic_tool_use(tool_type, tool_config, message_content): + """Test Anthropic tool use with computer use and web fetch tools.""" + from litellm import completion + litellm._turn_on_debug() + + tools = [tool_config] model = "claude-3-5-sonnet-20241022" - messages = [{"role": "user", "content": "Save a picture of a cat to my desktop."}] + messages = [{"role": "user", "content": message_content}] try: resp = completion( model=model, messages=messages, tools=tools, - # headers={"anthropic-beta": "computer-use-2024-10-22"}, ) + print(f"Tool type: {tool_type}") print(resp) except litellm.InternalServerError: pass From 539d10f9cb7d05912b21fe9f86eba949c2e1d8cf Mon Sep 17 00:00:00 2001 From: Ishaan Jaffer Date: Fri, 26 Sep 2025 11:06:26 -0700 Subject: [PATCH 64/70] test fix --- .../proxy/client/cli/test_auth_commands.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/tests/test_litellm/proxy/client/cli/test_auth_commands.py b/tests/test_litellm/proxy/client/cli/test_auth_commands.py index 0ea81c70eb1..fa4ec0e98d3 100644 --- a/tests/test_litellm/proxy/client/cli/test_auth_commands.py +++ b/tests/test_litellm/proxy/client/cli/test_auth_commands.py @@ -204,7 +204,7 @@ class TestLoginCommand: patch('requests.get', return_value=mock_response) as mock_get, \ patch('litellm.proxy.client.cli.commands.auth.save_token') as mock_save, \ patch('litellm.proxy.client.cli.interface.show_commands') as mock_show_commands, \ - patch('uuid.uuid4', return_value='test-uuid-123'): + patch('litellm._uuid.uuid.uuid4', return_value='test-uuid-123'): result = self.runner.invoke(login, obj=mock_context.obj) @@ -240,7 +240,7 @@ class TestLoginCommand: with patch('webbrowser.open'), \ patch('requests.get', return_value=mock_response), \ patch('time.sleep') as mock_sleep, \ - patch('uuid.uuid4', return_value='test-uuid-123'): + patch('litellm._uuid.uuid.uuid4', return_value='test-uuid-123'): # Mock time.sleep to avoid actual delays in tests result = self.runner.invoke(login, obj=mock_context.obj) @@ -260,7 +260,7 @@ class TestLoginCommand: with patch('webbrowser.open'), \ patch('requests.get', return_value=mock_response), \ patch('time.sleep'), \ - patch('uuid.uuid4', return_value='test-uuid-123'): + patch('litellm._uuid.uuid.uuid4', return_value='test-uuid-123'): result = self.runner.invoke(login, obj=mock_context.obj) @@ -276,7 +276,7 @@ class TestLoginCommand: with patch('webbrowser.open'), \ patch('requests.get', side_effect=requests.RequestException("Connection failed")), \ patch('time.sleep'), \ - patch('uuid.uuid4', return_value='test-uuid-123'): + patch('litellm._uuid.uuid.uuid4', return_value='test-uuid-123'): result = self.runner.invoke(login, obj=mock_context.obj) @@ -290,7 +290,7 @@ class TestLoginCommand: with patch('webbrowser.open'), \ patch('requests.get', side_effect=KeyboardInterrupt), \ - patch('uuid.uuid4', return_value='test-uuid-123'): + patch('litellm._uuid.uuid.uuid4', return_value='test-uuid-123'): result = self.runner.invoke(login, obj=mock_context.obj) @@ -313,7 +313,7 @@ class TestLoginCommand: with patch('webbrowser.open'), \ patch('requests.get', return_value=mock_response), \ patch('time.sleep'), \ - patch('uuid.uuid4', return_value='test-uuid-123'): + patch('litellm._uuid.uuid.uuid4', return_value='test-uuid-123'): result = self.runner.invoke(login, obj=mock_context.obj) @@ -327,7 +327,7 @@ class TestLoginCommand: with patch('webbrowser.open'), \ patch('requests.get', side_effect=ValueError("Invalid value")), \ - patch('uuid.uuid4', return_value='test-uuid-123'): + patch('litellm._uuid.uuid.uuid4', return_value='test-uuid-123'): result = self.runner.invoke(login, obj=mock_context.obj) @@ -465,7 +465,7 @@ class TestCLIKeyRegenerationFlow: patch('litellm.proxy.client.cli.commands.auth.get_stored_api_key', return_value=existing_key) as mock_get_stored, \ patch('litellm.proxy.client.cli.commands.auth.save_token') as mock_save, \ patch('litellm.proxy.client.cli.interface.show_commands') as mock_show_commands, \ - patch('uuid.uuid4', return_value='new-session-uuid-789'): + patch('litellm._uuid.uuid.uuid4', return_value='new-session-uuid-789'): result = self.runner.invoke(login, obj=mock_context.obj) @@ -517,7 +517,7 @@ class TestCLIKeyRegenerationFlow: patch('litellm.proxy.client.cli.commands.auth.get_stored_api_key', return_value=None) as mock_get_stored, \ patch('litellm.proxy.client.cli.commands.auth.save_token') as mock_save, \ patch('litellm.proxy.client.cli.interface.show_commands'), \ - patch('uuid.uuid4', return_value='new-session-uuid-999'): + patch('litellm._uuid.uuid.uuid4', return_value='new-session-uuid-999'): result = self.runner.invoke(login, obj=mock_context.obj) From 50f625433de80aef2a9acc1386fbe840d070b9c2 Mon Sep 17 00:00:00 2001 From: Nicolas Herment Date: Fri, 26 Sep 2025 20:19:52 +0200 Subject: [PATCH 65/70] Revert incorrect changes to sonnet-4 max output tokens (#14933) --- ...odel_prices_and_context_window_backup.json | 28 ++++++++-------- model_prices_and_context_window.json | 32 +++++++++---------- 2 files changed, 30 insertions(+), 30 deletions(-) diff --git a/litellm/model_prices_and_context_window_backup.json b/litellm/model_prices_and_context_window_backup.json index 48a88f7afcb..e5cc96ea528 100644 --- a/litellm/model_prices_and_context_window_backup.json +++ b/litellm/model_prices_and_context_window_backup.json @@ -566,8 +566,8 @@ "cache_read_input_token_cost_above_200k_tokens": 6e-07, "litellm_provider": "bedrock_converse", "max_input_tokens": 1000000, - "max_output_tokens": 1000000, - "max_tokens": 1000000, + "max_output_tokens": 64000, + "max_tokens": 64000, "mode": "chat", "output_cost_per_token": 1.5e-05, "search_context_cost_per_query": { @@ -834,8 +834,8 @@ "cache_read_input_token_cost_above_200k_tokens": 6e-07, "litellm_provider": "bedrock_converse", "max_input_tokens": 1000000, - "max_output_tokens": 1000000, - "max_tokens": 1000000, + "max_output_tokens": 64000, + "max_tokens": 64000, "mode": "chat", "output_cost_per_token": 1.5e-05, "search_context_cost_per_query": { @@ -4831,8 +4831,8 @@ "cache_read_input_token_cost_above_200k_tokens": 6e-07, "litellm_provider": "anthropic", "max_input_tokens": 1000000, - "max_output_tokens": 1000000, - "max_tokens": 1000000, + "max_output_tokens": 64000, + "max_tokens": 64000, "mode": "chat", "output_cost_per_token": 1.5e-05, "search_context_cost_per_query": { @@ -7650,8 +7650,8 @@ "cache_read_input_token_cost_above_200k_tokens": 6e-07, "litellm_provider": "bedrock_converse", "max_input_tokens": 1000000, - "max_output_tokens": 1000000, - "max_tokens": 1000000, + "max_output_tokens": 64000, + "max_tokens": 64000, "mode": "chat", "output_cost_per_token": 1.5e-05, "search_context_cost_per_query": { @@ -19639,8 +19639,8 @@ "cache_read_input_token_cost_above_200k_tokens": 6e-07, "litellm_provider": "bedrock_converse", "max_input_tokens": 1000000, - "max_output_tokens": 1000000, - "max_tokens": 1000000, + "max_output_tokens": 64000, + "max_tokens": 64000, "mode": "chat", "output_cost_per_token": 1.5e-05, "search_context_cost_per_query": { @@ -20979,8 +20979,8 @@ "cache_read_input_token_cost_above_200k_tokens": 6e-07, "litellm_provider": "vertex_ai-anthropic_models", "max_input_tokens": 1000000, - "max_output_tokens": 1000000, - "max_tokens": 1000000, + "max_output_tokens": 64000, + "max_tokens": 64000, "mode": "chat", "output_cost_per_token": 1.5e-05, "search_context_cost_per_query": { @@ -21009,8 +21009,8 @@ "cache_read_input_token_cost_above_200k_tokens": 6e-07, "litellm_provider": "vertex_ai-anthropic_models", "max_input_tokens": 1000000, - "max_output_tokens": 1000000, - "max_tokens": 1000000, + "max_output_tokens": 64000, + "max_tokens": 64000, "mode": "chat", "output_cost_per_token": 1.5e-05, "search_context_cost_per_query": { diff --git a/model_prices_and_context_window.json b/model_prices_and_context_window.json index 48a88f7afcb..e04c75655db 100644 --- a/model_prices_and_context_window.json +++ b/model_prices_and_context_window.json @@ -566,8 +566,8 @@ "cache_read_input_token_cost_above_200k_tokens": 6e-07, "litellm_provider": "bedrock_converse", "max_input_tokens": 1000000, - "max_output_tokens": 1000000, - "max_tokens": 1000000, + "max_output_tokens": 64000, + "max_tokens": 64000, "mode": "chat", "output_cost_per_token": 1.5e-05, "search_context_cost_per_query": { @@ -834,8 +834,8 @@ "cache_read_input_token_cost_above_200k_tokens": 6e-07, "litellm_provider": "bedrock_converse", "max_input_tokens": 1000000, - "max_output_tokens": 1000000, - "max_tokens": 1000000, + "max_output_tokens": 64000, + "max_tokens": 64000, "mode": "chat", "output_cost_per_token": 1.5e-05, "search_context_cost_per_query": { @@ -4831,8 +4831,8 @@ "cache_read_input_token_cost_above_200k_tokens": 6e-07, "litellm_provider": "anthropic", "max_input_tokens": 1000000, - "max_output_tokens": 1000000, - "max_tokens": 1000000, + "max_output_tokens": 64000, + "max_tokens": 64000, "mode": "chat", "output_cost_per_token": 1.5e-05, "search_context_cost_per_query": { @@ -7650,8 +7650,8 @@ "cache_read_input_token_cost_above_200k_tokens": 6e-07, "litellm_provider": "bedrock_converse", "max_input_tokens": 1000000, - "max_output_tokens": 1000000, - "max_tokens": 1000000, + "max_output_tokens": 64000, + "max_tokens": 64000, "mode": "chat", "output_cost_per_token": 1.5e-05, "search_context_cost_per_query": { @@ -16660,8 +16660,8 @@ "output_cost_per_token_above_200k_tokens": 2.25e-05, "litellm_provider": "openrouter", "max_input_tokens": 1000000, - "max_output_tokens": 1000000, - "max_tokens": 1000000, + "max_output_tokens": 64000, + "max_tokens": 64000, "mode": "chat", "output_cost_per_token": 1.5e-05, "supports_assistant_prefill": true, @@ -19639,8 +19639,8 @@ "cache_read_input_token_cost_above_200k_tokens": 6e-07, "litellm_provider": "bedrock_converse", "max_input_tokens": 1000000, - "max_output_tokens": 1000000, - "max_tokens": 1000000, + "max_output_tokens": 64000, + "max_tokens": 64000, "mode": "chat", "output_cost_per_token": 1.5e-05, "search_context_cost_per_query": { @@ -20979,8 +20979,8 @@ "cache_read_input_token_cost_above_200k_tokens": 6e-07, "litellm_provider": "vertex_ai-anthropic_models", "max_input_tokens": 1000000, - "max_output_tokens": 1000000, - "max_tokens": 1000000, + "max_output_tokens": 64000, + "max_tokens": 64000, "mode": "chat", "output_cost_per_token": 1.5e-05, "search_context_cost_per_query": { @@ -21009,8 +21009,8 @@ "cache_read_input_token_cost_above_200k_tokens": 6e-07, "litellm_provider": "vertex_ai-anthropic_models", "max_input_tokens": 1000000, - "max_output_tokens": 1000000, - "max_tokens": 1000000, + "max_output_tokens": 64000, + "max_tokens": 64000, "mode": "chat", "output_cost_per_token": 1.5e-05, "search_context_cost_per_query": { From ea8d0bb7d58d461cf984eaa81e5b52a0bee80c6a Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Wed, 24 Sep 2025 21:37:56 -0700 Subject: [PATCH 66/70] fix: re-add scheduled rotations --- .circleci/config.yml | 3 +- docs/my-website/docs/proxy/config_settings.md | 2 + ...tellm_proxy_extras-0.2.20-py3-none-any.whl | Bin 0 -> 30470 bytes .../dist/litellm_proxy_extras-0.2.20.tar.gz | Bin 0 -> 15576 bytes .../litellm_proxy_extras/schema.prisma | 4 + litellm-proxy-extras/pyproject.toml | 4 +- litellm/constants.py | 4 + ...odel_prices_and_context_window_backup.json | 4 +- litellm/proxy/_types.py | 31 ++- .../common_utils/key_rotation_manager.py | 146 +++++++++++++ .../key_management_endpoints.py | 17 +- litellm/proxy/proxy_config.yaml | 3 +- litellm/proxy/proxy_server.py | 40 +++- litellm/proxy/schema.prisma | 4 + poetry.lock | 10 +- pyproject.toml | 2 +- requirements.txt | 2 +- schema.prisma | 4 + .../common_utils/test_key_rotation_manager.py | 194 ++++++++++++++++++ .../test_key_management_endpoints.py | 71 +++++++ 20 files changed, 528 insertions(+), 17 deletions(-) create mode 100644 litellm-proxy-extras/dist/litellm_proxy_extras-0.2.20-py3-none-any.whl create mode 100644 litellm-proxy-extras/dist/litellm_proxy_extras-0.2.20.tar.gz create mode 100644 litellm/proxy/common_utils/key_rotation_manager.py create mode 100644 tests/test_litellm/proxy/common_utils/test_key_rotation_manager.py diff --git a/.circleci/config.yml b/.circleci/config.yml index ef6445ca0ca..ba42ccbc362 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -148,7 +148,8 @@ jobs: python -m pip install types-requests types-setuptools types-redis types-PyYAML if ! python -m mypy . \ --config-file mypy.ini \ - --ignore-missing-imports; then + --ignore-missing-imports \ + --no-incremental; then echo "mypy detected errors" exit 1 fi diff --git a/docs/my-website/docs/proxy/config_settings.md b/docs/my-website/docs/proxy/config_settings.md index f70701886b5..ad3afd59a02 100644 --- a/docs/my-website/docs/proxy/config_settings.md +++ b/docs/my-website/docs/proxy/config_settings.md @@ -614,6 +614,8 @@ router_settings: | LITELLM_MIGRATION_DIR | Custom migrations directory for prisma migrations, used for baselining db in read-only file systems. | LITELLM_HOSTED_UI | URL of the hosted UI for LiteLLM | LITELM_ENVIRONMENT | Environment of LiteLLM Instance, used by logging services. Currently only used by DeepEval. +| LITELLM_KEY_ROTATION_ENABLED | Enable auto-key rotation for LiteLLM (boolean). Default is false. +| LITELLM_KEY_ROTATION_CHECK_INTERVAL_SECONDS | Interval in seconds for how often to run job that auto-rotates keys. Default is 86400 (24 hours). | LITELLM_LICENSE | License key for LiteLLM usage | LITELLM_LOCAL_MODEL_COST_MAP | Local configuration for model cost mapping in LiteLLM | LITELLM_LOG | Enable detailed logging for LiteLLM diff --git a/litellm-proxy-extras/dist/litellm_proxy_extras-0.2.20-py3-none-any.whl b/litellm-proxy-extras/dist/litellm_proxy_extras-0.2.20-py3-none-any.whl new file mode 100644 index 0000000000000000000000000000000000000000..0a94ef6ff621531d9f9d3d4e398315f607384e97 GIT binary patch literal 30470 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 WRz|!2$GbVNetfP@emzb9`0XG0pa)+7 literal 0 HcmV?d00001 diff --git a/litellm-proxy-extras/dist/litellm_proxy_extras-0.2.20.tar.gz b/litellm-proxy-extras/dist/litellm_proxy_extras-0.2.20.tar.gz new file mode 100644 index 0000000000000000000000000000000000000000..1562aacc22b73a2db570b62ef49ebfe06c602535 GIT binary patch literal 15576 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 literal 0 HcmV?d00001 diff --git a/litellm-proxy-extras/litellm_proxy_extras/schema.prisma b/litellm-proxy-extras/litellm_proxy_extras/schema.prisma index 2b1e20820f9..9d3726d29d6 100644 --- a/litellm-proxy-extras/litellm_proxy_extras/schema.prisma +++ b/litellm-proxy-extras/litellm_proxy_extras/schema.prisma @@ -221,6 +221,10 @@ model LiteLLM_VerificationToken { created_by String? updated_at DateTime? @default(now()) @updatedAt @map("updated_at") updated_by String? + rotation_count Int? @default(0) // Number of times key has been rotated + auto_rotate Boolean? @default(false) // Whether this key should be auto-rotated + rotation_interval String? // How often to rotate (e.g., "30d", "90d") + last_rotation_at DateTime? // When this key was last rotated litellm_budget_table LiteLLM_BudgetTable? @relation(fields: [budget_id], references: [budget_id]) litellm_organization_table LiteLLM_OrganizationTable? @relation(fields: [organization_id], references: [organization_id]) object_permission LiteLLM_ObjectPermissionTable? @relation(fields: [object_permission_id], references: [object_permission_id]) diff --git a/litellm-proxy-extras/pyproject.toml b/litellm-proxy-extras/pyproject.toml index 1c368d58077..dd0b2138dcf 100644 --- a/litellm-proxy-extras/pyproject.toml +++ b/litellm-proxy-extras/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "litellm-proxy-extras" -version = "0.2.19" +version = "0.2.20" description = "Additional files for the LiteLLM Proxy. Reduces the size of the main litellm package." authors = ["BerriAI"] readme = "README.md" @@ -22,7 +22,7 @@ requires = ["poetry-core"] build-backend = "poetry.core.masonry.api" [tool.commitizen] -version = "0.2.19" +version = "0.2.20" version_files = [ "pyproject.toml:version", "../requirements.txt:litellm-proxy-extras==", diff --git a/litellm/constants.py b/litellm/constants.py index 6e70ae0671f..1ed9f237a29 100644 --- a/litellm/constants.py +++ b/litellm/constants.py @@ -989,7 +989,11 @@ HEALTH_CHECK_TIMEOUT_SECONDS = int( ) # 60 seconds LITTELM_INTERNAL_HEALTH_SERVICE_ACCOUNT_NAME = "litellm-internal-health-check" LITTELM_CLI_SERVICE_ACCOUNT_NAME = "litellm-cli" +LITELLM_INTERNAL_JOBS_SERVICE_ACCOUNT_NAME = "litellm_internal_jobs" +# Key Rotation Constants +LITELLM_KEY_ROTATION_ENABLED = os.getenv("LITELLM_KEY_ROTATION_ENABLED", "false") +LITELLM_KEY_ROTATION_CHECK_INTERVAL_SECONDS = int(os.getenv("LITELLM_KEY_ROTATION_CHECK_INTERVAL_SECONDS", 86400)) # 24 hours default UI_SESSION_TOKEN_TEAM_ID = "litellm-dashboard" LITELLM_PROXY_ADMIN_NAME = "default_user_id" diff --git a/litellm/model_prices_and_context_window_backup.json b/litellm/model_prices_and_context_window_backup.json index e5cc96ea528..e04c75655db 100644 --- a/litellm/model_prices_and_context_window_backup.json +++ b/litellm/model_prices_and_context_window_backup.json @@ -16660,8 +16660,8 @@ "output_cost_per_token_above_200k_tokens": 2.25e-05, "litellm_provider": "openrouter", "max_input_tokens": 1000000, - "max_output_tokens": 1000000, - "max_tokens": 1000000, + "max_output_tokens": 64000, + "max_tokens": 64000, "mode": "chat", "output_cost_per_token": 1.5e-05, "supports_assistant_prefill": true, diff --git a/litellm/proxy/_types.py b/litellm/proxy/_types.py index 6f2732af371..345322a0021 100644 --- a/litellm/proxy/_types.py +++ b/litellm/proxy/_types.py @@ -1,6 +1,5 @@ import enum import json -from litellm._uuid import uuid from datetime import datetime from typing import ( TYPE_CHECKING, @@ -24,6 +23,7 @@ from pydantic import ( ) from typing_extensions import Required, TypedDict +from litellm._uuid import uuid from litellm.types.integrations.slack_alerting import AlertType from litellm.types.llms.openai import AllMessageValues, OpenAIFileObject from litellm.types.mcp import ( @@ -787,6 +787,14 @@ class GenerateKeyRequest(KeyRequestBase): default=LiteLLMKeyType.DEFAULT, description="Type of key that determines default allowed routes.", ) + auto_rotate: Optional[bool] = Field( + default=False, + description="Whether this key should be automatically rotated" + ) + rotation_interval: Optional[str] = Field( + default=None, + description="How often to rotate this key (e.g., '30d', '90d'). Required if auto_rotate=True" + ) class GenerateKeyResponse(KeyRequestBase): @@ -1802,6 +1810,10 @@ class LiteLLM_VerificationToken(LiteLLMPydanticObjectBase): updated_by: Optional[str] = None object_permission_id: Optional[str] = None object_permission: Optional[LiteLLM_ObjectPermissionTable] = None + rotation_count: Optional[int] = 0 # Number of times key has been rotated + auto_rotate: Optional[bool] = False # Whether this key should be auto-rotated + rotation_interval: Optional[str] = None # How often to rotate (e.g., "30d", "90d") + last_rotation_at: Optional[datetime] = None # When this key was last rotated model_config = ConfigDict(protected_namespaces=()) @@ -1932,6 +1944,23 @@ class UserAPIKeyAuth( key_alias=LITTELM_CLI_SERVICE_ACCOUNT_NAME, team_alias=LITTELM_CLI_SERVICE_ACCOUNT_NAME, ) + + @classmethod + def get_litellm_internal_jobs_user_api_key_auth(cls) -> "UserAPIKeyAuth": + """ + Returns a `UserAPIKeyAuth` object for internal LiteLLM jobs like key rotation. + + This is used to track actions performed by automated system jobs. + """ + from litellm.constants import LITELLM_INTERNAL_JOBS_SERVICE_ACCOUNT_NAME + + return cls( + api_key=LITELLM_INTERNAL_JOBS_SERVICE_ACCOUNT_NAME, + team_id="system", + key_alias=LITELLM_INTERNAL_JOBS_SERVICE_ACCOUNT_NAME, + team_alias="system", + user_id="system", + ) class UserInfoResponse(LiteLLMPydanticObjectBase): diff --git a/litellm/proxy/common_utils/key_rotation_manager.py b/litellm/proxy/common_utils/key_rotation_manager.py new file mode 100644 index 00000000000..319d5ed5233 --- /dev/null +++ b/litellm/proxy/common_utils/key_rotation_manager.py @@ -0,0 +1,146 @@ +""" +Key Rotation Manager - Automated key rotation based on rotation schedules + +Handles finding keys that need rotation based on their individual schedules. +""" + +from datetime import datetime, timedelta, timezone +from typing import List + +from litellm._logging import verbose_proxy_logger +from litellm.constants import LITELLM_INTERNAL_JOBS_SERVICE_ACCOUNT_NAME +from litellm.proxy._types import ( + GenerateKeyResponse, + LiteLLM_VerificationToken, + RegenerateKeyRequest, +) +from litellm.proxy.hooks.key_management_event_hooks import KeyManagementEventHooks +from litellm.proxy.management_endpoints.key_management_endpoints import ( + regenerate_key_fn, +) +from litellm.proxy.utils import PrismaClient + + +class KeyRotationManager: + """ + Manages automated key rotation based on individual key rotation schedules. + """ + + def __init__(self, prisma_client: PrismaClient): + self.prisma_client = prisma_client + + async def process_rotations(self): + """ + Main entry point - find and rotate keys that are due for rotation + """ + try: + verbose_proxy_logger.info("Starting scheduled key rotation check...") + + # Find keys that are due for rotation + keys_to_rotate = await self._find_keys_needing_rotation() + + if not keys_to_rotate: + verbose_proxy_logger.debug("No keys are due for rotation at this time") + return + + verbose_proxy_logger.info(f"Found {len(keys_to_rotate)} keys due for rotation") + + # Rotate each key + for key in keys_to_rotate: + try: + await self._rotate_key(key) + key_identifier = key.key_name or (key.token[:8] + "..." if key.token else "unknown") + verbose_proxy_logger.info(f"Successfully rotated key: {key_identifier}") + except Exception as e: + key_identifier = key.key_name or (key.token[:8] + "..." if key.token else "unknown") + verbose_proxy_logger.error(f"Failed to rotate key {key_identifier}: {e}") + + except Exception as e: + verbose_proxy_logger.error(f"Key rotation process failed: {e}") + + async def _find_keys_needing_rotation(self) -> List[LiteLLM_VerificationToken]: + """ + Find keys that are due for rotation based on their rotation interval. + + Logic: + - Key has auto_rotate = true + - Key has rotation_interval set + - Either: never been rotated (last_rotation_at is null) OR + - Time since last rotation >= rotation_interval + """ + keys_with_rotation = await self.prisma_client.db.litellm_verificationtoken.find_many( + where={ + "auto_rotate": True, # Only keys marked for auto rotation + "rotation_interval": {"not": None} # Must have rotation interval set + } + ) + + # Filter keys that need rotation based on last_rotation_at + interval + keys_needing_rotation = [] + now = datetime.now(timezone.utc) + + for key in keys_with_rotation: + if self._should_rotate_key(key, now): + keys_needing_rotation.append(key) + + return keys_needing_rotation + + def _should_rotate_key(self, key: LiteLLM_VerificationToken, now: datetime) -> bool: + """ + Determine if a key should be rotated based on last rotation time and interval. + """ + if not key.rotation_interval: + return False + + # If never rotated, rotate immediately + if key.last_rotation_at is None: + return True + + # Calculate if enough time has passed since last rotation + from litellm.litellm_core_utils.duration_parser import duration_in_seconds + + interval_seconds = duration_in_seconds(key.rotation_interval) + next_rotation_time = key.last_rotation_at + timedelta(seconds=interval_seconds) + + return now >= next_rotation_time + + async def _rotate_key(self, key: LiteLLM_VerificationToken): + """ + Rotate a single key using existing regenerate_key_fn and call the rotation hook + """ + # Create regenerate request + regenerate_request = RegenerateKeyRequest( + key=key.token or "" + ) + + # Create a system user for key rotation + from litellm.proxy._types import UserAPIKeyAuth + system_user = UserAPIKeyAuth.get_litellm_internal_jobs_user_api_key_auth() + + # Use existing regenerate key function + response = await regenerate_key_fn( + data=regenerate_request, + user_api_key_dict=system_user, + litellm_changed_by=LITELLM_INTERNAL_JOBS_SERVICE_ACCOUNT_NAME + ) + + # Update the NEW key with rotation info (regenerate_key_fn creates a new token) + if isinstance(response, GenerateKeyResponse) and response.token_id: + await self.prisma_client.db.litellm_verificationtoken.update( + where={"token": response.token_id}, + data={ + "rotation_count": (key.rotation_count or 0) + 1, + "last_rotation_at": datetime.now(timezone.utc) + } + ) + + # Call the existing rotation hook for notifications, audit logs, etc. + if isinstance(response, GenerateKeyResponse): + await KeyManagementEventHooks.async_key_rotated_hook( + data=regenerate_request, + existing_key_row=key, + response=response, + user_api_key_dict=system_user, + litellm_changed_by=LITELLM_INTERNAL_JOBS_SERVICE_ACCOUNT_NAME + ) + \ No newline at end of file diff --git a/litellm/proxy/management_endpoints/key_management_endpoints.py b/litellm/proxy/management_endpoints/key_management_endpoints.py index aa56cc68a8a..c230018eceb 100644 --- a/litellm/proxy/management_endpoints/key_management_endpoints.py +++ b/litellm/proxy/management_endpoints/key_management_endpoints.py @@ -14,7 +14,6 @@ import copy import json import secrets import traceback -from litellm._uuid import uuid from datetime import datetime, timedelta, timezone from typing import List, Literal, Optional, Tuple, cast @@ -23,6 +22,7 @@ from fastapi import APIRouter, Depends, Header, HTTPException, Query, Request, s import litellm from litellm._logging import verbose_proxy_logger +from litellm._uuid import uuid from litellm.caching import DualCache from litellm.constants import LENGTH_OF_LITELLM_GENERATED_KEY, UI_SESSION_TOKEN_TEAM_ID from litellm.litellm_core_utils.duration_parser import duration_in_seconds @@ -642,6 +642,9 @@ async def generate_key_fn( - object_permission: Optional[LiteLLM_ObjectPermissionBase] - key-specific object permission. Example - {"vector_stores": ["vector_store_1", "vector_store_2"]}. IF null or {} then no object permission. - key_type: Optional[str] - Type of key that determines default allowed routes. Options: "llm_api" (can call LLM API routes), "management" (can call management routes), "read_only" (can only call info/read routes), "default" (uses default allowed routes). Defaults to "default". - prompts: Optional[List[str]] - List of allowed prompts for the key. If specified, the key will only be able to use these specific prompts. + - auto_rotate: Optional[bool] - Whether this key should be automatically rotated (regenerated) + - rotation_interval: Optional[str] - How often to auto-rotate this key (e.g., '30s', '30m', '30h', '30d'). Required if auto_rotate=True. + Examples: 1. Allow users to turn on/off pii masking @@ -1558,6 +1561,8 @@ def _check_model_access_group( return True + + async def generate_key_helper_fn( # noqa: PLR0915 request_type: Literal[ "user", "key" @@ -1611,6 +1616,8 @@ async def generate_key_helper_fn( # noqa: PLR0915 str ] = None, # object_permission_id <-> LiteLLM_ObjectPermissionTable object_permission: Optional[LiteLLM_ObjectPermissionBase] = None, + auto_rotate: Optional[bool] = None, + rotation_interval: Optional[str] = None, ): from litellm.proxy.proxy_server import premium_user, prisma_client @@ -1718,6 +1725,14 @@ async def generate_key_helper_fn( # noqa: PLR0915 "allowed_routes": allowed_routes or [], "object_permission_id": object_permission_id, } + + # Add rotation fields if auto_rotate is enabled + if auto_rotate and rotation_interval: + key_data.update({ + "auto_rotate": auto_rotate, + "rotation_interval": rotation_interval + # last_rotation_at will be null initially - rotation happens on first check + }) if ( get_secret("DISABLE_KEY_NAME", False) is True diff --git a/litellm/proxy/proxy_config.yaml b/litellm/proxy/proxy_config.yaml index 9897e17d29a..b35c7e95a16 100644 --- a/litellm/proxy/proxy_config.yaml +++ b/litellm/proxy/proxy_config.yaml @@ -42,4 +42,5 @@ litellm_settings: datadog_params: turn_off_message_logging: true datadog_llm_observability_params: - turn_off_message_logging: true \ No newline at end of file + turn_off_message_logging: true +# proxy_config.yaml diff --git a/litellm/proxy/proxy_server.py b/litellm/proxy/proxy_server.py index 23d9acfc1e5..f51b65c9155 100644 --- a/litellm/proxy/proxy_server.py +++ b/litellm/proxy/proxy_server.py @@ -9,7 +9,6 @@ import subprocess import sys import time import traceback -from litellm._uuid import uuid import warnings from datetime import datetime, timedelta from typing import ( @@ -27,6 +26,7 @@ from typing import ( get_type_hints, ) +from litellm._uuid import uuid from litellm.constants import ( BASE_MCP_ROUTE, DEFAULT_MAX_RECURSE_DEPTH, @@ -3811,9 +3811,10 @@ class ProxyStartupEvent: cls, scheduler: AsyncIOScheduler ): """ - Initialize the spend tracking background jobs + Initialize the spend tracking and other background jobs 1. CloudZero Background Job 2. Prometheus Background Job + 3. Key Rotation Background Job Args: scheduler: The scheduler to add the background jobs to @@ -3838,6 +3839,41 @@ class ProxyStartupEvent: except Exception: PrometheusLogger = None + ######################################################## + # Key Rotation Background Job + ######################################################## + from litellm.constants import ( + LITELLM_KEY_ROTATION_CHECK_INTERVAL_SECONDS, + LITELLM_KEY_ROTATION_ENABLED, + ) + + key_rotation_enabled: Optional[bool] = str_to_bool(LITELLM_KEY_ROTATION_ENABLED) + verbose_proxy_logger.debug(f"key_rotation_enabled: {key_rotation_enabled}") + + if key_rotation_enabled is True: + try: + from litellm.proxy.common_utils.key_rotation_manager import ( + KeyRotationManager, + ) + + # Get prisma_client from global scope + global prisma_client + if prisma_client is not None: + key_rotation_manager = KeyRotationManager(prisma_client) + verbose_proxy_logger.debug(f"Key rotation background job scheduled every {LITELLM_KEY_ROTATION_CHECK_INTERVAL_SECONDS} seconds (LITELLM_KEY_ROTATION_ENABLED=true)") + scheduler.add_job( + key_rotation_manager.process_rotations, + "interval", + seconds=LITELLM_KEY_ROTATION_CHECK_INTERVAL_SECONDS, + id="key_rotation_job" + ) + else: + verbose_proxy_logger.warning("Key rotation enabled but prisma_client not available") + except Exception as e: + verbose_proxy_logger.warning(f"Failed to setup key rotation job: {e}") + else: + verbose_proxy_logger.debug("Key rotation disabled (set LITELLM_KEY_ROTATION_ENABLED=true to enable)") + @classmethod async def _setup_prisma_client( cls, diff --git a/litellm/proxy/schema.prisma b/litellm/proxy/schema.prisma index 2b1e20820f9..9d3726d29d6 100644 --- a/litellm/proxy/schema.prisma +++ b/litellm/proxy/schema.prisma @@ -221,6 +221,10 @@ model LiteLLM_VerificationToken { created_by String? updated_at DateTime? @default(now()) @updatedAt @map("updated_at") updated_by String? + rotation_count Int? @default(0) // Number of times key has been rotated + auto_rotate Boolean? @default(false) // Whether this key should be auto-rotated + rotation_interval String? // How often to rotate (e.g., "30d", "90d") + last_rotation_at DateTime? // When this key was last rotated litellm_budget_table LiteLLM_BudgetTable? @relation(fields: [budget_id], references: [budget_id]) litellm_organization_table LiteLLM_OrganizationTable? @relation(fields: [organization_id], references: [organization_id]) object_permission LiteLLM_ObjectPermissionTable? @relation(fields: [object_permission_id], references: [object_permission_id]) diff --git a/poetry.lock b/poetry.lock index b6ed7801bed..c05400e1390 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 2.2.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.2.0 and should not be changed by hand. [[package]] name = "aiohappyeyeballs" @@ -3804,15 +3804,15 @@ files = [ [[package]] name = "litellm-proxy-extras" -version = "0.2.19" +version = "0.2.20" description = "Additional files for the LiteLLM Proxy. Reduces the size of the main litellm package." optional = true python-versions = "!=2.7.*,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,!=3.7.*,>=3.8" groups = ["main"] markers = "extra == \"proxy\"" files = [ - {file = "litellm_proxy_extras-0.2.19-py3-none-any.whl", hash = "sha256:cd4cc5fc639ac24bc99af70b8b56b7cc0480b0e72a2a53638f557beb7e9f64fd"}, - {file = "litellm_proxy_extras-0.2.19.tar.gz", hash = "sha256:e53592e39eb0b9c3b6cd32f29e6a0a53be51d7ad31dedfb69b58c24073b5b3ec"}, + {file = "litellm_proxy_extras-0.2.20-py3-none-any.whl", hash = "sha256:80ebe03be210eed99ddab0bf8e1a14169f24a502ecdaa98f4bb5032ebe65a944"}, + {file = "litellm_proxy_extras-0.2.20.tar.gz", hash = "sha256:8dea657f965122490be480327995ce534e75dc96778cb3e05c562c7996f85cce"}, ] [[package]] @@ -9598,4 +9598,4 @@ utils = ["numpydoc"] [metadata] lock-version = "2.1" python-versions = ">=3.8.1,<4.0, !=3.9.7" -content-hash = "b775339256b9eb6ae6ffc72cbe7d17d379897a3d72f762cb0df58e4044cded0c" +content-hash = "1fc7c0289412c4ea4b35448c3a2857eb4c5656796ec51df5f20d4c134c383271" diff --git a/pyproject.toml b/pyproject.toml index 77d80732b45..f8704342b19 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -59,7 +59,7 @@ websockets = {version = "^13.1.0", optional = true} boto3 = {version = "1.36.0", optional = true} redisvl = {version = "^0.4.1", optional = true, markers = "python_version >= '3.9' and python_version < '3.14'"} mcp = {version = "^1.10.0", optional = true, python = ">=3.10"} -litellm-proxy-extras = {version = "0.2.19", optional = true} +litellm-proxy-extras = {version = "0.2.20", optional = true} rich = {version = "13.7.1", optional = true} litellm-enterprise = {version = "0.1.20", optional = true} diskcache = {version = "^5.6.1", optional = true} diff --git a/requirements.txt b/requirements.txt index 3c55f682a0f..10929ab819b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -43,7 +43,7 @@ sentry_sdk==2.21.0 # for sentry error handling detect-secrets==1.5.0 # Enterprise - secret detection / masking in LLM requests cryptography==43.0.1 tzdata==2025.1 # IANA time zone database -litellm-proxy-extras==0.2.19 # for proxy extras - e.g. prisma migrations +litellm-proxy-extras==0.2.20 # for proxy extras - e.g. prisma migrations ### LITELLM PACKAGE DEPENDENCIES python-dotenv==1.0.1 # for env tiktoken==0.8.0 # for calculating usage diff --git a/schema.prisma b/schema.prisma index 2b1e20820f9..9d3726d29d6 100644 --- a/schema.prisma +++ b/schema.prisma @@ -221,6 +221,10 @@ model LiteLLM_VerificationToken { created_by String? updated_at DateTime? @default(now()) @updatedAt @map("updated_at") updated_by String? + rotation_count Int? @default(0) // Number of times key has been rotated + auto_rotate Boolean? @default(false) // Whether this key should be auto-rotated + rotation_interval String? // How often to rotate (e.g., "30d", "90d") + last_rotation_at DateTime? // When this key was last rotated litellm_budget_table LiteLLM_BudgetTable? @relation(fields: [budget_id], references: [budget_id]) litellm_organization_table LiteLLM_OrganizationTable? @relation(fields: [organization_id], references: [organization_id]) object_permission LiteLLM_ObjectPermissionTable? @relation(fields: [object_permission_id], references: [object_permission_id]) diff --git a/tests/test_litellm/proxy/common_utils/test_key_rotation_manager.py b/tests/test_litellm/proxy/common_utils/test_key_rotation_manager.py new file mode 100644 index 00000000000..8aab67bf588 --- /dev/null +++ b/tests/test_litellm/proxy/common_utils/test_key_rotation_manager.py @@ -0,0 +1,194 @@ +""" +Test key rotation manager functionality +""" +import os +import sys +from datetime import datetime, timedelta, timezone +from unittest.mock import AsyncMock, MagicMock + +import pytest + +sys.path.insert(0, os.path.abspath("../../../..")) + +from litellm.proxy._types import ( + GenerateKeyResponse, + LiteLLM_VerificationToken, +) +from litellm.proxy.common_utils.key_rotation_manager import KeyRotationManager + + +class TestKeyRotationManager: + """Test the KeyRotationManager class functionality.""" + + @pytest.mark.asyncio + async def test_should_rotate_key_logic(self): + """ + Test the core logic for determining when a key should be rotated. + + This tests: + - Keys with null last_rotation_at should rotate immediately + - Keys with recent rotation should not rotate + - Keys with old rotation should rotate + """ + # Setup + mock_prisma_client = AsyncMock() + manager = KeyRotationManager(mock_prisma_client) + + now = datetime.now(timezone.utc) + + # Test Case 1: Never rotated (last_rotation_at = None) - should rotate + key_never_rotated = LiteLLM_VerificationToken( + token="test-token-1", + auto_rotate=True, + rotation_interval="30s", + last_rotation_at=None, + rotation_count=0 + ) + + assert manager._should_rotate_key(key_never_rotated, now) == True + + # Test Case 2: Recently rotated (10s ago, interval 30s) - should NOT rotate + key_recently_rotated = LiteLLM_VerificationToken( + token="test-token-2", + auto_rotate=True, + rotation_interval="30s", + last_rotation_at=now - timedelta(seconds=10), + rotation_count=1 + ) + + assert manager._should_rotate_key(key_recently_rotated, now) == False + + # Test Case 3: Old rotation (60s ago, interval 30s) - should rotate + key_old_rotation = LiteLLM_VerificationToken( + token="test-token-3", + auto_rotate=True, + rotation_interval="30s", + last_rotation_at=now - timedelta(seconds=60), + rotation_count=2 + ) + + assert manager._should_rotate_key(key_old_rotation, now) == True + + # Test Case 4: No rotation interval - should NOT rotate + key_no_interval = LiteLLM_VerificationToken( + token="test-token-4", + auto_rotate=True, + rotation_interval=None, + last_rotation_at=None, + rotation_count=0 + ) + + assert manager._should_rotate_key(key_no_interval, now) == False + + @pytest.mark.asyncio + async def test_find_keys_needing_rotation(self): + """ + Test finding keys that need rotation from database. + + This tests: + - Only keys with auto_rotate=True and rotation_interval are considered + - Filtering logic works correctly + - Database query is constructed properly + """ + # Setup + mock_prisma_client = AsyncMock() + manager = KeyRotationManager(mock_prisma_client) + + now = datetime.now(timezone.utc) + + # Mock database response + mock_keys = [ + LiteLLM_VerificationToken( + token="token-1", + auto_rotate=True, + rotation_interval="30s", + last_rotation_at=None, # Should rotate + rotation_count=0 + ), + LiteLLM_VerificationToken( + token="token-2", + auto_rotate=True, + rotation_interval="60s", + last_rotation_at=now - timedelta(seconds=30), # Should NOT rotate (30s < 60s) + rotation_count=1 + ), + LiteLLM_VerificationToken( + token="token-3", + auto_rotate=True, + rotation_interval="30s", + last_rotation_at=now - timedelta(seconds=45), # Should rotate (45s > 30s) + rotation_count=2 + ) + ] + + mock_prisma_client.db.litellm_verificationtoken.find_many.return_value = mock_keys + + # Execute + keys_needing_rotation = await manager._find_keys_needing_rotation() + + # Verify database query + mock_prisma_client.db.litellm_verificationtoken.find_many.assert_called_once_with( + where={ + "auto_rotate": True, + "rotation_interval": {"not": None} + } + ) + + # Verify filtering logic + assert len(keys_needing_rotation) == 2 # token-1 and token-3 should need rotation + + tokens_needing_rotation = [key.token for key in keys_needing_rotation] + assert "token-1" in tokens_needing_rotation # Never rotated + assert "token-2" not in tokens_needing_rotation # Recently rotated + assert "token-3" in tokens_needing_rotation # Old rotation + + @pytest.mark.asyncio + async def test_rotate_key_updates_database(self): + """ + Test that key rotation properly updates the database with new rotation info. + + This tests: + - Rotation count is incremented + - last_rotation_at is set to current time + - New key token is updated (not old one) + """ + # Setup + mock_prisma_client = AsyncMock() + manager = KeyRotationManager(mock_prisma_client) + + # Mock key to rotate + key_to_rotate = LiteLLM_VerificationToken( + token="old-token", + auto_rotate=True, + rotation_interval="30s", + last_rotation_at=None, + rotation_count=0 + ) + + # Mock regenerate_key_fn response + mock_response = GenerateKeyResponse( + key="new-api-key", + token_id="new-token-id", + user_id="test-user" + ) + + # Mock the regenerate function + from unittest.mock import patch + with patch('litellm.proxy.common_utils.key_rotation_manager.regenerate_key_fn', return_value=mock_response): + with patch('litellm.proxy.common_utils.key_rotation_manager.KeyManagementEventHooks.async_key_rotated_hook'): + # Execute + await manager._rotate_key(key_to_rotate) + + # Verify database update was called with correct data + mock_prisma_client.db.litellm_verificationtoken.update.assert_called_once() + + call_args = mock_prisma_client.db.litellm_verificationtoken.update.call_args + + # Check the WHERE clause targets the new token + assert call_args[1]["where"]["token"] == "new-token-id" + + # Check the data being updated + update_data = call_args[1]["data"] + assert update_data["rotation_count"] == 1 # Incremented from 0 + assert "last_rotation_at" in update_data + assert isinstance(update_data["last_rotation_at"], datetime) diff --git a/tests/test_litellm/proxy/management_endpoints/test_key_management_endpoints.py b/tests/test_litellm/proxy/management_endpoints/test_key_management_endpoints.py index da64a866d51..14b5833f14f 100644 --- a/tests/test_litellm/proxy/management_endpoints/test_key_management_endpoints.py +++ b/tests/test_litellm/proxy/management_endpoints/test_key_management_endpoints.py @@ -23,6 +23,7 @@ from litellm.proxy.auth.user_api_key_auth import UserAPIKeyAuth from litellm.proxy.management_endpoints.key_management_endpoints import ( _common_key_generation_helper, _list_key_helper, + generate_key_helper_fn, prepare_key_update_data, validate_key_team_change, ) @@ -1091,3 +1092,73 @@ def test_validate_key_team_change_with_member_permissions(): team_table=mock_team, route=KeyManagementRoutes.KEY_UPDATE.value ) + + +def test_key_rotation_fields_helper(): + """ + Test the key data update logic for rotation fields. + + This test focuses on the core logic that adds rotation fields to key_data + when auto_rotate is enabled, without the complexity of full key generation. + """ + # Test Case 1: With rotation enabled + key_data = { + "models": ["gpt-3.5-turbo"], + "user_id": "test-user" + } + + auto_rotate = True + rotation_interval = "30d" + + # Simulate the rotation logic from generate_key_helper_fn + if auto_rotate and rotation_interval: + key_data.update({ + "auto_rotate": auto_rotate, + "rotation_interval": rotation_interval + }) + + # Verify rotation fields are added + assert key_data["auto_rotate"] == True + assert key_data["rotation_interval"] == "30d" + assert key_data["models"] == ["gpt-3.5-turbo"] # Original fields preserved + + # Test Case 2: Without rotation enabled + key_data2 = { + "models": ["gpt-4"], + "user_id": "test-user" + } + + auto_rotate2 = False + rotation_interval2 = None + + # Simulate the rotation logic + if auto_rotate2 and rotation_interval2: + key_data2.update({ + "auto_rotate": auto_rotate2, + "rotation_interval": rotation_interval2 + }) + + # Verify rotation fields are NOT added + assert "auto_rotate" not in key_data2 + assert "rotation_interval" not in key_data2 + assert key_data2["models"] == ["gpt-4"] # Original fields preserved + + # Test Case 3: auto_rotate=True but no interval + key_data3 = { + "models": ["claude-3"], + "user_id": "test-user" + } + + auto_rotate3 = True + rotation_interval3 = None + + # Simulate the rotation logic + if auto_rotate3 and rotation_interval3: + key_data3.update({ + "auto_rotate": auto_rotate3, + "rotation_interval": rotation_interval3 + }) + + # Verify rotation fields are NOT added (missing interval) + assert "auto_rotate" not in key_data3 + assert "rotation_interval" not in key_data3 From 4a75d042fbc4bad3e3ccd89d2ef8327c1f0f94f3 Mon Sep 17 00:00:00 2001 From: Ishaan Jaffer Date: Fri, 26 Sep 2025 11:45:52 -0700 Subject: [PATCH 67/70] fix test --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 10929ab819b..b00ac53ca7d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,7 +8,7 @@ backoff==2.2.1 # server dep pyyaml==6.0.2 # server dep uvicorn==0.29.0 # server dep gunicorn==23.0.0 # server dep -fastuuid==0.12.0 # for uuid4 +fastuuid==0.13.0 # for uuid4 uvloop==0.21.0 # uvicorn dep, gives us much better performance under load boto3==1.36.0 # aws bedrock/sagemaker calls redis==5.2.1 # redis caching From e2e2ae78a4e61513e4ed283fd1320c2f83d92ab5 Mon Sep 17 00:00:00 2001 From: Ishaan Jaffer Date: Fri, 26 Sep 2025 11:48:28 -0700 Subject: [PATCH 68/70] test fix --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index b00ac53ca7d..6f6a3d14a74 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,7 +8,7 @@ backoff==2.2.1 # server dep pyyaml==6.0.2 # server dep uvicorn==0.29.0 # server dep gunicorn==23.0.0 # server dep -fastuuid==0.13.0 # for uuid4 +fastuuid==0.13.5 # for uuid4 uvloop==0.21.0 # uvicorn dep, gives us much better performance under load boto3==1.36.0 # aws bedrock/sagemaker calls redis==5.2.1 # redis caching From 1009a38ff49eadbe8e4374b438a5242fea4e30c9 Mon Sep 17 00:00:00 2001 From: Ishaan Jaffer Date: Fri, 26 Sep 2025 12:02:14 -0700 Subject: [PATCH 69/70] Add key expiry and auto-rotation settings - Create KeyLifecycleSettings component grouping expiry and auto-rotation - Add dedicated 'Key Expiry & Auto-Rotation' accordion section - Fix auto-rotation toggle visibility with proper layout - Move expire key field from Optional Settings to new section - Remove old AutoRotationSettings component - Integrate auto-rotation metadata in form submission --- .../index.html} | 0 .../proxy/_experimental/out/onboarding.html | 1 - .../KeyLifecycleSettings.tsx | 93 +++++++++++++++++++ .../organisms/create_key_button.tsx | 47 +++++++--- 4 files changed, 126 insertions(+), 15 deletions(-) rename litellm/proxy/_experimental/out/{model_hub_table.html => model_hub_table/index.html} (100%) delete mode 100644 litellm/proxy/_experimental/out/onboarding.html create mode 100644 ui/litellm-dashboard/src/components/common_components/KeyLifecycleSettings.tsx diff --git a/litellm/proxy/_experimental/out/model_hub_table.html b/litellm/proxy/_experimental/out/model_hub_table/index.html similarity index 100% rename from litellm/proxy/_experimental/out/model_hub_table.html rename to litellm/proxy/_experimental/out/model_hub_table/index.html diff --git a/litellm/proxy/_experimental/out/onboarding.html b/litellm/proxy/_experimental/out/onboarding.html deleted file mode 100644 index 1dc5fdb1f85..00000000000 --- a/litellm/proxy/_experimental/out/onboarding.html +++ /dev/null @@ -1 +0,0 @@ -LiteLLM Dashboard \ No newline at end of file diff --git a/ui/litellm-dashboard/src/components/common_components/KeyLifecycleSettings.tsx b/ui/litellm-dashboard/src/components/common_components/KeyLifecycleSettings.tsx new file mode 100644 index 00000000000..d3b009599d7 --- /dev/null +++ b/ui/litellm-dashboard/src/components/common_components/KeyLifecycleSettings.tsx @@ -0,0 +1,93 @@ +import React from "react"; +import { Form, Switch, Select, Typography, Tooltip } from "antd"; +import { InfoCircleOutlined } from "@ant-design/icons"; +import { TextInput } from "@tremor/react"; + +const { Text } = Typography; +const { Option } = Select; + +interface KeyLifecycleSettingsProps { + form: any; // Form instance from parent + autoRotationEnabled: boolean; + onAutoRotationChange: (enabled: boolean) => void; + rotationInterval: string; + onRotationIntervalChange: (interval: string) => void; +} + +const KeyLifecycleSettings: React.FC = ({ + form, + autoRotationEnabled, + onAutoRotationChange, + rotationInterval, + onRotationIntervalChange, +}) => { + return ( +

+ + Expire Key{' '} + + + + + } + name="duration" + className="mb-4" + > + + + +
+
+
+ Enable Auto-Rotation + + + +
+ +
+ + {autoRotationEnabled && ( +
+ + Rotation Interval{' '} + + + + + } + name="rotation_interval" + className="mb-2" + > + + + + + When rotation occurs, you'll receive a notification with the new key. The old key will be deactivated after a brief grace period. + +
+ )} +
+
+ ); +}; + +export default KeyLifecycleSettings; diff --git a/ui/litellm-dashboard/src/components/organisms/create_key_button.tsx b/ui/litellm-dashboard/src/components/organisms/create_key_button.tsx index 3771ea5b1bc..df9e30d2521 100644 --- a/ui/litellm-dashboard/src/components/organisms/create_key_button.tsx +++ b/ui/litellm-dashboard/src/components/organisms/create_key_button.tsx @@ -34,6 +34,7 @@ import { callback_map, mapDisplayToInternalNames } from "../callback_info_helper import MCPServerSelector from "../mcp_server_management/MCPServerSelector" import ModelAliasManager from "../common_components/ModelAliasManager" import NotificationsManager from "../molecules/notifications_manager" +import KeyLifecycleSettings from "../common_components/KeyLifecycleSettings" const { Option } = Select; @@ -169,6 +170,8 @@ const CreateKey: React.FC = ({ const [disabledCallbacks, setDisabledCallbacks] = useState([]); const [keyType, setKeyType] = useState("default"); const [modelAliases, setModelAliases] = useState<{ [key: string]: string }>({}); + const [autoRotationEnabled, setAutoRotationEnabled] = useState(false); + const [rotationInterval, setRotationInterval] = useState("30d"); const handleOk = () => { setIsModalVisible(false); @@ -177,6 +180,8 @@ const CreateKey: React.FC = ({ setDisabledCallbacks([]); setKeyType("default"); setModelAliases({}); + setAutoRotationEnabled(false); + setRotationInterval("30d"); }; const handleCancel = () => { @@ -188,6 +193,8 @@ const CreateKey: React.FC = ({ setDisabledCallbacks([]); setKeyType("default"); setModelAliases({}); + setAutoRotationEnabled(false); + setRotationInterval("30d"); }; useEffect(() => { @@ -320,6 +327,15 @@ const CreateKey: React.FC = ({ }; } + // Add auto-rotation settings to the metadata + if (autoRotationEnabled) { + metadata = { + ...metadata, + auto_rotation_enabled: true, + rotation_interval: rotationInterval + }; + } + // Update the formValues with the final metadata formValues.metadata = JSON.stringify(metadata); @@ -823,20 +839,6 @@ const CreateKey: React.FC = ({ > - - Expire Key{' '} - - - - - } - name="duration" - className="mt-4" - > - - @@ -1053,6 +1055,23 @@ const CreateKey: React.FC = ({ + + + + Key Expiry & Auto-Rotation + + +
+ +
+
+
From 35930d7ccb3d68f1acf78207c805df5d837c25de Mon Sep 17 00:00:00 2001 From: Ishaan Jaffer Date: Fri, 26 Sep 2025 12:21:57 -0700 Subject: [PATCH 70/70] fix key rotation settings --- .../KeyLifecycleSettings.tsx | 105 ++++++++++-------- .../organisms/create_key_button.tsx | 7 +- 2 files changed, 62 insertions(+), 50 deletions(-) diff --git a/ui/litellm-dashboard/src/components/common_components/KeyLifecycleSettings.tsx b/ui/litellm-dashboard/src/components/common_components/KeyLifecycleSettings.tsx index d3b009599d7..ca0d8f67b72 100644 --- a/ui/litellm-dashboard/src/components/common_components/KeyLifecycleSettings.tsx +++ b/ui/litellm-dashboard/src/components/common_components/KeyLifecycleSettings.tsx @@ -1,9 +1,8 @@ import React from "react"; -import { Form, Switch, Select, Typography, Tooltip } from "antd"; +import { Select, Tooltip, Divider, Switch } from "antd"; import { InfoCircleOutlined } from "@ant-design/icons"; import { TextInput } from "@tremor/react"; -const { Text } = Typography; const { Option } = Select; interface KeyLifecycleSettingsProps { @@ -22,67 +21,75 @@ const KeyLifecycleSettings: React.FC = ({ onRotationIntervalChange, }) => { return ( -
- - Expire Key{' '} - - - - - } - name="duration" - className="mb-4" - > - - +
+ {/* Key Expiry Section */} +
+ Key Expiry Settings -
-
-
- Enable Auto-Rotation - - +
+
- +
+
- {autoRotationEnabled && ( -
- - Rotation Interval{' '} - - - - - } - name="rotation_interval" - className="mb-2" - > + + + {/* Auto-Rotation Section */} +
+ Auto-Rotation Settings + +
+
+ + +
+ + {autoRotationEnabled && ( +
+ - - - - When rotation occurs, you'll receive a notification with the new key. The old key will be deactivated after a brief grace period. - +
+ )} +
+ + {autoRotationEnabled && ( +
+ When rotation occurs, you'll receive a notification with the new key. The old key will be deactivated after a brief grace period.
)}
diff --git a/ui/litellm-dashboard/src/components/organisms/create_key_button.tsx b/ui/litellm-dashboard/src/components/organisms/create_key_button.tsx index df9e30d2521..c74511e6c0a 100644 --- a/ui/litellm-dashboard/src/components/organisms/create_key_button.tsx +++ b/ui/litellm-dashboard/src/components/organisms/create_key_button.tsx @@ -335,6 +335,11 @@ const CreateKey: React.FC = ({ rotation_interval: rotationInterval }; } + + // Handle duration field for key expiry + if (formValues.duration) { + formValues.duration = formValues.duration; + } // Update the formValues with the final metadata formValues.metadata = JSON.stringify(metadata); @@ -1058,7 +1063,7 @@ const CreateKey: React.FC = ({ - Key Expiry & Auto-Rotation + Key Lifecycle