Skip to content

fix(a2a): allow LiteLLMSendMessageResponse.id to be str | int | None (LIT-2818) - #29196

Open
oss-agent-shin wants to merge 1 commit into
BerriAI:litellm_oss_agent_shin_daily_branchfrom
oss-agent-shin:fix/lit-2818-litellm-send-message-response-id-optional
Open

fix(a2a): allow LiteLLMSendMessageResponse.id to be str | int | None (LIT-2818)#29196
oss-agent-shin wants to merge 1 commit into
BerriAI:litellm_oss_agent_shin_daily_branchfrom
oss-agent-shin:fix/lit-2818-litellm-send-message-response-id-optional

Conversation

@oss-agent-shin

@oss-agent-shin oss-agent-shin commented May 28, 2026

Copy link
Copy Markdown
Contributor

Summary

Relax LiteLLMSendMessageResponse.id from str (required) to Optional[Union[str, int]] = None to match the JSON-RPC 2.0 spec and the
upstream a2a SDK (a2a-sdk==0.3.24).

Linear

LIT-2818 — "Investigate LiteLLM error-response validation failure for A2A".

Root cause

LiteLLMSendMessageResponse in litellm/types/agents.py:301 wraps the
upstream a2a SDK’s SendMessageResponse. Per JSON-RPC 2.0 §4 / §5, the
response id is a String | Number | Null value, and error responses that
could not be correlated to a request MUST use null. The upstream a2a
SDK (a2a-sdk==0.3.24) reflects this:

# from a2a/types/__init__.py
class SendMessageSuccessResponse(...):
    id: str | int | None = None

class JSONRPCErrorResponse(...):
    id: str | int | None = None

Pre-fix, LiteLLMSendMessageResponse.id: str (required) rejected three
shapes the SDK accepts:

  1. id is a number (per spec, id MAY be a Number).
  2. id is null (per spec, error responses that could not be correlated
    to a request MUST use null).
  3. id is missing — produced by a model_dump(exclude_none=True)
    roundtrip on a null-id response (this is exactly the path
    litellm.a2a_protocol.main.asend_message takes:
    a2a_response.model_dump(mode="json", exclude_none=True)
    LiteLLMSendMessageResponse.from_a2a_response).

The success path (result.kind="task" / "message") passes because
production agents echo the client’s string id. The error-response path
(e.g. upstream JSON parse error or generic Internal error) fails Pydantic
validation, which the proxy surfaces as a 500 — matching the report on
LiteLLM v1.82.0.

Fix

Single-field change in litellm/types/agents.py:LiteLLMSendMessageResponse:

# before
id: str

# after
id: Optional[Union[str, int]] = None

Matches SendMessageSuccessResponse.id and JSONRPCErrorResponse.id on the
pinned SDK version (a2a-sdk==0.3.24). No callers rely on id being a
non-None string — grep across litellm/ shows it is only assigned/round-tripped
through to the response object.

Evidence

Direct reproduction against the two production code paths
(LiteLLMSendMessageResponse.from_dict used by
_send_message_via_completion_bridge, and
LiteLLMSendMessageResponse.from_a2a_response used by asend_message) on
the same checkout (litellm_oss_agent_shin_daily_branch HEAD 1fe911d) —
first without the patch, then with the patch.

Before (clean litellm_oss_agent_shin_daily_branch)

--- from_dict, id=str (success path)
  OK  -> LiteLLMSendMessageResponse(id='req-1', error={'code': -32603, 'message': 'Internal error'})
--- from_dict, id=int (JSON-RPC 2.0 numeric id)
  FAIL -> ValidationError: 1 validation error for LiteLLMSendMessageResponse
--- from_dict, id=None (JSON-RPC 2.0 parse-error)
  FAIL -> ValidationError: 1 validation error for LiteLLMSendMessageResponse
--- from_dict, id omitted (model_dump exclude_none roundtrip)
  FAIL -> ValidationError: 1 validation error for LiteLLMSendMessageResponse
--- from_a2a_response, id=str
  OK  -> LiteLLMSendMessageResponse(id='req-1', error={'code': -32603, 'message': 'Internal error'})
--- from_a2a_response, id=int
  FAIL -> ValidationError: 1 validation error for LiteLLMSendMessageResponse
--- from_a2a_response, id=None
  FAIL -> ValidationError: 1 validation error for LiteLLMSendMessageResponse

SUMMARY: 2/7 cases accepted by LiteLLMSendMessageResponse

After (this PR)

--- from_dict, id=str (success path)
  OK  -> LiteLLMSendMessageResponse(id='req-1', error={'code': -32603, 'message': 'Internal error'})
--- from_dict, id=int (JSON-RPC 2.0 numeric id)
  OK  -> LiteLLMSendMessageResponse(id=42, error={'code': -32603, 'message': 'Internal error'})
--- from_dict, id=None (JSON-RPC 2.0 parse-error)
  OK  -> LiteLLMSendMessageResponse(id=None, error={'code': -32603, 'message': 'Internal error'})
--- from_dict, id omitted (model_dump exclude_none roundtrip)
  OK  -> LiteLLMSendMessageResponse(id=None, error={'code': -32603, 'message': 'Internal error'})
--- from_a2a_response, id=str
  OK  -> LiteLLMSendMessageResponse(id='req-1', error={'code': -32603, 'message': 'Internal error'})
--- from_a2a_response, id=int
  OK  -> LiteLLMSendMessageResponse(id=7, error={'code': -32603, 'message': 'Internal error'})
--- from_a2a_response, id=None
  OK  -> LiteLLMSendMessageResponse(id=None, error={'code': -32603, 'message': 'Internal error'})

SUMMARY: 7/7 cases accepted by LiteLLMSendMessageResponse

All five previously-rejected JSON-RPC error-response shapes
(numeric id, null id, omitted id across both code paths) are now accepted.
The success path (string id) is unchanged.

Tests

Added tests/test_litellm/a2a_protocol/test_send_message_response_id_lit2818.py
with 14 cases covering:

  • Constructor: id is string, int, None, or missing.
  • from_dict(...): all three JSON-RPC id shapes + omitted-id roundtrip.
  • from_a2a_response(...): real a2a.types.SendMessageResponse(root= JSONRPCErrorResponse(...)) payloads for each id shape — exercises the
    same model_dump(exclude_none=True) roundtrip the proxy takes.
  • Negative guards: float, list, dict ids are still rejected (we only
    widened the allowed set, did not loosen type-checking).

Full tests/test_litellm/a2a_protocol/ suite: 41 passed.

Notes

Pushed via Git Data API (blobs → tree → commit → ref) against the upstream
base tree, so the merge-base diff should be exactly the two files above.

Verification (ship-pr)

Check Result
Base branch is litellm_oss_agent_shin_daily_branch PASS (target: litellm_oss_agent_shin_daily_branch)
All GitHub Actions green PASS (44/44 success, 0 failures)
Greptile review >= 4/5 PASS (5/5 — "Safe to merge")
Veria AI review PASS ("No security issues found")
Codecov patch coverage PASS (100% on modified lines)
Runtime evidence captured (before + after) PASS (see Evidence section above)
Unit tests added with the fix PASS (14 tests in tests/test_litellm/a2a_protocol/test_send_message_response_id_lit2818.py, full a2a_protocol suite 41 passed)
mergeable_state clean PASS
No customer name in PR title/body PASS
Diff scope matches task PASS (2 files: litellm/types/agents.py +10/-1, new regression test file)

…(LIT-2818)

Per JSON-RPC 2.0 the response id may be a String, Number, or Null. The
upstream a2a SDK (a2a-sdk==0.3.24) reflects this on both
SendMessageSuccessResponse and JSONRPCErrorResponse with
id: str | int | None = None. Pinning id: str (required) on
LiteLLMSendMessageResponse made the proxy raise a Pydantic ValidationError
on legitimate JSON-RPC error responses with numeric or null id, surfacing
as a 500 on the error-response path.
@CLAassistant

Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.
You have signed the CLA already but the status is still pending? Let us recheck it.

@greptile-apps

greptile-apps Bot commented May 28, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes a Pydantic validation crash on the A2A proxy error-response path by relaxing LiteLLMSendMessageResponse.id from required str to Optional[Union[str, int]] = None, matching the JSON-RPC 2.0 spec and the upstream a2a-sdk==0.3.24 contract.

  • litellm/types/agents.py: One-line type change on LiteLLMSendMessageResponse.id; numeric and null ids — which are valid per JSON-RPC 2.0 and produced by the SDK on error responses — no longer cause a 500.
  • tests/test_litellm/a2a_protocol/test_send_message_response_id_lit2818.py: New regression suite with 14 cases exercising all three valid id shapes plus four invalid ones; covers both from_dict and from_a2a_response code paths.

Confidence Score: 5/5

Safe to merge — the change only widens an accepted type from str to str | int | None, so all existing callers that pass a string continue to work unchanged.

The fix is a minimal, well-scoped relaxation of a type constraint that was too strict relative to the upstream SDK and the JSON-RPC 2.0 spec. The from_a2a_response path's exclude_none=True serialization round-trip is correctly handled by the new = None default. No callers depend on id being a non-None string. The new regression tests exercise all four constructor shapes (str, int, None, missing), both factory methods, and three invalid shapes that should still be rejected.

No files require special attention.

Important Files Changed

Filename Overview
litellm/types/agents.py Single-field change relaxing LiteLLMSendMessageResponse.id from required str to Optional[Union[str, int]] = None, aligning with JSON-RPC 2.0 spec and the upstream a2a SDK; well-commented and backward-compatible.
tests/test_litellm/a2a_protocol/test_send_message_response_id_lit2818.py New regression test file with 14 cases covering all JSON-RPC id shapes (str/int/None/omitted) via constructor, from_dict, and from_a2a_response; also guards against invalid types (float, list, dict). No real network calls.

Reviews (1): Last reviewed commit: "fix(a2a): allow LiteLLMSendMessageRespon..." | Re-trigger Greptile

@codecov

codecov Bot commented May 28, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@github-actions

Copy link
Copy Markdown
Contributor

This pull request has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs.

@github-actions github-actions Bot added the stale label Aug 27, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants