diff --git a/litellm/llms/azure/containers/transformation.py b/litellm/llms/azure/containers/transformation.py index fac46822a8d..8304c0cca4e 100644 --- a/litellm/llms/azure/containers/transformation.py +++ b/litellm/llms/azure/containers/transformation.py @@ -1,4 +1,5 @@ -from typing import Optional +from typing import Any, Dict, Optional, Tuple +from urllib.parse import urlparse, urlunparse import httpx @@ -83,3 +84,83 @@ class AzureOpenAIContainerConfig(OpenAIContainerConfig): ] = container_cost return container_obj + + def _construct_url_with_subpath(self, api_base: str, subpath: str) -> str: + """Construct a URL by inserting subpath before query parameters. + + Azure URLs contain ?api-version=... query params. Naively appending + path segments via string concatenation would place them after the + query string, producing malformed URLs. This helper parses the URL + and appends the subpath to the path component only. + """ + parsed = urlparse(api_base) + new_path = f"{parsed.path.rstrip('/')}/{subpath}" + return urlunparse( + ( + parsed.scheme, + parsed.netloc, + new_path, + parsed.params, + parsed.query, + parsed.fragment, + ) + ) + + def transform_container_retrieve_request( + self, + container_id: str, + api_base: str, + litellm_params: GenericLiteLLMParams, + headers: dict, + ) -> Tuple[str, Dict]: + url = self._construct_url_with_subpath(api_base, container_id) + data: Dict[str, Any] = {} + return url, data + + def transform_container_delete_request( + self, + container_id: str, + api_base: str, + litellm_params: GenericLiteLLMParams, + headers: dict, + ) -> Tuple[str, Dict]: + url = self._construct_url_with_subpath(api_base, container_id) + data: Dict[str, Any] = {} + return url, data + + def transform_container_file_list_request( + self, + container_id: str, + api_base: str, + litellm_params: GenericLiteLLMParams, + headers: dict, + after: Optional[str] = None, + limit: Optional[int] = None, + order: Optional[str] = None, + extra_query: Optional[Dict[str, Any]] = None, + ) -> Tuple[str, Dict]: + url = self._construct_url_with_subpath(api_base, f"{container_id}/files") + params: Dict[str, Any] = {} + if after is not None: + params["after"] = after + if limit is not None: + params["limit"] = str(limit) + if order is not None: + params["order"] = order + if extra_query: + params.update(extra_query) + return url, params + + def transform_container_file_content_request( + self, + container_id: str, + file_id: str, + api_base: str, + litellm_params: GenericLiteLLMParams, + headers: dict, + ) -> Tuple[str, Dict]: + url = self._construct_url_with_subpath( + api_base, f"{container_id}/files/{file_id}/content" + ) + params: Dict[str, Any] = {} + return url, params diff --git a/tests/test_litellm/containers/test_azure_container_transformation.py b/tests/test_litellm/containers/test_azure_container_transformation.py index c0260542dec..ec6289dd412 100644 --- a/tests/test_litellm/containers/test_azure_container_transformation.py +++ b/tests/test_litellm/containers/test_azure_container_transformation.py @@ -205,8 +205,32 @@ class TestAzureContainerTransformations: assert isinstance(result, ContainerListResponse) assert len(result.data) == 1 + def test_transform_container_retrieve_request_with_query_params(self): + api_base = "https://my-resource.openai.azure.com/openai/v1/containers?api-version=preview" + url, data = self.config.transform_container_retrieve_request( + container_id="cntr_123", + api_base=api_base, + litellm_params={}, + headers={"api-key": "test"}, + ) + assert "/containers/cntr_123" in url + assert "?api-version=preview" in url + assert url.index("/cntr_123") < url.index("?api-version") + + def test_transform_container_delete_request_with_query_params(self): + api_base = "https://my-resource.openai.azure.com/openai/v1/containers?api-version=preview" + url, data = self.config.transform_container_delete_request( + container_id="cntr_123", + api_base=api_base, + litellm_params={}, + headers={"api-key": "test"}, + ) + assert "/containers/cntr_123" in url + assert "?api-version=preview" in url + assert url.index("/cntr_123") < url.index("?api-version") + def test_transform_container_file_list_request(self): - api_base = "https://my-resource.openai.azure.com/openai/v1/containers" + api_base = "https://my-resource.openai.azure.com/openai/v1/containers?api-version=preview" url, params = self.config.transform_container_file_list_request( container_id="cntr_123", api_base=api_base, @@ -214,11 +238,13 @@ class TestAzureContainerTransformations: headers={"api-key": "test"}, limit=10, ) - assert "cntr_123/files" in url + assert "/containers/cntr_123/files" in url + assert "?api-version=preview" in url + assert url.index("/cntr_123/files") < url.index("?api-version") assert params["limit"] == "10" def test_transform_container_file_content_request(self): - api_base = "https://my-resource.openai.azure.com/openai/v1/containers" + api_base = "https://my-resource.openai.azure.com/openai/v1/containers?api-version=preview" url, params = self.config.transform_container_file_content_request( container_id="cntr_123", file_id="file_456", @@ -226,7 +252,9 @@ class TestAzureContainerTransformations: litellm_params={}, headers={"api-key": "test"}, ) - assert "cntr_123/files/file_456/content" in url + assert "/containers/cntr_123/files/file_456/content" in url + assert "?api-version=preview" in url + assert url.index("/file_456/content") < url.index("?api-version") def test_transform_container_file_content_response(self): mock_response = MagicMock(spec=httpx.Response)