Skip to content

Commit aee370b

Browse files
committed
LCORE-1958: Fix duplicate A2A operationIds and OpenAPI response metadata
Separate GET/POST operationId values on /a2a, tighten route summaries and descriptions for the spec, and fix Responses streaming OpenAPI (200 text/event- stream: drop invalid sibling description; clarify JSON success description). Regenerate docs/openapi.json for those paths only (no global tag list yet).
1 parent c50425e commit aee370b

4 files changed

Lines changed: 158 additions & 15 deletions

File tree

docs/openapi.json

Lines changed: 41 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -9205,7 +9205,7 @@
92059205
},
92069206
"responses": {
92079207
"200": {
9208-
"description": "Successful response",
9208+
"description": "Successful response. For `text/event-stream`, the body is a Server-Sent Events stream.",
92099209
"content": {
92109210
"application/json": {
92119211
"schema": {
@@ -9262,8 +9262,7 @@
92629262
"schema": {
92639263
"type": "string"
92649264
},
9265-
"example": "event: response.created\ndata: {\"type\":\"response.created\",\"sequence_number\":0,\"response\":{\"id\":\"resp_abc\",\"object\":\"response\",\"created_at\":1704067200,\"status\":\"in_progress\",\"model\":\"openai/gpt-4o-mini\",\"output\":[],\"store\":true,\"text\":{\"format\":{\"type\":\"text\"}},\"conversation\":\"0d21ba731f21f798dc9680125d5d6f49\",\"available_quotas\":{},\"output_text\":\"\"}}\n\nevent: response.output_item.added\ndata: {\"type\":\"response.output_item.added\",\"sequence_number\":1,\"response_id\":\"resp_abc\",\"output_index\":0,\"item\":{\"id\":\"msg_abc\",\"type\":\"message\",\"status\":\"in_progress\",\"role\":\"assistant\",\"content\":[]}}\n\n...\n\nevent: response.completed\ndata: {\"type\":\"response.completed\",\"sequence_number\":30,\"response\":{\"id\":\"resp_abc\",\"object\":\"response\",\"created_at\":1704067200,\"status\":\"completed\",\"model\":\"openai/gpt-4o-mini\",\"output\":[{\"id\":\"msg_abc\",\"type\":\"message\",\"status\":\"completed\",\"role\":\"assistant\",\"content\":[{\"type\":\"output_text\",\"text\":\"Hello! How can I help?\",\"annotations\":[]}]}],\"store\":true,\"text\":{\"format\":{\"type\":\"text\"}},\"usage\":{\"input_tokens\":10,\"output_tokens\":6,\"total_tokens\":16,\"input_tokens_details\":{\"cached_tokens\":0},\"output_tokens_details\":{\"reasoning_tokens\":0}},\"conversation\":\"0d21ba731f21f798dc9680125d5d6f49\",\"available_quotas\":{\"daily\":1000,\"monthly\":50000},\"output_text\":\"Hello! How can I help?\"}}\n\ndata: [DONE]\n\n",
9266-
"description": "SSE stream of events"
9265+
"example": "event: response.created\ndata: {\"type\":\"response.created\",\"sequence_number\":0,\"response\":{\"id\":\"resp_abc\",\"object\":\"response\",\"created_at\":1704067200,\"status\":\"in_progress\",\"model\":\"openai/gpt-4o-mini\",\"output\":[],\"store\":true,\"text\":{\"format\":{\"type\":\"text\"}},\"conversation\":\"0d21ba731f21f798dc9680125d5d6f49\",\"available_quotas\":{},\"output_text\":\"\"}}\n\nevent: response.output_item.added\ndata: {\"type\":\"response.output_item.added\",\"sequence_number\":1,\"response_id\":\"resp_abc\",\"output_index\":0,\"item\":{\"id\":\"msg_abc\",\"type\":\"message\",\"status\":\"in_progress\",\"role\":\"assistant\",\"content\":[]}}\n\n...\n\nevent: response.completed\ndata: {\"type\":\"response.completed\",\"sequence_number\":30,\"response\":{\"id\":\"resp_abc\",\"object\":\"response\",\"created_at\":1704067200,\"status\":\"completed\",\"model\":\"openai/gpt-4o-mini\",\"output\":[{\"id\":\"msg_abc\",\"type\":\"message\",\"status\":\"completed\",\"role\":\"assistant\",\"content\":[{\"type\":\"output_text\",\"text\":\"Hello! How can I help?\",\"annotations\":[]}]}],\"store\":true,\"text\":{\"format\":{\"type\":\"text\"}},\"usage\":{\"input_tokens\":10,\"output_tokens\":6,\"total_tokens\":16,\"input_tokens_details\":{\"cached_tokens\":0},\"output_tokens_details\":{\"reasoning_tokens\":0}},\"conversation\":\"0d21ba731f21f798dc9680125d5d6f49\",\"available_quotas\":{\"daily\":1000,\"monthly\":50000},\"output_text\":\"Hello! How can I help?\"}}\n\ndata: [DONE]\n\n"
92679266
}
92689267
}
92699268
},
@@ -10600,15 +10599,30 @@
1060010599
"tags": [
1060110600
"a2a"
1060210601
],
10603-
"summary": "Handle A2A Jsonrpc",
10604-
"description": "Handle A2A JSON-RPC requests following the A2A protocol specification.\n\nThis endpoint uses the DefaultRequestHandler from the A2A SDK to handle\nall JSON-RPC requests including message/send, message/stream, etc.\n\nThe A2A SDK application is created per-request to include authentication\ncontext while still leveraging FastAPI's authorization middleware.\n\nAutomatically detects streaming requests (message/stream JSON-RPC method)\nand returns a StreamingResponse to enable real-time chunk delivery.\n\nArgs:\n request: FastAPI request object\n auth: Authentication tuple\n mcp_headers: MCP headers for context propagation\n\nReturns:\n JSON-RPC response or streaming response",
10602+
"summary": "Handle A2A JSON-RPC GET",
10603+
"description": "Handle GET on /a2a for A2A JSON-RPC requests following the A2A protocol specification.",
1060510604
"operationId": "handle_a2a_jsonrpc_a2a_get",
1060610605
"responses": {
1060710606
"200": {
10608-
"description": "Successful Response",
10607+
"description": "Successful response: buffered JSON-RPC or HTTP payload for non-streaming calls, or a Server-Sent Events stream when the JSON-RPC method is message/stream.",
1060910608
"content": {
1061010609
"application/json": {
10611-
"schema": {}
10610+
"schema": {
10611+
"type": "object",
10612+
"description": "JSON-RPC 2.0 response or A2A-over-HTTP payload"
10613+
},
10614+
"example": {
10615+
"jsonrpc": "2.0",
10616+
"id": "1",
10617+
"result": {}
10618+
}
10619+
},
10620+
"text/event-stream": {
10621+
"schema": {
10622+
"type": "string",
10623+
"format": "text/event-stream"
10624+
},
10625+
"example": "data: {\"jsonrpc\":\"2.0\",\"id\":\"1\",\"result\":{}}\n\n"
1061210626
}
1061310627
}
1061410628
}
@@ -10618,15 +10632,30 @@
1061810632
"tags": [
1061910633
"a2a"
1062010634
],
10621-
"summary": "Handle A2A Jsonrpc",
10622-
"description": "Handle A2A JSON-RPC requests following the A2A protocol specification.\n\nThis endpoint uses the DefaultRequestHandler from the A2A SDK to handle\nall JSON-RPC requests including message/send, message/stream, etc.\n\nThe A2A SDK application is created per-request to include authentication\ncontext while still leveraging FastAPI's authorization middleware.\n\nAutomatically detects streaming requests (message/stream JSON-RPC method)\nand returns a StreamingResponse to enable real-time chunk delivery.\n\nArgs:\n request: FastAPI request object\n auth: Authentication tuple\n mcp_headers: MCP headers for context propagation\n\nReturns:\n JSON-RPC response or streaming response",
10623-
"operationId": "handle_a2a_jsonrpc_a2a_get",
10635+
"summary": "Handle A2A JSON-RPC POST",
10636+
"description": "Handle POST on /a2a for A2A JSON-RPC requests following the A2A protocol specification.",
10637+
"operationId": "handle_a2a_jsonrpc_a2a_post",
1062410638
"responses": {
1062510639
"200": {
10626-
"description": "Successful Response",
10640+
"description": "Successful response: buffered JSON-RPC or HTTP payload for non-streaming calls, or a Server-Sent Events stream when the JSON-RPC method is message/stream.",
1062710641
"content": {
1062810642
"application/json": {
10629-
"schema": {}
10643+
"schema": {
10644+
"type": "object",
10645+
"description": "JSON-RPC 2.0 response or A2A-over-HTTP payload"
10646+
},
10647+
"example": {
10648+
"jsonrpc": "2.0",
10649+
"id": "1",
10650+
"result": {}
10651+
}
10652+
},
10653+
"text/event-stream": {
10654+
"schema": {
10655+
"type": "string",
10656+
"format": "text/event-stream"
10657+
},
10658+
"example": "data: {\"jsonrpc\":\"2.0\",\"id\":\"1\",\"result\":{}}\n\n"
1063010659
}
1063110660
}
1063210661
}

src/app/endpoints/a2a.py

Lines changed: 85 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
from starlette.responses import Response, StreamingResponse
3737

3838
from a2a_storage import A2AContextStore, A2AStorageFactory
39+
from app.endpoints.a2a_openapi import a2a_jsonrpc_responses
3940
from authentication import get_auth_dependency
4041
from authentication.interface import AuthTuple
4142
from authorization.middleware import authorize
@@ -692,12 +693,94 @@ async def _create_a2a_app(
692693
return a2a_app.build()
693694

694695

695-
@router.api_route("/a2a", methods=["GET", "POST"], response_model=None)
696+
@router.get(
697+
"/a2a",
698+
response_model=None,
699+
responses=a2a_jsonrpc_responses,
700+
operation_id="handle_a2a_jsonrpc_a2a_get",
701+
summary="Handle A2A JSON-RPC GET",
702+
description=(
703+
"Handle GET on /a2a for A2A JSON-RPC requests following the A2A protocol specification."
704+
),
705+
)
706+
@authorize(Action.A2A_JSONRPC)
707+
async def handle_a2a_jsonrpc_get(
708+
request: Request,
709+
auth: Annotated[AuthTuple, Depends(auth_dependency)],
710+
mcp_headers: McpHeaders = Depends(mcp_headers_dependency),
711+
) -> Response | StreamingResponse:
712+
"""Serve A2A JSON-RPC over HTTP GET on ``/a2a``.
713+
714+
Thin wrapper that delegates to ``_handle_a2a_jsonrpc`` so GET and POST share
715+
the same processing path while keeping distinct OpenAPI operation metadata.
716+
717+
Args:
718+
request: Incoming ASGI/FastAPI request (body, scope, headers).
719+
auth: Resolved authentication tuple from ``auth_dependency`` (user
720+
identity and bearer token used to build the per-request A2A app).
721+
mcp_headers: MCP-related headers from ``mcp_headers_dependency``, forwarded
722+
into the A2A executor for downstream tool/context propagation.
723+
724+
Returns:
725+
``Response`` with the full buffered JSON-RPC (or HTTP) payload when the
726+
request is non-streaming, or ``StreamingResponse`` (SSE) when the
727+
JSON-RPC method is ``message/stream`` and chunks are streamed to the
728+
client. Error conditions are generally expressed as JSON-RPC or HTTP
729+
responses rather than by raising from this wrapper.
730+
731+
Raises:
732+
HTTPException: If authentication or ``@authorize`` rejects the request
733+
before or while entering the handler chain.
734+
"""
735+
return await _handle_a2a_jsonrpc(request, auth, mcp_headers)
736+
737+
738+
@router.post(
739+
"/a2a",
740+
response_model=None,
741+
responses=a2a_jsonrpc_responses,
742+
operation_id="handle_a2a_jsonrpc_a2a_post",
743+
summary="Handle A2A JSON-RPC POST",
744+
description=(
745+
"Handle POST on /a2a for A2A JSON-RPC requests following the A2A protocol specification."
746+
),
747+
)
696748
@authorize(Action.A2A_JSONRPC)
697-
async def handle_a2a_jsonrpc( # pylint: disable=too-many-locals,too-many-statements
749+
async def handle_a2a_jsonrpc_post(
698750
request: Request,
699751
auth: Annotated[AuthTuple, Depends(auth_dependency)],
700752
mcp_headers: McpHeaders = Depends(mcp_headers_dependency),
753+
) -> Response | StreamingResponse:
754+
"""Serve A2A JSON-RPC over HTTP POST on ``/a2a``.
755+
756+
Thin wrapper that delegates to ``_handle_a2a_jsonrpc`` so GET and POST share
757+
the same processing path while keeping distinct OpenAPI operation metadata.
758+
759+
Args:
760+
request: Incoming ASGI/FastAPI request (body, scope, headers).
761+
auth: Resolved authentication tuple from ``auth_dependency`` (user
762+
identity and bearer token used to build the per-request A2A app).
763+
mcp_headers: MCP-related headers from ``mcp_headers_dependency``, forwarded
764+
into the A2A executor for downstream tool/context propagation.
765+
766+
Returns:
767+
``Response`` with the full buffered JSON-RPC (or HTTP) payload when the
768+
request is non-streaming, or ``StreamingResponse`` (SSE) when the
769+
JSON-RPC method is ``message/stream`` and chunks are streamed to the
770+
client. Error conditions are generally expressed as JSON-RPC or HTTP
771+
responses rather than by raising from this wrapper.
772+
773+
Raises:
774+
HTTPException: If authentication or ``@authorize`` rejects the request
775+
before or while entering the handler chain.
776+
"""
777+
return await _handle_a2a_jsonrpc(request, auth, mcp_headers)
778+
779+
780+
async def _handle_a2a_jsonrpc( # pylint: disable=too-many-locals,too-many-statements
781+
request: Request,
782+
auth: AuthTuple,
783+
mcp_headers: McpHeaders,
701784
) -> Response | StreamingResponse:
702785
"""
703786
Handle A2A JSON-RPC requests following the A2A protocol specification.

src/app/endpoints/a2a_openapi.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
"""OpenAPI-only metadata for A2A JSON-RPC routes."""
2+
3+
from typing import Any, Final
4+
5+
from constants import MEDIA_TYPE_EVENT_STREAM, MEDIA_TYPE_JSON
6+
7+
# 200 may be buffered JSON-RPC (application/json) or SSE (text/event-stream).
8+
a2a_jsonrpc_responses: Final[dict[int | str, dict[str, Any]]] = {
9+
200: {
10+
"description": "Successful response",
11+
"content": {
12+
MEDIA_TYPE_JSON: {
13+
"schema": {
14+
"type": "object",
15+
"description": "JSON-RPC 2.0 response or A2A-over-HTTP payload",
16+
},
17+
"example": {"jsonrpc": "2.0", "id": "1", "result": {}},
18+
},
19+
MEDIA_TYPE_EVENT_STREAM: {
20+
"schema": {
21+
"type": "string",
22+
"description": (
23+
"Server-Sent Events stream when "
24+
"the JSON-RPC method is message/stream"
25+
),
26+
"format": MEDIA_TYPE_EVENT_STREAM,
27+
},
28+
"example": 'data: {"jsonrpc":"2.0","id":"1","result":{}}\n\n',
29+
},
30+
},
31+
},
32+
}

src/models/responses.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1681,7 +1681,6 @@ def openapi_response(cls) -> dict[str, Any]:
16811681
"application/json": {"example": json_example} if json_example else {},
16821682
"text/event-stream": {
16831683
"schema": {"type": "string"},
1684-
"description": "SSE stream of events",
16851684
"example": sse_example,
16861685
},
16871686
}

0 commit comments

Comments
 (0)