mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-31 06:56:59 +00:00
* test(integration): hand every harness server a port from one per-worker block
About 45 places in the integration harness picked a port by binding port 0,
reading the number back, closing the socket and starting a server on that
number later. Another process could take the port in between, which surfaced
as "Address already in use" flakes. Earlier fixes retried around the race in
single call sites without removing it.
tests/integration/_support/ports.py now owns every port the harness picks
ahead of a server. Each pytest process claims a 200-port block between 10000
and 32767 by taking an exclusive flock on a per-block lock file in the system
temp directory, so parallel runs on one machine never share a block and the
lock goes away with the process. The range is above the fixed service ports
and below the Linux and macOS ephemeral ranges. Ports go out in order, are
never reused in a run, skip anything an outside process already holds, and
a process that exhausts its block claims the next free one.
The three _free_port copies, worker_endpoints' port-0 binds,
free_loopback_port, the owned proxy's lost-port-race retry and the owned
Redis retry loop are gone. Tests that restart a server on the same port now
allocate that port up front instead of reading it back from the first
server. TQ012 in scripts/check_test_quality.py rejects a port-0 bind read
back with getsockname anywhere under tests/integration.
* ci(integration): record listening sockets and the ephemeral port range
* Revert "ci(integration): record listening sockets and the ephemeral port range"
This reverts commit d3fafc3da2.
298 lines
16 KiB
Python
298 lines
16 KiB
Python
"""Per-agent ``litellm_params.a2a_protocol_version`` pins the A2A transport the proxy speaks to the agent.
|
|
|
|
langgraph-api 0.15 serves cards declaring ``JSONRPC``/``1.0`` while answering in the 0.3 ``kind`` dialect, so
|
|
a2a-sdk picks its strict v1 transport and rejects every reply. Pinning "0.3" routes the SDK onto its v0.3
|
|
compat transport; pinning "1.0" keeps v1 even where the mis-cased-binding fingerprint would downgrade.
|
|
Every cell asserts the caller's response, the exact request the scripted agent received, and Postgres.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import os
|
|
import time
|
|
from typing import Final
|
|
|
|
import pytest
|
|
from integration._support.client import Gateway, eventually
|
|
from integration._support.database import read_rows
|
|
from integration._support.ports import allocate_port
|
|
from integration.compatibility._a2a_peers import (
|
|
HINT,
|
|
KIND_ERROR,
|
|
legacy_send,
|
|
peer,
|
|
posts,
|
|
register,
|
|
spend_rows,
|
|
sse_frames,
|
|
)
|
|
from pydantic import JsonValue
|
|
|
|
from litellm.constants import PROXY_CONFIG_RELOAD_INTERVAL_SECONDS
|
|
|
|
PROXY_WORKERS: Final = int(os.environ.get("INTEGRATION_PROXY_WORKERS", "1"))
|
|
WORKER_SYNC_SECONDS: Final = 0.0 if PROXY_WORKERS == 1 else PROXY_CONFIG_RELOAD_INTERVAL_SECONDS + 5.0
|
|
FRESH_CONNECTION: Final = {"Connection": "close"}
|
|
PINNED: Final = {"a2a_protocol_version": "0.3"}
|
|
ROUTES: Final = ("/a2a/{id}", "/a2a/{id}/message/send", "/v1/a2a/{id}/message/send")
|
|
|
|
|
|
def _artifact_text(result: dict[str, JsonValue]) -> str:
|
|
task: Final = result.get("task", result)
|
|
assert isinstance(task, dict), result
|
|
artifacts: Final = task["artifacts"]
|
|
assert isinstance(artifacts, list) and len(artifacts) == 1, result
|
|
parts: Final = artifacts[0]["parts"] # type: ignore[index]
|
|
return str(parts[0]["text"]) # type: ignore[index]
|
|
|
|
|
|
def _assert_legacy_wire(sent: tuple[dict[str, JsonValue], ...], marker: str, method: str = "message/send") -> None:
|
|
assert len(sent) == 1, sent
|
|
assert sent[0]["method"] == method, sent
|
|
message: Final = sent[0]["params"]["message"] # type: ignore[index]
|
|
assert message["kind"] == "message" and message["role"] == "user", sent
|
|
assert message["parts"] == [{"kind": "text", "text": "synthetic ping"}], sent
|
|
assert message["messageId"] == marker + "-in", sent
|
|
|
|
|
|
def _assert_v1_wire(sent: tuple[dict[str, JsonValue], ...], marker: str, method: str = "SendMessage") -> None:
|
|
assert len(sent) == 1, sent
|
|
assert sent[0]["method"] == method, sent
|
|
message: Final = sent[0]["params"]["message"] # type: ignore[index]
|
|
assert "kind" not in message and message["role"] == "ROLE_USER", sent
|
|
assert message["parts"] == [{"text": "synthetic ping"}], sent
|
|
assert message["messageId"] == marker + "-in", sent
|
|
|
|
|
|
def _one_spend_row(request_id: str, call_type: str, status: str) -> None:
|
|
rows: Final = eventually(lambda: spend_rows(request_id), lambda values: len(values) == 1, seconds=70)
|
|
assert rows[0]["call_type"] == call_type and rows[0]["status"] == status, rows
|
|
|
|
|
|
def _send_ok(gateway: Gateway, identity: str, marker: str, path: str = "/a2a/{id}") -> dict[str, JsonValue]:
|
|
response: Final = gateway.request("POST", path.format(id=identity), legacy_send(marker))
|
|
assert response.status_code == 200, response.text
|
|
body: Final = response.json()
|
|
assert body["id"] == marker and "error" not in body, response.text
|
|
assert _artifact_text(body["result"]) == "echo: synthetic ping", response.text
|
|
return body
|
|
|
|
|
|
def _send_kind_error(gateway: Gateway, identity: str, marker: str) -> str:
|
|
response: Final = gateway.request("POST", f"/a2a/{identity}", legacy_send(marker))
|
|
assert response.status_code == 500, response.text
|
|
message: Final = str(response.json()["error"]["message"])
|
|
assert KIND_ERROR in message, response.text
|
|
return message
|
|
|
|
|
|
@pytest.mark.parametrize("route", ROUTES)
|
|
def test_pinned_langgraph_agent_answers_message_send_in_the_legacy_dialect(gateway: Gateway, route: str) -> None:
|
|
with peer("lgsend", "langgraph") as agent, gateway.scenario() as scenario:
|
|
identity: Final = register(gateway, scenario.cleanups.callback, agent.marker, agent.url, PINNED)
|
|
_send_ok(gateway, identity, agent.marker, route)
|
|
_assert_legacy_wire(posts(agent.wire), agent.marker) # type: ignore[arg-type]
|
|
_one_spend_row(agent.marker, "asend_message", "success")
|
|
|
|
|
|
def test_pinned_langgraph_agent_streams_message_stream_to_the_final_frame(gateway: Gateway) -> None:
|
|
with peer("lgstream", "langgraph") as agent, gateway.scenario() as scenario:
|
|
identity: Final = register(gateway, scenario.cleanups.callback, agent.marker, agent.url, PINNED)
|
|
response: Final = gateway.request(
|
|
"POST", f"/a2a/{identity}", legacy_send(agent.marker, method="message/stream")
|
|
)
|
|
assert response.status_code == 200, response.text
|
|
assert response.headers["content-type"].startswith("text/event-stream"), response.headers
|
|
frames: Final = sse_frames(response.text)
|
|
assert all(frame["id"] == agent.marker and "error" not in frame for frame in frames), response.text
|
|
results: Final = [frame["result"] for frame in frames]
|
|
assert len(results) == 3, response.text
|
|
assert results[0]["statusUpdate"]["status"]["state"] == "TASK_STATE_WORKING", response.text # type: ignore[index]
|
|
assert results[0]["statusUpdate"]["status"]["message"]["parts"] == [{"text": "echo: synthetic ping"}] # type: ignore[index]
|
|
assert results[1]["artifactUpdate"]["artifact"]["parts"] == [{"text": "echo: synthetic ping"}] # type: ignore[index]
|
|
assert results[2]["statusUpdate"]["status"]["state"] == "TASK_STATE_COMPLETED", response.text # type: ignore[index]
|
|
_assert_legacy_wire(posts(agent.wire), agent.marker, "message/stream") # type: ignore[arg-type]
|
|
_one_spend_row(agent.marker, "asend_message_streaming", "success")
|
|
|
|
|
|
def test_v1_caller_reaches_pinned_legacy_agent_in_its_own_envelope(gateway: Gateway) -> None:
|
|
with peer("lgv1caller", "langgraph") as agent, gateway.scenario() as scenario:
|
|
identity: Final = register(gateway, scenario.cleanups.callback, agent.marker, agent.url, PINNED)
|
|
response: Final = gateway.client.post(
|
|
f"/a2a/{identity}",
|
|
headers={"Authorization": f"Bearer {gateway.key}", "a2a-version": "1.0"},
|
|
json={
|
|
"jsonrpc": "2.0",
|
|
"id": agent.marker,
|
|
"method": "SendMessage",
|
|
"params": {
|
|
"message": {
|
|
"role": "ROLE_USER",
|
|
"messageId": agent.marker + "-in",
|
|
"parts": [{"text": "synthetic ping"}],
|
|
}
|
|
},
|
|
},
|
|
)
|
|
assert response.status_code == 200, response.text
|
|
body: Final = response.json()
|
|
assert body["id"] == agent.marker and "error" not in body, response.text
|
|
task: Final = body["result"]["task"]
|
|
assert "kind" not in task and task["status"]["state"] == "TASK_STATE_COMPLETED", response.text
|
|
assert task["artifacts"][0]["parts"] == [{"text": "echo: synthetic ping"}], response.text
|
|
_assert_legacy_wire(posts(agent.wire), agent.marker) # type: ignore[arg-type]
|
|
_one_spend_row(agent.marker, "asend_message", "success")
|
|
|
|
|
|
@pytest.mark.parametrize("value", ("0.3.0", 0.3, " 0.3 "), ids=("dotted", "number", "spaced"))
|
|
def test_protocol_version_aliases_pin_the_legacy_transport(gateway: Gateway, value: JsonValue) -> None:
|
|
with peer("lgalias", "langgraph") as agent, gateway.scenario() as scenario:
|
|
identity: Final = register(
|
|
gateway, scenario.cleanups.callback, agent.marker, agent.url, {"a2a_protocol_version": value}
|
|
)
|
|
_send_ok(gateway, identity, agent.marker)
|
|
_assert_legacy_wire(posts(agent.wire), agent.marker) # type: ignore[arg-type]
|
|
_one_spend_row(agent.marker, "asend_message", "success")
|
|
|
|
|
|
def test_genuine_v1_agent_pinned_one_point_zero_is_unchanged(gateway: Gateway) -> None:
|
|
with peer("v1pinned", "v1") as agent, gateway.scenario() as scenario:
|
|
identity: Final = register(
|
|
gateway, scenario.cleanups.callback, agent.marker, agent.url, {"a2a_protocol_version": "1.0"}
|
|
)
|
|
_send_ok(gateway, identity, agent.marker)
|
|
_assert_v1_wire(posts(agent.wire), agent.marker) # type: ignore[arg-type]
|
|
_one_spend_row(agent.marker, "asend_message", "success")
|
|
|
|
|
|
def test_genuine_v1_agent_unpinned_is_unchanged(gateway: Gateway) -> None:
|
|
with peer("v1plain", "v1") as agent, gateway.scenario() as scenario:
|
|
identity: Final = register(gateway, scenario.cleanups.callback, agent.marker, agent.url, None)
|
|
_send_ok(gateway, identity, agent.marker)
|
|
_assert_v1_wire(posts(agent.wire), agent.marker) # type: ignore[arg-type]
|
|
_one_spend_row(agent.marker, "asend_message", "success")
|
|
|
|
|
|
@pytest.mark.parametrize("pin", (None, "0.3"), ids=("unpinned", "pinned-legacy"))
|
|
def test_lowercase_binding_still_downgrades_to_the_legacy_transport(gateway: Gateway, pin: str | None) -> None:
|
|
with (
|
|
peer("lglower", "langgraph", interfaces=[{"protocolBinding": "jsonrpc"}]) as agent,
|
|
gateway.scenario() as scenario,
|
|
):
|
|
params: Final = None if pin is None else {"a2a_protocol_version": pin}
|
|
identity: Final = register(gateway, scenario.cleanups.callback, agent.marker, agent.url, params)
|
|
_send_ok(gateway, identity, agent.marker)
|
|
_assert_legacy_wire(posts(agent.wire), agent.marker) # type: ignore[arg-type]
|
|
_one_spend_row(agent.marker, "asend_message", "success")
|
|
|
|
|
|
def test_explicit_one_point_zero_opts_out_of_the_lowercase_downgrade(gateway: Gateway) -> None:
|
|
with peer("v1lower", "v1", interfaces=[{"protocolBinding": "jsonrpc"}]) as agent, gateway.scenario() as scenario:
|
|
identity: Final = register(
|
|
gateway, scenario.cleanups.callback, agent.marker, agent.url, {"a2a_protocol_version": "1.0"}
|
|
)
|
|
_send_ok(gateway, identity, agent.marker)
|
|
_assert_v1_wire(posts(agent.wire), agent.marker) # type: ignore[arg-type]
|
|
_one_spend_row(agent.marker, "asend_message", "success")
|
|
|
|
|
|
def test_one_point_zero_pin_on_a_legacy_card_sends_v1(gateway: Gateway) -> None:
|
|
legacy_card: Final = [{"protocolBinding": "JSONRPC", "protocolVersion": "0.3"}]
|
|
with peer("v1oncard03", "v1", interfaces=legacy_card) as agent, gateway.scenario() as scenario:
|
|
identity: Final = register(
|
|
gateway, scenario.cleanups.callback, agent.marker, agent.url, {"a2a_protocol_version": "1.0"}
|
|
)
|
|
_send_ok(gateway, identity, agent.marker)
|
|
_assert_v1_wire(posts(agent.wire), agent.marker) # type: ignore[arg-type]
|
|
_one_spend_row(agent.marker, "asend_message", "success")
|
|
|
|
|
|
def test_override_rewrites_only_canonical_interfaces(gateway: Gateway) -> None:
|
|
with peer("lgmulti", "langgraph") as agent, peer("lgdecoy", "langgraph") as decoy, gateway.scenario() as scenario:
|
|
agent.interfaces = [
|
|
{"protocolBinding": "JSONRPC", "url": agent.url},
|
|
{"protocolBinding": "custom-ws", "url": decoy.url},
|
|
{"protocolBinding": "HTTP+JSON", "url": decoy.url},
|
|
]
|
|
identity: Final = register(gateway, scenario.cleanups.callback, agent.marker, agent.url, PINNED)
|
|
_send_ok(gateway, identity, agent.marker)
|
|
_assert_legacy_wire(posts(agent.wire), agent.marker) # type: ignore[arg-type]
|
|
assert decoy.wire.drain() == (), "the HTTP+JSON and custom bindings must not be selected" # type: ignore[union-attr]
|
|
_one_spend_row(agent.marker, "asend_message", "success")
|
|
|
|
|
|
def test_override_added_and_removed_by_patch_takes_effect_on_both_workers(gateway: Gateway) -> None:
|
|
with peer("lgpatch", "langgraph") as agent, gateway.scenario() as scenario:
|
|
identity: Final = register(gateway, scenario.cleanups.callback, agent.marker, agent.url, None)
|
|
assert HINT in _send_kind_error(gateway, identity, agent.marker + "-before")
|
|
before: Final = posts(agent.wire) # type: ignore[arg-type]
|
|
assert [item["method"] for item in before] == ["SendMessage"], before
|
|
|
|
def send(suffix: str) -> int:
|
|
response: Final = gateway.request(
|
|
"POST", f"/a2a/{identity}", legacy_send(agent.marker + suffix), headers=FRESH_CONNECTION
|
|
)
|
|
return response.status_code
|
|
|
|
def settle(params: dict[str, JsonValue], expected: int) -> None:
|
|
patched: Final = gateway.request("PATCH", f"/v1/agents/{identity}", {"litellm_params": params})
|
|
assert patched.status_code == 200, patched.text
|
|
assert patched.json()["litellm_params"].get("a2a_protocol_version") == params.get("a2a_protocol_version")
|
|
written_at: Final = time.monotonic()
|
|
counter = iter(range(10_000))
|
|
eventually(
|
|
lambda: (time.monotonic() - written_at, tuple(send(f"-{next(counter)}") for _ in range(4))),
|
|
lambda stamped: stamped[0] >= WORKER_SYNC_SECONDS and set(stamped[1]) == {expected},
|
|
seconds=WORKER_SYNC_SECONDS + 30,
|
|
)
|
|
agent.wire.drain() # type: ignore[union-attr]
|
|
|
|
settle(dict(PINNED), 200)
|
|
response: Final = gateway.request("POST", f"/a2a/{identity}", legacy_send(agent.marker))
|
|
assert response.status_code == 200, response.text
|
|
_assert_legacy_wire(posts(agent.wire), agent.marker) # type: ignore[arg-type]
|
|
settle({}, 500)
|
|
assert HINT in _send_kind_error(gateway, identity, agent.marker + "-after")
|
|
|
|
|
|
def test_repeated_pinned_sends_log_one_row_each(gateway: Gateway) -> None:
|
|
with peer("lgrepeat", "langgraph") as agent, gateway.scenario() as scenario:
|
|
identity: Final = register(gateway, scenario.cleanups.callback, agent.marker, agent.url, PINNED)
|
|
markers: Final = tuple(f"{agent.marker}-{index}" for index in range(10))
|
|
for marker in markers:
|
|
response = gateway.request("POST", f"/a2a/{identity}", legacy_send(marker))
|
|
assert response.status_code == 200 and response.json()["id"] == marker, response.text
|
|
sent: Final = posts(agent.wire) # type: ignore[arg-type]
|
|
assert [item["params"]["message"]["messageId"] for item in sent] == [m + "-in" for m in markers], sent # type: ignore[index]
|
|
assert len({item["id"] for item in sent}) == len(markers), sent
|
|
assert {item["method"] for item in sent} == {"message/send"}, sent
|
|
for marker in markers:
|
|
_one_spend_row(marker, "asend_message", "success")
|
|
|
|
|
|
def test_registered_agent_persists_protocol_version(gateway: Gateway) -> None:
|
|
with peer("lgstored", "langgraph") as agent, gateway.scenario() as scenario:
|
|
identity: Final = register(gateway, scenario.cleanups.callback, agent.marker, agent.url, PINNED)
|
|
assert gateway.get(f"/v1/agents/{identity}")["litellm_params"]["a2a_protocol_version"] == "0.3" # type: ignore[index]
|
|
rows: Final = read_rows('SELECT litellm_params FROM "LiteLLM_AgentsTable" WHERE agent_id=%s', (identity,))
|
|
assert len(rows) == 1, rows
|
|
stored: Final = rows[0]["litellm_params"]
|
|
stored_params: Final = json.loads(stored) if isinstance(stored, str) else stored
|
|
assert stored_params["a2a_protocol_version"] == "0.3", rows # type: ignore[index]
|
|
_send_ok(gateway, identity, agent.marker)
|
|
_assert_legacy_wire(posts(agent.wire), agent.marker) # type: ignore[arg-type]
|
|
|
|
|
|
def test_pinned_agent_survives_localhost_retry(gateway: Gateway) -> None:
|
|
dead: Final = f"http://localhost:{allocate_port()}/a2a/unreachable"
|
|
with peer("lgretry", "langgraph", card_url=dead) as agent, gateway.scenario() as scenario:
|
|
identity: Final = register(gateway, scenario.cleanups.callback, agent.marker, agent.url, PINNED)
|
|
_send_ok(gateway, identity, agent.marker)
|
|
traffic: Final = agent.wire.drain() # type: ignore[union-attr]
|
|
sent: Final = tuple(json.loads(item.body) for item in traffic if item.method == "POST")
|
|
assert tuple(item.target for item in traffic if item.method == "POST") == ("/a2a/" + agent.marker + "/",), (
|
|
traffic
|
|
)
|
|
_assert_legacy_wire(sent, agent.marker)
|
|
_one_spend_row(agent.marker, "asend_message", "success")
|