mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-24 00:52:24 +00:00
fix(oauth): don't eager-resolve tokens during add_deployment cycles
``get_llm_provider_logic.py`` instantiates ``ChatGPTConfig`` / ``GithubCopilotConfig`` and calls ``_get_openai_compatible_provider_info`` at every ``add_deployment`` cycle during proxy startup (30s tick when ``STORE_MODEL_IN_DB=True``). The old code eagerly called ``get_access_token()`` / ``get_api_key()`` through the filesystem authenticator at that point. With no tokens on disk this falls through to ``_login_device_code()``, which prints a device prompt to stdout and polls the IdP for up to 15 minutes — blocking startup and (for OAuth credentials stored in the DB) duplicating work that ``validate_environment`` is about to do correctly at request time. Resolution is now a pure metadata pass: pick an ``api_base``, let the ``oauth:<name>`` marker pass through untouched. Actual token resolution still happens at request time via ``resolve_authenticator`` inside ``validate_environment``, which is where we have the full ``litellm_params`` anyway. Tests updated to assert ``get_access_token`` / ``get_api_key`` are NOT called during resolution, and that the ``oauth:`` marker passes through.
This commit is contained in:
parent
2d70e7ba82
commit
f2653df86b
5 changed files with 174 additions and 163 deletions
|
|
@ -1,12 +1,10 @@
|
|||
from typing import Any, List, Optional, Tuple
|
||||
|
||||
from litellm.exceptions import AuthenticationError
|
||||
from litellm.llms.openai.openai import OpenAIConfig
|
||||
from litellm.types.llms.openai import AllMessageValues
|
||||
|
||||
from ..authenticator import Authenticator
|
||||
from ..common_utils import (
|
||||
GetAccessTokenError,
|
||||
ensure_chatgpt_session_id,
|
||||
get_chatgpt_default_headers,
|
||||
)
|
||||
|
|
@ -31,17 +29,20 @@ class ChatGPTConfig(OpenAIConfig):
|
|||
api_key: Optional[str],
|
||||
custom_llm_provider: str,
|
||||
) -> Tuple[Optional[str], Optional[str], str]:
|
||||
# NOTE: we deliberately do NOT call ``get_access_token()`` here.
|
||||
# ``get_llm_provider`` is the resolution stage and runs at proxy
|
||||
# startup for every deployment (via ``add_deployment`` cycles).
|
||||
# Calling the filesystem ``Authenticator.get_access_token()`` with
|
||||
# no tokens on disk triggers ``_login_device_code()``, which
|
||||
# prints a device code to stdout and polls OpenAI for up to 15
|
||||
# minutes — blocking proxy startup and spamming the logs every
|
||||
# 30s once the background scheduler kicks in. Actual auth
|
||||
# resolution happens in ``validate_environment`` at request time,
|
||||
# which does the right thing via ``resolve_authenticator`` and
|
||||
# the DB-backed cache.
|
||||
authenticator = resolve_authenticator(api_key, None, self.authenticator)
|
||||
dynamic_api_base = authenticator.get_api_base()
|
||||
try:
|
||||
dynamic_api_key = authenticator.get_access_token()
|
||||
except GetAccessTokenError as e:
|
||||
raise AuthenticationError(
|
||||
model=model,
|
||||
llm_provider=custom_llm_provider,
|
||||
message=str(e),
|
||||
)
|
||||
return dynamic_api_base, dynamic_api_key, custom_llm_provider
|
||||
dynamic_api_base = api_base or authenticator.get_api_base()
|
||||
return dynamic_api_base, api_key, custom_llm_provider
|
||||
|
||||
def validate_environment(
|
||||
self,
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
from typing import List, Optional, Tuple
|
||||
|
||||
|
||||
from litellm.exceptions import AuthenticationError
|
||||
from litellm.llms.openai.openai import OpenAIConfig
|
||||
from litellm.types.llms.openai import AllMessageValues
|
||||
|
||||
|
|
@ -31,17 +30,17 @@ class GithubCopilotConfig(OpenAIConfig):
|
|||
api_key: Optional[str],
|
||||
custom_llm_provider: str,
|
||||
) -> Tuple[Optional[str], Optional[str], str]:
|
||||
# Do NOT call ``get_api_key()`` here — see the ChatGPT chat
|
||||
# transformation for the full rationale. At startup-time proxy
|
||||
# resolution, triggering the filesystem authenticator with no
|
||||
# tokens would fire the GitHub device-code flow and block. Actual
|
||||
# auth resolution happens in ``validate_environment`` at request
|
||||
# time via ``resolve_authenticator``.
|
||||
authenticator = resolve_authenticator(api_key, None, self.authenticator)
|
||||
dynamic_api_base = authenticator.get_api_base() or GITHUB_COPILOT_API_BASE
|
||||
try:
|
||||
dynamic_api_key = authenticator.get_api_key()
|
||||
except GetAPIKeyError as e:
|
||||
raise AuthenticationError(
|
||||
model=model,
|
||||
llm_provider=custom_llm_provider,
|
||||
message=str(e),
|
||||
)
|
||||
return dynamic_api_base, dynamic_api_key, custom_llm_provider
|
||||
dynamic_api_base = (
|
||||
api_base or authenticator.get_api_base() or GITHUB_COPILOT_API_BASE
|
||||
)
|
||||
return dynamic_api_base, api_key, custom_llm_provider
|
||||
|
||||
def _transform_messages(
|
||||
self,
|
||||
|
|
|
|||
|
|
@ -18,25 +18,31 @@ from litellm.llms.chatgpt.db_authenticator import (
|
|||
|
||||
|
||||
class TestChatTransformationDispatch:
|
||||
def test_get_openai_compatible_provider_info_uses_db_authenticator(self):
|
||||
def test_get_openai_compatible_provider_info_does_not_eagerly_fetch_token(self):
|
||||
"""
|
||||
At resolution time (startup ``add_deployment`` cycles, every 30s),
|
||||
the chat transformation's ``_get_openai_compatible_provider_info``
|
||||
must NOT call ``get_access_token``. The filesystem Authenticator
|
||||
otherwise triggers ``_login_device_code`` with no tokens on disk,
|
||||
which prints a device prompt and polls for 15 minutes — blocking
|
||||
proxy startup. Actual token resolution happens at request time in
|
||||
``validate_environment``.
|
||||
"""
|
||||
config = ChatGPTConfig()
|
||||
fs_auth = MagicMock(spec=Authenticator)
|
||||
fs_auth.get_access_token.side_effect = AssertionError(
|
||||
"Filesystem authenticator must not be called for oauth: prefix"
|
||||
)
|
||||
fs_auth.get_api_base.side_effect = AssertionError(
|
||||
"Filesystem authenticator must not be called for oauth: prefix"
|
||||
"get_access_token must not be called at resolution time"
|
||||
)
|
||||
fs_auth.get_api_base.return_value = "https://chatgpt.com/backend-api/codex"
|
||||
config.authenticator = fs_auth
|
||||
|
||||
with (
|
||||
patch.object(
|
||||
DBAuthenticator,
|
||||
"get_api_base",
|
||||
return_value="https://chatgpt.com/backend-api/codex",
|
||||
),
|
||||
patch.object(
|
||||
DBAuthenticator, "get_access_token", return_value="db-access-token"
|
||||
# Even the DB-backed authenticator's get_access_token must not
|
||||
# fire here — resolution is a pure metadata pass.
|
||||
with patch.object(
|
||||
DBAuthenticator,
|
||||
"get_access_token",
|
||||
side_effect=AssertionError(
|
||||
"get_access_token must not be called at resolution time"
|
||||
),
|
||||
):
|
||||
base, key, _ = config._get_openai_compatible_provider_info(
|
||||
|
|
@ -45,7 +51,9 @@ class TestChatTransformationDispatch:
|
|||
api_key=f"{OAUTH_CREDENTIAL_API_KEY_PREFIX}my-creds",
|
||||
custom_llm_provider="chatgpt",
|
||||
)
|
||||
assert key == "db-access-token"
|
||||
# The raw oauth: marker passes through; validate_environment
|
||||
# resolves it at request time.
|
||||
assert key == f"{OAUTH_CREDENTIAL_API_KEY_PREFIX}my-creds"
|
||||
assert base == "https://chatgpt.com/backend-api/codex"
|
||||
|
||||
def test_validate_environment_uses_db_authenticator(self):
|
||||
|
|
|
|||
|
|
@ -22,22 +22,28 @@ def _reset_cache():
|
|||
|
||||
|
||||
class TestChatTransformationDispatch:
|
||||
def test_oauth_prefix_api_key_uses_db_authenticator(self):
|
||||
def test_get_openai_compatible_provider_info_does_not_eagerly_fetch_api_key(self):
|
||||
"""
|
||||
Symmetric to the ChatGPT fix: the resolution stage must not call
|
||||
``get_api_key`` on either authenticator. That would otherwise hit
|
||||
GitHub's ``/copilot_internal/v2/token`` (or trigger a device-code
|
||||
login if no access token is stored) at every
|
||||
``add_deployment`` cycle during proxy startup.
|
||||
"""
|
||||
config = GithubCopilotConfig()
|
||||
fs_auth = MagicMock(spec=Authenticator)
|
||||
fs_auth.get_api_base.side_effect = AssertionError(
|
||||
"Filesystem authenticator must not be used for oauth: prefix"
|
||||
)
|
||||
fs_auth.get_api_key.side_effect = AssertionError(
|
||||
"Filesystem authenticator must not be used for oauth: prefix"
|
||||
"get_api_key must not be called at resolution time"
|
||||
)
|
||||
fs_auth.get_api_base.return_value = None
|
||||
config.authenticator = fs_auth
|
||||
|
||||
with (
|
||||
patch.object(
|
||||
DBAuthenticator, "get_api_base", return_value="https://x.example"
|
||||
with patch.object(
|
||||
DBAuthenticator,
|
||||
"get_api_key",
|
||||
side_effect=AssertionError(
|
||||
"get_api_key must not be called at resolution time"
|
||||
),
|
||||
patch.object(DBAuthenticator, "get_api_key", return_value="cop-key"),
|
||||
):
|
||||
base, key, _ = config._get_openai_compatible_provider_info(
|
||||
model="gpt-5",
|
||||
|
|
@ -45,22 +51,9 @@ class TestChatTransformationDispatch:
|
|||
api_key=f"{OAUTH_CREDENTIAL_API_KEY_PREFIX}my-creds",
|
||||
custom_llm_provider="github_copilot",
|
||||
)
|
||||
assert key == "cop-key"
|
||||
assert base == "https://x.example"
|
||||
|
||||
def test_plain_api_key_uses_filesystem_authenticator(self):
|
||||
config = GithubCopilotConfig()
|
||||
config.authenticator = MagicMock(spec=Authenticator)
|
||||
config.authenticator.get_api_base.return_value = None
|
||||
config.authenticator.get_api_key.return_value = "fs-key"
|
||||
|
||||
_, key, _ = config._get_openai_compatible_provider_info(
|
||||
model="gpt-5",
|
||||
api_base=None,
|
||||
api_key="sk-plain",
|
||||
custom_llm_provider="github_copilot",
|
||||
)
|
||||
assert key == "fs-key"
|
||||
# Passthrough — validate_environment resolves at request time.
|
||||
assert key == f"{OAUTH_CREDENTIAL_API_KEY_PREFIX}my-creds"
|
||||
assert base is not None # Falls back to GITHUB_COPILOT_API_BASE
|
||||
|
||||
|
||||
class TestResponsesTransformationDispatch:
|
||||
|
|
|
|||
|
|
@ -19,31 +19,38 @@ import litellm
|
|||
# Import at the top to make the patch work correctly
|
||||
import litellm.llms.github_copilot.chat.transformation
|
||||
from litellm import Choices, Message, ModelResponse, Usage, acompletion, completion
|
||||
from litellm.exceptions import AuthenticationError
|
||||
from litellm.llms.github_copilot.authenticator import Authenticator
|
||||
from litellm.llms.github_copilot.chat.transformation import GithubCopilotConfig
|
||||
from litellm.llms.github_copilot.common_utils import (
|
||||
APIKeyExpiredError,
|
||||
GetAccessTokenError,
|
||||
GetAPIKeyError,
|
||||
GetDeviceCodeError,
|
||||
RefreshAPIKeyError,
|
||||
)
|
||||
|
||||
|
||||
def test_github_copilot_config_get_openai_compatible_provider_info():
|
||||
"""Test the GitHub Copilot configuration provider info retrieval."""
|
||||
"""
|
||||
Test the GitHub Copilot configuration provider info retrieval.
|
||||
|
||||
``_get_openai_compatible_provider_info`` runs at proxy startup (every
|
||||
``add_deployment`` cycle). Calling ``get_api_key()`` eagerly here would
|
||||
hit GitHub's token endpoint — or worse, kick off the CLI device-code
|
||||
login with no tokens on disk — which blocks startup. Actual API-key
|
||||
resolution happens at request time in ``validate_environment``.
|
||||
"""
|
||||
|
||||
config = GithubCopilotConfig()
|
||||
|
||||
# Mock the authenticator to avoid actual API calls
|
||||
mock_api_key = "gh.test-key-123456789"
|
||||
# The authenticator should NOT be asked for an API key at this stage.
|
||||
config.authenticator = MagicMock()
|
||||
config.authenticator.get_api_key.return_value = mock_api_key
|
||||
# Test with dynamic endpoint
|
||||
config.authenticator.get_api_base.return_value = "https://api.enterprise.githubcopilot.com"
|
||||
config.authenticator.get_api_key.side_effect = AssertionError(
|
||||
"get_api_key must not be called at resolution time"
|
||||
)
|
||||
config.authenticator.get_api_base.return_value = (
|
||||
"https://api.enterprise.githubcopilot.com"
|
||||
)
|
||||
|
||||
# Test with default values
|
||||
model = "github_copilot/gpt-4"
|
||||
(
|
||||
api_base,
|
||||
|
|
@ -56,11 +63,13 @@ def test_github_copilot_config_get_openai_compatible_provider_info():
|
|||
custom_llm_provider="github_copilot",
|
||||
)
|
||||
|
||||
# Uses the authenticator's configured api_base, passes api_key through.
|
||||
assert api_base == "https://api.enterprise.githubcopilot.com"
|
||||
assert dynamic_api_key == mock_api_key
|
||||
assert dynamic_api_key is None
|
||||
assert custom_llm_provider == "github_copilot"
|
||||
|
||||
# Test fallback to default if no dynamic endpoint
|
||||
# Falls back to the default GitHub Copilot base when the authenticator
|
||||
# doesn't supply one.
|
||||
config.authenticator.get_api_base.return_value = None
|
||||
(
|
||||
api_base,
|
||||
|
|
@ -73,22 +82,7 @@ def test_github_copilot_config_get_openai_compatible_provider_info():
|
|||
custom_llm_provider="github_copilot",
|
||||
)
|
||||
assert api_base == "https://api.githubcopilot.com"
|
||||
|
||||
# Test with authentication failure
|
||||
config.authenticator.get_api_key.side_effect = GetAPIKeyError(
|
||||
message="Failed to get API key",
|
||||
status_code=401,
|
||||
)
|
||||
|
||||
with pytest.raises(AuthenticationError) as excinfo:
|
||||
config._get_openai_compatible_provider_info(
|
||||
model=model,
|
||||
api_base=None,
|
||||
api_key=None,
|
||||
custom_llm_provider="github_copilot",
|
||||
)
|
||||
|
||||
assert "Failed to get API key" in str(excinfo.value)
|
||||
assert dynamic_api_key is None
|
||||
|
||||
|
||||
@patch("litellm.llms.github_copilot.authenticator.Authenticator.get_api_key")
|
||||
|
|
@ -157,19 +151,25 @@ def test_transform_messages_disable_copilot_system_to_assistant(monkeypatch):
|
|||
{"role": "system", "content": "System message."},
|
||||
{"role": "user", "content": "User message."},
|
||||
]
|
||||
out = config._transform_messages([m.copy() for m in messages], model="github_copilot/gpt-4")
|
||||
out = config._transform_messages(
|
||||
[m.copy() for m in messages], model="github_copilot/gpt-4"
|
||||
)
|
||||
assert out[0]["role"] == "assistant"
|
||||
assert out[1]["role"] == "user"
|
||||
|
||||
# Case 2: Flag is True (conversion does not happen)
|
||||
litellm.disable_copilot_system_to_assistant = True
|
||||
out = config._transform_messages([m.copy() for m in messages], model="github_copilot/gpt-4")
|
||||
out = config._transform_messages(
|
||||
[m.copy() for m in messages], model="github_copilot/gpt-4"
|
||||
)
|
||||
assert out[0]["role"] == "system"
|
||||
assert out[1]["role"] == "user"
|
||||
|
||||
# Case 3: Flag is False again (conversion happens)
|
||||
litellm.disable_copilot_system_to_assistant = False
|
||||
out = config._transform_messages([m.copy() for m in messages], model="github_copilot/gpt-4")
|
||||
out = config._transform_messages(
|
||||
[m.copy() for m in messages], model="github_copilot/gpt-4"
|
||||
)
|
||||
assert out[0]["role"] == "assistant"
|
||||
assert out[1]["role"] == "user"
|
||||
finally:
|
||||
|
|
@ -180,7 +180,7 @@ def test_transform_messages_disable_copilot_system_to_assistant(monkeypatch):
|
|||
def test_x_initiator_header_user_request():
|
||||
"""Test that user-only messages result in X-Initiator: user header"""
|
||||
config = GithubCopilotConfig()
|
||||
|
||||
|
||||
# Mock the authenticator
|
||||
config.authenticator = MagicMock()
|
||||
config.authenticator.get_api_key.return_value = "gh.test-key-123"
|
||||
|
|
@ -190,7 +190,7 @@ def test_x_initiator_header_user_request():
|
|||
{"role": "system", "content": "You are an assistant."},
|
||||
{"role": "user", "content": "Hello!"},
|
||||
]
|
||||
|
||||
|
||||
headers = config.validate_environment(
|
||||
headers={},
|
||||
model="github_copilot/gpt-4",
|
||||
|
|
@ -200,14 +200,14 @@ def test_x_initiator_header_user_request():
|
|||
api_key=None,
|
||||
api_base=None,
|
||||
)
|
||||
|
||||
|
||||
assert headers["X-Initiator"] == "user"
|
||||
|
||||
|
||||
def test_x_initiator_header_agent_request_with_assistant():
|
||||
"""Test that messages with assistant role result in X-Initiator: agent header"""
|
||||
config = GithubCopilotConfig()
|
||||
|
||||
|
||||
# Mock the authenticator
|
||||
config.authenticator = MagicMock()
|
||||
config.authenticator.get_api_key.return_value = "gh.test-key-123"
|
||||
|
|
@ -217,24 +217,24 @@ def test_x_initiator_header_agent_request_with_assistant():
|
|||
{"role": "system", "content": "You are an assistant."},
|
||||
{"role": "assistant", "content": "I can help you."},
|
||||
]
|
||||
|
||||
|
||||
headers = config.validate_environment(
|
||||
headers={},
|
||||
model="github_copilot/gpt-4",
|
||||
model="github_copilot/gpt-4",
|
||||
messages=messages,
|
||||
optional_params={},
|
||||
litellm_params={},
|
||||
api_key=None,
|
||||
api_base=None,
|
||||
)
|
||||
|
||||
|
||||
assert headers["X-Initiator"] == "agent"
|
||||
|
||||
|
||||
def test_x_initiator_header_agent_request_with_tool():
|
||||
"""Test that messages with tool role result in X-Initiator: agent header"""
|
||||
config = GithubCopilotConfig()
|
||||
|
||||
|
||||
# Mock the authenticator
|
||||
config.authenticator = MagicMock()
|
||||
config.authenticator.get_api_key.return_value = "gh.test-key-123"
|
||||
|
|
@ -244,25 +244,25 @@ def test_x_initiator_header_agent_request_with_tool():
|
|||
{"role": "system", "content": "You are an assistant."},
|
||||
{"role": "tool", "content": "Tool response.", "tool_call_id": "123"},
|
||||
]
|
||||
|
||||
|
||||
headers = config.validate_environment(
|
||||
headers={},
|
||||
model="github_copilot/gpt-4",
|
||||
model="github_copilot/gpt-4",
|
||||
messages=messages,
|
||||
optional_params={},
|
||||
litellm_params={},
|
||||
api_key=None,
|
||||
api_base=None,
|
||||
)
|
||||
|
||||
|
||||
assert headers["X-Initiator"] == "agent"
|
||||
|
||||
|
||||
def test_x_initiator_header_mixed_messages_with_agent_roles():
|
||||
"""Test that mixed messages with agent roles (assistant/tool) result in X-Initiator: agent header"""
|
||||
config = GithubCopilotConfig()
|
||||
|
||||
# Mock the authenticator
|
||||
|
||||
# Mock the authenticator
|
||||
config.authenticator = MagicMock()
|
||||
config.authenticator.get_api_key.return_value = "gh.test-key-123"
|
||||
config.authenticator.get_api_base.return_value = None
|
||||
|
|
@ -272,25 +272,25 @@ def test_x_initiator_header_mixed_messages_with_agent_roles():
|
|||
{"role": "assistant", "content": "Previous response."},
|
||||
{"role": "user", "content": "Follow up question."},
|
||||
]
|
||||
|
||||
|
||||
headers = config.validate_environment(
|
||||
headers={},
|
||||
model="github_copilot/gpt-4",
|
||||
messages=messages,
|
||||
messages=messages,
|
||||
optional_params={},
|
||||
litellm_params={},
|
||||
api_key=None,
|
||||
api_base=None,
|
||||
)
|
||||
|
||||
|
||||
assert headers["X-Initiator"] == "agent"
|
||||
|
||||
|
||||
def test_x_initiator_header_user_only_messages():
|
||||
"""Test that user + system only messages result in X-Initiator: user header"""
|
||||
config = GithubCopilotConfig()
|
||||
|
||||
# Mock the authenticator
|
||||
|
||||
# Mock the authenticator
|
||||
config.authenticator = MagicMock()
|
||||
config.authenticator.get_api_key.return_value = "gh.test-key-123"
|
||||
config.authenticator.get_api_base.return_value = None
|
||||
|
|
@ -300,31 +300,7 @@ def test_x_initiator_header_user_only_messages():
|
|||
{"role": "user", "content": "Hello"},
|
||||
{"role": "user", "content": "Follow up question."},
|
||||
]
|
||||
|
||||
headers = config.validate_environment(
|
||||
headers={},
|
||||
model="github_copilot/gpt-4",
|
||||
messages=messages,
|
||||
optional_params={},
|
||||
litellm_params={},
|
||||
api_key=None,
|
||||
api_base=None,
|
||||
)
|
||||
|
||||
assert headers["X-Initiator"] == "user"
|
||||
|
||||
|
||||
def test_x_initiator_header_empty_messages():
|
||||
"""Test that empty messages result in X-Initiator: user header"""
|
||||
config = GithubCopilotConfig()
|
||||
|
||||
# Mock the authenticator
|
||||
config.authenticator = MagicMock()
|
||||
config.authenticator.get_api_key.return_value = "gh.test-key-123"
|
||||
config.authenticator.get_api_base.return_value = None
|
||||
|
||||
messages = []
|
||||
|
||||
headers = config.validate_environment(
|
||||
headers={},
|
||||
model="github_copilot/gpt-4",
|
||||
|
|
@ -334,14 +310,38 @@ def test_x_initiator_header_empty_messages():
|
|||
api_key=None,
|
||||
api_base=None,
|
||||
)
|
||||
|
||||
|
||||
assert headers["X-Initiator"] == "user"
|
||||
|
||||
|
||||
def test_x_initiator_header_empty_messages():
|
||||
"""Test that empty messages result in X-Initiator: user header"""
|
||||
config = GithubCopilotConfig()
|
||||
|
||||
# Mock the authenticator
|
||||
config.authenticator = MagicMock()
|
||||
config.authenticator.get_api_key.return_value = "gh.test-key-123"
|
||||
config.authenticator.get_api_base.return_value = None
|
||||
|
||||
messages = []
|
||||
|
||||
headers = config.validate_environment(
|
||||
headers={},
|
||||
model="github_copilot/gpt-4",
|
||||
messages=messages,
|
||||
optional_params={},
|
||||
litellm_params={},
|
||||
api_key=None,
|
||||
api_base=None,
|
||||
)
|
||||
|
||||
assert headers["X-Initiator"] == "user"
|
||||
|
||||
|
||||
def test_x_initiator_header_system_only_messages():
|
||||
"""Test that system-only messages result in X-Initiator: user header"""
|
||||
config = GithubCopilotConfig()
|
||||
|
||||
|
||||
# Mock the authenticator
|
||||
config.authenticator = MagicMock()
|
||||
config.authenticator.get_api_key.return_value = "gh.test-key-123"
|
||||
|
|
@ -350,7 +350,7 @@ def test_x_initiator_header_system_only_messages():
|
|||
messages = [
|
||||
{"role": "system", "content": "You are an assistant."},
|
||||
]
|
||||
|
||||
|
||||
headers = config.validate_environment(
|
||||
headers={},
|
||||
model="github_copilot/gpt-4",
|
||||
|
|
@ -360,35 +360,37 @@ def test_x_initiator_header_system_only_messages():
|
|||
api_key=None,
|
||||
api_base=None,
|
||||
)
|
||||
|
||||
|
||||
assert headers["X-Initiator"] == "user"
|
||||
|
||||
|
||||
def test_get_supported_openai_params_claude_model():
|
||||
"""Test that Claude models with extended thinking support have thinking and reasoning parameters."""
|
||||
config = GithubCopilotConfig()
|
||||
|
||||
|
||||
# Test Claude 4 model supports thinking and reasoning_effort parameters
|
||||
supported_params = config.get_supported_openai_params("claude-sonnet-4-20250514")
|
||||
assert "thinking" in supported_params
|
||||
assert "reasoning_effort" in supported_params
|
||||
|
||||
|
||||
# Test Claude 3-7 model supports thinking and reasoning_effort parameters
|
||||
supported_params_claude37 = config.get_supported_openai_params("claude-3-7-sonnet-20250219")
|
||||
supported_params_claude37 = config.get_supported_openai_params(
|
||||
"claude-3-7-sonnet-20250219"
|
||||
)
|
||||
assert "thinking" in supported_params_claude37
|
||||
assert "reasoning_effort" in supported_params_claude37
|
||||
|
||||
|
||||
# Test Claude 3.5 model does NOT support thinking parameters (no extended thinking)
|
||||
supported_params_claude35 = config.get_supported_openai_params("claude-3.5-sonnet")
|
||||
assert "thinking" not in supported_params_claude35
|
||||
assert "reasoning_effort" not in supported_params_claude35
|
||||
|
||||
|
||||
# Test non-Claude model doesn't include thinking parameters but may include reasoning_effort
|
||||
supported_params_gpt = config.get_supported_openai_params("gpt-4o")
|
||||
assert "thinking" not in supported_params_gpt
|
||||
# gpt-4o should NOT have reasoning_effort (not a reasoning model)
|
||||
assert "reasoning_effort" not in supported_params_gpt
|
||||
|
||||
|
||||
# Test O-series reasoning models include reasoning_effort but not thinking
|
||||
supported_params_o3 = config.get_supported_openai_params("o3-mini")
|
||||
assert "thinking" not in supported_params_o3
|
||||
|
|
@ -399,26 +401,31 @@ def test_get_supported_openai_params_claude_model():
|
|||
def test_get_supported_openai_params_case_insensitive():
|
||||
"""Test that Claude model detection is case-insensitive for models with extended thinking."""
|
||||
config = GithubCopilotConfig()
|
||||
|
||||
|
||||
# Test uppercase Claude 4 model with full model name
|
||||
supported_params_upper = config.get_supported_openai_params("CLAUDE-SONNET-4-20250514")
|
||||
supported_params_upper = config.get_supported_openai_params(
|
||||
"CLAUDE-SONNET-4-20250514"
|
||||
)
|
||||
assert "thinking" in supported_params_upper
|
||||
assert "reasoning_effort" in supported_params_upper
|
||||
|
||||
|
||||
# Test mixed case Claude 3-7 model (has extended thinking) with full model name
|
||||
supported_params_mixed = config.get_supported_openai_params("Claude-3-7-Sonnet-20250219")
|
||||
supported_params_mixed = config.get_supported_openai_params(
|
||||
"Claude-3-7-Sonnet-20250219"
|
||||
)
|
||||
assert "thinking" in supported_params_mixed
|
||||
assert "reasoning_effort" in supported_params_mixed
|
||||
|
||||
|
||||
# Test that Claude 3.5 models don't have thinking support (case insensitive)
|
||||
supported_params_35 = config.get_supported_openai_params("CLAUDE-3.5-SONNET")
|
||||
assert "thinking" not in supported_params_35
|
||||
assert "reasoning_effort" not in supported_params_35
|
||||
|
||||
|
||||
def test_copilot_vision_request_header_with_image():
|
||||
"""Test that Copilot-Vision-Request header is added when messages contain images"""
|
||||
config = GithubCopilotConfig()
|
||||
|
||||
|
||||
# Mock the authenticator
|
||||
config.authenticator = MagicMock()
|
||||
config.authenticator.get_api_key.return_value = "gh.test-key-123"
|
||||
|
|
@ -431,12 +438,12 @@ def test_copilot_vision_request_header_with_image():
|
|||
{"type": "text", "text": "What's in this image?"},
|
||||
{
|
||||
"type": "image_url",
|
||||
"image_url": {"url": "data:image/jpeg;base64,abc123"}
|
||||
}
|
||||
]
|
||||
"image_url": {"url": "data:image/jpeg;base64,abc123"},
|
||||
},
|
||||
],
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
headers = config.validate_environment(
|
||||
headers={},
|
||||
model="github_copilot/gpt-4-vision-preview",
|
||||
|
|
@ -446,7 +453,7 @@ def test_copilot_vision_request_header_with_image():
|
|||
api_key=None,
|
||||
api_base=None,
|
||||
)
|
||||
|
||||
|
||||
assert headers["Copilot-Vision-Request"] == "true"
|
||||
assert headers["X-Initiator"] == "user"
|
||||
|
||||
|
|
@ -454,7 +461,7 @@ def test_copilot_vision_request_header_with_image():
|
|||
def test_copilot_vision_request_header_text_only():
|
||||
"""Test that Copilot-Vision-Request header is not added for text-only messages"""
|
||||
config = GithubCopilotConfig()
|
||||
|
||||
|
||||
# Mock the authenticator
|
||||
config.authenticator = MagicMock()
|
||||
config.authenticator.get_api_key.return_value = "gh.test-key-123"
|
||||
|
|
@ -463,7 +470,7 @@ def test_copilot_vision_request_header_text_only():
|
|||
messages = [
|
||||
{"role": "user", "content": "Just a text message"},
|
||||
]
|
||||
|
||||
|
||||
headers = config.validate_environment(
|
||||
headers={},
|
||||
model="github_copilot/gpt-4",
|
||||
|
|
@ -473,7 +480,7 @@ def test_copilot_vision_request_header_text_only():
|
|||
api_key=None,
|
||||
api_base=None,
|
||||
)
|
||||
|
||||
|
||||
assert "Copilot-Vision-Request" not in headers
|
||||
assert headers["X-Initiator"] == "user"
|
||||
|
||||
|
|
@ -481,7 +488,7 @@ def test_copilot_vision_request_header_text_only():
|
|||
def test_copilot_vision_request_header_with_type_image_url():
|
||||
"""Test that Copilot-Vision-Request header is added for content with type: image_url"""
|
||||
config = GithubCopilotConfig()
|
||||
|
||||
|
||||
# Mock the authenticator
|
||||
config.authenticator = MagicMock()
|
||||
config.authenticator.get_api_key.return_value = "gh.test-key-123"
|
||||
|
|
@ -492,11 +499,14 @@ def test_copilot_vision_request_header_with_type_image_url():
|
|||
"role": "user",
|
||||
"content": [
|
||||
{"type": "text", "text": "Analyze this image"},
|
||||
{"type": "image_url", "image_url": {"url": "https://example.com/image.jpg"}}
|
||||
]
|
||||
{
|
||||
"type": "image_url",
|
||||
"image_url": {"url": "https://example.com/image.jpg"},
|
||||
},
|
||||
],
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
headers = config.validate_environment(
|
||||
headers={},
|
||||
model="github_copilot/gpt-4-vision-preview",
|
||||
|
|
@ -506,6 +516,6 @@ def test_copilot_vision_request_header_with_type_image_url():
|
|||
api_key=None,
|
||||
api_base=None,
|
||||
)
|
||||
|
||||
|
||||
assert headers["Copilot-Vision-Request"] == "true"
|
||||
assert headers["X-Initiator"] == "user"
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue