This commit is contained in:
AlphaRex-pixel 2026-09-12 14:54:22 -04:00 committed by GitHub
commit cf615f69e8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 186 additions and 5 deletions

View file

@ -388,6 +388,7 @@ async def asend_message(
litellm_params: dict[str, object] | None = None,
agent_id: str | None = None,
agent_extra_headers: dict[str, str] | None = None,
agent_card_params: dict[str, object] | None = None,
**kwargs: object,
) -> LiteLLMSendMessageResponse:
"""
@ -474,7 +475,11 @@ async def asend_message(
# Overlay agent-level headers (agent headers take precedence over LiteLLM internal ones)
if agent_extra_headers:
extra_headers.update(agent_extra_headers)
a2a_client = await create_a2a_client(base_url=api_base, extra_headers=extra_headers)
a2a_client = await create_a2a_client(
base_url=api_base,
extra_headers=extra_headers,
agent_card_params=agent_card_params,
)
# Type assertion: a2a_client is guaranteed to be non-None here
assert a2a_client is not None
@ -610,6 +615,7 @@ async def asend_message_streaming(
metadata: dict[str, object] | None = None,
proxy_server_request: dict[str, object] | None = None,
agent_extra_headers: dict[str, str] | None = None,
agent_card_params: dict[str, object] | None = None,
**kwargs: object,
) -> AsyncIterator[Any]:
"""
@ -699,6 +705,7 @@ async def asend_message_streaming(
base_url=api_base,
extra_headers=extra_headers,
streaming=True,
agent_card_params=agent_card_params,
)
assert a2a_client is not None
@ -745,6 +752,7 @@ async def create_a2a_client(
timeout: float = DEFAULT_A2A_AGENT_TIMEOUT,
extra_headers: dict[str, str] | None = None,
streaming: bool = False,
agent_card_params: dict[str, object] | None = None,
) -> "A2AClientType":
"""
Create an A2A client for the given agent URL.
@ -787,10 +795,31 @@ async def create_a2a_client(
if extra_headers:
verbose_proxy_logger.debug("A2A client created with extra_headers=%s", list(extra_headers.keys()))
resolver: Final = A2ACardResolver(httpx_client=httpx_client, base_url=base_url)
agent_card: Final = normalize_agent_card_interfaces(
await resolver.get_agent_card(http_kwargs={"headers": extra_headers} if extra_headers else None)
)
agent_card: AgentCard | None = None
if agent_card_params:
from a2a.compat.v0_3 import conversions as _conversions
from a2a.compat.v0_3.types import AgentCard as _CompatAgentCard
from pydantic import ValidationError as _ValidationError
try:
compat_card = _CompatAgentCard.model_validate(agent_card_params)
agent_card = normalize_agent_card_interfaces(_conversions.to_core_agent_card(compat_card))
verbose_logger.info("Using pre-registered agent card for %s (skipping well-known discovery)", base_url)
except _ValidationError as e:
verbose_logger.warning(
"Stored agent_card_params for %s failed AgentCard validation (%s); "
"falling back to well-known discovery.",
base_url,
e,
)
agent_card = None
if agent_card is None:
resolver: Final = A2ACardResolver(httpx_client=httpx_client, base_url=base_url)
agent_card = normalize_agent_card_interfaces(
await resolver.get_agent_card(http_kwargs={"headers": extra_headers} if extra_headers else None)
)
a2a_client: Final = await create_client( # pyright: ignore[reportOptionalCall]
agent_card,

View file

@ -404,6 +404,7 @@ async def _handle_stream_message(
user_api_key_dict: UserAPIKeyAuth | None = None,
request_data: dict[str, object] | None = None,
proxy_logging_obj: ProxyLogging | None = None,
agent_card_params: dict[str, object] | None = None,
served_version: A2AVersion = "0.3",
) -> StreamingResponse:
"""Handle message/stream method via SDK functions.
@ -474,6 +475,7 @@ async def _handle_stream_message(
metadata=metadata,
proxy_server_request=proxy_server_request,
agent_extra_headers=agent_extra_headers,
agent_card_params=agent_card_params,
)
if (
@ -863,6 +865,7 @@ async def invoke_agent_a2a(
proxy_server_request=data.get("proxy_server_request"),
litellm_logging_obj=logging_obj,
agent_extra_headers=agent_extra_headers,
agent_card_params=agent_card_params or None,
)
try:
@ -904,6 +907,7 @@ async def invoke_agent_a2a(
agent_extra_headers=agent_extra_headers,
user_api_key_dict=user_api_key_dict,
request_data=data,
agent_card_params=agent_card_params or None,
proxy_logging_obj=proxy_logging_obj,
served_version=served_version,
)

View file

@ -0,0 +1,148 @@
"""
Regression tests for GH #40586: message/send re-discovered the agent card
from well-known paths instead of using the registered agent_card_params.
These tests fake the HTTP boundary with a real httpx.AsyncClient wired to an
httpx.MockTransport, rather than patching SDK internals, so they verify
actual behavior: whether a well-known-path request goes out over the wire,
and what agent card the resulting client ends up holding.
"""
from unittest.mock import patch
import httpx
import pytest
from litellm.a2a_protocol.main import create_a2a_client
BASE_URL = "https://example.com/agents/a2a"
WELL_KNOWN_PATH = "/agents/a2a/.well-known/agent-card.json"
MINIMAL_AGENT_CARD_PARAMS = {
"protocolVersion": "1.0",
"name": "pre-registered-agent",
"description": "A test assistant.",
"url": BASE_URL,
"version": "1.0",
"capabilities": {"streaming": False},
"defaultInputModes": ["text"],
"defaultOutputModes": ["text"],
"skills": [],
}
DISCOVERED_AGENT_CARD_JSON = {
"protocolVersion": "1.0",
"name": "discovered-agent",
"description": "A test assistant found via well-known discovery.",
"url": BASE_URL,
"version": "1.0",
"capabilities": {"streaming": False},
"defaultInputModes": ["text"],
"defaultOutputModes": ["text"],
"skills": [],
}
def _mock_client(handler):
"""A real httpx.AsyncClient wired to a local handler instead of the network."""
transport = httpx.MockTransport(handler)
client = httpx.AsyncClient(transport=transport)
class _FakeAsyncHandler:
def __init__(self, c):
self.client = c
return _FakeAsyncHandler(client)
@pytest.mark.asyncio
async def test_create_a2a_client_skips_discovery_when_agent_card_params_given():
"""
When a caller already has a resolved agent card (e.g. one stored via
POST /v1/agents), create_a2a_client must build the client from it
directly instead of making a well-known-path HTTP request.
"""
requests_made: list[str] = []
def handler(request: httpx.Request) -> httpx.Response:
requests_made.append(str(request.url))
return httpx.Response(404, json={"error": "should not be called"})
with (
patch( # test-quality-ok: injects a real httpx.AsyncClient wired to httpx.MockTransport, not a mock of behavior
"litellm.a2a_protocol.main.get_async_httpx_client",
return_value=_mock_client(handler),
)
):
a2a_client = await create_a2a_client(
base_url=BASE_URL,
agent_card_params=MINIMAL_AGENT_CARD_PARAMS,
)
# The real, observable proof of the fix: no HTTP request went out at all.
assert requests_made == []
resolved_card = a2a_client._litellm_agent_card
assert resolved_card.name == "pre-registered-agent"
assert resolved_card.supported_interfaces[0].url == BASE_URL
@pytest.mark.asyncio
async def test_create_a2a_client_falls_back_to_discovery_without_agent_card_params():
"""
Unchanged behavior: when no agent_card_params is supplied, the client
resolves the card over HTTP from the well-known path, same as before
the fix.
"""
requests_made: list[str] = []
def handler(request: httpx.Request) -> httpx.Response:
requests_made.append(str(request.url))
if request.url.path == WELL_KNOWN_PATH:
return httpx.Response(200, json=DISCOVERED_AGENT_CARD_JSON)
return httpx.Response(404)
with (
patch( # test-quality-ok: injects a real httpx.AsyncClient wired to httpx.MockTransport, not a mock of behavior
"litellm.a2a_protocol.main.get_async_httpx_client",
return_value=_mock_client(handler),
)
):
a2a_client = await create_a2a_client(base_url=BASE_URL)
assert any(WELL_KNOWN_PATH in url for url in requests_made)
resolved_card = a2a_client._litellm_agent_card
assert resolved_card.name == "discovered-agent"
@pytest.mark.asyncio
async def test_create_a2a_client_falls_back_to_discovery_on_invalid_agent_card_params():
"""
If the stored agent_card_params doesn't validate as a full AgentCard
(e.g. a partial card missing required fields), create_a2a_client must
fall back to well-known-path discovery rather than raising.
"""
requests_made: list[str] = []
def handler(request: httpx.Request) -> httpx.Response:
requests_made.append(str(request.url))
if request.url.path == WELL_KNOWN_PATH:
return httpx.Response(200, json=DISCOVERED_AGENT_CARD_JSON)
return httpx.Response(404)
incomplete_agent_card_params = {"url": BASE_URL} # missing required fields
with (
patch( # test-quality-ok: injects a real httpx.AsyncClient wired to httpx.MockTransport, not a mock of behavior
"litellm.a2a_protocol.main.get_async_httpx_client",
return_value=_mock_client(handler),
)
):
a2a_client = await create_a2a_client(
base_url=BASE_URL,
agent_card_params=incomplete_agent_card_params,
)
assert any(WELL_KNOWN_PATH in url for url in requests_made)
resolved_card = a2a_client._litellm_agent_card
assert resolved_card.name == "discovered-agent"