From 36229dc69f9378cf46fb34ee74b485e33ebed79d Mon Sep 17 00:00:00 2001 From: "Jugal D. Bhatt" <55304795+jugaldb@users.noreply.github.com> Date: Mon, 4 Aug 2025 08:36:49 -0700 Subject: [PATCH 01/69] [LLM Translation] Fix Model Usage not having text tokens (#13234) * fix + test * remove test comments * fix mypy * fix mypy * fix tests --- litellm/cost_calculator.py | 4 +- .../streaming_chunk_builder_utils.py | 7 ++- litellm/types/utils.py | 3 ++ tests/local_testing/test_caching.py | 8 +-- tests/test_litellm/types/test_types_utils.py | 53 +++++++++++++++++++ 5 files changed, 68 insertions(+), 7 deletions(-) diff --git a/litellm/cost_calculator.py b/litellm/cost_calculator.py index 6fecc7fa976..9956a9d314a 100644 --- a/litellm/cost_calculator.py +++ b/litellm/cost_calculator.py @@ -1265,7 +1265,7 @@ class BaseTokenUsageProcessor: Combine multiple Usage objects into a single Usage object, checking model keys for nested values. """ from litellm.types.utils import ( - CompletionTokensDetails, + CompletionTokensDetailsWrapper, PromptTokensDetailsWrapper, Usage, ) @@ -1320,7 +1320,7 @@ class BaseTokenUsageProcessor: not hasattr(combined, "completion_tokens_details") or not combined.completion_tokens_details ): - combined.completion_tokens_details = CompletionTokensDetails() + combined.completion_tokens_details = CompletionTokensDetailsWrapper() # Check what keys exist in the model's completion_tokens_details for attr in usage.completion_tokens_details.model_fields: diff --git a/litellm/litellm_core_utils/streaming_chunk_builder_utils.py b/litellm/litellm_core_utils/streaming_chunk_builder_utils.py index fb919afd49d..2f85c7aef60 100644 --- a/litellm/litellm_core_utils/streaming_chunk_builder_utils.py +++ b/litellm/litellm_core_utils/streaming_chunk_builder_utils.py @@ -527,7 +527,12 @@ class ChunkProcessor: returned_usage, "cache_read_input_tokens", cache_read_input_tokens ) # for anthropic if completion_tokens_details is not None: - returned_usage.completion_tokens_details = completion_tokens_details + if isinstance(completion_tokens_details, CompletionTokensDetails): + returned_usage.completion_tokens_details = CompletionTokensDetailsWrapper( + **completion_tokens_details.model_dump() + ) + else: + returned_usage.completion_tokens_details = completion_tokens_details if reasoning_tokens is not None: if returned_usage.completion_tokens_details is None: diff --git a/litellm/types/utils.py b/litellm/types/utils.py index d1b2f57c725..c5af6e4ed0a 100644 --- a/litellm/types/utils.py +++ b/litellm/types/utils.py @@ -888,6 +888,9 @@ class Usage(CompletionUsage): server_tool_use: Optional[ServerToolUse] = None + completion_tokens_details: Optional[CompletionTokensDetailsWrapper] = None + """Breakdown of tokens used in a completion.""" + prompt_tokens_details: Optional[PromptTokensDetailsWrapper] = None """Breakdown of tokens used in the prompt.""" diff --git a/tests/local_testing/test_caching.py b/tests/local_testing/test_caching.py index 26df08ecc64..1f68e8a43ad 100644 --- a/tests/local_testing/test_caching.py +++ b/tests/local_testing/test_caching.py @@ -2155,8 +2155,8 @@ async def test_caching_kwargs_input(sync_mode): Message, ModelResponse, Usage, - CompletionTokensDetails, - PromptTokensDetails, + CompletionTokensDetailsWrapper, + PromptTokensDetailsWrapper, ) from datetime import datetime @@ -2187,10 +2187,10 @@ async def test_caching_kwargs_input(sync_mode): completion_tokens=31, prompt_tokens=16, total_tokens=47, - completion_tokens_details=CompletionTokensDetails( + completion_tokens_details=CompletionTokensDetailsWrapper( audio_tokens=None, reasoning_tokens=0 ), - prompt_tokens_details=PromptTokensDetails( + prompt_tokens_details=PromptTokensDetailsWrapper( audio_tokens=None, cached_tokens=0 ), ), diff --git a/tests/test_litellm/types/test_types_utils.py b/tests/test_litellm/types/test_types_utils.py index 1bf005db503..71b92475186 100644 --- a/tests/test_litellm/types/test_types_utils.py +++ b/tests/test_litellm/types/test_types_utils.py @@ -73,3 +73,56 @@ def test_usage_dump(): new_usage = Usage(**current_usage.model_dump()) assert new_usage.prompt_tokens_details.web_search_requests == 1 + + +def test_usage_completion_tokens_details_text_tokens(): + from litellm.types.utils import Usage + + # Test data from the reported issue + usage_data = { + 'completion_tokens': 77, + 'prompt_tokens': 11937, + 'total_tokens': 12014, + 'completion_tokens_details': { + 'accepted_prediction_tokens': None, + 'audio_tokens': None, + 'reasoning_tokens': 65, + 'rejected_prediction_tokens': None, + 'text_tokens': 12 + }, + 'prompt_tokens_details': { + 'audio_tokens': None, + 'cached_tokens': None, + 'text_tokens': 11937, + 'image_tokens': None + } + } + + # Create Usage object + u = Usage(**usage_data) + + # Verify the object has the text_tokens field + assert hasattr(u.completion_tokens_details, 'text_tokens') + assert u.completion_tokens_details.text_tokens == 12 + + # Get model_dump output + dump_result = u.model_dump() + + # Verify text_tokens is present in the model_dump output + assert 'completion_tokens_details' in dump_result + assert 'text_tokens' in dump_result['completion_tokens_details'] + assert dump_result['completion_tokens_details']['text_tokens'] == 12 + + # Verify the full completion_tokens_details structure + expected_completion_details = { + 'accepted_prediction_tokens': None, + 'audio_tokens': None, + 'reasoning_tokens': 65, + 'rejected_prediction_tokens': None, + 'text_tokens': 12 + } + assert dump_result['completion_tokens_details'] == expected_completion_details + + # Verify round-trip serialization works + new_usage = Usage(**dump_result) + assert new_usage.completion_tokens_details.text_tokens == 12 From b318f55faad4c59260c54fe7578c9b82753bfc3d Mon Sep 17 00:00:00 2001 From: "Jugal D. Bhatt" <55304795+jugaldb@users.noreply.github.com> Date: Mon, 4 Aug 2025 08:42:06 -0700 Subject: [PATCH 02/69] [UI] Add team deletion check for teams with keys (#12953) * added check option * Add underline * make less verbosE --- ui/litellm-dashboard/src/components/teams.tsx | 124 +++++++++++------- 1 file changed, 75 insertions(+), 49 deletions(-) diff --git a/ui/litellm-dashboard/src/components/teams.tsx b/ui/litellm-dashboard/src/components/teams.tsx index 5d142521e59..09fc5cd4e4d 100644 --- a/ui/litellm-dashboard/src/components/teams.tsx +++ b/ui/litellm-dashboard/src/components/teams.tsx @@ -72,6 +72,7 @@ import VectorStoreSelector from "./vector_store_management/VectorStoreSelector"; import PremiumLoggingSettings from "./common_components/PremiumLoggingSettings"; import type { KeyResponse, Team } from "./key_team_helpers/key_list"; import { formatNumberWithCommas } from "../utils/dataUtils"; +import { AlertTriangleIcon, XIcon } from 'lucide-react'; import MCPServerSelector from "./mcp_server_management/MCPServerSelector"; interface TeamProps { @@ -203,6 +204,7 @@ const Teams: React.FC = ({ const [loggingSettings, setLoggingSettings] = useState([]); const [mcpAccessGroups, setMcpAccessGroups] = useState([]); const [mcpAccessGroupsLoaded, setMcpAccessGroupsLoaded] = useState(false); + const [deleteConfirmInput, setDeleteConfirmInput] = useState(""); useEffect(() => { console.log(`currentOrgForCreateTeam: ${currentOrgForCreateTeam}`); @@ -1077,55 +1079,79 @@ const Teams: React.FC = ({ : null} - {isDeleteModalOpen && ( -
-
- - - {/* Modal Panel */} - - - {/* Confirmation Modal Content */} -
-
-
-
-

- Delete Team -

-
-

- Are you sure you want to delete this - team ? -

-
-
-
-
-
- - -
-
-
-
- )} + {isDeleteModalOpen && (() => { + const team = teams?.find((t) => t.team_id === teamToDelete); + const teamName = team?.team_alias || ""; + const keyCount = team?.keys?.length || 0; + const isValid = deleteConfirmInput === teamName; + return ( +
+
+
+
+

Delete Team

+ +
+
+ {keyCount > 0 && ( +
+
+ +
+
+

+ Warning: This team has {keyCount} associated key{keyCount > 1 ? 's' : ''}. +

+

+ Deleting the team will also delete all associated keys. This action is irreversible. +

+
+
+ )} +

+ Are you sure you want to force delete this team and all its keys? +

+
+ + setDeleteConfirmInput(e.target.value)} + placeholder="Enter team name exactly" + className="w-full px-4 py-3 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 text-base" + autoFocus + /> +
+
+
+
+ + +
+
+
+ ); +})()} From dae72003a7b51ed32635565b70d4b465a6ac91dd Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Mon, 4 Aug 2025 10:51:53 -0700 Subject: [PATCH 03/69] [Bug Fix] OpenAI / Azure Responses API - Add `service_tier` , `safety_identifier` supported params (#13258) * test_aresponses_service_tier_and_safety_identifier * add service_tier + safety_identifier * fix get_supported_openai_params * add safety_identifier + service_tier for responses() --- .../llms/openai/responses/transformation.py | 6 +- litellm/responses/main.py | 6 ++ litellm/types/llms/openai.py | 2 + .../test_openai_responses_api.py | 87 +++++++++++++++++++ 4 files changed, 100 insertions(+), 1 deletion(-) diff --git a/litellm/llms/openai/responses/transformation.py b/litellm/llms/openai/responses/transformation.py index 527ae4a9d49..12814286f6c 100644 --- a/litellm/llms/openai/responses/transformation.py +++ b/litellm/llms/openai/responses/transformation.py @@ -4,6 +4,9 @@ import httpx import litellm from litellm._logging import verbose_logger +from litellm.litellm_core_utils.llm_response_utils.convert_dict_to_response import ( + _safe_convert_created_field, +) from litellm.llms.base_llm.responses.transformation import BaseResponsesAPIConfig from litellm.secret_managers.main import get_secret_str from litellm.types.llms.openai import * @@ -11,7 +14,6 @@ from litellm.types.responses.main import * from litellm.types.router import GenericLiteLLMParams from ..common_utils import OpenAIError -from litellm.litellm_core_utils.llm_response_utils.convert_dict_to_response import _safe_convert_created_field if TYPE_CHECKING: from litellm.litellm_core_utils.litellm_logging import Logging as _LiteLLMLoggingObj @@ -47,6 +49,8 @@ class OpenAIResponsesAPIConfig(BaseResponsesAPIConfig): "top_p", "truncation", "user", + "service_tier", + "safety_identifier", "extra_headers", "extra_query", "extra_body", diff --git a/litellm/responses/main.py b/litellm/responses/main.py index 07459547a0a..b5f18cf9e1a 100644 --- a/litellm/responses/main.py +++ b/litellm/responses/main.py @@ -243,6 +243,8 @@ async def aresponses( top_p: Optional[float] = None, truncation: Optional[Literal["auto", "disabled"]] = None, user: Optional[str] = None, + service_tier: Optional[str] = None, + safety_identifier: Optional[str] = None, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Optional[Dict[str, Any]] = None, @@ -294,6 +296,8 @@ async def aresponses( extra_body=extra_body, timeout=timeout, custom_llm_provider=custom_llm_provider, + service_tier=service_tier, + safety_identifier=safety_identifier, **kwargs, ) @@ -350,6 +354,8 @@ def responses( top_p: Optional[float] = None, truncation: Optional[Literal["auto", "disabled"]] = None, user: Optional[str] = None, + service_tier: Optional[str] = None, + safety_identifier: Optional[str] = None, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Optional[Dict[str, Any]] = None, diff --git a/litellm/types/llms/openai.py b/litellm/types/llms/openai.py index 1955bfac5f8..bc3cfeee6a3 100644 --- a/litellm/types/llms/openai.py +++ b/litellm/types/llms/openai.py @@ -965,6 +965,8 @@ class ResponsesAPIOptionalRequestParams(TypedDict, total=False): top_p: Optional[float] truncation: Optional[Literal["auto", "disabled"]] user: Optional[str] + service_tier: Optional[str] + safety_identifier: Optional[str] prompt: Optional[PromptObject] diff --git a/tests/llm_responses_api_testing/test_openai_responses_api.py b/tests/llm_responses_api_testing/test_openai_responses_api.py index 87488f67e7a..5cd515be232 100644 --- a/tests/llm_responses_api_testing/test_openai_responses_api.py +++ b/tests/llm_responses_api_testing/test_openai_responses_api.py @@ -1308,3 +1308,90 @@ async def test_store_field_transformation(): assert response.created_at == 1751443898, "created_at should maintain the same value after conversion" +@pytest.mark.asyncio +async def test_aresponses_service_tier_and_safety_identifier(): + """ + Test that service_tier and safety_identifier parameters are correctly sent in the request body + when using litellm.aresponses. + """ + mock_response = { + "id": "resp_01234567890abcdef", + "object": "response", + "created_at": 1753060947, + "status": "completed", + "error": None, + "incomplete_details": None, + "instructions": None, + "max_output_tokens": None, + "model": "gpt-4o-2024-05-13", + "output": [ + { + "type": "text", + "id": "out_01234567890abcdef", + "text": "This is a test response with service tier and safety identifier.", + } + ], + "parallel_tool_calls": True, + "previous_response_id": None, + "reasoning": None, + "store": True, + "temperature": 1.0, + "text": {"format": {"type": "text"}}, + "tool_choice": "auto", + "tools": [], + "top_p": 1.0, + "truncation": "disabled", + "usage": { + "input_tokens": 15, + "input_tokens_details": {"cached_tokens": 0}, + "output_tokens": 25, + "output_tokens_details": {"reasoning_tokens": 0}, + "total_tokens": 40, + }, + "user": None, + "metadata": {}, + } + + class MockResponse: + def __init__(self, json_data, status_code): + self._json_data = json_data + self.status_code = status_code + self.text = json.dumps(json_data) + + def json(self): + return self._json_data + + with patch( + "litellm.llms.custom_httpx.http_handler.AsyncHTTPHandler.post", + new_callable=AsyncMock, + ) as mock_post: + # Configure the mock to return our response + mock_post.return_value = MockResponse(mock_response, 200) + + litellm._turn_on_debug() + litellm.set_verbose = True + + # Call aresponses with service_tier and safety_identifier + response = await litellm.aresponses( + model="openai/gpt-4o", + input="Test with service tier and safety identifier", + service_tier="flex", + safety_identifier="123", + ) + + # Verify the request was made correctly + mock_post.assert_called_once() + request_body = mock_post.call_args.kwargs["json"] + print("request_body=", json.dumps(request_body, indent=4, default=str)) + + # Validate that both parameters are present in the request body + assert request_body["service_tier"] == "flex", "service_tier should be 'flex' in request body" + assert request_body["safety_identifier"] == "123", "safety_identifier should be '123' in request body" + assert request_body["model"] == "gpt-4o" + assert request_body["input"] == "Test with service tier and safety identifier" + + # Validate the response + print("Response:", json.dumps(response, indent=4, default=str)) + + + From f3749709b8743fa00d8ae86629a76811e1223c46 Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Mon, 4 Aug 2025 12:01:33 -0700 Subject: [PATCH 04/69] Bug Fix - Responses API raises error with Gemini Tool Calls in `input` (#13260) * add _transform_responses_api_function_call_to_chat_completion_message * test_responses_api_with_tool_calls * TestFunctionCallTransformation * fixes for responses API testing google ai studio * TestGoogleAIStudioResponsesAPITest * test_responses_api_with_tool_calls * test_responses_api_with_tool_calls * test_basic_openai_responses_streaming_delete_endpoint --- .../transformation.py | 64 ++++- .../base_responses_api.py | 80 ++++++- .../test_google_ai_studio_responses_api.py | 20 +- .../test_litellm_completion_responses.py | 221 +++++++++++++++++- 4 files changed, 377 insertions(+), 8 deletions(-) diff --git a/litellm/responses/litellm_completion_transformation/transformation.py b/litellm/responses/litellm_completion_transformation/transformation.py index b8cde45c611..13791666044 100644 --- a/litellm/responses/litellm_completion_transformation/transformation.py +++ b/litellm/responses/litellm_completion_transformation/transformation.py @@ -22,6 +22,7 @@ from litellm.caching import InMemoryCache from litellm.litellm_core_utils.litellm_logging import Logging as LiteLLMLoggingObj from litellm.types.llms.openai import ( AllMessageValues, + ChatCompletionImageUrlObject, ChatCompletionResponseMessage, ChatCompletionSystemMessage, ChatCompletionToolCallChunk, @@ -40,7 +41,6 @@ from litellm.types.llms.openai import ( ResponsesAPIOptionalRequestParams, ResponsesAPIResponse, ResponseTextConfig, - ChatCompletionImageUrlObject, ) from litellm.types.responses.main import ( GenericResponseOutputItem, @@ -264,6 +264,10 @@ class LiteLLMCompletionResponsesConfig: chat_completion_messages = LiteLLMCompletionResponsesConfig._transform_responses_api_input_item_to_chat_completion_message( input_item=_input ) + + ######################################################### + # If Input Item is a Tool Call Output, add it to the tool_call_output_messages list + ######################################################### if LiteLLMCompletionResponsesConfig._is_input_item_tool_call_output( input_item=_input ): @@ -316,6 +320,11 @@ class LiteLLMCompletionResponsesConfig: return LiteLLMCompletionResponsesConfig._transform_responses_api_tool_call_output_to_chat_completion_message( tool_call_output=input_item ) + elif LiteLLMCompletionResponsesConfig._is_input_item_function_call(input_item): + # handle function call input items + return LiteLLMCompletionResponsesConfig._transform_responses_api_function_call_to_chat_completion_message( + function_call=input_item + ) else: return [ GenericChatCompletionMessage( @@ -337,6 +346,13 @@ class LiteLLMCompletionResponsesConfig: "computer_call_output", ] + @staticmethod + def _is_input_item_function_call(input_item: Any) -> bool: + """ + Check if the input item is a function call + """ + return input_item.get("type") == "function_call" + @staticmethod def _transform_responses_api_tool_call_output_to_chat_completion_message( tool_call_output: Dict[str, Any], @@ -402,6 +418,52 @@ class LiteLLMCompletionResponsesConfig: return [tool_output_message] + @staticmethod + def _transform_responses_api_function_call_to_chat_completion_message( + function_call: Dict[str, Any], + ) -> List[ + Union[ + AllMessageValues, + GenericChatCompletionMessage, + ChatCompletionResponseMessage, + ] + ]: + """ + Transform a Responses API function_call into a Chat Completion message with tool calls + + Handles Input items of this type: + function_call: + ```json + { + "type": "function_call", + "arguments":"{\"location\": \"São Paulo, Brazil\"}", + "call_id": "call_v2wlBzrlTIFl9FxPeY774GHZ", + "name": "get_weather", + "id": "fc_685c42deefc0819a822b6936faaa30be0c76bc1491ab6619", + "status": "completed" + } + ``` + """ + # Create a tool call for the function call + tool_call = ChatCompletionToolCallChunk( + id=function_call.get("call_id") or function_call.get("id") or "", + type="function", + function=ChatCompletionToolCallFunctionChunk( + name=function_call.get("name") or "", + arguments=function_call.get("arguments") or "", + ), + index=0, + ) + + # Create an assistant message with the tool call + chat_completion_response_message = ChatCompletionResponseMessage( + tool_calls=[tool_call], + role="assistant", + content=None, # Function calls don't have content + ) + + return [chat_completion_response_message] + @staticmethod def _transform_input_file_item_to_file_item(item: Dict[str, Any]) -> Dict[str, Any]: """ diff --git a/tests/llm_responses_api_testing/base_responses_api.py b/tests/llm_responses_api_testing/base_responses_api.py index 8087e0ead3c..a4ee9a8835a 100644 --- a/tests/llm_responses_api_testing/base_responses_api.py +++ b/tests/llm_responses_api_testing/base_responses_api.py @@ -25,6 +25,9 @@ from litellm.types.llms.openai import ( ResponseAPIUsage, IncompleteDetails, ) +from openai.types.responses.response_create_params import ( + ResponseInputParam, +) from litellm.llms.custom_httpx.http_handler import AsyncHTTPHandler @@ -184,8 +187,8 @@ class BaseResponsesAPITest(ABC): # basic test assert the usage seems reasonable print("response_completed_event.response.usage=", response_completed_event.response.usage) assert response_completed_event.response.usage.input_tokens > 0 and response_completed_event.response.usage.input_tokens < 100 - assert response_completed_event.response.usage.output_tokens > 0 and response_completed_event.response.usage.output_tokens < 1000 - assert response_completed_event.response.usage.total_tokens > 0 and response_completed_event.response.usage.total_tokens < 1000 + assert response_completed_event.response.usage.output_tokens > 0 and response_completed_event.response.usage.output_tokens < 2000 + assert response_completed_event.response.usage.total_tokens > 0 and response_completed_event.response.usage.total_tokens < 2000 # total tokens should be the sum of input and output tokens assert response_completed_event.response.usage.total_tokens == response_completed_event.response.usage.input_tokens + response_completed_event.response.usage.output_tokens @@ -229,6 +232,7 @@ class BaseResponsesAPITest(ABC): @pytest.mark.parametrize("sync_mode", [True, False]) + @pytest.mark.flaky(retries=3, delay=2) @pytest.mark.asyncio async def test_basic_openai_responses_streaming_delete_endpoint(self, sync_mode): #litellm._turn_on_debug() @@ -278,6 +282,7 @@ class BaseResponsesAPITest(ABC): ) @pytest.mark.parametrize("sync_mode", [False, True]) + @pytest.mark.flaky(retries=3, delay=2) @pytest.mark.asyncio async def test_basic_openai_responses_get_endpoint(self, sync_mode): litellm._turn_on_debug() @@ -318,6 +323,7 @@ class BaseResponsesAPITest(ABC): raise ValueError("response is not a ResponsesAPIResponse") @pytest.mark.asyncio + @pytest.mark.flaky(retries=3, delay=2) async def test_basic_openai_list_input_items_endpoint(self): """Test that calls the OpenAI List Input Items endpoint""" litellm._turn_on_debug() @@ -364,3 +370,73 @@ class BaseResponsesAPITest(ABC): # assert the response is not None assert response_1 is not None assert response_2 is not None + + @pytest.mark.asyncio + async def test_responses_api_with_tool_calls(self): + """Test that calls the Responses API with tool calls including function call and output""" + litellm._turn_on_debug() + litellm.set_verbose = True + base_completion_call_args = self.get_base_completion_call_args() + + # Define the input with message, function call, and function call output + input_data: ResponseInputParam = [ + { + "type": "message", + "role": "user", + "content": "How is the weather in São Paulo today ?" + }, + { + "type": "function_call", + "arguments": "{\"location\": \"São Paulo, Brazil\"}", + "call_id": "fc_1fe70e2a-a596-45ef-b72c-9b8567c460e5", + "name": "get_weather", + "id": "fc_1fe70e2a-a596-45ef-b72c-9b8567c460e5", + "status": "completed" + }, + { + "type": "function_call_output", + "call_id": "fc_1fe70e2a-a596-45ef-b72c-9b8567c460e5", + "output": "Rainy" + } + ] + + # Define the tools + tools = [ + { + "type": "function", + "name": "get_weather", + "description": "Get current temperature for a given location.", + "parameters": { + "type": "object", + "properties": { + "location": { + "type": "string", + "description": "City and country e.g. Bogotá, Colombia" + } + }, + "required": ["location"], + "additionalProperties": False + } + } + ] + + try: + # Make the responses API call + response = await litellm.aresponses( + input=input_data, + store=False, + tools=tools, + **base_completion_call_args + ) + except litellm.InternalServerError: + pytest.skip("Skipping test due to litellm.InternalServerError") + + print("litellm response=", json.dumps(response, indent=4, default=str)) + + # Validate the response structure + validate_responses_api_response(response, final_chunk=True) + + # Additional assertions specific to tool calls + assert response is not None + assert "output" in response + assert len(response["output"]) > 0 diff --git a/tests/llm_responses_api_testing/test_google_ai_studio_responses_api.py b/tests/llm_responses_api_testing/test_google_ai_studio_responses_api.py index 0357f8c6496..81daaea238d 100644 --- a/tests/llm_responses_api_testing/test_google_ai_studio_responses_api.py +++ b/tests/llm_responses_api_testing/test_google_ai_studio_responses_api.py @@ -5,7 +5,7 @@ from unittest.mock import patch, AsyncMock sys.path.insert(0, os.path.abspath("../..")) import litellm import json - +from base_responses_api import BaseResponsesAPITest @pytest.mark.asyncio async def test_basic_google_ai_studio_responses_api_with_tools(): litellm._turn_on_debug() @@ -85,10 +85,22 @@ async def test_mock_basic_google_ai_studio_responses_api_with_tools(): assert call_kwargs["messages"][0]["content"] == "what is the latest version of supabase python package and when was it released?" assert call_kwargs["tools"] == [] # web search tools are converted to web_search_options, not kept as tools +class TestGoogleAIStudioResponsesAPITest(BaseResponsesAPITest): + def get_base_completion_call_args(self): + #litellm._turn_on_debug() + return { + "model": "gemini/gemini-2.5-flash-lite" + } + + async def test_basic_openai_responses_delete_endpoint(self, sync_mode=False): + pass + + async def test_basic_openai_responses_streaming_delete_endpoint(self, sync_mode=False): + pass - - - + async def test_basic_openai_responses_get_endpoint(self, sync_mode=False): + pass + diff --git a/tests/test_litellm/responses/litellm_completion_transformation/test_litellm_completion_responses.py b/tests/test_litellm/responses/litellm_completion_transformation/test_litellm_completion_responses.py index 752b6ab8f4f..00c55b9f60f 100644 --- a/tests/test_litellm/responses/litellm_completion_transformation/test_litellm_completion_responses.py +++ b/tests/test_litellm/responses/litellm_completion_transformation/test_litellm_completion_responses.py @@ -8,7 +8,11 @@ sys.path.insert( from litellm.responses.litellm_completion_transformation.transformation import ( LiteLLMCompletionResponsesConfig, ) -from litellm.types.utils import ModelResponse, Choices, Message +from litellm.types.llms.openai import ( + ChatCompletionResponseMessage, + ChatCompletionToolMessage, +) +from litellm.types.utils import Choices, Message, ModelResponse class TestLiteLLMCompletionResponsesConfig: @@ -364,3 +368,218 @@ class TestLiteLLMCompletionResponsesConfig: item for item in responses_api_response.output if item.type == "message" ] assert len(message_items) == 2, "Should have two message items" + + + + +class TestFunctionCallTransformation: + """Test cases for function_call input transformation""" + + def test_function_call_detection(self): + """Test that function_call items are correctly detected""" + function_call_item = { + "type": "function_call", + "name": "get_weather", + "arguments": '{"location": "test"}', + "call_id": "test_id" + } + + function_call_output_item = { + "type": "function_call_output", + "call_id": "test_id", + "output": "result" + } + + regular_message = { + "type": "message", + "role": "user", + "content": "Hello" + } + + # Test function_call detection + assert LiteLLMCompletionResponsesConfig._is_input_item_function_call(function_call_item) + assert not LiteLLMCompletionResponsesConfig._is_input_item_function_call(function_call_output_item) + assert not LiteLLMCompletionResponsesConfig._is_input_item_function_call(regular_message) + + # Test function_call_output detection (should still work) + assert LiteLLMCompletionResponsesConfig._is_input_item_tool_call_output(function_call_output_item) + assert not LiteLLMCompletionResponsesConfig._is_input_item_tool_call_output(function_call_item) + assert not LiteLLMCompletionResponsesConfig._is_input_item_tool_call_output(regular_message) + + def test_function_call_transformation(self): + """Test that function_call items are correctly transformed to assistant messages with tool calls""" + function_call_item = { + "type": "function_call", + "name": "get_weather", + "arguments": '{"location": "São Paulo, Brazil"}', + "call_id": "call_123", + "id": "call_123", + "status": "completed" + } + + result = LiteLLMCompletionResponsesConfig._transform_responses_api_function_call_to_chat_completion_message( + function_call=function_call_item + ) + + assert len(result) == 1 + message = result[0] + + # Should be an assistant message + assert message.get("role") == "assistant" + assert message.get("content") is None # Function calls don't have content + + # Should have tool calls + tool_calls = message.get("tool_calls", []) + assert len(tool_calls) == 1 + + tool_call = tool_calls[0] + assert tool_call.get("id") == "call_123" + assert tool_call.get("type") == "function" + + function = tool_call.get("function", {}) + assert function.get("name") == "get_weather" + assert function.get("arguments") == '{"location": "São Paulo, Brazil"}' + + def test_complete_input_transformation_with_function_calls(self): + """Test the complete transformation with the exact input from the issue""" + test_input = [ + { + "type": "message", + "role": "user", + "content": "How is the weather in São Paulo today ?" + }, + { + "type": "function_call", + "arguments": '{"location": "São Paulo, Brazil"}', + "call_id": "call_1fe70e2a-a596-45ef-b72c-9b8567c460e5", + "name": "get_weather", + "id": "call_1fe70e2a-a596-45ef-b72c-9b8567c460e5", + "status": "completed" + }, + { + "type": "function_call_output", + "call_id": "call_1fe70e2a-a596-45ef-b72c-9b8567c460e5", + "output": "Rainy" + } + ] + + # This should not raise an error (previously would raise "Invalid content type: ") + messages = LiteLLMCompletionResponsesConfig._transform_response_input_param_to_chat_completion_message( + input=test_input + ) + + assert len(messages) == 3 + + # First message: user message + user_msg = messages[0] + assert user_msg.get("role") == "user" + assert user_msg.get("content") == "How is the weather in São Paulo today ?" + + # Second message: assistant message with tool call + assistant_msg = messages[1] + assert assistant_msg.get("role") == "assistant" + assert assistant_msg.get("tool_calls") is not None + assert len(assistant_msg.get("tool_calls", [])) == 1 + + tool_call = assistant_msg.get("tool_calls")[0] + assert tool_call.get("function", {}).get("name") == "get_weather" + + # Third message: tool output + tool_msg = messages[2] + assert tool_msg.get("role") == "tool" + assert tool_msg.get("content") == "Rainy" + assert tool_msg.get("tool_call_id") == "call_1fe70e2a-a596-45ef-b72c-9b8567c460e5" + + def test_complete_request_transformation_with_function_calls(self): + """Test the complete request transformation that would be used by the responses API""" + test_input = [ + { + "type": "message", + "role": "user", + "content": "How is the weather in São Paulo today ?" + }, + { + "type": "function_call", + "arguments": '{"location": "São Paulo, Brazil"}', + "call_id": "call_1fe70e2a-a596-45ef-b72c-9b8567c460e5", + "name": "get_weather", + "id": "call_1fe70e2a-a596-45ef-b72c-9b8567c460e5", + "status": "completed" + }, + { + "type": "function_call_output", + "call_id": "call_1fe70e2a-a596-45ef-b72c-9b8567c460e5", + "output": "Rainy" + } + ] + + tools = [ + { + "type": "function", + "name": "get_weather", + "description": "Get current temperature for a given location.", + "parameters": { + "type": "object", + "properties": { + "location": { + "type": "string", + "description": "City and country e.g. Bogotá, Colombia" + } + }, + "required": ["location"], + "additionalProperties": False + } + } + ] + + responses_api_request = { + "store": False, + "tools": tools + } + + # This should work without errors for non-OpenAI models + result = LiteLLMCompletionResponsesConfig.transform_responses_api_request_to_chat_completion_request( + model="gemini/gemini-2.0-flash", + input=test_input, + responses_api_request=responses_api_request + ) + + assert "messages" in result + assert "model" in result + assert "tools" in result + + messages = result["messages"] + assert len(messages) == 3 + assert result["model"] == "gemini/gemini-2.0-flash" + + # Verify the structure is correct for chat completion + user_msg = messages[0] + assert user_msg["role"] == "user" + + assistant_msg = messages[1] + assert assistant_msg["role"] == "assistant" + assert "tool_calls" in assistant_msg + + tool_msg = messages[2] + assert tool_msg["role"] == "tool" + + def test_function_call_without_call_id_fallback_to_id(self): + """Test that function_call items can use 'id' field when 'call_id' is missing""" + function_call_item = { + "type": "function_call", + "name": "get_weather", + "arguments": '{"location": "test"}', + "id": "fallback_id" # Only has 'id', not 'call_id' + } + + result = LiteLLMCompletionResponsesConfig._transform_responses_api_function_call_to_chat_completion_message( + function_call=function_call_item + ) + + assert len(result) == 1 + message = result[0] + tool_calls = message.get("tool_calls", []) + assert len(tool_calls) == 1 + + tool_call = tool_calls[0] + assert tool_call.get("id") == "fallback_id" \ No newline at end of file From bfecab2cc59ee70060ec1499223b4861ba2e3970 Mon Sep 17 00:00:00 2001 From: Krrish Dholakia Date: Mon, 4 Aug 2025 14:17:55 -0700 Subject: [PATCH 05/69] docs(index.md): cleanup tag --- docs/my-website/release_notes/v1.74.15-stable/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/my-website/release_notes/v1.74.15-stable/index.md b/docs/my-website/release_notes/v1.74.15-stable/index.md index a94359e3403..4fbb76bdbc4 100644 --- a/docs/my-website/release_notes/v1.74.15-stable/index.md +++ b/docs/my-website/release_notes/v1.74.15-stable/index.md @@ -28,7 +28,7 @@ import TabItem from '@theme/TabItem'; docker run \ -e STORE_MODEL_IN_DB=True \ -p 4000:4000 \ -ghcr.io/berriai/litellm:v1.74.15.rc.1 +ghcr.io/berriai/litellm:1.74.15.rc.1 ``` From 747467c819e2a879ff102f77e3cdf9c246459efd Mon Sep 17 00:00:00 2001 From: Krrish Dholakia Date: Mon, 4 Aug 2025 15:14:13 -0700 Subject: [PATCH 06/69] docs(user_keys.md): add litellm python sdk tab --- docs/my-website/docs/proxy/user_keys.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/my-website/docs/proxy/user_keys.md b/docs/my-website/docs/proxy/user_keys.md index e56cc6867df..ecf6f2d0532 100644 --- a/docs/my-website/docs/proxy/user_keys.md +++ b/docs/my-website/docs/proxy/user_keys.md @@ -86,6 +86,11 @@ response = client.chat.completions.create( print(response) ``` + + + +[**👉 Go Here**](../providers/litellm_proxy#send-all-sdk-requests-to-litellm-proxy) + From 779579a604b102ff92b2ae957c7f283c69cb2a19 Mon Sep 17 00:00:00 2001 From: Benjamin Bartels Date: Mon, 4 Aug 2025 23:56:36 +0100 Subject: [PATCH 07/69] Update model_prices_and_context_window.json (#13244) --- model_prices_and_context_window.json | 52 ++++++++++++++-------------- 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/model_prices_and_context_window.json b/model_prices_and_context_window.json index ef4891c2686..0656f6b260c 100644 --- a/model_prices_and_context_window.json +++ b/model_prices_and_context_window.json @@ -3750,7 +3750,7 @@ "max_input_tokens": 131072, "max_output_tokens": 131072, "input_cost_per_token": 3.3e-06, - "output_cost_per_token": 16.5e-06, + "output_cost_per_token": 1.65e-05, "litellm_provider": "azure_ai", "mode": "chat", "supports_function_calling": true, @@ -3764,7 +3764,7 @@ "max_input_tokens": 131072, "max_output_tokens": 131072, "input_cost_per_token": 3e-06, - "output_cost_per_token": 15e-06, + "output_cost_per_token": 1.5e-05, "litellm_provider": "azure_ai", "mode": "chat", "supports_function_calling": true, @@ -3777,7 +3777,7 @@ "max_tokens": 131072, "max_input_tokens": 131072, "max_output_tokens": 131072, - "input_cost_per_token": 0.25e-06, + "input_cost_per_token": 2.5e-07, "output_cost_per_token": 1.27e-06, "litellm_provider": "azure_ai", "mode": "chat", @@ -3792,7 +3792,7 @@ "max_tokens": 131072, "max_input_tokens": 131072, "max_output_tokens": 131072, - "input_cost_per_token": 0.275e-06, + "input_cost_per_token": 2.75e-07, "output_cost_per_token": 1.38e-06, "litellm_provider": "azure_ai", "mode": "chat", @@ -7337,12 +7337,12 @@ "max_audio_length_hours": 8.4, "max_audio_per_prompt": 1, "max_pdf_size_mb": 30, - "input_cost_per_token": 3.5e-07, + "input_cost_per_token": 3.5e-07, "input_cost_per_audio_token": 2.1e-06, "input_cost_per_image": 2.1e-06, "input_cost_per_video_per_second": 2.1e-06, "output_cost_per_token": 1.5e-06, - "output_cost_per_audio_token": 8.5e-06, + "output_cost_per_audio_token": 8.5e-06, "litellm_provider": "gemini", "mode": "chat", "rpm": 10, @@ -9039,9 +9039,9 @@ "supports_tool_choice": true }, "vertex_ai/meta/llama-4-scout-17b-16e-instruct-maas": { - "max_tokens": 10000000.0, - "max_input_tokens": 10000000.0, - "max_output_tokens": 10000000.0, + "max_tokens": 10000000, + "max_input_tokens": 10000000, + "max_output_tokens": 10000000, "input_cost_per_token": 2.5e-07, "output_cost_per_token": 7e-07, "litellm_provider": "vertex_ai-llama_models", @@ -9059,9 +9059,9 @@ ] }, "vertex_ai/meta/llama-4-scout-17b-128e-instruct-maas": { - "max_tokens": 10000000.0, - "max_input_tokens": 10000000.0, - "max_output_tokens": 10000000.0, + "max_tokens": 10000000, + "max_input_tokens": 10000000, + "max_output_tokens": 10000000, "input_cost_per_token": 2.5e-07, "output_cost_per_token": 7e-07, "litellm_provider": "vertex_ai-llama_models", @@ -9079,9 +9079,9 @@ ] }, "vertex_ai/meta/llama-4-maverick-17b-128e-instruct-maas": { - "max_tokens": 1000000.0, - "max_input_tokens": 1000000.0, - "max_output_tokens": 1000000.0, + "max_tokens": 1000000, + "max_input_tokens": 1000000, + "max_output_tokens": 1000000, "input_cost_per_token": 3.5e-07, "output_cost_per_token": 1.15e-06, "litellm_provider": "vertex_ai-llama_models", @@ -9099,9 +9099,9 @@ ] }, "vertex_ai/meta/llama-4-maverick-17b-16e-instruct-maas": { - "max_tokens": 1000000.0, - "max_input_tokens": 1000000.0, - "max_output_tokens": 1000000.0, + "max_tokens": 1000000, + "max_input_tokens": 1000000, + "max_output_tokens": 1000000, "input_cost_per_token": 3.5e-07, "output_cost_per_token": 1.15e-06, "litellm_provider": "vertex_ai-llama_models", @@ -9174,7 +9174,7 @@ "max_input_tokens": 128000, "max_output_tokens": 2048, "input_cost_per_token": 5e-06, - "output_cost_per_token": 16e-06, + "output_cost_per_token": 1.6e-05, "litellm_provider": "vertex_ai-llama_models", "mode": "chat", "supports_system_messages": true, @@ -10480,7 +10480,7 @@ "supports_tool_choice": true, "supports_prompt_caching": true }, - "openrouter/x-ai/grok-4":{ + "openrouter/x-ai/grok-4": { "max_tokens": 256000, "max_input_tokens": 256000, "max_output_tokens": 256000, @@ -10494,12 +10494,12 @@ "source": "https://openrouter.ai/x-ai/grok-4", "supports_web_search": true }, - "openrouter/bytedance/ui-tars-1.5-7b":{ + "openrouter/bytedance/ui-tars-1.5-7b": { "max_tokens": 2048, "max_input_tokens": 131072, "max_output_tokens": 2048, - "input_cost_per_token": 0.1e-06, - "output_cost_per_token": 0.2e-06, + "input_cost_per_token": 1e-07, + "output_cost_per_token": 2e-07, "litellm_provider": "openrouter", "mode": "chat", "source": "https://openrouter.ai/api/v1/models/bytedance/ui-tars-1.5-7b", @@ -11178,8 +11178,8 @@ "max_tokens": 8192, "max_input_tokens": 8192, "max_output_tokens": 2048, - "input_cost_per_token": 0.21e-06, - "output_cost_per_token": 0.63e-06, + "input_cost_per_token": 2.1e-07, + "output_cost_per_token": 6.3e-07, "litellm_provider": "openrouter", "mode": "chat", "supports_tool_choice": true @@ -14763,7 +14763,7 @@ "max_tokens": 131072, "max_input_tokens": 131072, "max_output_tokens": 16384, - "input_cost_per_token": 0.6e-06, + "input_cost_per_token": 6e-07, "output_cost_per_token": 2.5e-06, "litellm_provider": "fireworks_ai", "mode": "chat", From 849f5e0ba01aabf26f583cf7a455e8c2f29a4a36 Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Mon, 4 Aug 2025 15:58:28 -0700 Subject: [PATCH 08/69] [Bug Fix] Fix Server root path regression on UI when using "Login" (#13267) * bug fix serve_login_page * test_serve_login_page_server_root_path --- litellm/proxy/management_endpoints/ui_sso.py | 19 ++++---- .../proxy/management_endpoints/test_ui_sso.py | 44 +++++++++++++++++++ 2 files changed, 55 insertions(+), 8 deletions(-) diff --git a/litellm/proxy/management_endpoints/ui_sso.py b/litellm/proxy/management_endpoints/ui_sso.py index 14f273f3269..3031508adeb 100644 --- a/litellm/proxy/management_endpoints/ui_sso.py +++ b/litellm/proxy/management_endpoints/ui_sso.py @@ -116,6 +116,14 @@ async def serve_login_page( missing_env_vars = show_missing_vars_in_env() if missing_env_vars is not None: return missing_env_vars + ######################################################### + # Construct Redirect URL + base_url_to_redirect_to: Optional[str] = None + base_url_to_redirect_to = os.getenv("PROXY_BASE_URL", "") + server_root_path = os.getenv("SERVER_ROOT_PATH", "") + if server_root_path != "": + base_url_to_redirect_to += server_root_path + ######################################################### # Build the unified login page HTML error_message = "" @@ -137,7 +145,7 @@ async def serve_login_page( sso_button = "" if sso_available: - sso_button = """ + sso_button = f"""
or

- 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 c7c67cbba71..245f350be1b 100644 --- a/tests/test_litellm/proxy/management_endpoints/test_ui_sso.py +++ b/tests/test_litellm/proxy/management_endpoints/test_ui_sso.py @@ -1245,3 +1245,47 @@ class TestCustomUISSO: # Verify the result is the redirect response assert result == mock_redirect_response assert result.status_code == 303 + + +@pytest.mark.asyncio +async def test_serve_login_page_server_root_path(): + """ + Test that serve_login_page includes SERVER_ROOT_PATH in the SSO login URL + when SERVER_ROOT_PATH is set. + """ + # Arrange + mock_request = MagicMock(spec=Request) + captured_html = "" + + # Mock environment variables + env_vars = { + "PROXY_BASE_URL": "https://example.com", + "SERVER_ROOT_PATH": "/api/v1", + "GOOGLE_CLIENT_ID": "mock_google_client_id", # Enable SSO + "DATABASE_URL": "mock_db_url", # Satisfy show_missing_vars_in_env + "LITELLM_MASTER_KEY": "mock_master_key", # Satisfy show_missing_vars_in_env + } + + # Patch HTMLResponse to capture the content + def mock_html_response(content, status_code=200): + nonlocal captured_html + captured_html = content + return MagicMock() + + with patch.dict(os.environ, env_vars): + with patch("litellm.proxy.proxy_server.premium_user", True): + with patch("litellm.proxy.proxy_server.prisma_client", MagicMock()): + with patch("litellm.proxy.proxy_server.master_key", "mock_master_key"): + with patch("fastapi.responses.HTMLResponse", side_effect=mock_html_response): + # Import the function to test + from litellm.proxy.management_endpoints.ui_sso import ( + serve_login_page, + ) + + # Act + result = await serve_login_page(request=mock_request) + + # Assert + assert result is not None + expected_url = "https://example.com/api/v1/sso/login" + assert expected_url in captured_html, f"Expected URL '{expected_url}' not found in HTML content" From 056b60a9fa5153c1b6894bcaeea979a1f8f3d9fd Mon Sep 17 00:00:00 2001 From: breno-aumo <160534746+breno-aumo@users.noreply.github.com> Date: Mon, 4 Aug 2025 19:59:25 -0300 Subject: [PATCH 09/69] Support OCI provider (#13206) * create OCI required files * request and response conversion for non-streaming chat * support tool calling with OCI generic API without streaming * adaptation of api call for generic and cohere format * include tool calls and responses in generic api and dropping support for cohere * fix invalid content-length error * support streaming for generic api * fix auth error when using acompletion with streaming * refactor: use base_llm_http_handler and include API type definitions * update types and add type safety in different methods * fix OCIFunction format * create custom stream wrapper for decoding OCI stream * remove unused files * create unit tests for OCI * lint the code * remove manual test * docs: update the docs to include OCI --- docs/my-website/docs/providers/oci.md | 84 ++ docs/my-website/sidebars.js | 3 +- litellm/__init__.py | 1 + litellm/constants.py | 1 + .../get_llm_provider_logic.py | 2 + litellm/llms/oci/chat/transformation.py | 850 ++++++++++++++++++ litellm/llms/oci/common_utils.py | 19 + litellm/main.py | 20 + litellm/types/llms/oci.py | 183 ++++ litellm/types/utils.py | 1 + litellm/utils.py | 2 + .../oci/chat/test_oci_chat_transformation.py | 297 ++++++ 12 files changed, 1462 insertions(+), 1 deletion(-) create mode 100644 docs/my-website/docs/providers/oci.md create mode 100644 litellm/llms/oci/chat/transformation.py create mode 100644 litellm/llms/oci/common_utils.py create mode 100644 litellm/types/llms/oci.py create mode 100644 tests/test_litellm/llms/oci/chat/test_oci_chat_transformation.py diff --git a/docs/my-website/docs/providers/oci.md b/docs/my-website/docs/providers/oci.md new file mode 100644 index 00000000000..36971376866 --- /dev/null +++ b/docs/my-website/docs/providers/oci.md @@ -0,0 +1,84 @@ +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +# Oracle Cloud Infrastructure (OCI) +LiteLLM supports the following models for OCI on-demand GenAI API. + +Check the [OCI Models List](https://docs.oracle.com/en-us/iaas/Content/generative-ai/pretrained-models.htm) to see if the model is available for your region. + +- `cohere.command-a-03-2025` +- `cohere.command-r-08-2024` +- `cohere.command-plus-latest` (alias `cohere.command-r-plus-08-2024`) +- `cohere.command-r-16k` (deprecated) +- `cohere.command-r-plus` (deprecated) + +- `meta.llama-4-maverick-17b-128e-instruct-fp8` +- `meta.llama-4-scout-17b-16e-instruct` +- `meta.llama-3.3-70b-instruct` +- `meta.llama-3.2-90b-vision-instruct` +- `meta.llama-3.2-11b-vision-instruct` +- `meta.llama-3.1-405b-instruct` +- `meta.llama-3.1-70b-instruct` +- `meta.llama-3-70b-instruct` + +- `xai.grok-4` +- `xai.grok-3` +- `xai.grok-3-fast` +- `xai.grok-3-mini` +- `xai.grok-3-mini-fast` + +## Authentication + +LiteLLM uses OCI signing key authentication. Follow the [official Oracle tutorial](https://docs.oracle.com/en-us/iaas/Content/API/Concepts/apisigningkey.htm) to create a signing key and obtain the following parameters: + +- `user` +- `fingerprint` +- `tenancy` +- `region` +- `key_file` + +## Usage + +Input the parameters obtained from the OCI signing key creation process into the `completion` function. + +```python +import os +from litellm import completion + +messages = [{"role": "user", "content": "Hey! how's it going?"}] +response = completion( + model="oci/xai.grok-4", + messages=messages, + oci_region=, + oci_user=, + oci_fingerprint=, + oci_tenancy=, + oci_key=, + oci_compartment_id=, +) +print(response) +``` + + +## Usage - Streaming +Just set `stream=True` when calling completion. + +```python +import os +from litellm import completion + +messages = [{"role": "user", "content": "Hey! how's it going?"}] +response = completion( + model="oci/xai.grok-4", + messages=messages, + stream=True, + oci_region=, + oci_user=, + oci_fingerprint=, + oci_tenancy=, + oci_key=, + oci_compartment_id=, +) +for chunk in response: + print(chunk["choices"][0]["delta"]["content"]) # same as openai format +``` diff --git a/docs/my-website/sidebars.js b/docs/my-website/sidebars.js index 83ebfe29dc4..bffa8a91b6d 100644 --- a/docs/my-website/sidebars.js +++ b/docs/my-website/sidebars.js @@ -469,7 +469,8 @@ const sidebars = { "providers/featherless_ai", "providers/nebius", "providers/dashscope", - "providers/bytez" + "providers/bytez", + "providers/oci", ], }, { diff --git a/litellm/__init__.py b/litellm/__init__.py index 8cf7d3922a9..03cec9afb15 100644 --- a/litellm/__init__.py +++ b/litellm/__init__.py @@ -1199,6 +1199,7 @@ from .llms.nebius.chat.transformation import NebiusConfig from .llms.dashscope.chat.transformation import DashScopeChatConfig from .llms.moonshot.chat.transformation import MoonshotChatConfig from .llms.v0.chat.transformation import V0ChatConfig +from .llms.oci.chat.transformation import OCIChatConfig from .llms.morph.chat.transformation import MorphChatConfig from .llms.lambda_ai.chat.transformation import LambdaAIChatConfig from .llms.hyperbolic.chat.transformation import HyperbolicChatConfig diff --git a/litellm/constants.py b/litellm/constants.py index b25e2ff0fb9..27cea0eb040 100644 --- a/litellm/constants.py +++ b/litellm/constants.py @@ -279,6 +279,7 @@ LITELLM_CHAT_PROVIDERS = [ "dashscope", "moonshot", "v0", + "oci", "morph", "lambda_ai", ] diff --git a/litellm/litellm_core_utils/get_llm_provider_logic.py b/litellm/litellm_core_utils/get_llm_provider_logic.py index 4e0a2efb0c6..702196a7f05 100644 --- a/litellm/litellm_core_utils/get_llm_provider_logic.py +++ b/litellm/litellm_core_utils/get_llm_provider_logic.py @@ -356,6 +356,8 @@ def get_llm_provider( # noqa: PLR0915 # bytez models elif model.startswith("bytez/"): custom_llm_provider = "bytez" + elif model.startswith("oci/"): + custom_llm_provider = "oci" if not custom_llm_provider: if litellm.suppress_debug_info is False: print() # noqa diff --git a/litellm/llms/oci/chat/transformation.py b/litellm/llms/oci/chat/transformation.py new file mode 100644 index 00000000000..e08c1d12407 --- /dev/null +++ b/litellm/llms/oci/chat/transformation.py @@ -0,0 +1,850 @@ +import base64 +import datetime +import hashlib +from urllib.parse import urlparse +import litellm +import json +from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple, Union +from cryptography.hazmat.primitives import hashes, serialization +from cryptography.hazmat.primitives.asymmetric import padding, rsa + +import httpx + +from litellm.litellm_core_utils.logging_utils import track_llm_api_timing +from litellm.llms.base_llm.chat.transformation import BaseConfig, BaseLLMException +from litellm.llms.custom_httpx.http_handler import ( + AsyncHTTPHandler, + HTTPHandler, + _get_httpx_client, + get_async_httpx_client, + version, +) +from litellm.types.llms.openai import AllMessageValues +from litellm.types.utils import LlmProviders +from litellm.utils import ( + ChatCompletionMessageToolCall, + CustomStreamWrapper, + ModelResponse, + Usage, +) +from litellm.types.llms.oci import ( + OCIChatRequestPayload, + OCICompletionPayload, + OCICompletionResponse, + OCIContentPartUnion, + OCIImageContentPart, + OCIMessage, + OCIRoles, + OCIServingMode, + OCIStreamChunk, + OCITextContentPart, + OCIToolCall, + OCIToolDefinition, + OCIVendors, +) +from litellm.llms.oci.common_utils import OCIError +from litellm.types.utils import ( + Delta, + ModelResponseStream, + StreamingChoices, +) + +if TYPE_CHECKING: + from litellm.litellm_core_utils.litellm_logging import Logging as _LiteLLMLoggingObj + + LiteLLMLoggingObj = _LiteLLMLoggingObj +else: + LiteLLMLoggingObj = Any + + +def sha256_base64(data: bytes) -> str: + digest = hashlib.sha256(data).digest() + return base64.b64encode(digest).decode() + + +def build_signature_string(method, path, headers, signed_headers): + lines = [] + for header in signed_headers: + if header == "(request-target)": + value = f"{method.lower()} {path}" + else: + value = headers[header] + lines.append(f"{header}: {value}") + return "\n".join(lines) + + +def load_private_key_from_str(key_str: str): + key = serialization.load_pem_private_key( + key_str.encode("utf-8"), + password=None, + ) + if not isinstance(key, rsa.RSAPrivateKey): + raise TypeError( + "The provided private key is not an RSA key, which is required for OCI signing." + ) + return key + + +def get_vendor_from_model(model: str) -> OCIVendors: + """ + Extracts the vendor from the model name. + Args: + model (str): The model name. + Returns: + str: The vendor name. + """ + vendor = model.split(".")[0].lower() + if vendor == "cohere": + return OCIVendors.COHERE + else: + return OCIVendors.GENERIC + + +# 5 minute timeout (models may need to load) +STREAMING_TIMEOUT = 60 * 5 + + +class OCIChatConfig(BaseConfig): + """ + Configuration class for OCI's API interface. + """ + + def __init__( + self, + ) -> None: + locals_ = locals().copy() + for key, value in locals_.items(): + if key != "self" and value is not None: + setattr(self.__class__, key, value) + # mark the class as using a custom stream wrapper because the default only iterates on lines + setattr(self.__class__, "has_custom_stream_wrapper", True) + + self.openai_to_oci_generic_param_map = { + "stream": "isStream", + "max_tokens": "maxTokens", + "max_completion_tokens": "maxTokens", + "temperature": "temperature", + "tools": "tools", + "frequency_penalty": "frequencyPenalty", + "logprobs": "logProbs", + "logit_bias": "logitBias", + "n": "numGenerations", + "presence_penalty": "presencePenalty", + "seed": "seed", + "stop": "stop", + "tool_choice": "toolChoice", + "top_p": "topP", + "max_retries": False, + "top_logprobs": False, + "modalities": False, + "prediction": False, + "stream_options": False, + "function_call": False, + "functions": False, + "extra_headers": False, + "parallel_tool_calls": False, + "audio": False, + "web_search_options": False, + } + + def get_supported_openai_params(self, model: str) -> List[str]: + supported_params = [] + vendor = get_vendor_from_model(model) + if vendor == OCIVendors.COHERE: + raise ValueError( + "Cohere models are not yet supported in the litellm OCI chat completion endpoint. Use the Cohere API directly." + ) + else: + open_ai_to_oci_param_map = self.openai_to_oci_generic_param_map + for key, value in open_ai_to_oci_param_map.items(): + if value: + supported_params.append(key) + + return supported_params + + def map_openai_params( + self, + non_default_params: dict, + optional_params: dict, + model: str, + drop_params: bool, + ) -> dict: + adapted_params = {} + vendor = get_vendor_from_model(model) + if vendor == OCIVendors.COHERE: + raise ValueError( + "Cohere models are not yet supported in the litellm OCI chat completion endpoint. Use the Cohere API directly." + ) + else: + open_ai_to_oci_param_map = self.openai_to_oci_generic_param_map + + all_params = {**non_default_params, **optional_params} + + for key, value in all_params.items(): + alias = open_ai_to_oci_param_map.get(key) + + if alias is False: + if drop_params: + continue + + raise Exception(f"param `{key}` is not supported on OCI") + + if alias is None: + adapted_params[key] = value + continue + + adapted_params[alias] = value + + return adapted_params + + def sign_request( + self, + headers: dict, + optional_params: dict, + request_data: dict, + api_base: str, + api_key: Optional[str] = None, + model: Optional[str] = None, + stream: Optional[bool] = None, + fake_stream: Optional[bool] = None, + ) -> Tuple[dict, Optional[bytes]]: + """ + Some providers like Bedrock require signing the request. The sign request funtion needs access to `request_data` and `complete_url` + Args: + headers: dict + optional_params: dict + request_data: dict - the request body being sent in http request + api_base: str - the complete url being sent in http request + Returns: + dict - the signed headers + """ + import json + + oci_region = optional_params.get("oci_region", "us-ashburn-1") + api_base = ( + api_base + or litellm.api_base + or f"https://inference.generativeai.{oci_region}.oci.oraclecloud.com" + ) + oci_user = optional_params.get("oci_user") + oci_fingerprint = optional_params.get("oci_fingerprint") + oci_tenancy = optional_params.get("oci_tenancy") + oci_key = optional_params.get("oci_key") + + if not oci_user or not oci_fingerprint or not oci_tenancy or not oci_key: + raise Exception( + "Missing one of the following parameters: oci_user, oci_fingerprint, oci_tenancy, oci_key" + ) + + method = str(optional_params.get("method", "POST")).upper() + body = json.dumps(request_data).encode("utf-8") + parsed = urlparse(api_base) + path = parsed.path or "/" + host = parsed.netloc + + date = datetime.datetime.utcnow().strftime("%a, %d %b %Y %H:%M:%S GMT") + content_type = headers.get("content-type", "application/json") + content_length = str(len(body)) + x_content_sha256 = sha256_base64(body) + + headers_to_sign = { + "date": date, + "host": host, + "content-type": content_type, + "content-length": content_length, + "x-content-sha256": x_content_sha256, + } + + signed_headers = [ + "date", + "(request-target)", + "host", + "content-length", + "content-type", + "x-content-sha256", + ] + signing_string = build_signature_string( + method, path, headers_to_sign, signed_headers + ) + + private_key = load_private_key_from_str(oci_key) + signature = private_key.sign( + signing_string.encode("utf-8"), + padding.PKCS1v15(), + hashes.SHA256(), + ) + signature_b64 = base64.b64encode(signature).decode() + + key_id = f"{oci_tenancy}/{oci_user}/{oci_fingerprint}" + + authorization = ( + 'Signature version="1",' + f'keyId="{key_id}",' + 'algorithm="rsa-sha256",' + f'headers="{" ".join(signed_headers)}",' + f'signature="{signature_b64}"' + ) + + headers.update( + { + "authorization": authorization, + "date": date, + "host": host, + "content-type": content_type, + "content-length": content_length, + "x-content-sha256": x_content_sha256, + } + ) + + return headers, None + + def validate_environment( + self, + headers: dict, + model: str, + messages: List[AllMessageValues], + optional_params: dict, + litellm_params: dict, + api_key: Optional[str] = None, + api_base: Optional[str] = None, + ) -> dict: + oci_region = optional_params.get("oci_region", "us-ashburn-1") + api_base = ( + api_base + or litellm.api_base + or f"https://inference.generativeai.{oci_region}.oci.oraclecloud.com" + ) + oci_user = optional_params.get("oci_user") + oci_fingerprint = optional_params.get("oci_fingerprint") + oci_tenancy = optional_params.get("oci_tenancy") + oci_key = optional_params.get("oci_key") + oci_compartment_id = optional_params.get("oci_compartment_id") + + if ( + not oci_user + or not oci_fingerprint + or not oci_tenancy + or not oci_key + or not oci_compartment_id + ): + raise Exception( + "Missing one of the following parameters: oci_user, oci_fingerprint, oci_tenancy, oci_key, oci_compartment_id" + ) + + if not api_base: + raise Exception( + "Either `api_base` must be provided or `litellm.api_base` must be set. Alternatively, you can set the `oci_region` optional parameter to use the default OCI region." + ) + + headers.update( + { + "content-type": "application/json", + "user-agent": f"litellm/{version}", + } + ) + + if not messages: + raise Exception( + "kwarg `messages` must be an array of messages that follow the openai chat standard" + ) + + return headers + + def get_complete_url( + self, + api_base: Optional[str], + api_key: Optional[str], + model: str, + optional_params: dict, + litellm_params: dict, + stream: Optional[bool] = None, + ) -> str: + oci_region = optional_params.get("oci_region", "us-ashburn-1") + return f"https://inference.generativeai.{oci_region}.oci.oraclecloud.com/20231130/actions/chat" + + def _get_optional_params(self, vendor: OCIVendors, optional_params: dict) -> Dict: + selected_params = {} + if vendor == OCIVendors.COHERE: + raise ValueError( + "Cohere models are not yet supported in the litellm OCI chat completion endpoint. Use the Cohere API directly." + ) + else: + open_ai_to_oci_param_map = self.openai_to_oci_generic_param_map + + for value in open_ai_to_oci_param_map.values(): + if value in optional_params: + selected_params[value] = optional_params[value] + if "tools" in selected_params: + selected_params["tools"] = adapt_tool_definition_to_oci_standard( + selected_params["tools"], vendor + ) + return selected_params + + def transform_request( + self, + model: str, + messages: List[AllMessageValues], + optional_params: dict, + litellm_params: dict, + headers: dict, + ) -> dict: + oci_compartment_id = optional_params.get("oci_compartment_id", None) + if not oci_compartment_id: + raise Exception("kwarg `oci_compartment_id` is required for OCI requests") + + vendor = get_vendor_from_model(model) + + if vendor == OCIVendors.COHERE: + raise Exception( + "Cohere models are not yet supported in the litellm OCI chat completion endpoint. Use the Cohere API directly." + ) + else: + data = OCICompletionPayload( + compartmentId=oci_compartment_id, + servingMode=OCIServingMode( + servingType="ON_DEMAND", + modelId=model, + ), + chatRequest=OCIChatRequestPayload( + apiFormat=vendor.value, + messages=adapt_messages_to_generic_oci_standard(messages), + **self._get_optional_params(vendor, optional_params), + ), + ) + + return data.model_dump(exclude_none=True) + + def transform_response( + self, + model: str, + raw_response: httpx.Response, + model_response: ModelResponse, + logging_obj: LiteLLMLoggingObj, + request_data: dict, + messages: List[AllMessageValues], + optional_params: dict, + litellm_params: dict, + encoding: Any, + api_key: Optional[str] = None, + json_mode: Optional[bool] = None, + ) -> ModelResponse: + json = raw_response.json() # noqa: F811 + + error = json.get("error") + + if error is not None: + raise OCIError( + message=str(json["error"]), + status_code=raw_response.status_code, + ) + + if not isinstance(json, dict): + raise OCIError( + message="Invalid response format from OCI", + status_code=raw_response.status_code, + ) + + try: + completion_response = OCICompletionResponse(**json) + except TypeError as e: + raise OCIError( + message=f"Response cannot be casted to OCICompletionResponse: {str(e)}", + status_code=raw_response.status_code, + ) + + vendor = get_vendor_from_model(model) + if vendor == OCIVendors.COHERE: + raise ValueError( + "Cohere models are not yet supported in the litellm OCI chat completion endpoint. Use the Cohere API directly." + ) + else: + iso_str = completion_response.chatResponse.timeCreated + dt = datetime.datetime.fromisoformat(iso_str.replace("Z", "+00:00")) + model_response.created = int(dt.timestamp()) + + model_response.model = completion_response.modelId + + message = model_response.choices[0].message # type: ignore + if vendor == OCIVendors.COHERE: + raise ValueError( + "Cohere models are not yet supported in the litellm OCI chat completion endpoint. Use the Cohere API directly." + ) + else: + response_message = completion_response.chatResponse.choices[0].message + if response_message.content and response_message.content[0].type == "TEXT": + message.content = response_message.content[0].text + if response_message.toolCalls: + message.tool_calls = adapt_tools_to_openai_standard( + response_message.toolCalls + ) + + usage = Usage( + prompt_tokens=completion_response.chatResponse.usage.promptTokens, + completion_tokens=completion_response.chatResponse.usage.completionTokens, + total_tokens=completion_response.chatResponse.usage.totalTokens, + ) + model_response.usage = usage # type: ignore + + model_response._hidden_params["additional_headers"] = raw_response.headers + + return model_response + + @track_llm_api_timing() + def get_sync_custom_stream_wrapper( + self, + model: str, + custom_llm_provider: str, + logging_obj: LiteLLMLoggingObj, + api_base: str, + headers: dict, + data: dict, + messages: list, + client: Optional[Union[HTTPHandler, AsyncHTTPHandler]] = None, + json_mode: Optional[bool] = None, + signed_json_body: Optional[bytes] = None, + ) -> "OCIStreamWrapper": + if "stream" in data: + del data["stream"] + if client is None or isinstance(client, AsyncHTTPHandler): + client = _get_httpx_client(params={}) + + try: + response = client.post( + api_base, + headers=headers, + data=json.dumps(data), + stream=True, + logging_obj=logging_obj, + timeout=STREAMING_TIMEOUT, + ) + except httpx.HTTPStatusError as e: + raise OCIError(status_code=e.response.status_code, message=e.response.text) + + if response.status_code != 200: + raise OCIError(status_code=response.status_code, message=response.text) + + completion_stream = response.iter_text() + + streaming_response = OCIStreamWrapper( + completion_stream=completion_stream, + model=model, + custom_llm_provider=custom_llm_provider, + logging_obj=logging_obj, + ) + return streaming_response + + @track_llm_api_timing() + async def get_async_custom_stream_wrapper( + self, + model: str, + custom_llm_provider: str, + logging_obj: LiteLLMLoggingObj, + api_base: str, + headers: dict, + data: dict, + messages: list, + client: Optional[Union[HTTPHandler, AsyncHTTPHandler]] = None, + json_mode: Optional[bool] = None, + signed_json_body: Optional[bytes] = None, + ) -> "OCIStreamWrapper": + if "stream" in data: + del data["stream"] + + if client is None or isinstance(client, HTTPHandler): + client = get_async_httpx_client(llm_provider=LlmProviders.BYTEZ, params={}) + + try: + response = await client.post( + api_base, + headers=headers, + data=json.dumps(data), + stream=True, + logging_obj=logging_obj, + timeout=STREAMING_TIMEOUT, + ) + except httpx.HTTPStatusError as e: + raise OCIError(status_code=e.response.status_code, message=e.response.text) + + if response.status_code != 200: + raise OCIError(status_code=response.status_code, message=response.text) + + completion_stream = response.aiter_text() + + streaming_response = OCIStreamWrapper( + completion_stream=completion_stream, + model=model, + custom_llm_provider=custom_llm_provider, + logging_obj=logging_obj, + ) + return streaming_response + + def get_error_class( + self, error_message: str, status_code: int, headers: Union[dict, httpx.Headers] + ) -> BaseLLMException: + return OCIError(status_code=status_code, message=error_message) + + +open_ai_to_generic_oci_role_map: Dict[str, OCIRoles] = { + "system": "SYSTEM", + "user": "USER", + "assistant": "ASSISTANT", + "tool": "TOOL", +} + + +def adapt_messages_to_generic_oci_standard_content_message( + role: str, content: str | list +) -> OCIMessage: + new_content: list[OCIContentPartUnion] = [] + if isinstance(content, str): + return OCIMessage( + role=open_ai_to_generic_oci_role_map[role], + content=[OCITextContentPart(text=content)], + toolCalls=None, + toolCallId=None, + ) + + # content is a list of content items: + # [ + # {"type": "text", "text": "Hello"}, + # {"type": "image_url", "image_url": "https://example.com/image.png"} + # ] + for content_item in content: + if not isinstance(content_item, dict): + raise Exception("Each content item must be a dictionary") + + type = content_item.get("type") + if not isinstance(type, str): + raise Exception("Prop `type` is not a string") + + if type not in ["text", "image_url"]: + raise Exception(f"Prop `{type}` is not supported") + + if type == "text": + text = content_item.get("text") + if not isinstance(text, str): + raise Exception("Prop `text` is not a string") + new_content.append(OCITextContentPart(text=text)) + + elif type == "image_url": + image_url = content_item.get("image_url") + if not isinstance(image_url, str): + raise Exception("Prop `image_url` is not a string") + new_content.append(OCIImageContentPart(imageUrl=image_url)) + + return OCIMessage( + role=open_ai_to_generic_oci_role_map[role], + content=new_content, + toolCalls=None, + toolCallId=None, + ) + + +def adapt_messages_to_generic_oci_standard_tool_call( + role: str, tool_calls: list +) -> OCIMessage: + tool_calls_formated = [] + for tool_call in tool_calls: + if not isinstance(tool_call, dict): + raise Exception("Each tool call must be a dictionary") + + if tool_call.get("type") != "function": + raise Exception("OCI only supports function tools") + + tool_call_id = tool_call.get("id") + if not isinstance(tool_call_id, str): + raise Exception("Prop `id` is not a string") + + tool_function = tool_call.get("function") + if not isinstance(tool_function, dict): + raise Exception("Prop `function` is not a dictionary") + + function_name = tool_function.get("name") + if not isinstance(function_name, str): + raise Exception("Prop `name` is not a string") + + arguments = tool_call["function"].get("arguments", "{}") + if not isinstance(arguments, str): + raise Exception("Prop `arguments` is not a string") + + # tool_calls_formated.append(OCIToolCall( + # id=tool_call_id, + # type="FUNCTION", + # function=OCIFunction( + # name=function_name, + # arguments=arguments + # ) + # )) + + tool_calls_formated.append( + OCIToolCall( + id=tool_call_id, + type="FUNCTION", + name=function_name, + arguments=arguments, + ) + ) + + return OCIMessage( + role=open_ai_to_generic_oci_role_map[role], + content=None, + toolCalls=tool_calls_formated, + toolCallId=None, + ) + + +def adapt_messages_to_generic_oci_standard_tool_response( + role: str, tool_call_id: str, content: str +) -> OCIMessage: + return OCIMessage( + role=open_ai_to_generic_oci_role_map[role], + content=[OCITextContentPart(text=content)], + toolCalls=None, + toolCallId=tool_call_id, + ) + + +def adapt_messages_to_generic_oci_standard( + messages: List[AllMessageValues], +) -> List[OCIMessage]: + new_messages = [] + for message in messages: + role = message["role"] + content = message.get("content") + tool_calls = message.get("tool_calls") + tool_call_id = message.get("tool_call_id") + + if role in ["system", "user", "assistant"] and content is not None: + if not isinstance(content, (str, list)): + raise Exception( + "Prop `content` must be a string or a list of content items" + ) + new_messages.append( + adapt_messages_to_generic_oci_standard_content_message(role, content) + ) + + elif role == "assistant" and tool_calls is not None: + if not isinstance(tool_calls, list): + raise Exception("Prop `tool_calls` must be a list of tool calls") + new_messages.append( + adapt_messages_to_generic_oci_standard_tool_call(role, tool_calls) + ) + + elif role == "tool": + if not isinstance(tool_call_id, str): + raise Exception("Prop `tool_call_id` is required and must be a string") + if not isinstance(content, str): + raise Exception("Prop `content` is not a string") + new_messages.append( + adapt_messages_to_generic_oci_standard_tool_response( + role, tool_call_id, content + ) + ) + + return new_messages + + +def adapt_tool_definition_to_oci_standard(tools: List[Dict], vendor: OCIVendors): + new_tools = [] + if vendor == OCIVendors.COHERE: + raise ValueError( + "Cohere models are not yet supported in the litellm OCI chat completion endpoint. Use the Cohere API directly." + ) + else: + for tool in tools: + if tool["type"] != "function": + raise Exception("OCI only supports function tools") + + tool_function = tool.get("function") + if not isinstance(tool_function, dict): + raise Exception("Prop `function` is not a dictionary") + + new_tool = OCIToolDefinition( + type="FUNCTION", + name=tool_function.get("name"), + description=tool_function.get("description", ""), + parameters=tool_function.get("parameters", {}), + ) + new_tools.append(new_tool) + + return new_tools + + +def adapt_tools_to_openai_standard( + tools: list[OCIToolCall], +) -> list[ChatCompletionMessageToolCall]: + new_tools = [] + for tool in tools: + new_tool = ChatCompletionMessageToolCall( + id=tool.id, + type="function", + function={ + "name": tool.name, + "arguments": tool.arguments, + }, + ) + new_tools.append(new_tool) + return new_tools + + +class OCIStreamWrapper(CustomStreamWrapper): + """ + Custom stream wrapper for OCI responses. + This class is used to handle streaming responses from OCI's API. + """ + + def __init__( + self, + **kwargs: Any, + ): + super().__init__(**kwargs) + + def chunk_creator(self, chunk: Any): + if not isinstance(chunk, str): + raise ValueError(f"Chunk is not a string: {chunk}") + if not chunk.startswith("data:"): + raise ValueError(f"Chunk does not start with 'data:': {chunk}") + dict_chunk = json.loads(chunk[5:]) # Remove 'data: ' prefix and parse JSON + try: + typed_chunk = OCIStreamChunk(**dict_chunk) + except TypeError as e: + raise ValueError(f"Chunk cannot be casted to OCIStreamChunk: {str(e)}") + + if typed_chunk.index is None: + typed_chunk.index = 0 + + text = "" + if typed_chunk.message and typed_chunk.message.content: + for item in typed_chunk.message.content: + if isinstance(item, OCITextContentPart): + text += item.text + elif isinstance(item, OCIImageContentPart): + raise ValueError( + "OCI does not support image content in streaming responses" + ) + else: + raise ValueError( + f"Unsupported content type in OCI response: {item.type}" + ) + + tool_calls = None + if typed_chunk.message and typed_chunk.message.toolCalls: + tool_calls = adapt_tools_to_openai_standard(typed_chunk.message.toolCalls) + + return ModelResponseStream( + choices=[ + StreamingChoices( + index=typed_chunk.index if typed_chunk.index else 0, + delta=Delta( + content=text, + tool_calls=[tool.model_dump() for tool in tool_calls] + if tool_calls + else None, + provider_specific_fields=None, # OCI does not have provider specific fields in the response + thinking_blocks=None, # OCI does not have thinking blocks in the response + reasoning_content=None, # OCI does not have reasoning content in the response + ), + finish_reason=typed_chunk.finishReason, + ) + ] + ) diff --git a/litellm/llms/oci/common_utils.py b/litellm/llms/oci/common_utils.py new file mode 100644 index 00000000000..661a6c89e4b --- /dev/null +++ b/litellm/llms/oci/common_utils.py @@ -0,0 +1,19 @@ +from typing import Optional + +import httpx + +from litellm.llms.base_llm.chat.transformation import BaseLLMException + + +class OCIError(BaseLLMException): + def __init__( + self, + status_code: int, + message: str, + headers: Optional[httpx.Headers] = None, + ): + super().__init__( + status_code=status_code, + message=message, + headers=headers, + ) diff --git a/litellm/main.py b/litellm/main.py index 4fc81cce6db..a6583ab0593 100644 --- a/litellm/main.py +++ b/litellm/main.py @@ -151,6 +151,7 @@ from .llms.gemini.common_utils import get_api_key_from_env from .llms.groq.chat.handler import GroqChatCompletion from .llms.huggingface.embedding.handler import HuggingFaceEmbedding from .llms.nlp_cloud.chat.handler import completion as nlp_cloud_chat_completion +from .llms.oci.chat.transformation import OCIChatConfig from .llms.ollama.completion import handler as ollama from .llms.oobabooga.chat import oobabooga from .llms.openai.completion.handler import OpenAITextCompletion @@ -252,6 +253,7 @@ base_llm_http_handler = BaseLLMHTTPHandler() base_llm_aiohttp_handler = BaseLLMAIOHTTPHandler() sagemaker_chat_completion = SagemakerChatHandler() bytez_transformation = BytezChatConfig() +oci_transformation = OCIChatConfig() ####### COMPLETION ENDPOINTS ################ @@ -2399,6 +2401,24 @@ def completion( # type: ignore # noqa: PLR0915 encoding=encoding, stream=stream, ) + elif custom_llm_provider == "oci": + response = base_llm_http_handler.completion( + model=model, + messages=messages, + headers=headers, + model_response=model_response, + api_key=api_key, + api_base=api_base, + acompletion=acompletion, + logging_obj=logging, + optional_params=optional_params, + litellm_params=litellm_params, + timeout=timeout, # type: ignore + client=client, + custom_llm_provider=custom_llm_provider, + encoding=encoding, + stream=stream, + ) elif custom_llm_provider == "oobabooga": custom_llm_provider = "oobabooga" model_response = oobabooga.completion( diff --git a/litellm/types/llms/oci.py b/litellm/types/llms/oci.py new file mode 100644 index 00000000000..cb76bf87473 --- /dev/null +++ b/litellm/types/llms/oci.py @@ -0,0 +1,183 @@ +from __future__ import annotations + +from typing import Any, Literal, Union + +from pydantic import BaseModel +from enum import Enum + +OCIRoles = Literal["SYSTEM", "USER", "ASSISTANT", "TOOL"] + + +class OCIVendors(Enum): + """ + A class to hold the vendor names for OCI models. + This is used to map model names to their respective vendors. + """ + + COHERE = "COHERE" + GENERIC = "GENERIC" + + +# --- Base Models and Content Parts --- + + +class OCIContentPart(BaseModel): + """Base model for content parts in an OCI message.""" + + type: str + + +class OCITextContentPart(OCIContentPart): + """Text content part for the OCI API.""" + + type: Literal["TEXT"] = "TEXT" + text: str + + +class OCIImageContentPart(OCIContentPart): + """Image content part for the OCI API.""" + + type: Literal["IMAGE"] = "IMAGE" + imageUrl: str + + +OCIContentPartUnion = Union[OCITextContentPart, OCIImageContentPart] + +# --- Models for Tools and Tool Calls --- + + +class OCIToolCall(BaseModel): + """Represents a tool call made by the model.""" + + id: str + type: Literal["FUNCTION"] = "FUNCTION" + name: str + arguments: str # Arguments should be a JSON-serialized string + + +class OCIToolDefinition(BaseModel): + """Defines a tool that can be used by the model.""" + + type: Literal["FUNCTION"] = "FUNCTION" + name: str | None = None + description: str | None = None + parameters: dict | None = None + + +# --- Message Models (Request and Response) --- + + +class OCIMessage(BaseModel): + """Model for a single message in the request/response payload.""" + + role: OCIRoles + content: list[OCIContentPartUnion] | None = None + toolCalls: list[OCIToolCall] | None = None + toolCallId: str | None = None + + +# --- Request Payload Models --- + + +class OCIChatRequestPayload(BaseModel): + """Internal 'chatRequest' payload for the OCI API.""" + + apiFormat: str + messages: list[OCIMessage] + tools: list[OCIToolDefinition] | None = None + isStream: bool = False + numGenerations: int | None = None + maxTokens: int | None = None + temperature: float | None = None + topP: float | None = None + stop: list[str] | None = None + seed: int | None = None + frequencyPenalty: float | None = None + presencePenalty: float | None = None + + +class OCIServingMode(BaseModel): + """Defines the serving mode and the model to be used.""" + + servingType: str + modelId: str + + +class OCICompletionPayload(BaseModel): + """Pydantic model for the complete OCI chat request body.""" + + compartmentId: str + servingMode: OCIServingMode + chatRequest: OCIChatRequestPayload + + +# --- API Response Models (Non-streaming) --- + + +class OCICompletionTokenDetails(BaseModel): + """Completion token details in the OCI response.""" + + acceptedPredictionTokens: int + reasoningTokens: int + + +class OCIPropmtTokensDetails(BaseModel): + """Prompt token details in the OCI response.""" + + cachedTokens: int + + +class OCIResponseUsage(BaseModel): + """Token usage in the OCI response.""" + + promptTokens: int + completionTokens: int + totalTokens: int + completionTokensDetails: OCICompletionTokenDetails + promptTokensDetails: OCIPropmtTokensDetails + + +class OCIResponseChoice(BaseModel): + """A completion choice in the OCI response.""" + + index: int + message: OCIMessage + finishReason: str | None + logprobs: dict[str, Any] | None = None + + +class OCIChatResponse(BaseModel): + """The 'chatResponse' object in the OCI response.""" + + apiFormat: str + timeCreated: str + choices: list[OCIResponseChoice] + usage: OCIResponseUsage + + +class OCICompletionResponse(BaseModel): + """Model for the complete non-streaming OCI response body.""" + + modelId: str + modelVersion: str + chatResponse: OCIChatResponse + + +# --- API Response Models (Streaming) --- + + +class OCIStreamDelta(BaseModel): + """The content delta in a streaming chunk.""" + + content: list[OCIContentPartUnion] | None = None + role: str | None = None + toolCalls: list[OCIToolCall] | None = None + + +class OCIStreamChunk(BaseModel): + """Model for a single SSE event chunk from OCI.""" + + finishReason: str | None = None + message: OCIStreamDelta | None = None + pad: str | None = None + index: int | None = None diff --git a/litellm/types/utils.py b/litellm/types/utils.py index c5af6e4ed0a..f8df5781deb 100644 --- a/litellm/types/utils.py +++ b/litellm/types/utils.py @@ -2321,6 +2321,7 @@ class LlmProviders(str, Enum): PG_VECTOR = "pg_vector" HYPERBOLIC = "hyperbolic" RECRAFT = "recraft" + OCI = "oci" AUTO_ROUTER = "auto_router" DOTPROMPT = "dotprompt" diff --git a/litellm/utils.py b/litellm/utils.py index 9e9a112f304..a9695e14c0f 100644 --- a/litellm/utils.py +++ b/litellm/utils.py @@ -6916,6 +6916,8 @@ class ProviderConfigManager: return litellm.OpenAIGPTConfig() elif litellm.LlmProviders.NSCALE == provider: return litellm.NscaleConfig() + elif litellm.LlmProviders.OCI == provider: + return litellm.OCIChatConfig() elif litellm.LlmProviders.HYPERBOLIC == provider: return litellm.HyperbolicChatConfig() return None diff --git a/tests/test_litellm/llms/oci/chat/test_oci_chat_transformation.py b/tests/test_litellm/llms/oci/chat/test_oci_chat_transformation.py new file mode 100644 index 00000000000..d5369253503 --- /dev/null +++ b/tests/test_litellm/llms/oci/chat/test_oci_chat_transformation.py @@ -0,0 +1,297 @@ +import datetime +import os +import sys +import httpx +import pytest +import json + +import litellm + +# Adds the parent directory to the system path +sys.path.insert(0, os.path.abspath("../../../../..")) + +from litellm import ModelResponse +from litellm.llms.oci.chat.transformation import OCIChatConfig, version + +TEST_MODEL_NAME = "xai.grok-4" +TEST_MODEL = f"oci/{TEST_MODEL_NAME}" +TEST_MESSAGES = [{"role": "user", "content": "Hello, how are you?"}] +TEST_COMPARTMENT_ID = "ocid1.compartment.oc1..xxxxxx" +TEST_OCI_PARAMS = { + "oci_region": "us-ashburn-1", + "oci_user": "ocid1.user.oc1..xxxxxxEXAMPLExxxxxx", + "oci_fingerprint": "4f:29:77:cc:b1:3e:55:ab:61:2a:de:47:f1:38:4c:90", + "oci_tenancy": "ocid1.tenancy.oc1..xxxxxxEXAMPLExxxxxx", + "oci_compartment_id": TEST_COMPARTMENT_ID, + "oci_key": "" +} + + +class TestOCIChatConfig: + def test_validate_environment_with_oci_region(self): + config = OCIChatConfig() + + headers = {} + + result = config.validate_environment( + headers=headers, + model=TEST_MODEL, + messages=TEST_MESSAGES, # type: ignore + optional_params=TEST_OCI_PARAMS, + litellm_params={}, + ) + + assert result["content-type"] == "application/json" + assert result["user-agent"] == f"litellm/{version}" + + def test_missing_oci_auth_parameters(self): + optional_params = TEST_OCI_PARAMS.copy() + optional_params.pop("oci_region") + + # Remove optional_params one by one and verify that an exception is raised + for key in optional_params.keys(): + modified_params = optional_params.copy() + del modified_params[key] + + with pytest.raises(Exception) as excinfo: + config = OCIChatConfig() + + headers = {} + + config.validate_environment( + headers=headers, + model=TEST_MODEL, + messages=TEST_MESSAGES, # type: ignore + optional_params=modified_params, + api_base="https://api.oci.example.com", + litellm_params={}, + ) + + assert f"Missing one of the following parameters: oci_user, oci_fingerprint, oci_tenancy, oci_key, oci_compartment_id" in str(excinfo.value) + + def test_transform_request_simple(self): + """ + Tests if a simple request is transformed correctly. + """ + config = OCIChatConfig() + optional_params = {"oci_compartment_id": TEST_COMPARTMENT_ID} + transformed_request = config.transform_request( + model=TEST_MODEL_NAME, + messages=TEST_MESSAGES, # type: ignore + optional_params=optional_params, + litellm_params={}, + headers={}, + ) + + expected_output = { + "compartmentId": TEST_COMPARTMENT_ID, + "servingMode": {"servingType": "ON_DEMAND", "modelId": TEST_MODEL_NAME}, + "chatRequest": { + "apiFormat": "GENERIC", + "isStream": False, + "messages": [ + { + "role": "USER", + "content": [{"type": "TEXT", "text": "Hello, how are you?"}], + } + ], + }, + } + assert transformed_request == expected_output + + def test_transform_request_with_tools(self): + """ + Tests if a request with tools is transformed correctly. + """ + config = OCIChatConfig() + tools = [ + { + "type": "function", + "function": { + "name": "get_current_weather", + "description": "Get the current weather in a given location", + "parameters": { + "type": "object", + "properties": { + "location": { + "type": "string", + "description": "The city and state, e.g. San Francisco, CA", + }, + }, + "required": ["location"], + }, + }, + } + ] + optional_params = { + "oci_compartment_id": TEST_COMPARTMENT_ID, + "tools": tools, + } + transformed_request = config.transform_request( + model=TEST_MODEL_NAME, + messages=TEST_MESSAGES, # type: ignore + optional_params=optional_params, + litellm_params={}, + headers={}, + ) + assert "tools" in transformed_request["chatRequest"] + assert transformed_request["chatRequest"]["tools"][0]["name"] == "get_current_weather" + assert transformed_request["chatRequest"]["tools"][0]["type"] == "FUNCTION" + assert transformed_request["chatRequest"]["tools"][0]["description"] == "Get the current weather in a given location" + assert transformed_request["chatRequest"]["tools"][0]["parameters"] is not None + + def test_transform_response_simple_text(self): + """ + Tests if a simple text response is transformed correctly. + """ + config = OCIChatConfig() + created_time = datetime.datetime.now(datetime.timezone.utc).isoformat().replace("+00:00", "Z") + mock_oci_response = { + "modelId": TEST_MODEL_NAME, + "modelVersion": "1.0", + "chatResponse": { + "apiFormat": "GENERIC", + "choices": [ + { + "index": 0, + "message": { + "role": "ASSISTANT", + "content": [{"type": "TEXT", "text": "I am doing well, thank you!"}], + }, + "finishReason": "STOP", + } + ], + "timeCreated": created_time, + "usage": { + "promptTokens": 10, + "completionTokens": 20, + "totalTokens": 30, + "completionTokensDetails": { + "acceptedPredictionTokens": 20, + "reasoningTokens": 20, + }, + "promptTokensDetails": { + "cachedTokens": 10, + }, + }, + }, + } + response = httpx.Response( + status_code=200, json=mock_oci_response, headers={"Content-Type": "application/json"} + ) + result = config.transform_response( + model=TEST_MODEL_NAME, + raw_response=response, + model_response=ModelResponse(), + logging_obj={}, # type: ignore + request_data={}, + messages=[], + optional_params={}, + litellm_params={}, + encoding={}, + ) + + assert isinstance(result, ModelResponse) + assert len(result.choices) == 1 + assert isinstance(result.choices[0], litellm.Choices) + assert result.choices[0].message + assert result.choices[0].message.content == "I am doing well, thank you!" + assert result.choices[0].finish_reason == "stop" + assert result.model == TEST_MODEL_NAME + assert hasattr(result, "usage") + assert isinstance(result.usage, litellm.Usage) # type: ignore + assert result.usage.prompt_tokens == 10 # type: ignore + assert result.usage.completion_tokens == 20 # type: ignore + assert result.usage.total_tokens == 30 # type: ignore + + def test_transform_response_with_tool_calls(self): + """ + Tests if a response with tool calls is transformed correctly. + """ + config = OCIChatConfig() + created_time = datetime.datetime.now(datetime.timezone.utc).isoformat().replace("+00:00", "Z") + mock_oci_response = { + "modelId": TEST_MODEL_NAME, + "modelVersion": "1.0", + "chatResponse": { + "apiFormat": "GENERIC", + "choices": [ + { + "index": 0, + "message": { + "role": "ASSISTANT", + "content": None, + "toolCalls": [ + { + "id": "call_abc123", + "type": "FUNCTION", + "name": "get_weather", + "arguments": '{"location": "Vila Velha, BR"}', + } + ], + }, + "finishReason": "stop", + } + ], + "timeCreated": created_time, + "usage": { + "promptTokens": 10, + "completionTokens": 20, + "totalTokens": 30, + "completionTokensDetails": { + "acceptedPredictionTokens": 20, + "reasoningTokens": 20, + }, + "promptTokensDetails": { + "cachedTokens": 10, + }, + }, + }, + } + response = httpx.Response(status_code=200, json=mock_oci_response) + model_response = ModelResponse( + choices=[litellm.Choices(index=0, message=litellm.Message())] + ) + + result = config.transform_response( + model=TEST_MODEL_NAME, + raw_response=response, + model_response=model_response, + logging_obj={}, # type: ignore + request_data={}, + messages=[], + optional_params={}, + litellm_params={}, + encoding={}, + ) + + # General assertions + assert isinstance(result, ModelResponse) + assert len(result.choices) == 1 + + choice = result.choices[0] + assert isinstance(choice, litellm.Choices) + assert choice.finish_reason == "stop" + + # Message and tool_calls assertions + message = choice.message + assert isinstance(message, litellm.Message) + assert hasattr(message, "tool_calls") + assert isinstance(message.tool_calls, list) + assert len(message.tool_calls) == 1 + + # Specific tool_call assertions + tool_call = message.tool_calls[0] + assert isinstance(tool_call, litellm.utils.ChatCompletionMessageToolCall) + assert tool_call.id == "call_abc123" + assert tool_call.type == "function" + assert tool_call.function["name"] == "get_weather" + assert tool_call.function["arguments"] == '{"location": "Vila Velha, BR"}' + + # Usage assertions + assert hasattr(result, "usage") + usage = result.usage # type: ignore + assert isinstance(usage, litellm.Usage) # type: ignore + assert usage.prompt_tokens == 10 # type: ignore + assert usage.completion_tokens == 20 # type: ignore + assert usage.total_tokens == 30 # type: ignore From a17d483c89ea90ca61489760b2e48d36c942fd7a Mon Sep 17 00:00:00 2001 From: Pascal Bro Date: Tue, 5 Aug 2025 01:09:33 +0200 Subject: [PATCH 10/69] Add GCS bucket caching support (#13122) --- docs/my-website/docs/caching/all_caches.md | 40 +++++++- litellm/caching/__init__.py | 3 +- litellm/caching/caching.py | 15 +++ litellm/caching/gcs_cache.py | 97 +++++++++++++++++++ litellm/types/caching.py | 1 + .../test_gcs_cache_unit_tests.py | 6 ++ tests/test_litellm/caching/test_gcs_cache.py | 36 +++++++ 7 files changed, 192 insertions(+), 6 deletions(-) create mode 100644 litellm/caching/gcs_cache.py create mode 100644 tests/local_testing/test_gcs_cache_unit_tests.py create mode 100644 tests/test_litellm/caching/test_gcs_cache.py diff --git a/docs/my-website/docs/caching/all_caches.md b/docs/my-website/docs/caching/all_caches.md index a6be3396291..e3bf363e67d 100644 --- a/docs/my-website/docs/caching/all_caches.md +++ b/docs/my-website/docs/caching/all_caches.md @@ -1,7 +1,7 @@ import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; -# Caching - In-Memory, Redis, s3, Redis Semantic Cache, Disk +# Caching - In-Memory, Redis, s3, gcs, Redis Semantic Cache, Disk [**See Code**](https://github.com/BerriAI/litellm/blob/main/litellm/caching/caching.py) @@ -14,7 +14,7 @@ import TabItem from '@theme/TabItem'; ::: -## Initialize Cache - In Memory, Redis, s3 Bucket, Redis Semantic, Disk Cache, Qdrant Semantic +## Initialize Cache - In Memory, Redis, s3 Bucket, gcs Bucket, Redis Semantic, Disk Cache, Qdrant Semantic @@ -50,6 +50,36 @@ response2 = completion( + + +Set environment variables + +```shell +GCS_BUCKET_NAME="my-cache-bucket" +GCS_PATH_SERVICE_ACCOUNT="/path/to/service_account.json" +``` + +```python +import litellm +from litellm import completion +from litellm.caching.caching import Cache + +litellm.cache = Cache(type="gcs", gcs_bucket_name="my-cache-bucket", gcs_path_service_account="/path/to/service_account.json") + +response1 = completion( + model="gpt-3.5-turbo", + messages=[{"role": "user", "content": "Tell me a joke."}] +) +response2 = completion( + model="gpt-3.5-turbo", + messages=[{"role": "user", "content": "Tell me a joke."}] +) + +# response1 == response2, response 1 is cached +``` + + + @@ -405,7 +435,7 @@ Advanced Params ```python litellm.enable_cache( - type: Optional[Literal["local", "redis", "s3", "disk"]] = "local", + type: Optional[Literal["local", "redis", "s3", "gcs", "disk"]] = "local", host: Optional[str] = None, port: Optional[str] = None, password: Optional[str] = None, @@ -429,7 +459,7 @@ Update the Cache params ```python litellm.update_cache( - type: Optional[Literal["local", "redis", "s3", "disk"]] = "local", + type: Optional[Literal["local", "redis", "s3", "gcs", "disk"]] = "local", host: Optional[str] = None, port: Optional[str] = None, password: Optional[str] = None, @@ -490,7 +520,7 @@ cache.get_cache = get_cache ```python def __init__( self, - type: Optional[Literal["local", "redis", "redis-semantic", "s3", "disk"]] = "local", + type: Optional[Literal["local", "redis", "redis-semantic", "s3", "gcs", "disk"]] = "local", supported_call_types: Optional[ List[Literal["completion", "acompletion", "embedding", "aembedding", "atranscription", "transcription"]] ] = ["completion", "acompletion", "embedding", "aembedding", "atranscription", "transcription"], diff --git a/litellm/caching/__init__.py b/litellm/caching/__init__.py index badc462e09b..bbe90b04121 100644 --- a/litellm/caching/__init__.py +++ b/litellm/caching/__init__.py @@ -7,4 +7,5 @@ from .qdrant_semantic_cache import QdrantSemanticCache from .redis_cache import RedisCache from .redis_cluster_cache import RedisClusterCache from .redis_semantic_cache import RedisSemanticCache -from .s3_cache import S3Cache \ No newline at end of file +from .s3_cache import S3Cache +from .gcs_cache import GCSCache diff --git a/litellm/caching/caching.py b/litellm/caching/caching.py index 6959467cddd..8416790edb1 100644 --- a/litellm/caching/caching.py +++ b/litellm/caching/caching.py @@ -34,6 +34,7 @@ from .redis_cache import RedisCache from .redis_cluster_cache import RedisClusterCache from .redis_semantic_cache import RedisSemanticCache from .s3_cache import S3Cache +from .gcs_cache import GCSCache def print_verbose(print_statement): @@ -92,6 +93,9 @@ class Cache: s3_aws_session_token: Optional[str] = None, s3_config: Optional[Any] = None, s3_path: Optional[str] = None, + gcs_bucket_name: Optional[str] = None, + gcs_path_service_account: Optional[str] = None, + gcs_path: Optional[str] = None, redis_semantic_cache_embedding_model: str = "text-embedding-ada-002", redis_semantic_cache_index_name: Optional[str] = None, redis_flush_size: Optional[int] = None, @@ -140,6 +144,11 @@ class Cache: s3_aws_session_token (str, optional): The aws session token for the s3 cache. Defaults to None. s3_config (dict, optional): The config for the s3 cache. Defaults to None. + # GCS Cache Args + gcs_bucket_name (str, optional): The bucket name for the gcs cache. Defaults to None. + gcs_path_service_account (str, optional): Path to the service account json. + gcs_path (str, optional): Folder path inside the bucket to store cache files. + # Common Cache Args supported_call_types (list, optional): List of call types to cache for. Defaults to cache == on for all call types. **kwargs: Additional keyword arguments for redis.Redis() cache @@ -204,6 +213,12 @@ class Cache: s3_path=s3_path, **kwargs, ) + elif type == LiteLLMCacheType.GCS: + self.cache = GCSCache( + bucket_name=gcs_bucket_name, + path_service_account=gcs_path_service_account, + gcs_path=gcs_path, + ) elif type == LiteLLMCacheType.AZURE_BLOB: self.cache = AzureBlobCache( account_url=azure_account_url, diff --git a/litellm/caching/gcs_cache.py b/litellm/caching/gcs_cache.py new file mode 100644 index 00000000000..88857ba0e70 --- /dev/null +++ b/litellm/caching/gcs_cache.py @@ -0,0 +1,97 @@ +"""GCS Cache implementation +Supports syncing responses to Google Cloud Storage Buckets using HTTP requests. +""" +import json +import asyncio +from typing import Optional + +from litellm._logging import print_verbose, verbose_logger +from litellm.integrations.gcs_bucket.gcs_bucket_base import GCSBucketBase +from litellm.llms.custom_httpx.http_handler import ( + get_async_httpx_client, + _get_httpx_client, + httpxSpecialProvider, +) +from .base_cache import BaseCache + + +class GCSCache(BaseCache): + def __init__(self, bucket_name: Optional[str] = None, path_service_account: Optional[str] = None, gcs_path: Optional[str] = None) -> None: + super().__init__() + self.bucket_name = bucket_name or GCSBucketBase(bucket_name=None).BUCKET_NAME + self.path_service_account = path_service_account or GCSBucketBase(bucket_name=None).path_service_account_json + self.key_prefix = gcs_path.rstrip("/") + "/" if gcs_path else "" + # create httpx clients + self.async_client = get_async_httpx_client(llm_provider=httpxSpecialProvider.LoggingCallback) + self.sync_client = _get_httpx_client() + + def _construct_headers(self) -> dict: + base = GCSBucketBase(bucket_name=self.bucket_name) + base.path_service_account_json = self.path_service_account + base.BUCKET_NAME = self.bucket_name + return base.sync_construct_request_headers() + + def set_cache(self, key, value, **kwargs): + try: + print_verbose(f"LiteLLM SET Cache - GCS. Key={key}. Value={value}") + headers = self._construct_headers() + object_name = self.key_prefix + key + bucket_name = self.bucket_name + url = f"https://storage.googleapis.com/upload/storage/v1/b/{bucket_name}/o?uploadType=media&name={object_name}" + data = json.dumps(value) + self.sync_client.post(url=url, data=data, headers=headers) + except Exception as e: + print_verbose(f"GCS Caching: set_cache() - Got exception from GCS: {e}") + + async def async_set_cache(self, key, value, **kwargs): + try: + headers = self._construct_headers() + object_name = self.key_prefix + key + bucket_name = self.bucket_name + url = f"https://storage.googleapis.com/upload/storage/v1/b/{bucket_name}/o?uploadType=media&name={object_name}" + data = json.dumps(value) + await self.async_client.post(url=url, data=data, headers=headers) + except Exception as e: + print_verbose(f"GCS Caching: async_set_cache() - Got exception from GCS: {e}") + + def get_cache(self, key, **kwargs): + try: + headers = self._construct_headers() + object_name = self.key_prefix + key + bucket_name = self.bucket_name + url = f"https://storage.googleapis.com/storage/v1/b/{bucket_name}/o/{object_name}?alt=media" + response = self.sync_client.get(url=url, headers=headers) + if response.status_code == 200: + cached_response = json.loads(response.text) + verbose_logger.debug( + f"Got GCS Cache: key: {key}, cached_response {cached_response}. Type Response {type(cached_response)}" + ) + return cached_response + return None + except Exception as e: + verbose_logger.error(f"GCS Caching: get_cache() - Got exception from GCS: {e}") + + async def async_get_cache(self, key, **kwargs): + try: + headers = self._construct_headers() + object_name = self.key_prefix + key + bucket_name = self.bucket_name + url = f"https://storage.googleapis.com/storage/v1/b/{bucket_name}/o/{object_name}?alt=media" + response = await self.async_client.get(url=url, headers=headers) + if response.status_code == 200: + return json.loads(response.text) + return None + except Exception as e: + verbose_logger.error(f"GCS Caching: async_get_cache() - Got exception from GCS: {e}") + + def flush_cache(self): + pass + + async def disconnect(self): + pass + + async def async_set_cache_pipeline(self, cache_list, **kwargs): + tasks = [] + for val in cache_list: + tasks.append(self.async_set_cache(val[0], val[1], **kwargs)) + await asyncio.gather(*tasks) diff --git a/litellm/types/caching.py b/litellm/types/caching.py index 2531444ae81..67b0238ce8a 100644 --- a/litellm/types/caching.py +++ b/litellm/types/caching.py @@ -12,6 +12,7 @@ class LiteLLMCacheType(str, Enum): DISK = "disk" QDRANT_SEMANTIC = "qdrant-semantic" AZURE_BLOB = "azure-blob" + GCS = "gcs" CachingSupportedCallTypes = Literal[ diff --git a/tests/local_testing/test_gcs_cache_unit_tests.py b/tests/local_testing/test_gcs_cache_unit_tests.py new file mode 100644 index 00000000000..305dfd95d7d --- /dev/null +++ b/tests/local_testing/test_gcs_cache_unit_tests.py @@ -0,0 +1,6 @@ +from cache_unit_tests import LLMCachingUnitTests +from litellm.caching import LiteLLMCacheType + +class TestGCSCacheUnitTests(LLMCachingUnitTests): + def get_cache_type(self) -> LiteLLMCacheType: + return LiteLLMCacheType.GCS diff --git a/tests/test_litellm/caching/test_gcs_cache.py b/tests/test_litellm/caching/test_gcs_cache.py new file mode 100644 index 00000000000..c346570bb05 --- /dev/null +++ b/tests/test_litellm/caching/test_gcs_cache.py @@ -0,0 +1,36 @@ +import os +import sys +from unittest.mock import MagicMock, AsyncMock, patch + +import pytest + +sys.path.insert(0, os.path.abspath("../../..")) + +from litellm.caching.gcs_cache import GCSCache + + +@pytest.fixture +def mock_gcs_dependencies(): + """Mock httpx clients and GCS auth""" + mock_sync_client = MagicMock() + mock_async_client = AsyncMock() + + with patch("litellm.caching.gcs_cache._get_httpx_client", return_value=mock_sync_client), \ + patch("litellm.caching.gcs_cache.get_async_httpx_client", return_value=mock_async_client), \ + patch("litellm.caching.gcs_cache.GCSBucketBase.sync_construct_request_headers", return_value={}): + yield { + "sync_client": mock_sync_client, + "async_client": mock_async_client, + } + + +@pytest.mark.asyncio +async def test_gcs_cache_async_set_and_get(mock_gcs_dependencies): + cache = GCSCache(bucket_name="test-bucket") + await cache.async_set_cache("key", {"foo": "bar"}) + mock_gcs_dependencies["async_client"].post.assert_called_once() + + mock_gcs_dependencies["async_client"].get.return_value.status_code = 200 + mock_gcs_dependencies["async_client"].get.return_value.text = "{\"foo\": \"bar\"}" + result = await cache.async_get_cache("key") + assert result == {"foo": "bar"} From 1c6be9bdadbeac3ae66bac096e281bc305cd6b5b Mon Sep 17 00:00:00 2001 From: Perling Date: Tue, 5 Aug 2025 07:12:51 +0800 Subject: [PATCH 11/69] Fix: Langfuse reporting "client closed" error due to httpx client TTL (#13045) * Fix: Langfuse reporting "client closed" error due to httpx client TTL * remove log --- .../integrations/langfuse/langfuse_handler.py | 48 +++++++++++++++++-- 1 file changed, 45 insertions(+), 3 deletions(-) diff --git a/litellm/integrations/langfuse/langfuse_handler.py b/litellm/integrations/langfuse/langfuse_handler.py index f9d27f6cf00..56ed01563be 100644 --- a/litellm/integrations/langfuse/langfuse_handler.py +++ b/litellm/integrations/langfuse/langfuse_handler.py @@ -12,6 +12,10 @@ from litellm.litellm_core_utils.litellm_logging import StandardCallbackDynamicPa from .langfuse import LangFuseLogger, LangfuseLoggingConfig +from httpx._client import ClientState + +from litellm._logging import verbose_logger + if TYPE_CHECKING: from litellm.litellm_core_utils.litellm_logging import DynamicLoggingCache else: @@ -60,7 +64,17 @@ class LangFuseHandler: ) # if not cached, create a new langfuse logger and cache it - if temp_langfuse_logger is None: + if temp_langfuse_logger is None or not LangFuseHandler._logger_httpx_client_is_unclosed(temp_langfuse_logger): + # if the cached logger is closed, remove it from cache + if temp_langfuse_logger is not None: + key = in_memory_dynamic_logger_cache.get_cache_key( + args={**credentials_dict, "service_name": "langfuse"} + ) + in_memory_dynamic_logger_cache.cache._remove_key(key) + verbose_logger.warning( + "LangFuseLogger was found in cache but it was closed. Removing it from cache and creating a new one." + ) + temp_langfuse_logger = ( LangFuseHandler._create_langfuse_logger_from_credentials( credentials=credentials_dict, @@ -83,7 +97,7 @@ class LangFuseHandler: If no Global LangfuseLogger is set, it will check in_memory_dynamic_logger_cache for a cached LangFuseLogger This function is used to return the globalLangfuseLogger if it exists, otherwise it will check in_memory_dynamic_logger_cache for a cached LangFuseLogger """ - if globalLangfuseLogger is not None: + if globalLangfuseLogger is not None and LangFuseHandler._logger_httpx_client_is_unclosed(globalLangfuseLogger): return globalLangfuseLogger credentials_dict: Dict[ @@ -95,7 +109,17 @@ class LangFuseHandler: credentials=credentials_dict, service_name="langfuse", ) - if globalLangfuseLogger is None: + if globalLangfuseLogger is None or not LangFuseHandler._logger_httpx_client_is_unclosed(globalLangfuseLogger): + # if the cached logger is closed, remove it from cache + if globalLangfuseLogger is not None: + key = in_memory_dynamic_logger_cache.get_cache_key( + args={**credentials_dict, "service_name": "langfuse"} + ) + in_memory_dynamic_logger_cache.cache._remove_key(key) + verbose_logger.warning( + "LangFuseLogger was found in cache but it was closed. Removing it from cache and creating a new one." + ) + globalLangfuseLogger = ( LangFuseHandler._create_langfuse_logger_from_credentials( credentials=credentials_dict, @@ -168,3 +192,21 @@ class LangFuseHandler: ): return True return False + + @staticmethod + def _logger_httpx_client_is_unclosed( + logger: LangFuseLogger, + ) -> bool: + """ + This function checks if the httpx client used by the logger is not closed. + + Args: + logger (LangFuseLogger): The LangFuseLogger instance to check. + + Returns: + bool: True if the httpx client is not closed, False otherwise. + """ + verbose_logger.debug(f"LangFuseLogger's httpx client state: {logger.Langfuse.httpx_client._state}") + if logger is not None and logger.Langfuse.httpx_client._state != ClientState.CLOSED: + return True + return False From 368c9b75b9c0c8b3381d377438cdbea1ac7439bf Mon Sep 17 00:00:00 2001 From: "Jugal D. Bhatt" <55304795+jugaldb@users.noreply.github.com> Date: Mon, 4 Aug 2025 17:00:16 -0700 Subject: [PATCH 12/69] add correct pricing (#13269) --- litellm/model_prices_and_context_window_backup.json | 6 +++--- model_prices_and_context_window.json | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/litellm/model_prices_and_context_window_backup.json b/litellm/model_prices_and_context_window_backup.json index ef4891c2686..cbb44128bb8 100644 --- a/litellm/model_prices_and_context_window_backup.json +++ b/litellm/model_prices_and_context_window_backup.json @@ -607,9 +607,9 @@ "supports_system_messages": true, "supports_tool_choice": true, "search_context_cost_per_query": { - "search_context_size_low": 30.0, - "search_context_size_medium": 35.0, - "search_context_size_high": 50.0 + "search_context_size_low": 0.025, + "search_context_size_medium": 0.0275, + "search_context_size_high": 0.03 } }, "codex-mini-latest": { diff --git a/model_prices_and_context_window.json b/model_prices_and_context_window.json index 0656f6b260c..bd04a82d767 100644 --- a/model_prices_and_context_window.json +++ b/model_prices_and_context_window.json @@ -607,9 +607,9 @@ "supports_system_messages": true, "supports_tool_choice": true, "search_context_cost_per_query": { - "search_context_size_low": 30.0, - "search_context_size_medium": 35.0, - "search_context_size_high": 50.0 + "search_context_size_low": 0.025, + "search_context_size_medium": 0.0275, + "search_context_size_high": 0.03 } }, "codex-mini-latest": { From 37cb44301060efc9649b0e38d794d71f641bff0e Mon Sep 17 00:00:00 2001 From: Krrish Dholakia Date: Mon, 4 Aug 2025 17:12:36 -0700 Subject: [PATCH 13/69] refactor(oci/chat/transformation.py): lazy load package imports --- litellm/llms/oci/chat/transformation.py | 50 ++++++++++++------ ...odel_prices_and_context_window_backup.json | 52 +++++++++---------- 2 files changed, 60 insertions(+), 42 deletions(-) diff --git a/litellm/llms/oci/chat/transformation.py b/litellm/llms/oci/chat/transformation.py index e08c1d12407..5175979e7b8 100644 --- a/litellm/llms/oci/chat/transformation.py +++ b/litellm/llms/oci/chat/transformation.py @@ -1,15 +1,13 @@ import base64 import datetime import hashlib -from urllib.parse import urlparse -import litellm import json from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple, Union -from cryptography.hazmat.primitives import hashes, serialization -from cryptography.hazmat.primitives.asymmetric import padding, rsa +from urllib.parse import urlparse import httpx +import litellm from litellm.litellm_core_utils.logging_utils import track_llm_api_timing from litellm.llms.base_llm.chat.transformation import BaseConfig, BaseLLMException from litellm.llms.custom_httpx.http_handler import ( @@ -19,14 +17,7 @@ from litellm.llms.custom_httpx.http_handler import ( get_async_httpx_client, version, ) -from litellm.types.llms.openai import AllMessageValues -from litellm.types.utils import LlmProviders -from litellm.utils import ( - ChatCompletionMessageToolCall, - CustomStreamWrapper, - ModelResponse, - Usage, -) +from litellm.llms.oci.common_utils import OCIError from litellm.types.llms.oci import ( OCIChatRequestPayload, OCICompletionPayload, @@ -42,12 +33,19 @@ from litellm.types.llms.oci import ( OCIToolDefinition, OCIVendors, ) -from litellm.llms.oci.common_utils import OCIError +from litellm.types.llms.openai import AllMessageValues from litellm.types.utils import ( Delta, + LlmProviders, ModelResponseStream, StreamingChoices, ) +from litellm.utils import ( + ChatCompletionMessageToolCall, + CustomStreamWrapper, + ModelResponse, + Usage, +) if TYPE_CHECKING: from litellm.litellm_core_utils.litellm_logging import Logging as _LiteLLMLoggingObj @@ -74,6 +72,15 @@ def build_signature_string(method, path, headers, signed_headers): def load_private_key_from_str(key_str: str): + try: + from cryptography.hazmat.primitives import serialization + from cryptography.hazmat.primitives.asymmetric import rsa + except ImportError as e: + raise ImportError( + "cryptography package is required for OCI authentication. " + "Please install it with: pip install cryptography" + ) from e + key = serialization.load_pem_private_key( key_str.encode("utf-8"), password=None, @@ -267,6 +274,15 @@ class OCIChatConfig(BaseConfig): method, path, headers_to_sign, signed_headers ) + try: + from cryptography.hazmat.primitives import hashes + from cryptography.hazmat.primitives.asymmetric import padding + except ImportError as e: + raise ImportError( + "cryptography package is required for OCI authentication. " + "Please install it with: pip install cryptography" + ) from e + private_key = load_private_key_from_str(oci_key) signature = private_key.sign( signing_string.encode("utf-8"), @@ -837,9 +853,11 @@ class OCIStreamWrapper(CustomStreamWrapper): index=typed_chunk.index if typed_chunk.index else 0, delta=Delta( content=text, - tool_calls=[tool.model_dump() for tool in tool_calls] - if tool_calls - else None, + tool_calls=( + [tool.model_dump() for tool in tool_calls] + if tool_calls + else None + ), provider_specific_fields=None, # OCI does not have provider specific fields in the response thinking_blocks=None, # OCI does not have thinking blocks in the response reasoning_content=None, # OCI does not have reasoning content in the response diff --git a/litellm/model_prices_and_context_window_backup.json b/litellm/model_prices_and_context_window_backup.json index cbb44128bb8..bd04a82d767 100644 --- a/litellm/model_prices_and_context_window_backup.json +++ b/litellm/model_prices_and_context_window_backup.json @@ -3750,7 +3750,7 @@ "max_input_tokens": 131072, "max_output_tokens": 131072, "input_cost_per_token": 3.3e-06, - "output_cost_per_token": 16.5e-06, + "output_cost_per_token": 1.65e-05, "litellm_provider": "azure_ai", "mode": "chat", "supports_function_calling": true, @@ -3764,7 +3764,7 @@ "max_input_tokens": 131072, "max_output_tokens": 131072, "input_cost_per_token": 3e-06, - "output_cost_per_token": 15e-06, + "output_cost_per_token": 1.5e-05, "litellm_provider": "azure_ai", "mode": "chat", "supports_function_calling": true, @@ -3777,7 +3777,7 @@ "max_tokens": 131072, "max_input_tokens": 131072, "max_output_tokens": 131072, - "input_cost_per_token": 0.25e-06, + "input_cost_per_token": 2.5e-07, "output_cost_per_token": 1.27e-06, "litellm_provider": "azure_ai", "mode": "chat", @@ -3792,7 +3792,7 @@ "max_tokens": 131072, "max_input_tokens": 131072, "max_output_tokens": 131072, - "input_cost_per_token": 0.275e-06, + "input_cost_per_token": 2.75e-07, "output_cost_per_token": 1.38e-06, "litellm_provider": "azure_ai", "mode": "chat", @@ -7337,12 +7337,12 @@ "max_audio_length_hours": 8.4, "max_audio_per_prompt": 1, "max_pdf_size_mb": 30, - "input_cost_per_token": 3.5e-07, + "input_cost_per_token": 3.5e-07, "input_cost_per_audio_token": 2.1e-06, "input_cost_per_image": 2.1e-06, "input_cost_per_video_per_second": 2.1e-06, "output_cost_per_token": 1.5e-06, - "output_cost_per_audio_token": 8.5e-06, + "output_cost_per_audio_token": 8.5e-06, "litellm_provider": "gemini", "mode": "chat", "rpm": 10, @@ -9039,9 +9039,9 @@ "supports_tool_choice": true }, "vertex_ai/meta/llama-4-scout-17b-16e-instruct-maas": { - "max_tokens": 10000000.0, - "max_input_tokens": 10000000.0, - "max_output_tokens": 10000000.0, + "max_tokens": 10000000, + "max_input_tokens": 10000000, + "max_output_tokens": 10000000, "input_cost_per_token": 2.5e-07, "output_cost_per_token": 7e-07, "litellm_provider": "vertex_ai-llama_models", @@ -9059,9 +9059,9 @@ ] }, "vertex_ai/meta/llama-4-scout-17b-128e-instruct-maas": { - "max_tokens": 10000000.0, - "max_input_tokens": 10000000.0, - "max_output_tokens": 10000000.0, + "max_tokens": 10000000, + "max_input_tokens": 10000000, + "max_output_tokens": 10000000, "input_cost_per_token": 2.5e-07, "output_cost_per_token": 7e-07, "litellm_provider": "vertex_ai-llama_models", @@ -9079,9 +9079,9 @@ ] }, "vertex_ai/meta/llama-4-maverick-17b-128e-instruct-maas": { - "max_tokens": 1000000.0, - "max_input_tokens": 1000000.0, - "max_output_tokens": 1000000.0, + "max_tokens": 1000000, + "max_input_tokens": 1000000, + "max_output_tokens": 1000000, "input_cost_per_token": 3.5e-07, "output_cost_per_token": 1.15e-06, "litellm_provider": "vertex_ai-llama_models", @@ -9099,9 +9099,9 @@ ] }, "vertex_ai/meta/llama-4-maverick-17b-16e-instruct-maas": { - "max_tokens": 1000000.0, - "max_input_tokens": 1000000.0, - "max_output_tokens": 1000000.0, + "max_tokens": 1000000, + "max_input_tokens": 1000000, + "max_output_tokens": 1000000, "input_cost_per_token": 3.5e-07, "output_cost_per_token": 1.15e-06, "litellm_provider": "vertex_ai-llama_models", @@ -9174,7 +9174,7 @@ "max_input_tokens": 128000, "max_output_tokens": 2048, "input_cost_per_token": 5e-06, - "output_cost_per_token": 16e-06, + "output_cost_per_token": 1.6e-05, "litellm_provider": "vertex_ai-llama_models", "mode": "chat", "supports_system_messages": true, @@ -10480,7 +10480,7 @@ "supports_tool_choice": true, "supports_prompt_caching": true }, - "openrouter/x-ai/grok-4":{ + "openrouter/x-ai/grok-4": { "max_tokens": 256000, "max_input_tokens": 256000, "max_output_tokens": 256000, @@ -10494,12 +10494,12 @@ "source": "https://openrouter.ai/x-ai/grok-4", "supports_web_search": true }, - "openrouter/bytedance/ui-tars-1.5-7b":{ + "openrouter/bytedance/ui-tars-1.5-7b": { "max_tokens": 2048, "max_input_tokens": 131072, "max_output_tokens": 2048, - "input_cost_per_token": 0.1e-06, - "output_cost_per_token": 0.2e-06, + "input_cost_per_token": 1e-07, + "output_cost_per_token": 2e-07, "litellm_provider": "openrouter", "mode": "chat", "source": "https://openrouter.ai/api/v1/models/bytedance/ui-tars-1.5-7b", @@ -11178,8 +11178,8 @@ "max_tokens": 8192, "max_input_tokens": 8192, "max_output_tokens": 2048, - "input_cost_per_token": 0.21e-06, - "output_cost_per_token": 0.63e-06, + "input_cost_per_token": 2.1e-07, + "output_cost_per_token": 6.3e-07, "litellm_provider": "openrouter", "mode": "chat", "supports_tool_choice": true @@ -14763,7 +14763,7 @@ "max_tokens": 131072, "max_input_tokens": 131072, "max_output_tokens": 16384, - "input_cost_per_token": 0.6e-06, + "input_cost_per_token": 6e-07, "output_cost_per_token": 2.5e-06, "litellm_provider": "fireworks_ai", "mode": "chat", From ba1882fdd5722de5996380fd1c1e3016998ba385 Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Mon, 4 Aug 2025 17:22:21 -0700 Subject: [PATCH 14/69] [Bug Fix] Prometheus - fix for `litellm_input_tokens_metric`, `litellm_output_tokens_metric` - Note this updates the metric name (#13271) * fixes for litellm_tokens_metric * test_prometheus_token_metrics_with_prometheus_config --- .../integrations/prometheus.py | 6 +- .../test_prometheus_logging_callbacks.py | 180 +++++++++++++++++- 2 files changed, 175 insertions(+), 11 deletions(-) diff --git a/enterprise/litellm_enterprise/integrations/prometheus.py b/enterprise/litellm_enterprise/integrations/prometheus.py index ddbcf948c85..b3bff733e77 100644 --- a/enterprise/litellm_enterprise/integrations/prometheus.py +++ b/enterprise/litellm_enterprise/integrations/prometheus.py @@ -122,19 +122,19 @@ class PrometheusLogger(CustomLogger): # Counter for total_output_tokens self.litellm_tokens_metric = self._counter_factory( - "litellm_total_tokens", + "litellm_total_tokens_metric", "Total number of input + output tokens from LLM requests", labelnames=self.get_labels_for_metric("litellm_total_tokens_metric"), ) self.litellm_input_tokens_metric = self._counter_factory( - "litellm_input_tokens", + "litellm_input_tokens_metric", "Total number of input tokens from LLM requests", labelnames=self.get_labels_for_metric("litellm_input_tokens_metric"), ) self.litellm_output_tokens_metric = self._counter_factory( - "litellm_output_tokens", + "litellm_output_tokens_metric", "Total number of output tokens from LLM requests", labelnames=self.get_labels_for_metric("litellm_output_tokens_metric"), ) 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 31c849fea52..cfb38299541 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 @@ -7,6 +7,8 @@ sys.path.insert(0, os.path.abspath("../..")) import asyncio import logging import uuid +from datetime import datetime, timedelta, timezone +from unittest.mock import MagicMock, call, patch import pytest from prometheus_client import REGISTRY, CollectorRegistry @@ -16,16 +18,18 @@ from litellm import completion from litellm._logging import verbose_logger from litellm.llms.custom_httpx.http_handler import AsyncHTTPHandler from litellm.types.utils import ( - StandardLoggingPayload, - StandardLoggingMetadata, StandardLoggingHiddenParams, + StandardLoggingMetadata, StandardLoggingModelInformation, + StandardLoggingPayload, ) -import pytest -from unittest.mock import MagicMock, patch, call -from datetime import datetime, timedelta, timezone + try: - from litellm_enterprise.integrations.prometheus import PrometheusLogger, UserAPIKeyLabelValues, get_custom_labels_from_metadata + from litellm_enterprise.integrations.prometheus import ( + PrometheusLogger, + UserAPIKeyLabelValues, + get_custom_labels_from_metadata, + ) except Exception: PrometheusLogger = None from litellm.proxy._types import UserAPIKeyAuth @@ -1054,6 +1058,7 @@ def test_increment_deployment_cooled_down(prometheus_logger): @pytest.mark.parametrize("enable_end_user_cost_tracking_prometheus_only", [True, False]) def test_prometheus_factory(monkeypatch, enable_end_user_cost_tracking_prometheus_only): from litellm_enterprise.integrations.prometheus import prometheus_label_factory + from litellm.types.integrations.prometheus import UserAPIKeyLabelValues monkeypatch.setattr( @@ -1532,7 +1537,11 @@ def test_prometheus_label_factory_with_custom_tags(monkeypatch): """ Test that prometheus_label_factory correctly handles custom tags """ - from litellm_enterprise.integrations.prometheus import get_custom_labels_from_tags, prometheus_label_factory + from litellm_enterprise.integrations.prometheus import ( + get_custom_labels_from_tags, + prometheus_label_factory, + ) + from litellm.types.integrations.prometheus import UserAPIKeyLabelValues # Set custom tags configuration @@ -1567,7 +1576,11 @@ def test_prometheus_label_factory_with_no_custom_tags(monkeypatch): """ Test that prometheus_label_factory works when no custom tags are configured """ - from litellm_enterprise.integrations.prometheus import get_custom_labels_from_tags, prometheus_label_factory + from litellm_enterprise.integrations.prometheus import ( + get_custom_labels_from_tags, + prometheus_label_factory, + ) + from litellm.types.integrations.prometheus import UserAPIKeyLabelValues # Set empty custom tags configuration @@ -1776,3 +1789,154 @@ def test_set_llm_deployment_success_metrics_with_label_filtering(): ) prometheus_logger.litellm_deployment_success_responses.labels().inc.assert_called_once() prometheus_logger.litellm_deployment_total_requests.labels().inc.assert_called_once() + + +@pytest.mark.asyncio +async def test_prometheus_token_metrics_with_prometheus_config(): + """ + Test that validates the renamed token metrics are incremented correctly with a prometheus config. + + This test ensures that after the metric renaming (git diff): + - litellm_total_tokens -> litellm_total_tokens_metric + - litellm_input_tokens -> litellm_input_tokens_metric + - litellm_output_tokens -> litellm_output_tokens_metric + + All three metrics should be properly incremented when making a successful completion request. + """ + from prometheus_client import CollectorRegistry, Counter + + import litellm + from litellm.types.integrations.prometheus import PrometheusMetricsConfig + + # Clear registry before test + collectors = list(REGISTRY._collector_to_names.keys()) + for collector in collectors: + REGISTRY.unregister(collector) + + # Set up prometheus configuration that includes the token metrics + config = [ + PrometheusMetricsConfig( + group="token_metrics_test", + metrics=[ + "litellm_total_tokens_metric", + "litellm_input_tokens_metric", + "litellm_output_tokens_metric", + "litellm_requests_metric" + ], + include_labels=[ + "model", + "hashed_api_key", + "api_key_alias", + "team", + "team_alias" + ], + ) + ] + + # Mock litellm.prometheus_metrics_config + with patch("litellm.prometheus_metrics_config", config): + # Create PrometheusLogger with the configuration + prometheus_logger = PrometheusLogger() + + # Test data with specific token counts + standard_logging_payload = create_standard_logging_payload() + standard_logging_payload["total_tokens"] = 1500 + standard_logging_payload["prompt_tokens"] = 900 + standard_logging_payload["completion_tokens"] = 600 + standard_logging_payload["response_cost"] = 0.075 + + kwargs = { + "model": "gpt-3.5-turbo", + "stream": False, + "litellm_params": { + "metadata": { + "user_api_key": "test_key_hash", + "user_api_key_user_id": "test_user", + "user_api_key_team_id": "test_team", + "user_api_key_alias": "test_alias", + "user_api_key_team_alias": "test_team_alias", + } + }, + "start_time": datetime.now() - timedelta(seconds=2), + "completion_start_time": datetime.now() - timedelta(seconds=1), + "api_call_start_time": datetime.now() - timedelta(seconds=1.5), + "end_time": datetime.now(), + "standard_logging_object": standard_logging_payload, + } + response_obj = MagicMock() + + # Make the completion call through the logger + await prometheus_logger.async_log_success_event( + kwargs, response_obj, kwargs["start_time"], kwargs["end_time"] + ) + + await asyncio.sleep(2) + + print("final registry values", REGISTRY._collector_to_names) + + # Get metric collectors directly from registry + metric_collectors = {} + for collector, names in REGISTRY._collector_to_names.items(): + metric_name = names[0] # First name is the base metric name + metric_collectors[metric_name] = collector + + print("=== Final Metric Values (Direct Access) ===") + + # Expected values + expected_values = { + "litellm_total_tokens_metric": 1500.0, + "litellm_input_tokens_metric": 900.0, + "litellm_output_tokens_metric": 600.0, + "litellm_requests_metric": 1.0 + } + + expected_label_values = { + 'api_key_alias': 'test_alias', + 'hashed_api_key': 'test_hash', + 'model': 'gpt-3.5-turbo', + 'team': 'test_team', + 'team_alias': 'test_team_alias' + } + + # Validate each metric directly + for metric_name, expected_value in expected_values.items(): + if metric_name in metric_collectors: + collector = metric_collectors[metric_name] + + # Get all samples for this metric + samples = list(collector.collect())[0].samples + + # Find the _total sample (the actual counter value) + total_sample = None + for sample in samples: + if sample.name.endswith('_total'): + total_sample = sample + break + + if total_sample: + actual_value = total_sample.value + actual_labels = total_sample.labels + + print(f"✓ {metric_name}: expected={expected_value}, actual={actual_value}") + print(f" Labels: {actual_labels}") + + # Validate the value + assert actual_value == expected_value, f"Expected {expected_value}, got {actual_value} for {metric_name}" + + # Validate the labels + for label_key, expected_label_value in expected_label_values.items(): + actual_label_value = actual_labels.get(label_key) + assert actual_label_value == expected_label_value, f"Expected label {label_key}={expected_label_value}, got {actual_label_value}" + + print(f" ✓ {metric_name} VALIDATED") + else: + raise AssertionError(f"No _total sample found for {metric_name}") + else: + raise AssertionError(f"Metric {metric_name} not found in registry") + + print("✓ All token metrics validated successfully!") + + # check final value of metrics in registry + + + From d948b47dba4657afced71690f0c8133d121f8093 Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Mon, 4 Aug 2025 17:23:55 -0700 Subject: [PATCH 15/69] =?UTF-8?q?bump:=20version=201.74.15=20=E2=86=92=201?= =?UTF-8?q?.75.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pyproject.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 27bded35275..caea1a67bef 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "litellm" -version = "1.74.15" +version = "1.75.0" description = "Library to easily interface with LLM API providers" authors = ["BerriAI"] license = "MIT" @@ -152,7 +152,7 @@ requires = ["poetry-core", "wheel"] build-backend = "poetry.core.masonry.api" [tool.commitizen] -version = "1.74.15" +version = "1.75.0" version_files = [ "pyproject.toml:^version" ] From 8314227a6096f00c6a2f921371c10580af2e2aa0 Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Mon, 4 Aug 2025 17:24:40 -0700 Subject: [PATCH 16/69] =?UTF-8?q?bump:=20version=201.75.0=20=E2=86=92=201.?= =?UTF-8?q?75.1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index caea1a67bef..666c3a21b26 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -152,7 +152,7 @@ requires = ["poetry-core", "wheel"] build-backend = "poetry.core.masonry.api" [tool.commitizen] -version = "1.75.0" +version = "1.75.1" version_files = [ "pyproject.toml:^version" ] From ad26cba8b609e078f438ce13f3e3768b5d7ac3b3 Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Mon, 4 Aug 2025 17:26:50 -0700 Subject: [PATCH 17/69] add litellm-enterprise==0.1.17 --- ...litellm_enterprise-0.1.17-py3-none-any.whl | Bin 0 -> 121883 bytes .../dist/litellm_enterprise-0.1.17.tar.gz | Bin 0 -> 59084 bytes enterprise/pyproject.toml | 4 +- poetry.lock | 487 +++++++++++++++--- pyproject.toml | 2 +- requirements.txt | 2 +- 6 files changed, 429 insertions(+), 66 deletions(-) create mode 100644 enterprise/dist/litellm_enterprise-0.1.17-py3-none-any.whl create mode 100644 enterprise/dist/litellm_enterprise-0.1.17.tar.gz diff --git a/enterprise/dist/litellm_enterprise-0.1.17-py3-none-any.whl b/enterprise/dist/litellm_enterprise-0.1.17-py3-none-any.whl new file mode 100644 index 0000000000000000000000000000000000000000..9c2856b4652eda9c836825de80fb020ccef50510 GIT binary patch literal 121883 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 @2X_f8*F!o!M{M?M+r+*Lke-r8a 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 yP@t^(L+JZ)E_=3.2.0)", "brotlicffi"] +speedups = ["Brotli ; platform_python_implementation == \"CPython\"", "aiodns (>=3.2.0) ; sys_platform == \"linux\" or sys_platform == \"darwin\"", "brotlicffi ; platform_python_implementation != \"CPython\""] [[package]] name = "aiosignal" @@ -129,6 +131,7 @@ version = "1.3.1" description = "aiosignal: a list of registered asynchronous callbacks" optional = false python-versions = ">=3.7" +groups = ["main"] files = [ {file = "aiosignal-1.3.1-py3-none-any.whl", hash = "sha256:f8376fb07dd1e86a584e4fcdec80b36b7f81aac666ebc724e2c090300dd83b17"}, {file = "aiosignal-1.3.1.tar.gz", hash = "sha256:54cd96e15e1649b75d6c87526a6ff0b6c1b0dd3459f43d9ca11d48c339b68cfc"}, @@ -143,6 +146,8 @@ version = "0.7.13" description = "A configurable sidebar-enabled Sphinx theme" optional = true python-versions = ">=3.6" +groups = ["main"] +markers = "extra == \"utils\"" files = [ {file = "alabaster-0.7.13-py3-none-any.whl", hash = "sha256:1ee19aca801bbabb5ba3f5f258e4422dfa86f82f3e9cefb0859b283cdd7f62a3"}, {file = "alabaster-0.7.13.tar.gz", hash = "sha256:a27a4a084d5e690e16e01e03ad2b2e552c61a65469419b907243193de1a84ae2"}, @@ -154,6 +159,8 @@ version = "1.16.4" description = "A database migration tool for SQLAlchemy." optional = true python-versions = ">=3.9" +groups = ["main"] +markers = "python_version >= \"3.10\" and extra == \"mlflow\"" files = [ {file = "alembic-1.16.4-py3-none-any.whl", hash = "sha256:b05e51e8e82efc1abd14ba2af6392897e145930c3e0a2faf2b0da2f7f7fd660d"}, {file = "alembic-1.16.4.tar.gz", hash = "sha256:efab6ada0dd0fae2c92060800e0bf5c1dc26af15a10e02fb4babff164b4725e2"}, @@ -174,6 +181,7 @@ version = "0.7.0" description = "Reusable constraint types to use with typing.Annotated" optional = false python-versions = ">=3.8" +groups = ["main", "dev", "proxy-dev"] files = [ {file = "annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53"}, {file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"}, @@ -188,6 +196,7 @@ version = "4.5.2" description = "High level compatibility layer for multiple asynchronous event loop implementations" optional = false python-versions = ">=3.8" +groups = ["main", "dev", "proxy-dev"] files = [ {file = "anyio-4.5.2-py3-none-any.whl", hash = "sha256:c011ee36bc1e8ba40e5a81cb9df91925c218fe9b778554e0b56a21e1b5d4716f"}, {file = "anyio-4.5.2.tar.gz", hash = "sha256:23009af4ed04ce05991845451e11ef02fc7c5ed29179ac9a420e5ad0ac7ddc5b"}, @@ -201,7 +210,7 @@ typing-extensions = {version = ">=4.1", markers = "python_version < \"3.11\""} [package.extras] doc = ["Sphinx (>=7.4,<8.0)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme"] -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)", "uvloop (>=0.21.0b1)"] +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]] @@ -210,6 +219,8 @@ version = "3.11.0" description = "In-process task scheduler with Cron-like capabilities" optional = true python-versions = ">=3.8" +groups = ["main"] +markers = "extra == \"proxy\"" files = [ {file = "APScheduler-3.11.0-py3-none-any.whl", hash = "sha256:fc134ca32e50f5eadcc4938e3a4545ab19131435e851abb40b34d63d5141c6da"}, {file = "apscheduler-3.11.0.tar.gz", hash = "sha256:4c622d250b0955a65d5d0eb91c33e6d43fd879834bf541e0a18661ae60460133"}, @@ -227,7 +238,7 @@ mongodb = ["pymongo (>=3.0)"] redis = ["redis (>=3.0)"] rethinkdb = ["rethinkdb (>=2.4.0)"] sqlalchemy = ["sqlalchemy (>=1.4)"] -test = ["APScheduler[etcd,mongodb,redis,rethinkdb,sqlalchemy,tornado,zookeeper]", "PySide6", "anyio (>=4.5.2)", "gevent", "pytest", "pytz", "twisted"] +test = ["APScheduler[etcd,mongodb,redis,rethinkdb,sqlalchemy,tornado,zookeeper]", "PySide6 ; platform_python_implementation == \"CPython\" and python_version < \"3.14\"", "anyio (>=4.5.2)", "gevent ; python_version < \"3.14\"", "pytest", "pytz", "twisted ; python_version < \"3.14\""] tornado = ["tornado (>=4.3)"] twisted = ["twisted"] zookeeper = ["kazoo"] @@ -236,8 +247,10 @@ zookeeper = ["kazoo"] name = "async-timeout" version = "5.0.1" description = "Timeout context manager for asyncio programs" -optional = false +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\"" 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"}, @@ -249,18 +262,19 @@ version = "25.3.0" description = "Classes Without Boilerplate" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "attrs-25.3.0-py3-none-any.whl", hash = "sha256:427318ce031701fea540783410126f03899a97ffc6f61596ad581ac2e40e3bc3"}, {file = "attrs-25.3.0.tar.gz", hash = "sha256:75d7cefc7fb576747b2c81b4442d4d4a1ce0900973527c011d1030fd3bf4af1b"}, ] [package.extras] -benchmark = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-codspeed", "pytest-mypy-plugins", "pytest-xdist[psutil]"] -cov = ["cloudpickle", "coverage[toml] (>=5.3)", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] -dev = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pre-commit-uv", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] +benchmark = ["cloudpickle ; platform_python_implementation == \"CPython\"", "hypothesis", "mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pympler", "pytest (>=4.3.0)", "pytest-codspeed", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-xdist[psutil]"] +cov = ["cloudpickle ; platform_python_implementation == \"CPython\"", "coverage[toml] (>=5.3)", "hypothesis", "mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-xdist[psutil]"] +dev = ["cloudpickle ; platform_python_implementation == \"CPython\"", "hypothesis", "mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pre-commit-uv", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-xdist[psutil]"] docs = ["cogapp", "furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier"] -tests = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] -tests-mypy = ["mypy (>=1.11.1)", "pytest-mypy-plugins"] +tests = ["cloudpickle ; platform_python_implementation == \"CPython\"", "hypothesis", "mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-xdist[psutil]"] +tests-mypy = ["mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\""] [[package]] name = "azure-core" @@ -268,6 +282,7 @@ version = "1.33.0" description = "Microsoft Azure Core Library for Python" optional = false python-versions = ">=3.8" +groups = ["main", "proxy-dev"] 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"}, @@ -288,6 +303,7 @@ version = "1.21.0" description = "Microsoft Azure Identity Library for Python" optional = false python-versions = ">=3.8" +groups = ["main", "proxy-dev"] 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"}, @@ -306,6 +322,8 @@ version = "4.9.0" description = "Microsoft Azure Key Vault Secrets Client Library for Python" optional = true python-versions = ">=3.8" +groups = ["main"] +markers = "extra == \"extra-proxy\"" 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"}, @@ -322,6 +340,8 @@ version = "12.26.0" description = "Microsoft Azure Blob Storage Client Library for Python" optional = true python-versions = ">=3.8" +groups = ["main"] +markers = "extra == \"proxy\"" files = [ {file = "azure_storage_blob-12.26.0-py3-none-any.whl", hash = "sha256:8c5631b8b22b4f53ec5fff2f3bededf34cfef111e2af613ad42c9e6de00a77fe"}, {file = "azure_storage_blob-12.26.0.tar.gz", hash = "sha256:5dd7d7824224f7de00bfeb032753601c982655173061e242f13be6e26d78d71f"}, @@ -342,6 +362,8 @@ version = "2.17.0" description = "Internationalization utilities" optional = true python-versions = ">=3.8" +groups = ["main"] +markers = "extra == \"utils\"" files = [ {file = "babel-2.17.0-py3-none-any.whl", hash = "sha256:4d0b53093fdfb4b21c92b5213dba5a1b23885afa8383709427046b21c366e5f2"}, {file = "babel-2.17.0.tar.gz", hash = "sha256:0c54cffb19f690cdcc52a3b50bcbf71e07a808d1c80d549f2459b9d2cf0afb9d"}, @@ -351,7 +373,7 @@ files = [ pytz = {version = ">=2015.7", markers = "python_version < \"3.9\""} [package.extras] -dev = ["backports.zoneinfo", "freezegun (>=1.0,<2.0)", "jinja2 (>=3.0)", "pytest (>=6.0)", "pytest-cov", "pytz", "setuptools", "tzdata"] +dev = ["backports.zoneinfo ; python_version < \"3.9\"", "freezegun (>=1.0,<2.0)", "jinja2 (>=3.0)", "pytest (>=6.0)", "pytest-cov", "pytz", "setuptools", "tzdata ; sys_platform == \"win32\""] [[package]] name = "backoff" @@ -359,10 +381,12 @@ version = "2.2.1" description = "Function decoration for backoff and retry" optional = false python-versions = ">=3.7,<4.0" +groups = ["main", "dev"] 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\""} [[package]] name = "backports-zoneinfo" @@ -370,6 +394,8 @@ version = "0.2.1" description = "Backport of the standard library zoneinfo module" optional = true python-versions = ">=3.6" +groups = ["main"] +markers = "extra == \"proxy\" and python_version < \"3.9\"" 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"}, @@ -398,6 +424,7 @@ version = "23.12.1" description = "The uncompromising code formatter." optional = false python-versions = ">=3.8" +groups = ["main", "dev"] files = [ {file = "black-23.12.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e0aaf6041986767a5e0ce663c7a2f0e9eaf21e6ff87a5f95cbf3675bfd4c41d2"}, {file = "black-23.12.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c88b3711d12905b74206227109272673edce0cb29f27e1385f33b0163c414bba"}, @@ -434,7 +461,7 @@ typing-extensions = {version = ">=4.0.1", markers = "python_version < \"3.11\""} [package.extras] colorama = ["colorama (>=0.4.3)"] -d = ["aiohttp (>=3.7.4)", "aiohttp (>=3.7.4,!=3.9.0)"] +d = ["aiohttp (>=3.7.4) ; sys_platform != \"win32\" or implementation_name != \"pypy\"", "aiohttp (>=3.7.4,!=3.9.0) ; sys_platform == \"win32\" and implementation_name == \"pypy\""] jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] uvloop = ["uvloop (>=0.15.2)"] @@ -444,6 +471,8 @@ version = "1.9.0" description = "Fast, simple object-to-object and broadcast signaling" optional = true python-versions = ">=3.9" +groups = ["main"] +markers = "python_version >= \"3.10\" and extra == \"mlflow\"" files = [ {file = "blinker-1.9.0-py3-none-any.whl", hash = "sha256:ba0efaa9080b619ff2f3459d1d500c57bddea4a6b424b60a91141db6fd2f08bc"}, {file = "blinker-1.9.0.tar.gz", hash = "sha256:b4ce2265a7abece45e7cc896e98dbebe6cead56bcf805a3d23136d145f5445bf"}, @@ -455,6 +484,8 @@ version = "1.34.34" description = "The AWS SDK for Python" optional = true python-versions = ">= 3.8" +groups = ["main"] +markers = "extra == \"proxy\"" files = [ {file = "boto3-1.34.34-py3-none-any.whl", hash = "sha256:33a8b6d9136fa7427160edb92d2e50f2035f04e9d63a2d1027349053e12626aa"}, {file = "boto3-1.34.34.tar.gz", hash = "sha256:b2f321e20966f021ec800b7f2c01287a3dd04fc5965acdfbaa9c505a24ca45d1"}, @@ -474,6 +505,8 @@ version = "1.34.162" description = "Low-level, data-driven core of boto 3." optional = true python-versions = ">=3.8" +groups = ["main"] +markers = "extra == \"proxy\"" files = [ {file = "botocore-1.34.162-py3-none-any.whl", hash = "sha256:2d918b02db88d27a75b48275e6fb2506e9adaaddbec1ffa6a8a0898b34e769be"}, {file = "botocore-1.34.162.tar.gz", hash = "sha256:adc23be4fb99ad31961236342b7cbf3c0bfc62532cd02852196032e8c0d682f3"}, @@ -483,8 +516,8 @@ files = [ jmespath = ">=0.7.1,<2.0.0" python-dateutil = ">=2.1,<3.0.0" urllib3 = [ - {version = ">=1.25.4,<1.27", markers = "python_version < \"3.10\""}, {version = ">=1.25.4,<2.2.0 || >2.2.0,<3", markers = "python_version >= \"3.10\""}, + {version = ">=1.25.4,<1.27", markers = "python_version < \"3.10\""}, ] [package.extras] @@ -496,6 +529,8 @@ version = "5.5.2" 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\"" files = [ {file = "cachetools-5.5.2-py3-none-any.whl", hash = "sha256:d26a22bcc62eb95c3beabd9f1ee5e820d3d2704fe2967cbe350e20c8ffcd3f0a"}, {file = "cachetools-5.5.2.tar.gz", hash = "sha256:1a661caa9175d26759571b2e19580f9d6393969e5dfca11fdb1f947a23e640d4"}, @@ -507,6 +542,7 @@ version = "2025.8.3" description = "Python package for providing Mozilla's CA Bundle." optional = false python-versions = ">=3.7" +groups = ["main", "dev", "proxy-dev"] files = [ {file = "certifi-2025.8.3-py3-none-any.whl", hash = "sha256:f6c12493cfb1b06ba2ff328595af9350c65d6644968e5d3a2ffd78699af217a5"}, {file = "certifi-2025.8.3.tar.gz", hash = "sha256:e564105f78ded564e3ae7c923924435e1daa7463faeab5bb932bc53ffae63407"}, @@ -518,6 +554,7 @@ version = "1.17.1" description = "Foreign Function Interface for Python calling C code." optional = false python-versions = ">=3.8" +groups = ["main", "dev", "proxy-dev"] 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"}, @@ -587,6 +624,7 @@ files = [ {file = "cffi-1.17.1-cp39-cp39-win_amd64.whl", hash = "sha256:d016c76bdd850f3c626af19b0542c9677ba156e4ee4fccfdd7848803533ef662"}, {file = "cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824"}, ] +markers = {main = "platform_python_implementation != \"PyPy\" or extra == \"proxy\"", dev = "platform_python_implementation != \"PyPy\"", proxy-dev = "platform_python_implementation != \"PyPy\""} [package.dependencies] pycparser = "*" @@ -597,6 +635,7 @@ version = "3.4.2" description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." optional = false python-versions = ">=3.7" +groups = ["main", "dev", "proxy-dev"] files = [ {file = "charset_normalizer-3.4.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7c48ed483eb946e6c04ccbe02c6b4d1d48e51944b6db70f697e089c193404941"}, {file = "charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b2d318c11350e10662026ad0eb71bb51c7812fc8590825304ae0bdd4ac283acd"}, @@ -698,6 +737,7 @@ version = "8.1.8" description = "Composable command line interface toolkit" optional = false python-versions = ">=3.7" +groups = ["main", "dev", "proxy-dev"] files = [ {file = "click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2"}, {file = "click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a"}, @@ -712,6 +752,8 @@ version = "3.1.1" description = "Pickler class to extend the standard pickle.Pickler functionality" optional = true python-versions = ">=3.8" +groups = ["main"] +markers = "python_version >= \"3.10\" and extra == \"mlflow\"" files = [ {file = "cloudpickle-3.1.1-py3-none-any.whl", hash = "sha256:c8c5a44295039331ee9dad40ba100a9c7297b6f988e50e87ccdf3765a668350e"}, {file = "cloudpickle-3.1.1.tar.gz", hash = "sha256:b216fa8ae4019d5482a8ac3c95d8f6346115d8835911fd4aefd1a445e4242c64"}, @@ -723,6 +765,8 @@ version = "4.57" 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\"" files = [ {file = "cohere-4.57-py3-none-any.whl", hash = "sha256:479bdea81ae119e53f671f1ae808fcff9df88211780525d7ef2f7b99dfb32e59"}, {file = "cohere-4.57.tar.gz", hash = "sha256:71ace0204a92d1a2a8d4b949b88b353b4f22fc645486851924284cc5a0eb700d"}, @@ -742,10 +786,12 @@ version = "0.4.6" description = "Cross-platform colored terminal text." optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +groups = ["main", "dev", "proxy-dev"] 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 = "sys_platform == \"win32\" and (extra == \"utils\" or extra == \"semantic-router\") and python_version >= \"3.9\" or platform_system == \"Windows\" or sys_platform == \"win32\" and extra == \"utils\"", dev = "platform_system == \"Windows\" or sys_platform == \"win32\"", proxy-dev = "platform_system == \"Windows\""} [[package]] name = "coloredlogs" @@ -753,6 +799,8 @@ version = "15.0.1" description = "Colored terminal output for Python's logging module" optional = true python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +groups = ["main"] +markers = "python_version >= \"3.9\" and extra == \"extra-proxy\" and python_version < \"3.14\"" files = [ {file = "coloredlogs-15.0.1-py2.py3-none-any.whl", hash = "sha256:612ee75c546f53e92e70049c9dbfcc18c935a2b9a53b66085ce9ef6a6e5c0934"}, {file = "coloredlogs-15.0.1.tar.gz", hash = "sha256:7c991aa71a4577af2f82600d8f8f3a89f936baeaf9b50a9c197da014e5bf16b0"}, @@ -770,6 +818,8 @@ version = "6.9.0" description = "Add colours to the output of Python's logging module." optional = true python-versions = ">=3.6" +groups = ["main"] +markers = "python_version >= \"3.9\" and extra == \"semantic-router\"" files = [ {file = "colorlog-6.9.0-py3-none-any.whl", hash = "sha256:5906e71acd67cb07a71e779c47c4bcb45fb8c2993eebe9e5adcd6a6f1b283eff"}, {file = "colorlog-6.9.0.tar.gz", hash = "sha256:bfba54a1b93b94f54e1f4fe48395725a3d92fd2a4af702f6bd70946bdc0c6ac2"}, @@ -787,6 +837,8 @@ version = "1.3.2" 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\"" 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"}, @@ -863,6 +915,7 @@ version = "43.0.3" description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." optional = false python-versions = ">=3.7" +groups = ["main", "dev", "proxy-dev"] 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"}, @@ -912,6 +965,8 @@ version = "0.12.1" description = "Composable style cycles" optional = true python-versions = ">=3.8" +groups = ["main"] +markers = "python_version >= \"3.10\" and extra == \"mlflow\"" files = [ {file = "cycler-0.12.1-py3-none-any.whl", hash = "sha256:85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30"}, {file = "cycler-0.12.1.tar.gz", hash = "sha256:88bb128f02ba341da8ef447245a9e138fae777f6a23943da4540077d3601eb1c"}, @@ -927,6 +982,8 @@ version = "0.61.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.61.0-py3-none-any.whl", hash = "sha256:709ac7c709f843567b04fba6cea8a53ee644b79314e9a9ac4db0c1b3c1d2d5fe"}, {file = "databricks_sdk-0.61.0.tar.gz", hash = "sha256:06e50663c2c87e94f5e505390b74bc5c7c5330f4b4be35616b7aed06cf940af0"}, @@ -937,9 +994,9 @@ google-auth = ">=2.0,<3.0" requests = ">=2.28.1,<3" [package.extras] -dev = ["autoflake", "black", "build", "databricks-connect", "httpx", "ipython", "ipywidgets", "isort", "langchain-openai", "openai", "pycodestyle", "pyfakefs", "pytest", "pytest-cov", "pytest-mock", "pytest-rerunfailures", "pytest-xdist", "requests-mock", "wheel"] +dev = ["autoflake", "black", "build", "databricks-connect", "httpx", "ipython", "ipywidgets", "isort", "langchain-openai ; python_version > \"3.7\"", "openai", "pycodestyle", "pyfakefs", "pytest", "pytest-cov", "pytest-mock", "pytest-rerunfailures", "pytest-xdist", "requests-mock", "wheel"] notebook = ["ipython (>=8,<10)", "ipywidgets (>=8,<9)"] -openai = ["httpx", "langchain-openai", "openai"] +openai = ["httpx", "langchain-openai ; python_version > \"3.7\"", "openai"] [[package]] name = "deprecated" @@ -947,16 +1004,18 @@ version = "1.2.18" description = "Python @deprecated decorator to deprecate old python classes, functions or methods." optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7" +groups = ["main", "dev", "proxy-dev"] files = [ {file = "Deprecated-1.2.18-py2.py3-none-any.whl", hash = "sha256:bd5011788200372a32418f888e326a09ff80d0214bd961147cfed01b5c018eec"}, {file = "deprecated-1.2.18.tar.gz", hash = "sha256:422b6f6d859da6f2ef57857761bfb392480502a64c3028ca9bbe86085d72115d"}, ] +markers = {main = "python_version >= \"3.10\""} [package.dependencies] wrapt = ">=1.10,<2" [package.extras] -dev = ["PyTest", "PyTest-Cov", "bump2version (<1)", "setuptools", "tox"] +dev = ["PyTest", "PyTest-Cov", "bump2version (<1)", "setuptools ; python_version >= \"3.12\"", "tox"] [[package]] name = "diskcache" @@ -964,6 +1023,8 @@ version = "5.6.3" description = "Disk Cache -- Disk and file backed persistent cache." optional = true python-versions = ">=3" +groups = ["main"] +markers = "extra == \"caching\"" files = [ {file = "diskcache-5.6.3-py3-none-any.whl", hash = "sha256:5e31b2d5fbad117cc363ebaf6b689474db18a1f6438bc82358b024abd4c2ca19"}, {file = "diskcache-5.6.3.tar.gz", hash = "sha256:2c3a3fa2743d8535d832ec61c2054a1641f41775aa7c556758a109941e33e4fc"}, @@ -975,6 +1036,7 @@ version = "1.9.0" description = "Distro - an OS platform information API" optional = false python-versions = ">=3.6" +groups = ["main"] files = [ {file = "distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2"}, {file = "distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed"}, @@ -986,6 +1048,8 @@ version = "2.6.1" description = "DNS toolkit" optional = true python-versions = ">=3.8" +groups = ["main"] +markers = "extra == \"proxy\"" files = [ {file = "dnspython-2.6.1-py3-none-any.whl", hash = "sha256:5ef3b9680161f6fa89daf8ad451b5f1a33b18ae8a1c6778cdf4b43f08c0a6e50"}, {file = "dnspython-2.6.1.tar.gz", hash = "sha256:e8f0f9c23a7b7cb99ded64e6c3a6f3e701d78f50c55e002b839dea7225cff7cc"}, @@ -1006,6 +1070,8 @@ version = "7.1.0" description = "A Python library for the Docker Engine API." optional = true python-versions = ">=3.8" +groups = ["main"] +markers = "python_version >= \"3.10\" and extra == \"mlflow\"" files = [ {file = "docker-7.1.0-py3-none-any.whl", hash = "sha256:c96b93b7f0a746f9e77d325bcfb87422a3d8bd4f03136ae8a85b37f1898d5fc0"}, {file = "docker-7.1.0.tar.gz", hash = "sha256:ad8c70e6e3f8926cb8a92619b832b4ea5299e2831c14284663184e200546fa6c"}, @@ -1028,6 +1094,8 @@ version = "0.20.1" description = "Docutils -- Python Documentation Utilities" optional = true python-versions = ">=3.7" +groups = ["main"] +markers = "extra == \"utils\"" files = [ {file = "docutils-0.20.1-py3-none-any.whl", hash = "sha256:96f387a2c5562db4476f09f13bbab2192e764cac08ebbf3a34a95d9b1e4a59d6"}, {file = "docutils-0.20.1.tar.gz", hash = "sha256:f08a4e276c3a1583a86dce3e34aba3fe04d02bba2dd51ed16106244e8a923e3b"}, @@ -1039,6 +1107,8 @@ version = "2.2.0" description = "A robust email address syntax and deliverability validation library." optional = true python-versions = ">=3.8" +groups = ["main"] +markers = "extra == \"proxy\"" files = [ {file = "email_validator-2.2.0-py3-none-any.whl", hash = "sha256:561977c2d73ce3611850a06fa56b414621e0c8faa9d66f2611407d87465da631"}, {file = "email_validator-2.2.0.tar.gz", hash = "sha256:cb690f344c617a714f22e66ae771445a1ceb46821152df8e165c5f9a364582b7"}, @@ -1054,6 +1124,8 @@ version = "1.3.0" description = "Backport of PEP 654 (exception groups)" optional = false python-versions = ">=3.7" +groups = ["main", "dev", "proxy-dev"] +markers = "python_version <= \"3.10\"" files = [ {file = "exceptiongroup-1.3.0-py3-none-any.whl", hash = "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10"}, {file = "exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88"}, @@ -1071,6 +1143,8 @@ version = "0.115.14" description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production" optional = true python-versions = ">=3.8" +groups = ["main"] +markers = "python_version >= \"3.10\" and (extra == \"mlflow\" or extra == \"proxy\") or extra == \"proxy\"" files = [ {file = "fastapi-0.115.14-py3-none-any.whl", hash = "sha256:6c0c8bf9420bd58f565e585036d971872472b4f7d3f6c73b698e10cffdefb3ca"}, {file = "fastapi-0.115.14.tar.gz", hash = "sha256:b1de15cdc1c499a4da47914db35d0e4ef8f1ce62b624e94e0e5824421df99739"}, @@ -1091,6 +1165,8 @@ version = "0.16.0" description = "FastAPI plugin to enable SSO to most common providers (such as Facebook login, Google login and login via Microsoft Office 365 Account)" optional = true python-versions = "<4.0,>=3.8" +groups = ["main"] +markers = "extra == \"proxy\"" files = [ {file = "fastapi_sso-0.16.0-py3-none-any.whl", hash = "sha256:3a66a942474ef9756d3a9d8b945d55bd9faf99781facdb9b87a40b73d6d6b0c3"}, {file = "fastapi_sso-0.16.0.tar.gz", hash = "sha256:f3941f986347566b7d3747c710cf474a907f581bfb6697ff3bb3e44eb76b438c"}, @@ -1109,6 +1185,8 @@ version = "1.12.0" description = "Fast read/write of AVRO files" optional = true python-versions = ">=3.9" +groups = ["main"] +markers = "python_version >= \"3.9\" 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"}, @@ -1160,6 +1238,7 @@ version = "3.16.1" description = "A platform independent file lock." optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "filelock-3.16.1-py3-none-any.whl", hash = "sha256:2082e5703d51fbf98ea75855d9d5527e33d8ff23099bec374a134febee6946b0"}, {file = "filelock-3.16.1.tar.gz", hash = "sha256:c249fbfcd5db47e5e2d6d62198e565475ee65e4831e2561c8e313fa7eb961435"}, @@ -1168,7 +1247,7 @@ files = [ [package.extras] docs = ["furo (>=2024.8.6)", "sphinx (>=8.0.2)", "sphinx-autodoc-typehints (>=2.4.1)"] 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)"] +typing = ["typing-extensions (>=4.12.2) ; python_version < \"3.11\""] [[package]] name = "flake8" @@ -1176,6 +1255,7 @@ version = "6.1.0" description = "the modular source code checker: pep8 pyflakes and co" optional = false python-versions = ">=3.8.1" +groups = ["dev"] files = [ {file = "flake8-6.1.0-py2.py3-none-any.whl", hash = "sha256:ffdfce58ea94c6580c77888a86506937f9a1a227dfcd15f245d694ae20a6b6e5"}, {file = "flake8-6.1.0.tar.gz", hash = "sha256:d5b3857f07c030bdb5bf41c7f53799571d75c4491748a3adcd47de929e34cd23"}, @@ -1192,6 +1272,8 @@ version = "3.1.1" description = "A simple framework for building complex web applications." optional = true python-versions = ">=3.9" +groups = ["main"] +markers = "python_version >= \"3.10\" and extra == \"mlflow\"" files = [ {file = "flask-3.1.1-py3-none-any.whl", hash = "sha256:07aae2bb5eaf77993ef57e357491839f5fd9f4dc281593a81a9e4d79a24f295c"}, {file = "flask-3.1.1.tar.gz", hash = "sha256:284c7b8f2f58cb737f0cf1c30fd7eaf0ccfcde196099d24ecede3fc2005aa59e"}, @@ -1215,6 +1297,8 @@ version = "4.59.0" description = "Tools to manipulate font files" optional = true python-versions = ">=3.9" +groups = ["main"] +markers = "python_version >= \"3.10\" and extra == \"mlflow\"" files = [ {file = "fonttools-4.59.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:524133c1be38445c5c0575eacea42dbd44374b310b1ffc4b60ff01d881fabb96"}, {file = "fonttools-4.59.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:21e606b2d38fed938dde871c5736822dd6bda7a4631b92e509a1f5cd1b90c5df"}, @@ -1261,17 +1345,17 @@ files = [ ] [package.extras] -all = ["brotli (>=1.0.1)", "brotlicffi (>=0.8.0)", "lxml (>=4.0)", "lz4 (>=1.7.4.2)", "matplotlib", "munkres", "pycairo", "scipy", "skia-pathops (>=0.5.0)", "sympy", "uharfbuzz (>=0.23.0)", "unicodedata2 (>=15.1.0)", "xattr", "zopfli (>=0.1.4)"] +all = ["brotli (>=1.0.1) ; platform_python_implementation == \"CPython\"", "brotlicffi (>=0.8.0) ; platform_python_implementation != \"CPython\"", "lxml (>=4.0)", "lz4 (>=1.7.4.2)", "matplotlib", "munkres ; platform_python_implementation == \"PyPy\"", "pycairo", "scipy ; platform_python_implementation != \"PyPy\"", "skia-pathops (>=0.5.0)", "sympy", "uharfbuzz (>=0.23.0)", "unicodedata2 (>=15.1.0) ; python_version <= \"3.12\"", "xattr ; sys_platform == \"darwin\"", "zopfli (>=0.1.4)"] graphite = ["lz4 (>=1.7.4.2)"] -interpolatable = ["munkres", "pycairo", "scipy"] +interpolatable = ["munkres ; platform_python_implementation == \"PyPy\"", "pycairo", "scipy ; platform_python_implementation != \"PyPy\""] lxml = ["lxml (>=4.0)"] pathops = ["skia-pathops (>=0.5.0)"] plot = ["matplotlib"] repacker = ["uharfbuzz (>=0.23.0)"] symfont = ["sympy"] -type1 = ["xattr"] -unicode = ["unicodedata2 (>=15.1.0)"] -woff = ["brotli (>=1.0.1)", "brotlicffi (>=0.8.0)", "zopfli (>=0.1.4)"] +type1 = ["xattr ; sys_platform == \"darwin\""] +unicode = ["unicodedata2 (>=15.1.0) ; python_version <= \"3.12\""] +woff = ["brotli (>=1.0.1) ; platform_python_implementation == \"CPython\"", "brotlicffi (>=0.8.0) ; platform_python_implementation != \"CPython\"", "zopfli (>=0.1.4)"] [[package]] name = "frozenlist" @@ -1279,6 +1363,7 @@ version = "1.5.0" description = "A list-like structure which implements collections.abc.MutableSequence" optional = false python-versions = ">=3.8" +groups = ["main"] 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"}, @@ -1380,6 +1465,7 @@ version = "2025.3.0" description = "File-system specification" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "fsspec-2025.3.0-py3-none-any.whl", hash = "sha256:efb87af3efa9103f94ca91a7f8cb7a4df91af9f74fc106c9c7ea0efd7277c1b3"}, {file = "fsspec-2025.3.0.tar.gz", hash = "sha256:a935fd1ea872591f2b5148907d103488fc523295e6c64b835cfad8c3eca44972"}, @@ -1419,6 +1505,8 @@ version = "4.0.12" description = "Git Object Database" optional = true python-versions = ">=3.7" +groups = ["main"] +markers = "python_version >= \"3.10\" and extra == \"mlflow\"" files = [ {file = "gitdb-4.0.12-py3-none-any.whl", hash = "sha256:67073e15955400952c6565cc3e707c554a4eea2e428946f7a4c162fab9bd9bcf"}, {file = "gitdb-4.0.12.tar.gz", hash = "sha256:5ef71f855d191a3326fcfbc0d5da835f26b13fbcba60c32c21091c349ffdb571"}, @@ -1433,6 +1521,8 @@ version = "3.1.45" description = "GitPython is a Python library used to interact with Git repositories" optional = true python-versions = ">=3.7" +groups = ["main"] +markers = "python_version >= \"3.10\" and extra == \"mlflow\"" files = [ {file = "gitpython-3.1.45-py3-none-any.whl", hash = "sha256:8908cb2e02fb3b93b7eb0f2827125cb699869470432cc885f019b8fd0fccff77"}, {file = "gitpython-3.1.45.tar.gz", hash = "sha256:85b0ee964ceddf211c41b9f27a49086010a190fd8132a24e21f362a4b36a791c"}, @@ -1443,7 +1533,7 @@ gitdb = ">=4.0.1,<5" [package.extras] doc = ["sphinx (>=7.1.2,<7.2)", "sphinx-autodoc-typehints", "sphinx_rtd_theme"] -test = ["coverage[toml]", "ddt (>=1.1.1,!=1.4.3)", "mock", "mypy", "pre-commit", "pytest (>=7.3.1)", "pytest-cov", "pytest-instafail", "pytest-mock", "pytest-sugar", "typing-extensions"] +test = ["coverage[toml]", "ddt (>=1.1.1,!=1.4.3)", "mock ; python_version < \"3.8\"", "mypy", "pre-commit", "pytest (>=7.3.1)", "pytest-cov", "pytest-instafail", "pytest-mock", "pytest-sugar", "typing-extensions ; python_version < \"3.11\""] [[package]] name = "google-api-core" @@ -1451,6 +1541,8 @@ version = "2.25.1" description = "Google API client core library" optional = true python-versions = ">=3.7" +groups = ["main"] +markers = "extra == \"extra-proxy\"" files = [ {file = "google_api_core-2.25.1-py3-none-any.whl", hash = "sha256:8a2a56c1fef82987a524371f99f3bd0143702fecc670c72e600c1cda6bf8dbb7"}, {file = "google_api_core-2.25.1.tar.gz", hash = "sha256:d2aaa0b13c78c61cb3f4282c464c046e45fbd75755683c9c525e6e8f7ed0a5e8"}, @@ -1460,15 +1552,15 @@ files = [ google-auth = ">=2.14.1,<3.0.0" googleapis-common-protos = ">=1.56.2,<2.0.0" grpcio = [ - {version = ">=1.33.2,<2.0.0", optional = true, markers = "python_version < \"3.11\" and extra == \"grpc\""}, {version = ">=1.49.1,<2.0.0", optional = true, markers = "python_version >= \"3.11\" and extra == \"grpc\""}, + {version = ">=1.33.2,<2.0.0", optional = true, markers = "python_version < \"3.11\" and extra == \"grpc\""}, ] grpcio-status = [ - {version = ">=1.33.2,<2.0.0", optional = true, markers = "python_version < \"3.11\" and extra == \"grpc\""}, {version = ">=1.49.1,<2.0.0", optional = true, markers = "python_version >= \"3.11\" and extra == \"grpc\""}, + {version = ">=1.33.2,<2.0.0", optional = true, markers = "extra == \"grpc\""}, ] proto-plus = [ - {version = ">=1.22.3,<2.0.0", markers = "python_version < \"3.13\""}, + {version = ">=1.22.3,<2.0.0"}, {version = ">=1.25.0,<2.0.0", markers = "python_version >= \"3.13\""}, ] protobuf = ">=3.19.5,<3.20.0 || >3.20.0,<3.20.1 || >3.20.1,<4.21.0 || >4.21.0,<4.21.1 || >4.21.1,<4.21.2 || >4.21.2,<4.21.3 || >4.21.3,<4.21.4 || >4.21.4,<4.21.5 || >4.21.5,<7.0.0" @@ -1476,7 +1568,7 @@ requests = ">=2.18.0,<3.0.0" [package.extras] async-rest = ["google-auth[aiohttp] (>=2.35.0,<3.0.0)"] -grpc = ["grpcio (>=1.33.2,<2.0.0)", "grpcio (>=1.49.1,<2.0.0)", "grpcio-status (>=1.33.2,<2.0.0)", "grpcio-status (>=1.49.1,<2.0.0)"] +grpc = ["grpcio (>=1.33.2,<2.0.0)", "grpcio (>=1.49.1,<2.0.0) ; python_version >= \"3.11\"", "grpcio-status (>=1.33.2,<2.0.0)", "grpcio-status (>=1.49.1,<2.0.0) ; python_version >= \"3.11\""] grpcgcp = ["grpcio-gcp (>=0.2.2,<1.0.0)"] grpcio-gcp = ["grpcio-gcp (>=0.2.2,<1.0.0)"] @@ -1486,6 +1578,8 @@ version = "2.40.3" 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\"" 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"}, @@ -1499,11 +1593,11 @@ rsa = ">=3.1.4,<5" [package.extras] aiohttp = ["aiohttp (>=3.6.2,<4.0.0)", "requests (>=2.20.0,<3.0.0)"] enterprise-cert = ["cryptography", "pyopenssl"] -pyjwt = ["cryptography (<39.0.0)", "cryptography (>=38.0.3)", "pyjwt (>=2.0)"] -pyopenssl = ["cryptography (<39.0.0)", "cryptography (>=38.0.3)", "pyopenssl (>=20.0.0)"] +pyjwt = ["cryptography (<39.0.0) ; python_version < \"3.8\"", "cryptography (>=38.0.3)", "pyjwt (>=2.0)"] +pyopenssl = ["cryptography (<39.0.0) ; python_version < \"3.8\"", "cryptography (>=38.0.3)", "pyopenssl (>=20.0.0)"] reauth = ["pyu2f (>=0.1.5)"] requests = ["requests (>=2.20.0,<3.0.0)"] -testing = ["aiohttp (<3.10.0)", "aiohttp (>=3.6.2,<4.0.0)", "aioresponses", "cryptography (<39.0.0)", "cryptography (>=38.0.3)", "flask", "freezegun", "grpcio", "mock", "oauth2client", "packaging", "pyjwt (>=2.0)", "pyopenssl (<24.3.0)", "pyopenssl (>=20.0.0)", "pytest", "pytest-asyncio", "pytest-cov", "pytest-localserver", "pyu2f (>=0.1.5)", "requests (>=2.20.0,<3.0.0)", "responses", "urllib3"] +testing = ["aiohttp (<3.10.0)", "aiohttp (>=3.6.2,<4.0.0)", "aioresponses", "cryptography (<39.0.0) ; python_version < \"3.8\"", "cryptography (>=38.0.3)", "flask", "freezegun", "grpcio", "mock", "oauth2client", "packaging", "pyjwt (>=2.0)", "pyopenssl (<24.3.0)", "pyopenssl (>=20.0.0)", "pytest", "pytest-asyncio", "pytest-cov", "pytest-localserver", "pyu2f (>=0.1.5)", "requests (>=2.20.0,<3.0.0)", "responses", "urllib3"] urllib3 = ["packaging", "urllib3"] [[package]] @@ -1512,6 +1606,8 @@ version = "2.24.2" description = "Google Cloud Kms API client library" optional = true python-versions = ">=3.7" +groups = ["main"] +markers = "extra == \"extra-proxy\"" files = [ {file = "google_cloud_kms-2.24.2-py2.py3-none-any.whl", hash = "sha256:368209b035dfac691a467c1cf50986d8b1b26cac1166bdfbaa25d738df91ff7b"}, {file = "google_cloud_kms-2.24.2.tar.gz", hash = "sha256:e9e18bbfafd1a4035c76c03fb5ff03f4f57f596d08e1a9ede7e69ec0151b27a1"}, @@ -1530,10 +1626,12 @@ version = "1.70.0" description = "Common protobufs used in Google APIs" optional = false python-versions = ">=3.7" +groups = ["main", "dev", "proxy-dev"] files = [ {file = "googleapis_common_protos-1.70.0-py3-none-any.whl", hash = "sha256:b8bfcca8c25a2bb253e0e0b0adaf8c00773e5e6af6fd92397576680b807e0fd8"}, {file = "googleapis_common_protos-1.70.0.tar.gz", hash = "sha256:0e1b44e0ea153e6594f9f394fef15193a68aaaea2d843f83e2742717ca753257"}, ] +markers = {main = "extra == \"extra-proxy\""} [package.dependencies] grpcio = {version = ">=1.44.0,<2.0.0", optional = true, markers = "extra == \"grpc\""} @@ -1548,6 +1646,8 @@ version = "3.4.3" description = "GraphQL Framework for Python" optional = true python-versions = "*" +groups = ["main"] +markers = "python_version >= \"3.10\" and extra == \"mlflow\"" files = [ {file = "graphene-3.4.3-py2.py3-none-any.whl", hash = "sha256:820db6289754c181007a150db1f7fff544b94142b556d12e3ebc777a7bf36c71"}, {file = "graphene-3.4.3.tar.gz", hash = "sha256:2a3786948ce75fe7e078443d37f609cbe5bb36ad8d6b828740ad3b95ed1a0aaa"}, @@ -1569,6 +1669,8 @@ version = "3.2.6" description = "GraphQL implementation for Python, a port of GraphQL.js, the JavaScript reference implementation for GraphQL." optional = true python-versions = "<4,>=3.6" +groups = ["main"] +markers = "python_version >= \"3.10\" and extra == \"mlflow\"" files = [ {file = "graphql_core-3.2.6-py3-none-any.whl", hash = "sha256:78b016718c161a6fb20a7d97bbf107f331cd1afe53e45566c59f776ed7f0b45f"}, {file = "graphql_core-3.2.6.tar.gz", hash = "sha256:c08eec22f9e40f0bd61d805907e3b3b1b9a320bc606e23dc145eebca07c8fbab"}, @@ -1580,6 +1682,8 @@ version = "3.2.0" description = "Relay library for graphql-core" optional = true python-versions = ">=3.6,<4" +groups = ["main"] +markers = "python_version >= \"3.10\" and extra == \"mlflow\"" files = [ {file = "graphql-relay-3.2.0.tar.gz", hash = "sha256:1ff1c51298356e481a0be009ccdff249832ce53f30559c1338f22a0e0d17250c"}, {file = "graphql_relay-3.2.0-py3-none-any.whl", hash = "sha256:c9b22bd28b170ba1fe674c74384a8ff30a76c8e26f88ac3aa1584dd3179953e5"}, @@ -1594,6 +1698,8 @@ version = "3.2.3" description = "Lightweight in-process concurrent programming" optional = true python-versions = ">=3.9" +groups = ["main"] +markers = "python_version >= \"3.10\" and (platform_machine == \"aarch64\" or platform_machine == \"ppc64le\" or platform_machine == \"x86_64\" or platform_machine == \"amd64\" or platform_machine == \"AMD64\" or platform_machine == \"win32\" or platform_machine == \"WIN32\") and extra == \"mlflow\" and python_version < \"3.14\"" files = [ {file = "greenlet-3.2.3-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:1afd685acd5597349ee6d7a88a8bec83ce13c106ac78c196ee9dde7c04fe87be"}, {file = "greenlet-3.2.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:761917cac215c61e9dc7324b2606107b3b292a8349bdebb31503ab4de3f559ac"}, @@ -1661,6 +1767,8 @@ version = "0.14.2" description = "IAM API client library" optional = true python-versions = ">=3.7" +groups = ["main"] +markers = "extra == \"extra-proxy\"" files = [ {file = "grpc_google_iam_v1-0.14.2-py3-none-any.whl", hash = "sha256:a3171468459770907926d56a440b2bb643eec1d7ba215f48f3ecece42b4d8351"}, {file = "grpc_google_iam_v1-0.14.2.tar.gz", hash = "sha256:b3e1fc387a1a329e41672197d0ace9de22c78dd7d215048c4c78712073f7bd20"}, @@ -1677,6 +1785,7 @@ version = "1.70.0" description = "HTTP/2-based RPC framework" optional = false python-versions = ">=3.8" +groups = ["main", "dev", "proxy-dev"] files = [ {file = "grpcio-1.70.0-cp310-cp310-linux_armv7l.whl", hash = "sha256:95469d1977429f45fe7df441f586521361e235982a0b39e33841549143ae2851"}, {file = "grpcio-1.70.0-cp310-cp310-macosx_12_0_universal2.whl", hash = "sha256:ed9718f17fbdb472e33b869c77a16d0b55e166b100ec57b016dc7de9c8d236bf"}, @@ -1734,6 +1843,7 @@ 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\""} [package.extras] protobuf = ["grpcio-tools (>=1.70.0)"] @@ -1744,6 +1854,8 @@ version = "1.62.3" description = "Status proto mapping for gRPC" optional = true python-versions = ">=3.6" +groups = ["main"] +markers = "extra == \"extra-proxy\"" files = [ {file = "grpcio-status-1.62.3.tar.gz", hash = "sha256:289bdd7b2459794a12cf95dc0cb727bd4a1742c37bd823f760236c937e53a485"}, {file = "grpcio_status-1.62.3-py3-none-any.whl", hash = "sha256:f9049b762ba8de6b1086789d8315846e094edac2c50beaf462338b301a8fd4b8"}, @@ -1760,6 +1872,8 @@ version = "23.0.0" 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\"" files = [ {file = "gunicorn-23.0.0-py3-none-any.whl", hash = "sha256:ec400d38950de4dfd418cff8328b2c8faed0edb0d517d3394e457c317908ca4d"}, {file = "gunicorn-23.0.0.tar.gz", hash = "sha256:f014447a0101dc57e294f6c18ca6b40227a4c90e9bdb586042628030cba004ec"}, @@ -1781,6 +1895,7 @@ version = "0.16.0" description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" optional = false python-versions = ">=3.8" +groups = ["main", "dev", "proxy-dev"] files = [ {file = "h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86"}, {file = "h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1"}, @@ -1792,6 +1907,7 @@ version = "4.1.0" description = "HTTP/2 State-Machine based protocol implementation" optional = false python-versions = ">=3.6.1" +groups = ["proxy-dev"] files = [ {file = "h2-4.1.0-py3-none-any.whl", hash = "sha256:03a46bcf682256c95b5fd9e9a99c1323584c3eec6440d379b9903d709476bc6d"}, {file = "h2-4.1.0.tar.gz", hash = "sha256:a83aca08fbe7aacb79fec788c9c0bac936343560ed9ec18b82a13a12c28d2abb"}, @@ -1807,6 +1923,8 @@ version = "1.1.5" description = "Fast transfer of large files with the Hugging Face Hub." optional = false python-versions = ">=3.8" +groups = ["main"] +markers = "platform_machine == \"x86_64\" or platform_machine == \"amd64\" or platform_machine == \"arm64\" or platform_machine == \"aarch64\"" files = [ {file = "hf_xet-1.1.5-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:f52c2fa3635b8c37c7764d8796dfa72706cc4eded19d638331161e82b0792e23"}, {file = "hf_xet-1.1.5-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:9fa6e3ee5d61912c4a113e0708eaaef987047616465ac7aa30f7121a48fc1af8"}, @@ -1827,6 +1945,7 @@ version = "4.0.0" description = "Pure-Python HPACK header compression" optional = false python-versions = ">=3.6.1" +groups = ["proxy-dev"] files = [ {file = "hpack-4.0.0-py3-none-any.whl", hash = "sha256:84a076fad3dc9a9f8063ccb8041ef100867b1878b25ef0ee63847a5d53818a6c"}, {file = "hpack-4.0.0.tar.gz", hash = "sha256:fc41de0c63e687ebffde81187a948221294896f6bdc0ae2312708df339430095"}, @@ -1838,6 +1957,7 @@ version = "1.0.9" description = "A minimal low-level HTTP client." optional = false python-versions = ">=3.8" +groups = ["main", "dev", "proxy-dev"] files = [ {file = "httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55"}, {file = "httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8"}, @@ -1859,6 +1979,7 @@ version = "0.28.1" description = "The next generation HTTP client." optional = false python-versions = ">=3.8" +groups = ["main", "dev", "proxy-dev"] files = [ {file = "httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad"}, {file = "httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc"}, @@ -1871,7 +1992,7 @@ httpcore = "==1.*" idna = "*" [package.extras] -brotli = ["brotli", "brotlicffi"] +brotli = ["brotli ; platform_python_implementation == \"CPython\"", "brotlicffi ; platform_python_implementation != \"CPython\""] cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<14)"] http2 = ["h2 (>=3,<5)"] socks = ["socksio (==1.*)"] @@ -1883,6 +2004,8 @@ version = "0.4.1" description = "Consume Server-Sent Event (SSE) messages with HTTPX." optional = true python-versions = ">=3.9" +groups = ["main"] +markers = "python_version >= \"3.10\" and extra == \"proxy\"" files = [ {file = "httpx_sse-0.4.1-py3-none-any.whl", hash = "sha256:cba42174344c3a5b06f255ce65b350880f962d99ead85e776f23c6618a377a37"}, {file = "httpx_sse-0.4.1.tar.gz", hash = "sha256:8f44d34414bc7b21bf3602713005c5df4917884f76072479b21f68befa4ea26e"}, @@ -1894,6 +2017,7 @@ version = "0.34.3" 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.34.3-py3-none-any.whl", hash = "sha256:5444550099e2d86e68b2898b09e85878fbd788fc2957b506c6a79ce060e39492"}, {file = "huggingface_hub-0.34.3.tar.gz", hash = "sha256:d58130fd5aa7408480681475491c0abd7e835442082fbc3ef4d45b6c39f83853"}, @@ -1910,16 +2034,16 @@ tqdm = ">=4.42.1" typing-extensions = ">=3.7.4.3" [package.extras] -all = ["InquirerPy (==0.3.4)", "Jinja2", "Pillow", "aiohttp", "authlib (>=1.3.2)", "fastapi", "gradio (>=4.0.0)", "httpx", "itsdangerous", "jedi", "libcst (>=1.4.0)", "mypy (==1.15.0)", "mypy (>=1.14.1,<1.15.0)", "numpy", "pytest (>=8.1.1,<8.2.2)", "pytest-asyncio", "pytest-cov", "pytest-env", "pytest-mock", "pytest-rerunfailures", "pytest-vcr", "pytest-xdist", "ruff (>=0.9.0)", "soundfile", "types-PyYAML", "types-requests", "types-simplejson", "types-toml", "types-tqdm", "types-urllib3", "typing-extensions (>=4.8.0)", "urllib3 (<2.0)"] +all = ["InquirerPy (==0.3.4)", "Jinja2", "Pillow", "aiohttp", "authlib (>=1.3.2)", "fastapi", "gradio (>=4.0.0)", "httpx", "itsdangerous", "jedi", "libcst (>=1.4.0)", "mypy (==1.15.0) ; python_version >= \"3.9\"", "mypy (>=1.14.1,<1.15.0) ; python_version == \"3.8\"", "numpy", "pytest (>=8.1.1,<8.2.2)", "pytest-asyncio", "pytest-cov", "pytest-env", "pytest-mock", "pytest-rerunfailures", "pytest-vcr", "pytest-xdist", "ruff (>=0.9.0)", "soundfile", "types-PyYAML", "types-requests", "types-simplejson", "types-toml", "types-tqdm", "types-urllib3", "typing-extensions (>=4.8.0)", "urllib3 (<2.0)"] cli = ["InquirerPy (==0.3.4)"] -dev = ["InquirerPy (==0.3.4)", "Jinja2", "Pillow", "aiohttp", "authlib (>=1.3.2)", "fastapi", "gradio (>=4.0.0)", "httpx", "itsdangerous", "jedi", "libcst (>=1.4.0)", "mypy (==1.15.0)", "mypy (>=1.14.1,<1.15.0)", "numpy", "pytest (>=8.1.1,<8.2.2)", "pytest-asyncio", "pytest-cov", "pytest-env", "pytest-mock", "pytest-rerunfailures", "pytest-vcr", "pytest-xdist", "ruff (>=0.9.0)", "soundfile", "types-PyYAML", "types-requests", "types-simplejson", "types-toml", "types-tqdm", "types-urllib3", "typing-extensions (>=4.8.0)", "urllib3 (<2.0)"] +dev = ["InquirerPy (==0.3.4)", "Jinja2", "Pillow", "aiohttp", "authlib (>=1.3.2)", "fastapi", "gradio (>=4.0.0)", "httpx", "itsdangerous", "jedi", "libcst (>=1.4.0)", "mypy (==1.15.0) ; python_version >= \"3.9\"", "mypy (>=1.14.1,<1.15.0) ; python_version == \"3.8\"", "numpy", "pytest (>=8.1.1,<8.2.2)", "pytest-asyncio", "pytest-cov", "pytest-env", "pytest-mock", "pytest-rerunfailures", "pytest-vcr", "pytest-xdist", "ruff (>=0.9.0)", "soundfile", "types-PyYAML", "types-requests", "types-simplejson", "types-toml", "types-tqdm", "types-urllib3", "typing-extensions (>=4.8.0)", "urllib3 (<2.0)"] fastai = ["fastai (>=2.4)", "fastcore (>=1.3.27)", "toml"] hf-transfer = ["hf-transfer (>=0.1.4)"] hf-xet = ["hf-xet (>=1.1.2,<2.0.0)"] inference = ["aiohttp"] mcp = ["aiohttp", "mcp (>=1.8.0)", "typer"] oauth = ["authlib (>=1.3.2)", "fastapi", "httpx", "itsdangerous"] -quality = ["libcst (>=1.4.0)", "mypy (==1.15.0)", "mypy (>=1.14.1,<1.15.0)", "ruff (>=0.9.0)"] +quality = ["libcst (>=1.4.0)", "mypy (==1.15.0) ; python_version >= \"3.9\"", "mypy (>=1.14.1,<1.15.0) ; python_version == \"3.8\"", "ruff (>=0.9.0)"] tensorflow = ["graphviz", "pydot", "tensorflow"] tensorflow-testing = ["keras (<3.0)", "tensorflow"] testing = ["InquirerPy (==0.3.4)", "Jinja2", "Pillow", "aiohttp", "authlib (>=1.3.2)", "fastapi", "gradio (>=4.0.0)", "httpx", "itsdangerous", "jedi", "numpy", "pytest (>=8.1.1,<8.2.2)", "pytest-asyncio", "pytest-cov", "pytest-env", "pytest-mock", "pytest-rerunfailures", "pytest-vcr", "pytest-xdist", "soundfile", "urllib3 (<2.0)"] @@ -1932,6 +2056,8 @@ version = "10.0" description = "Human friendly output for text interfaces using Python" optional = true python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +groups = ["main"] +markers = "python_version >= \"3.9\" and extra == \"extra-proxy\" and python_version < \"3.14\"" files = [ {file = "humanfriendly-10.0-py2.py3-none-any.whl", hash = "sha256:1697e1a8a8f550fd43c2865cd84542fc175a61dcb779b6fee18cf6b6ccba1477"}, {file = "humanfriendly-10.0.tar.gz", hash = "sha256:6b0b831ce8f15f7300721aa49829fc4e83921a9a301cc7f606be6686a2288ddc"}, @@ -1946,6 +2072,7 @@ version = "0.15.0" description = "A ASGI Server based on Hyper libraries and inspired by Gunicorn" optional = false python-versions = ">=3.7" +groups = ["proxy-dev"] files = [ {file = "hypercorn-0.15.0-py3-none-any.whl", hash = "sha256:5008944999612fd188d7a1ca02e89d20065642b89503020ac392dfed11840730"}, {file = "hypercorn-0.15.0.tar.gz", hash = "sha256:d517f68d5dc7afa9a9d50ecefb0f769f466ebe8c1c18d2c2f447a24e763c9a63"}, @@ -1963,7 +2090,7 @@ wsproto = ">=0.14.0" docs = ["pydata_sphinx_theme", "sphinxcontrib_mermaid"] h3 = ["aioquic (>=0.9.0,<1.0)"] trio = ["exceptiongroup (>=1.1.0)", "trio (>=0.22.0)"] -uvloop = ["uvloop"] +uvloop = ["uvloop ; platform_system != \"Windows\""] [[package]] name = "hyperframe" @@ -1971,6 +2098,7 @@ version = "6.0.1" description = "HTTP/2 framing layer for Python" optional = false python-versions = ">=3.6.1" +groups = ["proxy-dev"] files = [ {file = "hyperframe-6.0.1-py3-none-any.whl", hash = "sha256:0ec6bafd80d8ad2195c4f03aacba3a8265e57bc4cff261e802bf39970ed02a15"}, {file = "hyperframe-6.0.1.tar.gz", hash = "sha256:ae510046231dc8e9ecb1a6586f63d2347bf4c8905914aa84ba585ae85f28a914"}, @@ -1982,6 +2110,7 @@ version = "3.10" description = "Internationalized Domain Names in Applications (IDNA)" optional = false python-versions = ">=3.6" +groups = ["main", "dev", "proxy-dev"] files = [ {file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"}, {file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"}, @@ -1996,6 +2125,8 @@ version = "1.4.1" description = "Getting image size from png/jpeg/jpeg2000/gif file" optional = true python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +groups = ["main"] +markers = "extra == \"utils\"" files = [ {file = "imagesize-1.4.1-py2.py3-none-any.whl", hash = "sha256:0d8d18d08f840c19d0ee7ca1fd82490fdc3729b7ac93f49870406ddde8ef8d8b"}, {file = "imagesize-1.4.1.tar.gz", hash = "sha256:69150444affb9cb0d5cc5a92b3676f0b2fb7cd9ae39e947a5e11a36b4497cd4a"}, @@ -2007,6 +2138,7 @@ version = "6.11.0" description = "Read metadata from Python packages" optional = false python-versions = ">=3.8" +groups = ["main", "dev", "proxy-dev"] 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"}, @@ -2018,7 +2150,7 @@ zipp = ">=0.5" [package.extras] docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-lint"] perf = ["ipython"] -testing = ["flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pyfakefs", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy (>=0.9.1)", "pytest-perf (>=0.9.2)", "pytest-ruff"] +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-resources" @@ -2026,6 +2158,8 @@ version = "6.4.5" description = "Read resources from Python packages" optional = false python-versions = ">=3.8" +groups = ["main"] +markers = "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"}, @@ -2035,7 +2169,7 @@ files = [ zipp = {version = ">=3.1.0", markers = "python_version < \"3.10\""} [package.extras] -check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)"] +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)"] @@ -2048,6 +2182,7 @@ version = "2.1.0" description = "brain-dead simple config-ini parsing" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760"}, {file = "iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7"}, @@ -2059,6 +2194,8 @@ version = "0.7.2" description = "An ISO 8601 date/time/duration parser and formatter" optional = true python-versions = ">=3.7" +groups = ["main"] +markers = "extra == \"extra-proxy\" or extra == \"proxy\"" files = [ {file = "isodate-0.7.2-py3-none-any.whl", hash = "sha256:28009937d8031054830160fce6d409ed342816b543597cece116d966c6d99e15"}, {file = "isodate-0.7.2.tar.gz", hash = "sha256:4cd1aa0f43ca76f4a6c6c0292a85f40b35ec2e43e315b59f06e6d32171a953e6"}, @@ -2070,6 +2207,8 @@ version = "2.2.0" description = "Safely pass data to untrusted environments and back." optional = true python-versions = ">=3.8" +groups = ["main"] +markers = "python_version >= \"3.10\" and extra == \"mlflow\"" files = [ {file = "itsdangerous-2.2.0-py3-none-any.whl", hash = "sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef"}, {file = "itsdangerous-2.2.0.tar.gz", hash = "sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173"}, @@ -2081,6 +2220,7 @@ version = "3.1.6" description = "A very fast and expressive template engine." optional = false python-versions = ">=3.7" +groups = ["main", "proxy-dev"] files = [ {file = "jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67"}, {file = "jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d"}, @@ -2098,6 +2238,7 @@ version = "0.9.1" description = "Fast iterable JSON parser." optional = false python-versions = ">=3.8" +groups = ["main"] 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"}, @@ -2183,6 +2324,8 @@ version = "1.0.1" description = "JSON Matching Expressions" optional = true python-versions = ">=3.7" +groups = ["main"] +markers = "extra == \"proxy\"" files = [ {file = "jmespath-1.0.1-py3-none-any.whl", hash = "sha256:02e2e4cc71b5bcab88332eebf907519190dd9e6e82107fa7f83b1003a6252980"}, {file = "jmespath-1.0.1.tar.gz", hash = "sha256:90261b206d6defd58fdd5e85f478bf633a2901798906be2ad389150c5c60edbe"}, @@ -2194,6 +2337,8 @@ version = "1.5.1" description = "Lightweight pipelining with Python functions" optional = true python-versions = ">=3.9" +groups = ["main"] +markers = "python_version >= \"3.10\" and extra == \"mlflow\"" files = [ {file = "joblib-1.5.1-py3-none-any.whl", hash = "sha256:4719a31f054c7d766948dcd83e9613686b27114f190f717cec7eaa2084f8a74a"}, {file = "joblib-1.5.1.tar.gz", hash = "sha256:f4f86e351f39fe3d0d32a9f2c3d8af1ee4cec285aafcb27003dda5205576b444"}, @@ -2205,6 +2350,7 @@ version = "4.23.0" description = "An implementation of JSON Schema validation for Python" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "jsonschema-4.23.0-py3-none-any.whl", hash = "sha256:fbadb6f8b144a8f8cf9f0b89ba94501d143e50411a1278633f56a7acf7fd5566"}, {file = "jsonschema-4.23.0.tar.gz", hash = "sha256:d71497fef26351a33265337fa77ffeb82423f3ea21283cd9467bb03999266bc4"}, @@ -2228,6 +2374,7 @@ version = "2023.12.1" description = "The JSON Schema meta-schemas and vocabularies, exposed as a Registry" optional = false python-versions = ">=3.8" +groups = ["main"] 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"}, @@ -2243,6 +2390,8 @@ version = "1.4.8" description = "A fast implementation of the Cassowary constraint solver" optional = true python-versions = ">=3.10" +groups = ["main"] +markers = "python_version >= \"3.10\" and extra == \"mlflow\"" files = [ {file = "kiwisolver-1.4.8-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:88c6f252f6816a73b1f8c904f7bbe02fd67c09a69f7cb8a0eecdbf5ce78e63db"}, {file = "kiwisolver-1.4.8-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c72941acb7b67138f35b879bbe85be0f6c6a70cab78fe3ef6db9c024d9223e5b"}, @@ -2332,6 +2481,7 @@ version = "2.54.1" description = "A client library for accessing langfuse" optional = false python-versions = "<4.0,>=3.8.1" +groups = ["dev"] files = [ {file = "langfuse-2.54.1-py3-none-any.whl", hash = "sha256:1f1261cf763886758c70e192133340ff296169cc0930cde725eee52d467eb661"}, {file = "langfuse-2.54.1.tar.gz", hash = "sha256:7efc70799740ffa0ac7e04066e0596fb6433e8e501fc850c6a4e7967de6de8a7"}, @@ -2353,13 +2503,14 @@ openai = ["openai (>=0.27.8)"] [[package]] name = "litellm-enterprise" -version = "0.1.16" +version = "0.1.17" description = "Package for LiteLLM Enterprise features" 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_enterprise-0.1.16-py3-none-any.whl", hash = "sha256:ceccc8cb579e06fb12c1d209065064188336305be6d024cb050d44e0b5ad9cf3"}, - {file = "litellm_enterprise-0.1.16.tar.gz", hash = "sha256:726194d3c3e8b154912ef021253a4a1dd6cb9ffa7f5249cd32c59c7c1235b3a8"}, + {file = "litellm_enterprise-0.1.17.tar.gz", hash = "sha256:5d327c18eb0ce4129d1d456f17a3ba818bd11447b7e48ab325418b5d38b58ce4"}, ] [[package]] @@ -2368,6 +2519,8 @@ version = "0.2.15" 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.15-py3-none-any.whl", hash = "sha256:25e7d7cabe3f10233e2802d4521576bef438eea8b0800b1dbf38ba83ba751bb8"}, {file = "litellm_proxy_extras-0.2.15.tar.gz", hash = "sha256:62a9fdcb77d25aa7bfdfa04ac878e452f185dfb0b9538d4d8988a1b512360649"}, @@ -2379,6 +2532,8 @@ version = "1.3.10" description = "A super-fast templating language that borrows the best ideas from the existing templating languages." optional = true python-versions = ">=3.8" +groups = ["main"] +markers = "python_version >= \"3.10\" and extra == \"mlflow\"" files = [ {file = "mako-1.3.10-py3-none-any.whl", hash = "sha256:baef24a52fc4fc514a0887ac600f9f1cff3d82c61d4d700a1fa84d597b88db59"}, {file = "mako-1.3.10.tar.gz", hash = "sha256:99579a6f39583fa7e5630a28c3c1f440e4e97a414b80372649c0ce338da2ea28"}, @@ -2398,6 +2553,8 @@ version = "3.0.0" description = "Python port of markdown-it. Markdown parsing, done right!" optional = true python-versions = ">=3.8" +groups = ["main"] +markers = "extra == \"proxy\"" 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"}, @@ -2422,6 +2579,7 @@ version = "2.1.5" description = "Safely add untrusted strings to HTML/XML markup." optional = false python-versions = ">=3.7" +groups = ["main", "proxy-dev"] 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"}, @@ -2491,6 +2649,8 @@ version = "3.10.5" description = "Python plotting package" optional = true python-versions = ">=3.10" +groups = ["main"] +markers = "python_version >= \"3.10\" and extra == \"mlflow\"" files = [ {file = "matplotlib-3.10.5-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:5d4773a6d1c106ca05cb5a5515d277a6bb96ed09e5c8fab6b7741b8fcaa62c8f"}, {file = "matplotlib-3.10.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dc88af74e7ba27de6cbe6faee916024ea35d895ed3d61ef6f58c4ce97da7185a"}, @@ -2569,6 +2729,7 @@ version = "0.7.0" description = "McCabe checker, plugin for flake8" optional = false python-versions = ">=3.6" +groups = ["dev"] files = [ {file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"}, {file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"}, @@ -2580,6 +2741,8 @@ version = "1.12.3" description = "Model Context Protocol SDK" optional = true python-versions = ">=3.10" +groups = ["main"] +markers = "python_version >= \"3.10\" and extra == \"proxy\"" files = [ {file = "mcp-1.12.3-py3-none-any.whl", hash = "sha256:5483345bf39033b858920a5b6348a303acacf45b23936972160ff152107b850e"}, {file = "mcp-1.12.3.tar.gz", hash = "sha256:ab2e05f5e5c13e1dc90a4a9ef23ac500a6121362a564447855ef0ab643a99fed"}, @@ -2609,6 +2772,8 @@ version = "0.1.2" description = "Markdown URL utilities" optional = true python-versions = ">=3.7" +groups = ["main"] +markers = "extra == \"proxy\"" files = [ {file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"}, {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"}, @@ -2620,6 +2785,8 @@ version = "0.4.1" description = "" optional = true python-versions = ">=3.9" +groups = ["main"] +markers = "python_version >= \"3.9\" and extra == \"extra-proxy\" and python_version < \"3.14\"" files = [ {file = "ml_dtypes-0.4.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:1fe8b5b5e70cd67211db94b05cfd58dace592f24489b038dc6f9fe347d2e07d5"}, {file = "ml_dtypes-0.4.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c09a6d11d8475c2a9fd2bc0695628aec105f97cab3b3a3fb7c9660348ff7d24"}, @@ -2642,10 +2809,10 @@ files = [ [package.dependencies] numpy = [ - {version = ">1.20", markers = "python_version < \"3.10\""}, + {version = ">=1.23.3", markers = "python_version >= \"3.11\""}, + {version = ">1.20"}, + {version = ">=1.21.2", markers = "python_version >= \"3.10\""}, {version = ">=1.26.0", markers = "python_version >= \"3.12\""}, - {version = ">=1.23.3", markers = "python_version >= \"3.11\" and python_version < \"3.12\""}, - {version = ">=1.21.2", markers = "python_version >= \"3.10\" and python_version < \"3.11\""}, ] [package.extras] @@ -2657,6 +2824,8 @@ version = "3.2.0rc0" description = "MLflow is an open source platform for the complete machine learning lifecycle" optional = true python-versions = ">=3.10" +groups = ["main"] +markers = "python_version >= \"3.10\" and extra == \"mlflow\"" files = [ {file = "mlflow-3.2.0rc0-py3-none-any.whl", hash = "sha256:c6b8bf6cf03ff292885cbb01f0c30cb8e80a8f21af96eb9ba0030ed8a8b7ee59"}, {file = "mlflow-3.2.0rc0.tar.gz", hash = "sha256:5e6b3499a8e1b331c7806c64c5e1e44cdb8640e4d438be450cf8f82baca48c42"}, @@ -2698,6 +2867,8 @@ version = "3.2.0rc0" description = "MLflow is an open source platform for the complete machine learning lifecycle" optional = true python-versions = ">=3.10" +groups = ["main"] +markers = "python_version >= \"3.10\" and extra == \"mlflow\"" files = [ {file = "mlflow_skinny-3.2.0rc0-py3-none-any.whl", hash = "sha256:eb832fbc5232c9917f61c4d51ba6f6294b475e48e7dc747b98d0f55b701cc198"}, {file = "mlflow_skinny-3.2.0rc0.tar.gz", hash = "sha256:1321228d91a01a4fbdfb17083f51b7d475934da4df7fe3637523b6d7949a81e5"}, @@ -2741,6 +2912,8 @@ version = "3.2.0rc0" description = "MLflow Tracing SDK is an open-source, lightweight Python package that only includes the minimum set of dependencies and functionality to instrument your code/models/agents with MLflow Tracing." optional = true python-versions = ">=3.10" +groups = ["main"] +markers = "python_version >= \"3.10\" and extra == \"mlflow\"" files = [ {file = "mlflow_tracing-3.2.0rc0-py3-none-any.whl", hash = "sha256:4bb5e24e1bee4d16037c985710bf2b5b0589e6e77aef36ca7f96279f4cb6ed46"}, {file = "mlflow_tracing-3.2.0rc0.tar.gz", hash = "sha256:30b487ef6443bffa2f24a0c53e81283d05347c587742efa89c499bbbe049d2d4"}, @@ -2761,6 +2934,7 @@ version = "1.33.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" +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"}, @@ -2772,7 +2946,7 @@ PyJWT = {version = ">=1.0.0,<3", extras = ["crypto"]} requests = ">=2.0.0,<3" [package.extras] -broker = ["pymsalruntime (>=0.14,<0.19)", "pymsalruntime (>=0.17,<0.19)", "pymsalruntime (>=0.18,<0.19)"] +broker = ["pymsalruntime (>=0.14,<0.19) ; python_version >= \"3.6\" and platform_system == \"Windows\"", "pymsalruntime (>=0.17,<0.19) ; python_version >= \"3.8\" and platform_system == \"Darwin\"", "pymsalruntime (>=0.18,<0.19) ; python_version >= \"3.8\" and platform_system == \"Linux\""] [[package]] name = "msal-extensions" @@ -2780,6 +2954,7 @@ version = "1.3.0" 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.7" +groups = ["main", "proxy-dev"] 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"}, @@ -2797,6 +2972,7 @@ version = "6.1.0" description = "multidict implementation" optional = false python-versions = ">=3.8" +groups = ["main"] 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"}, @@ -2901,6 +3077,7 @@ version = "1.14.1" description = "Optional static typing for Python" optional = false python-versions = ">=3.8" +groups = ["dev"] 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"}, @@ -2960,6 +3137,7 @@ version = "1.1.0" description = "Type system extensions for programs checked with the mypy type checker." optional = false python-versions = ">=3.8" +groups = ["main", "dev"] 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"}, @@ -2971,6 +3149,7 @@ version = "1.9.1" description = "Node.js virtual environment builder" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +groups = ["main", "proxy-dev"] files = [ {file = "nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9"}, {file = "nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f"}, @@ -2982,6 +3161,8 @@ version = "1.26.4" description = "Fundamental package for array computing in Python" optional = true python-versions = ">=3.9" +groups = ["main"] +markers = "python_version >= \"3.9\" and (python_version >= \"3.10\" or extra == \"extra-proxy\" or extra == \"semantic-router\") and (python_version < \"3.14\" or extra == \"semantic-router\" or extra == \"mlflow\") and (extra == \"extra-proxy\" or extra == \"semantic-router\" or extra == \"mlflow\")" files = [ {file = "numpy-1.26.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9ff0f4f29c51e2803569d7a51c2304de5554655a60c5d776e35b4a41413830d0"}, {file = "numpy-1.26.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2e4ee3380d6de9c9ec04745830fd9e2eccb3e6cf790d39d7b98ffd19b0dd754a"}, @@ -3027,6 +3208,8 @@ version = "1.7.0" description = "Sphinx extension to support docstrings in Numpy format" optional = true python-versions = ">=3.8" +groups = ["main"] +markers = "extra == \"utils\"" files = [ {file = "numpydoc-1.7.0-py3-none-any.whl", hash = "sha256:5a56419d931310d79a06cfc2a126d1558700feeb9b4f3d8dcae1a8134be829c9"}, {file = "numpydoc-1.7.0.tar.gz", hash = "sha256:866e5ae5b6509dcf873fc6381120f5c31acf13b135636c1a81d68c166a95f921"}, @@ -3038,7 +3221,7 @@ tabulate = ">=0.8.10" tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} [package.extras] -developer = ["pre-commit (>=3.3)", "tomli"] +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"] @@ -3048,6 +3231,8 @@ version = "3.3.1" description = "A generic, spec-compliant, thorough implementation of the OAuth request-signing logic" optional = true python-versions = ">=3.8" +groups = ["main"] +markers = "extra == \"proxy\"" files = [ {file = "oauthlib-3.3.1-py3-none-any.whl", hash = "sha256:88119c938d2b8fb88561af5f6ee0eec8cc8d552b7bb1f712743136eb7523b7a1"}, {file = "oauthlib-3.3.1.tar.gz", hash = "sha256:0f0f8aa759826a193cf66c12ea1af1637f87b9b4622d46e866952bb022e538c9"}, @@ -3064,6 +3249,7 @@ version = "1.98.0" description = "The official Python library for the openai API" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "openai-1.98.0-py3-none-any.whl", hash = "sha256:b99b794ef92196829120e2df37647722104772d2a74d08305df9ced5f26eae34"}, {file = "openai-1.98.0.tar.gz", hash = "sha256:3ee0fcc50ae95267fd22bd1ad095ba5402098f3df2162592e68109999f685427"}, @@ -3091,10 +3277,12 @@ version = "1.25.0" description = "OpenTelemetry Python API" optional = false python-versions = ">=3.8" +groups = ["main", "dev", "proxy-dev"] files = [ {file = "opentelemetry_api-1.25.0-py3-none-any.whl", hash = "sha256:757fa1aa020a0f8fa139f8959e53dec2051cc26b832e76fa839a6d76ecefd737"}, {file = "opentelemetry_api-1.25.0.tar.gz", hash = "sha256:77c4985f62f2614e42ce77ee4c9da5fa5f0bc1e1821085e9a47533a9323ae869"}, ] +markers = {main = "python_version >= \"3.10\""} [package.dependencies] deprecated = ">=1.2.6" @@ -3106,6 +3294,7 @@ version = "1.25.0" description = "OpenTelemetry Collector Exporters" optional = false python-versions = ">=3.8" +groups = ["dev", "proxy-dev"] files = [ {file = "opentelemetry_exporter_otlp-1.25.0-py3-none-any.whl", hash = "sha256:d67a831757014a3bc3174e4cd629ae1493b7ba8d189e8a007003cacb9f1a6b60"}, {file = "opentelemetry_exporter_otlp-1.25.0.tar.gz", hash = "sha256:ce03199c1680a845f82e12c0a6a8f61036048c07ec7a0bd943142aca8fa6ced0"}, @@ -3121,6 +3310,7 @@ version = "1.25.0" description = "OpenTelemetry Protobuf encoding" optional = false python-versions = ">=3.8" +groups = ["dev", "proxy-dev"] files = [ {file = "opentelemetry_exporter_otlp_proto_common-1.25.0-py3-none-any.whl", hash = "sha256:15637b7d580c2675f70246563363775b4e6de947871e01d0f4e3881d1848d693"}, {file = "opentelemetry_exporter_otlp_proto_common-1.25.0.tar.gz", hash = "sha256:c93f4e30da4eee02bacd1e004eb82ce4da143a2f8e15b987a9f603e0a85407d3"}, @@ -3135,6 +3325,7 @@ version = "1.25.0" description = "OpenTelemetry Collector Protobuf over gRPC Exporter" optional = false python-versions = ">=3.8" +groups = ["dev", "proxy-dev"] files = [ {file = "opentelemetry_exporter_otlp_proto_grpc-1.25.0-py3-none-any.whl", hash = "sha256:3131028f0c0a155a64c430ca600fd658e8e37043cb13209f0109db5c1a3e4eb4"}, {file = "opentelemetry_exporter_otlp_proto_grpc-1.25.0.tar.gz", hash = "sha256:c0b1661415acec5af87625587efa1ccab68b873745ca0ee96b69bb1042087eac"}, @@ -3155,6 +3346,7 @@ version = "1.25.0" description = "OpenTelemetry Collector Protobuf over HTTP Exporter" optional = false python-versions = ">=3.8" +groups = ["dev", "proxy-dev"] files = [ {file = "opentelemetry_exporter_otlp_proto_http-1.25.0-py3-none-any.whl", hash = "sha256:2eca686ee11b27acd28198b3ea5e5863a53d1266b91cda47c839d95d5e0541a6"}, {file = "opentelemetry_exporter_otlp_proto_http-1.25.0.tar.gz", hash = "sha256:9f8723859e37c75183ea7afa73a3542f01d0fd274a5b97487ea24cb683d7d684"}, @@ -3175,6 +3367,7 @@ version = "1.25.0" description = "OpenTelemetry Python Proto" optional = false python-versions = ">=3.8" +groups = ["dev", "proxy-dev"] files = [ {file = "opentelemetry_proto-1.25.0-py3-none-any.whl", hash = "sha256:f07e3341c78d835d9b86665903b199893befa5e98866f63d22b00d0b7ca4972f"}, {file = "opentelemetry_proto-1.25.0.tar.gz", hash = "sha256:35b6ef9dc4a9f7853ecc5006738ad40443701e52c26099e197895cbda8b815a3"}, @@ -3189,10 +3382,12 @@ version = "1.25.0" description = "OpenTelemetry Python SDK" optional = false python-versions = ">=3.8" +groups = ["main", "dev", "proxy-dev"] files = [ {file = "opentelemetry_sdk-1.25.0-py3-none-any.whl", hash = "sha256:d97ff7ec4b351692e9d5a15af570c693b8715ad78b8aafbec5c7100fe966b4c9"}, {file = "opentelemetry_sdk-1.25.0.tar.gz", hash = "sha256:ce7fc319c57707ef5bf8b74fb9f8ebdb8bfafbe11898410e0d2a761d08a98ec7"}, ] +markers = {main = "python_version >= \"3.10\""} [package.dependencies] opentelemetry-api = "1.25.0" @@ -3205,10 +3400,12 @@ version = "0.46b0" description = "OpenTelemetry Semantic Conventions" optional = false python-versions = ">=3.8" +groups = ["main", "dev", "proxy-dev"] files = [ {file = "opentelemetry_semantic_conventions-0.46b0-py3-none-any.whl", hash = "sha256:6daef4ef9fa51d51855d9f8e0ccd3a1bd59e0e545abe99ac6203804e36ab3e07"}, {file = "opentelemetry_semantic_conventions-0.46b0.tar.gz", hash = "sha256:fbc982ecbb6a6e90869b15c1673be90bd18c8a56ff1cffc0864e38e2edffaefa"}, ] +markers = {main = "python_version >= \"3.10\""} [package.dependencies] opentelemetry-api = "1.25.0" @@ -3219,6 +3416,8 @@ version = "3.10.15" description = "Fast, correct Python JSON library supporting dataclasses, datetimes, and numpy" optional = true python-versions = ">=3.8" +groups = ["main"] +markers = "extra == \"proxy\"" 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"}, @@ -3307,6 +3506,7 @@ version = "24.2" description = "Core utilities for Python packages" optional = false python-versions = ">=3.8" +groups = ["main", "dev"] files = [ {file = "packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759"}, {file = "packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"}, @@ -3318,6 +3518,8 @@ version = "2.3.1" description = "Powerful data structures for data analysis, time series, and statistics" optional = true python-versions = ">=3.9" +groups = ["main"] +markers = "python_version >= \"3.10\" and extra == \"mlflow\"" files = [ {file = "pandas-2.3.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:22c2e866f7209ebc3a8f08d75766566aae02bcc91d196935a1d9e59c7b990ac9"}, {file = "pandas-2.3.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:3583d348546201aff730c8c47e49bc159833f971c2899d6097bce68b9112a4f1"}, @@ -3365,9 +3567,9 @@ files = [ [package.dependencies] numpy = [ + {version = ">=1.23.2", markers = "python_version == \"3.11\""}, {version = ">=1.22.4", markers = "python_version < \"3.11\""}, {version = ">=1.26.0", markers = "python_version >= \"3.12\""}, - {version = ">=1.23.2", markers = "python_version == \"3.11\""}, ] python-dateutil = ">=2.8.2" pytz = ">=2020.1" @@ -3404,6 +3606,7 @@ version = "0.12.1" description = "Utility library for gitignore style pattern matching of file paths." optional = false python-versions = ">=3.8" +groups = ["main", "dev"] files = [ {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"}, {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, @@ -3415,6 +3618,8 @@ version = "11.3.0" description = "Python Imaging Library (Fork)" optional = true python-versions = ">=3.9" +groups = ["main"] +markers = "python_version >= \"3.10\" and extra == \"mlflow\"" files = [ {file = "pillow-11.3.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:1b9c17fd4ace828b3003dfd1e30bff24863e0eb59b535e8f80194d9cc7ecf860"}, {file = "pillow-11.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:65dc69160114cdd0ca0f35cb434633c75e8e7fad4cf855177a05bf38678f73ad"}, @@ -3530,7 +3735,7 @@ fpx = ["olefile"] mic = ["olefile"] test-arrow = ["pyarrow"] tests = ["check-manifest", "coverage (>=7.4.2)", "defusedxml", "markdown2", "olefile", "packaging", "pyroma", "pytest", "pytest-cov", "pytest-timeout", "pytest-xdist", "trove-classifiers (>=2024.10.12)"] -typing = ["typing-extensions"] +typing = ["typing-extensions ; python_version < \"3.10\""] xmp = ["defusedxml"] [[package]] @@ -3539,6 +3744,8 @@ version = "1.3.10" description = "Resolve a name to an object." optional = false python-versions = ">=3.6" +groups = ["main"] +markers = "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"}, @@ -3550,6 +3757,7 @@ 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"] files = [ {file = "platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb"}, {file = "platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907"}, @@ -3566,6 +3774,7 @@ version = "1.5.0" description = "plugin and hook calling mechanisms for python" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, @@ -3581,6 +3790,8 @@ version = "1.32.0" description = "Blazingly fast DataFrame library" optional = true python-versions = ">=3.9" +groups = ["main"] +markers = "python_version >= \"3.10\" and extra == \"proxy\"" files = [ {file = "polars-1.32.0-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:94f7c6a3b30bc99bc6b682ea42bb1ae983e33a302ca21aacbac50ae19e34fcf2"}, {file = "polars-1.32.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:8bf14c16164839e62c741a863942a94a9a463db21e797452fca996c8afaf8827"}, @@ -3614,7 +3825,7 @@ pyarrow = ["pyarrow (>=7.0.0)"] pydantic = ["pydantic"] sqlalchemy = ["polars[pandas]", "sqlalchemy"] style = ["great-tables (>=0.8.0)"] -timezone = ["tzdata"] +timezone = ["tzdata ; platform_system == \"Windows\""] xlsx2csv = ["xlsx2csv (>=0.8.0)"] xlsxwriter = ["xlsxwriter"] @@ -3624,6 +3835,7 @@ version = "2.0.0" description = "A pure-Python implementation of the HTTP/2 priority tree" optional = false python-versions = ">=3.6.1" +groups = ["proxy-dev"] files = [ {file = "priority-2.0.0-py3-none-any.whl", hash = "sha256:6f8eefce5f3ad59baf2c080a664037bb4725cd0a790d53d59ab4059288faf6aa"}, {file = "priority-2.0.0.tar.gz", hash = "sha256:c965d54f1b8d0d0b19479db3924c7c36cf672dbf2aec92d43fbdaf4492ba18c0"}, @@ -3635,6 +3847,7 @@ version = "0.11.0" description = "Prisma Client Python is an auto-generated and fully type-safe database client" optional = false python-versions = ">=3.7.0" +groups = ["main", "proxy-dev"] files = [ {file = "prisma-0.11.0-py3-none-any.whl", hash = "sha256:22bb869e59a2968b99f3483bb417717273ffbc569fd1e9ceed95e5614cbaf53a"}, {file = "prisma-0.11.0.tar.gz", hash = "sha256:3f2f2fd2361e1ec5ff655f2a04c7860c2f2a5bc4c91f78ca9c5c6349735bf693"}, @@ -3660,6 +3873,7 @@ version = "0.20.0" description = "Python client for the Prometheus monitoring system." optional = false python-versions = ">=3.8" +groups = ["proxy-dev"] files = [ {file = "prometheus_client-0.20.0-py3-none-any.whl", hash = "sha256:cde524a85bce83ca359cc837f28b8c0db5cac7aa653a588fd7e84ba061c329e7"}, {file = "prometheus_client-0.20.0.tar.gz", hash = "sha256:287629d00b147a32dcb2be0b9df905da599b2d82f80377083ec8463309a4bb89"}, @@ -3674,6 +3888,7 @@ version = "0.2.0" description = "Accelerated property cache" optional = false python-versions = ">=3.8" +groups = ["main"] 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"}, @@ -3781,6 +3996,8 @@ version = "1.26.1" description = "Beautiful, Pythonic protocol buffers" optional = true python-versions = ">=3.7" +groups = ["main"] +markers = "extra == \"extra-proxy\"" files = [ {file = "proto_plus-1.26.1-py3-none-any.whl", hash = "sha256:13285478c2dcf2abb829db158e1047e2f1e8d63a077d94263c2b88b043c75a66"}, {file = "proto_plus-1.26.1.tar.gz", hash = "sha256:21a515a4c4c0088a773899e23c7bbade3d18f9c66c73edd4c7ee3816bc96a012"}, @@ -3798,6 +4015,7 @@ version = "4.25.8" description = "" optional = false python-versions = ">=3.8" +groups = ["main", "dev", "proxy-dev"] files = [ {file = "protobuf-4.25.8-cp310-abi3-win32.whl", hash = "sha256:504435d831565f7cfac9f0714440028907f1975e4bed228e58e72ecfff58a1e0"}, {file = "protobuf-4.25.8-cp310-abi3-win_amd64.whl", hash = "sha256:bd551eb1fe1d7e92c1af1d75bdfa572eff1ab0e5bf1736716814cdccdb2360f9"}, @@ -3811,6 +4029,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\""} [[package]] name = "pyarrow" @@ -3818,6 +4037,8 @@ version = "21.0.0" description = "Python library for Apache Arrow" optional = true python-versions = ">=3.9" +groups = ["main"] +markers = "python_version >= \"3.10\" and extra == \"mlflow\"" files = [ {file = "pyarrow-21.0.0-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:e563271e2c5ff4d4a4cbeb2c83d5cf0d4938b891518e676025f7268c6fe5fe26"}, {file = "pyarrow-21.0.0-cp310-cp310-macosx_12_0_x86_64.whl", hash = "sha256:fee33b0ca46f4c85443d6c450357101e47d53e6c3f008d658c27a2d020d44c79"}, @@ -3873,6 +4094,8 @@ version = "0.6.1" description = "Pure-Python implementation of ASN.1 types and DER/BER/CER codecs (X.208)" optional = true python-versions = ">=3.8" +groups = ["main"] +markers = "python_version >= \"3.10\" and (extra == \"mlflow\" or extra == \"extra-proxy\") 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"}, @@ -3884,6 +4107,8 @@ version = "0.4.2" 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\"" 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"}, @@ -3898,6 +4123,7 @@ version = "2.11.1" description = "Python style guide checker" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "pycodestyle-2.11.1-py2.py3-none-any.whl", hash = "sha256:44fe31000b2d866f2e41841b18528a505fbd7fef9017b04eff4e2648a0fadc67"}, {file = "pycodestyle-2.11.1.tar.gz", hash = "sha256:41ba0e7afc9752dfb53ced5489e89f8186be00e599e712660695b7a75ff2663f"}, @@ -3909,10 +4135,12 @@ version = "2.22" description = "C parser in Python" optional = false python-versions = ">=3.8" +groups = ["main", "dev", "proxy-dev"] files = [ {file = "pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc"}, {file = "pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6"}, ] +markers = {main = "platform_python_implementation != \"PyPy\" or extra == \"proxy\"", dev = "platform_python_implementation != \"PyPy\"", proxy-dev = "platform_python_implementation != \"PyPy\""} [[package]] name = "pydantic" @@ -3920,6 +4148,7 @@ version = "2.10.6" description = "Data validation using Python type hints" optional = false python-versions = ">=3.8" +groups = ["main", "dev", "proxy-dev"] files = [ {file = "pydantic-2.10.6-py3-none-any.whl", hash = "sha256:427d664bf0b8a2b34ff5dd0f5a18df00591adcee7198fbd71981054cef37b584"}, {file = "pydantic-2.10.6.tar.gz", hash = "sha256:ca5daa827cce33de7a42be142548b0096bf05a7e7b365aebfa5f8eeec7128236"}, @@ -3933,7 +4162,7 @@ typing-extensions = ">=4.12.2" [package.extras] email = ["email-validator (>=2.0.0)"] -timezone = ["tzdata"] +timezone = ["tzdata ; python_version >= \"3.9\" and platform_system == \"Windows\""] [[package]] name = "pydantic-core" @@ -3941,6 +4170,7 @@ version = "2.27.2" description = "Core functionality for Pydantic validation and serialization" optional = false python-versions = ">=3.8" +groups = ["main", "dev", "proxy-dev"] 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"}, @@ -4053,6 +4283,8 @@ version = "2.10.1" 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"}, @@ -4076,6 +4308,7 @@ version = "3.1.0" description = "passive checker of Python programs" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "pyflakes-3.1.0-py2.py3-none-any.whl", hash = "sha256:4132f6d49cb4dae6819e5379898f2b8cce3c5f23994194c24b77d5da2e36f774"}, {file = "pyflakes-3.1.0.tar.gz", hash = "sha256:a0aae034c444db0071aa077972ba4768d40c830d9539fd45bf4cd3f8f6992efc"}, @@ -4087,6 +4320,8 @@ version = "2.19.2" description = "Pygments is a syntax highlighting package written in Python." optional = true python-versions = ">=3.8" +groups = ["main"] +markers = "extra == \"utils\" or extra == \"proxy\"" files = [ {file = "pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b"}, {file = "pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887"}, @@ -4101,6 +4336,7 @@ version = "2.9.0" description = "JSON Web Token implementation in Python" optional = false python-versions = ">=3.8" +groups = ["main", "proxy-dev"] files = [ {file = "PyJWT-2.9.0-py3-none-any.whl", hash = "sha256:3b02fb0f44517787776cf48f2ae25d8e14f300e6d7545a4315cee571a415e850"}, {file = "pyjwt-2.9.0.tar.gz", hash = "sha256:7e1e5b56cc735432a7369cbfa0efe50fa113ebecdc04ae6922deba8b84582d0c"}, @@ -4121,6 +4357,8 @@ version = "1.5.0" description = "Python binding to the Networking and Cryptography (NaCl) library" optional = true python-versions = ">=3.6" +groups = ["main"] +markers = "extra == \"proxy\"" files = [ {file = "PyNaCl-1.5.0-cp36-abi3-macosx_10_10_universal2.whl", hash = "sha256:401002a4aaa07c9414132aaed7f6836ff98f59277a234704ff66878c2ee4a0d1"}, {file = "PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:52cb72a79269189d4e0dc537556f4740f7f0a9ec41c1322598799b0bdad4ef92"}, @@ -4147,6 +4385,8 @@ version = "3.2.3" description = "pyparsing module - 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.3-py3-none-any.whl", hash = "sha256:a749938e02d6fd0b59b356ca504a24982314bb090c383e3cf201c95ef7e2bfcf"}, {file = "pyparsing-3.2.3.tar.gz", hash = "sha256:b9c13f1ab8b3b542f72e28f634bad4de758ab3ce4546e4301970ad6fa77c38be"}, @@ -4161,6 +4401,8 @@ version = "3.5.4" description = "A python implementation of GNU readline." optional = true python-versions = ">=3.8" +groups = ["main"] +markers = "python_version >= \"3.9\" and sys_platform == \"win32\" and extra == \"extra-proxy\" and python_version < \"3.14\"" files = [ {file = "pyreadline3-3.5.4-py3-none-any.whl", hash = "sha256:eaf8e6cc3c49bcccf145fc6067ba8643d1df34d604a1ec0eccbf7a18e6d3fae6"}, {file = "pyreadline3-3.5.4.tar.gz", hash = "sha256:8d57d53039a1c75adba8e50dd3d992b28143480816187ea5efbd5c78e6c885b7"}, @@ -4175,6 +4417,7 @@ version = "7.4.4" description = "pytest: simple powerful testing with Python" optional = false python-versions = ">=3.7" +groups = ["dev"] files = [ {file = "pytest-7.4.4-py3-none-any.whl", hash = "sha256:b090cdf5ed60bf4c45261be03239c2c1c22df034fbffe691abe93cd80cea01d8"}, {file = "pytest-7.4.4.tar.gz", hash = "sha256:2cf0005922c6ace4a3e2ec8b4080eb0d9753fdc93107415332f50ce9e7994280"}, @@ -4197,6 +4440,7 @@ version = "0.21.2" description = "Pytest support for asyncio" optional = false python-versions = ">=3.7" +groups = ["dev"] files = [ {file = "pytest_asyncio-0.21.2-py3-none-any.whl", hash = "sha256:ab664c88bb7998f711d8039cacd4884da6430886ae8bbd4eded552ed2004f16b"}, {file = "pytest_asyncio-0.21.2.tar.gz", hash = "sha256:d67738fc232b94b326b9d060750beb16e0074210b98dd8b58a5239fa2a154f45"}, @@ -4215,6 +4459,7 @@ version = "3.14.1" description = "Thin-wrapper around the mock package for easier use with pytest" optional = false python-versions = ">=3.8" +groups = ["dev"] 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"}, @@ -4232,6 +4477,8 @@ version = "2.9.0.post0" 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\"" 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"}, @@ -4246,6 +4493,7 @@ version = "1.0.1" description = "Read key-value pairs from a .env file and set them as environment variables" optional = false python-versions = ">=3.8" +groups = ["main", "proxy-dev"] 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"}, @@ -4260,6 +4508,8 @@ version = "0.0.18" description = "A streaming multipart parser for Python" optional = true python-versions = ">=3.8" +groups = ["main"] +markers = "extra == \"proxy\"" files = [ {file = "python_multipart-0.0.18-py3-none-any.whl", hash = "sha256:efe91480f485f6a361427a541db4796f9e1591afc0fb8e7a4ba06bfbc6708996"}, {file = "python_multipart-0.0.18.tar.gz", hash = "sha256:7a68db60c8bfb82e460637fa4750727b45af1d5e2ed215593f917f64694d34fe"}, @@ -4271,6 +4521,8 @@ version = "3.0.0" description = "Universally unique lexicographically sortable identifier" optional = true python-versions = ">=3.9" +groups = ["main"] +markers = "python_version >= \"3.9\" and extra == \"extra-proxy\" and python_version < \"3.14\"" files = [ {file = "python_ulid-3.0.0-py3-none-any.whl", hash = "sha256:e4c4942ff50dbd79167ad01ac725ec58f924b4018025ce22c858bfcff99a5e31"}, {file = "python_ulid-3.0.0.tar.gz", hash = "sha256:e50296a47dc8209d28629a22fc81ca26c00982c78934bd7766377ba37ea49a9f"}, @@ -4285,6 +4537,8 @@ version = "2025.2" 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\"" files = [ {file = "pytz-2025.2-py2.py3-none-any.whl", hash = "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00"}, {file = "pytz-2025.2.tar.gz", hash = "sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3"}, @@ -4296,6 +4550,8 @@ version = "311" description = "Python for Window Extensions" optional = true python-versions = "*" +groups = ["main"] +markers = "python_version >= \"3.10\" and sys_platform == \"win32\" and (extra == \"proxy\" or extra == \"mlflow\")" files = [ {file = "pywin32-311-cp310-cp310-win32.whl", hash = "sha256:d03ff496d2a0cd4a5893504789d4a15399133fe82517455e78bad62efbb7f0a3"}, {file = "pywin32-311-cp310-cp310-win_amd64.whl", hash = "sha256:797c2772017851984b97180b0bebe4b620bb86328e8a884bb626156295a63b3b"}, @@ -4325,6 +4581,7 @@ version = "6.0.2" description = "YAML parser and emitter for Python" optional = false python-versions = ">=3.8" +groups = ["main", "dev"] files = [ {file = "PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086"}, {file = "PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf"}, @@ -4387,6 +4644,8 @@ version = "5.3.1" 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\"" files = [ {file = "redis-5.3.1-py3-none-any.whl", hash = "sha256:dc1909bd24669cc31b5f67a039700b16ec30571096c5f1f0d9d2324bff31af97"}, {file = "redis-5.3.1.tar.gz", hash = "sha256:ca49577a531ea64039b5a36db3d6cd1a0c7a60c34124d46924a45b956e8cf14c"}, @@ -4406,6 +4665,8 @@ version = "0.4.1" description = "Python client library and CLI for using Redis as a vector database" optional = true python-versions = "<3.14,>=3.9" +groups = ["main"] +markers = "python_version >= \"3.9\" and extra == \"extra-proxy\" and python_version < \"3.14\"" files = [ {file = "redisvl-0.4.1-py3-none-any.whl", hash = "sha256:6db5d5bc95b1fe8032a1cdae74ce1c65bc7fe9054e5429b5d34d5a91d28bae5f"}, {file = "redisvl-0.4.1.tar.gz", hash = "sha256:fd6a36426ba94792c0efca20915c31232d4ee3cc58eb23794a62c142696401e6"}, @@ -4430,7 +4691,7 @@ bedrock = ["boto3[bedrock] (>=1.36.0,<2.0.0)"] cohere = ["cohere (>=4.44)"] mistralai = ["mistralai (>=1.0.0)"] openai = ["openai (>=1.13.0,<2.0.0)"] -sentence-transformers = ["scipy (<1.15)", "scipy (>=1.15,<2.0)", "sentence-transformers (>=3.4.0,<4.0.0)"] +sentence-transformers = ["scipy (<1.15) ; python_version < \"3.10\"", "scipy (>=1.15,<2.0) ; python_version >= \"3.10\"", "sentence-transformers (>=3.4.0,<4.0.0)"] vertexai = ["google-cloud-aiplatform (>=1.26,<2.0)", "protobuf (>=5.29.1,<6.0.0)"] voyageai = ["voyageai (>=0.2.2)"] @@ -4440,6 +4701,7 @@ version = "0.35.1" description = "JSON Referencing + Python" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "referencing-0.35.1-py3-none-any.whl", hash = "sha256:eda6d3234d62814d1c64e305c1331c9a3a6132da475ab6382eaa997b21ee75de"}, {file = "referencing-0.35.1.tar.gz", hash = "sha256:25b42124a6c8b632a425174f24087783efb348a6f1e0008e63cd4466fedf703c"}, @@ -4455,6 +4717,7 @@ version = "2024.11.6" description = "Alternative regular expression module, to replace re." optional = false python-versions = ">=3.8" +groups = ["main"] 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"}, @@ -4558,6 +4821,7 @@ version = "2.31.0" description = "Python HTTP for Humans." optional = false python-versions = ">=3.7" +groups = ["main", "dev", "proxy-dev"] files = [ {file = "requests-2.31.0-py3-none-any.whl", hash = "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f"}, {file = "requests-2.31.0.tar.gz", hash = "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1"}, @@ -4579,6 +4843,7 @@ version = "1.12.1" description = "Mock out responses from the requests package" optional = false python-versions = ">=3.5" +groups = ["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"}, @@ -4596,6 +4861,8 @@ version = "0.8.0" description = "Resend Python SDK" optional = true python-versions = ">=3.7" +groups = ["main"] +markers = "extra == \"extra-proxy\"" files = [ {file = "resend-0.8.0-py2.py3-none-any.whl", hash = "sha256:adc1515dadf4f4fc6b90db55a237f0f37fc56fd74287a986519a8a187fdb661d"}, {file = "resend-0.8.0.tar.gz", hash = "sha256:94142394701724dbcfcd8f760f675c662a1025013e741dd7cc773ca885526257"}, @@ -4610,6 +4877,7 @@ version = "0.25.7" description = "A utility library for mocking out the `requests` Python library." optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "responses-0.25.7-py3-none-any.whl", hash = "sha256:92ca17416c90fe6b35921f52179bff29332076bb32694c0df02dcac2c6bc043c"}, {file = "responses-0.25.7.tar.gz", hash = "sha256:8ebae11405d7a5df79ab6fd54277f6f2bc29b2d002d0dd2d5c632594d1ddcedb"}, @@ -4621,7 +4889,7 @@ requests = ">=2.30.0,<3.0" urllib3 = ">=1.25.10,<3.0" [package.extras] -tests = ["coverage (>=6.0.0)", "flake8", "mypy", "pytest (>=7.0.0)", "pytest-asyncio", "pytest-cov", "pytest-httpserver", "tomli", "tomli-w", "types-PyYAML", "types-requests"] +tests = ["coverage (>=6.0.0)", "flake8", "mypy", "pytest (>=7.0.0)", "pytest-asyncio", "pytest-cov", "pytest-httpserver", "tomli ; python_version < \"3.11\"", "tomli-w", "types-PyYAML", "types-requests"] [[package]] name = "respx" @@ -4629,6 +4897,7 @@ version = "0.22.0" description = "A utility for mocking out the Python HTTPX and HTTP Core libraries." optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "respx-0.22.0-py2.py3-none-any.whl", hash = "sha256:631128d4c9aba15e56903fb5f66fb1eff412ce28dd387ca3a81339e52dbd3ad0"}, {file = "respx-0.22.0.tar.gz", hash = "sha256:3c8924caa2a50bd71aefc07aa812f2466ff489f1848c96e954a5362d17095d91"}, @@ -4643,6 +4912,8 @@ version = "13.7.1" description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" optional = true python-versions = ">=3.7.0" +groups = ["main"] +markers = "extra == \"proxy\"" files = [ {file = "rich-13.7.1-py3-none-any.whl", hash = "sha256:4edbae314f59eb482f54e9e30bf00d33350aaa94f4bfcd4e9e3110e64d0d7222"}, {file = "rich-13.7.1.tar.gz", hash = "sha256:9be308cb1fe2f1f57d67ce99e95af38a1e2bc71ad9813b0e247cf7ffbcc3a432"}, @@ -4662,6 +4933,7 @@ version = "0.20.1" description = "Python bindings to Rust's persistent data structures (rpds)" optional = false python-versions = ">=3.8" +groups = ["main"] 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"}, @@ -4774,6 +5046,8 @@ version = "2.3.3" description = "RQ is a simple, lightweight, library for creating background jobs, and processing them." optional = true python-versions = ">=3.8" +groups = ["main"] +markers = "extra == \"proxy\"" files = [ {file = "rq-2.3.3-py3-none-any.whl", hash = "sha256:2202c4409c4c527ac4bee409867d6c02515dd110030499eb0de54c7374aee0ce"}, {file = "rq-2.3.3.tar.gz", hash = "sha256:20c41c977b6f27c852a41bd855893717402bae7b8d9607dca21fe9dd55453e22"}, @@ -4789,6 +5063,8 @@ version = "4.9.1" 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\"" files = [ {file = "rsa-4.9.1-py3-none-any.whl", hash = "sha256:68635866661c6836b8d39430f97a996acbd61bfa49406748ea243539fe239762"}, {file = "rsa-4.9.1.tar.gz", hash = "sha256:e7bdbfdb5497da4c07dfd35530e1a902659db6ff241e39d9953cad06ebd0ae75"}, @@ -4803,6 +5079,7 @@ version = "0.1.15" description = "An extremely fast Python linter and code formatter, written in Rust." optional = false python-versions = ">=3.7" +groups = ["dev"] files = [ {file = "ruff-0.1.15-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:5fe8d54df166ecc24106db7dd6a68d44852d14eb0729ea4672bb4d96c320b7df"}, {file = "ruff-0.1.15-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:6f0bfbb53c4b4de117ac4d6ddfd33aa5fc31beeaa21d23c45c6dd249faf9126f"}, @@ -4829,6 +5106,8 @@ version = "0.10.4" description = "An Amazon S3 Transfer Manager" optional = true python-versions = ">=3.8" +groups = ["main"] +markers = "extra == \"proxy\"" files = [ {file = "s3transfer-0.10.4-py3-none-any.whl", hash = "sha256:244a76a24355363a68164241438de1b72f8781664920260c48465896b712a41e"}, {file = "s3transfer-0.10.4.tar.gz", hash = "sha256:29edc09801743c21eb5ecbc617a152df41d3c287f67b615f73e5f750583666a7"}, @@ -4846,6 +5125,8 @@ version = "1.7.1" description = "A set of python modules for machine learning and data mining" optional = true python-versions = ">=3.10" +groups = ["main"] +markers = "python_version >= \"3.10\" and extra == \"mlflow\"" files = [ {file = "scikit_learn-1.7.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:406204dd4004f0517f0b23cf4b28c6245cbd51ab1b6b78153bc784def214946d"}, {file = "scikit_learn-1.7.1-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:16af2e44164f05d04337fd1fc3ae7c4ea61fd9b0d527e22665346336920fe0e1"}, @@ -4896,6 +5177,8 @@ version = "1.15.3" description = "Fundamental algorithms for scientific computing in Python" optional = true python-versions = ">=3.10" +groups = ["main"] +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"}, @@ -4951,7 +5234,7 @@ numpy = ">=1.23.5,<2.5" [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", "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", "pooch", "pytest", "pytest-cov", "pytest-timeout", "pytest-xdist", "scikit-umfpack", "threadpoolctl"] +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 = "semantic-router" @@ -4959,6 +5242,8 @@ version = "0.0.20" 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\"" 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"}, @@ -4974,7 +5259,7 @@ pydantic = ">=2.5.3,<3.0.0" pyyaml = ">=6.0.1,<7.0.0" [package.extras] -fastembed = ["fastembed (>=0.1.3,<0.2.0)"] +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)"] @@ -4984,6 +5269,7 @@ version = "1.17.0" description = "Python 2 and 3 compatibility utilities" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +groups = ["main", "proxy-dev"] files = [ {file = "six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274"}, {file = "six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"}, @@ -4995,6 +5281,8 @@ version = "5.0.2" description = "A pure Python implementation of a sliding window memory map manager" optional = true python-versions = ">=3.7" +groups = ["main"] +markers = "python_version >= \"3.10\" and extra == \"mlflow\"" files = [ {file = "smmap-5.0.2-py3-none-any.whl", hash = "sha256:b30115f0def7d7531d22a0fb6502488d879e75b260a9db4d0819cfb25403af5e"}, {file = "smmap-5.0.2.tar.gz", hash = "sha256:26ea65a03958fa0c8a1c7e8c7a58fdc77221b8910f6be2131affade476898ad5"}, @@ -5006,6 +5294,7 @@ version = "1.3.1" description = "Sniff out which async library your code is running under" optional = false python-versions = ">=3.7" +groups = ["main", "dev", "proxy-dev"] files = [ {file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"}, {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, @@ -5017,6 +5306,8 @@ version = "3.0.1" description = "This package provides 32 stemmers for 30 languages generated from Snowball algorithms." optional = true python-versions = "!=3.0.*,!=3.1.*,!=3.2.*" +groups = ["main"] +markers = "extra == \"utils\"" files = [ {file = "snowballstemmer-3.0.1-py3-none-any.whl", hash = "sha256:6cd7b3897da8d6c9ffb968a6781fa6532dce9c3618a4b127d920dab764a19064"}, {file = "snowballstemmer-3.0.1.tar.gz", hash = "sha256:6d5eeeec8e9f84d4d56b847692bacf79bc2c8e90c7f80ca4444ff8b6f2e52895"}, @@ -5028,6 +5319,8 @@ version = "7.1.2" description = "Python documentation generator" optional = true python-versions = ">=3.8" +groups = ["main"] +markers = "extra == \"utils\"" files = [ {file = "sphinx-7.1.2-py3-none-any.whl", hash = "sha256:d170a81825b2fcacb6dfd5a0d7f578a053e45d3f2b153fecc948c37344eb4cbe"}, {file = "sphinx-7.1.2.tar.gz", hash = "sha256:780f4d32f1d7d1126576e0e5ecc19dc32ab76cd24e950228dcf7b1f6d3d9e22f"}, @@ -5063,6 +5356,8 @@ version = "1.0.4" description = "sphinxcontrib-applehelp is a Sphinx extension which outputs Apple help books" optional = true python-versions = ">=3.8" +groups = ["main"] +markers = "extra == \"utils\"" 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"}, @@ -5078,6 +5373,8 @@ version = "1.0.2" description = "sphinxcontrib-devhelp is a sphinx extension which outputs Devhelp document." optional = true python-versions = ">=3.5" +groups = ["main"] +markers = "extra == \"utils\"" 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"}, @@ -5093,6 +5390,8 @@ version = "2.0.1" description = "sphinxcontrib-htmlhelp is a sphinx extension which renders HTML help files" optional = true python-versions = ">=3.8" +groups = ["main"] +markers = "extra == \"utils\"" 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"}, @@ -5108,6 +5407,8 @@ version = "1.0.1" description = "A sphinx extension which renders display math in HTML via JavaScript" optional = true python-versions = ">=3.5" +groups = ["main"] +markers = "extra == \"utils\"" files = [ {file = "sphinxcontrib-jsmath-1.0.1.tar.gz", hash = "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8"}, {file = "sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl", hash = "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178"}, @@ -5122,6 +5423,8 @@ version = "1.0.3" description = "sphinxcontrib-qthelp is a sphinx extension which outputs QtHelp document." optional = true python-versions = ">=3.5" +groups = ["main"] +markers = "extra == \"utils\"" 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"}, @@ -5137,6 +5440,8 @@ version = "1.1.5" description = "sphinxcontrib-serializinghtml is a sphinx extension which outputs \"serialized\" HTML files (json and pickle)." optional = true python-versions = ">=3.5" +groups = ["main"] +markers = "extra == \"utils\"" 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"}, @@ -5152,6 +5457,8 @@ version = "2.0.42" description = "Database Abstraction Library" optional = true python-versions = ">=3.7" +groups = ["main"] +markers = "python_version >= \"3.10\" and extra == \"mlflow\"" files = [ {file = "SQLAlchemy-2.0.42-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:7ee065898359fdee83961aed5cf1fb4cfa913ba71b58b41e036001d90bebbf7a"}, {file = "SQLAlchemy-2.0.42-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:56bc76d86216443daa2e27e6b04a9b96423f0b69b5d0c40c7f4b9a4cdf7d8d90"}, @@ -5247,6 +5554,8 @@ version = "0.5.3" description = "A non-validating SQL parser." optional = true python-versions = ">=3.8" +groups = ["main"] +markers = "python_version >= \"3.10\" and extra == \"mlflow\"" files = [ {file = "sqlparse-0.5.3-py3-none-any.whl", hash = "sha256:cf2196ed3418f3ba5de6af7e82c694a9fbdbfecccdfc72e281548517081f16ca"}, {file = "sqlparse-0.5.3.tar.gz", hash = "sha256:09f67787f56a0b16ecdbde1bfc7f5d9c3371ca683cfeaa8e6ff60b4807ec9272"}, @@ -5262,6 +5571,8 @@ version = "2.1.3" description = "SSE plugin for Starlette" optional = true python-versions = ">=3.8" +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"}, @@ -5281,6 +5592,8 @@ version = "0.44.0" description = "The little ASGI library that shines." optional = true python-versions = ">=3.8" +groups = ["main"] +markers = "python_version >= \"3.10\" and (extra == \"mlflow\" or extra == \"proxy\") or extra == \"proxy\"" files = [ {file = "starlette-0.44.0-py3-none-any.whl", hash = "sha256:19edeb75844c16dcd4f9dd72f22f9108c1539f3fc9c4c88885654fef64f85aea"}, {file = "starlette-0.44.0.tar.gz", hash = "sha256:e35166950a3ccccc701962fe0711db0bc14f2ecd37c6f9fe5e3eae0cbaea8715"}, @@ -5288,7 +5601,6 @@ files = [ [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"] @@ -5299,6 +5611,8 @@ version = "0.9.0" 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\"" files = [ {file = "tabulate-0.9.0-py3-none-any.whl", hash = "sha256:024ca478df22e9340661486f85298cff5f6dcdba14f3813e8830015b9ed1948f"}, {file = "tabulate-0.9.0.tar.gz", hash = "sha256:0095b12bf5966de529c0feb1fa08671671b3368eec77d7ef7ab114be2c068b3c"}, @@ -5313,6 +5627,8 @@ version = "0.2.2" description = "backport of asyncio.TaskGroup, asyncio.Runner and asyncio.timeout" optional = false python-versions = "*" +groups = ["proxy-dev"] +markers = "python_version <= \"3.10\"" files = [ {file = "taskgroup-0.2.2-py2.py3-none-any.whl", hash = "sha256:e2c53121609f4ae97303e9ea1524304b4de6faf9eb2c9280c7f87976479a52fb"}, {file = "taskgroup-0.2.2.tar.gz", hash = "sha256:078483ac3e78f2e3f973e2edbf6941374fbea81b9c5d0a96f51d297717f4752d"}, @@ -5328,6 +5644,8 @@ version = "9.1.2" description = "Retry code until it succeeds" optional = true python-versions = ">=3.9" +groups = ["main"] +markers = "python_version >= \"3.9\" and extra == \"extra-proxy\" and python_version < \"3.14\"" files = [ {file = "tenacity-9.1.2-py3-none-any.whl", hash = "sha256:f77bf36710d8b73a50b2dd155c97b870017ad21afe6ab300326b0371b3b05138"}, {file = "tenacity-9.1.2.tar.gz", hash = "sha256:1169d376c297e7de388d18b4481760d478b0e99a777cad3a9c86e556f4b697cb"}, @@ -5343,6 +5661,8 @@ version = "3.6.0" description = "threadpoolctl" optional = true python-versions = ">=3.9" +groups = ["main"] +markers = "python_version >= \"3.10\" and extra == \"mlflow\"" files = [ {file = "threadpoolctl-3.6.0-py3-none-any.whl", hash = "sha256:43a0b8fd5a2928500110039e43a5eed8480b918967083ea48dc3ab9f13c4a7fb"}, {file = "threadpoolctl-3.6.0.tar.gz", hash = "sha256:8ab8b4aa3491d812b623328249fab5302a68d2d71745c8a4c719a2fcaba9f44e"}, @@ -5354,6 +5674,7 @@ version = "0.7.0" description = "tiktoken is a fast BPE tokeniser for use with OpenAI's models" optional = false python-versions = ">=3.8" +groups = ["main"] 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"}, @@ -5406,6 +5727,7 @@ version = "0.21.0" description = "" optional = false python-versions = ">=3.7" +groups = ["main"] 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"}, @@ -5438,6 +5760,8 @@ version = "2.2.1" 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"}, @@ -5479,6 +5803,7 @@ version = "0.13.3" description = "Style preserving TOML library" optional = false python-versions = ">=3.8" +groups = ["main", "proxy-dev"] files = [ {file = "tomlkit-0.13.3-py3-none-any.whl", hash = "sha256:c89c649d79ee40629a9fda55f8ace8c6a1b42deb912b2a8fd8d942ddadb606b0"}, {file = "tomlkit-0.13.3.tar.gz", hash = "sha256:430cf247ee57df2b94ee3fbe588e71d362a941ebb545dec29b53961d61add2a1"}, @@ -5490,6 +5815,7 @@ version = "4.67.1" description = "Fast, Extensible Progress Meter" optional = false python-versions = ">=3.7" +groups = ["main"] files = [ {file = "tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2"}, {file = "tqdm-4.67.1.tar.gz", hash = "sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2"}, @@ -5511,6 +5837,7 @@ version = "1.16.0.20241221" description = "Typing stubs for cffi" optional = false python-versions = ">=3.8" +groups = ["dev"] 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"}, @@ -5525,6 +5852,7 @@ version = "24.1.0.20240722" description = "Typing stubs for pyOpenSSL" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "types-pyOpenSSL-24.1.0.20240722.tar.gz", hash = "sha256:47913b4678a01d879f503a12044468221ed8576263c1540dcb0484ca21b08c39"}, {file = "types_pyOpenSSL-24.1.0.20240722-py3-none-any.whl", hash = "sha256:6a7a5d2ec042537934cfb4c9d4deb0e16c4c6250b09358df1f083682fe6fda54"}, @@ -5540,6 +5868,7 @@ version = "6.0.12.20241230" description = "Typing stubs for PyYAML" optional = false python-versions = ">=3.8" +groups = ["dev"] 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"}, @@ -5551,6 +5880,7 @@ version = "4.6.0.20241004" description = "Typing stubs for redis" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "types-redis-4.6.0.20241004.tar.gz", hash = "sha256:5f17d2b3f9091ab75384153bfa276619ffa1cf6a38da60e10d5e6749cc5b902e"}, {file = "types_redis-4.6.0.20241004-py3-none-any.whl", hash = "sha256:ef5da68cb827e5f606c8f9c0b49eeee4c2669d6d97122f301d3a55dc6a63f6ed"}, @@ -5566,6 +5896,8 @@ version = "2.31.0.6" description = "Typing stubs for requests" optional = false python-versions = ">=3.7" +groups = ["dev"] +markers = "python_version < \"3.10\"" files = [ {file = "types-requests-2.31.0.6.tar.gz", hash = "sha256:cd74ce3b53c461f1228a9b783929ac73a666658f223e28ed29753771477b3bd0"}, {file = "types_requests-2.31.0.6-py3-none-any.whl", hash = "sha256:a2db9cb228a81da8348b49ad6db3f5519452dd20a9c1e1a868c83c5fe88fd1a9"}, @@ -5580,6 +5912,8 @@ version = "2.32.0.20241016" description = "Typing stubs for requests" optional = false python-versions = ">=3.8" +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"}, @@ -5594,6 +5928,7 @@ version = "75.8.0.20250110" description = "Typing stubs for setuptools" optional = false python-versions = ">=3.8" +groups = ["dev"] 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"}, @@ -5605,6 +5940,8 @@ version = "1.26.25.14" description = "Typing stubs for urllib3" optional = false python-versions = "*" +groups = ["dev"] +markers = "python_version < \"3.10\"" files = [ {file = "types-urllib3-1.26.25.14.tar.gz", hash = "sha256:229b7f577c951b8c1b92c1bc2b2fdb0b49847bd2af6d1cc2a2e3dd340f3bda8f"}, {file = "types_urllib3-1.26.25.14-py3-none-any.whl", hash = "sha256:9683bbb7fb72e32bfe9d2be6e04875fbe1b3eeec3cbb4ea231435aa7fd6b4f0e"}, @@ -5616,6 +5953,7 @@ version = "4.13.2" description = "Backported and Experimental Type Hints for Python 3.8+" optional = false python-versions = ">=3.8" +groups = ["main", "dev", "proxy-dev"] 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"}, @@ -5627,6 +5965,8 @@ version = "0.4.1" description = "Runtime typing introspection tools" optional = true python-versions = ">=3.9" +groups = ["main"] +markers = "python_version >= \"3.10\" and extra == \"proxy\"" 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"}, @@ -5641,6 +5981,8 @@ version = "2025.2" 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\"" files = [ {file = "tzdata-2025.2-py2.py3-none-any.whl", hash = "sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8"}, {file = "tzdata-2025.2.tar.gz", hash = "sha256:b60a638fcc0daffadf82fe0f57e53d06bdec2f36c4df66280ae79bce6bd6f2b9"}, @@ -5652,6 +5994,8 @@ version = "5.2" description = "tzinfo object for the local timezone" optional = true python-versions = ">=3.8" +groups = ["main"] +markers = "extra == \"proxy\"" files = [ {file = "tzlocal-5.2-py3-none-any.whl", hash = "sha256:49816ef2fe65ea8ac19d19aa7a1ae0551c834303d5014c6d5a62e4cbda8047b8"}, {file = "tzlocal-5.2.tar.gz", hash = "sha256:8d399205578f1a9342816409cc1e46a93ebd5755e39ea2d85334bea911bf0e6e"}, @@ -5670,14 +6014,16 @@ version = "1.26.20" description = "HTTP library with thread-safe connection pooling, file post, and more." optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" +groups = ["main", "dev", "proxy-dev"] +markers = "python_version < \"3.10\"" files = [ {file = "urllib3-1.26.20-py2.py3-none-any.whl", hash = "sha256:0ed14ccfbf1c30a9072c7ca157e4319b70d65f623e91e7b32fadb2853431016e"}, {file = "urllib3-1.26.20.tar.gz", hash = "sha256:40c2dc0c681e47eb8f90e7e27bf6ff7df2e677421fd46756da1161c39ca70d32"}, ] [package.extras] -brotli = ["brotli (==1.0.9)", "brotli (>=1.0.9)", "brotlicffi (>=0.8.0)", "brotlipy (>=0.6.0)"] -secure = ["certifi", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "ipaddress", "pyOpenSSL (>=0.14)", "urllib3-secure-extra"] +brotli = ["brotli (==1.0.9) ; os_name != \"nt\" and python_version < \"3\" and platform_python_implementation == \"CPython\"", "brotli (>=1.0.9) ; python_version >= \"3\" and platform_python_implementation == \"CPython\"", "brotlicffi (>=0.8.0) ; (os_name != \"nt\" or python_version >= \"3\") and platform_python_implementation != \"CPython\"", "brotlipy (>=0.6.0) ; os_name == \"nt\" and python_version < \"3\""] +secure = ["certifi", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "ipaddress ; python_version == \"2.7\"", "pyOpenSSL (>=0.14)", "urllib3-secure-extra"] socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] [[package]] @@ -5686,13 +6032,15 @@ version = "2.2.3" description = "HTTP library with thread-safe connection pooling, file post, and more." optional = false python-versions = ">=3.8" +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"}, ] [package.extras] -brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] +brotli = ["brotli (>=1.0.9) ; platform_python_implementation == \"CPython\"", "brotlicffi (>=0.8.0) ; platform_python_implementation != \"CPython\""] h2 = ["h2 (>=4,<5)"] socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] zstd = ["zstandard (>=0.18.0)"] @@ -5703,6 +6051,8 @@ version = "0.29.0" 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\"" files = [ {file = "uvicorn-0.29.0-py3-none-any.whl", hash = "sha256:2c2aac7ff4f4365c206fd773a39bf4ebd1047c238f8b8268ad996829323473de"}, {file = "uvicorn-0.29.0.tar.gz", hash = "sha256:6a69214c0b6a087462412670b3ef21224fa48cae0e452b5883e8e8bdfdd11dd0"}, @@ -5714,7 +6064,7 @@ h11 = ">=0.8" typing-extensions = {version = ">=4.0", markers = "python_version < \"3.11\""} [package.extras] -standard = ["colorama (>=0.4)", "httptools (>=0.5.0)", "python-dotenv (>=0.13)", "pyyaml (>=5.1)", "uvloop (>=0.14.0,!=0.15.0,!=0.15.1)", "watchfiles (>=0.13)", "websockets (>=10.4)"] +standard = ["colorama (>=0.4) ; sys_platform == \"win32\"", "httptools (>=0.5.0)", "python-dotenv (>=0.13)", "pyyaml (>=5.1)", "uvloop (>=0.14.0,!=0.15.0,!=0.15.1) ; sys_platform != \"win32\" and sys_platform != \"cygwin\" and platform_python_implementation != \"PyPy\"", "watchfiles (>=0.13)", "websockets (>=10.4)"] [[package]] name = "uvloop" @@ -5722,6 +6072,8 @@ version = "0.21.0" description = "Fast implementation of asyncio event loop on top of libuv" optional = true python-versions = ">=3.8.0" +groups = ["main"] +markers = "sys_platform != \"win32\" and extra == \"proxy\"" files = [ {file = "uvloop-0.21.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ec7e6b09a6fdded42403182ab6b832b71f4edaf7f37a9a0e371a01db5f0cb45f"}, {file = "uvloop-0.21.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:196274f2adb9689a289ad7d65700d37df0c0930fd8e4e743fa4834e850d7719d"}, @@ -5773,6 +6125,8 @@ version = "3.0.2" description = "Waitress WSGI server" optional = true python-versions = ">=3.9.0" +groups = ["main"] +markers = "python_version >= \"3.10\" and platform_system == \"Windows\" and extra == \"mlflow\"" files = [ {file = "waitress-3.0.2-py3-none-any.whl", hash = "sha256:c56d67fd6e87c2ee598b76abdd4e96cfad1f24cacdea5078d382b1f9d7b5ed2e"}, {file = "waitress-3.0.2.tar.gz", hash = "sha256:682aaaf2af0c44ada4abfb70ded36393f0e307f4ab9456a215ce0020baefc31f"}, @@ -5788,6 +6142,8 @@ version = "13.1" description = "An implementation of the WebSocket Protocol (RFC 6455 & 7692)" optional = true python-versions = ">=3.8" +groups = ["main"] +markers = "extra == \"proxy\"" files = [ {file = "websockets-13.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:f48c749857f8fb598fb890a75f540e3221d0976ed0bf879cf3c7eef34151acee"}, {file = "websockets-13.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c7e72ce6bda6fb9409cc1e8164dd41d7c91466fb599eb047cfda72fe758a34a7"}, @@ -5883,6 +6239,8 @@ version = "3.1.3" description = "The comprehensive WSGI web application library." optional = true python-versions = ">=3.9" +groups = ["main"] +markers = "python_version >= \"3.10\" and extra == \"mlflow\"" files = [ {file = "werkzeug-3.1.3-py3-none-any.whl", hash = "sha256:54b78bf3716d19a65be4fceccc0d1d7b89e608834989dfae50ea87564639213e"}, {file = "werkzeug-3.1.3.tar.gz", hash = "sha256:60723ce945c19328679790e3282cc758aa4a6040e4bb330f53d30fa546d44746"}, @@ -5900,6 +6258,7 @@ version = "1.17.2" description = "Module for decorators, wrappers and monkey patching." optional = false python-versions = ">=3.8" +groups = ["main", "dev", "proxy-dev"] files = [ {file = "wrapt-1.17.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:3d57c572081fed831ad2d26fd430d565b76aa277ed1d30ff4d40670b1c0dd984"}, {file = "wrapt-1.17.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b5e251054542ae57ac7f3fba5d10bfff615b6c2fb09abeb37d2f1463f841ae22"}, @@ -5981,6 +6340,7 @@ files = [ {file = "wrapt-1.17.2-py3-none-any.whl", hash = "sha256:b18f2d1533a71f069c7f82d524a52599053d4c7166e9dd374ae2136b7f40f7c8"}, {file = "wrapt-1.17.2.tar.gz", hash = "sha256:41388e9d4d1522446fe79d3213196bd9e3b301a336965b9e27ca2788ebd122f3"}, ] +markers = {main = "python_version >= \"3.10\""} [[package]] name = "wsproto" @@ -5988,6 +6348,7 @@ version = "1.2.0" description = "WebSockets state-machine based protocol implementation" optional = false python-versions = ">=3.7.0" +groups = ["proxy-dev"] files = [ {file = "wsproto-1.2.0-py3-none-any.whl", hash = "sha256:b9acddd652b585d75b20477888c56642fdade28bdfd3579aa24a4d2c037dd736"}, {file = "wsproto-1.2.0.tar.gz", hash = "sha256:ad565f26ecb92588a3e43bc3d96164de84cd9902482b130d0ddbaa9664a85065"}, @@ -6002,6 +6363,7 @@ version = "1.15.2" description = "Yet another URL library" optional = false python-versions = ">=3.8" +groups = ["main"] 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"}, @@ -6114,17 +6476,18 @@ version = "3.20.2" description = "Backport of pathlib-compatible object wrapper for zip files" optional = false python-versions = ">=3.8" +groups = ["main", "dev", "proxy-dev"] files = [ {file = "zipp-3.20.2-py3-none-any.whl", hash = "sha256:a817ac80d6cf4b23bf7f2828b7cabf326f15a001bea8b1f9b49631780ba28350"}, {file = "zipp-3.20.2.tar.gz", hash = "sha256:bc9eb26f4506fda01b81bcde0ca78103b6e62f991b381fec825435c836edbc29"}, ] [package.extras] -check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)"] +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", "importlib-resources", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more-itertools", "pytest (>=6,!=8.1.*)", "pytest-ignore-flaky"] +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"] [extras] @@ -6136,6 +6499,6 @@ semantic-router = ["semantic-router"] utils = ["numpydoc"] [metadata] -lock-version = "2.0" +lock-version = "2.1" python-versions = ">=3.8.1,<4.0, !=3.9.7" -content-hash = "3b9cdf44dbcb105f490450cd4714bf5ced62825cb2b319a423afeac0219b5575" +content-hash = "d932532149768069e9b4002e113e8ced494318055c69d79e9ef22f535d96a20c" diff --git a/pyproject.toml b/pyproject.toml index 666c3a21b26..8bb8e03dad8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -59,7 +59,7 @@ redisvl = {version = "^0.4.1", optional = true, markers = "python_version >= '3. mcp = {version = "^1.10.0", optional = true, python = ">=3.10"} litellm-proxy-extras = {version = "0.2.15", optional = true} rich = {version = "13.7.1", optional = true} -litellm-enterprise = {version = "0.1.16", optional = true} +litellm-enterprise = {version = "0.1.17", optional = true} diskcache = {version = "^5.6.1", optional = true} polars = {version = "^1.31.0", optional = true, python = ">=3.10"} semantic-router = {version = "*", optional = true, python = ">=3.9"} diff --git a/requirements.txt b/requirements.txt index 364a4368827..9a7ce42e3be 100644 --- a/requirements.txt +++ b/requirements.txt @@ -59,4 +59,4 @@ websockets==13.1.0 # for realtime API ######################## # LITELLM ENTERPRISE DEPENDENCIES ######################## -litellm-enterprise==0.1.16 +litellm-enterprise==0.1.17 From de7108b5f8a4081be34241a3f006423bafd55065 Mon Sep 17 00:00:00 2001 From: "Jugal D. Bhatt" <55304795+jugaldb@users.noreply.github.com> Date: Mon, 4 Aug 2025 18:02:03 -0700 Subject: [PATCH 18/69] input cost per token higher than 1 test (#13270) --- tests/test_litellm/test_utils.py | 125 +++++++++++++++++++++++++++++++ 1 file changed, 125 insertions(+) diff --git a/tests/test_litellm/test_utils.py b/tests/test_litellm/test_utils.py index 1f59bb94dbd..b342f268cbd 100644 --- a/tests/test_litellm/test_utils.py +++ b/tests/test_litellm/test_utils.py @@ -373,6 +373,115 @@ def test_cohere_embedding_optional_params(): assert optional_params is not None +def validate_model_cost_values(model_data, exceptions=None): + """ + Validates that cost values in model data do not exceed 1. + + Args: + model_data (dict): The model data dictionary + exceptions (list, optional): List of model IDs that are allowed to have costs > 1 + + Returns: + tuple: (is_valid, violations) where is_valid is a boolean and violations is a list of error messages + """ + if exceptions is None: + exceptions = [] + + violations = [] + + # Define all cost-related fields to check + cost_fields = [ + "input_cost_per_token", + "output_cost_per_token", + "input_cost_per_character", + "output_cost_per_character", + "input_cost_per_image", + "output_cost_per_image", + "input_cost_per_pixel", + "output_cost_per_pixel", + "input_cost_per_second", + "output_cost_per_second", + "input_cost_per_query", + "input_cost_per_request", + "input_cost_per_audio_token", + "output_cost_per_audio_token", + "input_cost_per_audio_per_second", + "input_cost_per_video_per_second", + "input_cost_per_token_above_128k_tokens", + "output_cost_per_token_above_128k_tokens", + "input_cost_per_token_above_200k_tokens", + "output_cost_per_token_above_200k_tokens", + "input_cost_per_character_above_128k_tokens", + "output_cost_per_character_above_128k_tokens", + "input_cost_per_image_above_128k_tokens", + "input_cost_per_video_per_second_above_8s_interval", + "input_cost_per_video_per_second_above_15s_interval", + "input_cost_per_video_per_second_above_128k_tokens", + "input_cost_per_token_batch_requests", + "input_cost_per_token_batches", + "output_cost_per_token_batches", + "input_cost_per_token_cache_hit", + "cache_creation_input_token_cost", + "cache_creation_input_audio_token_cost", + "cache_read_input_token_cost", + "cache_read_input_audio_token_cost", + "input_dbu_cost_per_token", + "output_db_cost_per_token", + "output_dbu_cost_per_token", + "output_cost_per_reasoning_token", + "citation_cost_per_token", + ] + + # Also check nested cost fields + nested_cost_fields = [ + "search_context_cost_per_query", + ] + + for model_id, model_info in model_data.items(): + # Skip if this model is in exceptions + if model_id in exceptions: + continue + + # Check direct cost fields + for field in cost_fields: + if field in model_info and model_info[field] is not None: + cost_value = model_info[field] + + # Convert string values to float if needed + if isinstance(cost_value, str): + try: + cost_value = float(cost_value) + except (ValueError, TypeError): + # Skip if we can't convert to float + continue + + if isinstance(cost_value, (int, float)) and cost_value > 1: + violations.append( + f"Model '{model_id}' has {field} = {cost_value} which exceeds 1" + ) + + # Check nested cost fields + for field in nested_cost_fields: + if field in model_info and model_info[field] is not None: + nested_costs = model_info[field] + if isinstance(nested_costs, dict): + for nested_field, nested_value in nested_costs.items(): + # Convert string values to float if needed + if isinstance(nested_value, str): + try: + nested_value = float(nested_value) + except (ValueError, TypeError): + # Skip if we can't convert to float + continue + + if isinstance(nested_value, (int, float)) and nested_value > 1: + violations.append( + f"Model '{model_id}' has {field}.{nested_field} = {nested_value} which exceeds 1" + ) + + return len(violations) == 0, violations + + def test_aaamodel_prices_and_context_window_json_is_valid(): """ Validates the `model_prices_and_context_window.json` file. @@ -542,7 +651,23 @@ def test_aaamodel_prices_and_context_window_json_is_valid(): "sample_spec", None ) # remove the sample, whose schema is inconsistent with the real data + # Validate schema validate(actual_json, INTENDED_SCHEMA) + + # Validate cost values + # Define exceptions for models that are allowed to have costs > 1 + # Add model IDs here if they legitimately have costs > 1 + exceptions = [ + # Add any model IDs that should be exempt from the cost validation + # Example: "expensive-model-id", + ] + + is_valid, violations = validate_model_cost_values(actual_json, exceptions) + + if not is_valid: + error_message = "Cost validation failed:\n" + "\n".join(violations) + error_message += "\n\nTo add exceptions, add the model ID to the 'exceptions' list in the test function." + raise AssertionError(error_message) def test_get_model_info_gemini(): From efd34966dc3c291b3cc54319adfe928b81dbfe3a Mon Sep 17 00:00:00 2001 From: "Jugal D. Bhatt" <55304795+jugaldb@users.noreply.github.com> Date: Mon, 4 Aug 2025 18:03:59 -0700 Subject: [PATCH 19/69] [LLM Translation] Support /v1/models/{model_id} retrieval (#13268) * added model id endpoint * fix test * add route to internal users * make the functions reusable * fixed mypy --- litellm/proxy/_types.py | 2 + litellm/proxy/proxy_server.py | 223 +++++++----------- litellm/proxy/utils.py | 175 +++++++++++++- .../test_model_management_endpoints.py | 116 +++++++++ 4 files changed, 380 insertions(+), 136 deletions(-) diff --git a/litellm/proxy/_types.py b/litellm/proxy/_types.py index 9012dee8fc0..e9beadbb5e4 100644 --- a/litellm/proxy/_types.py +++ b/litellm/proxy/_types.py @@ -492,6 +492,8 @@ class LiteLLMRoutes(enum.Enum): "/global/spend/end_users", "/global/activity", "/global/activity/model", + "/v1/models/{model_id}", + "/models/{model_id}", ] + spend_tracking_routes + key_management_routes diff --git a/litellm/proxy/proxy_server.py b/litellm/proxy/proxy_server.py index af9f5d363da..dffb12873ce 100644 --- a/litellm/proxy/proxy_server.py +++ b/litellm/proxy/proxy_server.py @@ -1728,7 +1728,7 @@ class ProxyConfig: self._load_environment_variables(config=config) ## Callback settings - callback_settings = config.get("callback_settings", None) + callback_settings = config.get("callback_settings", {}) ## LITELLM MODULE SETTINGS (e.g. litellm.drop_params=True,..) litellm_settings = config.get("litellm_settings", None) @@ -3766,100 +3766,34 @@ async def model_list( Defaults to "general" when include_metadata=true """ global llm_model_list, general_settings, llm_router, prisma_client, user_api_key_cache, proxy_logging_obj - all_models = [] - model_access_groups: Dict[str, List[str]] = defaultdict(list) - ## CHECK IF MODEL RESTRICTIONS ARE SET AT KEY/TEAM LEVEL ## - if llm_router is None: - proxy_model_list = [] - else: - proxy_model_list = llm_router.get_model_names() - model_access_groups = llm_router.get_model_access_groups() - - ## if only_model_access_groups is True, - """ - 1. Get all models key/user/team has access to - 2. Filter out models that are not model access groups - 3. Return the models - """ - if only_model_access_groups is True: - include_model_access_groups = True - - key_models = get_key_models( + + from litellm.proxy.utils import get_available_models_for_user, create_model_info_response + + # Get available models for the user + all_models = await get_available_models_for_user( user_api_key_dict=user_api_key_dict, - proxy_model_list=proxy_model_list, - model_access_groups=model_access_groups, - include_model_access_groups=include_model_access_groups, - ) - - team_models: List[str] = user_api_key_dict.team_models - - if team_id: - key_models = [] - team_object = await get_team_object( - team_id=team_id, - prisma_client=prisma_client, - user_api_key_cache=user_api_key_cache, - proxy_logging_obj=proxy_logging_obj, - ) - validate_membership(user_api_key_dict=user_api_key_dict, team_table=team_object) - team_models = team_object.models - - team_models = get_team_models( - team_models=team_models, - proxy_model_list=proxy_model_list, - model_access_groups=model_access_groups, - include_model_access_groups=include_model_access_groups, - ) - - all_models = get_complete_model_list( - key_models=key_models, - team_models=team_models, - proxy_model_list=proxy_model_list, - user_model=user_model, - infer_model_from_keys=general_settings.get("infer_model_from_keys", False), - return_wildcard_routes=return_wildcard_routes, llm_router=llm_router, - model_access_groups=model_access_groups, - include_model_access_groups=include_model_access_groups, - only_model_access_groups=only_model_access_groups, + general_settings=general_settings, + user_model=user_model, + prisma_client=prisma_client, + proxy_logging_obj=proxy_logging_obj, + team_id=team_id, + include_model_access_groups=include_model_access_groups or False, + only_model_access_groups=only_model_access_groups or False, + return_wildcard_routes=return_wildcard_routes or False, + user_api_key_cache=user_api_key_cache, ) # Build response data model_data = [] for model in all_models: - model_info = { - "id": model, - "object": "model", - "created": DEFAULT_MODEL_CREATED_AT_TIME, - "owned_by": "openai", - } - - # Add metadata if requested - if include_metadata: - metadata = {} - - # Default fallback_type to "general" if include_metadata is true - effective_fallback_type = ( - fallback_type if fallback_type is not None else "general" - ) - - # Validate fallback_type - valid_fallback_types = ["general", "context_window", "content_policy"] - if effective_fallback_type not in valid_fallback_types: - raise HTTPException( - status_code=400, - detail=f"Invalid fallback_type. Must be one of: {valid_fallback_types}", - ) - - fallbacks = get_all_fallbacks( - model=model, - llm_router=llm_router, - fallback_type=effective_fallback_type, - ) - metadata["fallbacks"] = fallbacks - - model_info["metadata"] = metadata - + model_info = create_model_info_response( + model_id=model, + provider="openai", + include_metadata=include_metadata or False, + fallback_type=fallback_type, + llm_router=llm_router, + ) model_data.append(model_info) return dict( @@ -3868,6 +3802,60 @@ async def model_list( ) +@router.get( + "/v1/models/{model_id}", dependencies=[Depends(user_api_key_auth)], tags=["model management"] +) +@router.get( + "/models/{model_id}", dependencies=[Depends(user_api_key_auth)], tags=["model management"] +) +async def model_info( + model_id: str, + user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth), +): + """ + Retrieve information about a specific model accessible to your API key. + + Returns model details only if the model is available to your API key/team. + Returns 404 if the model doesn't exist or is not accessible. + + Follows OpenAI API specification for individual model retrieval. + https://platform.openai.com/docs/api-reference/models/retrieve + """ + global llm_model_list, general_settings, llm_router, prisma_client, user_api_key_cache, proxy_logging_obj + + from litellm.proxy.utils import get_available_models_for_user, validate_model_access, create_model_info_response + + # Get available models for the user + all_models = await get_available_models_for_user( + user_api_key_dict=user_api_key_dict, + llm_router=llm_router, + general_settings=general_settings, + user_model=user_model, + prisma_client=prisma_client, + proxy_logging_obj=proxy_logging_obj, + team_id=None, + include_model_access_groups=False, + only_model_access_groups=False, + return_wildcard_routes=False, + user_api_key_cache=user_api_key_cache, + ) + + # Validate that the requested model is accessible + validate_model_access(model_id=model_id, available_models=all_models) + + # Get provider information + _, provider, _, _ = litellm.get_llm_provider(model=model_id) + + # Return the model information in the same format as the list endpoint + return create_model_info_response( + model_id=model_id, + provider=provider, + include_metadata=False, + fallback_type=None, + llm_router=llm_router, + ) + + @router.post( "/v1/chat/completions", dependencies=[Depends(user_api_key_auth)], @@ -6930,56 +6918,21 @@ async def model_group_info( status_code=500, detail={"error": "LLM Router is not loaded in"} ) - ## CHECK IF MODEL RESTRICTIONS ARE SET AT KEY/TEAM LEVEL ## - model_access_groups: Dict[str, List[str]] = defaultdict(list) - if llm_router is None: - proxy_model_list = [] - else: - proxy_model_list = llm_router.get_model_names() - model_access_groups = llm_router.get_model_access_groups() + from litellm.proxy.utils import get_available_models_for_user - key_models = get_key_models( + # Get available models for the user + all_models_str = await get_available_models_for_user( user_api_key_dict=user_api_key_dict, - proxy_model_list=proxy_model_list, - model_access_groups=model_access_groups, - ) - team_models = [] - if ( - not user_api_key_dict.team_id - and user_api_key_dict.user_id is not None - and not _user_has_admin_view(user_api_key_dict) - ): - if prisma_client is None: - raise HTTPException( - status_code=500, - detail={"error": CommonProxyErrors.db_not_connected_error.value}, - ) - user_object = await prisma_client.db.litellm_usertable.find_first( - where={"user_id": user_api_key_dict.user_id} - ) - user_object_typed = LiteLLM_UserTable(**user_object.model_dump()) - user_models = [] - if user_object is not None: - user_models = get_team_models( - team_models=user_object_typed.models, - proxy_model_list=proxy_model_list, - model_access_groups=model_access_groups, - ) - team_models = user_models - else: - team_models = get_team_models( - team_models=user_api_key_dict.team_models, - proxy_model_list=proxy_model_list, - model_access_groups=model_access_groups, - ) - - all_models_str = get_complete_model_list( - key_models=key_models, - team_models=team_models, - proxy_model_list=proxy_model_list, - user_model=user_model, - infer_model_from_keys=general_settings.get("infer_model_from_keys", False), llm_router=llm_router, + general_settings=general_settings, + user_model=user_model, + prisma_client=prisma_client, + proxy_logging_obj=proxy_logging_obj, + team_id=None, + include_model_access_groups=False, + only_model_access_groups=False, + return_wildcard_routes=False, + user_api_key_cache=user_api_key_cache, ) model_groups: List[ModelGroupInfoProxy] = _get_model_group_info( llm_router=llm_router, all_models_str=all_models_str, model_group=model_group diff --git a/litellm/proxy/utils.py b/litellm/proxy/utils.py index f1e113d8ded..16e151e5e19 100644 --- a/litellm/proxy/utils.py +++ b/litellm/proxy/utils.py @@ -22,7 +22,7 @@ from typing import ( overload, ) -from litellm.constants import MAX_TEAM_LIST_LIMIT +from litellm.constants import MAX_TEAM_LIST_LIMIT, DEFAULT_MODEL_CREATED_AT_TIME from litellm.proxy._types import ( DB_CONNECTION_ERROR_TYPES, CommonProxyErrors, @@ -3831,3 +3831,176 @@ def construct_database_url_from_env_vars() -> Optional[str]: return database_url return None + + +async def get_available_models_for_user( + user_api_key_dict: "UserAPIKeyAuth", + llm_router: Optional["Router"], + general_settings: dict, + user_model: Optional[str], + prisma_client: Optional["PrismaClient"] = None, + proxy_logging_obj: Optional["ProxyLogging"] = None, + team_id: Optional[str] = None, + include_model_access_groups: bool = False, + only_model_access_groups: bool = False, + return_wildcard_routes: bool = False, + user_api_key_cache: Optional["DualCache"] = None, +) -> List[str]: + """ + Get the list of models available to a user based on their API key and team permissions. + + Args: + user_api_key_dict: User API key authentication object + llm_router: LiteLLM router instance + general_settings: General settings from config + user_model: User-specific model + prisma_client: Prisma client for database operations + proxy_logging_obj: Proxy logging object + team_id: Specific team ID to check (optional) + include_model_access_groups: Whether to include model access groups + only_model_access_groups: Whether to only return model access groups + return_wildcard_routes: Whether to return wildcard routes + + Returns: + List of model names available to the user + """ + from litellm.proxy.auth.model_checks import ( + get_key_models, + get_team_models, + get_complete_model_list, + ) + from litellm.proxy.auth.auth_checks import get_team_object + from litellm.proxy.management_endpoints.team_endpoints import validate_membership + + # Get proxy model list and access groups + if llm_router is None: + proxy_model_list = [] + model_access_groups = {} + else: + proxy_model_list = llm_router.get_model_names() + model_access_groups = llm_router.get_model_access_groups() + + # Get key models + key_models = get_key_models( + user_api_key_dict=user_api_key_dict, + proxy_model_list=proxy_model_list, + model_access_groups=model_access_groups, + include_model_access_groups=include_model_access_groups, + ) + + # Get team models + team_models: List[str] = user_api_key_dict.team_models + + # If specific team_id is provided, validate and get team models + if team_id and prisma_client and proxy_logging_obj and user_api_key_cache: + key_models = [] + team_object = await get_team_object( + team_id=team_id, + prisma_client=prisma_client, + user_api_key_cache=user_api_key_cache, + proxy_logging_obj=proxy_logging_obj, + ) + validate_membership(user_api_key_dict=user_api_key_dict, team_table=team_object) + team_models = team_object.models + + team_models = get_team_models( + team_models=team_models, + proxy_model_list=proxy_model_list, + model_access_groups=model_access_groups, + include_model_access_groups=include_model_access_groups, + ) + + # Get complete model list + all_models = get_complete_model_list( + key_models=key_models, + team_models=team_models, + proxy_model_list=proxy_model_list, + user_model=user_model, + infer_model_from_keys=general_settings.get("infer_model_from_keys", False), + return_wildcard_routes=return_wildcard_routes, + llm_router=llm_router, + model_access_groups=model_access_groups, + include_model_access_groups=include_model_access_groups, + only_model_access_groups=only_model_access_groups, + ) + + return all_models + + +def create_model_info_response( + model_id: str, + provider: str, + include_metadata: bool = False, + fallback_type: Optional[str] = None, + llm_router: Optional["Router"] = None, +) -> dict: + """ + Create a standardized model info response. + + Args: + model_id: The model ID + provider: The model provider + include_metadata: Whether to include metadata + fallback_type: Type of fallbacks to include + llm_router: LiteLLM router instance + + Returns: + Dictionary containing model information + """ + from litellm.proxy.auth.model_checks import get_all_fallbacks + + model_info = { + "id": model_id, + "object": "model", + "created": DEFAULT_MODEL_CREATED_AT_TIME, + "owned_by": provider, + } + + # Add metadata if requested + if include_metadata: + metadata = {} + + # Default fallback_type to "general" if include_metadata is true + effective_fallback_type = ( + fallback_type if fallback_type is not None else "general" + ) + + # Validate fallback_type + valid_fallback_types = ["general", "context_window", "content_policy"] + if effective_fallback_type not in valid_fallback_types: + raise HTTPException( + status_code=400, + detail=f"Invalid fallback_type. Must be one of: {valid_fallback_types}", + ) + + fallbacks = get_all_fallbacks( + model=model_id, + llm_router=llm_router, + fallback_type=effective_fallback_type, + ) + metadata["fallbacks"] = fallbacks + + model_info["metadata"] = metadata + + return model_info + + +def validate_model_access( + model_id: str, + available_models: List[str], +) -> None: + """ + Validate that a model is accessible to the user. + + Args: + model_id: The model ID to validate + available_models: List of models available to the user + + Raises: + HTTPException: If the model is not accessible + """ + if model_id not in available_models: + raise HTTPException( + status_code=404, + detail="The model `{}` does not exist or is not accessible".format(model_id) + ) \ No newline at end of file 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 c5fdbf4925a..bd37e9cbe41 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 @@ -451,3 +451,119 @@ class TestClearCache: mock_config.add_deployment.assert_called_once_with( prisma_client=mock_prisma, proxy_logging_obj=mock_logging ) + + +class TestModelInfoEndpoint: + """Test the model_info endpoint for retrieving individual model information""" + + @pytest.mark.asyncio + async def test_model_info_accessible_model_success(self): + """Test model_info returns model data for accessible models""" + from litellm.proxy.proxy_server import model_info + + # Mock user with access to specific models + user_api_key_dict = UserAPIKeyAuth( + user_id="test_user", + api_key="test_key", + models=["gpt-4", "claude-3"], + team_models=["gpt-3.5-turbo"] + ) + + with patch("litellm.proxy.proxy_server.llm_router") as mock_router, \ + patch("litellm.proxy.proxy_server.get_key_models") as mock_get_key_models, \ + patch("litellm.proxy.proxy_server.get_team_models") as mock_get_team_models, \ + patch("litellm.proxy.proxy_server.get_complete_model_list") as mock_get_complete_models, \ + patch("litellm.get_llm_provider") as mock_get_provider: + + # Setup mocks + mock_router.get_model_names.return_value = ["gpt-4", "claude-3", "gpt-3.5-turbo"] + mock_router.get_model_access_groups.return_value = {} + mock_get_key_models.return_value = ["gpt-4", "claude-3"] + mock_get_team_models.return_value = ["gpt-3.5-turbo"] + mock_get_complete_models.return_value = ["gpt-4", "claude-3", "gpt-3.5-turbo"] + mock_get_provider.return_value = (None, "openai", None, None) + + # Test accessible model + result = await model_info( + model_id="gpt-4", + user_api_key_dict=user_api_key_dict + ) + + assert result["id"] == "gpt-4" + assert result["object"] == "model" + assert result["owned_by"] == "openai" + assert "created" in result + + @pytest.mark.asyncio + async def test_model_info_inaccessible_model_returns_404(self): + """Test model_info returns 404 for inaccessible models""" + from litellm.proxy.proxy_server import model_info + from fastapi import HTTPException + + # Mock user with limited access + user_api_key_dict = UserAPIKeyAuth( + user_id="test_user", + api_key="test_key", + models=["gpt-4"], # Only has access to gpt-4 + team_models=[] + ) + + with patch("litellm.proxy.proxy_server.llm_router") as mock_router, \ + patch("litellm.proxy.proxy_server.get_key_models") as mock_get_key_models, \ + patch("litellm.proxy.proxy_server.get_team_models") as mock_get_team_models, \ + patch("litellm.proxy.proxy_server.get_complete_model_list") as mock_get_complete_models: + + # Setup mocks - user only has access to gpt-4 + mock_router.get_model_names.return_value = ["gpt-4", "claude-3"] + mock_router.get_model_access_groups.return_value = {} + mock_get_key_models.return_value = ["gpt-4"] + mock_get_team_models.return_value = [] + mock_get_complete_models.return_value = ["gpt-4"] # Only gpt-4 accessible + + # Test inaccessible model should raise 404 + with pytest.raises(HTTPException) as exc_info: + await model_info( + model_id="claude-3", # Not in user's accessible models + user_api_key_dict=user_api_key_dict + ) + + assert exc_info.value.status_code == 404 + assert "does not exist or is not accessible" in exc_info.value.detail + + @pytest.mark.asyncio + async def test_model_info_team_model_access(self): + """Test model_info works with team model access""" + from litellm.proxy.proxy_server import model_info + + # Mock user with team access + user_api_key_dict = UserAPIKeyAuth( + user_id="test_user", + api_key="test_key", + team_id="test_team", + models=[], # No direct key models + team_models=["team-model-1"] + ) + + with patch("litellm.proxy.proxy_server.llm_router") as mock_router, \ + patch("litellm.proxy.proxy_server.get_key_models") as mock_get_key_models, \ + patch("litellm.proxy.proxy_server.get_team_models") as mock_get_team_models, \ + patch("litellm.proxy.proxy_server.get_complete_model_list") as mock_get_complete_models, \ + patch("litellm.get_llm_provider") as mock_get_provider: + + # Setup mocks + mock_router.get_model_names.return_value = ["team-model-1"] + mock_router.get_model_access_groups.return_value = {} + mock_get_key_models.return_value = [] + mock_get_team_models.return_value = ["team-model-1"] + mock_get_complete_models.return_value = ["team-model-1"] + mock_get_provider.return_value = (None, "custom", None, None) + + # Test team model access + result = await model_info( + model_id="team-model-1", + user_api_key_dict=user_api_key_dict + ) + + assert result["id"] == "team-model-1" + assert result["object"] == "model" + assert result["owned_by"] == "custom" From ec205fec35169043ffc0da6a0914f8aca88cbe21 Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Mon, 4 Aug 2025 21:47:39 -0700 Subject: [PATCH 20/69] [UI] - Add ability to set model alias per key/team (#13276) * update model alias on keys * team model aliases * fix model aliases * fixes for teams --- .../common_components/ModelAliasManager.tsx | 342 ++++++++++++++++++ .../common_components/ModelSelector.tsx | 122 +++++++ .../src/components/create_key_button.tsx | 29 ++ ui/litellm-dashboard/src/components/teams.tsx | 32 ++ 4 files changed, 525 insertions(+) create mode 100644 ui/litellm-dashboard/src/components/common_components/ModelAliasManager.tsx create mode 100644 ui/litellm-dashboard/src/components/common_components/ModelSelector.tsx diff --git a/ui/litellm-dashboard/src/components/common_components/ModelAliasManager.tsx b/ui/litellm-dashboard/src/components/common_components/ModelAliasManager.tsx new file mode 100644 index 00000000000..db49e1d999c --- /dev/null +++ b/ui/litellm-dashboard/src/components/common_components/ModelAliasManager.tsx @@ -0,0 +1,342 @@ +import React, { useState, useEffect } from "react"; +import { message } from "antd"; +import { PlusCircleIcon, PencilIcon, TrashIcon } from "@heroicons/react/outline"; +import { + Card, + Title, + Text, + Table, + TableHead, + TableHeaderCell, + TableBody, + TableRow, + TableCell +} from "@tremor/react"; +import ModelSelector from "./ModelSelector"; + +interface ModelAliasManagerProps { + accessToken: string; + initialModelAliases?: { [key: string]: string }; + onAliasUpdate?: (updatedAliases: { [key: string]: string }) => void; + showExampleConfig?: boolean; +} + +interface AliasItem { + id: string; + aliasName: string; + targetModel: string; +} + +const ModelAliasManager: React.FC = ({ + accessToken, + initialModelAliases = {}, + onAliasUpdate, + showExampleConfig = true, +}) => { + const [aliases, setAliases] = useState([]); + const [newAlias, setNewAlias] = useState({ aliasName: "", targetModel: "" }); + const [editingAlias, setEditingAlias] = useState(null); + + useEffect(() => { + // Convert object to array for display + const aliasArray = Object.entries(initialModelAliases).map(([aliasName, targetModel], index) => ({ + id: `${index}-${aliasName}`, + aliasName, + targetModel, + })); + setAliases(aliasArray); + }, [initialModelAliases]); + + const handleAddAlias = () => { + if (!newAlias.aliasName || !newAlias.targetModel) { + message.error("Please provide both alias name and target model"); + return; + } + + // Check for duplicate alias names + if (aliases.some(alias => alias.aliasName === newAlias.aliasName)) { + message.error("An alias with this name already exists"); + return; + } + + const newAliasObj: AliasItem = { + id: `${Date.now()}-${newAlias.aliasName}`, + aliasName: newAlias.aliasName, + targetModel: newAlias.targetModel, + }; + + const updatedAliases = [...aliases, newAliasObj]; + setAliases(updatedAliases); + setNewAlias({ aliasName: "", targetModel: "" }); + + // Convert array back to object format and notify parent + const aliasObject: { [key: string]: string } = {}; + updatedAliases.forEach(alias => { + aliasObject[alias.aliasName] = alias.targetModel; + }); + + if (onAliasUpdate) { + onAliasUpdate(aliasObject); + } + + message.success("Alias added successfully"); + }; + + const handleEditAlias = (alias: AliasItem) => { + setEditingAlias({ ...alias }); + }; + + const handleUpdateAlias = () => { + if (!editingAlias) return; + + if (!editingAlias.aliasName || !editingAlias.targetModel) { + message.error("Please provide both alias name and target model"); + return; + } + + // Check for duplicate alias names (excluding current alias) + if (aliases.some(alias => alias.id !== editingAlias.id && alias.aliasName === editingAlias.aliasName)) { + message.error("An alias with this name already exists"); + return; + } + + const updatedAliases = aliases.map(alias => + alias.id === editingAlias.id ? editingAlias : alias + ); + + setAliases(updatedAliases); + setEditingAlias(null); + + // Convert array back to object format and notify parent + const aliasObject: { [key: string]: string } = {}; + updatedAliases.forEach(alias => { + aliasObject[alias.aliasName] = alias.targetModel; + }); + + if (onAliasUpdate) { + onAliasUpdate(aliasObject); + } + + message.success("Alias updated successfully"); + }; + + const handleCancelEdit = () => { + setEditingAlias(null); + }; + + const deleteAlias = (aliasId: string) => { + const updatedAliases = aliases.filter(alias => alias.id !== aliasId); + setAliases(updatedAliases); + + // Convert array back to object format and notify parent + const aliasObject: { [key: string]: string } = {}; + updatedAliases.forEach(alias => { + aliasObject[alias.aliasName] = alias.targetModel; + }); + + if (onAliasUpdate) { + onAliasUpdate(aliasObject); + } + + message.success("Alias deleted successfully"); + }; + + // Convert current aliases to object for config example + const aliasObject = aliases.reduce((acc, alias) => { + acc[alias.aliasName] = alias.targetModel; + return acc; + }, {} as { [key: string]: string }); + + return ( +
+
+ Add New Alias +
+
+ + + setNewAlias({ + ...newAlias, + aliasName: e.target.value, + }) + } + placeholder="e.g., gpt-4o" + className="w-full px-3 py-2 border border-gray-300 rounded-md text-sm" + /> +
+
+ + + setNewAlias({ + ...newAlias, + targetModel: value, + }) + } + showLabel={false} + /> +
+
+ +
+
+
+ + + Manage Existing Aliases + +
+
+ + + + + Alias Name + + + Target Model + + + Actions + + + + + {aliases.map((alias) => ( + + {editingAlias && editingAlias.id === alias.id ? ( + <> + + + setEditingAlias({ + ...editingAlias, + aliasName: e.target.value, + }) + } + className="w-full px-2 py-1 border border-gray-300 rounded-md text-sm" + /> + + + + setEditingAlias({ + ...editingAlias, + targetModel: value, + }) + } + showLabel={false} + style={{ height: '32px' }} + /> + + +
+ + +
+
+ + ) : ( + <> + + {alias.aliasName} + + + {alias.targetModel} + + +
+ + +
+
+ + )} +
+ ))} + {aliases.length === 0 && ( + + + No aliases added yet. Add a new alias above. + + + )} +
+
+
+
+ + {/* Configuration Example */} + {showExampleConfig && ( + + Configuration Example + + Here's how your current aliases would look in the config: + +
+
+ model_aliases: + {Object.keys(aliasObject).length === 0 ? ( + +
+   # No aliases configured yet +
+ ) : ( + Object.entries(aliasObject).map(([key, value]) => ( + +
+   "{key}": "{value}" +
+ )) + )} +
+
+
+ )} +
+ ); +}; + +export default ModelAliasManager; \ No newline at end of file diff --git a/ui/litellm-dashboard/src/components/common_components/ModelSelector.tsx b/ui/litellm-dashboard/src/components/common_components/ModelSelector.tsx new file mode 100644 index 00000000000..a8a8087cb62 --- /dev/null +++ b/ui/litellm-dashboard/src/components/common_components/ModelSelector.tsx @@ -0,0 +1,122 @@ +import React, { useState, useEffect, useRef } from "react"; +import { TextInput, Text } from "@tremor/react"; +import { Select } from "antd"; +import { RobotOutlined } from "@ant-design/icons"; +import { fetchAvailableModels, ModelGroup } from "../chat_ui/llm_calls/fetch_models"; + +interface ModelSelectorProps { + accessToken: string; + value?: string; + placeholder?: string; + onChange?: (value: string) => void; + disabled?: boolean; + style?: React.CSSProperties; + className?: string; + showLabel?: boolean; + labelText?: string; +} + +const ModelSelector: React.FC = ({ + accessToken, + value, + placeholder = "Select a Model", + onChange, + disabled = false, + style, + className, + showLabel = true, + labelText = "Select Model" +}) => { + const [selectedModel, setSelectedModel] = useState(value); + const [showCustomModelInput, setShowCustomModelInput] = useState(false); + const [modelInfo, setModelInfo] = useState([]); + const customModelTimeout = useRef(null); + + useEffect(() => { + setSelectedModel(value); + }, [value]); + + useEffect(() => { + if (!accessToken) return; + + const loadModels = async () => { + try { + const uniqueModels = await fetchAvailableModels(accessToken); + console.log("Fetched models for selector:", uniqueModels); + + if (uniqueModels.length > 0) { + setModelInfo(uniqueModels); + } + } catch (error) { + console.error("Error fetching model info:", error); + } + }; + + loadModels(); + }, [accessToken]); + + const onModelChange = (value: string) => { + if (value === 'custom') { + setShowCustomModelInput(true); + setSelectedModel(undefined); + } else { + setShowCustomModelInput(false); + setSelectedModel(value); + if (onChange) { + onChange(value); + } + } + }; + + const handleCustomModelChange = (value: string) => { + // Using setTimeout to create a simple debounce effect + if (customModelTimeout.current) { + clearTimeout(customModelTimeout.current); + } + + customModelTimeout.current = setTimeout(() => { + setSelectedModel(value); + if (onChange) { + onChange(value); + } + }, 500); // 500ms delay after typing stops + }; + + return ( +
+ {showLabel && ( + + {labelText} + + )} +