Repository navigation
fix: answer get_api_base for github_copilot and chatgpt without running the login flow #42602
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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: | ||
| 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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
This shared utility directly branches on GitHub Copilot and ChatGPT and constructs their configurations. Repository rules require provider-specific logic to remain under 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!
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You're right — this utility only dispatches to each adapter's |
||
|
|
||
| def _provider_default_api_base(model: str, custom_llm_provider: str | None, stream: bool) -> str | None: | ||
|
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. | ||
|
|
@@ -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) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,93 @@ | ||
| import json | ||
|
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 | ||
Uh oh!
There was an error while loading. Please reload this page.