mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-03 02:22:24 +00:00
fix(ui): parse current interaction stream events in playground
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
parent
07d593d831
commit
d09ccc4b0e
2 changed files with 79 additions and 7 deletions
|
|
@ -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<Uint8Array>({
|
||||
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");
|
||||
});
|
||||
});
|
||||
|
|
@ -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<string, unknown> | 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<string, unknown> | 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) {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue