fix: answer get_api_base for github_copilot and chatgpt without running the login flow (#42602)

* fix: answer get_api_base for github_copilot and chatgpt without running the login flow

* refactor(get_api_base): dispatch the provider helpers with if-chains

---------

Co-authored-by: mateo-berri <277851410+mateo-berri@users.noreply.github.com>
This commit is contained in:
devin-ai-integration[bot] 2026-09-22 17:23:44 -07:00 • committed by GitHub
parent 9bad2c35e5
commit ca95fc2bd4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 134 additions and 22 deletions

View file

@ -3,10 +3,30 @@ from typing import Final
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:
if provider == "github_copilot":
return litellm.GithubCopilotConfig().api_base_without_login()
if provider == "chatgpt":
return litellm.ChatGPTConfig().api_base_without_login()
return None
def _provider_default_api_base(model: str, custom_llm_provider: str | None, stream: bool) -> str | None:
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.
@ -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,
@ -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)

View file

@ -23,6 +23,9 @@ class ChatGPTConfig(OpenAIConfig):
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,
@ -30,7 +33,7 @@ class ChatGPTConfig(OpenAIConfig):
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:

View file

@ -31,6 +31,14 @@ class GithubCopilotConfig(OpenAIConfig):
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,
@ -38,12 +46,7 @@ class GithubCopilotConfig(OpenAIConfig):
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:

View file

@ -0,0 +1,93 @@
import json
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

View file

@ -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,
@ -257,6 +256,9 @@ async def test_arerank_declared_authenticating_provider_skips_resolution(monkeyp
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: