Skip to content

Commit

Permalink
feat(api): add structured outputs support
Browse files Browse the repository at this point in the history
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
  • Loading branch information
RobertCraigie committed Aug 6, 2024
1 parent f72b403 commit 573787c
Show file tree
Hide file tree
Showing 91 changed files with 5,148 additions and 300 deletions.
1 change: 0 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -45,4 +45,3 @@ jobs:

- name: Run tests
run: ./scripts/test

2 changes: 1 addition & 1 deletion .stats.yml
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
configured_endpoints: 68
openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/openai-b04761ffd2adad3cc19a6dc6fc696ac445878219972f891881a967340fa9a6b0.yml
openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/openai-c36d30a94622922f83d56a025cdf0095ff7cb18a5138838c698c8443f21fb3a8.yml
7 changes: 6 additions & 1 deletion api.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ Types:
- <code><a href="./src/resources/shared.ts">ErrorObject</a></code>
- <code><a href="./src/resources/shared.ts">FunctionDefinition</a></code>
- <code><a href="./src/resources/shared.ts">FunctionParameters</a></code>
- <code><a href="./src/resources/shared.ts">ResponseFormatJSONObject</a></code>
- <code><a href="./src/resources/shared.ts">ResponseFormatJSONSchema</a></code>
- <code><a href="./src/resources/shared.ts">ResponseFormatText</a></code>

# Completions

Expand Down Expand Up @@ -33,6 +36,7 @@ Types:
- <code><a href="./src/resources/chat/completions.ts">ChatCompletionChunk</a></code>
- <code><a href="./src/resources/chat/completions.ts">ChatCompletionContentPart</a></code>
- <code><a href="./src/resources/chat/completions.ts">ChatCompletionContentPartImage</a></code>
- <code><a href="./src/resources/chat/completions.ts">ChatCompletionContentPartRefusal</a></code>
- <code><a href="./src/resources/chat/completions.ts">ChatCompletionContentPartText</a></code>
- <code><a href="./src/resources/chat/completions.ts">ChatCompletionFunctionCallOption</a></code>
- <code><a href="./src/resources/chat/completions.ts">ChatCompletionFunctionMessageParam</a></code>
Expand Down Expand Up @@ -277,7 +281,6 @@ Methods:

Types:

- <code><a href="./src/resources/beta/threads/threads.ts">AssistantResponseFormat</a></code>
- <code><a href="./src/resources/beta/threads/threads.ts">AssistantResponseFormatOption</a></code>
- <code><a href="./src/resources/beta/threads/threads.ts">AssistantToolChoice</a></code>
- <code><a href="./src/resources/beta/threads/threads.ts">AssistantToolChoiceFunction</a></code>
Expand Down Expand Up @@ -370,6 +373,8 @@ Types:
- <code><a href="./src/resources/beta/threads/messages.ts">MessageDeleted</a></code>
- <code><a href="./src/resources/beta/threads/messages.ts">MessageDelta</a></code>
- <code><a href="./src/resources/beta/threads/messages.ts">MessageDeltaEvent</a></code>
- <code><a href="./src/resources/beta/threads/messages.ts">RefusalContentBlock</a></code>
- <code><a href="./src/resources/beta/threads/messages.ts">RefusalDeltaBlock</a></code>
- <code><a href="./src/resources/beta/threads/messages.ts">Text</a></code>
- <code><a href="./src/resources/beta/threads/messages.ts">TextContentBlock</a></code>
- <code><a href="./src/resources/beta/threads/messages.ts">TextContentBlockParam</a></code>
Expand Down
153 changes: 153 additions & 0 deletions examples/parsing-run-tools.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
import OpenAI from 'openai';
import z from 'zod';
import { zodFunction } from 'openai/helpers/zod';

const Table = z.enum(['orders', 'customers', 'products']);
const Column = z.enum([
'id',
'status',
'expected_delivery_date',
'delivered_at',
'shipped_at',
'ordered_at',
'canceled_at',
]);
const Operator = z.enum(['=', '>', '<', '<=', '>=', '!=']);
const OrderBy = z.enum(['asc', 'desc']);

const DynamicValue = z.object({
column_name: z.string(),
});

const Condition = z.object({
column: z.string(),
operator: Operator,
value: z.union([z.string(), z.number(), DynamicValue]),
});

const openai = new OpenAI();

async function main() {
const runner = openai.beta.chat.completions
.runTools({
model: 'gpt-4o-2024-08-06',
messages: [{ role: 'user', content: `What are the last 10 orders?` }],
stream: true,
tools: [
zodFunction({
name: 'query',
function: (args) => {
return { table_name: args.table_name, data: fakeOrders };
},
parameters: z.object({
location: z.string(),
table_name: Table,
columns: z.array(Column),
conditions: z.array(Condition),
order_by: OrderBy,
}),
}),
],
})
.on('tool_calls.function.arguments.done', (props) =>
console.log(`parsed function arguments: ${props.parsed_arguments}`),
);

await runner.done();

console.dir(runner.messages, { depth: 10 });
}

const fakeOrders = [
{
orderId: 'ORD-001',
customerName: 'Alice Johnson',
products: [{ name: 'Wireless Headphones', quantity: 1, price: 89.99 }],
totalPrice: 89.99,
orderDate: '2024-08-02',
},
{
orderId: 'ORD-002',
customerName: 'Bob Smith',
products: [
{ name: 'Smartphone Case', quantity: 2, price: 19.99 },
{ name: 'Screen Protector', quantity: 1, price: 9.99 },
],
totalPrice: 49.97,
orderDate: '2024-08-03',
},
{
orderId: 'ORD-003',
customerName: 'Carol Davis',
products: [
{ name: 'Laptop', quantity: 1, price: 999.99 },
{ name: 'Mouse', quantity: 1, price: 29.99 },
],
totalPrice: 1029.98,
orderDate: '2024-08-04',
},
{
orderId: 'ORD-004',
customerName: 'David Wilson',
products: [{ name: 'Coffee Maker', quantity: 1, price: 79.99 }],
totalPrice: 79.99,
orderDate: '2024-08-05',
},
{
orderId: 'ORD-005',
customerName: 'Eva Brown',
products: [
{ name: 'Fitness Tracker', quantity: 1, price: 129.99 },
{ name: 'Water Bottle', quantity: 2, price: 14.99 },
],
totalPrice: 159.97,
orderDate: '2024-08-06',
},
{
orderId: 'ORD-006',
customerName: 'Frank Miller',
products: [
{ name: 'Gaming Console', quantity: 1, price: 499.99 },
{ name: 'Controller', quantity: 2, price: 59.99 },
],
totalPrice: 619.97,
orderDate: '2024-08-07',
},
{
orderId: 'ORD-007',
customerName: 'Grace Lee',
products: [{ name: 'Bluetooth Speaker', quantity: 1, price: 69.99 }],
totalPrice: 69.99,
orderDate: '2024-08-08',
},
{
orderId: 'ORD-008',
customerName: 'Henry Taylor',
products: [
{ name: 'Smartwatch', quantity: 1, price: 199.99 },
{ name: 'Watch Band', quantity: 2, price: 24.99 },
],
totalPrice: 249.97,
orderDate: '2024-08-09',
},
{
orderId: 'ORD-009',
customerName: 'Isla Garcia',
products: [
{ name: 'Tablet', quantity: 1, price: 349.99 },
{ name: 'Tablet Case', quantity: 1, price: 29.99 },
{ name: 'Stylus', quantity: 1, price: 39.99 },
],
totalPrice: 419.97,
orderDate: '2024-08-10',
},
{
orderId: 'ORD-010',
customerName: 'Jack Robinson',
products: [{ name: 'Wireless Charger', quantity: 2, price: 34.99 }],
totalPrice: 69.98,
orderDate: '2024-08-11',
},
];

main();
57 changes: 57 additions & 0 deletions examples/parsing-stream.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { zodResponseFormat } from 'openai/helpers/zod';
import OpenAI from 'openai/index';
import { z } from 'zod';

const Step = z.object({
explanation: z.string(),
output: z.string(),
});

const MathResponse = z.object({
steps: z.array(Step),
final_answer: z.string(),
});

async function main() {
const client = new OpenAI();

const stream = client.beta.chat.completions
.stream({
model: 'gpt-4o-2024-08-06',
messages: [
{
role: 'user',
content: `What's the weather like in SF?`,
},
],
response_format: zodResponseFormat(MathResponse, 'math_response'),
})
.on('refusal.delta', ({ delta }) => {
process.stdout.write(delta);
})
.on('refusal.done', () => console.log('\n\nrequest refused 😱'))
.on('content.delta', ({ snapshot, parsed }) => {
console.log('content:', snapshot);
console.log('parsed:', parsed);
console.log();
})
.on('content.done', (props) => {
if (props.parsed) {
console.log('\n\nfinished parsing!');
console.log(`answer: ${props.parsed.final_answer}`);
}
});

await stream.done();

const completion = await stream.finalChatCompletion();

console.dir(completion, { depth: 5 });

const message = completion.choices[0]?.message;
if (message?.parsed) {
console.log(message.parsed.steps);
}
}

main();
43 changes: 43 additions & 0 deletions examples/parsing-tools-stream.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { zodFunction } from 'openai/helpers/zod';
import OpenAI from 'openai/index';
import { z } from 'zod';

const GetWeatherArgs = z.object({
city: z.string(),
country: z.string(),
units: z.enum(['c', 'f']).default('c'),
});

async function main() {
const client = new OpenAI();
const refusal = process.argv.includes('refusal');

const stream = client.beta.chat.completions
.stream({
model: 'gpt-4o-2024-08-06',
messages: [
{
role: 'user',
content: refusal ? 'How do I make anthrax?' : `What's the weather like in SF?`,
},
],
tools: [zodFunction({ name: 'get_weather', parameters: GetWeatherArgs })],
})
.on('tool_calls.function.arguments.delta', (props) =>
console.log('tool_calls.function.arguments.delta', props),
)
.on('tool_calls.function.arguments.done', (props) =>
console.log('tool_calls.function.arguments.done', props),
)
.on('refusal.delta', ({ delta }) => {
process.stdout.write(delta);
})
.on('refusal.done', () => console.log('\n\nrequest refused 😱'));

const completion = await stream.finalChatCompletion();

console.log('final completion:');
console.dir(completion, { depth: 10 });
}

main();
67 changes: 67 additions & 0 deletions examples/parsing-tools.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { zodFunction } from 'openai/helpers/zod';
import OpenAI from 'openai/index';
import { z } from 'zod';

const Table = z.enum(['orders', 'customers', 'products']);

const Column = z.enum([
'id',
'status',
'expected_delivery_date',
'delivered_at',
'shipped_at',
'ordered_at',
'canceled_at',
]);

const Operator = z.enum(['=', '>', '<', '<=', '>=', '!=']);

const OrderBy = z.enum(['asc', 'desc']);

const DynamicValue = z.object({
column_name: z.string(),
});

const Condition = z.object({
column: z.string(),
operator: Operator,
value: z.union([z.string(), z.number(), DynamicValue]),
});

const Query = z.object({
table_name: Table,
columns: z.array(Column),
conditions: z.array(Condition),
order_by: OrderBy,
});

async function main() {
const client = new OpenAI();

const completion = await client.beta.chat.completions.parse({
model: 'gpt-4o-2024-08-06',
messages: [
{
role: 'system',
content:
'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.',
},
{
role: 'user',
content:
'look up all my orders in november of last year that were fulfilled but not delivered on time',
},
],
tools: [zodFunction({ name: 'query', parameters: Query })],
});
console.dir(completion, { depth: 10 });

const toolCall = completion.choices[0]?.message.tool_calls?.[0];
if (toolCall) {
const args = toolCall.function.parsed_arguments as z.infer<typeof Query>;
console.log(args);
console.log(args.table_name);
}
}

main();
Loading

0 comments on commit 573787c

Please sign in to comment.