diff --git a/src/helpers/zod.ts b/src/helpers/zod.ts index 67057b60d2..35353f82c2 100644 --- a/src/helpers/zod.ts +++ b/src/helpers/zod.ts @@ -333,18 +333,8 @@ export function zodTextFormat( ); } -/** - * Creates a chat completion `function` tool that can be invoked - * automatically by the chat completion `.runTools()` method or automatically - * parsed by `.parse()` / `.stream()`. - * - * Arguments are converted to strict JSON Schema and validated with the supplied - * Zod schema before the optional callback receives them. - * - * @param options Model-visible function name, Zod parameter schema, description, - * and optional callback used by `chat.completions.runTools()`. - */ -export function zodFunction(options: { +/** Model-facing settings and an optional execution callback for a Zod function tool. */ +interface ZodFunctionOptions { /** Model-visible function name used to identify matching tool calls. */ name: string; @@ -356,19 +346,52 @@ export function zodFunction(options: { /** Optional model-visible explanation of when and how the function should be used. */ description?: string | undefined; -}): AutoParseableTool<{ +} + +/** A Zod function tool retaining its inferred argument and callback types. */ +type ZodFunctionTool< + Parameters extends ZodTypeLike, + Callback extends ZodFunctionOptions['function'], +> = AutoParseableTool<{ /** Inferred argument type produced by the Zod parameter schema. */ arguments: InferZodType; - /** Model-visible name used to match generated function calls. */ name: string; + /** Callback availability determines whether the tool can be executed. */ + function: Callback; +}>; - /** Callback signature associated with validated function-call arguments. */ - function: (args: InferZodType) => unknown; -}> { +/** + * Creates a chat completion `function` tool that can be invoked automatically + * by `.runTools()` or parsed by `.parse()` / `.stream()`. + * + * Arguments are converted to strict JSON Schema and validated with the supplied + * Zod schema before the callback receives them. + * + * @param options Model-visible function name, Zod parameter schema, description, + * and callback used by `chat.completions.runTools()`. + */ +export function zodFunction( + options: ZodFunctionOptions & { + /** Callback invoked with validated arguments by chat `runTools()`. */ + function: NonNullable['function']>; + }, +): ZodFunctionTool['function']>>; + +/** + * Creates a strict chat completion function tool for `.parse()` / `.stream()`. + * Without a guaranteed callback, the tool cannot be executed by `.runTools()`. + * + * @param options Model-visible function details and an optional callback. + */ +export function zodFunction( + options: ZodFunctionOptions, +): ZodFunctionTool['function']>; + +/** Builds a strict Chat Completions function tool from the supplied Zod schema. */ +export function zodFunction(options: ZodFunctionOptions) { const zodSchema = options.parameters as unknown as ZodSchema; - // @ts-expect-error TODO return makeParseableTool( { type: 'function', diff --git a/tests/helpers/zod.test.ts b/tests/helpers/zod.test.ts index c569321a76..f10d97cf3e 100644 --- a/tests/helpers/zod.test.ts +++ b/tests/helpers/zod.test.ts @@ -1,4 +1,5 @@ import { vi } from 'vitest'; +import type OpenAI from 'openai'; import { hasOwn } from 'openai/internal/utils/values'; import { @@ -794,7 +795,7 @@ describe.each([ } }); -function _typeTests() { +function _typeTests(client: OpenAI, maybeCallback?: (args: { hello: 'world' }) => unknown) { const MiniSchema = zv4Mini.object({ hello: zv4Mini.literal('world') }); type ParsedArguments = { hello: 'world' }; @@ -817,4 +818,51 @@ function _typeTests() { compareType>[0], ParsedArguments>(true); compareType, ParsedArguments>(true); compareType(true); + + const explicitGenericTool = zodFunction({ + name: 'explicit_generic', + parameters: MiniSchema, + function: (args) => { + expectType(args); + return Promise.resolve(args); + }, + }); + expectType(explicitGenericTool.__hasFunction); + expectType( + zodFunction({ name: 'explicit_parse_only', parameters: MiniSchema }).__hasFunction, + ); + + for (const parameters of [ + zv3.object({ hello: zv3.literal('world') }), + zv4.object({ hello: zv4.literal('world') }), + MiniSchema, + ]) { + const options = { name: 'greet', parameters }; + const runnable = zodFunction({ + ...options, + function: (args) => expectType(args), + }); + const callbackless = zodFunction(options); + const undefinedCallback = zodFunction({ ...options, function: undefined }); + const optionalCallback = zodFunction({ ...options, function: maybeCallback }); + + expectType(runnable.__hasFunction); + expectType(callbackless.__hasFunction); + expectType(undefinedCallback.__hasFunction); + expectType(optionalCallback.__hasFunction); + expectType(callbackless.__arguments); + + const request = { model: 'gpt-4o', messages: [] }; + client.chat.completions.runTools({ ...request, tools: [runnable] }); + client.chat.completions.runTools({ ...request, tools: [runnable], stream: true }); + + for (const tool of [callbackless, undefinedCallback, optionalCallback]) { + client.chat.completions.parse({ ...request, tools: [tool] }); + client.chat.completions.stream({ ...request, tools: [tool] }); + // @ts-expect-error a tool without a guaranteed callback cannot be executed + client.chat.completions.runTools({ ...request, tools: [tool] }); + // @ts-expect-error streaming tool execution also requires a guaranteed callback + client.chat.completions.runTools({ ...request, tools: [tool], stream: true }); + } + } }