diff --git a/src/helpers/zod.ts b/src/helpers/zod.ts index 400b448fdd..fe04a5f306 100644 --- a/src/helpers/zod.ts +++ b/src/helpers/zod.ts @@ -12,6 +12,7 @@ import { import { zodToJsonSchema as _zodToJsonSchema } from '../_vendor/zod-to-json-schema'; import { AutoParseableResponseTool, makeParseableResponseTool } from '../lib/ResponsesParser'; import { type ResponseFormatTextJSONSchemaConfig } from '../resources/responses/responses'; +import { type RealtimeFunctionTool } from '../resources/realtime/realtime'; import { toStrictJsonSchema } from '../lib/transform'; import { JSONSchema } from '../lib/jsonschema'; @@ -178,3 +179,47 @@ export function zodResponsesFunction }, ); } + +/** + * Creates a Realtime API `function` tool definition from the given Zod schema. + * + * Unlike {@link zodResponsesFunction}, this helper does **not** set `strict: true` + * because the Realtime API's `RealtimeFunctionTool` interface does not include + * that field. + * + * ```ts + * const session = await client.beta.realtime.sessions.create({ + * model: 'gpt-4o-realtime-preview', + * tools: [ + * zodRealtimeFunction({ + * name: 'get_weather', + * description: 'Get the current weather for a location', + * parameters: z.object({ + * location: z.string().describe('City and state, e.g. "San Francisco, CA"'), + * }), + * }), + * ], + * }); + * ``` + * + * When the model invokes the tool, parse the arguments yourself: + * + * ```ts + * const args = GetWeatherParams.parse(JSON.parse(event.arguments)); + * ``` + */ +export function zodRealtimeFunction(options: { + name: string; + parameters: Parameters; + description?: string | undefined; +}): RealtimeFunctionTool { + return { + type: 'function', + name: options.name, + parameters: + isZodV4(options.parameters) ? + zodV4ToJsonSchema(options.parameters) + : zodV3ToJsonSchema(options.parameters, { name: options.name }), + ...(options.description ? { description: options.description } : undefined), + }; +} diff --git a/tests/helpers/zod.test.ts b/tests/helpers/zod.test.ts index 1bc766376c..8819fcd45f 100644 --- a/tests/helpers/zod.test.ts +++ b/tests/helpers/zod.test.ts @@ -1,4 +1,4 @@ -import { zodResponseFormat } from 'openai/helpers/zod'; +import { zodResponseFormat, zodRealtimeFunction } from 'openai/helpers/zod'; import { z as zv3 } from 'zod/v3'; import { z as zv4 } from 'zod/v4'; @@ -360,3 +360,42 @@ describe.each([ expect(consoleSpy).toHaveBeenCalledTimes(0); }); }); + +describe.each([ + { version: 'v3', z: zv3 }, + { version: 'v4', z: zv4 as any as typeof zv3 }, +])('zodRealtimeFunction (Zod $version)', ({ z }) => { + it('builds a RealtimeFunctionTool without strict', () => { + const tool = zodRealtimeFunction({ + name: 'get_weather', + description: 'Get the current weather', + parameters: z.object({ + location: z.string(), + unit: z.enum(['c', 'f']), + }), + }); + + expect(tool.type).toBe('function'); + expect(tool.name).toBe('get_weather'); + expect(tool.description).toBe('Get the current weather'); + expect(tool).not.toHaveProperty('strict'); + expect(tool.parameters).toMatchObject({ + type: 'object', + properties: { + location: { type: 'string' }, + unit: { enum: ['c', 'f'], type: 'string' }, + }, + required: ['location', 'unit'], + }); + }); + + it('omits description when not provided', () => { + const tool = zodRealtimeFunction({ + name: 'ping', + parameters: z.object({ message: z.string() }), + }); + + expect(tool).not.toHaveProperty('description'); + expect(tool.name).toBe('ping'); + }); +});