Skip to content
Merged
21 changes: 13 additions & 8 deletions litellm/integrations/websearch_interception/handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,21 +120,26 @@ async def try_short_circuit_search(
if self.enabled_providers is not None and provider_str not in self.enabled_providers:
return None

# Only short-circuit for providers without native Anthropic Messages
# support. Providers that have a BaseAnthropicMessagesConfig (bedrock,
# vertex_ai, azure_ai, anthropic) already use the agentic loop, which
# includes a follow-up LLM call to synthesize the answer from search
# results. Short-circuiting those would skip that synthesis step and
# return raw search text — a regression for existing users.
# Only short-circuit for providers whose Anthropic Messages agentic loop
# does not run web_search itself. Providers that have a
# BaseAnthropicMessagesConfig which handles web search natively (bedrock,
# vertex_ai, azure_ai, anthropic) already perform the search plus a
# follow-up LLM synthesis step; short-circuiting those would skip that
# synthesis and return raw search text — a regression for existing users.
#
# github_copilot has a BaseAnthropicMessagesConfig (added for thinking
# passthrough) but does not handle web_search natively, so its config
# returns handles_web_search_natively() == False and we still short-circuit
# web-search-only requests against it.
try:
provider_enum = LlmProviders(provider_str)
anthropic_config = ProviderConfigManager.get_provider_anthropic_messages_config(
model=model, provider=provider_enum
)
if anthropic_config is not None:
if anthropic_config is not None and anthropic_config.handles_web_search_natively():
verbose_logger.debug(
f"WebSearchInterception: Skipping short-circuit for {provider_str} "
"(provider has native Anthropic Messages support, using agentic loop)"
"(provider handles web search natively via the agentic loop)"
)
return None
except (ValueError, Exception):
Expand Down
13 changes: 13 additions & 0 deletions litellm/llms/base_llm/anthropic_messages/transformation.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,19 @@ def should_filter_anthropic_beta_headers(self) -> bool:
"""
return True

def handles_web_search_natively(self) -> bool:
"""
Whether the upstream this config routes to executes ``web_search`` tools
itself as part of its Anthropic Messages agentic loop.

The web-search interception handler short-circuits web-search-only
requests (running the search itself and returning synthetic results) only
for providers that do NOT. Providers whose agentic loop already performs
the search plus a follow-up synthesis step (bedrock, vertex_ai, ...)
return True so those requests flow through untouched.
"""
return True

def get_async_streaming_response_iterator(
self,
model: str,
Expand Down
Empty file.
118 changes: 118 additions & 0 deletions litellm/llms/github_copilot/messages/transformation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
from typing import Any, Optional

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

_MESSAGES_PROXY_API_VERSION = "2026-06-01"


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 handles_web_search_natively(self) -> bool:
"""
Copilot's /v1/messages endpoint does not execute ``web_search`` tools, so
the interception handler must short-circuit web-search-only requests
instead of routing them here.
"""
return False
Comment thread
cursor[bot] marked this conversation as resolved.

def should_filter_anthropic_beta_headers(self) -> bool:
"""
Copilot's /v1/messages is a native Anthropic Messages passthrough, so
``anthropic-beta`` values injected by ``_update_headers_with_anthropic_beta``
(context_management, structured outputs, ...) must reach the upstream
verbatim. The default provider-scoped filter would drop them because
github_copilot has no entry in ``anthropic_beta_headers_config.json``.
"""
return False

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.

The caller-supplied ``api_base`` is intentionally ignored. Routing this
request anywhere other than the authenticated Copilot endpoint would
leak the Copilot bearer token to a caller-controlled URL.
"""
# Always use the Copilot endpoint resolved from the authenticated
# session, never the caller-supplied api_base. rstrip so a
# tenant-specific base with a trailing slash does not yield a
# double-slash URL once "/v1/messages" is appended downstream.
dynamic_api_base = (self.authenticator.get_api_base() or DEFAULT_GITHUB_COPILOT_API_BASE).rstrip("/")
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

headers["openai-intent"] = "messages-proxy"
headers["x-interaction-type"] = "messages-proxy"
headers["x-github-api-version"] = _MESSAGES_PROXY_API_VERSION

if "anthropic-version" not in headers:
headers["anthropic-version"] = "2023-06-01"

headers = self._update_headers_with_anthropic_beta(
headers, optional_params, custom_llm_provider="github_copilot"
)

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`` here is the value already resolved by
``validate_anthropic_messages_environment`` (the authenticated Copilot
host), not the raw caller-supplied base — that one is discarded there to
avoid leaking the Copilot bearer token to a caller-controlled URL. We
reuse it to avoid a second authenticator read, falling back to a fresh
resolution only if it was not provided.
"""
resolved = (api_base or self.authenticator.get_api_base() or DEFAULT_GITHUB_COPILOT_API_BASE).rstrip("/")
if not resolved.endswith("/v1/messages"):
resolved = f"{resolved}/v1/messages"
return resolved
Comment thread
greptile-apps[bot] marked this conversation as resolved.
9 changes: 6 additions & 3 deletions litellm/model_prices_and_context_window_backup.json
Original file line number Diff line number Diff line change
Expand Up @@ -19176,7 +19176,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,
Expand All @@ -19189,7 +19190,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,
Expand Down Expand Up @@ -19242,7 +19244,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,
Expand Down
7 changes: 7 additions & 0 deletions litellm/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -7984,6 +7984,13 @@ def _get_provider_anthropic_messages_config_cached(
)

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
Expand Down
Empty file.
Loading
Loading