fix url construction for trailing v1 base urls

This commit is contained in:
Rohan Shetty 2026-04-18 19:27:01 -04:00
parent 850fe595ac
commit 2184a3dab1
2 changed files with 62 additions and 6 deletions

View file

@ -721,14 +721,17 @@ class OpenAIGPTConfig(BaseLLMModelInfo, BaseConfig):
if api_key is None:
api_key = get_secret_str("OPENAI_API_KEY")
# Strip api_base to just the base URL (scheme + host + port)
parsed_url = httpx.URL(api_base)
base_url = f"{parsed_url.scheme}://{parsed_url.host}"
if parsed_url.port:
base_url += f":{parsed_url.port}"
# Remove trailing slash from api_base if present
api_base = api_base.rstrip("/")
# If api_base already ends with /v1, just append /models
if api_base.endswith("/v1"):
url = f"{api_base}/models"
else:
url = f"{api_base}/v1/models"
response = litellm.module_level_client.get(
url=f"{base_url}/v1/models",
url=url,
headers={"Authorization": f"Bearer {api_key}"},
)

View file

@ -0,0 +1,53 @@
from unittest.mock import MagicMock, patch
from litellm.llms.openai.chat.gpt_transformation import OpenAIGPTConfig
class TestGetModelsUrl:
"""Test that get_models() constructs the correct /v1/models URL from api_base."""
def _get_url_for_api_base(self, api_base: str) -> str:
"""Call get_models() with a mocked HTTP client and return the URL it used."""
mock_response = MagicMock()
mock_response.status_code = 200
mock_response.json.return_value = {"data": []}
with patch("litellm.module_level_client.get", return_value=mock_response) as mock_get:
OpenAIGPTConfig().get_models(api_key="fake-key", api_base=api_base)
return mock_get.call_args.kwargs["url"]
def test_plain_base_url(self):
"""Standard OpenAI base URL without path should get /v1/models appended."""
assert self._get_url_for_api_base("https://api.openai.com") == "https://api.openai.com/v1/models"
def test_trailing_slash(self):
"""Trailing slash should be stripped before appending /v1/models."""
assert self._get_url_for_api_base("https://api.openai.com/") == "https://api.openai.com/v1/models"
def test_v1_already_present(self):
"""api_base ending in /v1 should only get /models appended, not /v1/models."""
assert self._get_url_for_api_base("https://api.openai.com/v1") == "https://api.openai.com/v1/models"
def test_v1_with_trailing_slash(self):
"""api_base ending in /v1/ should be normalized and get /models appended."""
assert self._get_url_for_api_base("https://api.openai.com/v1/") == "https://api.openai.com/v1/models"
def test_subpath_with_v1(self):
"""api_base with a sub-path ending in /v1 should preserve the full path."""
assert self._get_url_for_api_base("https://opencode.ai/zen/v1") == "https://opencode.ai/zen/v1/models"
def test_subpath_with_v1_trailing_slash(self):
"""Sub-path with /v1/ should be normalized and get /models appended."""
assert self._get_url_for_api_base("https://opencode.ai/zen/v1/") == "https://opencode.ai/zen/v1/models"
def test_subpath_without_v1(self):
"""Sub-path without /v1 should get /v1/models appended."""
assert self._get_url_for_api_base("https://opencode.ai/zen") == "https://opencode.ai/zen/v1/models"
def test_localhost_with_port_and_v1(self):
"""Localhost with port and /v1 should only get /models appended."""
assert self._get_url_for_api_base("http://localhost:11434/v1") == "http://localhost:11434/v1/models"
def test_localhost_without_v1(self):
"""Localhost with port but no /v1 should get /v1/models appended."""
assert self._get_url_for_api_base("http://localhost:11434") == "http://localhost:11434/v1/models"