chore: initial snapshot for gitea/github upload

This commit is contained in:
Your Name
2026-03-26 16:04:46 +08:00
commit a699a1ac98
3497 changed files with 1586237 additions and 0 deletions

View File

@@ -0,0 +1,337 @@
"""
GitHub Copilot Responses API Configuration.
This module provides the configuration for GitHub Copilot's Responses API,
which is required for models like gpt-5.1-codex that only support the /responses endpoint.
Implementation based on analysis of the copilot-api project by caozhiyuan:
https://github.com/caozhiyuan/copilot-api
"""
from typing import TYPE_CHECKING, Any, Dict, Optional, Union
from litellm._logging import verbose_logger
from litellm.constants import DEFAULT_MAX_RECURSE_DEPTH
from litellm.exceptions import AuthenticationError
from litellm.llms.openai.responses.transformation import OpenAIResponsesAPIConfig
from litellm.types.llms.openai import (
ResponseInputParam,
ResponsesAPIOptionalRequestParams,
)
from litellm.types.router import GenericLiteLLMParams
from litellm.types.utils import LlmProviders
from ..authenticator import Authenticator
from ..common_utils import (
GITHUB_COPILOT_API_BASE,
GetAPIKeyError,
get_copilot_default_headers,
)
if TYPE_CHECKING:
from litellm.litellm_core_utils.litellm_logging import Logging as _LiteLLMLoggingObj
LiteLLMLoggingObj = _LiteLLMLoggingObj
else:
LiteLLMLoggingObj = Any
class GithubCopilotResponsesAPIConfig(OpenAIResponsesAPIConfig):
"""
Configuration for GitHub Copilot's Responses API.
Inherits from OpenAIResponsesAPIConfig since GitHub Copilot's Responses API
is compatible with OpenAI's Responses API specification.
Key differences from OpenAI:
- Uses OAuth Device Flow authentication (handled by Authenticator)
- Uses api.githubcopilot.com as the API base
- Requires specific headers for VSCode/Copilot integration
- Supports vision requests with special header
- Requires X-Initiator header based on input analysis
Reference: https://api.githubcopilot.com/
"""
def __init__(self) -> None:
super().__init__()
self.authenticator = Authenticator()
@property
def custom_llm_provider(self) -> LlmProviders:
"""Return the GitHub Copilot provider identifier."""
return LlmProviders.GITHUB_COPILOT
def get_supported_openai_params(self, model: str) -> list:
"""
Get supported parameters for GitHub Copilot Responses API.
GitHub Copilot supports all standard OpenAI Responses API parameters.
"""
return super().get_supported_openai_params(model)
def map_openai_params(
self,
response_api_optional_params: ResponsesAPIOptionalRequestParams,
model: str,
drop_params: bool,
) -> Dict:
"""
Map parameters for GitHub Copilot Responses API.
GitHub Copilot uses the same parameter format as OpenAI,
so no transformation is needed.
"""
return dict(response_api_optional_params)
def validate_environment(
self,
headers: dict,
model: str,
litellm_params: Optional[GenericLiteLLMParams],
) -> dict:
"""
Validate environment and set up headers for GitHub Copilot API.
Uses the Authenticator to obtain GitHub Copilot API key via OAuth Device Flow,
then configures all required headers for the Responses API.
Headers include:
- Authorization with API key
- Standard GitHub Copilot headers (editor-version, user-agent, etc.)
- X-Initiator based on input analysis
- copilot-vision-request if vision content detected
- User-provided extra_headers (merged with priority)
"""
try:
# Get GitHub Copilot API key via OAuth
api_key = self.authenticator.get_api_key()
if not api_key:
raise AuthenticationError(
model=model,
llm_provider="github_copilot",
message="GitHub Copilot API key is required. Please authenticate via OAuth Device Flow.",
)
# Get default headers (from copilot-api configuration)
default_headers = get_copilot_default_headers(api_key)
# Merge with existing headers (user's extra_headers take priority)
merged_headers = {**default_headers, **headers}
# Analyze input to determine additional headers
input_param = self._get_input_from_params(litellm_params)
# Add X-Initiator header based on input analysis
if input_param is not None:
initiator = self._get_initiator(input_param)
merged_headers["X-Initiator"] = initiator
verbose_logger.debug(
f"GitHub Copilot Responses API: Set X-Initiator={initiator}"
)
# Add vision header if input contains images
if self._has_vision_input(input_param):
merged_headers["copilot-vision-request"] = "true"
verbose_logger.debug(
"GitHub Copilot Responses API: Enabled vision request"
)
verbose_logger.debug(
f"GitHub Copilot Responses API: Successfully configured headers for model {model}"
)
return merged_headers
except GetAPIKeyError as e:
raise AuthenticationError(
model=model,
llm_provider="github_copilot",
message=str(e),
)
def get_complete_url(
self,
api_base: Optional[str],
litellm_params: dict,
) -> str:
"""
Get the complete URL for GitHub Copilot Responses API endpoint.
Returns: https://api.githubcopilot.com/responses
Note: Currently only supports individual accounts.
Business/enterprise accounts (api.business.githubcopilot.com) can be
added in the future by detecting account type.
"""
# Use provided api_base or fall back to authenticator's base or default
api_base = (
api_base or self.authenticator.get_api_base() or GITHUB_COPILOT_API_BASE
)
# Remove trailing slashes
api_base = api_base.rstrip("/")
# Return the responses endpoint
return f"{api_base}/responses"
def _handle_reasoning_item(self, item: Dict[str, Any]) -> Dict[str, Any]:
"""
Handle reasoning items for GitHub Copilot, preserving encrypted_content.
GitHub Copilot uses encrypted_content in reasoning items to maintain
conversation state across turns. The parent class strips this field
when converting to OpenAI's ResponseReasoningItem model, which causes
"encrypted content could not be verified" errors on multi-turn requests.
This override preserves encrypted_content while still filtering out
status=None which OpenAI's API rejects.
"""
if item.get("type") == "reasoning":
# Preserve encrypted_content before parent processing
encrypted_content = item.get("encrypted_content")
# Filter out None values for known problematic fields,
# but preserve encrypted_content even if it exists
filtered_item: Dict[str, Any] = {}
for k, v in item.items():
# Always include encrypted_content if present (even if None)
if k == "encrypted_content":
if encrypted_content is not None:
filtered_item[k] = v
continue
# Filter out status=None which OpenAI API rejects
if k == "status" and v is None:
continue
# Include all other non-None values
if v is not None:
filtered_item[k] = v
verbose_logger.debug(
f"GitHub Copilot reasoning item processed, encrypted_content preserved: {encrypted_content is not None}"
)
return filtered_item
return item
# ==================== Helper Methods ====================
def _get_input_from_params(
self, litellm_params: Optional[GenericLiteLLMParams]
) -> Optional[Union[str, ResponseInputParam]]:
"""
Extract input parameter from litellm_params.
The input parameter contains the conversation history and is needed
for vision detection and initiator determination.
"""
if litellm_params is None:
return None
# Try to get input from litellm_params
# This might be in different locations depending on how LiteLLM structures it
if hasattr(litellm_params, "input"):
return litellm_params.input
# If not found, return None and let the API handle it
return None
def _get_initiator(self, input_param: Union[str, ResponseInputParam]) -> str:
"""
Determine X-Initiator header value based on input analysis.
Based on copilot-api's hasAgentInitiator logic:
- Returns "agent" if input contains assistant role or items without role
- Returns "user" otherwise
Args:
input_param: The input parameter (string or list of input items)
Returns:
"agent" or "user"
"""
# If input is a string, it's user-initiated
if isinstance(input_param, str):
return "user"
# If input is a list, analyze items
if isinstance(input_param, list):
for item in input_param:
if not isinstance(item, dict):
continue
# Check if item has no role (agent-initiated)
if "role" not in item or not item.get("role"):
return "agent"
# Check if role is assistant (agent-initiated)
role = item.get("role")
if isinstance(role, str) and role.lower() == "assistant":
return "agent"
# Default to user-initiated
return "user"
def _has_vision_input(self, input_param: Union[str, ResponseInputParam]) -> bool:
"""
Check if input contains vision content (images).
Based on copilot-api's hasVisionInput and containsVisionContent logic.
Recursively searches for input_image type in the input structure.
Args:
input_param: The input parameter to analyze
Returns:
True if input contains image content, False otherwise
"""
return self._contains_vision_content(input_param)
def _contains_vision_content(
self, value: Any, depth: int = 0, max_depth: int = DEFAULT_MAX_RECURSE_DEPTH
) -> bool:
"""
Recursively check if a value contains vision content.
Looks for items with type="input_image" in the structure.
"""
if depth > max_depth:
verbose_logger.warning(
f"[GitHub Copilot] Max recursion depth {max_depth} reached while checking for vision content"
)
return False
if value is None:
return False
# Check arrays
if isinstance(value, list):
return any(
self._contains_vision_content(
item, depth=depth + 1, max_depth=max_depth
)
for item in value
)
# Only check dict/object types
if not isinstance(value, dict):
return False
# Check if this item is an input_image
item_type = value.get("type")
if isinstance(item_type, str) and item_type.lower() == "input_image":
return True
# Check content field recursively
if "content" in value and isinstance(value["content"], list):
return any(
self._contains_vision_content(
item, depth=depth + 1, max_depth=max_depth
)
for item in value["content"]
)
return False
def supports_native_websocket(self) -> bool:
"""GitHub Copilot does not support native WebSocket for Responses API"""
return False