Skip to content

fix(a2a): normalize agent card protocolBinding casing before transport match - #37917

Merged
mateo-berri merged 5 commits into
litellm_internal_stagingfrom
litellm_a2a_protocol_binding_casing
Aug 24, 2026
Merged

fix(a2a): normalize agent card protocolBinding casing before transport match#37917
mateo-berri merged 5 commits into
litellm_internal_stagingfrom
litellm_a2a_protocol_binding_casing

Conversation

@devin-ai-integration

@devin-ai-integration devin-ai-integration Bot commented Aug 22, 2026

Copy link
Copy Markdown
Contributor

TLDR

Problem this solves:

  • A2A calls fail with "no compatible transports found." since v1.97
  • Agent cards with lowercase protocolBinding (e.g. LangGraph Platform) never match
  • The same servers speak the A2A 0.3 "kind" dialect, which strict v1 parsing rejects

How it solves it:

  • Normalize known bindings ("jsonrpc" -> "JSONRPC") before creating the a2a-sdk client
  • Downgrade mis-cased interfaces to protocolVersion "0.3" so the SDK picks its compat transport
  • Regression tests covering the lowercase-binding card and the langgraph reply dialect

User Flow

Before: a gateway user calling a LangGraph Platform agent through the proxy gets a hard JSON-RPC error

  1. The proxy admin registers the agent under agents: in config.yaml and restarts the proxy
  2. The user sends POST https://litellm-domain/a2a/{agent_id}/message/send with a message/send JSON-RPC body
  3. The response is {"jsonrpc":"2.0","id":"req-1","error":{"code":-32603,"message":"Internal error: no compatible transports found."}}

After: the same request reaches the agent and returns its completed task

  1. The proxy admin registers the agent under agents: in config.yaml and restarts the proxy
  2. The user sends POST https://litellm-domain/a2a/{agent_id}/message/send with a message/send JSON-RPC body
  3. The response is the agent's completed task with its reply text, e.g. {"id":"req-1","jsonrpc":"2.0","result":{"task":{"id":"...","status":{"state":"TASK_STATE_COMPLETED",...},"artifacts":[{"name":"Assistant Response","parts":[{"text":"..."}]}],...}}}

Relevant issues

Reported by a customer (Pylon ticket, their internal ref TPEDP-13005): after upgrading from v1.91 to v1.97, calls to agents behind LangGraph Platform via A2A fail. Two things broke together in that upgrade's a2a-sdk bump (0.3.24 -> 1.1.0). First, langgraph-api serves "protocolBinding": "jsonrpc" while a2a-sdk's ClientFactory.create() matches transports case-sensitively against uppercase TransportProtocol constants, so client creation fails outright; v1.91's legacy A2AClient never validated the binding. Second, the same servers declare protocolVersion: "1.0" yet still answer in the A2A 0.3 JSON dialect ("kind"-discriminated tasks and messages), which the SDK's strict v1 transport rejects once the casing is fixed. The mis-cased binding fingerprints such a server, so normalization also downgrades that interface's declared version to "0.3", routing the SDK's own factory onto its v0.3 compat transport, which speaks that dialect in both directions

Linear ticket

Resolves LIT-6031

Pre-Submission checklist

Please complete all items before asking a LiteLLM maintainer to review your PR

  • I have added meaningful tests
  • The handful of test files covering my change pass locally, e.g. uv run pytest tests/test_litellm/<your_test_file>.py -v. Leave the suites (make test-unit-*, make test-unit) to CI: it finishes in ~15 minutes where a laptop takes an hour or more
  • My PR passes all required CI/CD checks (e.g., lint, schema.d.ts sync check, etc.)
  • My PR's scope is as isolated as possible; it only solves 1 specific problem
  • I have received a Greptile Confidence Score of at least 4/5 before requesting a maintainer review (Greptile reviews automatically once the PR is opened; only comment @greptileai to re-request a review after pushing changes)

Delays in PR merge?

If you're seeing a delay in your PR being merged, ping the LiteLLM Team on Slack (#pr-review).

Screenshots / Proof of Fix

Shared setup: a real LangGraph dev server (langgraph-api 0.13.0, langgraph dev) serving an echo graph "agent" as assistant fe096781-5601-53d2-b2f6-0d3403f7e9ca. Its agent card declares "protocolBinding": "jsonrpc" (lowercase) with "protocolVersion": "1.0" and its replies use the A2A 0.3 "kind" dialect, exactly like LangGraph Platform. Each leg boots the proxy with 2 uvicorn workers (--num_workers 2) and registers the agent (the Before leg ran langgraph on port 43117 and the proxy on 43118, the After leg on 44229/44230):

general_settings:
  master_key: sk-qa-lit6031
agents:
  - agent_name: langgraph-real
    agent_card_params:
      protocolVersion: "1.0"
      name: langgraph-real
      description: real langgraph dev agent
      version: "1.0.0"
      url: http://127.0.0.1:44229/a2a/fe096781-5601-53d2-b2f6-0d3403f7e9ca
      capabilities: {streaming: true}
      defaultInputModes: [text/plain]
      defaultOutputModes: [text/plain]
      skills: []

No LLM is involved by design: the failure is in agent card transport negotiation, before any model call, and the graph echoes its input

Before (7481649)

message/send

  1. Command:
curl -s -X POST http://127.0.0.1:43118/a2a/d207d52392b6026d7c064c2b58e86ccc58c59ebb8a8f811c5300b215ebe59ff7/message/send \
  -H "Authorization: Bearer sk-qa-lit6031" -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":"req-1","method":"message/send","params":{"message":{"messageId":"m1","role":"user","parts":[{"kind":"text","text":"ping"}]}}}'
  1. Observed output:
{"jsonrpc":"2.0","id":"req-1","error":{"code":-32603,"message":"Internal error: no compatible transports found."}}

message/stream (served on the bare /a2a/{agent_id} JSON-RPC route)

  1. Command:
curl -s -N -X POST http://127.0.0.1:43118/a2a/d207d52392b6026d7c064c2b58e86ccc58c59ebb8a8f811c5300b215ebe59ff7 \
  -H "Authorization: Bearer sk-qa-lit6031" -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":"req-2","method":"message/stream","params":{"message":{"messageId":"m2","role":"user","parts":[{"kind":"text","text":"ping"}]}}}'
  1. Observed output, a single SSE error frame (traceback truncated here, full one leaks to the client):
data: {"jsonrpc": "2.0", "id": "req-2", "error": {"code": -32603, "message": "no compatible transports found.\n\nTraceback (most recent call last):\n  ...\n  File \".../a2a/client/client_factory.py\", line 346, in create\n    raise ValueError('no compatible transports found.')"}}

After (fe567bd)

message/send

  1. Command:
curl -s -X POST http://127.0.0.1:44230/a2a/d207d52392b6026d7c064c2b58e86ccc58c59ebb8a8f811c5300b215ebe59ff7/message/send \
  -H "Authorization: Bearer sk-qa-lit6031" -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":"req-1","method":"message/send","params":{"message":{"messageId":"m-after-1","role":"user","parts":[{"kind":"text","text":"ping after fix"}]}}}'
  1. Observed output:
{"id":"req-1","jsonrpc":"2.0","result":{"task":{"id":"57b8e018-74e5-450f-96c7-fb77b9c6dbcb:01a034ea-f1d5-7952-b576-708e1de98354","contextId":"57b8e018-74e5-450f-96c7-fb77b9c6dbcb","status":{"state":"TASK_STATE_COMPLETED","timestamp":"2026-08-24T17:56:48.398749Z"},"artifacts":[{"artifactId":"7f58d5f1-3711-41f5-8b03-cd2dd56c57ed","name":"Assistant Response","description":"Response from assistant fe096781-5601-53d2-b2f6-0d3403f7e9ca","parts":[{"text":"langgraph echo: ping after fix"}]}],"history":[{"messageId":"m-after-1","contextId":"57b8e018-74e5-450f-96c7-fb77b9c6dbcb","taskId":"57b8e018-74e5-450f-96c7-fb77b9c6dbcb:01a034ea-f1d5-7952-b576-708e1de98354","role":"ROLE_USER","parts":[{"text":"ping after fix"}]},{"messageId":"38b9f459-d481-4480-b6b0-7224afe1fbe4","contextId":"57b8e018-74e5-450f-96c7-fb77b9c6dbcb","taskId":"57b8e018-74e5-450f-96c7-fb77b9c6dbcb:01a034ea-f1d5-7952-b576-708e1de98354","role":"ROLE_AGENT","parts":[{"text":"langgraph echo: ping after fix"}]}]}}}

message/stream (served on the bare /a2a/{agent_id} JSON-RPC route)

  1. Command:
curl -s -N -X POST http://127.0.0.1:44230/a2a/d207d52392b6026d7c064c2b58e86ccc58c59ebb8a8f811c5300b215ebe59ff7 \
  -H "Authorization: Bearer sk-qa-lit6031" -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":"req-2","method":"message/stream","params":{"message":{"messageId":"m-after-2","role":"user","parts":[{"kind":"text","text":"stream after fix"}]}}}'
  1. Observed output, three SSE frames:
data: {"id": "req-2", "jsonrpc": "2.0", "result": {"task": {"id": "5b6d2bd6-07f3-43b6-9289-51148060bf6e:01a034eb-0c2c-7173-9c46-e513ce6996e2", "contextId": "5b6d2bd6-07f3-43b6-9289-51148060bf6e", "status": {"state": "TASK_STATE_SUBMITTED", "timestamp": "2026-08-24T17:56:55.522325Z"}, "history": [{"messageId": "m-after-2", "contextId": "5b6d2bd6-07f3-43b6-9289-51148060bf6e", "taskId": "5b6d2bd6-07f3-43b6-9289-51148060bf6e:01a034eb-0c2c-7173-9c46-e513ce6996e2", "role": "ROLE_USER", "parts": [{"text": "stream after fix"}]}]}}}

data: {"id": "req-2", "jsonrpc": "2.0", "result": {"artifactUpdate": {"taskId": "5b6d2bd6-07f3-43b6-9289-51148060bf6e:01a034eb-0c2c-7173-9c46-e513ce6996e2", "contextId": "5b6d2bd6-07f3-43b6-9289-51148060bf6e", "artifact": {"artifactId": "c7872977-fcfb-486b-92ca-bf924b4b38a5", "name": "Assistant Response", "description": "Response from assistant fe096781-5601-53d2-b2f6-0d3403f7e9ca", "parts": [{"text": "langgraph echo: stream after fix"}]}, "lastChunk": true}}}

data: {"id": "req-2", "jsonrpc": "2.0", "result": {"statusUpdate": {"taskId": "5b6d2bd6-07f3-43b6-9289-51148060bf6e:01a034eb-0c2c-7173-9c46-e513ce6996e2", "contextId": "5b6d2bd6-07f3-43b6-9289-51148060bf6e", "status": {"state": "TASK_STATE_COMPLETED", "timestamp": "2026-08-24T17:56:55.556547Z"}}}}

Run observations:

  • No literal /message/stream route exists; pre-existing, unchanged here
  • Stream errors leak server tracebacks to clients; pre-existing
  • Result shape is v1 proto JSON; SDK-bump property, unchanged here

Type

🐛 Bug Fix

Caveats (if any)

  • Only known bindings are normalized: jsonrpc, http+json, grpc
  • Unknown bindings still fail as before, matching a2a-sdk behavior
  • Mis-cased interfaces also get the 0.3 compat dialect

Final Attestation

  • The tests check the right things, including the edge cases, and regressions in the respective real-world customer use-cases are not possible after this PR

Link to Devin session: https://app.devin.ai/sessions/e97eb796504147258f46ce84ea43d373


Note

Medium Risk
Changes the shared A2A client bootstrap and transport/version selection for every agent; mis-normalization could affect correctly declared v1 agents, though only miscased known bindings are altered.

Overview
Fixes A2A proxy calls to agents like LangGraph Platform that advertise lowercase protocolBinding values and still speak the A2A 0.3 "kind" JSON dialect while declaring protocolVersion 1.0.

Adds normalize_agent_card_interfaces, which copies the resolved card and rewrites known miscased bindings (jsonrpcJSONRPC, etc.). When a binding is corrected, that interface’s protocol_version is set to 0.3 so a2a-sdk selects its compat transport instead of failing transport match or strict v1 parsing.

create_a2a_client now fetches the agent card with LiteLLM’s resolver (including auth headers on the card GET), normalizes interfaces, then passes the agent_card into create_client rather than a base_url and internal resolver kwargs. The normalized card is always stored on _litellm_agent_card.

Tests cover normalization edge cases, an end-to-end lowercase-binding + LangGraph-style task reply, and updated mocks for the new client-creation flow.

Reviewed by Cursor Bugbot for commit fe567bd. Bugbot is set up for automated code reviews on this repo. Configure here.

…t match

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
@devin-ai-integration

Copy link
Copy Markdown
Contributor Author

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment, CI, and merge conflict monitoring

@greptile-apps

greptile-apps Bot commented Aug 22, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

The PR normalizes known mis-cased A2A protocol bindings on an independent agent-card copy and selects the legacy compatibility dialect for affected interfaces.

  • Resolves agent cards explicitly with caller-specific headers before client creation.
  • Canonicalizes known transport bindings without modifying the resolver-owned card.
  • Adds regression coverage for card independence, authenticated card resolution, and LangGraph-compatible request/response handling.

Confidence Score: 5/5

The PR appears safe to merge.

No blocking failure remains.

Important Files Changed

Filename Overview
litellm/a2a_protocol/card_resolver.py Adds copy-based interface normalization, preserving the original resolved card while canonicalizing transport metadata on the client-facing copy.
litellm/a2a_protocol/main.py Resolves and normalizes the agent card before passing it to the SDK client factory while preserving per-caller header isolation.
tests/test_litellm/a2a_protocol/test_card_resolver.py Verifies canonicalization, selective version downgrade, and independence of the original protobuf card.
tests/test_litellm/a2a_protocol/test_main.py Adds end-to-end mock-transport coverage for lowercase bindings and the compatibility response dialect.
tests/test_litellm/proxy/agent_endpoints/test_agent_header_isolation.py Updates client-construction mocks for explicit card resolution while retaining shared-client and timeout assertions.

Reviews (3): Last reviewed commit: "fix(a2a): speak the 0.3 dialect to serve..." | Re-trigger Greptile

Comment thread litellm/a2a_protocol/card_resolver.py Outdated
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
@codecov

codecov Bot commented Aug 22, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@codspeed-hq

codspeed-hq Bot commented Aug 22, 2026

Copy link
Copy Markdown
Contributor

Merging this PR will not alter performance

✅ 31 untouched benchmarks


Comparing litellm_a2a_protocol_binding_casing (fe567bd) with litellm_internal_staging (d447be1)1

Open in CodSpeed

Footnotes

  1. No successful run was found on litellm_internal_staging (3122600) during the generation of this report, so d447be1 was used instead as the comparison base. There might be some changes unrelated to this pull request in this report.

milan-berri and others added 2 commits August 23, 2026 03:14
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…resolved card

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
@devin-ai-integration

Copy link
Copy Markdown
Contributor Author

@greptileai the normalization helper now returns a fresh card copy instead of mutating the resolved SDK card

@mateo-berri

Copy link
Copy Markdown
Contributor

bugbot run

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit de0d8ce. Configure here.

@mateo-berri

Copy link
Copy Markdown
Contributor

@greptileai

@mateo-berri

Copy link
Copy Markdown
Contributor

bugbot run

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit fe567bd. Configure here.

@mateo-berri mateo-berri left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@mateo-berri
mateo-berri merged commit 10b42a8 into litellm_internal_staging Aug 24, 2026
79 of 80 checks passed
@mateo-berri
mateo-berri deleted the litellm_a2a_protocol_binding_casing branch August 24, 2026 18:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants