From ab624bd13a0bf1dc92665973dea559edcc31c951 Mon Sep 17 00:00:00 2001 From: Luke Parke <5702154+LukasParke@users.noreply.github.com> Date: Tue, 11 Aug 2026 14:58:44 -0500 Subject: [PATCH 1/5] feat: support Standard Schema tool validators --- .speakeasy/gen.yaml | 3 + CLAUDE.md | 2 +- README.md | 2 + .../call-model-typed-tool-calling.example.ts | 22 +- examples/package.json | 4 +- examples/pnpm-lock.yaml | 31 +++ examples/tools-example.ts | 2 +- package.json | 3 + pnpm-lock.yaml | 35 ++++ src/lib/tool-executor.ts | 137 +++++++++--- src/lib/tool-types.ts | 97 +++++---- src/lib/tool.ts | 119 ++++++----- tests/unit/standard-schema-tools.test.ts | 195 ++++++++++++++++++ 13 files changed, 513 insertions(+), 139 deletions(-) create mode 100644 tests/unit/standard-schema-tools.test.ts diff --git a/.speakeasy/gen.yaml b/.speakeasy/gen.yaml index 606f8260d..522498e59 100644 --- a/.speakeasy/gen.yaml +++ b/.speakeasy/gen.yaml @@ -41,10 +41,13 @@ typescript: acceptHeaderEnum: false additionalDependencies: dependencies: + '@standard-schema/spec': ^1.1.0 zod: ^3.25.0 || ^4.0.0 devDependencies: '@types/node': ^22.13.12 + '@valibot/to-json-schema': ^1.7.1 dotenv: ^16.4.7 + valibot: ^1.4.2 vitest: ^3.2.4 peerDependencies: {} additionalPackageJSON: diff --git a/CLAUDE.md b/CLAUDE.md index 49edfaa8e..9f0e9c7ec 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -127,7 +127,7 @@ Speakeasy's [persistent edits](https://www.speakeasy.com/docs/sdks/customize/cod - Uses `ReusableReadableStream` to enable multiple parallel consumers **Tool System** (`src/lib/tool.ts`, `src/lib/tool-types.ts`, `src/lib/tool-executor.ts`) -- `tool()` helper creates type-safe tools with Zod schemas +- `tool()` accepts Zod or Standard Schema v1 validators; non-Zod inputs also require `inputJsonSchema` for provider requests - Three tool types: - **Regular tools** (`execute: function`) - auto-executed, return final result - **Generator tools** (`execute: async generator`) - stream preliminary results diff --git a/README.md b/README.md index 7411a9be6..7ab06f57d 100644 --- a/README.md +++ b/README.md @@ -49,6 +49,8 @@ yarn add @openrouter/sdk > [!IMPORTANT] > `callModel` and its associated types have moved to the [`@openrouter/agent`](https://www.npmjs.com/package/@openrouter/agent) package. If you are using `callModel`, tool definitions, or related types from `@openrouter/sdk`, you should migrate to `@openrouter/agent`. > +> The legacy SDK tool API accepts Zod schemas or any [Standard Schema v1](https://standardschema.dev/) validator for `inputSchema`, `outputSchema`, and `eventSchema`. Because Standard Schema does not define JSON Schema generation, non-Zod input validators must also provide `inputJsonSchema`, which is sent to model providers after `~`-prefixed metadata is removed. Zod input schemas continue to use the built-in `z4.toJSONSchema` path. +> > To assist with the migration, run: > > ```bash diff --git a/examples/call-model-typed-tool-calling.example.ts b/examples/call-model-typed-tool-calling.example.ts index ae94b7515..a5a9faafd 100644 --- a/examples/call-model-typed-tool-calling.example.ts +++ b/examples/call-model-typed-tool-calling.example.ts @@ -3,7 +3,7 @@ * * This example demonstrates how to use the tool() function for * fully-typed tool definitions where execute params, return types, and event - * types are automatically inferred from Zod schemas. + * types are automatically inferred from Zod or any Standard Schema v1 validator. * * Tool types are auto-detected based on configuration: * - Generator tool: When `eventSchema` is provided @@ -17,7 +17,10 @@ import dotenv from "dotenv"; dotenv.config(); -import { OpenRouter, tool } from "../src/index.js"; +import { toJsonSchema } from "@valibot/to-json-schema"; +import * as v from "valibot"; +import { OpenRouter } from "../src/index.js"; +import { tool } from "../src/lib/tool.js"; import z from "zod"; const openRouter = new OpenRouter({ @@ -27,15 +30,18 @@ const openRouter = new OpenRouter({ // Create a typed regular tool using tool() // The execute function params are automatically typed as z.infer // The return type is enforced based on outputSchema +const weatherInputSchema = v.object({ location: v.string() }); + const weatherTool = tool({ name: "get_weather", description: "Get the current weather for a location", - inputSchema: z.object({ - location: z.string().describe("The city and country, e.g. San Francisco, CA"), - }), - outputSchema: z.object({ - temperature: z.number(), - description: z.string(), + inputSchema: weatherInputSchema, + // Standard Schema does not define JSON Schema conversion, so non-Zod + // validators provide the schema sent to model providers explicitly. + inputJsonSchema: toJsonSchema(weatherInputSchema), + outputSchema: v.object({ + temperature: v.number(), + description: v.string(), }), // params is automatically typed as { location: string } execute: async (params) => { diff --git a/examples/package.json b/examples/package.json index 410efafd6..a9012bae2 100644 --- a/examples/package.json +++ b/examples/package.json @@ -13,6 +13,8 @@ "tsx": "^4.19.2" }, "dependencies": { - "@openrouter/sdk": "file:.." + "@openrouter/sdk": "file:..", + "@valibot/to-json-schema": "^1.7.1", + "valibot": "^1.4.2" } } \ No newline at end of file diff --git a/examples/pnpm-lock.yaml b/examples/pnpm-lock.yaml index b08c0e315..808b39fc4 100644 --- a/examples/pnpm-lock.yaml +++ b/examples/pnpm-lock.yaml @@ -11,6 +11,12 @@ importers: '@openrouter/sdk': specifier: file:.. version: file:.. + '@valibot/to-json-schema': + specifier: ^1.7.1 + version: 1.7.1(valibot@1.4.2) + valibot: + specifier: ^1.4.2 + version: 1.4.2 devDependencies: '@types/node': specifier: ^20.0.0 @@ -183,9 +189,17 @@ packages: '@openrouter/sdk@file:..': resolution: {directory: .., type: directory} + '@standard-schema/spec@1.1.0': + resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} + '@types/node@20.19.25': resolution: {integrity: sha512-ZsJzA5thDQMSQO788d7IocwwQbI8B5OPzmqNvpf3NY/+MHDAS759Wo0gd2WQeXYt5AAAQjzcrTVC6SKCuYgoCQ==} + '@valibot/to-json-schema@1.7.1': + resolution: {integrity: sha512-3qkmU6KXWh8GIThEAW3kuRHPQBMjWkKy+Ppz3WkUucx53DTpOa6siMn4xDGSOhlVyMrDaJTCTMLYPZVAIk1P0A==} + peerDependencies: + valibot: ^1.4.0 + dotenv@16.6.1: resolution: {integrity: sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==} engines: {node: '>=12'} @@ -214,6 +228,14 @@ packages: undici-types@6.21.0: resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + valibot@1.4.2: + resolution: {integrity: sha512-gjdCvJ6d3RyHAneqxMYMW9QMCwYMb3jpOO0IyHZV1bnRHFBHrX3VkIILt5XYR0WhwHiH7Mty8ovuPZ/O3gamrg==} + peerDependencies: + typescript: '>=5' + peerDependenciesMeta: + typescript: + optional: true + zod@4.1.13: resolution: {integrity: sha512-AvvthqfqrAhNH9dnfmrfKzX5upOdjUVJYFqNSlkmGf64gRaTzlPwz99IHYnVs28qYAybvAlBV+H7pn0saFY4Ig==} @@ -299,12 +321,19 @@ snapshots: '@openrouter/sdk@file:..': dependencies: + '@standard-schema/spec': 1.1.0 zod: 4.1.13 + '@standard-schema/spec@1.1.0': {} + '@types/node@20.19.25': dependencies: undici-types: 6.21.0 + '@valibot/to-json-schema@1.7.1(valibot@1.4.2)': + dependencies: + valibot: 1.4.2 + dotenv@16.6.1: {} esbuild@0.27.0: @@ -354,4 +383,6 @@ snapshots: undici-types@6.21.0: {} + valibot@1.4.2: {} + zod@4.1.13: {} diff --git a/examples/tools-example.ts b/examples/tools-example.ts index d5ec52760..dcf4aa9ea 100644 --- a/examples/tools-example.ts +++ b/examples/tools-example.ts @@ -3,7 +3,7 @@ * * This file demonstrates the automatic tool execution feature. * When you provide tools with `execute` functions, they are automatically: - * 1. Validated using Zod schemas + * 1. Validated using Zod or any Standard Schema v1 validator * 2. Executed when the model calls them * 3. Results sent back to the model * 4. Process repeats until stopWhen condition is met (default: stepCountIs(5)) diff --git a/package.json b/package.json index 4e7a6558e..d1f1a15b7 100644 --- a/package.json +++ b/package.json @@ -87,14 +87,17 @@ "devDependencies": { "@eslint/js": "^9.26.0", "@types/node": "^22.13.12", + "@valibot/to-json-schema": "^1.7.1", "dotenv": "^16.4.7", "eslint": "^9.26.0", "globals": "^15.14.0", "typescript": "~5.8.3", "typescript-eslint": "^8.26.0", + "valibot": "^1.4.2", "vitest": "^3.2.4" }, "dependencies": { + "@standard-schema/spec": "^1.1.0", "zod": "^3.25.0 || ^4.0.0" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6fac9ead9..842fea49f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,6 +8,9 @@ importers: .: dependencies: + '@standard-schema/spec': + specifier: ^1.1.0 + version: 1.1.0 zod: specifier: ^3.25.0 || ^4.0.0 version: 4.2.1 @@ -18,6 +21,9 @@ importers: '@types/node': specifier: ^22.13.12 version: 22.18.13 + '@valibot/to-json-schema': + specifier: ^1.7.1 + version: 1.7.1(valibot@1.4.2(typescript@5.8.3)) dotenv: specifier: ^16.4.7 version: 16.6.1 @@ -33,6 +39,9 @@ importers: typescript-eslint: specifier: ^8.26.0 version: 8.46.2(eslint@9.38.0)(typescript@5.8.3) + valibot: + specifier: ^1.4.2 + version: 1.4.2(typescript@5.8.3) vitest: specifier: ^3.2.4 version: 3.2.4(@types/node@22.18.13) @@ -374,6 +383,9 @@ packages: cpu: [x64] os: [win32] + '@standard-schema/spec@1.1.0': + resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} + '@types/chai@5.2.3': resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} @@ -448,6 +460,11 @@ packages: resolution: {integrity: sha512-tUFMXI4gxzzMXt4xpGJEsBsTox0XbNQ1y94EwlD/CuZwFcQP79xfQqMhau9HsRc/J0cAPA/HZt1dZPtGn9V/7w==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@valibot/to-json-schema@1.7.1': + resolution: {integrity: sha512-3qkmU6KXWh8GIThEAW3kuRHPQBMjWkKy+Ppz3WkUucx53DTpOa6siMn4xDGSOhlVyMrDaJTCTMLYPZVAIk1P0A==} + peerDependencies: + valibot: ^1.4.0 + '@vitest/expect@3.2.4': resolution: {integrity: sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==} @@ -958,6 +975,14 @@ packages: uri-js@4.4.1: resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + valibot@1.4.2: + resolution: {integrity: sha512-gjdCvJ6d3RyHAneqxMYMW9QMCwYMb3jpOO0IyHZV1bnRHFBHrX3VkIILt5XYR0WhwHiH7Mty8ovuPZ/O3gamrg==} + peerDependencies: + typescript: '>=5' + peerDependenciesMeta: + typescript: + optional: true + vite-node@3.2.4: resolution: {integrity: sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==} engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} @@ -1269,6 +1294,8 @@ snapshots: '@rollup/rollup-win32-x64-msvc@4.52.5': optional: true + '@standard-schema/spec@1.1.0': {} + '@types/chai@5.2.3': dependencies: '@types/deep-eql': 4.0.2 @@ -1377,6 +1404,10 @@ snapshots: '@typescript-eslint/types': 8.46.2 eslint-visitor-keys: 4.2.1 + '@valibot/to-json-schema@1.7.1(valibot@1.4.2(typescript@5.8.3))': + dependencies: + valibot: 1.4.2(typescript@5.8.3) + '@vitest/expect@3.2.4': dependencies: '@types/chai': 5.2.3 @@ -1894,6 +1925,10 @@ snapshots: dependencies: punycode: 2.3.1 + valibot@1.4.2(typescript@5.8.3): + optionalDependencies: + typescript: 5.8.3 + vite-node@3.2.4(@types/node@22.18.13): dependencies: cac: 6.7.14 diff --git a/src/lib/tool-executor.ts b/src/lib/tool-executor.ts index 02bc49a09..d4de824ca 100644 --- a/src/lib/tool-executor.ts +++ b/src/lib/tool-executor.ts @@ -1,3 +1,4 @@ +import type { StandardSchemaV1 } from '@standard-schema/spec'; import type { $ZodType } from 'zod/v4/core'; import type { $ZodObject, $ZodShape } from 'zod/v4/core'; import type { @@ -7,6 +8,8 @@ import type { ToolExecutionResult, TurnContext, ToolExecuteContext, + ToolSchema, + InferSchemaOutput, } from './tool-types.js'; import * as z4 from 'zod/v4'; @@ -16,6 +19,16 @@ import { type ToolContextStore, buildToolExecuteContext } from './tool-context.j // Re-export ZodError for convenience export const ZodError = z4.ZodError; +export class StandardSchemaError extends Error { + readonly issues: ReadonlyArray; + + constructor(issues: ReadonlyArray) { + super('Standard Schema validation failed'); + this.name = 'StandardSchemaError'; + this.issues = issues; + } +} + /** * Typeguard to check if a value is a non-null object (not an array). */ @@ -73,6 +86,16 @@ function isZodSchema(value: unknown): value is z4.ZodType { return typeof value._zod === 'object'; } +function isStandardSchema(value: unknown): value is StandardSchemaV1 { + return typeof value === 'object' + && value !== null + && '~standard' in value + && typeof value['~standard'] === 'object' + && value['~standard'] !== null + && 'validate' in value['~standard'] + && typeof value['~standard'].validate === 'function'; +} + /** * Convert a Zod schema to JSON Schema using Zod v4's toJSONSchema function. * Accepts ZodType from the main zod package for user compatibility. @@ -97,38 +120,84 @@ export function convertZodToJsonSchema(zodSchema: $ZodType): Record ({ - type: 'function' as const, - name: tool.function.name, - description: tool.function.description || null, - strict: null, - parameters: convertZodToJsonSchema(tool.function.inputSchema), - })); + return tools.map((tool) => { + const parameters = isZodSchema(tool.function.inputSchema) + ? convertZodToJsonSchema(tool.function.inputSchema) + : tool.function.inputJsonSchema; + + if (!parameters) { + throw new Error( + `Tool "${tool.function.name}" must provide inputJsonSchema when inputSchema is not a Zod schema`, + ); + } + + return { + type: 'function' as const, + name: tool.function.name, + description: tool.function.description || null, + strict: null, + parameters: sanitizeJsonSchema(parameters), + }; + }); } /** * Validate tool input against Zod schema * @throws ZodError if validation fails */ -export function validateToolInput(schema: $ZodType, args: unknown): T { - return z4.parse(schema, args); +function validateSchema( + schema: T, + value: unknown, +): InferSchemaOutput | Promise> { + if (isZodSchema(schema)) { + return z4.parse(schema, value) as InferSchemaOutput; + } + if (!isStandardSchema(schema)) { + throw new Error('Invalid tool schema provided'); + } + + return Promise.resolve(schema['~standard'].validate(value)).then((result) => { + if (result.issues) { + throw new StandardSchemaError(result.issues); + } + return result.value as InferSchemaOutput; + }); } -/** - * Validate tool output against Zod schema - * @throws ZodError if validation fails - */ -export function validateToolOutput(schema: $ZodType, result: unknown): T { - return z4.parse(schema, result); +/** Validate tool input against its schema. Zod validation remains synchronous. */ +export function validateToolInput(schema: $ZodType, args: unknown): T; +export function validateToolInput( + schema: T, + args: unknown, +): Promise>; +export function validateToolInput( + schema: T, + args: unknown, +): InferSchemaOutput | Promise> { + return validateSchema(schema, args); } -/** - * Try to validate a value against a Zod schema without throwing - * @returns true if validation succeeds, false otherwise - */ -function tryValidate(schema: $ZodType, value: unknown): boolean { - const result = z4.safeParse(schema, value); - return result.success; +/** Validate tool output against its schema. Zod validation remains synchronous. */ +export function validateToolOutput(schema: $ZodType, result: unknown): T; +export function validateToolOutput( + schema: T, + result: unknown, +): Promise>; +export function validateToolOutput( + schema: T, + result: unknown, +): InferSchemaOutput | Promise> { + return validateSchema(schema, result); +} + +/** Try to validate a value against a schema without throwing. */ +async function tryValidate(schema: ToolSchema, value: unknown): Promise { + try { + await validateSchema(schema, value); + return true; + } catch { + return false; + } } /** @@ -187,7 +256,7 @@ export async function executeRegularTool( } try { - const validatedInput = validateToolInput(tool.function.inputSchema, toolCall.arguments); + const validatedInput = await validateToolInput(tool.function.inputSchema, toolCall.arguments); const executeContext = buildExecuteCtx(tool, context, contextStore, sharedSchema); // Execute tool with context @@ -195,7 +264,7 @@ export async function executeRegularTool( // Validate output if schema is provided if (tool.function.outputSchema) { - const validatedOutput = validateToolOutput(tool.function.outputSchema, result); + const validatedOutput = await validateToolOutput(tool.function.outputSchema, result); return { toolCallId: toolCall.id, @@ -238,7 +307,7 @@ export async function executeGeneratorTool( } try { - const validatedInput = validateToolInput(tool.function.inputSchema, toolCall.arguments); + const validatedInput = await validateToolInput(tool.function.inputSchema, toolCall.arguments); const executeContext = buildExecuteCtx(tool, context, contextStore, sharedSchema); const preliminaryResults: unknown[] = []; @@ -255,14 +324,14 @@ export async function executeGeneratorTool( lastEmittedValue = event; hasEmittedValue = true; - const matchesOutputSchema = tryValidate(tool.function.outputSchema, event); - const matchesEventSchema = tryValidate(tool.function.eventSchema, event); + const matchesOutputSchema = await tryValidate(tool.function.outputSchema, event); + const matchesEventSchema = await tryValidate(tool.function.eventSchema, event); if (matchesOutputSchema && !matchesEventSchema && !hasFinalResult) { - finalResult = validateToolOutput(tool.function.outputSchema, event); + finalResult = await validateToolOutput(tool.function.outputSchema, event); hasFinalResult = true; } else { - const validatedPreliminary = validateToolOutput(tool.function.eventSchema, event); + const validatedPreliminary = await validateToolOutput(tool.function.eventSchema, event); preliminaryResults.push(validatedPreliminary); if (onPreliminaryResult) { onPreliminaryResult(toolCall.id, validatedPreliminary); @@ -273,7 +342,7 @@ export async function executeGeneratorTool( } if (iterResult.value !== undefined) { - finalResult = validateToolOutput(tool.function.outputSchema, iterResult.value); + finalResult = await validateToolOutput(tool.function.outputSchema, iterResult.value); hasFinalResult = true; } @@ -283,7 +352,7 @@ export async function executeGeneratorTool( `Generator tool "${toolCall.name}" completed without emitting any values or returning a result`, ); } - finalResult = validateToolOutput(tool.function.outputSchema, lastEmittedValue); + finalResult = await validateToolOutput(tool.function.outputSchema, lastEmittedValue); } return { @@ -350,9 +419,11 @@ export function formatToolResultForModel(result: ToolExecutionResult): str * Create a user-friendly error message for tool execution errors */ export function formatToolExecutionError(error: Error, toolCall: ParsedToolCall): string { - if (error instanceof ZodError) { + if (error instanceof ZodError || error instanceof StandardSchemaError) { const issues = error.issues.map((issue) => ({ - path: issue.path.join('.'), + path: issue.path + ?.map((segment) => typeof segment === 'object' ? segment.key : segment) + .join('.') ?? '', message: issue.message, })); diff --git a/src/lib/tool-types.ts b/src/lib/tool-types.ts index a6272f310..38358cfdc 100644 --- a/src/lib/tool-types.ts +++ b/src/lib/tool-types.ts @@ -1,8 +1,24 @@ +import type { StandardSchemaV1 } from '@standard-schema/spec'; import type { $ZodObject, $ZodShape, $ZodType, infer as zodInfer } from 'zod/v4/core'; import type * as models from '../models/index.js'; import type { StreamEvents } from '../models/index.js'; import type { ModelResult } from './model-result.js'; +export type ToolInputSchema = $ZodObject<$ZodShape> | StandardSchemaV1; +export type ToolSchema = $ZodType | StandardSchemaV1; + +export type InferSchemaInput = T extends $ZodType + ? zodInfer + : T extends StandardSchemaV1 + ? StandardSchemaV1.InferInput + : never; + +export type InferSchemaOutput = T extends $ZodType + ? zodInfer + : T extends StandardSchemaV1 + ? StandardSchemaV1.InferOutput + : never; + /** * Tool type enum for enhanced tools */ @@ -136,20 +152,22 @@ export type ToolApprovalCheck = ( /** * Base tool function interface with inputSchema - * @template TInput - Zod schema for tool input + * @template TInput - Schema for tool input */ -export interface BaseToolFunction> { +export interface BaseToolFunction { name: string; description?: string; inputSchema: TInput; + /** Raw JSON Schema sent to providers for non-Zod input schemas. */ + inputJsonSchema?: Record; /** Zod schema declaring the context data this tool needs */ contextSchema?: $ZodObject<$ZodShape>; - nextTurnParams?: NextTurnParamsFunctions>; + nextTurnParams?: NextTurnParamsFunctions>; /** * Whether this tool requires human approval before execution * Can be a boolean or an async function that receives the tool's input params and context */ - requireApproval?: boolean | ToolApprovalCheck>; + requireApproval?: boolean | ToolApprovalCheck>; } /** @@ -158,16 +176,16 @@ export interface BaseToolFunction> { * @template TName - The tool's literal name string */ export interface ToolFunctionWithExecute< - TInput extends $ZodObject<$ZodShape>, - TOutput extends $ZodType = $ZodType, + TInput extends ToolInputSchema, + TOutput extends ToolSchema = $ZodType, TContext extends Record = Record, TName extends string = string, > extends BaseToolFunction { outputSchema?: TOutput; execute: ( - params: zodInfer, + params: InferSchemaOutput, context?: ToolExecuteContext, - ) => Promise> | zodInfer; + ) => Promise> | InferSchemaInput; } /** @@ -193,26 +211,29 @@ export interface ToolFunctionWithExecute< * ``` */ export interface ToolFunctionWithGenerator< - TInput extends $ZodObject<$ZodShape>, - TEvent extends $ZodType = $ZodType, - TOutput extends $ZodType = $ZodType, + TInput extends ToolInputSchema, + TEvent extends ToolSchema = $ZodType, + TOutput extends ToolSchema = $ZodType, TContext extends Record = Record, TName extends string = string, > extends BaseToolFunction { eventSchema: TEvent; outputSchema: TOutput; execute: ( - params: zodInfer, + params: InferSchemaOutput, context?: ToolExecuteContext, - ) => AsyncGenerator | zodInfer, zodInfer | void>; + ) => AsyncGenerator< + InferSchemaInput | InferSchemaInput, + InferSchemaInput | void + >; } /** * Manual tool without execute function - requires manual handling by developer */ export interface ManualToolFunction< - TInput extends $ZodObject<$ZodShape>, - TOutput extends $ZodType = $ZodType, + TInput extends ToolInputSchema, + TOutput extends ToolSchema = $ZodType, > extends BaseToolFunction { outputSchema?: TOutput; } @@ -221,8 +242,8 @@ export interface ManualToolFunction< * Tool with execute function (regular or generator) */ export type ToolWithExecute< - TInput extends $ZodObject<$ZodShape> = $ZodObject<$ZodShape>, - TOutput extends $ZodType = $ZodType, + TInput extends ToolInputSchema = $ZodObject<$ZodShape>, + TOutput extends ToolSchema = $ZodType, TContext extends Record = Record, > = { type: ToolType.Function; @@ -233,9 +254,9 @@ export type ToolWithExecute< * Tool with generator execute function */ export type ToolWithGenerator< - TInput extends $ZodObject<$ZodShape> = $ZodObject<$ZodShape>, - TEvent extends $ZodType = $ZodType, - TOutput extends $ZodType = $ZodType, + TInput extends ToolInputSchema = $ZodObject<$ZodShape>, + TEvent extends ToolSchema = $ZodType, + TOutput extends ToolSchema = $ZodType, TContext extends Record = Record, > = { type: ToolType.Function; @@ -246,8 +267,8 @@ export type ToolWithGenerator< * Tool without execute function (manual handling) */ export type ManualTool< - TInput extends $ZodObject<$ZodShape> = $ZodObject<$ZodShape>, - TOutput extends $ZodType = $ZodType, + TInput extends ToolInputSchema = $ZodObject<$ZodShape>, + TOutput extends ToolSchema = $ZodType, > = { type: ToolType.Function; function: ManualToolFunction; @@ -257,26 +278,22 @@ export type ManualTool< * Union type of all enhanced tool types */ export type Tool = - | ToolWithExecute<$ZodObject<$ZodShape>, $ZodType> - | ToolWithGenerator<$ZodObject<$ZodShape>, $ZodType, $ZodType> - | ManualTool<$ZodObject<$ZodShape>, $ZodType>; + | ToolWithExecute + | ToolWithGenerator + | ManualTool; /** * Extracts the input type from a tool definition */ -export type InferToolInput = T extends { function: { inputSchema: infer S } } - ? S extends $ZodType - ? zodInfer - : unknown +export type InferToolInput = T extends { function: { inputSchema: infer S extends ToolSchema } } + ? InferSchemaOutput : unknown; /** * Extracts the output type from a tool definition */ -export type InferToolOutput = T extends { function: { outputSchema: infer S } } - ? S extends $ZodType - ? zodInfer - : unknown +export type InferToolOutput = T extends { function: { outputSchema: infer S extends ToolSchema } } + ? InferSchemaOutput : unknown; /** @@ -314,10 +331,8 @@ export type InferToolOutputsUnion = { * Extracts the event type from a generator tool definition * Returns `never` for non-generator tools */ -export type InferToolEvent = T extends { function: { eventSchema: infer S } } - ? S extends $ZodType - ? zodInfer - : never +export type InferToolEvent = T extends { function: { eventSchema: infer S extends ToolSchema } } + ? InferSchemaOutput : never; /** @@ -375,12 +390,8 @@ export interface ParsedToolCall { export interface ToolExecutionResult { toolCallId: string; toolName: string; - result: T extends ToolWithExecute<$ZodObject<$ZodShape>, infer O> | ToolWithGenerator<$ZodObject<$ZodShape>, $ZodType, infer O> - ? zodInfer - : unknown; // Final result (sent to model) - preliminaryResults?: T extends ToolWithGenerator<$ZodObject<$ZodShape>, infer E> - ? zodInfer[] - : undefined; // All yielded values from generator + result: InferToolOutput; // Final result (sent to model) + preliminaryResults?: InferToolEvent[]; // All yielded values from generator error?: Error; } diff --git a/src/lib/tool.ts b/src/lib/tool.ts index afea78b4b..9fe242f78 100644 --- a/src/lib/tool.ts +++ b/src/lib/tool.ts @@ -1,4 +1,4 @@ -import type { $ZodObject, $ZodShape, $ZodType, infer as zodInfer } from 'zod/v4/core'; +import type { $ZodObject, $ZodShape, $ZodType } from 'zod/v4/core'; import { ToolType, SHARED_CONTEXT_KEY, @@ -9,54 +9,61 @@ import { type ManualTool, type NextTurnParamsFunctions, type ToolApprovalCheck, + type ToolInputSchema, + type ToolSchema, + type InferSchemaInput, + type InferSchemaOutput, } from "./tool-types.js"; //#region Config Types +type InputSchemaConfig = + TInput extends $ZodObject<$ZodShape> + ? { inputSchema: TInput; inputJsonSchema?: Record } + : { inputSchema: TInput; inputJsonSchema: Record }; + /** * Configuration for a regular tool with outputSchema */ type RegularToolConfigWithOutput< - TInput extends $ZodObject<$ZodShape>, - TOutput extends $ZodType, + TInput extends ToolInputSchema, + TOutput extends ToolSchema, TContext extends Record = Record, TName extends string = string, -> = { +> = InputSchemaConfig & { name: TName; description?: string; - inputSchema: TInput; outputSchema: TOutput; eventSchema?: undefined; /** Zod schema declaring the context data this tool needs */ contextSchema?: $ZodObject<$ZodShape>; - nextTurnParams?: NextTurnParamsFunctions>; - requireApproval?: boolean | ToolApprovalCheck>; + nextTurnParams?: NextTurnParamsFunctions>; + requireApproval?: boolean | ToolApprovalCheck>; execute: ( - params: zodInfer, + params: InferSchemaOutput, context?: ToolExecuteContext - ) => Promise> | zodInfer; + ) => Promise> | InferSchemaInput; }; /** * Configuration for a regular tool without outputSchema (infers return type from execute) */ type RegularToolConfigWithoutOutput< - TInput extends $ZodObject<$ZodShape>, + TInput extends ToolInputSchema, TReturn, TContext extends Record = Record, TName extends string = string, -> = { +> = InputSchemaConfig & { name: TName; description?: string; - inputSchema: TInput; outputSchema?: undefined; eventSchema?: undefined; /** Zod schema declaring the context data this tool needs */ contextSchema?: $ZodObject<$ZodShape>; - nextTurnParams?: NextTurnParamsFunctions>; - requireApproval?: boolean | ToolApprovalCheck>; + nextTurnParams?: NextTurnParamsFunctions>; + requireApproval?: boolean | ToolApprovalCheck>; execute: ( - params: zodInfer, + params: InferSchemaOutput, context?: ToolExecuteContext ) => Promise | TReturn; }; @@ -65,40 +72,38 @@ type RegularToolConfigWithoutOutput< * Configuration for a generator tool (with eventSchema) */ type GeneratorToolConfig< - TInput extends $ZodObject<$ZodShape>, - TEvent extends $ZodType, - TOutput extends $ZodType, + TInput extends ToolInputSchema, + TEvent extends ToolSchema, + TOutput extends ToolSchema, TContext extends Record = Record, TName extends string = string, -> = { +> = InputSchemaConfig & { name: TName; description?: string; - inputSchema: TInput; eventSchema: TEvent; outputSchema: TOutput; /** Zod schema declaring the context data this tool needs */ contextSchema?: $ZodObject<$ZodShape>; - nextTurnParams?: NextTurnParamsFunctions>; - requireApproval?: boolean | ToolApprovalCheck>; + nextTurnParams?: NextTurnParamsFunctions>; + requireApproval?: boolean | ToolApprovalCheck>; execute: ( - params: zodInfer, + params: InferSchemaOutput, context?: ToolExecuteContext - ) => AsyncGenerator | zodInfer>; + ) => AsyncGenerator | InferSchemaInput>; }; /** * Configuration for a manual tool (execute: false, no eventSchema or outputSchema) */ type ManualToolConfig< - TInput extends $ZodObject<$ZodShape>, -> = { + TInput extends ToolInputSchema, +> = InputSchemaConfig & { name: string; // Manual tools don't use TName since they have no execute description?: string; - inputSchema: TInput; /** Zod schema declaring the context data this tool needs */ contextSchema?: $ZodObject<$ZodShape>; - nextTurnParams?: NextTurnParamsFunctions>; - requireApproval?: boolean | ToolApprovalCheck>; + nextTurnParams?: NextTurnParamsFunctions>; + requireApproval?: boolean | ToolApprovalCheck>; execute: false; }; @@ -109,9 +114,10 @@ type ManualToolConfig< type ToolConfigWithSharedContext> = { name: string; description?: string; - inputSchema: $ZodObject<$ZodShape>; - outputSchema?: $ZodType; - eventSchema?: $ZodType; + inputSchema: ToolInputSchema; + inputJsonSchema?: Record; + outputSchema?: ToolSchema; + eventSchema?: ToolSchema; contextSchema?: $ZodObject<$ZodShape>; nextTurnParams?: NextTurnParamsFunctions>; requireApproval?: boolean | ToolApprovalCheck>; @@ -135,8 +141,8 @@ type ToolConfigWithSharedContext> = { * Union type for all regular tool configs */ type RegularToolConfig< - TInput extends $ZodObject<$ZodShape>, - TOutput extends $ZodType, + TInput extends ToolInputSchema, + TOutput extends ToolSchema, TReturn, TContext extends Record = Record, TName extends string = string, @@ -149,7 +155,7 @@ type RegularToolConfig< //#region tool() Factory /** - * Creates a tool with full type inference from Zod schemas. + * Creates a tool with full type inference from Zod or Standard Schema validators. * * The tool type is automatically determined based on the configuration: * - **Generator tool**: When `eventSchema` is provided @@ -176,9 +182,9 @@ type RegularToolConfig< */ // Overload for generator tools (when eventSchema is provided) export function tool< - TInput extends $ZodObject<$ZodShape>, - TEvent extends $ZodType, - TOutput extends $ZodType, + TInput extends ToolInputSchema, + TEvent extends ToolSchema, + TOutput extends ToolSchema, TContext extends Record = Record, TName extends string = string, >( @@ -186,21 +192,21 @@ export function tool< ): ToolWithGenerator; // Overload for manual tools (execute: false) -export function tool>( +export function tool( config: ManualToolConfig ): ManualTool; // Overload for regular tools with outputSchema export function tool< - TInput extends $ZodObject<$ZodShape>, - TOutput extends $ZodType, + TInput extends ToolInputSchema, + TOutput extends ToolSchema, TContext extends Record = Record, TName extends string = string, >(config: RegularToolConfigWithOutput): ToolWithExecute; // Overload for regular tools without outputSchema (infers return type) export function tool< - TInput extends $ZodObject<$ZodShape>, + TInput extends ToolInputSchema, TReturn, TContext extends Record = Record, TName extends string = string, @@ -217,9 +223,9 @@ export function tool>( // Implementation export function tool( config: - | GeneratorToolConfig<$ZodObject<$ZodShape>, $ZodType, $ZodType> - | RegularToolConfig<$ZodObject<$ZodShape>, $ZodType, unknown> - | ManualToolConfig<$ZodObject<$ZodShape>> + | GeneratorToolConfig + | RegularToolConfig + | ManualToolConfig | ToolConfigWithSharedContext> ): Tool { // 'shared' is reserved for shared context — forbid it as a tool name @@ -231,11 +237,15 @@ export function tool( // Check for manual tool first (execute === false) if (config.execute === false) { - const fn: ManualTool<$ZodObject<$ZodShape>>["function"] = { + const fn: ManualTool["function"] = { name: config.name, inputSchema: config.inputSchema, }; + if (config.inputJsonSchema !== undefined) { + fn.inputJsonSchema = config.inputJsonSchema; + } + if (config.description !== undefined) { fn.description = config.description; } @@ -245,11 +255,11 @@ export function tool( } if (config.nextTurnParams !== undefined) { - fn.nextTurnParams = config.nextTurnParams; + fn.nextTurnParams = config.nextTurnParams as NextTurnParamsFunctions; } if (config.requireApproval !== undefined) { - fn.requireApproval = config.requireApproval; + fn.requireApproval = config.requireApproval as boolean | ToolApprovalCheck; } return { @@ -266,7 +276,11 @@ export function tool( eventSchema: config.eventSchema, outputSchema: config.outputSchema, execute: config.execute, - } as ToolWithGenerator<$ZodObject<$ZodShape>, $ZodType, $ZodType>["function"]; + } as ToolWithGenerator["function"]; + + if (config.inputJsonSchema !== undefined) { + fn.inputJsonSchema = config.inputJsonSchema; + } if (config.description !== undefined) { fn.description = config.description; @@ -277,11 +291,11 @@ export function tool( } if (config.nextTurnParams !== undefined) { - fn.nextTurnParams = config.nextTurnParams; + fn.nextTurnParams = config.nextTurnParams as NextTurnParamsFunctions; } if (config.requireApproval !== undefined) { - fn.requireApproval = config.requireApproval; + fn.requireApproval = config.requireApproval as boolean | ToolApprovalCheck; } return { @@ -296,6 +310,7 @@ export function tool( inputSchema: config.inputSchema, execute: config.execute, ...(config.description !== undefined && { description: config.description }), + ...(config.inputJsonSchema !== undefined && { inputJsonSchema: config.inputJsonSchema }), ...(config.outputSchema !== undefined && { outputSchema: config.outputSchema }), ...(config.contextSchema !== undefined && { contextSchema: config.contextSchema }), ...(config.nextTurnParams !== undefined && { nextTurnParams: config.nextTurnParams }), @@ -304,7 +319,7 @@ export function tool( return { type: ToolType.Function, - function: functionObj, + function: functionObj as ToolWithExecute["function"], }; } diff --git a/tests/unit/standard-schema-tools.test.ts b/tests/unit/standard-schema-tools.test.ts new file mode 100644 index 000000000..ca872d636 --- /dev/null +++ b/tests/unit/standard-schema-tools.test.ts @@ -0,0 +1,195 @@ +import type { StandardSchemaV1 } from '@standard-schema/spec'; +import { toJsonSchema } from '@valibot/to-json-schema'; +import { describe, expect, expectTypeOf, it } from 'vitest'; +import * as v from 'valibot'; +import { z } from 'zod/v4'; +import { + StandardSchemaError, + convertToolsToAPIFormat, + executeGeneratorTool, + executeRegularTool, + formatToolExecutionError, + validateToolInput, + validateToolOutput, +} from '../../src/lib/tool-executor.js'; +import { tool } from '../../src/lib/tool.js'; +import type { InferToolEvent, InferToolInput, InferToolOutput } from '../../src/lib/tool-types.js'; +import { assertNoTildeKeys } from '../utils/schema-test-helpers.js'; + +const inputSchema = v.object({ + name: v.string(), + count: v.number(), +}); +const outputSchema = v.object({ message: v.string() }); +const inputJsonSchema = toJsonSchema(inputSchema) as Record; + +const valibotTool = tool({ + name: 'valibot_tool' as const, + inputSchema, + inputJsonSchema, + outputSchema, + execute: ({ name, count }) => ({ message: name.repeat(count) }), +}); + +describe('Standard Schema tools', () => { + it('infers Standard Schema input and output types', () => { + expectTypeOf>().toEqualTypeOf<{ + name: string; + count: number; + }>(); + expectTypeOf(valibotTool.function.name).toEqualTypeOf<'valibot_tool'>(); + expectTypeOf>().toEqualTypeOf<{ + message: string; + }>(); + expectTypeOf(valibotTool.function.execute).parameter(0).toEqualTypeOf<{ + name: string; + count: number; + }>(); + }); + + it('keeps the Zod path working without an explicit JSON Schema', async () => { + const zodTool = tool({ + name: 'zod_tool', + inputSchema: z.object({ value: z.string() }), + outputSchema: z.object({ length: z.number() }), + execute: ({ value }) => ({ length: value.length }), + }); + + expect(convertToolsToAPIFormat([zodTool])[0]?.parameters).toMatchObject({ + type: 'object', + required: ['value'], + }); + expect(validateToolInput(zodTool.function.inputSchema, { value: 'test' })).toEqual({ + value: 'test', + }); + expect(validateToolOutput(zodTool.function.outputSchema, { length: 4 })).toEqual({ + length: 4, + }); + + const result = await executeRegularTool( + zodTool, + { id: 'zod-call', name: 'zod_tool', arguments: { value: 'test' } }, + { numberOfTurns: 1 }, + ); + expect(result).toMatchObject({ result: { length: 4 } }); + expect(result.error).toBeUndefined(); + }); + + it('validates input and output through Valibot', async () => { + const result = await executeRegularTool( + valibotTool, + { id: 'valid-call', name: 'valibot_tool', arguments: { name: 'hi', count: 2 } }, + { numberOfTurns: 1 }, + ); + + expect(result).toMatchObject({ result: { message: 'hihi' } }); + expect(result.error).toBeUndefined(); + + const invalidInput = await executeRegularTool( + valibotTool, + { id: 'invalid-input', name: 'valibot_tool', arguments: { name: 'hi', count: 'two' } }, + { numberOfTurns: 1 }, + ); + expect(invalidInput.error).toBeInstanceOf(StandardSchemaError); + + const invalidOutputTool = tool({ + name: 'invalid_output', + inputSchema: v.object({}), + inputJsonSchema: toJsonSchema(v.object({})) as Record, + outputSchema, + execute: () => ({ message: 42 } as never), + }); + const invalidOutput = await executeRegularTool( + invalidOutputTool, + { id: 'invalid-output', name: 'invalid_output', arguments: {} }, + { numberOfTurns: 1 }, + ); + expect(invalidOutput.error).toBeInstanceOf(StandardSchemaError); + }); + + it('uses and sanitizes the explicit JSON Schema for providers', () => { + const apiTool = convertToolsToAPIFormat([ + tool({ + name: 'manual_standard_schema', + inputSchema, + inputJsonSchema: { + ...inputJsonSchema, + '~standard': { vendor: 'test' }, + properties: { + ...(inputJsonSchema['properties'] as Record), + hidden: { type: 'string', '~metadata': true }, + }, + }, + execute: false, + }), + ])[0]; + + expect(apiTool?.parameters).toMatchObject({ type: 'object' }); + assertNoTildeKeys(apiTool?.parameters); + }); + + it('requires explicit JSON Schema for non-Zod validators at runtime', () => { + const manuallyConstructedTool = { + type: 'function' as const, + function: { + name: 'missing_json_schema', + inputSchema, + }, + }; + + expect(() => convertToolsToAPIFormat([manuallyConstructedTool])).toThrow( + 'must provide inputJsonSchema when inputSchema is not a Zod schema', + ); + }); + + it('awaits asynchronous Standard Schema validators and maps their issues', async () => { + const asyncSchema: StandardSchemaV1 = { + '~standard': { + version: 1, + vendor: 'async-test', + validate: async (value) => { + await Promise.resolve(); + return value === 'valid' + ? { value } + : { issues: [{ message: 'Expected valid', path: [{ key: 'value' }] }] }; + }, + }, + }; + + await expect(validateToolInput(asyncSchema, 'valid')).resolves.toBe('valid'); + const error = await validateToolInput(asyncSchema, 'invalid').catch((caught) => caught); + expect(error).toBeInstanceOf(StandardSchemaError); + expect(formatToolExecutionError(error as Error, { + id: 'async-call', + name: 'async_tool', + arguments: 'invalid', + })).toContain('"path": "value"'); + }); + + it('validates generator events and output with Standard Schema', async () => { + const eventSchema = v.object({ progress: v.number() }); + const finalSchema = v.object({ result: v.string() }); + const generatorTool = tool({ + name: 'standard_generator', + inputSchema: v.object({ query: v.string() }), + inputJsonSchema: toJsonSchema(v.object({ query: v.string() })) as Record, + eventSchema, + outputSchema: finalSchema, + execute: async function* ({ query }) { + yield { progress: 50 }; + yield { result: query }; + }, + }); + + expectTypeOf>().toEqualTypeOf<{ progress: number }>(); + + const result = await executeGeneratorTool( + generatorTool, + { id: 'generator-call', name: 'standard_generator', arguments: { query: 'done' } }, + { numberOfTurns: 1 }, + ); + expect(result.result).toEqual({ result: 'done' }); + expect(result.preliminaryResults).toEqual([{ progress: 50 }]); + expect(result.error).toBeUndefined(); + }); +}); From 881ffe416a0e22483549fbe7e309f22627b6fd9f Mon Sep 17 00:00:00 2001 From: Luke Parke <5702154+LukasParke@users.noreply.github.com> Date: Tue, 11 Aug 2026 15:28:58 -0500 Subject: [PATCH 2/5] fix: normalize Standard Schema validation errors --- src/lib/tool-executor.ts | 49 +++++++++++++++++------- tests/unit/standard-schema-tools.test.ts | 20 ++++++++++ 2 files changed, 55 insertions(+), 14 deletions(-) diff --git a/src/lib/tool-executor.ts b/src/lib/tool-executor.ts index d4de824ca..d66f4ff5f 100644 --- a/src/lib/tool-executor.ts +++ b/src/lib/tool-executor.ts @@ -19,13 +19,26 @@ import { type ToolContextStore, buildToolExecuteContext } from './tool-context.j // Re-export ZodError for convenience export const ZodError = z4.ZodError; +export interface StandardSchemaIssue { + readonly message: string; + readonly path: PropertyKey[]; +} + export class StandardSchemaError extends Error { - readonly issues: ReadonlyArray; + readonly issues: StandardSchemaIssue[]; constructor(issues: ReadonlyArray) { - super('Standard Schema validation failed'); + const normalized = issues.map((issue) => ({ + message: issue.message, + path: (issue.path ?? []).map((segment) => + typeof segment === 'object' && segment !== null && 'key' in segment + ? segment.key + : segment, + ), + })); + super(JSON.stringify(normalized)); this.name = 'StandardSchemaError'; - this.issues = issues; + this.issues = normalized; } } @@ -87,13 +100,14 @@ function isZodSchema(value: unknown): value is z4.ZodType { } function isStandardSchema(value: unknown): value is StandardSchemaV1 { - return typeof value === 'object' - && value !== null - && '~standard' in value - && typeof value['~standard'] === 'object' - && value['~standard'] !== null - && 'validate' in value['~standard'] - && typeof value['~standard'].validate === 'function'; + if (typeof value !== 'object' || value === null || !('~standard' in value)) { + return false; + } + const standard = value['~standard'] as { + version?: unknown; + validate?: unknown; + }; + return standard.version === 1 && typeof standard.validate === 'function'; } /** @@ -419,11 +433,18 @@ export function formatToolResultForModel(result: ToolExecutionResult): str * Create a user-friendly error message for tool execution errors */ export function formatToolExecutionError(error: Error, toolCall: ParsedToolCall): string { - if (error instanceof ZodError || error instanceof StandardSchemaError) { + if (error instanceof ZodError) { + const issues = error.issues.map((issue) => ({ + path: issue.path.join('.'), + message: issue.message, + })); + + return `Tool "${toolCall.name}" validation error:\n${JSON.stringify(issues, null, 2)}`; + } + + if (error instanceof StandardSchemaError) { const issues = error.issues.map((issue) => ({ - path: issue.path - ?.map((segment) => typeof segment === 'object' ? segment.key : segment) - .join('.') ?? '', + path: issue.path.join('.'), message: issue.message, })); diff --git a/tests/unit/standard-schema-tools.test.ts b/tests/unit/standard-schema-tools.test.ts index ca872d636..235603bac 100644 --- a/tests/unit/standard-schema-tools.test.ts +++ b/tests/unit/standard-schema-tools.test.ts @@ -159,6 +159,12 @@ describe('Standard Schema tools', () => { await expect(validateToolInput(asyncSchema, 'valid')).resolves.toBe('valid'); const error = await validateToolInput(asyncSchema, 'invalid').catch((caught) => caught); expect(error).toBeInstanceOf(StandardSchemaError); + expect((error as StandardSchemaError).issues).toEqual([ + { message: 'Expected valid', path: ['value'] }, + ]); + expect((error as Error).message).toBe( + '[{"message":"Expected valid","path":["value"]}]', + ); expect(formatToolExecutionError(error as Error, { id: 'async-call', name: 'async_tool', @@ -166,6 +172,20 @@ describe('Standard Schema tools', () => { })).toContain('"path": "value"'); }); + it('rejects validators that do not declare Standard Schema v1', async () => { + const invalidVersionSchema = { + '~standard': { + version: 2, + vendor: 'future-test', + validate: (value: unknown) => ({ value }), + }, + } as unknown as StandardSchemaV1; + + expect(() => validateToolInput(invalidVersionSchema, 'value')).toThrow( + 'Invalid tool schema provided', + ); + }); + it('validates generator events and output with Standard Schema', async () => { const eventSchema = v.object({ progress: v.number() }); const finalSchema = v.object({ result: v.string() }); From 213898d0decd3d1e35faae0211c22596f955b467 Mon Sep 17 00:00:00 2001 From: Luke Parke <5702154+LukasParke@users.noreply.github.com> Date: Tue, 11 Aug 2026 16:02:27 -0500 Subject: [PATCH 3/5] feat: support Standard JSON Schema conversion --- CLAUDE.md | 2 +- README.md | 2 +- .../call-model-typed-tool-calling.example.ts | 10 +-- src/lib/tool-executor.ts | 33 ++++++++-- src/lib/tool-types.ts | 2 +- src/lib/tool.ts | 3 +- tests/unit/standard-schema-tools.test.ts | 66 ++++++++++++++++++- 7 files changed, 101 insertions(+), 17 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 9f0e9c7ec..0df9d154d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -127,7 +127,7 @@ Speakeasy's [persistent edits](https://www.speakeasy.com/docs/sdks/customize/cod - Uses `ReusableReadableStream` to enable multiple parallel consumers **Tool System** (`src/lib/tool.ts`, `src/lib/tool-types.ts`, `src/lib/tool-executor.ts`) -- `tool()` accepts Zod or Standard Schema v1 validators; non-Zod inputs also require `inputJsonSchema` for provider requests +- `tool()` accepts Zod or Standard Schema v1 validators; provider schemas use Zod conversion, the Standard JSON Schema trait, or explicit `inputJsonSchema` - Three tool types: - **Regular tools** (`execute: function`) - auto-executed, return final result - **Generator tools** (`execute: async generator`) - stream preliminary results diff --git a/README.md b/README.md index 7ab06f57d..a49231312 100644 --- a/README.md +++ b/README.md @@ -49,7 +49,7 @@ yarn add @openrouter/sdk > [!IMPORTANT] > `callModel` and its associated types have moved to the [`@openrouter/agent`](https://www.npmjs.com/package/@openrouter/agent) package. If you are using `callModel`, tool definitions, or related types from `@openrouter/sdk`, you should migrate to `@openrouter/agent`. > -> The legacy SDK tool API accepts Zod schemas or any [Standard Schema v1](https://standardschema.dev/) validator for `inputSchema`, `outputSchema`, and `eventSchema`. Because Standard Schema does not define JSON Schema generation, non-Zod input validators must also provide `inputJsonSchema`, which is sent to model providers after `~`-prefixed metadata is removed. Zod input schemas continue to use the built-in `z4.toJSONSchema` path. +> The legacy SDK tool API accepts Zod schemas or any [Standard Schema v1](https://standardschema.dev/) validator for `inputSchema`, `outputSchema`, and `eventSchema`. Provider JSON Schema uses three tiers: Zod schemas use the built-in `z4.toJSONSchema` path; non-Zod schemas implementing [Standard JSON Schema v1](https://standardschema.dev/json-schema) use their `~standard.jsonSchema.input` converter; otherwise callers provide `inputJsonSchema`. An explicit `inputJsonSchema` overrides the Standard JSON Schema trait, and all paths remove `~`-prefixed metadata. The trait is available in Zod 4.2+, ArkType 2.1.28+, Zod Mini, VineJS, and Sury; Valibot adds it with `toStandardJsonSchema()` from `@valibot/to-json-schema`. > > To assist with the migration, run: > diff --git a/examples/call-model-typed-tool-calling.example.ts b/examples/call-model-typed-tool-calling.example.ts index a5a9faafd..9f7740b36 100644 --- a/examples/call-model-typed-tool-calling.example.ts +++ b/examples/call-model-typed-tool-calling.example.ts @@ -17,7 +17,7 @@ import dotenv from "dotenv"; dotenv.config(); -import { toJsonSchema } from "@valibot/to-json-schema"; +import { toStandardJsonSchema } from "@valibot/to-json-schema"; import * as v from "valibot"; import { OpenRouter } from "../src/index.js"; import { tool } from "../src/lib/tool.js"; @@ -30,15 +30,15 @@ const openRouter = new OpenRouter({ // Create a typed regular tool using tool() // The execute function params are automatically typed as z.infer // The return type is enforced based on outputSchema -const weatherInputSchema = v.object({ location: v.string() }); +const weatherInputSchema = toStandardJsonSchema( + v.object({ location: v.string() }), +); const weatherTool = tool({ name: "get_weather", description: "Get the current weather for a location", + // This wrapper implements validation plus Standard JSON Schema conversion. inputSchema: weatherInputSchema, - // Standard Schema does not define JSON Schema conversion, so non-Zod - // validators provide the schema sent to model providers explicitly. - inputJsonSchema: toJsonSchema(weatherInputSchema), outputSchema: v.object({ temperature: v.number(), description: v.string(), diff --git a/src/lib/tool-executor.ts b/src/lib/tool-executor.ts index d66f4ff5f..f26089b0b 100644 --- a/src/lib/tool-executor.ts +++ b/src/lib/tool-executor.ts @@ -1,4 +1,4 @@ -import type { StandardSchemaV1 } from '@standard-schema/spec'; +import type { StandardJSONSchemaV1, StandardSchemaV1 } from '@standard-schema/spec'; import type { $ZodType } from 'zod/v4/core'; import type { $ZodObject, $ZodShape } from 'zod/v4/core'; import type { @@ -110,6 +110,17 @@ function isStandardSchema(value: unknown): value is StandardSchemaV1 { return standard.version === 1 && typeof standard.validate === 'function'; } +function isStandardJsonSchema(value: unknown): value is StandardJSONSchemaV1 { + if (typeof value !== 'object' || value === null || !('~standard' in value)) { + return false; + } + const standard = value['~standard'] as { + version?: unknown; + jsonSchema?: { input?: unknown }; + }; + return standard.version === 1 && typeof standard.jsonSchema?.input === 'function'; +} + /** * Convert a Zod schema to JSON Schema using Zod v4's toJSONSchema function. * Accepts ZodType from the main zod package for user compatibility. @@ -135,13 +146,25 @@ export function convertZodToJsonSchema(zodSchema: $ZodType): Record { - const parameters = isZodSchema(tool.function.inputSchema) - ? convertZodToJsonSchema(tool.function.inputSchema) - : tool.function.inputJsonSchema; + let parameters: Record | undefined; + + if (isZodSchema(tool.function.inputSchema)) { + parameters = convertZodToJsonSchema(tool.function.inputSchema); + } else if (tool.function.inputJsonSchema) { + parameters = tool.function.inputJsonSchema; + } else if (isStandardJsonSchema(tool.function.inputSchema)) { + try { + parameters = tool.function.inputSchema['~standard'].jsonSchema.input({ + target: 'draft-07', + }); + } catch { + // Fall through to the explicit-schema requirement below. + } + } if (!parameters) { throw new Error( - `Tool "${tool.function.name}" must provide inputJsonSchema when inputSchema is not a Zod schema`, + `Tool "${tool.function.name}" inputSchema must implement StandardJSONSchemaV1 or provide inputJsonSchema`, ); } diff --git a/src/lib/tool-types.ts b/src/lib/tool-types.ts index 38358cfdc..c3d6ecbae 100644 --- a/src/lib/tool-types.ts +++ b/src/lib/tool-types.ts @@ -158,7 +158,7 @@ export interface BaseToolFunction { name: string; description?: string; inputSchema: TInput; - /** Raw JSON Schema sent to providers for non-Zod input schemas. */ + /** Explicit JSON Schema override or fallback for non-Zod input schemas. */ inputJsonSchema?: Record; /** Zod schema declaring the context data this tool needs */ contextSchema?: $ZodObject<$ZodShape>; diff --git a/src/lib/tool.ts b/src/lib/tool.ts index 9fe242f78..3a6161cbf 100644 --- a/src/lib/tool.ts +++ b/src/lib/tool.ts @@ -1,3 +1,4 @@ +import type { StandardJSONSchemaV1 } from '@standard-schema/spec'; import type { $ZodObject, $ZodShape, $ZodType } from 'zod/v4/core'; import { ToolType, @@ -18,7 +19,7 @@ import { //#region Config Types type InputSchemaConfig = - TInput extends $ZodObject<$ZodShape> + TInput extends $ZodObject<$ZodShape> | StandardJSONSchemaV1 ? { inputSchema: TInput; inputJsonSchema?: Record } : { inputSchema: TInput; inputJsonSchema: Record }; diff --git a/tests/unit/standard-schema-tools.test.ts b/tests/unit/standard-schema-tools.test.ts index 235603bac..bcebeb38c 100644 --- a/tests/unit/standard-schema-tools.test.ts +++ b/tests/unit/standard-schema-tools.test.ts @@ -1,5 +1,5 @@ import type { StandardSchemaV1 } from '@standard-schema/spec'; -import { toJsonSchema } from '@valibot/to-json-schema'; +import { toJsonSchema, toStandardJsonSchema } from '@valibot/to-json-schema'; import { describe, expect, expectTypeOf, it } from 'vitest'; import * as v from 'valibot'; import { z } from 'zod/v4'; @@ -107,6 +107,23 @@ describe('Standard Schema tools', () => { expect(invalidOutput.error).toBeInstanceOf(StandardSchemaError); }); + it('uses the Standard JSON Schema trait without inputJsonSchema', () => { + const schema = toStandardJsonSchema(v.object({ query: v.string() })); + const apiTool = convertToolsToAPIFormat([ + tool({ + name: 'standard_json_schema', + inputSchema: schema, + execute: false, + }), + ])[0]; + + expect(apiTool?.parameters).toMatchObject({ + type: 'object', + required: ['query'], + }); + assertNoTildeKeys(apiTool?.parameters); + }); + it('uses and sanitizes the explicit JSON Schema for providers', () => { const apiTool = convertToolsToAPIFormat([ tool({ @@ -128,7 +145,50 @@ describe('Standard Schema tools', () => { assertNoTildeKeys(apiTool?.parameters); }); - it('requires explicit JSON Schema for non-Zod validators at runtime', () => { + it('uses inputJsonSchema when the Standard JSON Schema converter throws', () => { + const throwingSchema = { + '~standard': { + version: 1 as const, + vendor: 'throwing-test', + validate: (value: unknown) => ({ value }), + jsonSchema: { + input: () => { + throw new Error('Cannot convert'); + }, + output: () => { + throw new Error('Cannot convert'); + }, + }, + }, + }; + + const apiTool = convertToolsToAPIFormat([ + tool({ + name: 'throwing_converter', + inputSchema: throwingSchema, + inputJsonSchema: { type: 'string', description: 'fallback' }, + execute: false, + }), + ])[0]; + + expect(apiTool?.parameters).toEqual({ type: 'string', description: 'fallback' }); + }); + + it('prefers inputJsonSchema over the Standard JSON Schema trait', () => { + const schema = toStandardJsonSchema(v.object({ query: v.string() })); + const apiTool = convertToolsToAPIFormat([ + tool({ + name: 'explicit_override', + inputSchema: schema, + inputJsonSchema: { type: 'integer', description: 'override' }, + execute: false, + }), + ])[0]; + + expect(apiTool?.parameters).toEqual({ type: 'integer', description: 'override' }); + }); + + it('requires Standard JSON Schema or explicit JSON Schema at runtime', () => { const manuallyConstructedTool = { type: 'function' as const, function: { @@ -138,7 +198,7 @@ describe('Standard Schema tools', () => { }; expect(() => convertToolsToAPIFormat([manuallyConstructedTool])).toThrow( - 'must provide inputJsonSchema when inputSchema is not a Zod schema', + 'must implement StandardJSONSchemaV1 or provide inputJsonSchema', ); }); From a3c6108454840f5995844ed447a4cf2ab71f345b Mon Sep 17 00:00:00 2001 From: Luke Parke <5702154+LukasParke@users.noreply.github.com> Date: Tue, 11 Aug 2026 18:19:44 -0500 Subject: [PATCH 4/5] fix: honor explicit inputJsonSchema and harden Standard Schema errors - convertToolsToAPIFormat: check inputJsonSchema before the Zod fast path so the explicit escape hatch always wins - StandardSchemaError: stringify symbol path segments so JSON.stringify and formatToolExecutionError no longer crash or emit null paths - tool() overload: require inputJsonSchema for plain Standard Schema v1 validators via a non-inferred union config (explicit type arguments disable inference for remaining type parameters) Add regression tests for all three. --- src/lib/tool-executor.ts | 23 +++++---- src/lib/tool.ts | 32 +++++++++--- tests/unit/standard-schema-tools.test.ts | 62 ++++++++++++++++++++++++ 3 files changed, 101 insertions(+), 16 deletions(-) diff --git a/src/lib/tool-executor.ts b/src/lib/tool-executor.ts index f26089b0b..eba4446ea 100644 --- a/src/lib/tool-executor.ts +++ b/src/lib/tool-executor.ts @@ -21,7 +21,7 @@ export const ZodError = z4.ZodError; export interface StandardSchemaIssue { readonly message: string; - readonly path: PropertyKey[]; + readonly path: (string | number)[]; } export class StandardSchemaError extends Error { @@ -30,11 +30,14 @@ export class StandardSchemaError extends Error { constructor(issues: ReadonlyArray) { const normalized = issues.map((issue) => ({ message: issue.message, - path: (issue.path ?? []).map((segment) => - typeof segment === 'object' && segment !== null && 'key' in segment - ? segment.key - : segment, - ), + path: (issue.path ?? []).map((segment) => { + const key = + typeof segment === 'object' && segment !== null && 'key' in segment + ? segment.key + : segment; + // Symbols are legal PropertyKeys but break JSON.stringify and join('.') + return typeof key === 'symbol' ? key.toString() : key; + }), })); super(JSON.stringify(normalized)); this.name = 'StandardSchemaError'; @@ -148,10 +151,12 @@ export function convertToolsToAPIFormat(tools: readonly Tool[]): APITool[] { return tools.map((tool) => { let parameters: Record | undefined; - if (isZodSchema(tool.function.inputSchema)) { - parameters = convertZodToJsonSchema(tool.function.inputSchema); - } else if (tool.function.inputJsonSchema) { + // An explicit inputJsonSchema is the escape hatch and always wins, + // including over the Zod fast path. + if (tool.function.inputJsonSchema) { parameters = tool.function.inputJsonSchema; + } else if (isZodSchema(tool.function.inputSchema)) { + parameters = convertZodToJsonSchema(tool.function.inputSchema); } else if (isStandardJsonSchema(tool.function.inputSchema)) { try { parameters = tool.function.inputSchema['~standard'].jsonSchema.input({ diff --git a/src/lib/tool.ts b/src/lib/tool.ts index 3a6161cbf..cabc4c6d9 100644 --- a/src/lib/tool.ts +++ b/src/lib/tool.ts @@ -1,4 +1,4 @@ -import type { StandardJSONSchemaV1 } from '@standard-schema/spec'; +import type { StandardJSONSchemaV1, StandardSchemaV1 } from '@standard-schema/spec'; import type { $ZodObject, $ZodShape, $ZodType } from 'zod/v4/core'; import { ToolType, @@ -18,10 +18,30 @@ import { //#region Config Types -type InputSchemaConfig = +type InputJsonSchemaRequirement = TInput extends $ZodObject<$ZodShape> | StandardJSONSchemaV1 - ? { inputSchema: TInput; inputJsonSchema?: Record } - : { inputSchema: TInput; inputJsonSchema: Record }; + ? { inputJsonSchema?: Record } + : { inputJsonSchema: Record }; + +type InputSchemaConfig = + { inputSchema: TInput } & InputJsonSchemaRequirement; + +/** + * Non-generic equivalent of InputSchemaConfig for contexts where the schema + * type cannot be inferred (e.g. the tool() overload, where explicit + * type arguments disable inference for the remaining parameters). + */ +type LooseInputSchemaConfig = + | { + inputSchema: + | $ZodObject<$ZodShape> + | (StandardSchemaV1 & StandardJSONSchemaV1); + inputJsonSchema?: Record; + } + | { + inputSchema: StandardSchemaV1; + inputJsonSchema: Record; + }; /** * Configuration for a regular tool with outputSchema @@ -115,8 +135,6 @@ type ManualToolConfig< type ToolConfigWithSharedContext> = { name: string; description?: string; - inputSchema: ToolInputSchema; - inputJsonSchema?: Record; outputSchema?: ToolSchema; eventSchema?: ToolSchema; contextSchema?: $ZodObject<$ZodShape>; @@ -132,7 +150,7 @@ type ToolConfigWithSharedContext> = { context?: ToolExecuteContext, TShared>, ) => AsyncGenerator) | false; -}; +} & LooseInputSchemaConfig; //#endregion diff --git a/tests/unit/standard-schema-tools.test.ts b/tests/unit/standard-schema-tools.test.ts index bcebeb38c..dfc32d72d 100644 --- a/tests/unit/standard-schema-tools.test.ts +++ b/tests/unit/standard-schema-tools.test.ts @@ -188,6 +188,68 @@ describe('Standard Schema tools', () => { expect(apiTool?.parameters).toEqual({ type: 'integer', description: 'override' }); }); + it('prefers inputJsonSchema over the Zod fast path', () => { + const apiTool = convertToolsToAPIFormat([ + tool({ + name: 'zod_explicit_override', + inputSchema: z.object({ query: z.string() }), + inputJsonSchema: { type: 'object', description: 'explicit override' }, + execute: false, + }), + ])[0]; + + expect(apiTool?.parameters).toEqual({ type: 'object', description: 'explicit override' }); + }); + + it('handles symbol path segments without crashing error formatting', async () => { + const symbolSchema: StandardSchemaV1 = { + '~standard': { + version: 1, + vendor: 'symbol-test', + validate: () => ({ + issues: [{ message: 'bad symbol', path: [{ key: Symbol('nested') }] }], + }), + }, + }; + + const error = await validateToolInput(symbolSchema, 'x').catch((caught) => caught); + expect(error).toBeInstanceOf(StandardSchemaError); + expect((error as StandardSchemaError).issues).toEqual([ + { message: 'bad symbol', path: ['Symbol(nested)'] }, + ]); + expect((error as Error).message).toBe( + '[{"message":"bad symbol","path":["Symbol(nested)"]}]', + ); + expect( + formatToolExecutionError(error as Error, { + id: 'symbol-call', + name: 'symbol_tool', + arguments: 'x', + }), + ).toContain('Symbol(nested)'); + }); + + it('requires inputJsonSchema for plain Standard Schemas in the tool() overload', () => { + tool<{ sessionId?: string }>({ + name: 'shared_plain_standard', + // @ts-expect-error plain Standard Schema without the JSON Schema trait must provide inputJsonSchema + inputSchema, + execute: () => ({ ok: true }), + }); + + tool<{ sessionId?: string }>({ + name: 'shared_zod', + inputSchema: z.object({ q: z.string() }), + execute: () => ({ ok: true }), + }); + + tool<{ sessionId?: string }>({ + name: 'shared_trait', + inputSchema: toStandardJsonSchema(v.object({ q: v.string() })), + execute: () => ({ ok: true }), + }); + }); + it('requires Standard JSON Schema or explicit JSON Schema at runtime', () => { const manuallyConstructedTool = { type: 'function' as const, From 9c226d0fe627ec04b6a251fe1dcea69af9996fc1 Mon Sep 17 00:00:00 2001 From: Luke Parke <5702154+LukasParke@users.noreply.github.com> Date: Tue, 11 Aug 2026 18:28:54 -0500 Subject: [PATCH 5/5] fix: restore InferToolOutput for tools with optional outputSchema InferToolOutput matched a required outputSchema property, but regular and manual tools declare it optional, so their result types collapsed to unknown (Devin Review finding on PR #793). Match the property as optional and filter non-schema values instead. Add tests/unit/tool-types.test-d.ts: vitest typecheck only covers *.test-d.ts files, so the expectTypeOf assertions in .test.ts files were never typechecked. --- src/lib/tool-types.ts | 8 ++- tests/unit/tool-types.test-d.ts | 100 ++++++++++++++++++++++++++++++++ 2 files changed, 106 insertions(+), 2 deletions(-) create mode 100644 tests/unit/tool-types.test-d.ts diff --git a/src/lib/tool-types.ts b/src/lib/tool-types.ts index c3d6ecbae..6553fc894 100644 --- a/src/lib/tool-types.ts +++ b/src/lib/tool-types.ts @@ -292,8 +292,12 @@ export type InferToolInput = T extends { function: { inputSchema: infer S ext /** * Extracts the output type from a tool definition */ -export type InferToolOutput = T extends { function: { outputSchema: infer S extends ToolSchema } } - ? InferSchemaOutput +// outputSchema is optional on regular/manual tools, so the pattern must +// tolerate a missing property and reject the undefined-only case. +export type InferToolOutput = T extends { function: { outputSchema?: infer S } } + ? S extends ToolSchema + ? InferSchemaOutput + : unknown : unknown; /** diff --git a/tests/unit/tool-types.test-d.ts b/tests/unit/tool-types.test-d.ts new file mode 100644 index 000000000..a81705b3f --- /dev/null +++ b/tests/unit/tool-types.test-d.ts @@ -0,0 +1,100 @@ +import { toStandardJsonSchema } from '@valibot/to-json-schema'; +import { describe, expectTypeOf, it } from 'vitest'; +import * as v from 'valibot'; +import { z } from 'zod/v4'; +import { tool } from '../../src/lib/tool.js'; +import type { + InferToolEvent, + InferToolInput, + InferToolOutput, + ToolExecutionResult, +} from '../../src/lib/tool-types.js'; + +const regularZodTool = tool({ + name: 'regular_zod', + inputSchema: z.object({ q: z.string() }), + outputSchema: z.object({ length: z.number() }), + execute: ({ q }) => ({ length: q.length }), +}); + +const valibotTool = tool({ + name: 'valibot_tool', + inputSchema: v.object({ name: v.string(), count: v.number() }), + inputJsonSchema: { type: 'object' }, + outputSchema: v.object({ message: v.string() }), + execute: ({ name, count }) => ({ message: name.repeat(count) }), +}); + +const generatorTool = tool({ + name: 'generator_tool', + inputSchema: v.object({ query: v.string() }), + inputJsonSchema: { type: 'object' }, + eventSchema: v.object({ progress: v.number() }), + outputSchema: v.object({ result: v.string() }), + execute: async function* ({ query }) { + yield { progress: 50 }; + return { result: query }; + }, +}); + +describe('tool type inference', () => { + it('infers Zod tool types', () => { + expectTypeOf>().toEqualTypeOf<{ q: string }>(); + expectTypeOf>().toEqualTypeOf<{ length: number }>(); + expectTypeOf['result']>().toEqualTypeOf<{ + length: number; + }>(); + }); + + it('infers Standard Schema tool types', () => { + expectTypeOf>().toEqualTypeOf<{ + name: string; + count: number; + }>(); + expectTypeOf>().toEqualTypeOf<{ message: string }>(); + expectTypeOf['result']>().toEqualTypeOf<{ + message: string; + }>(); + }); + + it('infers generator tool event and output types', () => { + expectTypeOf>().toEqualTypeOf<{ progress: number }>(); + expectTypeOf>().toEqualTypeOf<{ result: string }>(); + }); + + it('keeps the Standard JSON Schema trait optional for inputJsonSchema', () => { + tool({ + name: 'trait_no_explicit', + inputSchema: toStandardJsonSchema(v.object({ q: v.string() })), + execute: () => ({ ok: true }), + }); + + tool<{ sessionId?: string }>({ + name: 'shared_trait', + inputSchema: toStandardJsonSchema(v.object({ q: v.string() })), + execute: () => ({ ok: true }), + }); + + tool<{ sessionId?: string }>({ + name: 'shared_zod', + inputSchema: z.object({ q: z.string() }), + execute: () => ({ ok: true }), + }); + }); + + it('requires inputJsonSchema for plain Standard Schemas', () => { + tool({ + name: 'plain_standard', + // @ts-expect-error no JSON Schema trait and no explicit inputJsonSchema + inputSchema: v.object({ q: v.string() }), + execute: () => ({ ok: true }), + }); + + tool<{ sessionId?: string }>({ + name: 'shared_plain_standard', + // @ts-expect-error no JSON Schema trait and no explicit inputJsonSchema + inputSchema: v.object({ q: v.string() }), + execute: () => ({ ok: true }), + }); + }); +});