mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-07 08:26:10 +00:00
feat(github-copilot): Add Responses API support for gpt-5.1-codex model (#16845)
- Implement GithubCopilotResponsesAPIConfig for /responses endpoint - Add support for models requiring responses API (e.g., gpt-5.1-codex) - Auto-detect vision requests and set X-Initiator header - Follow OpenAI Responses API compatibility pattern - Add comprehensive unit tests (16 tests passing) Fixes #16820
This commit is contained in:
parent
3ebe489082
commit
98dd866b26
4 changed files with 610 additions and 0 deletions
|
|
@ -1342,6 +1342,7 @@ from .llms.watsonx.completion.transformation import IBMWatsonXAIConfig
|
|||
from .llms.watsonx.chat.transformation import IBMWatsonXChatConfig
|
||||
from .llms.watsonx.embed.transformation import IBMWatsonXEmbeddingConfig
|
||||
from .llms.github_copilot.chat.transformation import GithubCopilotConfig
|
||||
from .llms.github_copilot.responses.transformation import GithubCopilotResponsesAPIConfig
|
||||
from .llms.nebius.chat.transformation import NebiusConfig
|
||||
from .llms.wandb.chat.transformation import WandbConfig
|
||||
from .llms.dashscope.chat.transformation import DashScopeChatConfig
|
||||
|
|
|
|||
304
litellm/llms/github_copilot/responses/transformation.py
Normal file
304
litellm/llms/github_copilot/responses/transformation.py
Normal file
|
|
@ -0,0 +1,304 @@
|
|||
"""
|
||||
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 uuid import uuid4
|
||||
|
||||
from litellm._logging import verbose_logger
|
||||
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 GetAPIKeyError
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from litellm.litellm_core_utils.litellm_logging import Logging as _LiteLLMLoggingObj
|
||||
|
||||
LiteLLMLoggingObj = _LiteLLMLoggingObj
|
||||
else:
|
||||
LiteLLMLoggingObj = Any
|
||||
|
||||
# GitHub Copilot API Constants (from copilot-api)
|
||||
COPILOT_VERSION = "0.26.7"
|
||||
EDITOR_PLUGIN_VERSION = f"copilot-chat/{COPILOT_VERSION}"
|
||||
USER_AGENT = f"GitHubCopilotChat/{COPILOT_VERSION}"
|
||||
API_VERSION = "2025-04-01"
|
||||
|
||||
|
||||
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/
|
||||
"""
|
||||
|
||||
GITHUB_COPILOT_API_BASE = "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 = self._get_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 self.GITHUB_COPILOT_API_BASE
|
||||
)
|
||||
|
||||
# Remove trailing slashes
|
||||
api_base = api_base.rstrip("/")
|
||||
|
||||
# Return the responses endpoint
|
||||
return f"{api_base}/responses"
|
||||
|
||||
# ==================== Helper Methods ====================
|
||||
|
||||
def _get_default_headers(self, api_key: str) -> Dict[str, str]:
|
||||
"""
|
||||
Get default headers for GitHub Copilot Responses API.
|
||||
|
||||
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_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) -> bool:
|
||||
"""
|
||||
Recursively check if a value contains vision content.
|
||||
|
||||
Looks for items with type="input_image" in the structure.
|
||||
"""
|
||||
if value is None:
|
||||
return False
|
||||
|
||||
# Check arrays
|
||||
if isinstance(value, list):
|
||||
return any(self._contains_vision_content(item) 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) for item in value["content"]
|
||||
)
|
||||
|
||||
return False
|
||||
|
|
@ -7389,6 +7389,8 @@ class ProviderConfigManager:
|
|||
return litellm.AzureOpenAIResponsesAPIConfig()
|
||||
elif litellm.LlmProviders.XAI == provider:
|
||||
return litellm.XAIResponsesAPIConfig()
|
||||
elif litellm.LlmProviders.GITHUB_COPILOT == provider:
|
||||
return litellm.GithubCopilotResponsesAPIConfig()
|
||||
elif litellm.LlmProviders.LITELLM_PROXY == provider:
|
||||
return litellm.LiteLLMProxyResponsesAPIConfig()
|
||||
return None
|
||||
|
|
|
|||
|
|
@ -0,0 +1,303 @@
|
|||
"""
|
||||
Tests for GitHub Copilot Responses API transformation
|
||||
|
||||
Tests the GithubCopilotResponsesAPIConfig class that handles GitHub Copilot-specific
|
||||
transformations for the Responses API.
|
||||
|
||||
Source: litellm/llms/github_copilot/responses/transformation.py
|
||||
"""
|
||||
import sys
|
||||
import os
|
||||
from unittest.mock import patch, MagicMock
|
||||
|
||||
sys.path.insert(0, os.path.abspath("../../../../.."))
|
||||
|
||||
import pytest
|
||||
from litellm.types.utils import LlmProviders
|
||||
from litellm.utils import ProviderConfigManager
|
||||
from litellm.llms.github_copilot.responses.transformation import (
|
||||
GithubCopilotResponsesAPIConfig,
|
||||
)
|
||||
from litellm.types.llms.openai import ResponsesAPIOptionalRequestParams
|
||||
|
||||
|
||||
class TestGithubCopilotResponsesAPITransformation:
|
||||
"""Test GitHub Copilot Responses API configuration and transformations"""
|
||||
|
||||
def test_github_copilot_provider_config_registration(self):
|
||||
"""Test that GitHub Copilot provider returns GithubCopilotResponsesAPIConfig"""
|
||||
config = ProviderConfigManager.get_provider_responses_api_config(
|
||||
model="github_copilot/gpt-5.1-codex",
|
||||
provider=LlmProviders.GITHUB_COPILOT,
|
||||
)
|
||||
|
||||
assert (
|
||||
config is not None
|
||||
), "Config should not be None for GitHub Copilot provider"
|
||||
assert isinstance(
|
||||
config, GithubCopilotResponsesAPIConfig
|
||||
), f"Expected GithubCopilotResponsesAPIConfig, got {type(config)}"
|
||||
assert (
|
||||
config.custom_llm_provider == LlmProviders.GITHUB_COPILOT
|
||||
), "custom_llm_provider should be GITHUB_COPILOT"
|
||||
|
||||
@patch("litellm.llms.github_copilot.responses.transformation.Authenticator")
|
||||
def test_github_copilot_responses_endpoint_url(self, mock_authenticator_class):
|
||||
"""Test that get_complete_url returns correct GitHub Copilot endpoint"""
|
||||
# Mock authenticator to return default base
|
||||
mock_auth_instance = MagicMock()
|
||||
mock_auth_instance.get_api_base.return_value = (
|
||||
"https://api.individual.githubcopilot.com"
|
||||
)
|
||||
mock_authenticator_class.return_value = mock_auth_instance
|
||||
|
||||
config = GithubCopilotResponsesAPIConfig()
|
||||
|
||||
# Test with default GitHub Copilot API base (from authenticator)
|
||||
url = config.get_complete_url(api_base=None, litellm_params={})
|
||||
assert url == "https://api.individual.githubcopilot.com/responses", (
|
||||
f"Expected GitHub Copilot responses endpoint, got {url}"
|
||||
)
|
||||
|
||||
# Test with custom api_base (overrides authenticator)
|
||||
custom_url = config.get_complete_url(
|
||||
api_base="https://custom.githubcopilot.com", litellm_params={}
|
||||
)
|
||||
assert custom_url == "https://custom.githubcopilot.com/responses", (
|
||||
f"Expected custom endpoint, got {custom_url}"
|
||||
)
|
||||
|
||||
# Test with trailing slash
|
||||
url_with_slash = config.get_complete_url(
|
||||
api_base="https://api.githubcopilot.com/", litellm_params={}
|
||||
)
|
||||
assert url_with_slash == "https://api.githubcopilot.com/responses", (
|
||||
"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
|
||||
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["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
|
||||
|
||||
@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 = "test-api-key-123"
|
||||
mock_authenticator_class.return_value = mock_auth_instance
|
||||
|
||||
config = GithubCopilotResponsesAPIConfig()
|
||||
|
||||
custom_headers = {
|
||||
"editor-version": "custom/2.0.0",
|
||||
"custom-header": "custom-value",
|
||||
}
|
||||
|
||||
headers = config.validate_environment(
|
||||
headers=custom_headers, model="gpt-5.1-codex", litellm_params={}
|
||||
)
|
||||
|
||||
# User header should override default
|
||||
assert headers["editor-version"] == "custom/2.0.0"
|
||||
# Custom header should be preserved
|
||||
assert headers["custom-header"] == "custom-value"
|
||||
# Default headers should still be present
|
||||
assert headers["Authorization"] == "Bearer test-api-key-123"
|
||||
|
||||
def test_get_initiator_with_assistant_role(self):
|
||||
"""Test _get_initiator returns 'agent' for assistant role"""
|
||||
config = GithubCopilotResponsesAPIConfig()
|
||||
|
||||
input_with_assistant = [
|
||||
{"role": "user", "content": "Hello"},
|
||||
{"role": "assistant", "content": "Hi there"},
|
||||
]
|
||||
|
||||
initiator = config._get_initiator(input_with_assistant)
|
||||
assert initiator == "agent", "Should return 'agent' for assistant role"
|
||||
|
||||
def test_get_initiator_with_no_role(self):
|
||||
"""Test _get_initiator returns 'agent' for items without role"""
|
||||
config = GithubCopilotResponsesAPIConfig()
|
||||
|
||||
input_without_role = [
|
||||
{"role": "user", "content": "Hello"},
|
||||
{"type": "reasoning", "content": "thinking..."}, # No role field
|
||||
]
|
||||
|
||||
initiator = config._get_initiator(input_without_role)
|
||||
assert initiator == "agent", "Should return 'agent' for items without role"
|
||||
|
||||
def test_get_initiator_with_user_only(self):
|
||||
"""Test _get_initiator returns 'user' for user-only messages"""
|
||||
config = GithubCopilotResponsesAPIConfig()
|
||||
|
||||
input_user_only = [{"role": "user", "content": "Hello"}]
|
||||
|
||||
initiator = config._get_initiator(input_user_only)
|
||||
assert initiator == "user", "Should return 'user' for user-only messages"
|
||||
|
||||
def test_get_initiator_with_string_input(self):
|
||||
"""Test _get_initiator returns 'user' for string input"""
|
||||
config = GithubCopilotResponsesAPIConfig()
|
||||
|
||||
initiator = config._get_initiator("Hello, how are you?")
|
||||
assert initiator == "user", "Should return 'user' for string input"
|
||||
|
||||
def test_has_vision_input_with_input_image(self):
|
||||
"""Test _has_vision_input detects input_image type"""
|
||||
config = GithubCopilotResponsesAPIConfig()
|
||||
|
||||
input_with_vision = [
|
||||
{"role": "user", "content": [{"type": "input_image", "data": "base64..."}]}
|
||||
]
|
||||
|
||||
has_vision = config._has_vision_input(input_with_vision)
|
||||
assert has_vision is True, "Should detect input_image type"
|
||||
|
||||
def test_has_vision_input_nested(self):
|
||||
"""Test _has_vision_input detects nested input_image"""
|
||||
config = GithubCopilotResponsesAPIConfig()
|
||||
|
||||
input_nested_vision = [
|
||||
{
|
||||
"role": "user",
|
||||
"content": [
|
||||
{"type": "text", "text": "What's in this image?"},
|
||||
{
|
||||
"type": "multipart",
|
||||
"content": [{"type": "input_image", "data": "base64..."}],
|
||||
},
|
||||
],
|
||||
}
|
||||
]
|
||||
|
||||
has_vision = config._has_vision_input(input_nested_vision)
|
||||
assert has_vision is True, "Should detect nested input_image"
|
||||
|
||||
def test_has_vision_input_without_vision(self):
|
||||
"""Test _has_vision_input returns False for text-only input"""
|
||||
config = GithubCopilotResponsesAPIConfig()
|
||||
|
||||
input_text_only = [
|
||||
{"role": "user", "content": "Hello"},
|
||||
{"role": "assistant", "content": "Hi"},
|
||||
]
|
||||
|
||||
has_vision = config._has_vision_input(input_text_only)
|
||||
assert has_vision is False, "Should return False for text-only input"
|
||||
|
||||
def test_has_vision_input_with_string(self):
|
||||
"""Test _has_vision_input returns False for string input"""
|
||||
config = GithubCopilotResponsesAPIConfig()
|
||||
|
||||
has_vision = config._has_vision_input("Just a text message")
|
||||
assert has_vision is False, "Should return False for string input"
|
||||
|
||||
@patch("litellm.llms.github_copilot.responses.transformation.Authenticator")
|
||||
def test_validate_environment_with_vision_header(self, mock_authenticator_class):
|
||||
"""Test that copilot-vision-request header is added for vision input"""
|
||||
mock_auth_instance = MagicMock()
|
||||
mock_auth_instance.get_api_key.return_value = "test-api-key"
|
||||
mock_authenticator_class.return_value = mock_auth_instance
|
||||
|
||||
config = GithubCopilotResponsesAPIConfig()
|
||||
|
||||
# Create mock litellm_params with input attribute
|
||||
mock_litellm_params = MagicMock()
|
||||
mock_litellm_params.input = [
|
||||
{
|
||||
"role": "user",
|
||||
"content": [{"type": "input_image", "data": "base64..."}],
|
||||
}
|
||||
]
|
||||
|
||||
headers = config.validate_environment(
|
||||
headers={}, model="gpt-5.1-codex", litellm_params=mock_litellm_params
|
||||
)
|
||||
|
||||
assert headers.get("copilot-vision-request") == "true", (
|
||||
"Should add copilot-vision-request header for vision input"
|
||||
)
|
||||
|
||||
@patch("litellm.llms.github_copilot.responses.transformation.Authenticator")
|
||||
def test_validate_environment_with_x_initiator(self, mock_authenticator_class):
|
||||
"""Test that X-Initiator header is set based on input"""
|
||||
mock_auth_instance = MagicMock()
|
||||
mock_auth_instance.get_api_key.return_value = "test-api-key"
|
||||
mock_authenticator_class.return_value = mock_auth_instance
|
||||
|
||||
config = GithubCopilotResponsesAPIConfig()
|
||||
|
||||
# Create mock litellm_params with input attribute
|
||||
mock_litellm_params = MagicMock()
|
||||
mock_litellm_params.input = [
|
||||
{"role": "user", "content": "Hello"},
|
||||
{"role": "assistant", "content": "Hi"},
|
||||
]
|
||||
|
||||
headers = config.validate_environment(
|
||||
headers={}, model="gpt-5.1-codex", litellm_params=mock_litellm_params
|
||||
)
|
||||
|
||||
assert headers.get("X-Initiator") == "agent", (
|
||||
"Should set X-Initiator to 'agent' for assistant role"
|
||||
)
|
||||
|
||||
def test_map_openai_params_no_transformation(self):
|
||||
"""Test that map_openai_params passes through parameters unchanged"""
|
||||
config = GithubCopilotResponsesAPIConfig()
|
||||
|
||||
params = ResponsesAPIOptionalRequestParams(
|
||||
temperature=0.7, max_output_tokens=1000, stream=False
|
||||
)
|
||||
|
||||
result = config.map_openai_params(
|
||||
response_api_optional_params=params, model="gpt-5.1-codex", drop_params=False
|
||||
)
|
||||
|
||||
assert result.get("temperature") == 0.7
|
||||
assert result.get("max_output_tokens") == 1000
|
||||
assert result.get("stream") is False
|
||||
|
||||
def test_get_supported_openai_params(self):
|
||||
"""Test that get_supported_openai_params returns expected parameters"""
|
||||
config = GithubCopilotResponsesAPIConfig()
|
||||
|
||||
supported = config.get_supported_openai_params("gpt-5.1-codex")
|
||||
|
||||
# Should include standard OpenAI Responses API parameters
|
||||
expected_params = [
|
||||
"model",
|
||||
"input",
|
||||
"instructions",
|
||||
"temperature",
|
||||
"max_output_tokens",
|
||||
"stream",
|
||||
"tools",
|
||||
"tool_choice",
|
||||
]
|
||||
|
||||
for param in expected_params:
|
||||
assert param in supported, f"{param} should be in supported params"
|
||||
Loading…
Add table
Reference in a new issue