Skip to content

Commit fc18132

Browse files
authoredNov 26, 2024··
feat (ai/core): experimental structured output for generateText (#3867)
1 parent 6373c60 commit fc18132

24 files changed

+1176
-191
lines changed
 

‎.changeset/moody-pens-jam.md

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
'@ai-sdk/openai-compatible': patch
3+
'@ai-sdk/openai': patch
4+
'ai': patch
5+
---
6+
7+
feat (ai/core): experimental output for generateText

‎content/docs/03-ai-sdk-core/10-generating-structured-data.mdx

+32
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,38 @@ async function generateRecipe(
224224
}
225225
```
226226

227+
## Structured output with `generateText`
228+
229+
<Note type="warning">
230+
Structured output with `generateText` is experimental and may change in the
231+
future.
232+
</Note>
233+
234+
You can also generate structured data with `generateText` by using the `experimental_output` setting.
235+
This enables you to use structured outputs together with tool calling (for models that support it - currently only OpenAI).
236+
237+
```ts highlight="1,3,4"
238+
const { experimental_output } = await generateText({
239+
// ...
240+
experimental_output: Output.object({
241+
schema: z.object({
242+
name: z.string(),
243+
age: z.number().nullable().describe('Age of the person.'),
244+
contact: z.object({
245+
type: z.literal('email'),
246+
value: z.string(),
247+
}),
248+
occupation: z.object({
249+
type: z.literal('employed'),
250+
company: z.string(),
251+
position: z.string(),
252+
}),
253+
}),
254+
}),
255+
prompt: 'Generate an example person for testing.',
256+
});
257+
```
258+
227259
## More Examples
228260

229261
You can see `generateObject` and `streamObject` in action using various frameworks in the following examples:

‎content/docs/03-ai-sdk-core/17-agents.mdx

+2
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,8 @@ You can use an **answer tool** and the `toolChoice: 'required'` setting to force
5959
the LLM to answer with a structured output that matches the schema of the answer tool.
6060
The answer tool has no `execute` function, so invoking it will terminate the agent.
6161

62+
Alternatively, you can use the [`experimental_output`](/docs/ai-sdk-core/generating-structured-data#structured-output-with-generatetext) setting for `generateText` to generate structured outputs.
63+
6264
### Example
6365

6466
```ts highlight="6,16-29,31,45"

‎content/docs/07-reference/01-ai-sdk-core/01-generate-text.mdx

+35
Original file line numberDiff line numberDiff line change
@@ -468,6 +468,41 @@ To see `generateText` in action, check out [these examples](#examples).
468468
description:
469469
'The tools that are currently active. All tools are active by default.',
470470
},
471+
{
472+
name: 'experimental_output',
473+
type: 'Output',
474+
isOptional: true,
475+
description: 'Experimental setting for generating structured outputs.',
476+
properties: [
477+
{
478+
type: 'Output',
479+
parameters: [
480+
{
481+
name: 'Output.text()',
482+
type: 'Output',
483+
description: 'Forward text output.',
484+
},
485+
{
486+
name: 'Output.object()',
487+
type: 'Output',
488+
description: 'Generate a JSON object of type OBJECT.',
489+
properties: [
490+
{
491+
type: 'Options',
492+
parameters: [
493+
{
494+
name: 'schema',
495+
type: 'Schema<OBJECT>',
496+
description: 'The schema of the JSON object to generate.',
497+
},
498+
],
499+
},
500+
],
501+
},
502+
],
503+
},
504+
],
505+
},
471506
{
472507
name: 'onStepFinish',
473508
type: '(result: onStepFinishResult) => Promise<void> | void',
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { google } from '@ai-sdk/google';
2+
import { generateText, Output } from 'ai';
3+
import 'dotenv/config';
4+
import { z } from 'zod';
5+
6+
async function main() {
7+
const { experimental_output } = await generateText({
8+
model: google('gemini-1.5-flash'),
9+
experimental_output: Output.object({
10+
schema: z.object({
11+
name: z.string(),
12+
age: z.number().nullable().describe('Age of the person.'),
13+
contact: z.object({
14+
type: z.literal('email'),
15+
value: z.string(),
16+
}),
17+
occupation: z.object({
18+
type: z.literal('employed'),
19+
company: z.string(),
20+
position: z.string(),
21+
}),
22+
}),
23+
}),
24+
prompt: 'Generate an example person for testing.',
25+
});
26+
27+
console.log(experimental_output);
28+
}
29+
30+
main().catch(console.error);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { vertex } from '@ai-sdk/google-vertex';
2+
import { generateText, Output } from 'ai';
3+
import 'dotenv/config';
4+
import { z } from 'zod';
5+
6+
async function main() {
7+
const { experimental_output } = await generateText({
8+
model: vertex('gemini-1.5-flash'),
9+
experimental_output: Output.object({
10+
schema: z.object({
11+
name: z.string(),
12+
age: z.number().nullable().describe('Age of the person.'),
13+
contact: z.object({
14+
type: z.literal('email'),
15+
value: z.string(),
16+
}),
17+
occupation: z.object({
18+
type: z.literal('employed'),
19+
company: z.string(),
20+
position: z.string(),
21+
}),
22+
}),
23+
}),
24+
prompt: 'Generate an example person for testing.',
25+
});
26+
27+
console.log(experimental_output);
28+
}
29+
30+
main().catch(console.error);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import { openai } from '@ai-sdk/openai';
2+
import { generateText, Output, tool } from 'ai';
3+
import 'dotenv/config';
4+
import { z } from 'zod';
5+
6+
async function main() {
7+
const { experimental_output } = await generateText({
8+
model: openai('gpt-4o-mini', { structuredOutputs: true }),
9+
tools: {
10+
weather: tool({
11+
description: 'Get the weather in a location',
12+
parameters: z.object({
13+
location: z.string().describe('The location to get the weather for'),
14+
}),
15+
// location below is inferred to be a string:
16+
execute: async ({ location }) => ({
17+
location,
18+
temperature: 72 + Math.floor(Math.random() * 21) - 10,
19+
}),
20+
}),
21+
},
22+
experimental_output: Output.object({
23+
schema: z.object({
24+
location: z.string(),
25+
temperature: z.number(),
26+
}),
27+
}),
28+
maxSteps: 2,
29+
prompt: 'What is the weather in San Francisco?',
30+
});
31+
32+
// { location: 'San Francisco', temperature: 81 }
33+
console.log(experimental_output);
34+
}
35+
36+
main().catch(console.error);

‎packages/ai/core/generate-object/generate-object-result.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,11 @@ import { LanguageModelUsage } from '../types/usage';
1111
/**
1212
The result of a `generateObject` call.
1313
*/
14-
export interface GenerateObjectResult<T> {
14+
export interface GenerateObjectResult<OBJECT> {
1515
/**
1616
The generated object (typed according to the schema).
1717
*/
18-
readonly object: T;
18+
readonly object: OBJECT;
1919

2020
/**
2121
The reason why the generation finished.

‎packages/ai/core/generate-text/generate-text-result.ts

+9-1
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,20 @@ import { ToolResultArray } from './tool-result';
1717
The result of a `generateText` call.
1818
It contains the generated text, the tool calls that were made during the generation, and the results of the tool calls.
1919
*/
20-
export interface GenerateTextResult<TOOLS extends Record<string, CoreTool>> {
20+
export interface GenerateTextResult<
21+
TOOLS extends Record<string, CoreTool>,
22+
OUTPUT,
23+
> {
2124
/**
2225
The generated text.
2326
*/
2427
readonly text: string;
2528

29+
/**
30+
The generated output.
31+
*/
32+
readonly experimental_output: OUTPUT;
33+
2634
/**
2735
The tool calls that were made during the generation.
2836
*/

‎packages/ai/core/generate-text/generate-text.test.ts

+200-3
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
import { jsonSchema } from '@ai-sdk/ui-utils';
22
import assert from 'node:assert';
33
import { z } from 'zod';
4+
import { Output } from '.';
45
import { MockLanguageModelV1 } from '../test/mock-language-model-v1';
56
import { MockTracer } from '../test/mock-tracer';
67
import { tool } from '../tool/tool';
78
import { generateText } from './generate-text';
89
import { GenerateTextResult } from './generate-text-result';
910
import { StepResult } from './step-result';
11+
import { LanguageModelV1CallOptions } from '@ai-sdk/provider';
1012

1113
const dummyResponseValues = {
1214
rawCall: { rawPrompt: 'prompt', rawSettings: {} },
@@ -19,7 +21,7 @@ describe('result.text', () => {
1921
const result = await generateText({
2022
model: new MockLanguageModelV1({
2123
doGenerate: async ({ prompt, mode }) => {
22-
assert.deepStrictEqual(mode, {
24+
expect(mode).toStrictEqual({
2325
type: 'regular',
2426
tools: undefined,
2527
toolChoice: undefined,
@@ -338,7 +340,7 @@ describe('result.response', () => {
338340

339341
describe('options.maxSteps', () => {
340342
describe('2 steps: initial, tool-result', () => {
341-
let result: GenerateTextResult<any>;
343+
let result: GenerateTextResult<any, any>;
342344
let onStepFinishResults: StepResult<any>[];
343345

344346
beforeEach(async () => {
@@ -531,7 +533,7 @@ describe('options.maxSteps', () => {
531533
});
532534

533535
describe('4 steps: initial, continue, continue, continue', () => {
534-
let result: GenerateTextResult<any>;
536+
let result: GenerateTextResult<any, any>;
535537
let onStepFinishResults: StepResult<any>[];
536538

537539
beforeEach(async () => {
@@ -1166,3 +1168,198 @@ describe('options.messages', () => {
11661168
expect(result.text).toStrictEqual('Hello, world!');
11671169
});
11681170
});
1171+
1172+
describe('options.output', () => {
1173+
describe('no output', () => {
1174+
it('should have undefined output', async () => {
1175+
const result = await generateText({
1176+
model: new MockLanguageModelV1({
1177+
doGenerate: async () => ({
1178+
...dummyResponseValues,
1179+
text: `Hello, world!`,
1180+
}),
1181+
}),
1182+
prompt: 'prompt',
1183+
});
1184+
1185+
expect(result.experimental_output).toBeUndefined();
1186+
});
1187+
});
1188+
1189+
describe('text output', () => {
1190+
it('should forward text as output', async () => {
1191+
const result = await generateText({
1192+
model: new MockLanguageModelV1({
1193+
doGenerate: async () => ({
1194+
...dummyResponseValues,
1195+
text: `Hello, world!`,
1196+
}),
1197+
}),
1198+
prompt: 'prompt',
1199+
experimental_output: Output.text(),
1200+
});
1201+
1202+
expect(result.experimental_output).toStrictEqual('Hello, world!');
1203+
});
1204+
1205+
it('should set responseFormat to text and not change the prompt', async () => {
1206+
let callOptions: LanguageModelV1CallOptions;
1207+
1208+
await generateText({
1209+
model: new MockLanguageModelV1({
1210+
doGenerate: async args => {
1211+
callOptions = args;
1212+
return {
1213+
...dummyResponseValues,
1214+
text: `Hello, world!`,
1215+
};
1216+
},
1217+
}),
1218+
prompt: 'prompt',
1219+
experimental_output: Output.text(),
1220+
});
1221+
1222+
expect(callOptions!).toEqual({
1223+
temperature: 0,
1224+
mode: { type: 'regular' },
1225+
responseFormat: { type: 'text' },
1226+
inputFormat: 'prompt',
1227+
prompt: [
1228+
{
1229+
content: [{ text: 'prompt', type: 'text' }],
1230+
providerMetadata: undefined,
1231+
role: 'user',
1232+
},
1233+
],
1234+
});
1235+
});
1236+
});
1237+
1238+
describe('object output', () => {
1239+
describe('without structured output model', () => {
1240+
it('should parse the output', async () => {
1241+
const result = await generateText({
1242+
model: new MockLanguageModelV1({
1243+
supportsStructuredOutputs: false,
1244+
doGenerate: async () => ({
1245+
...dummyResponseValues,
1246+
text: `{ "value": "test-value" }`,
1247+
}),
1248+
}),
1249+
prompt: 'prompt',
1250+
experimental_output: Output.object({
1251+
schema: z.object({ value: z.string() }),
1252+
}),
1253+
});
1254+
1255+
expect(result.experimental_output).toEqual({ value: 'test-value' });
1256+
});
1257+
1258+
it('should set responseFormat to json and inject schema and JSON instruction into the prompt', async () => {
1259+
let callOptions: LanguageModelV1CallOptions;
1260+
1261+
await generateText({
1262+
model: new MockLanguageModelV1({
1263+
supportsStructuredOutputs: false,
1264+
doGenerate: async args => {
1265+
callOptions = args;
1266+
return {
1267+
...dummyResponseValues,
1268+
text: `{ "value": "test-value" }`,
1269+
};
1270+
},
1271+
}),
1272+
prompt: 'prompt',
1273+
experimental_output: Output.object({
1274+
schema: z.object({ value: z.string() }),
1275+
}),
1276+
});
1277+
1278+
expect(callOptions!).toEqual({
1279+
temperature: 0,
1280+
mode: { type: 'regular' },
1281+
inputFormat: 'prompt',
1282+
responseFormat: { type: 'json', schema: undefined },
1283+
prompt: [
1284+
{
1285+
content:
1286+
'JSON schema:\n' +
1287+
'{"type":"object","properties":{"value":{"type":"string"}},"required":["value"],"additionalProperties":false,"$schema":"http://json-schema.org/draft-07/schema#"}\n' +
1288+
'You MUST answer with a JSON object that matches the JSON schema above.',
1289+
role: 'system',
1290+
},
1291+
{
1292+
content: [{ text: 'prompt', type: 'text' }],
1293+
providerMetadata: undefined,
1294+
role: 'user',
1295+
},
1296+
],
1297+
});
1298+
});
1299+
});
1300+
1301+
describe('with structured output model', () => {
1302+
it('should parse the output', async () => {
1303+
const result = await generateText({
1304+
model: new MockLanguageModelV1({
1305+
supportsStructuredOutputs: true,
1306+
doGenerate: async () => ({
1307+
...dummyResponseValues,
1308+
text: `{ "value": "test-value" }`,
1309+
}),
1310+
}),
1311+
prompt: 'prompt',
1312+
experimental_output: Output.object({
1313+
schema: z.object({ value: z.string() }),
1314+
}),
1315+
});
1316+
1317+
expect(result.experimental_output).toEqual({ value: 'test-value' });
1318+
});
1319+
1320+
it('should set responseFormat to json and send schema as part of the responseFormat', async () => {
1321+
let callOptions: LanguageModelV1CallOptions;
1322+
1323+
await generateText({
1324+
model: new MockLanguageModelV1({
1325+
supportsStructuredOutputs: true,
1326+
doGenerate: async args => {
1327+
callOptions = args;
1328+
return {
1329+
...dummyResponseValues,
1330+
text: `{ "value": "test-value" }`,
1331+
};
1332+
},
1333+
}),
1334+
prompt: 'prompt',
1335+
experimental_output: Output.object({
1336+
schema: z.object({ value: z.string() }),
1337+
}),
1338+
});
1339+
1340+
expect(callOptions!).toEqual({
1341+
temperature: 0,
1342+
mode: { type: 'regular' },
1343+
inputFormat: 'prompt',
1344+
responseFormat: {
1345+
type: 'json',
1346+
schema: {
1347+
$schema: 'http://json-schema.org/draft-07/schema#',
1348+
additionalProperties: false,
1349+
properties: { value: { type: 'string' } },
1350+
required: ['value'],
1351+
type: 'object',
1352+
},
1353+
},
1354+
prompt: [
1355+
{
1356+
content: [{ text: 'prompt', type: 'text' }],
1357+
providerMetadata: undefined,
1358+
role: 'user',
1359+
},
1360+
],
1361+
});
1362+
});
1363+
});
1364+
});
1365+
});

‎packages/ai/core/generate-text/generate-text.ts

+54-28
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import {
2323
} from '../types/usage';
2424
import { removeTextAfterLastWhitespace } from '../util/remove-text-after-last-whitespace';
2525
import { GenerateTextResult } from './generate-text-result';
26+
import { Output } from './output';
2627
import { parseToolCall } from './parse-tool-call';
2728
import { StepResult } from './step-result';
2829
import { toResponseMessages } from './to-response-messages';
@@ -77,7 +78,10 @@ If set and supported by the model, calls will generate deterministic results.
7778
@returns
7879
A result object that contains the generated text, the results of the tool calls, and additional information.
7980
*/
80-
export async function generateText<TOOLS extends Record<string, CoreTool>>({
81+
export async function generateText<
82+
TOOLS extends Record<string, CoreTool>,
83+
OUTPUT = never,
84+
>({
8185
model,
8286
tools,
8387
toolChoice,
@@ -88,6 +92,7 @@ export async function generateText<TOOLS extends Record<string, CoreTool>>({
8892
abortSignal,
8993
headers,
9094
maxSteps = 1,
95+
experimental_output: output,
9196
experimental_continueSteps: continueSteps = false,
9297
experimental_telemetry: telemetry,
9398
experimental_providerMetadata: providerMetadata,
@@ -149,6 +154,8 @@ changing the tool call and result types in the result.
149154
*/
150155
experimental_activeTools?: Array<keyof TOOLS>;
151156

157+
experimental_output?: Output<OUTPUT>;
158+
152159
/**
153160
Callback that is called when each step (LLM call) is finished, including intermediate steps.
154161
*/
@@ -161,7 +168,7 @@ changing the tool call and result types in the result.
161168
generateId?: () => string;
162169
currentDate?: () => Date;
163170
};
164-
}): Promise<GenerateTextResult<TOOLS>> {
171+
}): Promise<GenerateTextResult<TOOLS, OUTPUT>> {
165172
if (maxSteps < 1) {
166173
throw new InvalidArgumentError({
167174
parameter: 'maxSteps',
@@ -180,7 +187,11 @@ changing the tool call and result types in the result.
180187
});
181188

182189
const initialPrompt = standardizePrompt({
183-
prompt: { system, prompt, messages },
190+
prompt: {
191+
system: output?.injectIntoSystemPrompt({ system, model }) ?? system,
192+
prompt,
193+
messages,
194+
},
184195
tools,
185196
});
186197

@@ -221,7 +232,7 @@ changing the tool call and result types in the result.
221232
const responseMessages: Array<CoreAssistantMessage | CoreToolMessage> =
222233
[];
223234
let text = '';
224-
const steps: GenerateTextResult<TOOLS>['steps'] = [];
235+
const steps: GenerateTextResult<TOOLS, OUTPUT>['steps'] = [];
225236
const usage: LanguageModelUsage = {
226237
completionTokens: 0,
227238
promptTokens: 0,
@@ -293,6 +304,7 @@ changing the tool call and result types in the result.
293304
mode,
294305
...callSettings,
295306
inputFormat: promptFormat,
307+
responseFormat: output?.responseFormat({ model }),
296308
prompt: promptMessages,
297309
providerMetadata,
298310
abortSignal,
@@ -481,6 +493,8 @@ changing the tool call and result types in the result.
481493

482494
return new DefaultGenerateTextResult({
483495
text,
496+
output:
497+
output == null ? (undefined as never) : output.parseOutput({ text }),
484498
toolCalls: currentToolCalls,
485499
toolResults: currentToolResults,
486500
finishReason: currentModelResponse.finishReason,
@@ -582,33 +596,44 @@ async function executeTools<TOOLS extends Record<string, CoreTool>>({
582596
);
583597
}
584598

585-
class DefaultGenerateTextResult<TOOLS extends Record<string, CoreTool>>
586-
implements GenerateTextResult<TOOLS>
599+
class DefaultGenerateTextResult<TOOLS extends Record<string, CoreTool>, OUTPUT>
600+
implements GenerateTextResult<TOOLS, OUTPUT>
587601
{
588-
readonly text: GenerateTextResult<TOOLS>['text'];
589-
readonly toolCalls: GenerateTextResult<TOOLS>['toolCalls'];
590-
readonly toolResults: GenerateTextResult<TOOLS>['toolResults'];
591-
readonly finishReason: GenerateTextResult<TOOLS>['finishReason'];
592-
readonly usage: GenerateTextResult<TOOLS>['usage'];
593-
readonly warnings: GenerateTextResult<TOOLS>['warnings'];
594-
readonly steps: GenerateTextResult<TOOLS>['steps'];
595-
readonly logprobs: GenerateTextResult<TOOLS>['logprobs'];
596-
readonly experimental_providerMetadata: GenerateTextResult<TOOLS>['experimental_providerMetadata'];
597-
readonly response: GenerateTextResult<TOOLS>['response'];
598-
readonly request: GenerateTextResult<TOOLS>['request'];
602+
readonly text: GenerateTextResult<TOOLS, OUTPUT>['text'];
603+
readonly toolCalls: GenerateTextResult<TOOLS, OUTPUT>['toolCalls'];
604+
readonly toolResults: GenerateTextResult<TOOLS, OUTPUT>['toolResults'];
605+
readonly finishReason: GenerateTextResult<TOOLS, OUTPUT>['finishReason'];
606+
readonly usage: GenerateTextResult<TOOLS, OUTPUT>['usage'];
607+
readonly warnings: GenerateTextResult<TOOLS, OUTPUT>['warnings'];
608+
readonly steps: GenerateTextResult<TOOLS, OUTPUT>['steps'];
609+
readonly logprobs: GenerateTextResult<TOOLS, OUTPUT>['logprobs'];
610+
readonly experimental_providerMetadata: GenerateTextResult<
611+
TOOLS,
612+
OUTPUT
613+
>['experimental_providerMetadata'];
614+
readonly response: GenerateTextResult<TOOLS, OUTPUT>['response'];
615+
readonly request: GenerateTextResult<TOOLS, OUTPUT>['request'];
616+
readonly experimental_output: GenerateTextResult<
617+
TOOLS,
618+
OUTPUT
619+
>['experimental_output'];
599620

600621
constructor(options: {
601-
text: GenerateTextResult<TOOLS>['text'];
602-
toolCalls: GenerateTextResult<TOOLS>['toolCalls'];
603-
toolResults: GenerateTextResult<TOOLS>['toolResults'];
604-
finishReason: GenerateTextResult<TOOLS>['finishReason'];
605-
usage: GenerateTextResult<TOOLS>['usage'];
606-
warnings: GenerateTextResult<TOOLS>['warnings'];
607-
logprobs: GenerateTextResult<TOOLS>['logprobs'];
608-
steps: GenerateTextResult<TOOLS>['steps'];
609-
providerMetadata: GenerateTextResult<TOOLS>['experimental_providerMetadata'];
610-
response: GenerateTextResult<TOOLS>['response'];
611-
request: GenerateTextResult<TOOLS>['request'];
622+
text: GenerateTextResult<TOOLS, OUTPUT>['text'];
623+
toolCalls: GenerateTextResult<TOOLS, OUTPUT>['toolCalls'];
624+
toolResults: GenerateTextResult<TOOLS, OUTPUT>['toolResults'];
625+
finishReason: GenerateTextResult<TOOLS, OUTPUT>['finishReason'];
626+
usage: GenerateTextResult<TOOLS, OUTPUT>['usage'];
627+
warnings: GenerateTextResult<TOOLS, OUTPUT>['warnings'];
628+
logprobs: GenerateTextResult<TOOLS, OUTPUT>['logprobs'];
629+
steps: GenerateTextResult<TOOLS, OUTPUT>['steps'];
630+
providerMetadata: GenerateTextResult<
631+
TOOLS,
632+
OUTPUT
633+
>['experimental_providerMetadata'];
634+
response: GenerateTextResult<TOOLS, OUTPUT>['response'];
635+
request: GenerateTextResult<TOOLS, OUTPUT>['request'];
636+
output: GenerateTextResult<TOOLS, OUTPUT>['experimental_output'];
612637
}) {
613638
this.text = options.text;
614639
this.toolCalls = options.toolCalls;
@@ -621,5 +646,6 @@ class DefaultGenerateTextResult<TOOLS extends Record<string, CoreTool>>
621646
this.steps = options.steps;
622647
this.experimental_providerMetadata = options.providerMetadata;
623648
this.logprobs = options.logprobs;
649+
this.experimental_output = options.output;
624650
}
625651
}

‎packages/ai/core/generate-text/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ export type { GenerateTextResult } from './generate-text-result';
33
export type { StepResult } from './step-result';
44
export { streamText } from './stream-text';
55
export type { StreamTextResult, TextStreamPart } from './stream-text-result';
6+
export * as Output from './output';
67

78
// TODO 4.1: rename to ToolCall and ToolResult, deprecate old names
89
export type {
+60
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import { parseJSON } from '@ai-sdk/provider-utils';
2+
import { asSchema, Schema } from '@ai-sdk/ui-utils';
3+
import { z } from 'zod';
4+
import { injectJsonInstruction } from '../generate-object/inject-json-instruction';
5+
import {
6+
LanguageModel,
7+
LanguageModelV1CallOptions,
8+
} from '../types/language-model';
9+
10+
export interface Output<OUTPUT> {
11+
readonly type: 'object' | 'text';
12+
injectIntoSystemPrompt(options: {
13+
system: string | undefined;
14+
model: LanguageModel;
15+
}): string | undefined;
16+
responseFormat: (options: {
17+
model: LanguageModel;
18+
}) => LanguageModelV1CallOptions['responseFormat'];
19+
parseOutput(options: { text: string }): OUTPUT;
20+
}
21+
22+
export const text = (): Output<string> => ({
23+
type: 'text',
24+
responseFormat: () => ({ type: 'text' }),
25+
injectIntoSystemPrompt({ system }: { system: string | undefined }) {
26+
return system;
27+
},
28+
parseOutput({ text }: { text: string }) {
29+
return text;
30+
},
31+
});
32+
33+
export const object = <OUTPUT>({
34+
schema: inputSchema,
35+
}: {
36+
schema: z.Schema<OUTPUT, z.ZodTypeDef, any> | Schema<OUTPUT>;
37+
}): Output<OUTPUT> => {
38+
const schema = asSchema(inputSchema);
39+
40+
return {
41+
type: 'object',
42+
responseFormat: ({ model }) => ({
43+
type: 'json',
44+
schema: model.supportsStructuredOutputs ? schema.jsonSchema : undefined,
45+
}),
46+
injectIntoSystemPrompt({ system, model }) {
47+
// when the model supports structured outputs,
48+
// we can use the system prompt as is:
49+
return model.supportsStructuredOutputs
50+
? system
51+
: injectJsonInstruction({
52+
prompt: system,
53+
schema: schema.jsonSchema,
54+
});
55+
},
56+
parseOutput({ text }: { text: string }) {
57+
return parseJSON({ text, schema });
58+
},
59+
};
60+
};

‎packages/ai/core/generate-text/text-output.ts

Whitespace-only changes.

‎packages/google-vertex/src/google-vertex-language-model.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ export class GoogleVertexLanguageModel implements LanguageModelV1 {
3838
readonly supportsImageUrls = false;
3939

4040
get supportsStructuredOutputs() {
41-
return this.settings.structuredOutputs !== false;
41+
return this.settings.structuredOutputs ?? true;
4242
}
4343

4444
readonly modelId: GoogleVertexModelId;

‎packages/google/src/google-generative-ai-language-model.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ export class GoogleGenerativeAILanguageModel implements LanguageModelV1 {
3939
readonly supportsImageUrls = false;
4040

4141
get supportsStructuredOutputs() {
42-
return this.settings.structuredOutputs !== false;
42+
return this.settings.structuredOutputs ?? true;
4343
}
4444

4545
readonly modelId: GoogleGenerativeAIModelId;

‎packages/openai-compatible/src/openai-compatible-api-types.ts

+1
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ export interface OpenAICompatibleMessageToolCall {
4242
function: {
4343
arguments: string;
4444
name: string;
45+
strict?: boolean;
4546
};
4647
}
4748

‎packages/openai-compatible/src/openai-compatible-chat-language-model.test.ts

+359
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
convertReadableStreamToArray,
66
} from '@ai-sdk/provider-utils/test';
77
import { createOpenAICompatible } from './openai-compatible-provider';
8+
import { OpenAICompatibleChatLanguageModel } from './openai-compatible-chat-language-model';
89

910
const TEST_PROMPT: LanguageModelV1Prompt = [
1011
{ role: 'user', content: [{ type: 'text', text: 'Hello' }] },
@@ -386,6 +387,364 @@ describe('doGenerate', () => {
386387
]);
387388
});
388389

390+
describe('response format', () => {
391+
it('should not send a response_format when response format is text', async () => {
392+
prepareJsonResponse({ content: '{"value":"Spark"}' });
393+
394+
const model = new OpenAICompatibleChatLanguageModel(
395+
'gpt-4o-2024-08-06',
396+
{},
397+
{
398+
provider: 'test-provider',
399+
url: () => 'https://my.api.com/v1/chat/completions',
400+
headers: () => ({}),
401+
supportsStructuredOutputs: false,
402+
},
403+
);
404+
405+
await model.doGenerate({
406+
inputFormat: 'prompt',
407+
mode: { type: 'regular' },
408+
prompt: TEST_PROMPT,
409+
responseFormat: { type: 'text' },
410+
});
411+
412+
expect(await server.getRequestBodyJson()).toStrictEqual({
413+
model: 'gpt-4o-2024-08-06',
414+
messages: [{ role: 'user', content: 'Hello' }],
415+
});
416+
});
417+
418+
it('should forward json response format as "json_object" without schema', async () => {
419+
prepareJsonResponse({ content: '{"value":"Spark"}' });
420+
421+
const model = provider('gpt-4o-2024-08-06');
422+
423+
await model.doGenerate({
424+
inputFormat: 'prompt',
425+
mode: { type: 'regular' },
426+
prompt: TEST_PROMPT,
427+
responseFormat: { type: 'json' },
428+
});
429+
430+
expect(await server.getRequestBodyJson()).toStrictEqual({
431+
model: 'gpt-4o-2024-08-06',
432+
messages: [{ role: 'user', content: 'Hello' }],
433+
response_format: { type: 'json_object' },
434+
});
435+
});
436+
437+
it('should forward json response format as "json_object" and omit schema when structuredOutputs are disabled', async () => {
438+
prepareJsonResponse({ content: '{"value":"Spark"}' });
439+
440+
const model = new OpenAICompatibleChatLanguageModel(
441+
'gpt-4o-2024-08-06',
442+
{},
443+
{
444+
provider: 'test-provider',
445+
url: () => 'https://my.api.com/v1/chat/completions',
446+
headers: () => ({}),
447+
supportsStructuredOutputs: false,
448+
},
449+
);
450+
451+
const { warnings } = await model.doGenerate({
452+
inputFormat: 'prompt',
453+
mode: { type: 'regular' },
454+
prompt: TEST_PROMPT,
455+
responseFormat: {
456+
type: 'json',
457+
schema: {
458+
type: 'object',
459+
properties: { value: { type: 'string' } },
460+
required: ['value'],
461+
additionalProperties: false,
462+
$schema: 'http://json-schema.org/draft-07/schema#',
463+
},
464+
},
465+
});
466+
467+
expect(await server.getRequestBodyJson()).toStrictEqual({
468+
model: 'gpt-4o-2024-08-06',
469+
messages: [{ role: 'user', content: 'Hello' }],
470+
response_format: { type: 'json_object' },
471+
});
472+
473+
expect(warnings).toEqual([
474+
{
475+
details:
476+
'JSON response format schema is only supported with structuredOutputs',
477+
setting: 'responseFormat',
478+
type: 'unsupported-setting',
479+
},
480+
]);
481+
});
482+
483+
it('should forward json response format as "json_object" and include schema when structuredOutputs are enabled', async () => {
484+
prepareJsonResponse({ content: '{"value":"Spark"}' });
485+
486+
const model = new OpenAICompatibleChatLanguageModel(
487+
'gpt-4o-2024-08-06',
488+
{},
489+
{
490+
provider: 'test-provider',
491+
url: () => 'https://my.api.com/v1/chat/completions',
492+
headers: () => ({}),
493+
supportsStructuredOutputs: true,
494+
},
495+
);
496+
497+
const { warnings } = await model.doGenerate({
498+
inputFormat: 'prompt',
499+
mode: { type: 'regular' },
500+
prompt: TEST_PROMPT,
501+
responseFormat: {
502+
type: 'json',
503+
schema: {
504+
type: 'object',
505+
properties: { value: { type: 'string' } },
506+
required: ['value'],
507+
additionalProperties: false,
508+
$schema: 'http://json-schema.org/draft-07/schema#',
509+
},
510+
},
511+
});
512+
513+
expect(await server.getRequestBodyJson()).toStrictEqual({
514+
model: 'gpt-4o-2024-08-06',
515+
messages: [{ role: 'user', content: 'Hello' }],
516+
response_format: {
517+
type: 'json_schema',
518+
json_schema: {
519+
name: 'response',
520+
strict: true,
521+
schema: {
522+
type: 'object',
523+
properties: { value: { type: 'string' } },
524+
required: ['value'],
525+
additionalProperties: false,
526+
$schema: 'http://json-schema.org/draft-07/schema#',
527+
},
528+
},
529+
},
530+
});
531+
532+
expect(warnings).toEqual([]);
533+
});
534+
535+
it('should use json_schema & strict in object-json mode when structuredOutputs are enabled', async () => {
536+
prepareJsonResponse({ content: '{"value":"Spark"}' });
537+
538+
const model = new OpenAICompatibleChatLanguageModel(
539+
'gpt-4o-2024-08-06',
540+
{},
541+
{
542+
provider: 'test-provider',
543+
url: () => 'https://my.api.com/v1/chat/completions',
544+
headers: () => ({}),
545+
supportsStructuredOutputs: true,
546+
},
547+
);
548+
549+
await model.doGenerate({
550+
inputFormat: 'prompt',
551+
mode: {
552+
type: 'object-json',
553+
schema: {
554+
type: 'object',
555+
properties: { value: { type: 'string' } },
556+
required: ['value'],
557+
additionalProperties: false,
558+
$schema: 'http://json-schema.org/draft-07/schema#',
559+
},
560+
},
561+
prompt: TEST_PROMPT,
562+
});
563+
564+
expect(await server.getRequestBodyJson()).toStrictEqual({
565+
model: 'gpt-4o-2024-08-06',
566+
messages: [{ role: 'user', content: 'Hello' }],
567+
response_format: {
568+
type: 'json_schema',
569+
json_schema: {
570+
name: 'response',
571+
strict: true,
572+
schema: {
573+
type: 'object',
574+
properties: { value: { type: 'string' } },
575+
required: ['value'],
576+
additionalProperties: false,
577+
$schema: 'http://json-schema.org/draft-07/schema#',
578+
},
579+
},
580+
},
581+
});
582+
});
583+
584+
it('should set name & description in object-json mode when structuredOutputs are enabled', async () => {
585+
prepareJsonResponse({ content: '{"value":"Spark"}' });
586+
587+
const model = new OpenAICompatibleChatLanguageModel(
588+
'gpt-4o-2024-08-06',
589+
{},
590+
{
591+
provider: 'test-provider',
592+
url: () => 'https://my.api.com/v1/chat/completions',
593+
headers: () => ({}),
594+
supportsStructuredOutputs: true,
595+
},
596+
);
597+
598+
await model.doGenerate({
599+
inputFormat: 'prompt',
600+
mode: {
601+
type: 'object-json',
602+
name: 'test-name',
603+
description: 'test description',
604+
schema: {
605+
type: 'object',
606+
properties: { value: { type: 'string' } },
607+
required: ['value'],
608+
additionalProperties: false,
609+
$schema: 'http://json-schema.org/draft-07/schema#',
610+
},
611+
},
612+
prompt: TEST_PROMPT,
613+
});
614+
615+
expect(await server.getRequestBodyJson()).toStrictEqual({
616+
model: 'gpt-4o-2024-08-06',
617+
messages: [{ role: 'user', content: 'Hello' }],
618+
response_format: {
619+
type: 'json_schema',
620+
json_schema: {
621+
name: 'test-name',
622+
description: 'test description',
623+
strict: true,
624+
schema: {
625+
type: 'object',
626+
properties: { value: { type: 'string' } },
627+
required: ['value'],
628+
additionalProperties: false,
629+
$schema: 'http://json-schema.org/draft-07/schema#',
630+
},
631+
},
632+
},
633+
});
634+
});
635+
636+
it('should allow for undefined schema in object-json mode when structuredOutputs are enabled', async () => {
637+
prepareJsonResponse({ content: '{"value":"Spark"}' });
638+
639+
const model = new OpenAICompatibleChatLanguageModel(
640+
'gpt-4o-2024-08-06',
641+
{},
642+
{
643+
provider: 'test-provider',
644+
url: () => 'https://my.api.com/v1/chat/completions',
645+
headers: () => ({}),
646+
supportsStructuredOutputs: true,
647+
},
648+
);
649+
650+
await model.doGenerate({
651+
inputFormat: 'prompt',
652+
mode: {
653+
type: 'object-json',
654+
name: 'test-name',
655+
description: 'test description',
656+
},
657+
prompt: TEST_PROMPT,
658+
});
659+
660+
expect(await server.getRequestBodyJson()).toStrictEqual({
661+
model: 'gpt-4o-2024-08-06',
662+
messages: [{ role: 'user', content: 'Hello' }],
663+
response_format: {
664+
type: 'json_object',
665+
},
666+
});
667+
});
668+
669+
it('should set strict in object-tool mode when structuredOutputs are enabled', async () => {
670+
prepareJsonResponse({
671+
tool_calls: [
672+
{
673+
id: 'call_O17Uplv4lJvD6DVdIvFFeRMw',
674+
type: 'function',
675+
function: {
676+
name: 'test-tool',
677+
arguments: '{"value":"Spark"}',
678+
},
679+
},
680+
],
681+
});
682+
683+
const model = new OpenAICompatibleChatLanguageModel(
684+
'gpt-4o-2024-08-06',
685+
{},
686+
{
687+
provider: 'test-provider',
688+
url: () => 'https://my.api.com/v1/chat/completions',
689+
headers: () => ({}),
690+
supportsStructuredOutputs: true,
691+
},
692+
);
693+
694+
const result = await model.doGenerate({
695+
inputFormat: 'prompt',
696+
mode: {
697+
type: 'object-tool',
698+
tool: {
699+
type: 'function',
700+
name: 'test-tool',
701+
description: 'test description',
702+
parameters: {
703+
type: 'object',
704+
properties: { value: { type: 'string' } },
705+
required: ['value'],
706+
additionalProperties: false,
707+
$schema: 'http://json-schema.org/draft-07/schema#',
708+
},
709+
},
710+
},
711+
prompt: TEST_PROMPT,
712+
});
713+
714+
expect(await server.getRequestBodyJson()).toStrictEqual({
715+
model: 'gpt-4o-2024-08-06',
716+
messages: [{ role: 'user', content: 'Hello' }],
717+
tool_choice: { type: 'function', function: { name: 'test-tool' } },
718+
tools: [
719+
{
720+
type: 'function',
721+
function: {
722+
name: 'test-tool',
723+
description: 'test description',
724+
parameters: {
725+
type: 'object',
726+
properties: { value: { type: 'string' } },
727+
required: ['value'],
728+
additionalProperties: false,
729+
$schema: 'http://json-schema.org/draft-07/schema#',
730+
},
731+
strict: true,
732+
},
733+
},
734+
],
735+
});
736+
737+
expect(result.toolCalls).toStrictEqual([
738+
{
739+
args: '{"value":"Spark"}',
740+
toolCallId: 'call_O17Uplv4lJvD6DVdIvFFeRMw',
741+
toolCallType: 'function',
742+
toolName: 'test-tool',
743+
},
744+
]);
745+
});
746+
});
747+
389748
it('should send request body', async () => {
390749
prepareJsonResponse({ content: '' });
391750

‎packages/openai-compatible/src/openai-compatible-chat-language-model.ts

+51-22
Original file line numberDiff line numberDiff line change
@@ -43,12 +43,17 @@ no mode is specified. Should be the mode with the best results for this
4343
model. `undefined` can be specified if object generation is not supported.
4444
*/
4545
defaultObjectGenerationMode?: LanguageModelV1ObjectGenerationMode;
46+
47+
/**
48+
* Whether the model supports structured outputs.
49+
*/
50+
supportsStructuredOutputs?: boolean;
4651
};
4752

4853
export class OpenAICompatibleChatLanguageModel implements LanguageModelV1 {
4954
readonly specificationVersion = 'v1';
5055

51-
readonly supportsStructuredOutputs = false;
56+
readonly supportsStructuredOutputs: boolean;
5257

5358
readonly modelId: OpenAICompatibleChatModelId;
5459
readonly settings: OpenAICompatibleChatSettings;
@@ -63,6 +68,8 @@ export class OpenAICompatibleChatLanguageModel implements LanguageModelV1 {
6368
this.modelId = modelId;
6469
this.settings = settings;
6570
this.config = config;
71+
72+
this.supportsStructuredOutputs = config.supportsStructuredOutputs ?? false;
6673
}
6774

6875
get defaultObjectGenerationMode(): 'json' | 'tool' | undefined {
@@ -85,10 +92,7 @@ export class OpenAICompatibleChatLanguageModel implements LanguageModelV1 {
8592
stopSequences,
8693
responseFormat,
8794
seed,
88-
stream,
89-
}: Parameters<LanguageModelV1['doGenerate']>[0] & {
90-
stream: boolean;
91-
}) {
95+
}: Parameters<LanguageModelV1['doGenerate']>[0]) {
9296
const type = mode.type;
9397

9498
const warnings: LanguageModelV1CallWarning[] = [];
@@ -101,14 +105,15 @@ export class OpenAICompatibleChatLanguageModel implements LanguageModelV1 {
101105
}
102106

103107
if (
104-
responseFormat != null &&
105-
responseFormat.type === 'json' &&
106-
responseFormat.schema != null
108+
responseFormat?.type === 'json' &&
109+
responseFormat.schema != null &&
110+
!this.supportsStructuredOutputs
107111
) {
108112
warnings.push({
109113
type: 'unsupported-setting',
110114
setting: 'responseFormat',
111-
details: 'JSON response format schema is not supported',
115+
details:
116+
'JSON response format schema is only supported with structuredOutputs',
112117
});
113118
}
114119

@@ -125,26 +130,38 @@ export class OpenAICompatibleChatLanguageModel implements LanguageModelV1 {
125130
top_p: topP,
126131
frequency_penalty: frequencyPenalty,
127132
presence_penalty: presencePenalty,
133+
response_format:
134+
responseFormat?.type === 'json'
135+
? this.supportsStructuredOutputs === true &&
136+
responseFormat.schema != null
137+
? {
138+
type: 'json_schema',
139+
json_schema: {
140+
schema: responseFormat.schema,
141+
strict: true,
142+
name: responseFormat.name ?? 'response',
143+
description: responseFormat.description,
144+
},
145+
}
146+
: { type: 'json_object' }
147+
: undefined,
148+
128149
stop: stopSequences,
129150
seed,
130151

131-
// response format:
132-
response_format:
133-
responseFormat?.type === 'json' ? { type: 'json_object' } : undefined,
134-
135152
// messages:
136153
messages: convertToOpenAICompatibleChatMessages(prompt),
137154
};
138155

139156
switch (type) {
140157
case 'regular': {
141-
const { tools, tool_choice, toolWarnings } = prepareTools({ mode });
158+
const { tools, tool_choice, toolWarnings } = prepareTools({
159+
mode,
160+
structuredOutputs: this.supportsStructuredOutputs,
161+
});
162+
142163
return {
143-
args: {
144-
...baseArgs,
145-
tools,
146-
tool_choice,
147-
},
164+
args: { ...baseArgs, tools, tool_choice },
148165
warnings: [...warnings, ...toolWarnings],
149166
};
150167
}
@@ -153,7 +170,18 @@ export class OpenAICompatibleChatLanguageModel implements LanguageModelV1 {
153170
return {
154171
args: {
155172
...baseArgs,
156-
response_format: { type: 'json_object' },
173+
response_format:
174+
this.supportsStructuredOutputs === true && mode.schema != null
175+
? {
176+
type: 'json_schema',
177+
json_schema: {
178+
schema: mode.schema,
179+
strict: true,
180+
name: mode.name ?? 'response',
181+
description: mode.description,
182+
},
183+
}
184+
: { type: 'json_object' },
157185
},
158186
warnings,
159187
};
@@ -174,6 +202,7 @@ export class OpenAICompatibleChatLanguageModel implements LanguageModelV1 {
174202
name: mode.tool.name,
175203
description: mode.tool.description,
176204
parameters: mode.tool.parameters,
205+
strict: this.supportsStructuredOutputs ? true : undefined,
177206
},
178207
},
179208
],
@@ -192,7 +221,7 @@ export class OpenAICompatibleChatLanguageModel implements LanguageModelV1 {
192221
async doGenerate(
193222
options: Parameters<LanguageModelV1['doGenerate']>[0],
194223
): Promise<Awaited<ReturnType<LanguageModelV1['doGenerate']>>> {
195-
const { args, warnings } = this.getArgs({ ...options, stream: false });
224+
const { args, warnings } = this.getArgs({ ...options });
196225

197226
const body = JSON.stringify(args);
198227

@@ -238,7 +267,7 @@ export class OpenAICompatibleChatLanguageModel implements LanguageModelV1 {
238267
async doStream(
239268
options: Parameters<LanguageModelV1['doStream']>[0],
240269
): Promise<Awaited<ReturnType<LanguageModelV1['doStream']>>> {
241-
const { args, warnings } = this.getArgs({ ...options, stream: true });
270+
const { args, warnings } = this.getArgs({ ...options });
242271

243272
const body = JSON.stringify({ ...args, stream: true });
244273

‎packages/openai-compatible/src/openai-compatible-prepare-tools.ts

+5
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,12 @@ import {
66

77
export function prepareTools({
88
mode,
9+
structuredOutputs,
910
}: {
1011
mode: Parameters<LanguageModelV1['doGenerate']>[0]['mode'] & {
1112
type: 'regular';
1213
};
14+
structuredOutputs: boolean;
1315
}): {
1416
tools:
1517
| undefined
@@ -19,6 +21,7 @@ export function prepareTools({
1921
name: string;
2022
description: string | undefined;
2123
parameters: unknown;
24+
strict: boolean | undefined;
2225
};
2326
}>;
2427
tool_choice:
@@ -45,6 +48,7 @@ export function prepareTools({
4548
name: string;
4649
description: string | undefined;
4750
parameters: unknown;
51+
strict: boolean | undefined;
4852
};
4953
}> = [];
5054

@@ -58,6 +62,7 @@ export function prepareTools({
5862
name: tool.name,
5963
description: tool.description,
6064
parameters: tool.parameters,
65+
strict: structuredOutputs ? true : undefined,
6166
},
6267
});
6368
}

‎packages/openai/src/openai-chat-language-model.test.ts

+230-111
Large diffs are not rendered by default.

‎packages/openai/src/openai-chat-language-model.ts

+24-16
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ export class OpenAIChatLanguageModel implements LanguageModelV1 {
5757
}
5858

5959
get supportsStructuredOutputs(): boolean {
60-
return this.settings.structuredOutputs === true;
60+
return this.settings.structuredOutputs ?? false;
6161
}
6262

6363
get defaultObjectGenerationMode() {
@@ -104,14 +104,15 @@ export class OpenAIChatLanguageModel implements LanguageModelV1 {
104104
}
105105

106106
if (
107-
responseFormat != null &&
108-
responseFormat.type === 'json' &&
109-
responseFormat.schema != null
107+
responseFormat?.type === 'json' &&
108+
responseFormat.schema != null &&
109+
!this.supportsStructuredOutputs
110110
) {
111111
warnings.push({
112112
type: 'unsupported-setting',
113113
setting: 'responseFormat',
114-
details: 'JSON response format schema is not supported',
114+
details:
115+
'JSON response format schema is only supported with structuredOutputs',
115116
});
116117
}
117118

@@ -123,7 +124,7 @@ export class OpenAIChatLanguageModel implements LanguageModelV1 {
123124
});
124125
}
125126

126-
if (useLegacyFunctionCalling && this.settings.structuredOutputs === true) {
127+
if (useLegacyFunctionCalling && this.supportsStructuredOutputs) {
127128
throw new UnsupportedFunctionalityError({
128129
functionality: 'structuredOutputs with useLegacyFunctionCalling',
129130
});
@@ -157,6 +158,20 @@ export class OpenAIChatLanguageModel implements LanguageModelV1 {
157158
top_p: topP,
158159
frequency_penalty: frequencyPenalty,
159160
presence_penalty: presencePenalty,
161+
response_format:
162+
responseFormat?.type === 'json'
163+
? this.supportsStructuredOutputs && responseFormat.schema != null
164+
? {
165+
type: 'json_schema',
166+
json_schema: {
167+
schema: responseFormat.schema,
168+
strict: true,
169+
name: responseFormat.name ?? 'response',
170+
description: responseFormat.description,
171+
},
172+
}
173+
: { type: 'json_object' }
174+
: undefined,
160175
stop: stopSequences,
161176
seed,
162177

@@ -167,10 +182,6 @@ export class OpenAIChatLanguageModel implements LanguageModelV1 {
167182
metadata: providerMetadata?.openai?.metadata ?? undefined,
168183
prediction: providerMetadata?.openai?.prediction ?? undefined,
169184

170-
// response format:
171-
response_format:
172-
responseFormat?.type === 'json' ? { type: 'json_object' } : undefined,
173-
174185
// messages:
175186
messages: convertToOpenAIChatMessages({
176187
prompt,
@@ -192,7 +203,7 @@ export class OpenAIChatLanguageModel implements LanguageModelV1 {
192203
prepareTools({
193204
mode,
194205
useLegacyFunctionCalling,
195-
structuredOutputs: this.settings.structuredOutputs,
206+
structuredOutputs: this.supportsStructuredOutputs,
196207
});
197208

198209
return {
@@ -212,7 +223,7 @@ export class OpenAIChatLanguageModel implements LanguageModelV1 {
212223
args: {
213224
...baseArgs,
214225
response_format:
215-
this.settings.structuredOutputs === true && mode.schema != null
226+
this.supportsStructuredOutputs && mode.schema != null
216227
? {
217228
type: 'json_schema',
218229
json_schema: {
@@ -257,10 +268,7 @@ export class OpenAIChatLanguageModel implements LanguageModelV1 {
257268
name: mode.tool.name,
258269
description: mode.tool.description,
259270
parameters: mode.tool.parameters,
260-
strict:
261-
this.settings.structuredOutputs === true
262-
? true
263-
: undefined,
271+
strict: this.supportsStructuredOutputs ? true : undefined,
264272
},
265273
},
266274
],

‎packages/openai/src/openai-prepare-tools.ts

+5-5
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,13 @@ import {
88
export function prepareTools({
99
mode,
1010
useLegacyFunctionCalling = false,
11-
structuredOutputs = false,
11+
structuredOutputs,
1212
}: {
1313
mode: Parameters<LanguageModelV1['doGenerate']>[0]['mode'] & {
1414
type: 'regular';
1515
};
16-
useLegacyFunctionCalling?: boolean;
17-
structuredOutputs?: boolean;
16+
useLegacyFunctionCalling: boolean | undefined;
17+
structuredOutputs: boolean;
1818
}): {
1919
tools?: {
2020
type: 'function';
@@ -109,7 +109,7 @@ export function prepareTools({
109109
name: string;
110110
description: string | undefined;
111111
parameters: JSONSchema7;
112-
strict?: boolean;
112+
strict: boolean | undefined;
113113
};
114114
}> = [];
115115

@@ -123,7 +123,7 @@ export function prepareTools({
123123
name: tool.name,
124124
description: tool.description,
125125
parameters: tool.parameters,
126-
strict: structuredOutputs === true ? true : undefined,
126+
strict: structuredOutputs ? true : undefined,
127127
},
128128
});
129129
}

‎packages/swarm/src/run-swarm.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ export async function runSwarm<CONTEXT = any>({
4444
? [{ role: 'user' as const, content: prompt }]
4545
: prompt;
4646

47-
let lastResult: GenerateTextResult<any>;
47+
let lastResult: GenerateTextResult<any, any>;
4848
const responseMessages: Array<CoreMessage> = [];
4949

5050
do {

0 commit comments

Comments
 (0)
Please sign in to comment.