204 lines
5.7 KiB
Python
204 lines
5.7 KiB
Python
"""
|
|
A2A Protocol Exception Mapping Utils.
|
|
|
|
Maps A2A SDK exceptions to LiteLLM A2A exception types.
|
|
"""
|
|
|
|
from typing import TYPE_CHECKING, Any, Optional
|
|
|
|
from litellm._logging import verbose_logger
|
|
from litellm.a2a_protocol.card_resolver import (
|
|
fix_agent_card_url,
|
|
is_localhost_or_internal_url,
|
|
)
|
|
from litellm.a2a_protocol.exceptions import (
|
|
A2AAgentCardError,
|
|
A2AConnectionError,
|
|
A2AError,
|
|
A2ALocalhostURLError,
|
|
)
|
|
from litellm.constants import CONNECTION_ERROR_PATTERNS
|
|
|
|
if TYPE_CHECKING:
|
|
from a2a.client import A2AClient as A2AClientType
|
|
|
|
|
|
# Runtime import
|
|
A2A_SDK_AVAILABLE = False
|
|
try:
|
|
from a2a.client import A2AClient as _A2AClient # type: ignore[no-redef]
|
|
|
|
A2A_SDK_AVAILABLE = True
|
|
except ImportError:
|
|
_A2AClient = None # type: ignore[assignment, misc]
|
|
|
|
|
|
class A2AExceptionCheckers:
|
|
"""
|
|
Helper class for checking various A2A error conditions.
|
|
"""
|
|
|
|
@staticmethod
|
|
def is_connection_error(error_str: str) -> bool:
|
|
"""
|
|
Check if an error string indicates a connection error.
|
|
|
|
Args:
|
|
error_str: The error string to check
|
|
|
|
Returns:
|
|
True if the error indicates a connection issue
|
|
"""
|
|
if not isinstance(error_str, str):
|
|
return False
|
|
|
|
error_str_lower = error_str.lower()
|
|
return any(pattern in error_str_lower for pattern in CONNECTION_ERROR_PATTERNS)
|
|
|
|
@staticmethod
|
|
def is_localhost_url(url: Optional[str]) -> bool:
|
|
"""
|
|
Check if a URL is a localhost/internal URL.
|
|
|
|
Args:
|
|
url: The URL to check
|
|
|
|
Returns:
|
|
True if the URL is localhost/internal
|
|
"""
|
|
return is_localhost_or_internal_url(url)
|
|
|
|
@staticmethod
|
|
def is_agent_card_error(error_str: str) -> bool:
|
|
"""
|
|
Check if an error string indicates an agent card error.
|
|
|
|
Args:
|
|
error_str: The error string to check
|
|
|
|
Returns:
|
|
True if the error is related to agent card fetching/parsing
|
|
"""
|
|
if not isinstance(error_str, str):
|
|
return False
|
|
|
|
error_str_lower = error_str.lower()
|
|
agent_card_patterns = [
|
|
"agent card",
|
|
"agent-card",
|
|
".well-known",
|
|
"card not found",
|
|
"invalid agent",
|
|
]
|
|
return any(pattern in error_str_lower for pattern in agent_card_patterns)
|
|
|
|
|
|
def map_a2a_exception(
|
|
original_exception: Exception,
|
|
card_url: Optional[str] = None,
|
|
api_base: Optional[str] = None,
|
|
model: Optional[str] = None,
|
|
) -> Exception:
|
|
"""
|
|
Map an A2A SDK exception to a LiteLLM A2A exception type.
|
|
|
|
Args:
|
|
original_exception: The original exception from the A2A SDK
|
|
card_url: The URL from the agent card (if available)
|
|
api_base: The original API base URL
|
|
model: The model/agent name
|
|
|
|
Returns:
|
|
A mapped LiteLLM A2A exception
|
|
|
|
Raises:
|
|
A2ALocalhostURLError: If the error is a connection error to a localhost URL
|
|
A2AConnectionError: If the error is a general connection error
|
|
A2AAgentCardError: If the error is related to agent card issues
|
|
A2AError: For other A2A-related errors
|
|
"""
|
|
error_str = str(original_exception)
|
|
|
|
# Check for localhost URL connection error (special case - retryable)
|
|
if (
|
|
card_url
|
|
and api_base
|
|
and A2AExceptionCheckers.is_localhost_url(card_url)
|
|
and A2AExceptionCheckers.is_connection_error(error_str)
|
|
):
|
|
raise A2ALocalhostURLError(
|
|
localhost_url=card_url,
|
|
base_url=api_base,
|
|
original_error=original_exception,
|
|
model=model,
|
|
)
|
|
|
|
# Check for agent card errors
|
|
if A2AExceptionCheckers.is_agent_card_error(error_str):
|
|
raise A2AAgentCardError(
|
|
message=error_str,
|
|
url=api_base,
|
|
model=model,
|
|
)
|
|
|
|
# Check for general connection errors
|
|
if A2AExceptionCheckers.is_connection_error(error_str):
|
|
raise A2AConnectionError(
|
|
message=error_str,
|
|
url=card_url or api_base,
|
|
model=model,
|
|
)
|
|
|
|
# Default: wrap in generic A2AError
|
|
raise A2AError(
|
|
message=error_str,
|
|
model=model,
|
|
)
|
|
|
|
|
|
def handle_a2a_localhost_retry(
|
|
error: A2ALocalhostURLError,
|
|
agent_card: Any,
|
|
a2a_client: "A2AClientType",
|
|
is_streaming: bool = False,
|
|
) -> "A2AClientType":
|
|
"""
|
|
Handle A2ALocalhostURLError by fixing the URL and creating a new client.
|
|
|
|
This is called when we catch an A2ALocalhostURLError and want to retry
|
|
with the corrected URL.
|
|
|
|
Args:
|
|
error: The localhost URL error
|
|
agent_card: The agent card object to fix
|
|
a2a_client: The current A2A client
|
|
is_streaming: Whether this is a streaming request (for logging)
|
|
|
|
Returns:
|
|
A new A2A client with the fixed URL
|
|
|
|
Raises:
|
|
ImportError: If the A2A SDK is not installed
|
|
"""
|
|
if not A2A_SDK_AVAILABLE or _A2AClient is None:
|
|
raise ImportError(
|
|
"A2A SDK is required for localhost retry handling. "
|
|
"Install it with: pip install a2a"
|
|
)
|
|
|
|
request_type = "streaming " if is_streaming else ""
|
|
verbose_logger.warning(
|
|
f"A2A {request_type}request to '{error.localhost_url}' failed: {error.original_error}. "
|
|
f"Agent card contains localhost/internal URL. "
|
|
f"Retrying with base_url '{error.base_url}'."
|
|
)
|
|
|
|
# Fix the agent card URL
|
|
fix_agent_card_url(agent_card, error.base_url)
|
|
|
|
# Create a new client with the fixed agent card (transport caches URL)
|
|
return _A2AClient(
|
|
httpx_client=a2a_client._transport.httpx_client, # type: ignore[union-attr]
|
|
agent_card=agent_card,
|
|
)
|