fix: handle Azure URL query params in container transform methods

Override transform methods that append path segments to api_base,
using URL parsing to insert subpaths before query parameters.
Without this, Azure's ?api-version=... query param causes malformed URLs.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
zooneon 2026-03-28 00:09:52 +09:00
parent 41400c0a3b
commit 7339be571c
2 changed files with 114 additions and 5 deletions

View file

@ -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

View file

@ -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)