litellm/ui/litellm-dashboard
Sameer Kankute 8e30cfbeb1
feat(a2a): support a2a-sdk 1.x proxy routing for 0.3 and 1.0 agents (#30950)
* feat(a2a): support a2a-sdk 1.x proxy routing for 0.3 and 1.0 agents

Bump a2a-sdk to 1.x and wire send/stream through compat conversions so the proxy accepts A2A 1.0 JSON-RPC while preserving 0.3 wire clients.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Add user controlled protocol version in agents

* Fix exeception mapping

* Fix a2a base url

* Add e2e test for a2a

* Fix lint

* Fix lint

* fix(a2a): harden card version detection and header isolation coverage

Use protocolVersion when inferring agent card wire format, assert distinct httpx cache keys in the header-isolation test, and suppress targeted basedpyright errors for optional SDK imports.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(a2a): suppress reportArgumentType for SDK compat types and fix streaming trace ID

- Add pyright: ignore[reportArgumentType] to SendMessageSuccessResponse id= and
  result= args in _send_message, and SendStreamingMessageResponse root= in
  _stream_messages, where a2a-sdk compat types diverge from basedpyright's
  inferred signature, reducing the reportArgumentType count back within budget.
- Fix streaming trace ID in astream_a2a_message to use str(request.id) when
  available instead of always generating a new uuid4(), restoring JSON-RPC
  request-ID correlation for observability.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* style(a2a): expand SendStreamingMessageResponse for black formatting

Move pyright: ignore comment to the root= argument line so Black
accepts the expanded multi-line form.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(a2a): fix 2 reportArgumentType errors without suppression

- main.py: narrow logging_obj from object|None to Optional[Logging] via
  isinstance check before A2AStreamingIterator call, fixing the
  "Logging | object" argument type mismatch at line 699.
- a2a_endpoints.py: extract response_dict with explicit isinstance(dict)
  guard before passing to normalize_jsonrpc_response, fixing the
  "LLMResponseTypes | dict[str, Any]" type mismatch at line 835.
- Remove spurious pyright: ignore comments added in previous commits that
  were not suppressing the actual errors.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(a2a): rewrite upstream URL for 1.0 agent cards in getAuthenticatedExtendedCard

1.0 upstream agent cards store the endpoint URL in supportedInterfaces[0].url
rather than a top-level url field. The previous guard only rewrote url when
it existed at the top level, so after normalize_agent_card lowered a 1.0 card
to 0.3 the upstream internal address leaked into the url field of the 0.3
response.

Fix: rewrite both url and supportedInterfaces[0].url to the proxy address
before calling normalize_agent_card, ensuring the upstream address is never
visible to downstream clients regardless of the upstream card's wire format.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: extend _served_version to all PascalCase methods; add direct httpx-client isolation proof

- _served_version now checks `_PASCAL_TO_WIRE` membership instead of two
  hardcoded names, so GetTask/CancelTask/etc. are promoted to 1.0 wire format
  alongside SendMessage — prevents mixed wire formats mid-session
- test_create_a2a_client_uses_fresh_httpx_client now asserts
  a2a_client_a._litellm_httpx_client is not a2a_client_b._litellm_httpx_client
  (direct proof that header bleed cannot occur), in addition to the cache-key
  inequality check

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: id:0 silently dropped in version_convert; explicit continue in stream retry

- version_convert.py: replace `request_id or ""` with
  `str(request_id) if request_id is not None else ""` in both
  _send_result_to and _stream_result_to; id=0 is valid JSON-RPC and
  must not be coerced to "" which breaks response correlation
- main.py: add explicit `continue` after the A2ALocalhostURLError retry
  in _execute_a2a_stream_with_retry so the control flow (retry → next
  iteration → stream_succeeded guard) is unambiguous

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: preserve a2a retry and discovery card urls

* Fix black

* Fix test

* fix(a2a): avoid KeyError in discovery log after 0.3→1.0 card normalization

When a 0.3-style agent card is normalized to 1.0, the top-level url key is
replaced by supportedInterfaces; log the already-computed proxy_url instead.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(a2a): preserve taskId when lowering push notification config set params

Flatten 1.x create envelope fields before parsing into TaskPushNotificationConfig so 1.0 clients forwarding to 0.3 upstream keep taskId and config.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(a2a): ignore unknown fields in message/send proto fallback

ParseDict in _build_message_send_params now matches other inbound paths so 1.0 clients with extra proto fields are not rejected with -32602.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(a2a): normalize tasks/list params and response across protocol versions

Convert list task entries on the response path and lower ListTasksRequest params including status filters when forwarding 1.0 clients to 0.3 upstream.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(a2a): avoid reportArgumentType in _lower_list_tasks_params; use local var instead of _parse return

Co-authored-by: Cursor <cursoragent@cursor.com>

* refactor(a2a): drop private SDK symbol in tasks/list status lowering

_lower_list_tasks_params imported _CORE_TO_COMPAT_TASK_STATE, a private
a2a-sdk symbol that could disappear on a patch release and silently break
status-filter lowering. Derive the 0.3 wire string from the public
protobuf enum name instead (TASK_STATE_<NAME> maps to the 0.3 value once
the prefix is dropped and underscores become dashes) and validate the
result against the 0.3 TaskState enum's own values via a fully-typed pure
helper. Behavior is unchanged for every state; unspecified or unrecognized
states still drop the filter. Adds parametrized regression tests covering
dashed wire values (input-required, auth-required) and the unspecified drop.

* fix(a2a): drop redundant push-notification envelope key; unify MessageToDict import

_flatten_create_push_notification_params used `config or pushNotificationConfig`,
which short-circuits so a co-present pushNotificationConfig key was never popped and
leaked into the flattened params. Pop both keys unconditionally and prefer config
when present. Adds a regression test on the helper that fails on the old leak.

Also import MessageToDict from a2a.compat.v0_3.conversions in _lower_list_tasks_params
to match every other conversion helper in the module instead of pulling it straight
from google.protobuf.json_format.

* fix(a2a): reject invalid message/stream params early with -32602

_handle_stream_message built MessageSendParams lazily inside the
stream_response() generator, so malformed 1.0 params surfaced as a generic
-32603 after the 200 status line was already committed. The non-streaming
path validates up front and returns -32602 (Invalid params). Validate
eagerly before returning the StreamingResponse and emit -32602 on failure
so both paths reject malformed params identically. Adds a regression test
asserting the streamed error code is -32602.

* fix(a2a): raise clear error when non-streaming send ends on an update event

_send_message fed the SDK iterator's last event straight into
SendMessageSuccessResponse, whose result only accepts Message or Task. A
non-standard upstream whose final event is a TaskStatusUpdateEvent or
TaskArtifactUpdateEvent made the response construction raise an opaque
pydantic ValidationError. Guard the converted result and raise a clear
RuntimeError instead, consistent with the no-response guard above it.
Adds regression tests for the Message happy path and the update-event
rejection via an injected fake client.

* test(a2a): lock in clean merged agent-card URL without PROXY_BASE_URL

Regression coverage proving _build_merged_agent_card produces no double
slash in supportedInterfaces[0].url when PROXY_BASE_URL is unset and
request.base_url carries a trailing slash. get_custom_url routes through
join_paths, which rstrips the base, so the f-string join stays clean.

* style(a2a): modernize type annotations to satisfy strict ruff budget

After merging the black->ruff-format migration from base, the A2A files
owned by this PR still used Optional[X]/quoted annotations that pushed
UP037/UP045 over their lowered ceilings. Convert to X | None, drop the
now-unnecessary quoted local annotation in _send_message, and remove the
imports left unused by the rewrite. Type semantics are unchanged.

* style(a2a): type a2a_endpoints dict params as dict[str, Any]

The merge with the formatter-migration baseline tightened the
reportUnknownArgumentType ceiling; bare dict annotations made every value
Unknown and pushed the codebase total over cap. Annotate the JSON-RPC
params, body, metadata, and litellm_params dicts as dict[str, Any] so
their values are typed, dropping the unknown-argument count back under the
ceiling. No behavior change.

* fix(a2a): guard localhost retry against a missing agent card

handle_a2a_localhost_retry rewrote the card URL and called create_client
with whatever agent_card it received. The caller resolves the card from
the SDK client (Optional), so a None card reached set_agent_card_url and
create_client, surfacing an opaque SDK error instead of a clear one. Add
an early RuntimeError guard mirroring the httpx-client check, drop the now
always-true card None-check on the stash line, and cover it with a
regression test.

* style(a2a): disable reportUnknownArgumentType in a2a-sdk boundary modules

The lint env type-checks without the optional a2a-sdk/protobuf installed, so
every call into the protobuf-generated compat conversions counts as an
Unknown-typed argument and the new A2A code pushed the codebase
reportUnknownArgumentType total over its ceiling. These three modules are
the A2A SDK boundary; turn the rule off file-wide with a documented reason
instead of scattering dozens of per-line ignores across every SDK call.

* fix(a2a): tolerate unknown fields when lowering 1.0->0.3; align streaming trace id

Two issues greptile flagged:

version_convert: the 1.0->0.3 lowering paths (_send_result_to, _task_to,
_stream_result_to) called ParseDict without ignore_unknown_fields=True, so a
1.0 upstream response carrying vendor extensions raised and best-effort fell
back to passing the un-lowered 1.0 shape to a 0.3 client. Set the flag to match
the agent-card path and every inbound path; unknown fields are now dropped and
the result is correctly lowered.

main.py: asend_message_streaming derived X-LiteLLM-Trace-Id from the JSON-RPC
request id, unlike asend_message which uses the logging object's
litellm_trace_id. Prefer the logging trace id (then request id, then a uuid) so
streamed and non-streamed calls correlate under the same trace.

Adds regression tests for both, including the stream-event lowering path.

* style(a2a): apply ruff format to a2a protocol and proxy modules

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: mateo-berri <277851410+mateo-berri@users.noreply.github.com>
2026-06-29 09:32:39 +05:30
..
e2e_tests test(ui): scrub stale return-url cookie from e2e storageState (#30317) 2026-06-22 18:11:05 -07:00
public chore: litellm oss staging (#30745) 2026-06-18 13:55:35 -07:00
scripts feat(ui): track frontend lint counts in a committed snapshot (#31157) 2026-06-24 11:35:32 -07:00
src feat(a2a): support a2a-sdk 1.x proxy routing for 0.3 and 1.0 agents (#30950) 2026-06-29 09:32:39 +05:30
tests feat(ui): track frontend lint counts in a committed snapshot (#31157) 2026-06-24 11:35:32 -07:00
.env.development hidden dashboard routing, dev and build env files 2025-10-06 19:21:34 -07:00
.env.production remove next env 2026-04-18 16:45:32 -07:00
.npmrc [Fix] CI/Tooling: Correct min-release-age value in .npmrc files 2026-04-29 19:49:27 -07:00
.nvmrc [Infra] Promote internal staging to main (#27245) 2026-05-05 16:15:03 -07:00
.prettierignore feat(ui): generate dashboard API types from the proxy OpenAPI spec (#29816) 2026-06-05 17:20:01 -07:00
.prettierrc added and ran prettier autoformatter 2025-10-04 18:19:48 -07:00
build_release_ui.sh build release ui script 2026-01-17 18:16:14 -08:00
build_ui.sh chore: harden npm supply chain — pin overrides, enforce npm ci, add ignore-scripts (#24838) 2026-03-31 13:41:37 -07:00
build_ui_custom_path.sh chore: harden npm supply chain — pin overrides, enforce npm ci, add ignore-scripts (#24838) 2026-03-31 13:41:37 -07:00
CLAUDE.md feat(ui): generate dashboard API types from the proxy OpenAPI spec (#29816) 2026-06-05 17:20:01 -07:00
eslint-budgets.json ci(ui): frontend-lint job enforcing prettier + eslint on changed files (#29633) 2026-06-04 07:41:31 -07:00
eslint-metrics.json fix(router): persist global retry_policy via /config/update (#29540) 2026-06-28 00:20:20 +00:00
eslint-suppressions.json chore: remove in-product survey and Claude Code feedback nudges (#30773) 2026-06-18 15:51:30 -07:00
eslint.config.mjs feat(ui): generate dashboard API types from the proxy OpenAPI spec (#29816) 2026-06-05 17:20:01 -07:00
knip.json chore(ui): make knip recognize .mjs scripts and openapi-typescript (#30052) 2026-06-10 11:44:24 -07:00
next.config.mjs chore(admin-ui): regenerate static export with trailingSlash: true (#28112) 2026-05-25 21:06:50 -07:00
package-lock.json fix(deps): bump osv-flagged dependencies to clear known CVEs (#31122) 2026-06-23 15:50:50 -07:00
package.json feat(ui): track frontend lint counts in a committed snapshot (#31157) 2026-06-24 11:35:32 -07:00
postcss.config.js build(ui/litellm-dashboard): initial commit of litellm dashboard 2024-01-27 12:12:48 -08:00
README.md build(ui/litellm-dashboard): initial commit of litellm dashboard 2024-01-27 12:12:48 -08:00
tailwind.config.js added and ran prettier autoformatter 2025-10-04 18:19:48 -07:00
tailwind.config.ts added and ran prettier autoformatter 2025-10-04 18:19:48 -07:00
tsconfig.json style(ui): run prettier --write across the dashboard (#29622) 2026-06-04 11:37:54 -07:00
tsconfig.tsbuildinfo feat(ui): migrate projects and access-groups to path routes (#30226) 2026-06-11 13:20:21 -07:00
ui_colors.json added and ran prettier autoformatter 2025-10-04 18:19:48 -07:00
vitest.config.ts [Test] UI - Unit tests: raise global vitest timeout and remove per-test overrides 2026-04-09 23:58:35 -07:00

This is a Next.js project bootstrapped with create-next-app.

Getting Started

First, run the development server:

npm run dev
# or
yarn dev
# or
pnpm dev
# or
bun dev

Open http://localhost:3000 with your browser to see the result.

You can start editing the page by modifying app/page.tsx. The page auto-updates as you edit the file.

This project uses next/font to automatically optimize and load Inter, a custom Google Font.

Learn More

To learn more about Next.js, take a look at the following resources:

You can check out the Next.js GitHub repository - your feedback and contributions are welcome!

Deploy on Vercel

The easiest way to deploy your Next.js app is to use the Vercel Platform from the creators of Next.js.

Check out our Next.js deployment documentation for more details.