From 2ea6e89b2c17f897a405e0a700ab8a6f2eb3496c Mon Sep 17 00:00:00 2001 From: Milan Date: Fri, 10 Apr 2026 21:06:44 +0300 Subject: [PATCH 1/2] fix(a2a): default create_a2a_client timeout to DEFAULT_A2A_AGENT_TIMEOUT Align with aget_agent_card and the DEFAULT_A2A_AGENT_TIMEOUT env var so A2A message/send uses the same default as agent card fetch instead of a hardcoded 60s HTTP read timeout. Also correct aget_agent_card docstring for the timeout parameter. Made-with: Cursor --- litellm/a2a_protocol/main.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/litellm/a2a_protocol/main.py b/litellm/a2a_protocol/main.py index c86549da77a..6154c828804 100644 --- a/litellm/a2a_protocol/main.py +++ b/litellm/a2a_protocol/main.py @@ -615,7 +615,7 @@ async def asend_message_streaming( # noqa: PLR0915 async def create_a2a_client( base_url: str, - timeout: float = 60.0, + timeout: float = DEFAULT_A2A_AGENT_TIMEOUT, extra_headers: Optional[Dict[str, str]] = None, ) -> "A2AClientType": """ @@ -626,7 +626,7 @@ async def create_a2a_client( Args: base_url: The base URL of the A2A agent (e.g., "http://localhost:10001") - timeout: Request timeout in seconds (default: 60.0) + timeout: Request timeout in seconds (default: ``DEFAULT_A2A_AGENT_TIMEOUT`` / env ``DEFAULT_A2A_AGENT_TIMEOUT``) extra_headers: Optional additional headers to include in requests Returns: @@ -711,7 +711,7 @@ async def aget_agent_card( Args: base_url: The base URL of the A2A agent (e.g., "http://localhost:10001") - timeout: Request timeout in seconds (default: 60.0) + timeout: Request timeout in seconds (default: ``DEFAULT_A2A_AGENT_TIMEOUT`` / env ``DEFAULT_A2A_AGENT_TIMEOUT``) extra_headers: Optional additional headers to include in requests Returns: From 824269d585c83226187920ee053c2adba6ca740f Mon Sep 17 00:00:00 2001 From: Milan Date: Fri, 10 Apr 2026 21:10:28 +0300 Subject: [PATCH 2/2] test(a2a): assert create_a2a_client default timeout uses DEFAULT_A2A_AGENT_TIMEOUT Made-with: Cursor --- .../test_agent_header_isolation.py | 85 ++++++++++++++++++- 1 file changed, 84 insertions(+), 1 deletion(-) diff --git a/tests/test_litellm/proxy/agent_endpoints/test_agent_header_isolation.py b/tests/test_litellm/proxy/agent_endpoints/test_agent_header_isolation.py index 13a9adc3c63..c85987c19c8 100644 --- a/tests/test_litellm/proxy/agent_endpoints/test_agent_header_isolation.py +++ b/tests/test_litellm/proxy/agent_endpoints/test_agent_header_isolation.py @@ -4,6 +4,9 @@ Tests that prove header isolation between agents. Before the fix these tests FAIL — agent A's headers bleed into agent B because create_a2a_client mutates a globally cached httpx client. After the fix they pass. + +Also includes direct unit tests for create_a2a_client (fresh httpx client +per call; default timeout uses DEFAULT_A2A_AGENT_TIMEOUT). """ import sys @@ -11,6 +14,8 @@ from unittest.mock import AsyncMock, MagicMock, call, patch import pytest +from litellm.constants import DEFAULT_A2A_AGENT_TIMEOUT + # --------------------------------------------------------------------------- # Helpers @@ -199,7 +204,7 @@ async def test_each_agent_gets_only_its_own_static_headers(): # --------------------------------------------------------------------------- -# Unit test: create_a2a_client uses a fresh httpx client per call +# Unit tests: create_a2a_client (httpx client per call + timeout defaults) # --------------------------------------------------------------------------- @@ -246,3 +251,81 @@ async def test_create_a2a_client_uses_fresh_httpx_client(): assert created_clients[0] is not created_clients[1], ( "create_a2a_client reused a cached httpx client — headers will bleed between agents" ) + + +@pytest.mark.asyncio +async def test_create_a2a_client_default_timeout_matches_constant(): + """When timeout is omitted, httpx client params must use DEFAULT_A2A_AGENT_TIMEOUT.""" + from litellm.a2a_protocol.main import create_a2a_client + + captured: dict = {} + + def _capture_get_async_httpx_client(llm_provider, params, **kwargs): + captured["params"] = params + handler = MagicMock() + handler.client = MagicMock() + handler.client.headers = MagicMock() + return handler + + fake_agent_card = MagicMock() + fake_agent_card.name = "test-agent" + + class _FakeResolver: + def __init__(self, **kw): + pass + + async def get_agent_card(self): + return fake_agent_card + + class _FakeA2AClient: + def __init__(self, httpx_client, agent_card): + pass + + with patch("litellm.a2a_protocol.main.A2A_SDK_AVAILABLE", True), patch( + "litellm.a2a_protocol.main.get_async_httpx_client", + side_effect=_capture_get_async_httpx_client, + ), patch("litellm.a2a_protocol.main.A2ACardResolver", _FakeResolver), patch( + "litellm.a2a_protocol.main._A2AClient", _FakeA2AClient + ): + await create_a2a_client(base_url="http://127.0.0.1:9") + + assert captured["params"]["timeout"] == DEFAULT_A2A_AGENT_TIMEOUT + + +@pytest.mark.asyncio +async def test_create_a2a_client_explicit_timeout_overrides_default(): + """Explicit timeout= must be passed through to the httpx client params.""" + from litellm.a2a_protocol.main import create_a2a_client + + captured: dict = {} + + def _capture_get_async_httpx_client(llm_provider, params, **kwargs): + captured["params"] = params + handler = MagicMock() + handler.client = MagicMock() + handler.client.headers = MagicMock() + return handler + + fake_agent_card = MagicMock() + fake_agent_card.name = "test-agent" + + class _FakeResolver: + def __init__(self, **kw): + pass + + async def get_agent_card(self): + return fake_agent_card + + class _FakeA2AClient: + def __init__(self, httpx_client, agent_card): + pass + + with patch("litellm.a2a_protocol.main.A2A_SDK_AVAILABLE", True), patch( + "litellm.a2a_protocol.main.get_async_httpx_client", + side_effect=_capture_get_async_httpx_client, + ), patch("litellm.a2a_protocol.main.A2ACardResolver", _FakeResolver), patch( + "litellm.a2a_protocol.main._A2AClient", _FakeA2AClient + ): + await create_a2a_client(base_url="http://127.0.0.1:9", timeout=42.5) + + assert captured["params"]["timeout"] == 42.5