assistant-stream
Version:
Streaming utilities for AI assistants
229 lines (202 loc) • 6.17 kB
text/typescript
import { describe, expect, it } from "vitest";
import { asSchema } from "ai";
import { frontendTools } from "./frontendTools";
import { wrapModelContentEnvelope } from "./modelContentEnvelope";
const expectedFilePart = (
data: string,
mediaType: string,
filename?: string,
) => ({
...(process.env["AI_PEER_MAJOR"] === "7"
? { type: "file", data: { type: "data", data } }
: { type: "file-data", data }),
mediaType,
...(filename !== undefined && { filename }),
});
describe("frontendTools", () => {
it("preserves each tool's description and parameter schema", async () => {
const tools = frontendTools({
getWeather: {
description: "Get the weather",
parameters: {
type: "object",
properties: { city: { type: "string" } },
required: ["city"],
additionalProperties: false,
},
},
rollDice: {
description: "Roll dice",
parameters: {
type: "object",
properties: { count: { type: "integer", minimum: 1 } },
required: ["count"],
},
},
});
expect(tools.getWeather?.description).toBe("Get the weather");
expect(await asSchema(tools.getWeather!.inputSchema).jsonSchema).toEqual({
type: "object",
properties: { city: { type: "string" } },
required: ["city"],
additionalProperties: false,
});
expect(tools.rollDice?.description).toBe("Roll dice");
expect(await asSchema(tools.rollDice!.inputSchema).jsonSchema).toEqual({
type: "object",
properties: { count: { type: "integer", minimum: 1 } },
required: ["count"],
});
});
it("registers a toModelOutput that unwraps the modelContent envelope", async () => {
const tools = frontendTools({
readPdf: {
parameters: { type: "object" },
},
});
const envelope = wrapModelContentEnvelope(
{ mediaType: "application/pdf", base64: "JVBERi0xLjQK" },
[
{ type: "text", text: "PDF contents:" },
{
type: "file",
data: "JVBERi0xLjQK",
mediaType: "application/pdf",
filename: "doc.pdf",
},
],
);
const output = await tools.readPdf!.toModelOutput!({
toolCallId: "tc-1",
input: {},
output: envelope,
});
expect(output).toEqual({
type: "content",
value: [
{ type: "text", text: "PDF contents:" },
expectedFilePart("JVBERi0xLjQK", "application/pdf", "doc.pdf"),
],
});
});
it("emits a file part for image media types", async () => {
const tools = frontendTools({
screenshot: {
parameters: { type: "object" },
},
});
const envelope = wrapModelContentEnvelope(
{ mediaType: "image/png", base64: "iVBORw0KGgo=" },
[
{
type: "file",
data: "iVBORw0KGgo=",
mediaType: "image/png",
},
],
);
const output = await tools.screenshot!.toModelOutput!({
toolCallId: "tc-1",
input: {},
output: envelope,
});
expect(output).toEqual({
type: "content",
value: [expectedFilePart("iVBORw0KGgo=", "image/png")],
});
});
it("falls back to a JSON output for plain (non-envelope) results", async () => {
const tools = frontendTools({
getWeather: {
parameters: { type: "object" },
},
});
const output = await tools.getWeather!.toModelOutput!({
toolCallId: "tc-1",
input: {},
output: { temp: 72 },
});
expect(output).toEqual({ type: "json", value: { temp: 72 } });
});
it("falls back to a text output for plain string results", async () => {
const tools = frontendTools({
echo: {
parameters: { type: "object" },
},
});
const output = await tools.echo!.toModelOutput!({
toolCallId: "tc-1",
input: {},
output: "hello",
});
expect(output).toEqual({ type: "text", value: "hello" });
});
it("forwards providerOptions verbatim when present", () => {
const tools = frontendTools({
getWeather: {
description: "Get the weather",
parameters: { type: "object", properties: {} },
providerOptions: { anthropic: { deferLoading: true } },
},
});
expect(tools.getWeather?.providerOptions).toEqual({
anthropic: { deferLoading: true },
});
});
it("omits providerOptions when absent", () => {
const tools = frontendTools({
getWeather: {
parameters: { type: "object", properties: {} },
},
});
expect(tools.getWeather).not.toHaveProperty("providerOptions");
});
it("names the malformed frontend tool when parameters are missing", () => {
expect(() =>
frontendTools({
getWeather: {
description: "Get the weather",
},
} as never),
).toThrow(
'frontendTools() expected tool "getWeather" to include a JSON Schema parameters object.',
);
});
it("rejects non-object tools request bodies", () => {
expect(() => frontendTools(null as never)).toThrow(
"frontendTools() expected tools to be an object keyed by tool name.",
);
expect(() => frontendTools([] as never)).toThrow(
"frontendTools() expected tools to be an object keyed by tool name.",
);
});
it("names frontend tool entries with malformed field types", () => {
expect(() =>
frontendTools({
getWeather: null,
} as never),
).toThrow(
'frontendTools() expected tool "getWeather" to be an object with a JSON Schema parameters object.',
);
expect(() =>
frontendTools({
getWeather: {
description: 123,
parameters: { type: "object" },
},
} as never),
).toThrow(
'frontendTools() expected tool "getWeather" description to be a string.',
);
expect(() =>
frontendTools({
getWeather: {
parameters: { type: "object" },
providerOptions: "anthropic",
},
} as never),
).toThrow(
'frontendTools() expected tool "getWeather" providerOptions to be an object.',
);
});
});