Skip to content

Commit c972cfc

Browse files
RobertCraigiestainless-app[bot]
authored andcommitted
feat(api): add structured outputs support
This commit adds support for JSON schema response format & adds a separate `.beta.chat.completions.parse()` method to automatically deserialise the response content into a zod schema with the zodResponseFormat() helper function. For more details on structured outputs, see this guide https://platform.openai.com/docs/guides/structured-outputs
1 parent f72b403 commit c972cfc

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

91 files changed

+5148
-300
lines changed

.github/workflows/ci.yml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,4 +45,3 @@ jobs:
4545

4646
- name: Run tests
4747
run: ./scripts/test
48-

.stats.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
11
configured_endpoints: 68
2-
openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/openai-b04761ffd2adad3cc19a6dc6fc696ac445878219972f891881a967340fa9a6b0.yml
2+
openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/openai-c36d30a94622922f83d56a025cdf0095ff7cb18a5138838c698c8443f21fb3a8.yml

api.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@ Types:
55
- <code><a href="./src/resources/shared.ts">ErrorObject</a></code>
66
- <code><a href="./src/resources/shared.ts">FunctionDefinition</a></code>
77
- <code><a href="./src/resources/shared.ts">FunctionParameters</a></code>
8+
- <code><a href="./src/resources/shared.ts">ResponseFormatJSONObject</a></code>
9+
- <code><a href="./src/resources/shared.ts">ResponseFormatJSONSchema</a></code>
10+
- <code><a href="./src/resources/shared.ts">ResponseFormatText</a></code>
811

912
# Completions
1013

@@ -33,6 +36,7 @@ Types:
3336
- <code><a href="./src/resources/chat/completions.ts">ChatCompletionChunk</a></code>
3437
- <code><a href="./src/resources/chat/completions.ts">ChatCompletionContentPart</a></code>
3538
- <code><a href="./src/resources/chat/completions.ts">ChatCompletionContentPartImage</a></code>
39+
- <code><a href="./src/resources/chat/completions.ts">ChatCompletionContentPartRefusal</a></code>
3640
- <code><a href="./src/resources/chat/completions.ts">ChatCompletionContentPartText</a></code>
3741
- <code><a href="./src/resources/chat/completions.ts">ChatCompletionFunctionCallOption</a></code>
3842
- <code><a href="./src/resources/chat/completions.ts">ChatCompletionFunctionMessageParam</a></code>
@@ -277,7 +281,6 @@ Methods:
277281

278282
Types:
279283

280-
- <code><a href="./src/resources/beta/threads/threads.ts">AssistantResponseFormat</a></code>
281284
- <code><a href="./src/resources/beta/threads/threads.ts">AssistantResponseFormatOption</a></code>
282285
- <code><a href="./src/resources/beta/threads/threads.ts">AssistantToolChoice</a></code>
283286
- <code><a href="./src/resources/beta/threads/threads.ts">AssistantToolChoiceFunction</a></code>
@@ -370,6 +373,8 @@ Types:
370373
- <code><a href="./src/resources/beta/threads/messages.ts">MessageDeleted</a></code>
371374
- <code><a href="./src/resources/beta/threads/messages.ts">MessageDelta</a></code>
372375
- <code><a href="./src/resources/beta/threads/messages.ts">MessageDeltaEvent</a></code>
376+
- <code><a href="./src/resources/beta/threads/messages.ts">RefusalContentBlock</a></code>
377+
- <code><a href="./src/resources/beta/threads/messages.ts">RefusalDeltaBlock</a></code>
373378
- <code><a href="./src/resources/beta/threads/messages.ts">Text</a></code>
374379
- <code><a href="./src/resources/beta/threads/messages.ts">TextContentBlock</a></code>
375380
- <code><a href="./src/resources/beta/threads/messages.ts">TextContentBlockParam</a></code>

examples/parsing-run-tools.ts

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
import OpenAI from 'openai';
2+
import z from 'zod';
3+
import { zodFunction } from 'openai/helpers/zod';
4+
5+
const Table = z.enum(['orders', 'customers', 'products']);
6+
const Column = z.enum([
7+
'id',
8+
'status',
9+
'expected_delivery_date',
10+
'delivered_at',
11+
'shipped_at',
12+
'ordered_at',
13+
'canceled_at',
14+
]);
15+
const Operator = z.enum(['=', '>', '<', '<=', '>=', '!=']);
16+
const OrderBy = z.enum(['asc', 'desc']);
17+
18+
const DynamicValue = z.object({
19+
column_name: z.string(),
20+
});
21+
22+
const Condition = z.object({
23+
column: z.string(),
24+
operator: Operator,
25+
value: z.union([z.string(), z.number(), DynamicValue]),
26+
});
27+
28+
const openai = new OpenAI();
29+
30+
async function main() {
31+
const runner = openai.beta.chat.completions
32+
.runTools({
33+
model: 'gpt-4o-2024-08-06',
34+
messages: [{ role: 'user', content: `What are the last 10 orders?` }],
35+
stream: true,
36+
tools: [
37+
zodFunction({
38+
name: 'query',
39+
function: (args) => {
40+
return { table_name: args.table_name, data: fakeOrders };
41+
},
42+
parameters: z.object({
43+
location: z.string(),
44+
table_name: Table,
45+
columns: z.array(Column),
46+
conditions: z.array(Condition),
47+
order_by: OrderBy,
48+
}),
49+
}),
50+
],
51+
})
52+
.on('tool_calls.function.arguments.done', (props) =>
53+
console.log(`parsed function arguments: ${props.parsed_arguments}`),
54+
);
55+
56+
await runner.done();
57+
58+
console.dir(runner.messages, { depth: 10 });
59+
}
60+
61+
const fakeOrders = [
62+
{
63+
orderId: 'ORD-001',
64+
customerName: 'Alice Johnson',
65+
products: [{ name: 'Wireless Headphones', quantity: 1, price: 89.99 }],
66+
totalPrice: 89.99,
67+
orderDate: '2024-08-02',
68+
},
69+
{
70+
orderId: 'ORD-002',
71+
customerName: 'Bob Smith',
72+
products: [
73+
{ name: 'Smartphone Case', quantity: 2, price: 19.99 },
74+
{ name: 'Screen Protector', quantity: 1, price: 9.99 },
75+
],
76+
totalPrice: 49.97,
77+
orderDate: '2024-08-03',
78+
},
79+
{
80+
orderId: 'ORD-003',
81+
customerName: 'Carol Davis',
82+
products: [
83+
{ name: 'Laptop', quantity: 1, price: 999.99 },
84+
{ name: 'Mouse', quantity: 1, price: 29.99 },
85+
],
86+
totalPrice: 1029.98,
87+
orderDate: '2024-08-04',
88+
},
89+
{
90+
orderId: 'ORD-004',
91+
customerName: 'David Wilson',
92+
products: [{ name: 'Coffee Maker', quantity: 1, price: 79.99 }],
93+
totalPrice: 79.99,
94+
orderDate: '2024-08-05',
95+
},
96+
{
97+
orderId: 'ORD-005',
98+
customerName: 'Eva Brown',
99+
products: [
100+
{ name: 'Fitness Tracker', quantity: 1, price: 129.99 },
101+
{ name: 'Water Bottle', quantity: 2, price: 14.99 },
102+
],
103+
totalPrice: 159.97,
104+
orderDate: '2024-08-06',
105+
},
106+
{
107+
orderId: 'ORD-006',
108+
customerName: 'Frank Miller',
109+
products: [
110+
{ name: 'Gaming Console', quantity: 1, price: 499.99 },
111+
{ name: 'Controller', quantity: 2, price: 59.99 },
112+
],
113+
totalPrice: 619.97,
114+
orderDate: '2024-08-07',
115+
},
116+
{
117+
orderId: 'ORD-007',
118+
customerName: 'Grace Lee',
119+
products: [{ name: 'Bluetooth Speaker', quantity: 1, price: 69.99 }],
120+
totalPrice: 69.99,
121+
orderDate: '2024-08-08',
122+
},
123+
{
124+
orderId: 'ORD-008',
125+
customerName: 'Henry Taylor',
126+
products: [
127+
{ name: 'Smartwatch', quantity: 1, price: 199.99 },
128+
{ name: 'Watch Band', quantity: 2, price: 24.99 },
129+
],
130+
totalPrice: 249.97,
131+
orderDate: '2024-08-09',
132+
},
133+
{
134+
orderId: 'ORD-009',
135+
customerName: 'Isla Garcia',
136+
products: [
137+
{ name: 'Tablet', quantity: 1, price: 349.99 },
138+
{ name: 'Tablet Case', quantity: 1, price: 29.99 },
139+
{ name: 'Stylus', quantity: 1, price: 39.99 },
140+
],
141+
totalPrice: 419.97,
142+
orderDate: '2024-08-10',
143+
},
144+
{
145+
orderId: 'ORD-010',
146+
customerName: 'Jack Robinson',
147+
products: [{ name: 'Wireless Charger', quantity: 2, price: 34.99 }],
148+
totalPrice: 69.98,
149+
orderDate: '2024-08-11',
150+
},
151+
];
152+
153+
main();

examples/parsing-stream.ts

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import { zodResponseFormat } from 'openai/helpers/zod';
2+
import OpenAI from 'openai/index';
3+
import { z } from 'zod';
4+
5+
const Step = z.object({
6+
explanation: z.string(),
7+
output: z.string(),
8+
});
9+
10+
const MathResponse = z.object({
11+
steps: z.array(Step),
12+
final_answer: z.string(),
13+
});
14+
15+
async function main() {
16+
const client = new OpenAI();
17+
18+
const stream = client.beta.chat.completions
19+
.stream({
20+
model: 'gpt-4o-2024-08-06',
21+
messages: [
22+
{
23+
role: 'user',
24+
content: `What's the weather like in SF?`,
25+
},
26+
],
27+
response_format: zodResponseFormat(MathResponse, 'math_response'),
28+
})
29+
.on('refusal.delta', ({ delta }) => {
30+
process.stdout.write(delta);
31+
})
32+
.on('refusal.done', () => console.log('\n\nrequest refused 😱'))
33+
.on('content.delta', ({ snapshot, parsed }) => {
34+
console.log('content:', snapshot);
35+
console.log('parsed:', parsed);
36+
console.log();
37+
})
38+
.on('content.done', (props) => {
39+
if (props.parsed) {
40+
console.log('\n\nfinished parsing!');
41+
console.log(`answer: ${props.parsed.final_answer}`);
42+
}
43+
});
44+
45+
await stream.done();
46+
47+
const completion = await stream.finalChatCompletion();
48+
49+
console.dir(completion, { depth: 5 });
50+
51+
const message = completion.choices[0]?.message;
52+
if (message?.parsed) {
53+
console.log(message.parsed.steps);
54+
}
55+
}
56+
57+
main();

examples/parsing-tools-stream.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { zodFunction } from 'openai/helpers/zod';
2+
import OpenAI from 'openai/index';
3+
import { z } from 'zod';
4+
5+
const GetWeatherArgs = z.object({
6+
city: z.string(),
7+
country: z.string(),
8+
units: z.enum(['c', 'f']).default('c'),
9+
});
10+
11+
async function main() {
12+
const client = new OpenAI();
13+
const refusal = process.argv.includes('refusal');
14+
15+
const stream = client.beta.chat.completions
16+
.stream({
17+
model: 'gpt-4o-2024-08-06',
18+
messages: [
19+
{
20+
role: 'user',
21+
content: refusal ? 'How do I make anthrax?' : `What's the weather like in SF?`,
22+
},
23+
],
24+
tools: [zodFunction({ name: 'get_weather', parameters: GetWeatherArgs })],
25+
})
26+
.on('tool_calls.function.arguments.delta', (props) =>
27+
console.log('tool_calls.function.arguments.delta', props),
28+
)
29+
.on('tool_calls.function.arguments.done', (props) =>
30+
console.log('tool_calls.function.arguments.done', props),
31+
)
32+
.on('refusal.delta', ({ delta }) => {
33+
process.stdout.write(delta);
34+
})
35+
.on('refusal.done', () => console.log('\n\nrequest refused 😱'));
36+
37+
const completion = await stream.finalChatCompletion();
38+
39+
console.log('final completion:');
40+
console.dir(completion, { depth: 10 });
41+
}
42+
43+
main();

examples/parsing-tools.ts

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import { zodFunction } from 'openai/helpers/zod';
2+
import OpenAI from 'openai/index';
3+
import { z } from 'zod';
4+
5+
const Table = z.enum(['orders', 'customers', 'products']);
6+
7+
const Column = z.enum([
8+
'id',
9+
'status',
10+
'expected_delivery_date',
11+
'delivered_at',
12+
'shipped_at',
13+
'ordered_at',
14+
'canceled_at',
15+
]);
16+
17+
const Operator = z.enum(['=', '>', '<', '<=', '>=', '!=']);
18+
19+
const OrderBy = z.enum(['asc', 'desc']);
20+
21+
const DynamicValue = z.object({
22+
column_name: z.string(),
23+
});
24+
25+
const Condition = z.object({
26+
column: z.string(),
27+
operator: Operator,
28+
value: z.union([z.string(), z.number(), DynamicValue]),
29+
});
30+
31+
const Query = z.object({
32+
table_name: Table,
33+
columns: z.array(Column),
34+
conditions: z.array(Condition),
35+
order_by: OrderBy,
36+
});
37+
38+
async function main() {
39+
const client = new OpenAI();
40+
41+
const completion = await client.beta.chat.completions.parse({
42+
model: 'gpt-4o-2024-08-06',
43+
messages: [
44+
{
45+
role: 'system',
46+
content:
47+
'You are a helpful assistant. The current date is August 6, 2024. You help users query for the data they are looking for by calling the query function.',
48+
},
49+
{
50+
role: 'user',
51+
content:
52+
'look up all my orders in november of last year that were fulfilled but not delivered on time',
53+
},
54+
],
55+
tools: [zodFunction({ name: 'query', parameters: Query })],
56+
});
57+
console.dir(completion, { depth: 10 });
58+
59+
const toolCall = completion.choices[0]?.message.tool_calls?.[0];
60+
if (toolCall) {
61+
const args = toolCall.function.parsed_arguments as z.infer<typeof Query>;
62+
console.log(args);
63+
console.log(args.table_name);
64+
}
65+
}
66+
67+
main();

0 commit comments

Comments
 (0)