Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 25 additions & 14 deletions litellm/litellm_core_utils/llm_response_utils/get_api_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,30 @@
import litellm
from litellm import verbose_logger

from ...litellm_core_utils.get_llm_provider_logic import get_llm_provider
from ...litellm_core_utils.get_llm_provider_logic import (
declared_authenticating_provider,
get_llm_provider,
)
from ...types.router import LiteLLM_Params


def _api_base_without_login(provider: str) -> str | None:
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed
if provider == "github_copilot":
return litellm.GithubCopilotConfig().api_base_without_login()
if provider == "chatgpt":
return litellm.ChatGPTConfig().api_base_without_login()
return None

Comment on lines +13 to +19

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Provider Logic Outside Adapters

This shared utility directly branches on GitHub Copilot and ChatGPT and constructs their configurations. Repository rules require provider-specific logic to remain under litellm/llms/, so this requirement must be satisfied before merging

Rule Used: What: Avoid writing provider-specific code outside of the llms/ directory. Why: This practice ensures better maintainability and reduces complexity over time. Good: ```python # Handle provider-specific logic within llms/vertex_ai/transformation.py ... (source)

Knowledge Base Used: Provider adapters and capabilities

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The provider knowledge lives in each adapter's api_base_without_login under llms/. This util only dispatches on the provider name, like the gemini and openai branch below

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right — this utility only dispatches to each adapter's api_base_without_login(); the provider-specific URL logic remains under litellm/llms/. That is consistent with the existing Gemini/OpenAI default-base dispatch in this file, so the comment does not apply. I'll retract it.


def _provider_default_api_base(model: str, custom_llm_provider: str | None, stream: bool) -> str | None:
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed
if custom_llm_provider == "gemini":
action: Final = "streamGenerateContent" if stream else "generateContent"
return f"https://generativelanguage.googleapis.com/v1beta/models/{model}:{action}"
if custom_llm_provider == "openai":
return "https://api.openai.com"
return None


def get_api_base(model: str, optional_params: dict | LiteLLM_Params) -> str | None:
"""
Returns the api base used for calling the model.
Expand Down Expand Up @@ -42,6 +62,9 @@ def get_api_base(model: str, optional_params: dict | LiteLLM_Params) -> str | No

if litellm.model_alias_map and model in litellm.model_alias_map:
model = litellm.model_alias_map[model]
declared: Final = declared_authenticating_provider(model, _optional_params.custom_llm_provider)
if declared is not None:
return _api_base_without_login(declared)
try:
(
model,
Expand Down Expand Up @@ -83,16 +106,4 @@ def get_api_base(model: str, optional_params: dict | LiteLLM_Params) -> str | No
_api_base = f"{_optional_params.vertex_location}-aiplatform.googleapis.com/v1/projects/{_optional_params.vertex_project}/locations/{_optional_params.vertex_location}/publishers/google/models/{model}:generateContent"
return _api_base

if custom_llm_provider is None:
return None

if custom_llm_provider == "gemini":
if stream:
_api_base = f"https://generativelanguage.googleapis.com/v1beta/models/{model}:streamGenerateContent"
else:
_api_base = f"https://generativelanguage.googleapis.com/v1beta/models/{model}:generateContent"
return _api_base
elif custom_llm_provider == "openai":
_api_base = "https://api.openai.com"
return _api_base
return None
return _provider_default_api_base(model, custom_llm_provider, stream)
5 changes: 4 additions & 1 deletion litellm/llms/chatgpt/chat/transformation.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,17 @@ def __init__(
super().__init__()
self.authenticator = Authenticator()

def api_base_without_login(self) -> str:
return self.authenticator.get_api_base()

def _get_openai_compatible_provider_info(
self,
model: str,
api_base: str | None,
api_key: str | None,
custom_llm_provider: str,
) -> tuple[str | None, str | None, str]:
dynamic_api_base: Final = self.authenticator.get_api_base()
dynamic_api_base: Final = self.api_base_without_login()
try:
dynamic_api_key: Final = self.authenticator.get_access_token()
except GetAccessTokenError as e:
Expand Down
15 changes: 9 additions & 6 deletions litellm/llms/github_copilot/chat/transformation.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,19 +31,22 @@ def __init__(
super().__init__()
self.authenticator = Authenticator()

def api_base_without_login(self, api_base: str | None = None) -> str:
return (
api_base
or self.authenticator.get_api_base()
or os.getenv("GITHUB_COPILOT_API_BASE")
or DEFAULT_GITHUB_COPILOT_API_BASE
)

def _get_openai_compatible_provider_info(
self,
model: str,
api_base: str | None,
api_key: str | None,
custom_llm_provider: str,
) -> tuple[str | None, str | None, str]:
dynamic_api_base: Final = (
api_base
or self.authenticator.get_api_base()
or os.getenv("GITHUB_COPILOT_API_BASE")
or DEFAULT_GITHUB_COPILOT_API_BASE
)
dynamic_api_base: Final = self.api_base_without_login(api_base)
try:
dynamic_api_key: Final = self.authenticator.get_api_key()
except GetAPIKeyError as e:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import json
Comment thread
greptile-apps[bot] marked this conversation as resolved.
Resolved

import pytest

import litellm
from litellm.litellm_core_utils.llm_response_utils import get_api_base as get_api_base_module
from litellm.llms.chatgpt.common_utils import CHATGPT_API_BASE
from litellm.llms.github_copilot.common_utils import DEFAULT_GITHUB_COPILOT_API_BASE


@pytest.fixture
def isolated_token_dirs(tmp_path, monkeypatch):
monkeypatch.setenv("GITHUB_COPILOT_TOKEN_DIR", str(tmp_path / "github_copilot"))
monkeypatch.setenv("CHATGPT_TOKEN_DIR", str(tmp_path / "chatgpt"))
monkeypatch.delenv("GITHUB_COPILOT_API_BASE", raising=False)
monkeypatch.delenv("CHATGPT_API_BASE", raising=False)
monkeypatch.delenv("OPENAI_CHATGPT_API_BASE", raising=False)
return tmp_path


@pytest.fixture
def resolution_lookups(monkeypatch):
lookups: list = []

def _record(*args, **kwargs):
lookups.append((args, kwargs))
raise RuntimeError("provider resolution must not run for an authenticating provider")

monkeypatch.setattr(get_api_base_module, "get_llm_provider", _record)
return lookups


class TestDeclaredAuthenticatingProvider:
"""get_llm_provider runs the OAuth device flow for github_copilot and chatgpt, and get_api_base
runs on every response's hidden params and on every mapped exception, so it must answer from
the declaration without resolving. The recorder appends before raising, and get_api_base
swallows resolver errors, so an empty list proves the lookup never ran."""

@pytest.mark.parametrize(
"model, custom_llm_provider, expected",
[
("github_copilot/gpt-4o", None, DEFAULT_GITHUB_COPILOT_API_BASE),
("gpt-4o", "github_copilot", DEFAULT_GITHUB_COPILOT_API_BASE),
("chatgpt/gpt-5", None, CHATGPT_API_BASE),
("gpt-5", "chatgpt", CHATGPT_API_BASE),
],
)
def test_answers_without_resolving(
self, model, custom_llm_provider, expected, isolated_token_dirs, resolution_lookups
):
api_base = litellm.get_api_base(model=model, optional_params={"custom_llm_provider": custom_llm_provider})

assert resolution_lookups == []
assert api_base == expected

def test_copilot_keeps_the_enterprise_endpoint_from_disk(self, isolated_token_dirs, resolution_lookups):
token_dir = isolated_token_dirs / "github_copilot"
token_dir.mkdir()
(token_dir / "api-key.json").write_text(
json.dumps({"endpoints": {"api": "https://api.enterprise.githubcopilot.com"}})
)

api_base = litellm.get_api_base(model="github_copilot/gpt-4o", optional_params={})

assert resolution_lookups == []
assert api_base == "https://api.enterprise.githubcopilot.com"

def test_explicit_api_base_still_wins(self, isolated_token_dirs, resolution_lookups):
api_base = litellm.get_api_base(
model="github_copilot/gpt-4o", optional_params={"api_base": "https://copilot.example/v1"}
)

assert resolution_lookups == []
assert api_base == "https://copilot.example/v1"

def test_other_providers_still_resolve(self, isolated_token_dirs, resolution_lookups):
litellm.get_api_base(model="openai/gpt-4o", optional_params={})

assert len(resolution_lookups) == 1


@pytest.mark.parametrize(
"model, expected",
[
("gemini/gemini-2.5-pro", "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-pro:generateContent"),
("openai/gpt-4o", "https://api.openai.com"),
],
)
def test_providers_with_a_fixed_base_still_get_it(model, expected, monkeypatch):
for env in ("GEMINI_API_BASE", "OPENAI_API_BASE", "OPENAI_BASE_URL"):
monkeypatch.delenv(env, raising=False)

assert litellm.get_api_base(model=model, optional_params={}) == expected
4 changes: 3 additions & 1 deletion tests/test_litellm/rerank_api/test_main.py
Original file line number Diff line number Diff line change
Expand Up @@ -239,7 +239,6 @@ async def test_arerank_error_is_mapped_to_litellm_exception(respx_mock: respx.Mo


@pytest.mark.asyncio
@pytest.mark.timeout(300)
async def test_arerank_declared_authenticating_provider_skips_resolution(monkeypatch):
"""Regression for the event-loop hazard in arerank's provider pre-resolution:
get_llm_provider runs the blocking OAuth device flow for github_copilot/chatgpt,
Expand All @@ -257,6 +256,9 @@ def rerank_raises_provider_error(*args, **kwargs):
raise BaseLLMException(status_code=401, message='{"error":"bad key"}')

monkeypatch.setattr(litellm, "get_llm_provider", record_resolution)
monkeypatch.setattr(
"litellm.litellm_core_utils.llm_response_utils.get_api_base.get_llm_provider", record_resolution
)
monkeypatch.setattr("litellm.rerank_api.main.rerank", rerank_raises_provider_error)

with pytest.raises(litellm.AuthenticationError) as exc_info:
Expand Down
Loading