UNPKG

assistant-stream

Version:

Streaming utilities for AI assistants

171 lines (156 loc) • 6.02 kB
import { describe, expect, it } from "vitest"; import { RESUMABLE_STREAM_ID_HEADER, createResumableAssistantStreamResponse, createResumeAssistantStreamResponse, } from "../createResumableAssistantStreamResponse"; import { createResumableStreamContext } from "../ResumableStreamContext"; import { createInMemoryResumableStreamStore } from "../stores/InMemoryResumableStreamStore"; import { AssistantTransportDecoder, AssistantTransportEncoder, } from "../../core/serialization/assistant-transport/AssistantTransport"; import { DataStreamDecoder } from "../../core/serialization/data-stream/DataStream"; import { AssistantMessageAccumulator } from "../../core/accumulators/assistant-message-accumulator"; import type { AssistantStreamChunk } from "../../core/AssistantStreamChunk"; import type { AssistantMessage } from "../../core/utils/types"; async function decodeViaDataStream(body: ReadableStream<Uint8Array>) { const stream = body.pipeThrough(new DataStreamDecoder()); const out: string[] = []; for await (const chunk of stream) { if (chunk.type === "text-delta") out.push(chunk.textDelta); } return out.join(""); } async function accumulate( body: ReadableStream<Uint8Array>, decoder: TransformStream<Uint8Array, AssistantStreamChunk>, ): Promise<AssistantMessage> { const messages = body .pipeThrough(decoder) .pipeThrough(new AssistantMessageAccumulator()); let last: AssistantMessage | undefined; for await (const msg of messages) last = msg; if (!last) throw new Error("accumulator yielded no messages"); return last; } describe("resumable integration", () => { it("ten concurrent consumers see identical bytes for a 50 chunk stream", async () => { const ctx = createResumableStreamContext({ store: createInMemoryResumableStreamStore(), }); const producer = await createResumableAssistantStreamResponse({ context: ctx, streamId: "fanout", callback: async (controller) => { for (let i = 0; i < 50; i++) { controller.appendText(`chunk-${i}`); await Promise.resolve(); } }, }); const consumers = await Promise.all( Array.from({ length: 10 }, async () => { const r = await ctx.resume("fanout"); if (!r) throw new Error("resume returned null"); return new Response(r).arrayBuffer(); }), ); const producerBytes = await new Response(producer.body).arrayBuffer(); const expected = new Uint8Array(producerBytes); for (const c of consumers) { expect(new Uint8Array(c)).toEqual(expected); } const text = await decodeViaDataStream( new Response(new Uint8Array(producerBytes)).body!, ); expect(text).toBe( Array.from({ length: 50 }, (_, i) => `chunk-${i}`).join(""), ); }); it("non-ASCII payload round-trips byte-for-byte through resume", async () => { const allCodePoints = Array.from({ length: 256 }, (_, i) => String.fromCodePoint(i), ).join(""); const emoji = "hello 🌍 🚀 ✨"; const payload = allCodePoints + emoji; const ctx = createResumableStreamContext({ store: createInMemoryResumableStreamStore(), }); const producer = await createResumableAssistantStreamResponse({ context: ctx, streamId: "binary", callback: (controller) => controller.appendText(payload), }); const producerBytes = new Uint8Array( await new Response(producer.body).arrayBuffer(), ); const replay = await createResumeAssistantStreamResponse({ context: ctx, streamId: "binary", }); const replayBytes = new Uint8Array( await new Response(replay.body).arrayBuffer(), ); expect(replayBytes).toEqual(producerBytes); expect(replay.headers.get(RESUMABLE_STREAM_ID_HEADER)).toBe("binary"); const decoded = await decodeViaDataStream(new Response(replayBytes).body!); expect(decoded).toBe(payload); }); it("AssistantTransportEncoder round-trips through resume into an identical message", async () => { const ctx = createResumableStreamContext({ store: createInMemoryResumableStreamStore(), }); const producer = await createResumableAssistantStreamResponse({ context: ctx, streamId: "transport", encoder: () => new AssistantTransportEncoder(), callback: (controller) => { controller.appendText("processing... "); const tool = controller.addToolCallPart({ toolName: "lookup", toolCallId: "tool-1", args: { query: "weather" }, response: { result: { temperature: 72 } }, }); tool.close(); controller.appendText("done"); }, }); const producerMessage = await accumulate( producer.body!, new AssistantTransportDecoder(), ); const replay = await createResumeAssistantStreamResponse({ context: ctx, streamId: "transport", encoder: () => new AssistantTransportEncoder(), }); expect(replay.headers.get("content-type")).toBe("text/event-stream"); const replayMessage = await accumulate( replay.body!, new AssistantTransportDecoder(), ); // Tool-call timing is measured by the consuming accumulator, so a replay // re-measures it; compare parts modulo timing. const withoutTiming = (parts: typeof replayMessage.parts) => parts.map((part) => part.type === "tool-call" ? (({ timing: _timing, ...rest }) => rest)(part) : part, ); expect(withoutTiming(replayMessage.parts)).toEqual( withoutTiming(producerMessage.parts), ); expect(replayMessage.status).toEqual(producerMessage.status); const toolPart = replayMessage.parts.find((p) => p.type === "tool-call"); expect(toolPart).toBeDefined(); expect(toolPart).toMatchObject({ toolName: "lookup", toolCallId: "tool-1", args: { query: "weather" }, result: { temperature: 72 }, timing: { startedAt: expect.any(Number) }, }); }); });