Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 41 additions & 18 deletions src/helpers/zod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -333,18 +333,8 @@ export function zodTextFormat<ZodInput extends ZodTypeLike>(
);
}

/**
* 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<Parameters extends ZodTypeLike>(options: {
/** Model-facing settings and an optional execution callback for a Zod function tool. */
interface ZodFunctionOptions<Parameters extends ZodTypeLike> {
/** Model-visible function name used to identify matching tool calls. */
name: string;

Expand All @@ -356,19 +346,52 @@ export function zodFunction<Parameters extends ZodTypeLike>(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<Parameters>['function'],
> = AutoParseableTool<{
/** Inferred argument type produced by the Zod parameter schema. */
arguments: InferZodType<Parameters>;

/** 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<Parameters>) => 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<Parameters extends ZodTypeLike>(
options: ZodFunctionOptions<Parameters> & {
/** Callback invoked with validated arguments by chat `runTools()`. */
function: NonNullable<ZodFunctionOptions<Parameters>['function']>;
},
): ZodFunctionTool<Parameters, NonNullable<ZodFunctionOptions<Parameters>['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<Parameters extends ZodTypeLike>(
options: ZodFunctionOptions<Parameters>,
): ZodFunctionTool<Parameters, ZodFunctionOptions<Parameters>['function']>;

/** Builds a strict Chat Completions function tool from the supplied Zod schema. */
export function zodFunction<Parameters extends ZodTypeLike>(options: ZodFunctionOptions<Parameters>) {
const zodSchema = options.parameters as unknown as ZodSchema;

// @ts-expect-error TODO
return makeParseableTool<any>(
{
type: 'function',
Expand Down
50 changes: 49 additions & 1 deletion tests/helpers/zod.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { vi } from 'vitest';
import type OpenAI from 'openai';
import { hasOwn } from 'openai/internal/utils/values';

import {
Expand Down Expand Up @@ -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' };

Expand All @@ -817,4 +818,51 @@ function _typeTests() {
compareType<Parameters<NonNullable<typeof responseTool.$callback>>[0], ParsedArguments>(true);
compareType<ReturnType<typeof responseTool.$parseRaw>, ParsedArguments>(true);
compareType<typeof responseTool.__arguments, ParsedArguments>(true);

const explicitGenericTool = zodFunction<typeof MiniSchema>({
name: 'explicit_generic',
parameters: MiniSchema,
function: (args) => {
expectType<ParsedArguments>(args);
return Promise.resolve(args);
},
});
expectType<true>(explicitGenericTool.__hasFunction);
expectType<false>(
zodFunction<typeof MiniSchema>({ 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<ParsedArguments>(args),
});
const callbackless = zodFunction(options);
const undefinedCallback = zodFunction({ ...options, function: undefined });
const optionalCallback = zodFunction({ ...options, function: maybeCallback });

expectType<true>(runnable.__hasFunction);
expectType<false>(callbackless.__hasFunction);
expectType<false>(undefinedCallback.__hasFunction);
expectType<false>(optionalCallback.__hasFunction);
expectType<ParsedArguments>(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 });
}
}
}
Loading