From 712c15ee6899bc0ad0fd1efc7617075a77cce9e7 Mon Sep 17 00:00:00 2001 From: Ishaan Jaffer Date: Tue, 10 Mar 2026 17:12:43 -0700 Subject: [PATCH] fix(chat-ui): fix MCPEvent layering, batch localStorage writes, module-level test imports - Move MCPEvent interface definition into chat/types.ts (single source of truth) - MCPEventsDisplay.tsx now imports MCPEvent from types.ts instead of defining it locally - Batch MCP event localStorage writes: accumulate during stream, persist once in finally - Move test imports to module level per PEP 8 convention --- .../test_chat_ui_responses_session.py | 18 +++--------- .../src/components/chat/ChatPage.tsx | 9 ++++-- .../src/components/chat/types.ts | 29 +++++++++++++++++-- .../playground/chat_ui/MCPEventsDisplay.tsx | 29 ++----------------- 4 files changed, 39 insertions(+), 46 deletions(-) diff --git a/tests/test_litellm/test_chat_ui_responses_session.py b/tests/test_litellm/test_chat_ui_responses_session.py index 2ecd7050e21..6ff1f06fa7d 100644 --- a/tests/test_litellm/test_chat_ui_responses_session.py +++ b/tests/test_litellm/test_chat_ui_responses_session.py @@ -6,11 +6,15 @@ Verifies that: 2. Absence of previous_response_id does not break the call 3. The aresponses function signature exposes the expected parameters """ +import inspect +import json import os import sys +import unittest.mock as mock sys.path.insert(0, os.path.abspath("../..")) +import httpx import pytest import litellm @@ -21,8 +25,6 @@ class TestResponsesSessionChaining: def test_responses_api_signature_accepts_previous_response_id(self): """aresponses must accept previous_response_id and onResponseId-like params.""" - import inspect - sig = inspect.signature(litellm.aresponses) assert "previous_response_id" in sig.parameters, ( "aresponses must accept previous_response_id for multi-turn session chaining" @@ -33,13 +35,9 @@ class TestResponsesSessionChaining: @pytest.mark.asyncio async def test_previous_response_id_included_in_request_body(self): """previous_response_id must appear in the outgoing HTTP request body.""" - import httpx - captured_body: dict = {} async def mock_send(self_transport, request: httpx.Request, **kwargs): - import json - try: captured_body.update(json.loads(request.content)) except Exception: @@ -68,8 +66,6 @@ class TestResponsesSessionChaining: request=request, ) - import unittest.mock as mock - with mock.patch("httpx.AsyncClient.send", mock_send): try: await litellm.aresponses( @@ -88,13 +84,9 @@ class TestResponsesSessionChaining: @pytest.mark.asyncio async def test_no_previous_response_id_omitted_from_request(self): """When previous_response_id is None, it must not appear in the request body.""" - import httpx - captured_body: dict = {} async def mock_send(self_transport, request: httpx.Request, **kwargs): - import json - try: captured_body.update(json.loads(request.content)) except Exception: @@ -118,8 +110,6 @@ class TestResponsesSessionChaining: } return httpx.Response(200, json=response_json, request=request) - import unittest.mock as mock - with mock.patch("httpx.AsyncClient.send", mock_send): try: await litellm.aresponses( diff --git a/ui/litellm-dashboard/src/components/chat/ChatPage.tsx b/ui/litellm-dashboard/src/components/chat/ChatPage.tsx index 66d8fdf0c31..f9555fd9c8e 100644 --- a/ui/litellm-dashboard/src/components/chat/ChatPage.tsx +++ b/ui/litellm-dashboard/src/components/chat/ChatPage.tsx @@ -304,10 +304,9 @@ const ChatPage: React.FC = ({ accessToken, userRole, userId, user previousResponseId, (id: string) => setResponsesSessionId(id), (event: MCPEvent) => { + // Accumulate locally only — persisted once in finally to avoid + // one full localStorage write per MCP event during streaming. accumulatedMCPEvents.push(event); - // Persist a snapshot to the assistant message so events survive - // across turns instead of disappearing when the next send starts. - updateLastAssistantMessage(convId!, { mcpEvents: [...accumulatedMCPEvents] }); }, ); } catch (err: unknown) { @@ -321,6 +320,10 @@ const ChatPage: React.FC = ({ accessToken, userRole, userId, user }); } } finally { + // Persist MCP events once after the turn ends (single localStorage write). + if (accumulatedMCPEvents.length > 0) { + updateLastAssistantMessage(convId!, { mcpEvents: accumulatedMCPEvents }); + } setIsStreaming(false); abortControllerRef.current = null; } diff --git a/ui/litellm-dashboard/src/components/chat/types.ts b/ui/litellm-dashboard/src/components/chat/types.ts index 9542b31db14..5a05d1221dd 100644 --- a/ui/litellm-dashboard/src/components/chat/types.ts +++ b/ui/litellm-dashboard/src/components/chat/types.ts @@ -1,6 +1,29 @@ -import type { MCPEvent } from "../playground/chat_ui/MCPEventsDisplay"; - -export type { MCPEvent }; +/** Represents a single MCP tool event emitted during an assistant turn. */ +export interface MCPEvent { + type: string; + sequence_number?: number; + output_index?: number; + item_id?: string; + item?: { + id?: string; + type?: string; + server_label?: string; + tools?: Array<{ + name: string; + description: string; + annotations?: { + read_only?: boolean; + }; + input_schema?: unknown; + }>; + name?: string; + arguments?: string; + output?: string; + }; + delta?: string; + arguments?: string; + timestamp?: number; +} export interface ChatMessage { id: string; diff --git a/ui/litellm-dashboard/src/components/playground/chat_ui/MCPEventsDisplay.tsx b/ui/litellm-dashboard/src/components/playground/chat_ui/MCPEventsDisplay.tsx index b3ec75f01dc..feeacfdb79f 100644 --- a/ui/litellm-dashboard/src/components/playground/chat_ui/MCPEventsDisplay.tsx +++ b/ui/litellm-dashboard/src/components/playground/chat_ui/MCPEventsDisplay.tsx @@ -1,35 +1,12 @@ import React from "react"; import { Typography, Collapse } from "antd"; +import type { MCPEvent } from "../../chat/types"; + +export type { MCPEvent }; const { Text } = Typography; const { Panel } = Collapse; -export interface MCPEvent { - type: string; - sequence_number?: number; - output_index?: number; - item_id?: string; - item?: { - id?: string; - type?: string; - server_label?: string; - tools?: Array<{ - name: string; - description: string; - annotations?: { - read_only?: boolean; - }; - input_schema?: any; - }>; - name?: string; - arguments?: string; - output?: string; - }; - delta?: string; - arguments?: string; - timestamp?: number; -} - interface MCPEventsDisplayProps { events: MCPEvent[]; className?: string;