From 8f7d896e261ca693d49926e94ebf2e41c352922b Mon Sep 17 00:00:00 2001 From: Christoph Koehler Date: Mon, 28 Jul 2025 10:55:24 -0600 Subject: [PATCH] fix: add X-Initiator header for GitHub Copilot to reduce premium requests (#13016) - Implement X-Initiator header logic in GithubCopilotConfig.validate_environment() - Set header to "agent" when messages contain agent or tool roles, "user" otherwise - Reduces unnecessary premium Copilot API usage for non-user calls Fixes #12859 --- .../github_copilot/chat/transformation.py | 45 ++++- .../test_github_copilot_transformation.py | 185 ++++++++++++++++++ 2 files changed, 227 insertions(+), 3 deletions(-) diff --git a/litellm/llms/github_copilot/chat/transformation.py b/litellm/llms/github_copilot/chat/transformation.py index eff8acb5f5..4526e6247b 100644 --- a/litellm/llms/github_copilot/chat/transformation.py +++ b/litellm/llms/github_copilot/chat/transformation.py @@ -1,7 +1,8 @@ -from typing import Any, Optional, Tuple, cast +from typing import Any, Optional, Tuple, cast, List 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 GetAPIKeyError @@ -9,6 +10,7 @@ from ..common_utils import GetAPIKeyError class GithubCopilotConfig(OpenAIConfig): GITHUB_COPILOT_API_BASE = "https://api.githubcopilot.com/" + def __init__( self, api_key: Optional[str] = None, @@ -25,7 +27,9 @@ class GithubCopilotConfig(OpenAIConfig): api_key: Optional[str], custom_llm_provider: str, ) -> Tuple[Optional[str], Optional[str], str]: - dynamic_api_base = self.authenticator.get_api_base() or self.GITHUB_COPILOT_API_BASE + dynamic_api_base = ( + self.authenticator.get_api_base() or self.GITHUB_COPILOT_API_BASE + ) try: dynamic_api_key = self.authenticator.get_api_key() except GetAPIKeyError as e: @@ -42,9 +46,44 @@ class GithubCopilotConfig(OpenAIConfig): model: str, ): import litellm - disable_copilot_system_to_assistant = litellm.disable_copilot_system_to_assistant + + disable_copilot_system_to_assistant = ( + litellm.disable_copilot_system_to_assistant + ) if not disable_copilot_system_to_assistant: for message in messages: if "role" in message and message["role"] == "system": cast(Any, message)["role"] = "assistant" return messages + + def validate_environment( + self, + headers: dict, + model: str, + messages: List[AllMessageValues], + optional_params: dict, + litellm_params: dict, + api_key: Optional[str] = None, + api_base: Optional[str] = None, + ) -> dict: + # Get base headers from parent + validated_headers = super().validate_environment( + headers, model, messages, optional_params, litellm_params, api_key, api_base + ) + + # Add X-Initiator header based on message roles + initiator = self._determine_initiator(messages) + validated_headers["X-Initiator"] = initiator + + return validated_headers + + def _determine_initiator(self, messages: List[AllMessageValues]) -> str: + """ + Determine if request is user or agent initiated based on message roles. + Returns 'agent' if any message has role 'tool' or 'assistant', otherwise 'user'. + """ + for message in messages: + role = message.get("role") + if role in ["tool", "assistant"]: + return "agent" + return "user" diff --git a/tests/test_litellm/llms/github_copilot/test_github_copilot_transformation.py b/tests/test_litellm/llms/github_copilot/test_github_copilot_transformation.py index 388a50243f..f21c123579 100644 --- a/tests/test_litellm/llms/github_copilot/test_github_copilot_transformation.py +++ b/tests/test_litellm/llms/github_copilot/test_github_copilot_transformation.py @@ -177,3 +177,188 @@ def test_transform_messages_disable_copilot_system_to_assistant(monkeypatch): litellm.disable_copilot_system_to_assistant = original_flag +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" + config.authenticator.get_api_base.return_value = None + + messages = [ + {"role": "system", "content": "You are an assistant."}, + {"role": "user", "content": "Hello!"}, + ] + + 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_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" + config.authenticator.get_api_base.return_value = None + + messages = [ + {"role": "system", "content": "You are an assistant."}, + {"role": "assistant", "content": "I can help you."}, + ] + + 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"] == "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" + config.authenticator.get_api_base.return_value = None + + messages = [ + {"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", + 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 + config.authenticator = MagicMock() + config.authenticator.get_api_key.return_value = "gh.test-key-123" + config.authenticator.get_api_base.return_value = None + + messages = [ + {"role": "user", "content": "Hello"}, + {"role": "assistant", "content": "Previous response."}, + {"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"] == "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 + config.authenticator = MagicMock() + config.authenticator.get_api_key.return_value = "gh.test-key-123" + config.authenticator.get_api_base.return_value = None + + messages = [ + {"role": "system", "content": "You are an assistant."}, + {"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", + 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" + config.authenticator.get_api_base.return_value = None + + messages = [ + {"role": "system", "content": "You are an assistant."}, + ] + + 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"