mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-13 23:11:40 +00:00
Merge 9523a8f888 into 0c98afa780
This commit is contained in:
commit
cf615f69e8
3 changed files with 186 additions and 5 deletions
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
Loading…
Add table
Reference in a new issue