From 9bc2342572199f8db2d8488c32aa372d932aefec Mon Sep 17 00:00:00 2001 From: mateo Date: Thu, 3 Sep 2026 17:00:07 +0000 Subject: [PATCH 1/6] fix(chatgpt,github_copilot): refuse device-code login when an event loop is running Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- litellm/litellm_core_utils/asyncify.py | 8 ++++ litellm/llms/chatgpt/authenticator.py | 13 ++++++ litellm/llms/github_copilot/authenticator.py | 14 +++++++ .../chatgpt/test_chatgpt_authenticator.py | 42 +++++++++++++++++++ .../test_github_copilot_authenticator.py | 15 ++++++- 5 files changed, 91 insertions(+), 1 deletion(-) diff --git a/litellm/litellm_core_utils/asyncify.py b/litellm/litellm_core_utils/asyncify.py index b58e707b8f8..bd21974029e 100644 --- a/litellm/litellm_core_utils/asyncify.py +++ b/litellm/litellm_core_utils/asyncify.py @@ -68,6 +68,14 @@ def asyncify( return wrapper +def is_event_loop_running() -> bool: + try: + _ = asyncio.get_running_loop() + except RuntimeError: + return False + return True + + def run_async_function(async_function, *args, **kwargs): """ Helper utility to run an async function in a sync context. diff --git a/litellm/llms/chatgpt/authenticator.py b/litellm/llms/chatgpt/authenticator.py index 563826c2b93..1b5850d8d42 100644 --- a/litellm/llms/chatgpt/authenticator.py +++ b/litellm/llms/chatgpt/authenticator.py @@ -9,6 +9,7 @@ import httpx from pydantic import JsonValue, TypeAdapter, ValidationError from litellm._logging import verbose_logger +from litellm.litellm_core_utils.asyncify import is_event_loop_running from litellm.llms.custom_httpx.http_handler import _get_httpx_client from .common_utils import ( @@ -66,6 +67,18 @@ class Authenticator: except RefreshAccessTokenError as exc: verbose_logger.warning("ChatGPT refresh token failed, re-login required: %s", exc) + if is_event_loop_running(): + raise GetAccessTokenError( + message=( + "ChatGPT device-code login needs a human and cannot run inside a running event loop " + "(for example the LiteLLM proxy). Log in once outside the proxy with " + '`python -c "from litellm.llms.chatgpt.authenticator import Authenticator; ' + 'Authenticator().get_access_token()"` and mount the resulting ' + f"{self.auth_file} into the proxy, or set CHATGPT_TOKEN_DIR to a directory that already holds it." + ), + status_code=401, + ) + cooldown_remaining: Final = self._get_device_code_cooldown_remaining(auth_data) if cooldown_remaining > 0: token: Final = self._wait_for_access_token(cooldown_remaining) diff --git a/litellm/llms/github_copilot/authenticator.py b/litellm/llms/github_copilot/authenticator.py index 80fd4f755e7..269eaaa88b6 100644 --- a/litellm/llms/github_copilot/authenticator.py +++ b/litellm/llms/github_copilot/authenticator.py @@ -7,6 +7,7 @@ from typing import Any, Final import httpx from litellm._logging import verbose_logger +from litellm.litellm_core_utils.asyncify import is_event_loop_running from litellm.llms.custom_httpx.http_handler import _get_httpx_client from .common_utils import ( @@ -57,6 +58,19 @@ class Authenticator: except OSError: verbose_logger.warning("No existing access token found or error reading file") + if is_event_loop_running(): + raise GetAccessTokenError( + message=( + "GitHub Copilot device-code login needs a human and cannot run inside a running event loop " + "(for example the LiteLLM proxy). Log in once outside the proxy with " + '`python -c "from litellm.llms.github_copilot.authenticator import Authenticator; ' + 'Authenticator().get_access_token()"` and mount the resulting ' + f"{self.access_token_file} into the proxy, or set GITHUB_COPILOT_TOKEN_DIR to a directory " + "that already holds it." + ), + status_code=401, + ) + for attempt in range(3): verbose_logger.debug("Access token acquisition attempt %s/3", attempt + 1) try: diff --git a/tests/test_litellm/llms/chatgpt/test_chatgpt_authenticator.py b/tests/test_litellm/llms/chatgpt/test_chatgpt_authenticator.py index a9ced2afcf9..be54d33d18d 100644 --- a/tests/test_litellm/llms/chatgpt/test_chatgpt_authenticator.py +++ b/tests/test_litellm/llms/chatgpt/test_chatgpt_authenticator.py @@ -6,6 +6,7 @@ from unittest.mock import mock_open, patch import pytest from litellm.llms.chatgpt.authenticator import Authenticator +from litellm.llms.chatgpt.common_utils import GetAccessTokenError def _make_jwt(payload: dict) -> str: @@ -54,6 +55,47 @@ class TestChatGPTAuthenticator: token = authenticator.get_access_token() assert token == "token-new" + @pytest.mark.asyncio + async def test_get_access_token_refuses_device_code_login_in_event_loop(self, authenticator): + with ( + patch("builtins.open", side_effect=FileNotFoundError), + patch.object(authenticator, "_login_device_code") as mock_login, + patch.object(authenticator, "_wait_for_access_token") as mock_wait, + ): + with pytest.raises(GetAccessTokenError) as exc: + authenticator.get_access_token() + + assert exc.value.status_code == 401 + assert "event loop" in str(exc.value) + mock_login.assert_not_called() + mock_wait.assert_not_called() + + @pytest.mark.asyncio + async def test_get_access_token_refuses_cooldown_wait_in_event_loop(self, authenticator): + auth_data = json.dumps({"device_code_requested_at": time.time()}) + + with ( + patch("builtins.open", mock_open(read_data=auth_data)), + patch.object(authenticator, "_login_device_code") as mock_login, + patch.object(authenticator, "_wait_for_access_token") as mock_wait, + ): + with pytest.raises(GetAccessTokenError) as exc: + authenticator.get_access_token() + + assert exc.value.status_code == 401 + assert "event loop" in str(exc.value) + mock_login.assert_not_called() + mock_wait.assert_not_called() + + def test_get_access_token_device_code_login_without_event_loop(self, authenticator): + with ( + patch("builtins.open", side_effect=FileNotFoundError), + patch.object(authenticator, "_login_device_code", return_value={"access_token": "tok"}), + ): + token = authenticator.get_access_token() + + assert token == "tok" + def test_get_account_id_from_id_token(self, authenticator): id_token = _make_jwt( {"https://api.openai.com/auth": {"chatgpt_account_id": "acct-123"}} diff --git a/tests/test_litellm/llms/github_copilot/test_github_copilot_authenticator.py b/tests/test_litellm/llms/github_copilot/test_github_copilot_authenticator.py index 6c846a90c71..3fd0487b3ac 100644 --- a/tests/test_litellm/llms/github_copilot/test_github_copilot_authenticator.py +++ b/tests/test_litellm/llms/github_copilot/test_github_copilot_authenticator.py @@ -89,6 +89,19 @@ class TestGitHubCopilotAuthenticator: assert token == mock_token authenticator._login.assert_called_once() + @pytest.mark.asyncio + async def test_get_access_token_refuses_device_code_login_in_event_loop(self, authenticator): + with ( + patch("builtins.open", side_effect=FileNotFoundError), + patch.object(authenticator, "_login") as mock_login, + ): + with pytest.raises(GetAccessTokenError) as exc: + authenticator.get_access_token() + + assert exc.value.status_code == 401 + assert "event loop" in str(exc.value) + mock_login.assert_not_called() + def test_get_access_token_failure(self, authenticator): """Test that an exception is raised after multiple login failures.""" with ( @@ -305,5 +318,5 @@ class TestGitHubCopilotAuthenticator: patch("litellm.llms.github_copilot.authenticator._get_httpx_client", return_value=mock_client), \ patch.object(authenticator, "get_access_token", return_value="access-tok"): authenticator._refresh_api_key() - assert mock_client.get.call_args[0][0] == custom_url + assert mock_client.get.call_args[0][0] == custom_url From 220a77a4917b575557fa9747e57d1817b6cc400b Mon Sep 17 00:00:00 2001 From: mateo Date: Thu, 3 Sep 2026 17:00:56 +0000 Subject: [PATCH 2/6] test(github_copilot): drop stray whitespace change Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../llms/github_copilot/test_github_copilot_authenticator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_litellm/llms/github_copilot/test_github_copilot_authenticator.py b/tests/test_litellm/llms/github_copilot/test_github_copilot_authenticator.py index 3fd0487b3ac..a1f72b541f7 100644 --- a/tests/test_litellm/llms/github_copilot/test_github_copilot_authenticator.py +++ b/tests/test_litellm/llms/github_copilot/test_github_copilot_authenticator.py @@ -318,5 +318,5 @@ class TestGitHubCopilotAuthenticator: patch("litellm.llms.github_copilot.authenticator._get_httpx_client", return_value=mock_client), \ patch.object(authenticator, "get_access_token", return_value="access-tok"): authenticator._refresh_api_key() - assert mock_client.get.call_args[0][0] == custom_url + From 3b4903b628bea004a5fac5895548bfe8ff233913 Mon Sep 17 00:00:00 2001 From: mateo Date: Thu, 3 Sep 2026 17:16:57 +0000 Subject: [PATCH 3/6] fix(chatgpt): bound token refresh timeout and drop placeholder assignment Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- litellm/litellm_core_utils/asyncify.py | 2 +- litellm/llms/chatgpt/authenticator.py | 2 ++ .../chatgpt/test_chatgpt_authenticator.py | 28 +++++++++++++++---- 3 files changed, 26 insertions(+), 6 deletions(-) diff --git a/litellm/litellm_core_utils/asyncify.py b/litellm/litellm_core_utils/asyncify.py index bd21974029e..695312ee234 100644 --- a/litellm/litellm_core_utils/asyncify.py +++ b/litellm/litellm_core_utils/asyncify.py @@ -70,7 +70,7 @@ def asyncify( def is_event_loop_running() -> bool: try: - _ = asyncio.get_running_loop() + asyncio.get_running_loop() except RuntimeError: return False return True diff --git a/litellm/llms/chatgpt/authenticator.py b/litellm/llms/chatgpt/authenticator.py index 1b5850d8d42..244083e92e9 100644 --- a/litellm/llms/chatgpt/authenticator.py +++ b/litellm/llms/chatgpt/authenticator.py @@ -26,6 +26,7 @@ from .common_utils import ( ) TOKEN_EXPIRY_SKEW_SECONDS: Final = 60 +TOKEN_REFRESH_TIMEOUT_SECONDS: Final = 30 DEVICE_CODE_TIMEOUT_SECONDS: Final = 15 * 60 DEVICE_CODE_COOLDOWN_SECONDS: Final = 5 * 60 DEVICE_CODE_POLL_SLEEP_SECONDS: Final = 5 @@ -322,6 +323,7 @@ class Authenticator: "refresh_token": refresh_token, "scope": "openid profile email", }, + timeout=TOKEN_REFRESH_TIMEOUT_SECONDS, ) resp.raise_for_status() data: Final = _JSON_OBJECT_ADAPTER.validate_python(resp.json()) diff --git a/tests/test_litellm/llms/chatgpt/test_chatgpt_authenticator.py b/tests/test_litellm/llms/chatgpt/test_chatgpt_authenticator.py index be54d33d18d..982d4807caf 100644 --- a/tests/test_litellm/llms/chatgpt/test_chatgpt_authenticator.py +++ b/tests/test_litellm/llms/chatgpt/test_chatgpt_authenticator.py @@ -1,11 +1,14 @@ import base64 import json import time -from unittest.mock import mock_open, patch +from unittest.mock import MagicMock, mock_open, patch import pytest -from litellm.llms.chatgpt.authenticator import Authenticator +from litellm.llms.chatgpt.authenticator import ( + TOKEN_REFRESH_TIMEOUT_SECONDS, + Authenticator, +) from litellm.llms.chatgpt.common_utils import GetAccessTokenError @@ -55,6 +58,23 @@ class TestChatGPTAuthenticator: token = authenticator.get_access_token() assert token == "token-new" + def test_refresh_tokens_uses_bounded_timeout(self, authenticator): + client = MagicMock() + response = MagicMock() + response.json.return_value = { + "access_token": "token-new", + "id_token": "id-123", + } + client.post.return_value = response + + with patch( # test-quality-ok: requested seam for asserting timeout propagation + "litellm.llms.chatgpt.authenticator._get_httpx_client", return_value=client + ): + refreshed = authenticator._refresh_tokens("refresh-123") + + assert refreshed["access_token"] == "token-new" + assert client.post.call_args.kwargs["timeout"] == TOKEN_REFRESH_TIMEOUT_SECONDS + @pytest.mark.asyncio async def test_get_access_token_refuses_device_code_login_in_event_loop(self, authenticator): with ( @@ -97,9 +117,7 @@ class TestChatGPTAuthenticator: assert token == "tok" def test_get_account_id_from_id_token(self, authenticator): - id_token = _make_jwt( - {"https://api.openai.com/auth": {"chatgpt_account_id": "acct-123"}} - ) + id_token = _make_jwt({"https://api.openai.com/auth": {"chatgpt_account_id": "acct-123"}}) auth_data = json.dumps({"id_token": id_token}) with ( From 133d0b70720d925c28bcd38c1e8dde24a2b96f5e Mon Sep 17 00:00:00 2001 From: mateo-berri <277851410+mateo-berri@users.noreply.github.com> Date: Thu, 3 Sep 2026 11:29:14 -0700 Subject: [PATCH 4/6] fix(chatgpt,github_copilot): keep the token file path out of the event-loop 401 message --- litellm/llms/chatgpt/authenticator.py | 4 ++-- litellm/llms/github_copilot/authenticator.py | 5 ++--- .../test_litellm/llms/chatgpt/test_chatgpt_authenticator.py | 2 ++ .../llms/github_copilot/test_github_copilot_authenticator.py | 1 + 4 files changed, 7 insertions(+), 5 deletions(-) diff --git a/litellm/llms/chatgpt/authenticator.py b/litellm/llms/chatgpt/authenticator.py index 244083e92e9..eb8a8aabdbc 100644 --- a/litellm/llms/chatgpt/authenticator.py +++ b/litellm/llms/chatgpt/authenticator.py @@ -74,8 +74,8 @@ class Authenticator: "ChatGPT device-code login needs a human and cannot run inside a running event loop " "(for example the LiteLLM proxy). Log in once outside the proxy with " '`python -c "from litellm.llms.chatgpt.authenticator import Authenticator; ' - 'Authenticator().get_access_token()"` and mount the resulting ' - f"{self.auth_file} into the proxy, or set CHATGPT_TOKEN_DIR to a directory that already holds it." + 'Authenticator().get_access_token()"` and mount the resulting auth.json into the proxy, ' + "or set CHATGPT_TOKEN_DIR to a directory that already holds it." ), status_code=401, ) diff --git a/litellm/llms/github_copilot/authenticator.py b/litellm/llms/github_copilot/authenticator.py index 269eaaa88b6..1a51fd88608 100644 --- a/litellm/llms/github_copilot/authenticator.py +++ b/litellm/llms/github_copilot/authenticator.py @@ -64,9 +64,8 @@ class Authenticator: "GitHub Copilot device-code login needs a human and cannot run inside a running event loop " "(for example the LiteLLM proxy). Log in once outside the proxy with " '`python -c "from litellm.llms.github_copilot.authenticator import Authenticator; ' - 'Authenticator().get_access_token()"` and mount the resulting ' - f"{self.access_token_file} into the proxy, or set GITHUB_COPILOT_TOKEN_DIR to a directory " - "that already holds it." + 'Authenticator().get_access_token()"` and mount the resulting access-token file into ' + "the proxy, or set GITHUB_COPILOT_TOKEN_DIR to a directory that already holds it." ), status_code=401, ) diff --git a/tests/test_litellm/llms/chatgpt/test_chatgpt_authenticator.py b/tests/test_litellm/llms/chatgpt/test_chatgpt_authenticator.py index 982d4807caf..6c3c8adc948 100644 --- a/tests/test_litellm/llms/chatgpt/test_chatgpt_authenticator.py +++ b/tests/test_litellm/llms/chatgpt/test_chatgpt_authenticator.py @@ -87,6 +87,7 @@ class TestChatGPTAuthenticator: assert exc.value.status_code == 401 assert "event loop" in str(exc.value) + assert authenticator.auth_file not in str(exc.value) mock_login.assert_not_called() mock_wait.assert_not_called() @@ -104,6 +105,7 @@ class TestChatGPTAuthenticator: assert exc.value.status_code == 401 assert "event loop" in str(exc.value) + assert authenticator.auth_file not in str(exc.value) mock_login.assert_not_called() mock_wait.assert_not_called() diff --git a/tests/test_litellm/llms/github_copilot/test_github_copilot_authenticator.py b/tests/test_litellm/llms/github_copilot/test_github_copilot_authenticator.py index a1f72b541f7..98914fa4840 100644 --- a/tests/test_litellm/llms/github_copilot/test_github_copilot_authenticator.py +++ b/tests/test_litellm/llms/github_copilot/test_github_copilot_authenticator.py @@ -100,6 +100,7 @@ class TestGitHubCopilotAuthenticator: assert exc.value.status_code == 401 assert "event loop" in str(exc.value) + assert authenticator.access_token_file not in str(exc.value) mock_login.assert_not_called() def test_get_access_token_failure(self, authenticator): From 29baf5a6bdd8a9dbf63056e398c96db6b0a989d0 Mon Sep 17 00:00:00 2001 From: mateo-berri <277851410+mateo-berri@users.noreply.github.com> Date: Thu, 3 Sep 2026 11:43:18 -0700 Subject: [PATCH 5/6] fix(auth): refuse device-code login from worker threads too /v1/messages runs its handler in an executor thread, where the running-loop check never fires, so a chatgpt or github_copilot model still started the interactive device-code login there and the request hung for up to 15 minutes. The guard now also requires the main thread, so the login only runs where a human can actually answer it. --- litellm/litellm_core_utils/asyncify.py | 5 +++++ litellm/llms/chatgpt/authenticator.py | 6 +++--- litellm/llms/github_copilot/authenticator.py | 6 +++--- .../llms/chatgpt/test_chatgpt_authenticator.py | 17 +++++++++++++++++ .../test_github_copilot_authenticator.py | 15 +++++++++++++++ 5 files changed, 43 insertions(+), 6 deletions(-) diff --git a/litellm/litellm_core_utils/asyncify.py b/litellm/litellm_core_utils/asyncify.py index 695312ee234..17cfb3ef84f 100644 --- a/litellm/litellm_core_utils/asyncify.py +++ b/litellm/litellm_core_utils/asyncify.py @@ -1,5 +1,6 @@ import asyncio import functools +import threading from collections.abc import Awaitable, Callable from typing import Final @@ -76,6 +77,10 @@ def is_event_loop_running() -> bool: return True +def can_block_current_thread() -> bool: + return threading.current_thread() is threading.main_thread() and not is_event_loop_running() + + def run_async_function(async_function, *args, **kwargs): """ Helper utility to run an async function in a sync context. diff --git a/litellm/llms/chatgpt/authenticator.py b/litellm/llms/chatgpt/authenticator.py index eb8a8aabdbc..c27bb83437e 100644 --- a/litellm/llms/chatgpt/authenticator.py +++ b/litellm/llms/chatgpt/authenticator.py @@ -9,7 +9,7 @@ import httpx from pydantic import JsonValue, TypeAdapter, ValidationError from litellm._logging import verbose_logger -from litellm.litellm_core_utils.asyncify import is_event_loop_running +from litellm.litellm_core_utils.asyncify import can_block_current_thread from litellm.llms.custom_httpx.http_handler import _get_httpx_client from .common_utils import ( @@ -68,11 +68,11 @@ class Authenticator: except RefreshAccessTokenError as exc: verbose_logger.warning("ChatGPT refresh token failed, re-login required: %s", exc) - if is_event_loop_running(): + if not can_block_current_thread(): raise GetAccessTokenError( message=( "ChatGPT device-code login needs a human and cannot run inside a running event loop " - "(for example the LiteLLM proxy). Log in once outside the proxy with " + "or a worker thread (for example the LiteLLM proxy). Log in once outside the proxy with " '`python -c "from litellm.llms.chatgpt.authenticator import Authenticator; ' 'Authenticator().get_access_token()"` and mount the resulting auth.json into the proxy, ' "or set CHATGPT_TOKEN_DIR to a directory that already holds it." diff --git a/litellm/llms/github_copilot/authenticator.py b/litellm/llms/github_copilot/authenticator.py index 1a51fd88608..7821756bc16 100644 --- a/litellm/llms/github_copilot/authenticator.py +++ b/litellm/llms/github_copilot/authenticator.py @@ -7,7 +7,7 @@ from typing import Any, Final import httpx from litellm._logging import verbose_logger -from litellm.litellm_core_utils.asyncify import is_event_loop_running +from litellm.litellm_core_utils.asyncify import can_block_current_thread from litellm.llms.custom_httpx.http_handler import _get_httpx_client from .common_utils import ( @@ -58,11 +58,11 @@ class Authenticator: except OSError: verbose_logger.warning("No existing access token found or error reading file") - if is_event_loop_running(): + if not can_block_current_thread(): raise GetAccessTokenError( message=( "GitHub Copilot device-code login needs a human and cannot run inside a running event loop " - "(for example the LiteLLM proxy). Log in once outside the proxy with " + "or a worker thread (for example the LiteLLM proxy). Log in once outside the proxy with " '`python -c "from litellm.llms.github_copilot.authenticator import Authenticator; ' 'Authenticator().get_access_token()"` and mount the resulting access-token file into ' "the proxy, or set GITHUB_COPILOT_TOKEN_DIR to a directory that already holds it." diff --git a/tests/test_litellm/llms/chatgpt/test_chatgpt_authenticator.py b/tests/test_litellm/llms/chatgpt/test_chatgpt_authenticator.py index 6c3c8adc948..861404e9157 100644 --- a/tests/test_litellm/llms/chatgpt/test_chatgpt_authenticator.py +++ b/tests/test_litellm/llms/chatgpt/test_chatgpt_authenticator.py @@ -1,6 +1,7 @@ import base64 import json import time +from concurrent.futures import ThreadPoolExecutor from unittest.mock import MagicMock, mock_open, patch import pytest @@ -109,6 +110,22 @@ class TestChatGPTAuthenticator: mock_login.assert_not_called() mock_wait.assert_not_called() + def test_get_access_token_refuses_device_code_login_in_worker_thread(self, authenticator): + with ( + patch("builtins.open", side_effect=FileNotFoundError), + patch.object(authenticator, "_login_device_code") as mock_login, + patch.object(authenticator, "_wait_for_access_token") as mock_wait, + ): + with ThreadPoolExecutor(max_workers=1) as pool: + with pytest.raises(GetAccessTokenError) as exc: + pool.submit(authenticator.get_access_token).result() + + assert exc.value.status_code == 401 + assert "worker thread" in str(exc.value) + assert authenticator.auth_file not in str(exc.value) + mock_login.assert_not_called() + mock_wait.assert_not_called() + def test_get_access_token_device_code_login_without_event_loop(self, authenticator): with ( patch("builtins.open", side_effect=FileNotFoundError), diff --git a/tests/test_litellm/llms/github_copilot/test_github_copilot_authenticator.py b/tests/test_litellm/llms/github_copilot/test_github_copilot_authenticator.py index 98914fa4840..a49a4b44b74 100644 --- a/tests/test_litellm/llms/github_copilot/test_github_copilot_authenticator.py +++ b/tests/test_litellm/llms/github_copilot/test_github_copilot_authenticator.py @@ -1,6 +1,7 @@ import json import os import time +from concurrent.futures import ThreadPoolExecutor from datetime import datetime, timedelta from unittest.mock import MagicMock, mock_open, patch @@ -103,6 +104,20 @@ class TestGitHubCopilotAuthenticator: assert authenticator.access_token_file not in str(exc.value) mock_login.assert_not_called() + def test_get_access_token_refuses_device_code_login_in_worker_thread(self, authenticator): + with ( + patch("builtins.open", side_effect=FileNotFoundError), + patch.object(authenticator, "_login") as mock_login, + ): + with ThreadPoolExecutor(max_workers=1) as pool: + with pytest.raises(GetAccessTokenError) as exc: + pool.submit(authenticator.get_access_token).result() + + assert exc.value.status_code == 401 + assert "worker thread" in str(exc.value) + assert authenticator.access_token_file not in str(exc.value) + mock_login.assert_not_called() + def test_get_access_token_failure(self, authenticator): """Test that an exception is raised after multiple login failures.""" with ( From 94f34b70323c55cec9869f9b8e557d6d8199879b Mon Sep 17 00:00:00 2001 From: mateo-berri <277851410+mateo-berri@users.noreply.github.com> Date: Thu, 3 Sep 2026 11:57:04 -0700 Subject: [PATCH 6/6] test(chatgpt): keep authenticator tests out of the real token directory --- .../test_litellm/llms/chatgpt/test_chatgpt_authenticator.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test_litellm/llms/chatgpt/test_chatgpt_authenticator.py b/tests/test_litellm/llms/chatgpt/test_chatgpt_authenticator.py index 861404e9157..833220e8a67 100644 --- a/tests/test_litellm/llms/chatgpt/test_chatgpt_authenticator.py +++ b/tests/test_litellm/llms/chatgpt/test_chatgpt_authenticator.py @@ -25,9 +25,9 @@ def _make_jwt(payload: dict) -> str: class TestChatGPTAuthenticator: @pytest.fixture - def authenticator(self): - with patch("os.path.exists", return_value=True): - return Authenticator() + def authenticator(self, tmp_path, monkeypatch): + monkeypatch.setenv("CHATGPT_TOKEN_DIR", str(tmp_path)) + return Authenticator() def test_get_access_token_from_file(self, authenticator): future_time = time.time() + 3600