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:
mateo 2026-09-17 23:45:04 +00:00
parent 07d593d831
commit d09ccc4b0e
2 changed files with 79 additions and 7 deletions

View file

@ -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");
});
});

View file

@ -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) {