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.
This commit is contained in:
ririnto 2026-05-16 19:00:28 +09:00
parent 9c3ad1b094
commit 3063124e37
6 changed files with 211 additions and 3 deletions

View file

@ -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

View file

@ -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,

View file

@ -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

View file

@ -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