assistant-stream
Version:
Streaming utilities for AI assistants
774 lines (687 loc) • 23.2 kB
text/typescript
import { describe, expect, it } from "vitest";
import type { StandardJSONSchemaV1 } from "@standard-schema/spec";
import {
toJSONSchema,
toPartialJSONSchema,
toToolsJSONSchema,
} from "./schema-utils";
import type { Tool } from "./tool-types";
const standardSchemaReturning = (result: Record<string, unknown>) => ({
"~standard": {
version: 1 as const,
vendor: "test",
validate: () => ({ value: {} }),
jsonSchema: {
input: () => ({ type: "object", ...result }),
output: () => ({ type: "object" }),
},
},
});
describe("toJSONSchema", () => {
it("ignores the non-spec ~standard.toJSONSchema hook", () => {
const mockStandardSchema = {
"~standard": {
version: 1 as const,
vendor: "test",
validate: () => ({ value: {} }),
toJSONSchema: () => ({
type: "object",
properties: { name: { type: "string" } },
}),
},
};
expect(() => toJSONSchema(mockStandardSchema)).toThrow(
'Could not convert the "test" schema to JSON Schema',
);
});
it("passes the draft-07 target to object with toJSONSchema() method", () => {
let receivedOptions: StandardJSONSchemaV1.Options | undefined;
const schemaWithMethod = {
toJSONSchema: (options: StandardJSONSchemaV1.Options) => {
receivedOptions = options;
return {
$schema:
options.target === "draft-07"
? "http://json-schema.org/draft-07/schema#"
: "https://json-schema.org/draft/2020-12/schema",
type: "object",
properties: { age: { type: "number" } },
};
},
};
const result = toJSONSchema(schemaWithMethod as never);
expect(receivedOptions).toEqual({ target: "draft-07" });
expect(result).toEqual({
$schema: "http://json-schema.org/draft-07/schema#",
type: "object",
properties: { age: { type: "number" } },
});
});
it("converts object with toJSON() method", () => {
const schemaWithToJSON = {
toJSON: () => ({
type: "object",
properties: { active: { type: "boolean" } },
}),
};
const result = toJSONSchema(schemaWithToJSON as never);
expect(result).toEqual({
type: "object",
properties: { active: { type: "boolean" } },
});
});
it("passes through plain JSONSchema7", () => {
const plainSchema = {
type: "object" as const,
properties: {
email: { type: "string" as const, format: "email" },
},
required: ["email"],
};
const result = toJSONSchema(plainSchema);
expect(result).toEqual(plainSchema);
});
it("prioritizes StandardSchema over toJSONSchema method", () => {
const mixedSchema = {
"~standard": {
version: 1 as const,
vendor: "test",
validate: () => ({ value: {} }),
jsonSchema: {
input: () => ({ type: "string", description: "from standard" }),
output: () => ({ type: "number" }),
},
},
toJSONSchema: () => ({ type: "number", description: "from method" }),
};
const result = toJSONSchema(mixedSchema);
expect(result).toEqual({ type: "string", description: "from standard" });
});
it("prioritizes toJSONSchema over toJSON method", () => {
const mixedSchema = {
toJSONSchema: () => ({
type: "string",
description: "from toJSONSchema",
}),
toJSON: () => ({ type: "number", description: "from toJSON" }),
};
const result = toJSONSchema(mixedSchema as never);
expect(result).toEqual({
type: "string",
description: "from toJSONSchema",
});
});
it("throws when StandardSchema has no JSON Schema conversion method", () => {
const schemaWithoutMethod = {
"~standard": {
version: 1 as const,
vendor: "test",
validate: () => ({ value: {} }),
// no toJSONSchema method and no jsonSchema property
},
};
expect(() => toJSONSchema(schemaWithoutMethod)).toThrow(
"wrap the schema with test's Standard JSON Schema helper",
);
});
it("names the Zod helper when a Zod schema has no converter", () => {
const zodMiniSchema = {
"~standard": {
version: 1 as const,
vendor: "zod",
validate: () => ({ value: {} }),
},
};
expect(() => toJSONSchema(zodMiniSchema)).toThrow(
"convert it with z.toJSONSchema(schema)",
);
});
it("converts Standard JSON Schema inputs to draft-07 for tool parameters", () => {
const schema = {
"~standard": {
version: 1 as const,
vendor: "test",
validate: () => ({ value: {} }),
jsonSchema: {
input: ({ target }: StandardJSONSchemaV1.Options) => {
expect(target).toBe("draft-07");
return {
type: "object",
properties: { name: { type: "string" } },
};
},
output: () => ({ type: "number" }),
},
},
};
const result = toJSONSchema(schema);
expect(result).toEqual({
type: "object",
properties: { name: { type: "string" } },
});
expect(toToolsJSONSchema({ example: { parameters: schema } })).toEqual({
example: {
parameters: {
type: "object",
properties: { name: { type: "string" } },
},
},
});
});
it("rejects a Standard JSON Schema converter that ignores the target", () => {
const schema = {
"~standard": {
version: 1 as const,
vendor: "test",
validate: () => ({ value: {} }),
jsonSchema: {
input: () => ({
$schema: "https://json-schema.org/draft/2020-12/schema",
type: "object",
}),
output: () => ({ type: "object" }),
},
},
};
expect(() => toJSONSchema(schema)).toThrow(
'asked for a draft-07 JSON Schema and returned "https://json-schema.org/draft/2020-12/schema"',
);
});
it("accepts every spelling of the draft-07 dialect id", () => {
for (const $schema of [
"http://json-schema.org/draft-07/schema#",
"http://json-schema.org/draft-07/schema",
"https://json-schema.org/draft-07/schema#",
"https://json-schema.org/draft-07/schema",
]) {
expect(toJSONSchema(standardSchemaReturning({ $schema }))).toEqual({
$schema,
type: "object",
});
}
});
it("accepts an undeclared dialect from the Standard JSON Schema converter", () => {
const declared = {
type: "object",
properties: { item: { $ref: "#/$defs/item" } },
$defs: { item: { type: "string" } },
};
expect(toJSONSchema(standardSchemaReturning(declared))).toEqual(declared);
});
it("passes through a toJSONSchema() method answering in another dialect", () => {
const schema = {
toJSONSchema: () => ({
$schema: "https://json-schema.org/draft/2020-12/schema",
type: "object",
}),
};
expect(toJSONSchema(schema as never)).toEqual({
$schema: "https://json-schema.org/draft/2020-12/schema",
type: "object",
});
});
it("passes through a toJSON() result in another dialect", () => {
const schema = {
toJSON: () => ({
$schema: "https://json-schema.org/draft/2020-12/schema",
type: "object",
}),
};
expect(toJSONSchema(schema as never)).toEqual({
$schema: "https://json-schema.org/draft/2020-12/schema",
type: "object",
});
});
it("passes through a plain schema in another dialect", () => {
const plainSchema = {
$schema: "https://json-schema.org/draft/2020-12/schema",
type: "object" as const,
};
expect(toJSONSchema(plainSchema)).toEqual(plainSchema);
});
it("keeps converting tools whose parameters arrive in another dialect", () => {
const remoteInputSchema = {
$schema: "https://json-schema.org/draft/2020-12/schema",
type: "object" as const,
properties: { query: { type: "string" as const } },
};
expect(
toToolsJSONSchema({ remote: { parameters: remoteInputSchema } }),
).toEqual({ remote: { parameters: remoteInputSchema } });
});
});
describe("toPartialJSONSchema", () => {
it("removes required from a flat object schema", () => {
const schema = {
type: "object" as const,
properties: {
name: { type: "string" as const },
age: { type: "number" as const },
},
required: ["name", "age"],
};
const result = toPartialJSONSchema(schema);
expect(result.required).toBeUndefined();
expect(result.properties).toEqual(schema.properties);
});
it("recursively removes required from nested objects", () => {
const schema = {
type: "object" as const,
properties: {
address: {
type: "object" as const,
properties: {
street: { type: "string" as const },
city: { type: "string" as const },
},
required: ["street", "city"],
},
},
required: ["address"],
};
const result = toPartialJSONSchema(schema);
expect(result.required).toBeUndefined();
const address = result.properties!.address as Record<string, unknown>;
expect(address.required).toBeUndefined();
});
it("removes nested required without properties and preserves value constraints", () => {
const schema = {
type: "object" as const,
properties: {
settings: {
type: "object" as const,
required: ["name"],
additionalProperties: { type: "string" as const },
},
},
required: ["settings"],
};
expect(toPartialJSONSchema(schema)).toEqual({
type: "object",
properties: {
settings: {
type: "object",
additionalProperties: { type: "string" },
},
},
});
expect(schema.properties.settings.required).toEqual(["name"]);
});
it("leaves array item schemas unchanged", () => {
const schema = {
type: "object" as const,
properties: {
tags: {
type: "array" as const,
items: {
type: "object" as const,
properties: { label: { type: "string" as const } },
required: ["label"],
},
},
},
required: ["tags"],
};
const result = toPartialJSONSchema(schema);
expect(result.required).toBeUndefined();
const tags = result.properties!.tags as Record<string, unknown>;
const items = tags.items as Record<string, unknown>;
expect(items.required).toEqual(["label"]);
});
it("handles schema with no required field", () => {
const schema = {
type: "object" as const,
properties: { x: { type: "string" as const } },
};
const result = toPartialJSONSchema(schema);
expect(result).toEqual(schema);
});
it("does not mutate the input schema", () => {
const schema = {
type: "object" as const,
properties: { a: { type: "string" as const } },
required: ["a"],
};
toPartialJSONSchema(schema);
expect(schema.required).toEqual(["a"]);
});
it("preserves additionalProperties", () => {
const schema = {
type: "object" as const,
properties: { x: { type: "string" as const } },
required: ["x"],
additionalProperties: false,
};
const result = toPartialJSONSchema(schema);
expect(result.additionalProperties).toBe(false);
expect(result.required).toBeUndefined();
});
});
describe("toToolsJSONSchema", () => {
describe("filtering", () => {
it("excludes disabled tools by default", () => {
const tools: Record<string, Tool> = {
enabledTool: {
description: "Enabled tool",
parameters: { type: "object", properties: {} },
},
disabledTool: {
disabled: true,
description: "Disabled tool",
parameters: { type: "object", properties: {} },
},
};
const result = toToolsJSONSchema(tools);
expect(result).toHaveProperty("enabledTool");
expect(result).not.toHaveProperty("disabledTool");
});
it("excludes backend tools by default", () => {
const tools: Record<string, Tool> = {
frontendTool: {
type: "frontend",
description: "Frontend tool",
parameters: { type: "object", properties: {} },
execute: async () => {},
},
backendTool: {
type: "backend",
},
};
const result = toToolsJSONSchema(tools);
expect(result).toHaveProperty("frontendTool");
expect(result).not.toHaveProperty("backendTool");
});
it("includes frontend tools", () => {
const tools: Record<string, Tool> = {
myTool: {
type: "frontend",
description: "A frontend tool",
parameters: { type: "object", properties: { x: { type: "number" } } },
execute: async () => {},
},
};
const result = toToolsJSONSchema(tools);
expect(result).toEqual({
myTool: {
description: "A frontend tool",
parameters: { type: "object", properties: { x: { type: "number" } } },
},
});
});
it("excludes frontend tools without execute by default", () => {
const tools: Record<string, Tool> = {
stubbedTool: {
type: "frontend",
description: "A frontend tool supplied by local overrides",
parameters: { type: "object", properties: {} },
},
};
const result = toToolsJSONSchema(tools);
expect(result).not.toHaveProperty("stubbedTool");
});
it("includes human tools", () => {
const tools: Record<string, Tool> = {
humanTool: {
type: "human",
description: "A human tool",
parameters: { type: "object", properties: {} },
},
};
const result = toToolsJSONSchema(tools);
expect(result).toHaveProperty("humanTool");
});
it("omits schemas for tools with backend parameter defaults", () => {
const tools: Record<string, Tool> = {
generatedFrontendTool: {
type: "frontend",
description: "A generated frontend tool",
parameters: { type: "object", properties: {} },
execute: async () => {},
unstable_backendDefault: { parameters: true },
},
generatedHumanTool: {
type: "human",
description: "A generated human tool",
parameters: { type: "object", properties: {} },
unstable_backendDefault: { parameters: true },
},
olderFrontendTool: {
type: "frontend",
description: "An older frontend tool",
parameters: { type: "object", properties: {} },
execute: async () => {},
},
};
const result = toToolsJSONSchema(tools);
expect(result).not.toHaveProperty("generatedFrontendTool");
expect(result).not.toHaveProperty("generatedHumanTool");
expect(result).toHaveProperty("olderFrontendTool");
});
it("respects custom filter function", () => {
const tools: Record<string, Tool> = {
tool_a: {
disabled: true,
parameters: { type: "object", properties: {} },
},
tool_b: {
type: "backend",
},
tool_c: {
parameters: { type: "object", properties: {} },
},
};
// Custom filter that includes all tools regardless of disabled/backend
const result = toToolsJSONSchema(tools, {
filter: () => true,
});
// tool_a and tool_c have parameters, tool_b does not
expect(result).toHaveProperty("tool_a");
expect(result).not.toHaveProperty("tool_b"); // still excluded due to no parameters
expect(result).toHaveProperty("tool_c");
});
it("always excludes backend-default parameters with a custom filter", () => {
const tools: Record<string, Tool> = {
backendDefaultTool: {
type: "frontend",
parameters: { type: "object", properties: {} },
execute: async () => {},
unstable_backendDefault: { parameters: true },
},
normalTool: {
parameters: { type: "object", properties: {} },
},
};
const result = toToolsJSONSchema(tools, {
filter: () => true,
});
expect(result).not.toHaveProperty("backendDefaultTool");
expect(result).toHaveProperty("normalTool");
});
it("custom filter receives name and tool", () => {
const tools: Record<string, Tool> = {
prefixed_tool: {
description: "Should include",
parameters: { type: "object", properties: {} },
},
other_tool: {
description: "Should exclude",
parameters: { type: "object", properties: {} },
},
};
const result = toToolsJSONSchema(tools, {
filter: (name, tool) =>
name.startsWith("prefixed_") && tool.description !== undefined,
});
expect(result).toHaveProperty("prefixed_tool");
expect(result).not.toHaveProperty("other_tool");
});
});
describe("output format", () => {
it("includes description when present", () => {
const tools: Record<string, Tool> = {
myTool: {
description: "This is my tool",
parameters: { type: "object", properties: {} },
},
};
const result = toToolsJSONSchema(tools);
expect(result.myTool).toEqual({
description: "This is my tool",
parameters: { type: "object", properties: {} },
});
});
it("omits description when absent", () => {
const tools: Record<string, Tool> = {
myTool: {
parameters: { type: "object", properties: {} },
},
};
const result = toToolsJSONSchema(tools);
expect(result.myTool).toEqual({
parameters: { type: "object", properties: {} },
});
expect(result.myTool).not.toHaveProperty("description");
});
it("omits description when empty string", () => {
const tools: Record<string, Tool> = {
myTool: {
description: "",
parameters: { type: "object", properties: {} },
},
};
const result = toToolsJSONSchema(tools);
expect(result.myTool).not.toHaveProperty("description");
});
it("converts parameters via toJSONSchema", () => {
const mockStandardSchema = {
"~standard": {
version: 1 as const,
vendor: "test",
validate: () => ({ value: {} }),
jsonSchema: {
input: () => ({
type: "object",
properties: { converted: { type: "boolean" } },
}),
output: () => ({ type: "object" }),
},
},
};
const tools: Record<string, Tool> = {
myTool: {
description: "Test",
parameters: mockStandardSchema,
},
};
const result = toToolsJSONSchema(tools);
expect(result.myTool!.parameters).toEqual({
type: "object",
properties: { converted: { type: "boolean" } },
});
});
it("forwards providerOptions verbatim when present", () => {
const tools: Record<string, Tool> = {
myTool: {
description: "Test",
parameters: { type: "object", properties: {} },
providerOptions: { anthropic: { deferLoading: true } },
},
};
const result = toToolsJSONSchema(tools);
expect(result.myTool).toEqual({
description: "Test",
parameters: { type: "object", properties: {} },
providerOptions: { anthropic: { deferLoading: true } },
});
});
it("omits providerOptions when absent", () => {
const tools: Record<string, Tool> = {
myTool: {
parameters: { type: "object", properties: {} },
},
};
const result = toToolsJSONSchema(tools);
expect(result.myTool).not.toHaveProperty("providerOptions");
});
});
describe("stable ordering", () => {
it("emits tool names in alphabetical order", () => {
const tools: Record<string, Tool> = {
zebra: { parameters: { type: "object", properties: {} } },
apple: { parameters: { type: "object", properties: {} } },
mango: { parameters: { type: "object", properties: {} } },
};
const result = toToolsJSONSchema(tools);
expect(Object.keys(result)).toEqual(["apple", "mango", "zebra"]);
});
it("produces byte-identical output regardless of insertion order", () => {
const a: Record<string, Tool> = {
zebra: { parameters: { type: "object", properties: {} } },
apple: { parameters: { type: "object", properties: {} } },
};
const b: Record<string, Tool> = {
apple: { parameters: { type: "object", properties: {} } },
zebra: { parameters: { type: "object", properties: {} } },
};
expect(JSON.stringify(toToolsJSONSchema(a))).toBe(
JSON.stringify(toToolsJSONSchema(b)),
);
});
});
describe("edge cases", () => {
it("returns empty object for undefined tools", () => {
const result = toToolsJSONSchema(undefined);
expect(result).toEqual({});
});
it("returns empty object for empty tools", () => {
const result = toToolsJSONSchema({});
expect(result).toEqual({});
});
it("returns empty object when all tools are filtered out", () => {
const tools: Record<string, Tool> = {
disabled1: {
disabled: true,
parameters: { type: "object", properties: {} },
},
disabled2: {
disabled: true,
parameters: { type: "object", properties: {} },
},
};
const result = toToolsJSONSchema(tools);
expect(result).toEqual({});
});
it("handles tools with undefined type (defaults to frontend behavior)", () => {
const tools: Record<string, Tool> = {
myTool: {
// no type specified
description: "Tool without type",
parameters: { type: "object", properties: {} },
},
};
const result = toToolsJSONSchema(tools);
expect(result).toHaveProperty("myTool");
});
it("handles multiple tools correctly", () => {
const tools: Record<string, Tool> = {
tool_1: {
description: "First tool",
parameters: { type: "object", properties: { a: { type: "string" } } },
},
tool_2: {
description: "Second tool",
parameters: { type: "object", properties: { b: { type: "number" } } },
},
tool_3: {
disabled: true,
parameters: { type: "object", properties: {} },
},
};
const result = toToolsJSONSchema(tools);
expect(Object.keys(result)).toHaveLength(2);
expect(result).toHaveProperty("tool_1");
expect(result).toHaveProperty("tool_2");
expect(result).not.toHaveProperty("tool_3");
});
});
});