mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-11 03:38:38 +00:00
test(e2e): register the verbatim a2a-sdk 0.3.x card and guard malformed protocolVersion
This commit is contained in:
parent
c8e0253cd5
commit
43402b04bd
3 changed files with 84 additions and 18 deletions
|
|
@ -30,6 +30,7 @@ class A2ASkill(BaseModel):
|
|||
name: str
|
||||
description: str
|
||||
tags: list[str]
|
||||
examples: list[str] | None = None
|
||||
|
||||
|
||||
class AgentCardParams(BaseModel):
|
||||
|
|
@ -40,10 +41,12 @@ class AgentCardParams(BaseModel):
|
|||
name: str
|
||||
description: str
|
||||
version: str
|
||||
url: str | None = None
|
||||
capabilities: A2ACapabilities = A2ACapabilities()
|
||||
skills: list[A2ASkill]
|
||||
default_input_modes: list[str] = Field(default=["text"], serialization_alias="defaultInputModes")
|
||||
default_output_modes: list[str] = Field(default=["text"], serialization_alias="defaultOutputModes")
|
||||
preferred_transport: str | None = Field(default=None, serialization_alias="preferredTransport")
|
||||
|
||||
|
||||
class A2ABridgeParams(BaseModel):
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import pytest
|
|||
|
||||
from a2a_client import (
|
||||
A2ABridgeParams,
|
||||
A2ACapabilities,
|
||||
A2AClient,
|
||||
A2AJsonRpcRequest,
|
||||
A2AMessageSendParams,
|
||||
|
|
@ -25,7 +26,7 @@ from a2a_client import (
|
|||
AgentResponse,
|
||||
)
|
||||
from e2e_config import unique_marker
|
||||
from e2e_http import UnknownApiError, unwrap
|
||||
from e2e_http import Result, UnknownApiError, unwrap
|
||||
from lifecycle import ResourceManager
|
||||
|
||||
BRIDGE = A2ABridgeParams(custom_llm_provider="anthropic", model="claude-haiku-4-5")
|
||||
|
|
@ -51,6 +52,50 @@ def _register(client: A2AClient, resources: ResourceManager, protocol_version: s
|
|||
return agent
|
||||
|
||||
|
||||
def _google_sdk_default_card(marker: str) -> AgentCardParams:
|
||||
"""Field-for-field the card the Google a2a-sdk 0.3.x line serves for its helloworld
|
||||
sample, whose ``AgentCard`` defaults ``protocolVersion`` to the full semver "0.3.0"
|
||||
(the shape the v1.92 regression rejected); ``url`` and ``name`` are the
|
||||
deployment-specific fields the SDK requires callers to fill. The url points at a
|
||||
reserved example host: a url-bearing card makes message/send dial that upstream
|
||||
rather than the completion bridge, so the verbatim-card test asserts registration
|
||||
and card serving only."""
|
||||
return AgentCardParams(
|
||||
protocol_version="0.3.0",
|
||||
name=f"E2E A2A sdk {marker}",
|
||||
description="Just a hello world agent",
|
||||
version="1.0.0",
|
||||
url="http://e2e-a2a-upstream.example/",
|
||||
capabilities=A2ACapabilities(streaming=True),
|
||||
skills=[
|
||||
A2ASkill(
|
||||
id="hello_world",
|
||||
name="Returns hello world",
|
||||
description="just returns hello world",
|
||||
tags=["hello world"],
|
||||
examples=["hi", "hello world"],
|
||||
)
|
||||
],
|
||||
preferred_transport="JSONRPC",
|
||||
)
|
||||
|
||||
|
||||
def _register_rejection(client: A2AClient, protocol_version: str) -> Result[AgentResponse]:
|
||||
marker = unique_marker()
|
||||
body = AgentRegisterBody(
|
||||
agent_name=f"e2e-a2a-bad-{marker}",
|
||||
agent_card_params=AgentCardParams(
|
||||
protocol_version=protocol_version,
|
||||
name=f"E2E A2A bad {marker}",
|
||||
description="rejected at registration",
|
||||
version="1.0.0",
|
||||
skills=[A2ASkill(id="chat", name="Chat", description="c", tags=["chat"])],
|
||||
),
|
||||
litellm_params=BRIDGE,
|
||||
)
|
||||
return client.register_agent(body)
|
||||
|
||||
|
||||
def _ask(text: str) -> A2AJsonRpcRequest:
|
||||
return A2AJsonRpcRequest(
|
||||
id=f"e2e-{unique_marker()}",
|
||||
|
|
@ -72,13 +117,31 @@ class TestA2AAgentLifecycle:
|
|||
@pytest.mark.covers("other.a2a.register.semver_version_accepted")
|
||||
def test_semver_protocol_version_registers_and_serves(self, client: A2AClient, resources: ResourceManager, scoped_key: str) -> None:
|
||||
agent = _register(client, resources, "0.3.0")
|
||||
assert agent.agent_card_params.protocol_version.startswith("0.3")
|
||||
assert agent.agent_card_params.protocol_version == "0.3"
|
||||
card = unwrap(client.agent_card(agent.agent_id, scoped_key))
|
||||
assert card.protocol_version.startswith("0.3")
|
||||
assert card.protocol_version == "0.3"
|
||||
assert card.supported_interfaces is not None
|
||||
assert card.supported_interfaces[0].protocol_version == "0.3"
|
||||
result = unwrap(client.send_message(agent.agent_id, scoped_key, _ask("Say hi in one word"))).result
|
||||
assert result is not None
|
||||
assert result.text != ""
|
||||
|
||||
@pytest.mark.covers("other.a2a.register.sdk_default_card_accepted")
|
||||
def test_google_sdk_default_card_registers_verbatim(self, client: A2AClient, resources: ResourceManager, scoped_key: str) -> None:
|
||||
marker = unique_marker()
|
||||
body = AgentRegisterBody(
|
||||
agent_name=f"e2e-a2a-sdk-{marker}",
|
||||
agent_card_params=_google_sdk_default_card(marker),
|
||||
litellm_params=BRIDGE,
|
||||
)
|
||||
agent = unwrap(client.register_agent(body))
|
||||
resources.defer(lambda: client.delete_agent(agent.agent_id))
|
||||
assert agent.agent_card_params.protocol_version == "0.3"
|
||||
card = unwrap(client.agent_card(agent.agent_id, scoped_key))
|
||||
assert card.protocol_version == "0.3"
|
||||
assert card.supported_interfaces is not None
|
||||
assert card.supported_interfaces[0].protocol_version == "0.3"
|
||||
|
||||
@pytest.mark.covers("other.a2a.discovery.proxy_fronted_card")
|
||||
def test_discovery_card_is_proxy_fronted(self, client: A2AClient, resources: ResourceManager, scoped_key: str) -> None:
|
||||
agent = _register(client, resources, "0.3")
|
||||
|
|
@ -127,23 +190,21 @@ class TestA2AAgentLifecycle:
|
|||
assert result.text != ""
|
||||
|
||||
@pytest.mark.covers("other.a2a.register.unsupported_version_rejected")
|
||||
def test_unsupported_protocol_version_rejected(self, client: A2AClient, resources: ResourceManager) -> None:
|
||||
marker = unique_marker()
|
||||
body = AgentRegisterBody(
|
||||
agent_name=f"e2e-a2a-bad-{marker}",
|
||||
agent_card_params=AgentCardParams(
|
||||
protocol_version="9.9",
|
||||
name=f"E2E A2A bad {marker}",
|
||||
description="unsupported version",
|
||||
version="1.0.0",
|
||||
skills=[A2ASkill(id="chat", name="Chat", description="c", tags=["chat"])],
|
||||
),
|
||||
litellm_params=BRIDGE,
|
||||
)
|
||||
result = client.register_agent(body)
|
||||
def test_unsupported_protocol_version_rejected(self, client: A2AClient) -> None:
|
||||
result = _register_rejection(client, "9.9")
|
||||
match result:
|
||||
case UnknownApiError(status_code=status, body=detail):
|
||||
assert status == 400
|
||||
assert "protocolVersion" in detail
|
||||
case _:
|
||||
pytest.fail(f"expected 400 for unsupported protocolVersion, got {result}")
|
||||
|
||||
@pytest.mark.covers("other.a2a.register.malformed_version_rejected")
|
||||
def test_malformed_protocol_version_rejected(self, client: A2AClient) -> None:
|
||||
result = _register_rejection(client, "0.3.garbage")
|
||||
match result:
|
||||
case UnknownApiError(status_code=status, body=detail):
|
||||
assert status == 400
|
||||
assert "Unsupported protocolVersion '0.3.garbage'" in detail
|
||||
case _:
|
||||
pytest.fail(f"expected 400 for malformed protocolVersion, got {result}")
|
||||
|
|
|
|||
|
|
@ -30,7 +30,9 @@
|
|||
- {id: other.key_mgmt.spend_reset.resets_to_value, module: other, tier: P1, area: auth, assertions: [resets_to_value], source: "key_management_endpoints.py:4841", rationale: "reset_spend resets accumulated spend"}
|
||||
- {id: other.a2a.register.persists, module: other, tier: P1, area: a2a, assertions: [persists], source: "agent_endpoints/endpoints.py:325-443", rationale: "POST /v1/agents registers an agent card; GET /v1/agents/{id} reads it back"}
|
||||
- {id: other.a2a.register.unsupported_version_rejected, module: other, tier: P1, area: a2a, assertions: [unsupported_version_rejected], source: "agent_endpoints/endpoints.py _validate_protocol_version", rationale: "A card pinning a protocolVersion outside SUPPORTED_A2A_PROTOCOL_VERSIONS is refused with 400"}
|
||||
- {id: other.a2a.register.semver_version_accepted, module: other, tier: P1, area: a2a, assertions: [semver_version_accepted], source: "agent_endpoints/endpoints.py _validate_protocol_version", rationale: "A card pinning a patch-level semver like 0.3.0 (what the Google A2A SDK emits) registers and serves as the 0.3 family rather than 400ing; regression guard for the v1.92 report"}
|
||||
- {id: other.a2a.register.semver_version_accepted, module: other, tier: P1, area: a2a, assertions: [semver_version_accepted], source: "agent_endpoints/endpoints.py _validate_protocol_version", rationale: "A card pinning a patch-level semver like 0.3.0 (what the Google A2A SDK emits) registers, stores and serves the canonical 0.3 rather than 400ing; regression guard for the v1.92 report"}
|
||||
- {id: other.a2a.register.sdk_default_card_accepted, module: other, tier: P1, area: a2a, assertions: [sdk_default_card_accepted], source: "agent_endpoints/endpoints.py _build_merged_agent_card", rationale: "The full field set the Google a2a-sdk 0.3.x emits (protocolVersion 0.3.0, url, preferredTransport, capabilities, skill examples) registers verbatim and serves the canonical 0.3"}
|
||||
- {id: other.a2a.register.malformed_version_rejected, module: other, tier: P1, area: a2a, assertions: [malformed_version_rejected], source: "a2a/agent_card.py normalize_protocol_version", rationale: "A malformed protocolVersion like 0.3.garbage fails full-string semver validation and is refused with 400 instead of truncating to a supported family"}
|
||||
- {id: other.a2a.discovery.proxy_fronted_card, module: other, tier: P1, area: a2a, assertions: [proxy_fronted_card], source: "agent_endpoints/a2a_endpoints.py get_agent_card", rationale: "/.well-known/agent-card.json serves the proxy url + supportedInterfaces and the LiteLLM virtual-key bearer scheme, not the upstream"}
|
||||
- {id: other.a2a.message_send.bridge_invokes, module: other, tier: P1, area: a2a, assertions: [bridge_invokes], source: "a2a_protocol/litellm_completion_bridge/handler.py", rationale: "A2A message/send routes through the completion bridge to a real provider and logs an asend_message spend row"}
|
||||
- {id: other.a2a.version.serves_pinned_0_3, module: other, tier: P1, area: a2a, assertions: [serves_pinned_0_3], source: "agent_endpoints/a2a_endpoints.py _served_version", rationale: "An agent pinning 0.3 returns the flat 0.3 message shape (parts on the result)"}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue