From d09ccc4b0ed689fd2299f10267d34ecf49f4e88d Mon Sep 17 00:00:00 2001 From: mateo Date: Thu, 17 Sep 2026 23:45:04 +0000 Subject: [PATCH] fix(ui): parse current interaction stream events in playground Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../llm_calls/interactions_api.test.tsx | 77 +++++++++++++++++++ .../playground/llm_calls/interactions_api.tsx | 9 +-- 2 files changed, 79 insertions(+), 7 deletions(-) create mode 100644 ui/litellm-dashboard/src/app/(dashboard)/playground/llm_calls/interactions_api.test.tsx diff --git a/ui/litellm-dashboard/src/app/(dashboard)/playground/llm_calls/interactions_api.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/playground/llm_calls/interactions_api.test.tsx new file mode 100644 index 00000000000..b037763c79b --- /dev/null +++ b/ui/litellm-dashboard/src/app/(dashboard)/playground/llm_calls/interactions_api.test.tsx @@ -0,0 +1,77 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { makeInteractionsRequest } from "./interactions_api"; + +vi.mock("@/components/networking", () => ({ + getProxyBaseUrl: vi.fn(() => "https://example.com"), + getGlobalLitellmHeaderName: vi.fn(() => "Authorization"), +})); + +vi.mock("@/lib/toast", () => ({ + toast: { fromError: vi.fn() }, +})); + +const sseBody = (text: string) => + new ReadableStream({ + start(controller) { + controller.enqueue(new TextEncoder().encode(text)); + controller.close(); + }, + }); + +describe("interactions_api", () => { + const mockUpdateUI = vi.fn(); + const mockFetch = vi.fn(); + + beforeEach(() => { + // @ts-ignore - assigning to global for test environment + global.fetch = mockFetch; + }); + + afterEach(() => { + vi.clearAllMocks(); + }); + + it("parses native Gemini step events and captures the nested interaction model", async () => { + const sse = [ + 'data: {"event_type":"interaction.created","interaction":{"model":"gemini-3-flash"}}', + 'data: {"event_type":"step.start","index":0,"step":{"type":"text"}}', + 'data: {"event_type":"step.delta","index":0,"delta":{"type":"text","text":"Hello"}}', + 'data: {"event_type":"step.delta","index":0,"delta":{"type":"text","text":" world"}}', + 'data: {"event_type":"step.stop","index":0}', + 'data: {"event_type":"interaction.completed","interaction":{"model":"gemini-3-flash"}}', + "data: [DONE]", + "", + "", + ].join("\n\n"); + + mockFetch.mockResolvedValue({ ok: true, body: sseBody(sse) } as Response); + + await makeInteractionsRequest("hi", mockUpdateUI, "gemini-flash", "token"); + + expect(mockFetch).toHaveBeenCalledTimes(1); + expect(mockFetch.mock.calls[0][0]).toBe("https://example.com/v1beta/interactions"); + expect(mockUpdateUI).toHaveBeenCalledTimes(2); + expect(mockUpdateUI).toHaveBeenNthCalledWith(1, "Hello", "gemini-3-flash"); + expect(mockUpdateUI).toHaveBeenNthCalledWith(2, " world", "gemini-3-flash"); + }); + + it("parses bridge step events and captures the top-level model", async () => { + const sse = [ + 'data: {"event_type":"interaction.created","id":"x","model":"gpt-5.4","status":"in_progress"}', + 'data: {"event_type":"step.delta","index":0,"delta":{"type":"text","text":"Hi"}}', + 'data: {"event_type":"interaction.completed","id":"x","model":"gpt-5.4"}', + "data: [DONE]", + "", + "", + ].join("\n\n"); + + mockFetch.mockResolvedValue({ ok: true, body: sseBody(sse) } as Response); + + await makeInteractionsRequest("hi", mockUpdateUI, "gpt-mini", "token"); + + expect(mockFetch).toHaveBeenCalledTimes(1); + expect(mockFetch.mock.calls[0][0]).toBe("https://example.com/v1beta/interactions"); + expect(mockUpdateUI).toHaveBeenCalledTimes(1); + expect(mockUpdateUI).toHaveBeenCalledWith("Hi", "gpt-5.4"); + }); +}); diff --git a/ui/litellm-dashboard/src/app/(dashboard)/playground/llm_calls/interactions_api.tsx b/ui/litellm-dashboard/src/app/(dashboard)/playground/llm_calls/interactions_api.tsx index ab1bb7c1707..be4b09ae33b 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/playground/llm_calls/interactions_api.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/playground/llm_calls/interactions_api.tsx @@ -94,24 +94,19 @@ export async function makeInteractionsRequest( const eventType = event.event_type as string | undefined; - if (eventType === "interaction.start" || eventType === "interaction.complete") { - // Capture model from either the native Gemini shape (nested under - // `interaction`) or the bridge shape (top-level `model` field). + if (eventType === "interaction.created" || eventType === "interaction.completed") { const interaction = event.interaction as Record | undefined; if (typeof interaction?.model === "string" && interaction.model) { responseModel = interaction.model; } else if (typeof event.model === "string" && event.model) { responseModel = event.model; } - } else if (eventType === "content.delta" || eventType === "content.start") { + } else if (eventType === "step.delta") { const delta = event.delta as Record | undefined; - // Accept both native Gemini format {"type":"text","text":"..."} and bridge - // format {"text":"..."} (no type discriminator) if (typeof delta?.text === "string" && delta.text) { updateUI(delta.text, responseModel ?? selectedModel); } } - // content.start, content.stop, interaction.status_update — no UI action needed } } } catch (error: unknown) {