mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-24 00:52:24 +00:00
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:
parent
9bad2c35e5
commit
ca95fc2bd4
5 changed files with 134 additions and 22 deletions
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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:
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue