mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-08 03:08:45 +00:00
fix: exchange GitHub access token for inference token in embedding/responses
The embedding and responses transformations were passing the raw GitHub access token (ghu_xxx) as the Authorization header instead of exchanging it for a short-lived Copilot inference token. Also fix api_base precedence so caller-provided values win, update authenticator docstring and error message to mention both SDK and proxy auth paths. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
a0efd2bdf5
commit
788da36fea
5 changed files with 56 additions and 27 deletions
|
|
@ -60,13 +60,16 @@ class Authenticator:
|
|||
|
||||
def get_access_token(self) -> str:
|
||||
"""
|
||||
Login to Copilot with retry 3 times.
|
||||
Return the GitHub access token.
|
||||
|
||||
In credential mode, returns the injected token directly.
|
||||
In file-based mode (SDK), reads from the access-token file on disk.
|
||||
|
||||
Returns:
|
||||
str: The GitHub access token.
|
||||
|
||||
Raises:
|
||||
GetAccessTokenError: If unable to obtain an access token after retries.
|
||||
GetAccessTokenError: If no access token is available.
|
||||
"""
|
||||
if self._injected_access_token is not None:
|
||||
return self._injected_access_token
|
||||
|
|
@ -82,7 +85,8 @@ class Authenticator:
|
|||
raise GetAccessTokenError(
|
||||
message=(
|
||||
"No GitHub Copilot access token configured. "
|
||||
"Use a named credential via the LiteLLM proxy or UI before making requests. "
|
||||
"Run `litellm --login github_copilot` (SDK) or create a named credential "
|
||||
"via the LiteLLM proxy UI. "
|
||||
"See: https://docs.litellm.ai/docs/providers/github_copilot"
|
||||
),
|
||||
status_code=401,
|
||||
|
|
|
|||
|
|
@ -64,10 +64,11 @@ class GithubCopilotEmbeddingConfig(BaseEmbeddingConfig):
|
|||
message="GitHub Copilot API key is required. Please authenticate via OAuth Device Flow.",
|
||||
)
|
||||
|
||||
# api_key at this point is already the resolved copilot inference
|
||||
# token (exchanged by the provider info step or main.py).
|
||||
# Just use it directly — no re-authentication needed.
|
||||
default_headers = get_copilot_default_headers(api_key)
|
||||
# api_key is the GitHub access token (ghu_xxx). Exchange it for
|
||||
# a short-lived Copilot inference token (cached at module level).
|
||||
authenticator = Authenticator(access_token=api_key)
|
||||
inference_token = authenticator.get_api_key()
|
||||
default_headers = get_copilot_default_headers(inference_token)
|
||||
|
||||
# Merge with existing headers (user's extra_headers take priority)
|
||||
merged_headers = {**default_headers, **headers}
|
||||
|
|
@ -99,7 +100,8 @@ class GithubCopilotEmbeddingConfig(BaseEmbeddingConfig):
|
|||
"""
|
||||
# Use provided api_base or fall back to credential-resolved base or default
|
||||
if api_key:
|
||||
api_base = Authenticator(access_token=api_key).get_api_base() or api_base or GITHUB_COPILOT_API_BASE
|
||||
derived_api_base = Authenticator(access_token=api_key).get_api_base()
|
||||
api_base = api_base or derived_api_base or GITHUB_COPILOT_API_BASE
|
||||
else:
|
||||
api_base = api_base or GITHUB_COPILOT_API_BASE
|
||||
|
||||
|
|
|
|||
|
|
@ -106,7 +106,9 @@ class GithubCopilotResponsesAPIConfig(OpenAIResponsesAPIConfig):
|
|||
if isinstance(litellm_params, dict):
|
||||
_api_key = litellm_params.get("api_key")
|
||||
else:
|
||||
_api_key = getattr(litellm_params, "api_key", None) if litellm_params else None
|
||||
_api_key = (
|
||||
getattr(litellm_params, "api_key", None) if litellm_params else None
|
||||
)
|
||||
if not _api_key:
|
||||
raise AuthenticationError(
|
||||
model=model,
|
||||
|
|
@ -114,10 +116,11 @@ class GithubCopilotResponsesAPIConfig(OpenAIResponsesAPIConfig):
|
|||
message="GitHub Copilot API key is required. Please authenticate via OAuth Device Flow.",
|
||||
)
|
||||
|
||||
# api_key at this point is already the resolved copilot inference
|
||||
# token (exchanged by _get_openai_compatible_provider_info or
|
||||
# main.py). Just use it directly — no re-authentication needed.
|
||||
default_headers = get_copilot_default_headers(_api_key)
|
||||
# _api_key is the GitHub access token (ghu_xxx). Exchange it for
|
||||
# a short-lived Copilot inference token (cached at module level).
|
||||
authenticator = Authenticator(access_token=_api_key)
|
||||
inference_token = authenticator.get_api_key()
|
||||
default_headers = get_copilot_default_headers(inference_token)
|
||||
|
||||
# Merge with existing headers (user's extra_headers take priority)
|
||||
merged_headers = {**default_headers, **headers}
|
||||
|
|
@ -168,9 +171,15 @@ class GithubCopilotResponsesAPIConfig(OpenAIResponsesAPIConfig):
|
|||
added in the future by detecting account type.
|
||||
"""
|
||||
# Use provided api_base or fall back to credential-resolved base or default
|
||||
_api_key = litellm_params.get("api_key") if isinstance(litellm_params, dict) else None
|
||||
_api_key = (
|
||||
litellm_params.get("api_key") if isinstance(litellm_params, dict) else None
|
||||
)
|
||||
if _api_key:
|
||||
api_base = api_base or Authenticator(access_token=_api_key).get_api_base() or GITHUB_COPILOT_API_BASE
|
||||
api_base = (
|
||||
api_base
|
||||
or Authenticator(access_token=_api_key).get_api_base()
|
||||
or GITHUB_COPILOT_API_BASE
|
||||
)
|
||||
else:
|
||||
api_base = api_base or GITHUB_COPILOT_API_BASE
|
||||
|
||||
|
|
|
|||
|
|
@ -10,8 +10,13 @@ from litellm.exceptions import AuthenticationError
|
|||
from litellm.llms.github_copilot.embedding.transformation import GithubCopilotEmbeddingConfig
|
||||
from litellm.llms.github_copilot.common_utils import GetAPIKeyError
|
||||
|
||||
def test_github_copilot_embedding_config_validate_environment():
|
||||
@patch("litellm.llms.github_copilot.embedding.transformation.Authenticator")
|
||||
def test_github_copilot_embedding_config_validate_environment(mock_authenticator_class):
|
||||
"""Test the GitHub Copilot embedding configuration environment validation."""
|
||||
mock_auth_instance = MagicMock()
|
||||
mock_auth_instance.get_api_key.return_value = "copilot-inference-token"
|
||||
mock_authenticator_class.return_value = mock_auth_instance
|
||||
|
||||
config = GithubCopilotEmbeddingConfig()
|
||||
model = "github_copilot/text-embedding-3-small"
|
||||
|
||||
|
|
@ -24,8 +29,9 @@ def test_github_copilot_embedding_config_validate_environment():
|
|||
api_key="gh-access-token",
|
||||
)
|
||||
|
||||
# api_key is used directly — no re-exchange in validate_environment
|
||||
assert validated_headers["Authorization"] == "Bearer gh-access-token"
|
||||
# Access token is exchanged for inference token
|
||||
mock_authenticator_class.assert_called_with(access_token="gh-access-token")
|
||||
assert validated_headers["Authorization"] == "Bearer copilot-inference-token"
|
||||
assert validated_headers["copilot-integration-id"] == "vscode-chat"
|
||||
assert validated_headers["editor-version"] == "vscode/1.95.0"
|
||||
assert "x-request-id" in validated_headers
|
||||
|
|
@ -42,9 +48,6 @@ def test_github_copilot_embedding_config_validate_environment():
|
|||
)
|
||||
assert "required" in str(excinfo.value).lower()
|
||||
|
||||
# Auth failures now happen upstream (in _get_openai_compatible_provider_info
|
||||
# or main.py), not in validate_environment. No re-exchange test needed.
|
||||
|
||||
@patch("litellm.llms.github_copilot.embedding.transformation.Authenticator")
|
||||
def test_github_copilot_embedding_config_get_complete_url(mock_authenticator_class):
|
||||
"""Test the GitHub Copilot embedding configuration URL generation."""
|
||||
|
|
|
|||
|
|
@ -82,16 +82,22 @@ class TestGithubCopilotResponsesAPITransformation:
|
|||
"Should handle trailing slash"
|
||||
)
|
||||
|
||||
def test_validate_environment_default_headers(self):
|
||||
@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_auth_instance = MagicMock()
|
||||
mock_auth_instance.get_api_key.return_value = "copilot-inference-token"
|
||||
mock_authenticator_class.return_value = mock_auth_instance
|
||||
|
||||
config = GithubCopilotResponsesAPIConfig()
|
||||
|
||||
headers = config.validate_environment(
|
||||
headers={}, model="gpt-5.1-codex", litellm_params={"api_key": "gh-access-token"}
|
||||
)
|
||||
|
||||
# api_key is used directly — no re-exchange in validate_environment
|
||||
assert headers["Authorization"] == "Bearer gh-access-token"
|
||||
# Access token is exchanged for inference token
|
||||
mock_authenticator_class.assert_called_with(access_token="gh-access-token")
|
||||
assert headers["Authorization"] == "Bearer copilot-inference-token"
|
||||
assert headers["content-type"] == "application/json"
|
||||
assert headers["copilot-integration-id"] == "vscode-chat"
|
||||
assert headers["editor-version"] == "vscode/1.95.0"
|
||||
|
|
@ -101,8 +107,13 @@ class TestGithubCopilotResponsesAPITransformation:
|
|||
assert headers["x-github-api-version"] == "2025-04-01"
|
||||
assert "x-request-id" in headers
|
||||
|
||||
def test_validate_environment_user_headers_override(self):
|
||||
@patch("litellm.llms.github_copilot.responses.transformation.Authenticator")
|
||||
def test_validate_environment_user_headers_override(self, mock_authenticator_class):
|
||||
"""Test that user-provided headers override default headers"""
|
||||
mock_auth_instance = MagicMock()
|
||||
mock_auth_instance.get_api_key.return_value = "copilot-inference-token"
|
||||
mock_authenticator_class.return_value = mock_auth_instance
|
||||
|
||||
config = GithubCopilotResponsesAPIConfig()
|
||||
|
||||
custom_headers = {
|
||||
|
|
@ -118,8 +129,8 @@ class TestGithubCopilotResponsesAPITransformation:
|
|||
assert headers["editor-version"] == "custom/2.0.0"
|
||||
# Custom header should be preserved
|
||||
assert headers["custom-header"] == "custom-value"
|
||||
# api_key used directly
|
||||
assert headers["Authorization"] == "Bearer gh-access-token"
|
||||
# Access token exchanged for inference token
|
||||
assert headers["Authorization"] == "Bearer copilot-inference-token"
|
||||
|
||||
def test_get_initiator_with_assistant_role(self):
|
||||
"""Test _get_initiator returns 'agent' for assistant role"""
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue