From 3063124e37af36fe341b119be28b5b422ee7900d Mon Sep 17 00:00:00 2001 From: ririnto Date: Sat, 16 May 2026 19:00:28 +0900 Subject: [PATCH] feat(github_copilot): route /v1/messages to Copilot native Anthropic endpoint Add a GitHub Copilot Anthropic Messages transformation that routes supported Claude models through the native /v1/messages endpoint. This covers request URL construction, default headers, and supported model metadata. --- .../llms/github_copilot/messages/__init__.py | 0 .../github_copilot/messages/transformation.py | 81 ++++++++++++ ...odel_prices_and_context_window_backup.json | 9 +- litellm/utils.py | 7 ++ .../llms/github_copilot/messages/__init__.py | 0 ..._github_copilot_messages_transformation.py | 117 ++++++++++++++++++ 6 files changed, 211 insertions(+), 3 deletions(-) create mode 100644 litellm/llms/github_copilot/messages/__init__.py create mode 100644 litellm/llms/github_copilot/messages/transformation.py create mode 100644 tests/test_litellm/llms/github_copilot/messages/__init__.py create mode 100644 tests/test_litellm/llms/github_copilot/messages/test_github_copilot_messages_transformation.py diff --git a/litellm/llms/github_copilot/messages/__init__.py b/litellm/llms/github_copilot/messages/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/litellm/llms/github_copilot/messages/transformation.py b/litellm/llms/github_copilot/messages/transformation.py new file mode 100644 index 00000000000..a30a8142230 --- /dev/null +++ b/litellm/llms/github_copilot/messages/transformation.py @@ -0,0 +1,81 @@ +from typing import Any, List, Optional, Tuple + +from litellm.exceptions import AuthenticationError +from litellm.llms.anthropic.experimental_pass_through.messages.transformation import ( + AnthropicMessagesConfig, +) + +from ..authenticator import Authenticator +from ..common_utils import ( + DEFAULT_GITHUB_COPILOT_API_BASE, + GetAPIKeyError, + get_copilot_default_headers, +) + + +class GithubCopilotAnthropicMessagesConfig(AnthropicMessagesConfig): + """ + GitHub Copilot implementation of Anthropic messages API. + Routes requests to Copilot's /v1/messages endpoint with appropriate authentication and headers. + """ + + def __init__(self) -> None: + super().__init__() + self.authenticator = Authenticator() + + def validate_anthropic_messages_environment( + self, + headers: dict, + model: str, + messages: List[Any], + optional_params: dict, + litellm_params: dict, + api_key: Optional[str] = None, + api_base: Optional[str] = None, + ) -> Tuple[dict, Optional[str]]: + """ + Validate environment for GitHub Copilot and add Copilot-specific headers. + """ + # Get Copilot auth credentials + dynamic_api_base = ( + api_base + or self.authenticator.get_api_base() + or DEFAULT_GITHUB_COPILOT_API_BASE + ) + try: + dynamic_api_key = self.authenticator.get_api_key() + except GetAPIKeyError as e: + raise AuthenticationError( + model=model, + llm_provider="github_copilot", + message=str(e), + ) + + # Merge Copilot headers with provided headers + copilot_headers = get_copilot_default_headers(dynamic_api_key) + for key, value in copilot_headers.items(): + if key not in headers: + headers[key] = value + + # Set Anthropic version for messages API + if "anthropic-version" not in headers: + headers["anthropic-version"] = "2023-06-01" + + return headers, dynamic_api_base + + def get_complete_url( + self, + api_base: Optional[str], + api_key: Optional[str], + model: str, + optional_params: dict, + litellm_params: dict, + stream: Optional[bool] = None, + ) -> str: + """ + Return the complete URL for GitHub Copilot /v1/messages endpoint. + """ + api_base = api_base or DEFAULT_GITHUB_COPILOT_API_BASE + if not api_base.endswith("/v1/messages"): + api_base = f"{api_base}/v1/messages" + return api_base diff --git a/litellm/model_prices_and_context_window_backup.json b/litellm/model_prices_and_context_window_backup.json index 7a5f8b9e1e3..6a56ff57fe6 100644 --- a/litellm/model_prices_and_context_window_backup.json +++ b/litellm/model_prices_and_context_window_backup.json @@ -18951,7 +18951,8 @@ "max_tokens": 16000, "mode": "chat", "supported_endpoints": [ - "/v1/chat/completions" + "/v1/chat/completions", + "/v1/messages" ], "supports_function_calling": true, "supports_parallel_function_calling": true, @@ -18964,7 +18965,8 @@ "max_tokens": 16000, "mode": "chat", "supported_endpoints": [ - "/v1/chat/completions" + "/v1/chat/completions", + "/v1/messages" ], "supports_function_calling": true, "supports_parallel_function_calling": true, @@ -19015,7 +19017,8 @@ "max_tokens": 16000, "mode": "chat", "supported_endpoints": [ - "/v1/chat/completions" + "/v1/chat/completions", + "/v1/messages" ], "supports_function_calling": true, "supports_parallel_function_calling": true, diff --git a/litellm/utils.py b/litellm/utils.py index c9001e7d906..247ba3600ba 100644 --- a/litellm/utils.py +++ b/litellm/utils.py @@ -8893,6 +8893,13 @@ class ProviderConfigManager: ) return DeepSeekAnthropicMessagesConfig() + elif litellm.LlmProviders.GITHUB_COPILOT == provider: + if "claude" in model_lower: + from litellm.llms.github_copilot.messages.transformation import ( + GithubCopilotAnthropicMessagesConfig, + ) + + return GithubCopilotAnthropicMessagesConfig() return None @staticmethod diff --git a/tests/test_litellm/llms/github_copilot/messages/__init__.py b/tests/test_litellm/llms/github_copilot/messages/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/test_litellm/llms/github_copilot/messages/test_github_copilot_messages_transformation.py b/tests/test_litellm/llms/github_copilot/messages/test_github_copilot_messages_transformation.py new file mode 100644 index 00000000000..78bd5a8b34d --- /dev/null +++ b/tests/test_litellm/llms/github_copilot/messages/test_github_copilot_messages_transformation.py @@ -0,0 +1,117 @@ +import os +import sys +from unittest.mock import MagicMock + +import pytest + +sys.path.insert(0, os.path.abspath("../..")) + +from litellm.llms.github_copilot.messages.transformation import ( + GithubCopilotAnthropicMessagesConfig, +) +from litellm.llms.github_copilot.common_utils import GetAPIKeyError + + +def test_github_copilot_anthropic_messages_config_init(): + """Test GithubCopilotAnthropicMessagesConfig initialization.""" + config = GithubCopilotAnthropicMessagesConfig() + assert config is not None + assert hasattr(config, "authenticator") + + +def test_github_copilot_anthropic_messages_get_complete_url(): + """Test URL construction for GitHub Copilot messages endpoint.""" + config = GithubCopilotAnthropicMessagesConfig() + + # Test with default api_base + url = config.get_complete_url( + api_base=None, + api_key=None, + model="github_copilot/claude-haiku-4.5", + optional_params={}, + litellm_params={}, + ) + assert url == "https://api.githubcopilot.com/v1/messages" + + # Test with custom api_base + url = config.get_complete_url( + api_base="https://custom.api.com", + api_key=None, + model="github_copilot/claude-haiku-4.5", + optional_params={}, + litellm_params={}, + ) + assert url == "https://custom.api.com/v1/messages" + + # Test with api_base already ending with /v1/messages + url = config.get_complete_url( + api_base="https://custom.api.com/v1/messages", + api_key=None, + model="github_copilot/claude-haiku-4.5", + optional_params={}, + litellm_params={}, + ) + assert url == "https://custom.api.com/v1/messages" + + +def test_github_copilot_anthropic_messages_validate_environment(): + """Test environment validation and header injection.""" + config = GithubCopilotAnthropicMessagesConfig() + + # 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 + + headers = {} + validated_headers, api_base = config.validate_anthropic_messages_environment( + headers=headers, + model="github_copilot/claude-haiku-4.5", + messages=[{"role": "user", "content": "Hello"}], + optional_params={}, + litellm_params={}, + api_key=None, + api_base=None, + ) + + # Check that Copilot headers were added + assert "copilot-integration-id" in validated_headers + assert validated_headers["copilot-integration-id"] == "vscode-chat" + assert "Authorization" in validated_headers + assert "anthropic-version" in validated_headers + assert validated_headers["anthropic-version"] == "2023-06-01" + assert api_base == "https://api.githubcopilot.com" + + +def test_github_copilot_anthropic_messages_validate_environment_auth_error(): + """Test error handling when authentication fails.""" + config = GithubCopilotAnthropicMessagesConfig() + + # Mock the authenticator to raise an error + config.authenticator = MagicMock() + config.authenticator.get_api_key.side_effect = GetAPIKeyError( + status_code=401, message="No valid API key found" + ) + + with pytest.raises(Exception): # AuthenticationError + config.validate_anthropic_messages_environment( + headers={}, + model="github_copilot/claude-haiku-4.5", + messages=[{"role": "user", "content": "Hello"}], + optional_params={}, + litellm_params={}, + api_key=None, + api_base=None, + ) + + +def test_github_copilot_anthropic_messages_supported_params(): + """Test supported parameters list.""" + config = GithubCopilotAnthropicMessagesConfig() + params = config.get_supported_anthropic_messages_params("github_copilot/claude-haiku-4.5") + + # Should inherit from AnthropicMessagesConfig + assert "messages" in params + assert "model" in params + assert "max_tokens" in params + assert "thinking" in params