Skip to content

Commit 8b1b4b3

Browse files
RobertCraigiestainless-app[bot]
authored andcommitted
fix(helpers/zod): correct schema generation for recursive schemas
1 parent 086421f commit 8b1b4b3

File tree

3 files changed

+259
-4
lines changed

3 files changed

+259
-4
lines changed

src/helpers/zod.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,12 @@ import {
88
} from '../lib/parser';
99
import { zodToJsonSchema as _zodToJsonSchema } from '../_vendor/zod-to-json-schema';
1010

11-
function zodToJsonSchema(schema: z.ZodType): Record<string, unknown> {
12-
return _zodToJsonSchema(schema, { openaiStrictMode: true });
11+
function zodToJsonSchema(schema: z.ZodType, options: { name: string }): Record<string, unknown> {
12+
return _zodToJsonSchema(schema, {
13+
openaiStrictMode: true,
14+
name: options.name,
15+
nameStrategy: 'duplicate-ref',
16+
});
1317
}
1418

1519
/**
@@ -61,7 +65,7 @@ export function zodResponseFormat<ZodInput extends z.ZodType>(
6165
...props,
6266
name,
6367
strict: true,
64-
schema: zodToJsonSchema(zodObject),
68+
schema: zodToJsonSchema(zodObject, { name }),
6569
},
6670
},
6771
(content) => zodObject.parse(JSON.parse(content)),
@@ -89,7 +93,7 @@ export function zodFunction<Parameters extends z.ZodType>(options: {
8993
type: 'function',
9094
function: {
9195
name: options.name,
92-
parameters: zodToJsonSchema(options.parameters),
96+
parameters: zodToJsonSchema(options.parameters, { name: options.name }),
9397
strict: true,
9498
...(options.description ? { description: options.description } : undefined),
9599
},

tests/lib/__snapshots__/parser.test.ts.snap

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,3 +27,31 @@ exports[`.parse() zod deserialises response_format 1`] = `
2727
}
2828
"
2929
`;
30+
31+
exports[`.parse() zod top-level recursive schemas 1`] = `
32+
"{
33+
"id": "chatcmpl-9taiMDrRVRIkk1Xg1yE82UjnYuZjt",
34+
"object": "chat.completion",
35+
"created": 1723036198,
36+
"model": "gpt-4o-2024-08-06",
37+
"choices": [
38+
{
39+
"index": 0,
40+
"message": {
41+
"role": "assistant",
42+
"content": "{\\"type\\":\\"form\\",\\"label\\":\\"User Profile Form\\",\\"children\\":[{\\"type\\":\\"field\\",\\"label\\":\\"Full Name\\",\\"children\\":[],\\"attributes\\":[{\\"name\\":\\"type\\",\\"value\\":\\"text\\"},{\\"name\\":\\"placeholder\\",\\"value\\":\\"Enter your full name\\"}]},{\\"type\\":\\"field\\",\\"label\\":\\"Email Address\\",\\"children\\":[],\\"attributes\\":[{\\"name\\":\\"type\\",\\"value\\":\\"email\\"},{\\"name\\":\\"placeholder\\",\\"value\\":\\"Enter your email address\\"}]},{\\"type\\":\\"field\\",\\"label\\":\\"Phone Number\\",\\"children\\":[],\\"attributes\\":[{\\"name\\":\\"type\\",\\"value\\":\\"tel\\"},{\\"name\\":\\"placeholder\\",\\"value\\":\\"Enter your phone number\\"}]},{\\"type\\":\\"button\\",\\"label\\":\\"Submit\\",\\"children\\":[],\\"attributes\\":[{\\"name\\":\\"type\\",\\"value\\":\\"submit\\"}]}],\\"attributes\\":[{\\"name\\":\\"method\\",\\"value\\":\\"post\\"},{\\"name\\":\\"action\\",\\"value\\":\\"/submit-profile\\"}]}",
43+
"refusal": null
44+
},
45+
"logprobs": null,
46+
"finish_reason": "stop"
47+
}
48+
],
49+
"usage": {
50+
"prompt_tokens": 38,
51+
"completion_tokens": 168,
52+
"total_tokens": 206
53+
},
54+
"system_fingerprint": "fp_845eaabc1f"
55+
}
56+
"
57+
`;

tests/lib/parser.test.ts

Lines changed: 223 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,5 +44,228 @@ describe('.parse()', () => {
4444
}
4545
`);
4646
});
47+
48+
test('top-level recursive schemas', async () => {
49+
const UI: any = z.lazy(() =>
50+
z.object({
51+
type: z.enum(['div', 'button', 'header', 'section', 'field', 'form']),
52+
label: z.string(),
53+
children: z.array(UI),
54+
attributes: z.array(
55+
z.object({
56+
name: z.string(),
57+
value: z.string(),
58+
}),
59+
),
60+
}),
61+
);
62+
63+
const completion = await makeSnapshotRequest((openai) =>
64+
openai.beta.chat.completions.parse({
65+
model: 'gpt-4o-2024-08-06',
66+
messages: [
67+
{
68+
role: 'system',
69+
content: 'You are a UI generator AI. Convert the user input into a UI.',
70+
},
71+
{ role: 'user', content: 'Make a User Profile Form with 3 fields' },
72+
],
73+
response_format: zodResponseFormat(UI, 'ui'),
74+
}),
75+
);
76+
77+
expect(completion.choices[0]?.message).toMatchInlineSnapshot(`
78+
{
79+
"content": "{"type":"form","label":"User Profile Form","children":[{"type":"field","label":"Full Name","children":[],"attributes":[{"name":"type","value":"text"},{"name":"placeholder","value":"Enter your full name"}]},{"type":"field","label":"Email Address","children":[],"attributes":[{"name":"type","value":"email"},{"name":"placeholder","value":"Enter your email address"}]},{"type":"field","label":"Phone Number","children":[],"attributes":[{"name":"type","value":"tel"},{"name":"placeholder","value":"Enter your phone number"}]},{"type":"button","label":"Submit","children":[],"attributes":[{"name":"type","value":"submit"}]}],"attributes":[{"name":"method","value":"post"},{"name":"action","value":"/submit-profile"}]}",
80+
"parsed": {
81+
"attributes": [
82+
{
83+
"name": "method",
84+
"value": "post",
85+
},
86+
{
87+
"name": "action",
88+
"value": "/submit-profile",
89+
},
90+
],
91+
"children": [
92+
{
93+
"attributes": [
94+
{
95+
"name": "type",
96+
"value": "text",
97+
},
98+
{
99+
"name": "placeholder",
100+
"value": "Enter your full name",
101+
},
102+
],
103+
"children": [],
104+
"label": "Full Name",
105+
"type": "field",
106+
},
107+
{
108+
"attributes": [
109+
{
110+
"name": "type",
111+
"value": "email",
112+
},
113+
{
114+
"name": "placeholder",
115+
"value": "Enter your email address",
116+
},
117+
],
118+
"children": [],
119+
"label": "Email Address",
120+
"type": "field",
121+
},
122+
{
123+
"attributes": [
124+
{
125+
"name": "type",
126+
"value": "tel",
127+
},
128+
{
129+
"name": "placeholder",
130+
"value": "Enter your phone number",
131+
},
132+
],
133+
"children": [],
134+
"label": "Phone Number",
135+
"type": "field",
136+
},
137+
{
138+
"attributes": [
139+
{
140+
"name": "type",
141+
"value": "submit",
142+
},
143+
],
144+
"children": [],
145+
"label": "Submit",
146+
"type": "button",
147+
},
148+
],
149+
"label": "User Profile Form",
150+
"type": "form",
151+
},
152+
"refusal": null,
153+
"role": "assistant",
154+
"tool_calls": [],
155+
}
156+
`);
157+
158+
expect(zodResponseFormat(UI, 'ui').json_schema).toMatchInlineSnapshot(`
159+
{
160+
"name": "ui",
161+
"schema": {
162+
"$schema": "http://json-schema.org/draft-07/schema#",
163+
"additionalProperties": false,
164+
"definitions": {
165+
"ui": {
166+
"additionalProperties": false,
167+
"properties": {
168+
"attributes": {
169+
"items": {
170+
"additionalProperties": false,
171+
"properties": {
172+
"name": {
173+
"type": "string",
174+
},
175+
"value": {
176+
"type": "string",
177+
},
178+
},
179+
"required": [
180+
"name",
181+
"value",
182+
],
183+
"type": "object",
184+
},
185+
"type": "array",
186+
},
187+
"children": {
188+
"items": {
189+
"$ref": "#/definitions/ui",
190+
},
191+
"type": "array",
192+
},
193+
"label": {
194+
"type": "string",
195+
},
196+
"type": {
197+
"enum": [
198+
"div",
199+
"button",
200+
"header",
201+
"section",
202+
"field",
203+
"form",
204+
],
205+
"type": "string",
206+
},
207+
},
208+
"required": [
209+
"type",
210+
"label",
211+
"children",
212+
"attributes",
213+
],
214+
"type": "object",
215+
},
216+
},
217+
"properties": {
218+
"attributes": {
219+
"items": {
220+
"additionalProperties": false,
221+
"properties": {
222+
"name": {
223+
"type": "string",
224+
},
225+
"value": {
226+
"type": "string",
227+
},
228+
},
229+
"required": [
230+
"name",
231+
"value",
232+
],
233+
"type": "object",
234+
},
235+
"type": "array",
236+
},
237+
"children": {
238+
"items": {
239+
"$ref": "#/definitions/ui",
240+
},
241+
"type": "array",
242+
},
243+
"label": {
244+
"type": "string",
245+
},
246+
"type": {
247+
"enum": [
248+
"div",
249+
"button",
250+
"header",
251+
"section",
252+
"field",
253+
"form",
254+
],
255+
"type": "string",
256+
},
257+
},
258+
"required": [
259+
"type",
260+
"label",
261+
"children",
262+
"attributes",
263+
],
264+
"type": "object",
265+
},
266+
"strict": true,
267+
}
268+
`);
269+
});
47270
});
48271
});

0 commit comments

Comments
 (0)