mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-10 22:41:41 +00:00
fix(github-copilot): make client headers configurable
This commit is contained in:
parent
5290150a05
commit
e264b893bf
5 changed files with 169 additions and 60 deletions
|
|
@ -15,6 +15,7 @@ from .common_utils import (
|
|||
GetAPIKeyError,
|
||||
GetDeviceCodeError,
|
||||
RefreshAPIKeyError,
|
||||
get_copilot_default_headers,
|
||||
)
|
||||
|
||||
# Constants (default values — overridable via environment variables at call time)
|
||||
|
|
@ -188,30 +189,7 @@ class Authenticator:
|
|||
os.makedirs(self.token_dir, exist_ok=True)
|
||||
|
||||
def _get_github_headers(self, access_token: str | None = None) -> dict[str, str]:
|
||||
"""
|
||||
Generate standard GitHub headers for API requests.
|
||||
|
||||
Args:
|
||||
access_token: Optional access token to include in the headers.
|
||||
|
||||
Returns:
|
||||
Dict[str, str]: Headers for GitHub API requests.
|
||||
"""
|
||||
headers: Final = {
|
||||
"accept": "application/json",
|
||||
"editor-version": "vscode/1.85.1",
|
||||
"editor-plugin-version": "copilot/1.155.0",
|
||||
"user-agent": "GithubCopilot/1.155.0",
|
||||
"accept-encoding": "gzip,deflate,br",
|
||||
}
|
||||
|
||||
if access_token:
|
||||
headers["authorization"] = f"token {access_token}"
|
||||
|
||||
if "content-type" not in headers:
|
||||
headers["content-type"] = "application/json"
|
||||
|
||||
return headers
|
||||
return get_copilot_default_headers(access_token=access_token)
|
||||
|
||||
def _get_device_code(self) -> dict[str, str]:
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
Constants for Copilot integration
|
||||
"""
|
||||
|
||||
import os
|
||||
from typing import Final
|
||||
from uuid import uuid4
|
||||
|
||||
|
|
@ -9,12 +10,31 @@ import httpx
|
|||
|
||||
from litellm.llms.base_llm.chat.transformation import BaseLLMException
|
||||
|
||||
# Constants
|
||||
COPILOT_VERSION: Final = "0.26.7"
|
||||
EDITOR_PLUGIN_VERSION: Final = f"copilot-chat/{COPILOT_VERSION}"
|
||||
USER_AGENT: Final = f"GitHubCopilotChat/{COPILOT_VERSION}"
|
||||
API_VERSION: Final = "2025-04-01"
|
||||
DEFAULT_GITHUB_COPILOT_API_BASE: Final = "https://api.githubcopilot.com"
|
||||
DEFAULT_COPILOT_INTEGRATION_ID: Final = "vscode-chat"
|
||||
DEFAULT_COPILOT_EDITOR_VERSION: Final = "vscode/1.115.0"
|
||||
DEFAULT_COPILOT_EDITOR_PLUGIN_VERSION: Final = EDITOR_PLUGIN_VERSION
|
||||
DEFAULT_COPILOT_USER_AGENT: Final = USER_AGENT
|
||||
|
||||
_COPILOT_HEADER_CONFIG = (
|
||||
("accept", "GITHUB_COPILOT_ACCEPT", "application/json"),
|
||||
("content-type", "GITHUB_COPILOT_CONTENT_TYPE", "application/json"),
|
||||
("copilot-integration-id", "GITHUB_COPILOT_INTEGRATION_ID", DEFAULT_COPILOT_INTEGRATION_ID),
|
||||
("editor-version", "GITHUB_COPILOT_EDITOR_VERSION", DEFAULT_COPILOT_EDITOR_VERSION),
|
||||
("editor-plugin-version", "GITHUB_COPILOT_EDITOR_PLUGIN_VERSION", DEFAULT_COPILOT_EDITOR_PLUGIN_VERSION),
|
||||
("user-agent", "GITHUB_COPILOT_USER_AGENT", DEFAULT_COPILOT_USER_AGENT),
|
||||
("openai-intent", "GITHUB_COPILOT_OPENAI_INTENT", None),
|
||||
("x-github-api-version", "GITHUB_COPILOT_API_VERSION", None),
|
||||
(
|
||||
"x-vscode-user-agent-library-version",
|
||||
"GITHUB_COPILOT_USER_AGENT_LIBRARY_VERSION",
|
||||
None,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
class GithubCopilotError(BaseLLMException):
|
||||
|
|
@ -57,21 +77,23 @@ class GetAPIKeyError(GithubCopilotError):
|
|||
pass
|
||||
|
||||
|
||||
def get_copilot_default_headers(api_key: str) -> dict:
|
||||
"""
|
||||
Get default headers for GitHub Copilot Responses API.
|
||||
def _get_copilot_header_value(environment_variable: str, default: str | None) -> str | None:
|
||||
value = os.getenv(environment_variable)
|
||||
if value is None:
|
||||
return default
|
||||
return value or None
|
||||
|
||||
Based on copilot-api's header configuration.
|
||||
"""
|
||||
return {
|
||||
"Authorization": f"Bearer {api_key}",
|
||||
"content-type": "application/json",
|
||||
"copilot-integration-id": "vscode-chat",
|
||||
"editor-version": "vscode/1.95.0", # Fixed version for stability
|
||||
"editor-plugin-version": EDITOR_PLUGIN_VERSION,
|
||||
"user-agent": USER_AGENT,
|
||||
"openai-intent": "conversation-panel",
|
||||
"x-github-api-version": API_VERSION,
|
||||
"x-request-id": str(uuid4()),
|
||||
"x-vscode-user-agent-library-version": "electron-fetch",
|
||||
|
||||
def get_copilot_default_headers(
|
||||
api_key: str | None = None,
|
||||
*,
|
||||
access_token: str | None = None,
|
||||
) -> dict[str, str]:
|
||||
configured_headers = {
|
||||
header: value
|
||||
for header, environment_variable, default in _COPILOT_HEADER_CONFIG
|
||||
if (value := _get_copilot_header_value(environment_variable, default)) is not None
|
||||
}
|
||||
authorization = f"token {access_token}" if access_token else f"Bearer {api_key}" if api_key else None
|
||||
authorization_header = {"Authorization": authorization} if authorization else {}
|
||||
return {**configured_headers, **authorization_header}
|
||||
|
|
|
|||
|
|
@ -37,8 +37,8 @@ def test_github_copilot_embedding_config_validate_environment():
|
|||
|
||||
assert validated_headers["Authorization"] == f"Bearer {mock_api_key}"
|
||||
assert validated_headers["copilot-integration-id"] == "vscode-chat"
|
||||
assert validated_headers["editor-version"] == "vscode/1.95.0"
|
||||
assert "x-request-id" in validated_headers
|
||||
assert validated_headers["editor-version"] == "vscode/1.115.0"
|
||||
assert "x-request-id" not in validated_headers
|
||||
|
||||
# Test with authentication failure
|
||||
config.authenticator.get_api_key.side_effect = GetAPIKeyError(
|
||||
|
|
|
|||
|
|
@ -92,29 +92,80 @@ class TestGithubCopilotResponsesAPITransformation:
|
|||
), "Should handle trailing slash"
|
||||
|
||||
@patch("litellm.llms.github_copilot.responses.transformation.Authenticator")
|
||||
def test_validate_environment_default_headers(self, mock_authenticator_class):
|
||||
"""Test that validate_environment generates correct default headers"""
|
||||
# Mock the authenticator
|
||||
def test_validate_environment_default_headers(self, mock_authenticator_class, monkeypatch):
|
||||
for environment_variable in (
|
||||
"GITHUB_COPILOT_OPENAI_INTENT",
|
||||
"GITHUB_COPILOT_API_VERSION",
|
||||
"GITHUB_COPILOT_USER_AGENT_LIBRARY_VERSION",
|
||||
):
|
||||
monkeypatch.delenv(environment_variable, raising=False)
|
||||
|
||||
mock_auth_instance = MagicMock()
|
||||
mock_auth_instance.get_api_key.return_value = "test-api-key-123"
|
||||
mock_authenticator_class.return_value = mock_auth_instance
|
||||
|
||||
config = GithubCopilotResponsesAPIConfig()
|
||||
|
||||
headers = config.validate_environment(
|
||||
headers={}, model="gpt-5.1-codex", litellm_params={}
|
||||
)
|
||||
|
||||
# Check required headers
|
||||
assert headers["Authorization"] == "Bearer test-api-key-123"
|
||||
assert headers["accept"] == "application/json"
|
||||
assert headers["content-type"] == "application/json"
|
||||
assert headers["copilot-integration-id"] == "vscode-chat"
|
||||
assert headers["editor-version"] == "vscode/1.95.0"
|
||||
assert headers["editor-plugin-version"] == "copilot-chat/0.26.7"
|
||||
assert headers["user-agent"] == "GitHubCopilotChat/0.26.7"
|
||||
assert headers["openai-intent"] == "conversation-panel"
|
||||
assert headers["x-github-api-version"] == "2025-04-01"
|
||||
assert "x-request-id" in headers
|
||||
assert headers["editor-version"] == "vscode/1.115.0"
|
||||
assert headers["editor-plugin-version"] == "copilot-chat/0.44.0"
|
||||
assert headers["user-agent"] == "GitHubCopilotChat/0.44.0"
|
||||
assert "openai-intent" not in headers
|
||||
assert "x-github-api-version" not in headers
|
||||
assert "x-request-id" not in headers
|
||||
assert "x-vscode-user-agent-library-version" not in headers
|
||||
|
||||
@patch("litellm.llms.github_copilot.responses.transformation.Authenticator")
|
||||
def test_validate_environment_headers_from_environment(self, mock_authenticator_class):
|
||||
mock_auth_instance = MagicMock()
|
||||
mock_auth_instance.get_api_key.return_value = "test-api-key-123"
|
||||
mock_authenticator_class.return_value = mock_auth_instance
|
||||
environment = {
|
||||
"GITHUB_COPILOT_ACCEPT": "application/vnd.github+json",
|
||||
"GITHUB_COPILOT_CONTENT_TYPE": "application/custom+json",
|
||||
"GITHUB_COPILOT_INTEGRATION_ID": "custom-integration",
|
||||
"GITHUB_COPILOT_EDITOR_VERSION": "custom-editor/1.0",
|
||||
"GITHUB_COPILOT_EDITOR_PLUGIN_VERSION": "custom-plugin/2.0",
|
||||
"GITHUB_COPILOT_USER_AGENT": "CustomAgent/2.0",
|
||||
"GITHUB_COPILOT_OPENAI_INTENT": "custom-intent",
|
||||
"GITHUB_COPILOT_API_VERSION": "2099-01-01",
|
||||
"GITHUB_COPILOT_USER_AGENT_LIBRARY_VERSION": "custom-library",
|
||||
}
|
||||
|
||||
with patch.dict(os.environ, environment):
|
||||
headers = GithubCopilotResponsesAPIConfig().validate_environment(
|
||||
headers={}, model="gpt-5.1-codex", litellm_params={}
|
||||
)
|
||||
|
||||
assert headers["Authorization"] == "Bearer test-api-key-123"
|
||||
assert headers["accept"] == "application/vnd.github+json"
|
||||
assert headers["content-type"] == "application/custom+json"
|
||||
assert headers["copilot-integration-id"] == "custom-integration"
|
||||
assert headers["editor-version"] == "custom-editor/1.0"
|
||||
assert headers["editor-plugin-version"] == "custom-plugin/2.0"
|
||||
assert headers["user-agent"] == "CustomAgent/2.0"
|
||||
assert headers["openai-intent"] == "custom-intent"
|
||||
assert headers["x-github-api-version"] == "2099-01-01"
|
||||
assert headers["x-vscode-user-agent-library-version"] == "custom-library"
|
||||
|
||||
@patch("litellm.llms.github_copilot.responses.transformation.Authenticator")
|
||||
def test_empty_environment_header_omits_default(self, mock_authenticator_class):
|
||||
mock_auth_instance = MagicMock()
|
||||
mock_auth_instance.get_api_key.return_value = "test-api-key-123"
|
||||
mock_authenticator_class.return_value = mock_auth_instance
|
||||
|
||||
with patch.dict(os.environ, {"GITHUB_COPILOT_USER_AGENT": ""}):
|
||||
headers = GithubCopilotResponsesAPIConfig().validate_environment(
|
||||
headers={}, model="gpt-5.1-codex", litellm_params={}
|
||||
)
|
||||
|
||||
assert "user-agent" not in headers
|
||||
|
||||
@patch("litellm.llms.github_copilot.responses.transformation.Authenticator")
|
||||
def test_validate_environment_user_headers_override(self, mock_authenticator_class):
|
||||
|
|
|
|||
|
|
@ -58,15 +58,73 @@ class TestGitHubCopilotAuthenticator:
|
|||
mock_makedirs.assert_called_once_with(auth.token_dir, exist_ok=True)
|
||||
|
||||
def test_get_github_headers(self, authenticator):
|
||||
"""Test that GitHub headers are correctly generated."""
|
||||
headers = authenticator._get_github_headers()
|
||||
assert "accept" in headers
|
||||
assert "editor-version" in headers
|
||||
assert "user-agent" in headers
|
||||
assert "content-type" in headers
|
||||
assert headers == {
|
||||
"accept": "application/json",
|
||||
"content-type": "application/json",
|
||||
"copilot-integration-id": "vscode-chat",
|
||||
"editor-version": "vscode/1.115.0",
|
||||
"editor-plugin-version": "copilot-chat/0.44.0",
|
||||
"user-agent": "GitHubCopilotChat/0.44.0",
|
||||
}
|
||||
|
||||
headers_with_token = authenticator._get_github_headers("test-token")
|
||||
assert headers_with_token["authorization"] == "token test-token"
|
||||
assert headers_with_token["Authorization"] == "token test-token"
|
||||
|
||||
def test_auth_requests_use_custom_copilot_headers(self, authenticator, mock_http_client):
|
||||
mock_client, mock_response = mock_http_client
|
||||
mock_response.json.side_effect = (
|
||||
{
|
||||
"device_code": "dc",
|
||||
"user_code": "UC",
|
||||
"verification_uri": "https://example.com",
|
||||
},
|
||||
{"access_token": "access-token"},
|
||||
{"token": "api-token", "expires_at": 9999999999},
|
||||
)
|
||||
environment = {
|
||||
"GITHUB_COPILOT_ACCEPT": "application/vnd.github+json",
|
||||
"GITHUB_COPILOT_CONTENT_TYPE": "application/custom+json",
|
||||
"GITHUB_COPILOT_INTEGRATION_ID": "custom-integration",
|
||||
"GITHUB_COPILOT_EDITOR_VERSION": "custom-editor/1.0",
|
||||
"GITHUB_COPILOT_EDITOR_PLUGIN_VERSION": "custom-plugin/2.0",
|
||||
"GITHUB_COPILOT_USER_AGENT": "CustomAgent/2.0",
|
||||
"GITHUB_COPILOT_OPENAI_INTENT": "custom-intent",
|
||||
"GITHUB_COPILOT_API_VERSION": "2099-01-01",
|
||||
"GITHUB_COPILOT_USER_AGENT_LIBRARY_VERSION": "custom-library",
|
||||
}
|
||||
|
||||
with (
|
||||
patch.dict(os.environ, environment),
|
||||
patch(
|
||||
"litellm.llms.github_copilot.authenticator._get_httpx_client",
|
||||
return_value=mock_client,
|
||||
),
|
||||
patch.object(authenticator, "get_access_token", return_value="github-token"),
|
||||
):
|
||||
authenticator._get_device_code()
|
||||
authenticator._poll_for_access_token("dc")
|
||||
authenticator._refresh_api_key()
|
||||
|
||||
request_headers = (
|
||||
mock_client.post.call_args_list[0].kwargs["headers"],
|
||||
mock_client.post.call_args_list[1].kwargs["headers"],
|
||||
mock_client.get.call_args.kwargs["headers"],
|
||||
)
|
||||
for headers in request_headers:
|
||||
assert headers["accept"] == "application/vnd.github+json"
|
||||
assert headers["content-type"] == "application/custom+json"
|
||||
assert headers["copilot-integration-id"] == "custom-integration"
|
||||
assert headers["editor-version"] == "custom-editor/1.0"
|
||||
assert headers["editor-plugin-version"] == "custom-plugin/2.0"
|
||||
assert headers["user-agent"] == "CustomAgent/2.0"
|
||||
assert headers["openai-intent"] == "custom-intent"
|
||||
assert headers["x-github-api-version"] == "2099-01-01"
|
||||
assert headers["x-vscode-user-agent-library-version"] == "custom-library"
|
||||
|
||||
assert "Authorization" not in request_headers[0]
|
||||
assert "Authorization" not in request_headers[1]
|
||||
assert request_headers[2]["Authorization"] == "token github-token"
|
||||
|
||||
def test_get_access_token_from_file(self, authenticator):
|
||||
"""Test retrieving an access token from a file."""
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue