UNPKG

assistant-stream

Version:

Streaming utilities for AI assistants

1,712 lines (1,591 loc) • 44.7 kB
import { describe, expect, it } from "vitest"; import { toGenericMessages, type GenericFilePart } from "./toGenericMessages"; import type { ToolModelContentPart } from "../tool/tool-types"; describe("toGenericMessages", () => { describe("tool model content", () => { const modelContent: readonly ToolModelContentPart[] = [ { type: "text", text: "Report contents" }, { type: "file", data: "JVBERi0xLjQ=", mediaType: "application/pdf", filename: "report.pdf", }, ]; it.each([{ content: modelContent }, { content: [] }])( "preserves explicit content on a settled result: %j", ({ content }) => { const result = toGenericMessages([ { role: "assistant", content: [ { type: "tool-call", toolCallId: "call-1", toolName: "report", result: { documentId: "report-1" }, modelContent: content, }, ], }, ]); expect(result.at(-1)).toEqual({ role: "tool", content: [ { type: "tool-result", toolCallId: "call-1", toolName: "report", result: { documentId: "report-1" }, modelContent: content, }, ], }); }, ); it.each([{}, { result: "preview", isPreliminary: true }])( "does not forward content from an unfinished result: %j", (extra) => { const result = toGenericMessages([ { role: "assistant", content: [ { type: "tool-call", toolCallId: "call-1", toolName: "report", modelContent, ...extra, }, ], }, ]); expect(result.at(-1)).toEqual({ role: "tool", content: [ { type: "tool-result", toolCallId: "call-1", toolName: "report", result: { error: "Tool call was not completed" }, isError: true, }, ], }); }, ); }); describe("system messages", () => { it("converts text content", () => { const result = toGenericMessages([ { role: "system", content: [{ type: "text", text: "You are a helpful assistant." }], }, ]); expect(result).toEqual([ { role: "system", content: "You are a helpful assistant." }, ]); }); it("handles empty text", () => { const result = toGenericMessages([ { role: "system", content: [{ type: "text", text: "" }], }, ]); expect(result).toEqual([]); }); it("handles missing text property", () => { const result = toGenericMessages([ { role: "system", content: [{ type: "text" }], }, ]); expect(result).toEqual([]); }); it("uses first text part when multiple exist", () => { const result = toGenericMessages([ { role: "system", content: [ { type: "text", text: "First" }, { type: "text", text: "Second" }, ], }, ]); expect(result).toEqual([{ role: "system", content: "First" }]); }); it("skips a null part when finding the text", () => { const result = toGenericMessages([ { role: "system", content: [null, { type: "text", text: "Be brief." }], }, ] as never); expect(result).toEqual([{ role: "system", content: "Be brief." }]); }); }); describe("user messages", () => { describe.each(["mp3", "wav"] as const)("legacy %s audio", (format) => { it.each([false, true])( "preserves audio bytes (attachment: %s)", (attachment) => { const part = { type: "audio", audio: { data: "UklGRg==", format } }; const message = { role: "user" as const, content: attachment ? [] : [part], attachments: attachment ? [{ content: [part] }] : [], }; expect(toGenericMessages([message])).toEqual([ { role: "user", content: [ { type: "file", data: "UklGRg==", mediaType: `audio/${format}`, }, ], }, ]); }, ); it.each([ "https://example.com/recording", "data:audio/FORMAT;base64,UklGRg==", ])("preserves an audio URL: %s", (value) => { const data = value.replace("FORMAT", format); expect( toGenericMessages([ { role: "user", content: [{ type: "audio", audio: { data, format } }], }, ]), ).toEqual([ { role: "user", content: [ { type: "file", data: new URL(data), mediaType: `audio/${format}`, }, ], }, ]); }); it.each([ { name: "a mismatched data URL", input: "data:audio/mpeg;base64,QUJD", expected: `data:audio/${format};base64,QUJD`, isUrl: true, }, { name: "a matching data URL", input: `data:audio/${format};base64,QUJD`, expected: `data:audio/${format};base64,QUJD`, isUrl: true, }, { name: "raw base64", input: "QUJD", expected: "QUJD", isUrl: false, }, { name: "an HTTP URL", input: "http://example.com/recording", expected: "http://example.com/recording", isUrl: true, }, { name: "an HTTPS URL", input: "https://example.com/recording", expected: "https://example.com/recording", isUrl: true, }, ])("uses the typed format for $name", ({ input, expected, isUrl }) => { expect( toGenericMessages([ { role: "user", content: [{ type: "audio", audio: { data: input, format } }], }, ]), ).toEqual([ { role: "user", content: [ { type: "file", data: isUrl ? new URL(expected) : expected, mediaType: `audio/${format}`, }, ], }, ]); }); }); it.each([undefined, {}, { data: "UklGRg==" }, { format: "wav" }])( "skips an audio part missing its payload or format: %j", (audio) => { expect( toGenericMessages([ { role: "user", content: [ { type: "text", text: "Keep this" }, { type: "audio", audio }, ], }, ] as never), ).toEqual([ { role: "user", content: [{ type: "text", text: "Keep this" }] }, ]); }, ); it("converts text parts", () => { const result = toGenericMessages([ { role: "user", content: [{ type: "text", text: "Hello!" }], }, ]); expect(result).toEqual([ { role: "user", content: [{ type: "text", text: "Hello!" }] }, ]); }); it("converts image parts with media type inference", () => { const result = toGenericMessages([ { role: "user", content: [{ type: "image", image: "https://example.com/photo.jpg" }], }, ]); expect(result).toEqual([ { role: "user", content: [ { type: "file", data: new URL("https://example.com/photo.jpg"), mediaType: "image/jpeg", }, ], }, ]); }); it("converts file parts", () => { const result = toGenericMessages([ { role: "user", content: [ { type: "file", data: "https://example.com/doc.pdf", mimeType: "application/pdf", }, ], }, ]); expect(result).toEqual([ { role: "user", content: [ { type: "file", data: new URL("https://example.com/doc.pdf"), mediaType: "application/pdf", }, ], }, ]); }); it("preserves zero-byte file parts", () => { const result = toGenericMessages([ { role: "user", content: [ { type: "file", data: "", mimeType: "text/plain", filename: "empty.txt", }, ], }, ]); expect(result).toEqual([ { role: "user", content: [ { type: "file", data: "", mediaType: "text/plain", filename: "empty.txt", }, ], }, ]); }); it.each([ { label: "empty", mimeType: "" }, { label: "missing", mimeType: undefined }, ])("falls back when a file MIME type is $label", ({ mimeType }) => { const result = toGenericMessages([ { role: "user", content: [ { type: "file", data: "https://cdn.example.com/untyped-file", ...(mimeType !== undefined && { mimeType }), filename: "untyped-file", }, ], }, ]); expect(result).toEqual([ { role: "user", content: [ { type: "file", data: new URL("https://cdn.example.com/untyped-file"), mediaType: "application/octet-stream", filename: "untyped-file", }, ], }, ]); }); it("uses the data URL MIME type when the file MIME type is empty", () => { const data = "data:text/plain;base64,"; const result = toGenericMessages([ { role: "user", content: [{ type: "file", data, mimeType: "" }], }, ]); expect(result).toEqual([ { role: "user", content: [ { type: "file", data: new URL(data), mediaType: "text/plain", }, ], }, ]); }); it("does not infer a MIME type from a malformed data URL", () => { const data = "data:text/plain"; const result = toGenericMessages([ { role: "user", content: [{ type: "file", data, mimeType: "" }], }, ]); expect(result).toEqual([ { role: "user", content: [ { type: "file", data: new URL(data), mediaType: "application/octet-stream", }, ], }, ]); }); it("handles attachments", () => { const result = toGenericMessages([ { role: "user", content: [{ type: "text", text: "See attached" }], attachments: [ { content: [ { type: "file", data: "https://example.com/file.txt", mimeType: "text/plain", }, ], }, ], }, ]); expect(result).toEqual([ { role: "user", content: [ { type: "text", text: "See attached" }, { type: "file", data: new URL("https://example.com/file.txt"), mediaType: "text/plain", }, ], }, ]); }); it("uses an image attachment's media type for extensionless URLs", () => { const result = toGenericMessages([ { role: "user", content: [], attachments: [ { contentType: "image/jpeg", content: [ { type: "image", image: "https://cdn.example.com/image/123" }, ], }, ], }, ]); expect(result).toEqual([ { role: "user", content: [ { type: "file", data: new URL("https://cdn.example.com/image/123"), mediaType: "image/jpeg", }, ], }, ]); }); it("keeps attachment parts when the message has no content", () => { const result = toGenericMessages([ { role: "user", attachments: [{ content: [{ type: "text", text: "notes" }] }], }, ] as never); expect(result).toEqual([ { role: "user", content: [{ type: "text", text: "notes" }] }, ]); }); it("skips attachments without content and null attachments", () => { const result = toGenericMessages([ { role: "user", content: [{ type: "text", text: "See attached" }], attachments: [ { id: "uploading" }, null, { content: [{ type: "text", text: "notes" }] }, ], }, ] as never); expect(result).toEqual([ { role: "user", content: [ { type: "text", text: "See attached" }, { type: "text", text: "notes" }, ], }, ]); }); it("lowercases a mixed-case attachment media type", () => { const result = toGenericMessages([ { role: "user", content: [], attachments: [ { contentType: "Image/JPEG", content: [ { type: "image", image: "https://cdn.example.com/image/123" }, ], }, ], }, ]); expect(result[0]?.content[0]).toMatchObject({ mediaType: "image/jpeg" }); }); it("falls back past a wildcard attachment media type", () => { const result = toGenericMessages([ { role: "user", content: [], attachments: [ { contentType: "image/*", content: [ { type: "image", image: "https://cdn.example.com/photo.webp" }, ], }, ], }, ]); expect(result[0]?.content[0]).toMatchObject({ mediaType: "image/webp" }); }); it("carries a file part filename through", () => { const result = toGenericMessages([ { role: "user", content: [ { type: "file", data: "https://cdn.example.com/a.pdf", mimeType: "application/pdf", filename: "invoice.pdf", }, ], }, ] as never); expect(result).toEqual([ { role: "user", content: [ { type: "file", data: new URL("https://cdn.example.com/a.pdf"), mediaType: "application/pdf", filename: "invoice.pdf", }, ], }, ]); }); it("omits filename when the part has none", () => { const result = toGenericMessages([ { role: "user", content: [ { type: "file", data: "https://cdn.example.com/a.pdf", mimeType: "application/pdf", }, ], }, ] as never); const part = (result[0] as { content: unknown[] }).content[0]; expect(part).not.toHaveProperty("filename"); }); it("filters invalid parts", () => { const result = toGenericMessages([ { role: "user", content: [ { type: "text", text: "Valid" }, { type: "image" }, // missing image property { type: "file", data: null } as never, { type: "unknown" }, ], }, ]); expect(result).toEqual([ { role: "user", content: [{ type: "text", text: "Valid" }] }, ]); }); it("skips null parts in the message and its attachments", () => { const result = toGenericMessages([ { role: "user", content: [null, { type: "text", text: "Hi" }], attachments: [{ content: [null, { type: "text", text: "notes" }] }], }, ] as never); expect(result).toEqual([ { role: "user", content: [ { type: "text", text: "Hi" }, { type: "text", text: "notes" }, ], }, ]); }); it("handles data URL as URL object", () => { const dataUrl = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg=="; const result = toGenericMessages([ { role: "user", content: [{ type: "image", image: dataUrl }], }, ]); expect(result).toHaveLength(1); expect(result[0]!.role).toBe("user"); const content = (result[0] as { content: unknown[] }).content; expect(content[0]).toMatchObject({ type: "file", mediaType: "image/png", }); expect((content[0] as { data: unknown }).data).toBeInstanceOf(URL); }); it("infers a lowercase media type from an uppercase-scheme data URL", () => { const dataUrl = "DATA:IMAGE/PNG;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg=="; const result = toGenericMessages([ { role: "user", content: [{ type: "image", image: dataUrl }], }, ]); const content = (result[0] as { content: unknown[] }).content; expect(content[0]).toMatchObject({ type: "file", mediaType: "image/png", }); }); it("handles relative/invalid URL as string", () => { const result = toGenericMessages([ { role: "user", content: [ { type: "file", data: "/relative/path/file.txt", mimeType: "text/plain", }, ], }, ]); expect(result).toEqual([ { role: "user", content: [ { type: "file", data: "/relative/path/file.txt", mediaType: "text/plain", }, ], }, ]); }); it("produces empty result when all parts are invalid", () => { const result = toGenericMessages([ { role: "user", content: [{ type: "unknown" }], }, ]); expect(result).toEqual([]); }); }); describe("assistant messages", () => { it("converts text parts", () => { const result = toGenericMessages([ { role: "assistant", content: [{ type: "text", text: "Hello!" }], }, ]); expect(result).toEqual([ { role: "assistant", content: [{ type: "text", text: "Hello!" }] }, ]); }); it("converts generated file parts", () => { const result = toGenericMessages([ { role: "assistant", content: [ { type: "file", data: "iVBORw0KGgo=", mimeType: "image/png", filename: "generated.png", }, ], }, ]); expect(result).toEqual([ { role: "assistant", content: [ { type: "file", data: "iVBORw0KGgo=", mediaType: "image/png", filename: "generated.png", }, ], }, ]); }); it("converts generated image parts", () => { const dataUrl = "data:image/webp;base64,UklGRg=="; const result = toGenericMessages([ { role: "assistant", content: [ { type: "image", image: dataUrl, filename: "generated.webp" }, ], }, ]); expect(result).toHaveLength(1); expect(result[0]).toMatchObject({ role: "assistant", content: [ { type: "file", mediaType: "image/webp", filename: "generated.webp", }, ], }); const content = (result[0] as { content: GenericFilePart[] }).content; expect(content[0]?.data).toBeInstanceOf(URL); expect(String(content[0]?.data)).toBe(dataUrl); }); it("places generated files after settled tool results", () => { const result = toGenericMessages([ { role: "assistant", content: [ { type: "tool-call", toolCallId: "call_123", toolName: "create_image", args: {}, result: { created: true }, }, { type: "file", data: "iVBORw0KGgo=", mimeType: "image/png", filename: "generated.png", }, ], }, ]); expect(result.map((message) => message.role)).toEqual([ "assistant", "tool", "assistant", ]); expect(result[2]).toEqual({ role: "assistant", content: [ { type: "file", data: "iVBORw0KGgo=", mediaType: "image/png", filename: "generated.png", }, ], }); }); it("skips null parts", () => { const result = toGenericMessages([ { role: "assistant", content: [null, { type: "text", text: "Hello!" }, null], }, ] as never); expect(result).toEqual([ { role: "assistant", content: [{ type: "text", text: "Hello!" }] }, ]); }); it("closes out tool calls without results", () => { const result = toGenericMessages([ { role: "assistant", content: [ { type: "tool-call", toolCallId: "call_123", toolName: "get_weather", args: { city: "London" }, }, ], }, ]); expect(result).toEqual([ { role: "assistant", content: [ { type: "tool-call", toolCallId: "call_123", toolName: "get_weather", args: { city: "London" }, }, ], }, { role: "tool", content: [ { type: "tool-result", toolCallId: "call_123", toolName: "get_weather", result: { error: "Tool call was not completed" }, isError: true, }, ], }, ]); }); it("does not send a preliminary result to the model", () => { const result = toGenericMessages([ { role: "assistant", content: [ { type: "tool-call", toolCallId: "call_123", toolName: "get_weather", args: { city: "London" }, state: "call", result: { temperature: 20 }, isPreliminary: true, }, ], }, ]); expect(result.at(-1)).toEqual({ role: "tool", content: [ { type: "tool-result", toolCallId: "call_123", toolName: "get_weather", result: { error: "Tool call was not completed" }, isError: true, }, ], }); }); it.each([ ["a pending approval", { approval: { id: "ap_1" } }], [ "an approved call the host has not run yet", { approval: { id: "ap_1", approved: true }, }, ], ["an interrupted call", { interrupt: { type: "human", payload: {} } }], ])("leaves %s untouched instead of failing it", (_label, extra) => { const result = toGenericMessages([ { role: "assistant", content: [ { type: "tool-call", toolCallId: "call_123", toolName: "get_weather", args: { city: "London" }, ...extra, }, ], }, ]); expect(result).toEqual([ { role: "assistant", content: [ { type: "tool-call", toolCallId: "call_123", toolName: "get_weather", args: { city: "London" }, }, ], }, ]); }); it.each(["running", "requires-action"])( "leaves a pending approval untouched while its message is %s", (type) => { const result = toGenericMessages([ { role: "assistant", status: { type }, content: [ { type: "tool-call", toolCallId: "call_123", toolName: "get_weather", args: { city: "London" }, approval: { id: "ap_1" }, }, ], }, ]); expect(result.map((message) => message.role)).toEqual(["assistant"]); }, ); const awaitingHost = [ ["a pending approval", { approval: { id: "ap_1" } }], [ "an approved call the host never ran", { approval: { id: "ap_1", approved: true } }, ], ["an interrupted call", { interrupt: { type: "human", payload: {} } }], ] as const; const closedOut = [ { role: "assistant", content: [ { type: "tool-call", toolCallId: "call_123", toolName: "get_weather", args: { city: "London" }, }, ], }, { role: "tool", content: [ { type: "tool-result", toolCallId: "call_123", toolName: "get_weather", result: { error: "Tool call was not completed" }, isError: true, }, ], }, ]; it.each(awaitingHost)( "closes out %s once its message has settled", (_label, extra) => { const result = toGenericMessages([ { role: "assistant", status: { type: "incomplete" }, content: [ { type: "tool-call", toolCallId: "call_123", toolName: "get_weather", args: { city: "London" }, ...extra, }, ], }, ]); expect(result).toEqual(closedOut); }, ); it.each(awaitingHost)( "closes out %s in a message a later message follows", (_label, extra) => { const result = toGenericMessages([ { role: "assistant", content: [ { type: "tool-call", toolCallId: "call_123", toolName: "get_weather", args: { city: "London" }, ...extra, }, ], }, { role: "user", content: [{ type: "text", text: "never mind" }] }, ]); expect(result).toEqual([ ...closedOut, { role: "user", content: [{ type: "text", text: "never mind" }] }, ]); }, ); it.each([ ["a cancelled approval", { id: "ap_1", resolution: "cancelled" }], ["a denied approval carrying no result", { id: "ap_1", approved: false }], ])("closes out %s", (_label, approval) => { const result = toGenericMessages([ { role: "assistant", content: [ { type: "tool-call", toolCallId: "call_123", toolName: "get_weather", args: { city: "London" }, approval, }, ], }, ]); expect(result).toEqual([ { role: "assistant", content: [ { type: "tool-call", toolCallId: "call_123", toolName: "get_weather", args: { city: "London" }, }, ], }, { role: "tool", content: [ { type: "tool-result", toolCallId: "call_123", toolName: "get_weather", result: { error: "Tool call was not completed" }, isError: true, }, ], }, ]); }); it("treats a settled tool call carrying no result as completed", () => { const result = toGenericMessages([ { role: "assistant", content: [ { type: "tool-call", toolCallId: "call_123", toolName: "void_tool", args: {}, state: "result", }, ], }, ]); expect(result).toEqual([ { role: "assistant", content: [ { type: "tool-call", toolCallId: "call_123", toolName: "void_tool", args: {}, }, ], }, { role: "tool", content: [ { type: "tool-result", toolCallId: "call_123", toolName: "void_tool", result: "<no result>", }, ], }, ]); }); it("closes out a tool call left unresolved before a later message", () => { const result = toGenericMessages([ { role: "assistant", content: [ { type: "tool-call", toolCallId: "call_123", toolName: "get_weather", args: { city: "London" }, }, ], }, { role: "user", content: [{ type: "text", text: "never mind" }], }, ]); expect(result).toEqual([ { role: "assistant", content: [ { type: "tool-call", toolCallId: "call_123", toolName: "get_weather", args: { city: "London" }, }, ], }, { role: "tool", content: [ { type: "tool-result", toolCallId: "call_123", toolName: "get_weather", result: { error: "Tool call was not completed" }, isError: true, }, ], }, { role: "user", content: [{ type: "text", text: "never mind" }], }, ]); }); it("converts tool calls with results (creates tool message)", () => { const result = toGenericMessages([ { role: "assistant", content: [ { type: "tool-call", toolCallId: "call_123", toolName: "get_weather", args: { city: "London" }, result: { temperature: 20, unit: "celsius" }, }, ], }, ]); expect(result).toEqual([ { role: "assistant", content: [ { type: "tool-call", toolCallId: "call_123", toolName: "get_weather", args: { city: "London" }, }, ], }, { role: "tool", content: [ { type: "tool-result", toolCallId: "call_123", toolName: "get_weather", result: { temperature: 20, unit: "celsius" }, }, ], }, ]); }); it("includes isError in tool result when present", () => { const result = toGenericMessages([ { role: "assistant", content: [ { type: "tool-call", toolCallId: "call_123", toolName: "get_weather", args: { city: "London" }, result: "Error: City not found", isError: true, }, ], }, ]); expect(result).toEqual([ { role: "assistant", content: [ { type: "tool-call", toolCallId: "call_123", toolName: "get_weather", args: { city: "London" }, }, ], }, { role: "tool", content: [ { type: "tool-result", toolCallId: "call_123", toolName: "get_weather", result: "Error: City not found", isError: true, }, ], }, ]); }); it("handles text-tool interleaving (flush on text boundary)", () => { const result = toGenericMessages([ { role: "assistant", content: [ { type: "text", text: "Let me check the weather." }, { type: "tool-call", toolCallId: "call_1", toolName: "get_weather", args: { city: "London" }, result: { temp: 20 }, }, { type: "text", text: "The weather is nice." }, ], }, ]); // The flush happens BEFORE adding text when there are pending tool results // So first assistant message contains both initial text and tool call expect(result).toEqual([ { role: "assistant", content: [ { type: "text", text: "Let me check the weather." }, { type: "tool-call", toolCallId: "call_1", toolName: "get_weather", args: { city: "London" }, }, ], }, { role: "tool", content: [ { type: "tool-result", toolCallId: "call_1", toolName: "get_weather", result: { temp: 20 }, }, ], }, { role: "assistant", content: [{ type: "text", text: "The weather is nice." }], }, ]); }); it("handles missing toolCallId", () => { const result = toGenericMessages([ { role: "assistant", content: [ { type: "tool-call", toolName: "get_weather", args: { city: "London" }, }, ], }, ]); // Tool call should be skipped due to missing toolCallId expect(result).toEqual([]); }); it("handles missing toolName", () => { const result = toGenericMessages([ { role: "assistant", content: [ { type: "tool-call", toolCallId: "call_123", args: { city: "London" }, }, ], }, ]); // Tool call should be skipped due to missing toolName expect(result).toEqual([]); }); it("defaults args to empty object when missing", () => { const result = toGenericMessages([ { role: "assistant", content: [ { type: "tool-call", toolCallId: "call_123", toolName: "no_args_tool", }, ], }, ]); expect(result).toEqual([ { role: "assistant", content: [ { type: "tool-call", toolCallId: "call_123", toolName: "no_args_tool", args: {}, }, ], }, { role: "tool", content: [ { type: "tool-result", toolCallId: "call_123", toolName: "no_args_tool", result: { error: "Tool call was not completed" }, isError: true, }, ], }, ]); }); it("handles multiple tool calls with mixed results", () => { const result = toGenericMessages([ { role: "assistant", content: [ { type: "tool-call", toolCallId: "call_1", toolName: "tool_a", args: {}, result: "result_a", }, { type: "tool-call", toolCallId: "call_2", toolName: "tool_b", args: {}, // no result }, { type: "tool-call", toolCallId: "call_3", toolName: "tool_c", args: {}, result: "result_c", }, ], }, ]); expect(result).toEqual([ { role: "assistant", content: [ { type: "tool-call", toolCallId: "call_1", toolName: "tool_a", args: {}, }, { type: "tool-call", toolCallId: "call_2", toolName: "tool_b", args: {}, }, { type: "tool-call", toolCallId: "call_3", toolName: "tool_c", args: {}, }, ], }, { role: "tool", content: [ { type: "tool-result", toolCallId: "call_1", toolName: "tool_a", result: "result_a", }, { type: "tool-result", toolCallId: "call_2", toolName: "tool_b", result: { error: "Tool call was not completed" }, isError: true, }, { type: "tool-result", toolCallId: "call_3", toolName: "tool_c", result: "result_c", }, ], }, ]); }); }); describe("inferImageMediaType", () => { const testMediaType = (url: string, expectedType: string) => { const result = toGenericMessages([ { role: "user", content: [{ type: "image", image: url }], }, ]); const content = (result[0] as { content: { mediaType: string }[] }) .content; expect(content[0]!.mediaType).toBe(expectedType); }; it("maps known extensions to their media types", () => { testMediaType("https://example.com/photo.jpg", "image/jpeg"); testMediaType("https://example.com/photo.jpeg", "image/jpeg"); testMediaType("https://example.com/photo.png", "image/png"); testMediaType("https://example.com/photo.gif", "image/gif"); testMediaType("https://example.com/photo.webp", "image/webp"); testMediaType("https://example.com/icon.svg", "image/svg+xml"); testMediaType("https://example.com/photo.avif", "image/avif"); testMediaType("https://example.com/photo.bmp", "image/bmp"); testMediaType("https://example.com/favicon.ico", "image/x-icon"); testMediaType("https://example.com/photo.tiff", "image/tiff"); testMediaType("https://example.com/photo.tif", "image/tiff"); }); it("defaults to image/png for unknown or missing extensions", () => { testMediaType("https://example.com/photo.unknown", "image/png"); testMediaType("https://example.com/photo", "image/png"); }); it("ignores query params and casing in the extension", () => { testMediaType( "https://example.com/photo.jpg?size=large&quality=high", "image/jpeg", ); testMediaType("https://example.com/photo.JPG", "image/jpeg"); }); }); describe("toUrlOrString", () => { it("converts valid URL to URL object", () => { const result = toGenericMessages([ { role: "user", content: [{ type: "image", image: "https://example.com/photo.png" }], }, ]); const content = (result[0] as { content: { data: unknown }[] }).content; expect(content[0]!.data).toBeInstanceOf(URL); expect((content[0]!.data as URL).href).toBe( "https://example.com/photo.png", ); }); it("converts data URL to URL object", () => { const dataUrl = "data:image/png;base64,abc123"; const result = toGenericMessages([ { role: "user", content: [{ type: "image", image: dataUrl }], }, ]); const content = (result[0] as { content: { data: unknown }[] }).content; expect(content[0]!.data).toBeInstanceOf(URL); }); it("keeps relative path as string", () => { const result = toGenericMessages([ { role: "user", content: [ { type: "file", data: "/path/to/file.txt", mimeType: "text/plain" }, ], }, ]); const content = (result[0] as { content: { data: unknown }[] }).content; expect(content[0]!.data).toBe("/path/to/file.txt"); }); it("keeps invalid URL as string", () => { const result = toGenericMessages([ { role: "user", content: [ { type: "file", data: "not a valid url", mimeType: "text/plain" }, ], }, ]); const content = (result[0] as { content: { data: unknown }[] }).content; expect(content[0]!.data).toBe("not a valid url"); }); }); describe("multiple messages", () => { it("skips null and undefined messages", () => { const result = toGenericMessages([ null, { role: "user", content: [{ type: "text", text: "Hi" }] }, undefined, ] as never); expect(result).toEqual([ { role: "user", content: [{ type: "text", text: "Hi" }] }, ]); }); it("keeps a pending approval in the last message when a null entry trails it", () => { const result = toGenericMessages([ { role: "assistant", content: [ { type: "tool-call", toolCallId: "call_123", toolName: "get_weather", args: { city: "London" }, approval: { id: "ap_1" }, }, ], }, null, ] as never); expect(result).toEqual([ { role: "assistant", content: [ { type: "tool-call", toolCallId: "call_123", toolName: "get_weather", args: { city: "London" }, }, ], }, ]); }); it("converts a full conversation", () => { const result = toGenericMessages([ { role: "system", content: [{ type: "text", text: "You are helpful." }], }, { role: "user", content: [{ type: "text", text: "What is 2+2?" }], }, { role: "assistant", content: [{ type: "text", text: "2+2 equals 4." }], }, { role: "user", content: [{ type: "text", text: "Thanks!" }], }, ]); expect(result).toEqual([ { role: "system", content: "You are helpful." }, { role: "user", content: [{ type: "text", text: "What is 2+2?" }] }, { role: "assistant", content: [{ type: "text", text: "2+2 equals 4." }], }, { role: "user", content: [{ type: "text", text: "Thanks!" }] }, ]); }); it("skips messages without content and keeps the rest", () => { const result = toGenericMessages([ { role: "system" }, { role: "user" }, { role: "assistant" }, { role: "user", content: [{ type: "text", text: "Thanks!" }] }, ] as never); expect(result).toEqual([ { role: "user", content: [{ type: "text", text: "Thanks!" }] }, ]); }); it("handles empty messages array", () => { const result = toGenericMessages([]); expect(result).toEqual([]); }); }); });