@ai-sdk/openai 4.0.64 → 4.0.66

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ai-sdk/openai",
3
- "version": "4.0.64",
3
+ "version": "4.0.66",
4
4
  "type": "module",
5
5
  "license": "Apache-2.0",
6
6
  "sideEffects": false,
@@ -35,8 +35,8 @@
35
35
  }
36
36
  },
37
37
  "dependencies": {
38
- "@ai-sdk/provider": "4.0.12",
39
- "@ai-sdk/provider-utils": "5.0.38"
38
+ "@ai-sdk/provider": "4.0.14",
39
+ "@ai-sdk/provider-utils": "5.0.40"
40
40
  },
41
41
  "devDependencies": {
42
42
  "@ai-sdk/test-server": "2.0.1",
@@ -48,6 +48,7 @@ import {
48
48
  type OpenAIChatModelId,
49
49
  } from './openai-chat-language-model-options';
50
50
  import { prepareChatTools } from './openai-chat-prepare-tools';
51
+ import { normalizeOpenAIJsonSchema } from '../normalize-openai-json-schema';
51
52
 
52
53
  type OpenAIChatConfig = {
53
54
  provider: string;
@@ -159,6 +160,14 @@ export class OpenAIChatLanguageModel implements LanguageModelV4 {
159
160
  warnings.push(...messageWarnings);
160
161
 
161
162
  const strictJsonSchema = openaiOptions.strictJsonSchema ?? true;
163
+ const normalizedResponseFormatSchema =
164
+ responseFormat?.type === 'json' && responseFormat.schema != null
165
+ ? normalizeOpenAIJsonSchema(responseFormat.schema)
166
+ : undefined;
167
+
168
+ if (normalizedResponseFormatSchema != null) {
169
+ warnings.push(...normalizedResponseFormatSchema.warnings);
170
+ }
162
171
 
163
172
  const baseArgs = {
164
173
  // model id:
@@ -190,11 +199,11 @@ export class OpenAIChatLanguageModel implements LanguageModelV4 {
190
199
  presence_penalty: presencePenalty,
191
200
  response_format:
192
201
  responseFormat?.type === 'json'
193
- ? responseFormat.schema != null
202
+ ? normalizedResponseFormatSchema != null
194
203
  ? {
195
204
  type: 'json_schema',
196
205
  json_schema: {
197
- schema: responseFormat.schema,
206
+ schema: normalizedResponseFormatSchema.schema,
198
207
  strict: strictJsonSchema,
199
208
  name: responseFormat.name ?? 'response',
200
209
  description: responseFormat.description,
@@ -7,6 +7,7 @@ import type {
7
7
  OpenAIChatToolChoice,
8
8
  OpenAIChatFunctionTool,
9
9
  } from './openai-chat-api';
10
+ import { normalizeOpenAIJsonSchema } from '../normalize-openai-json-schema';
10
11
 
11
12
  export function prepareChatTools({
12
13
  tools,
@@ -32,17 +33,23 @@ export function prepareChatTools({
32
33
 
33
34
  for (const tool of tools) {
34
35
  switch (tool.type) {
35
- case 'function':
36
+ case 'function': {
37
+ const normalizedInputSchema = normalizeOpenAIJsonSchema(
38
+ tool.inputSchema,
39
+ );
40
+ toolWarnings.push(...normalizedInputSchema.warnings);
41
+
36
42
  openaiTools.push({
37
43
  type: 'function',
38
44
  function: {
39
45
  name: tool.name,
40
46
  description: tool.description,
41
- parameters: tool.inputSchema,
47
+ parameters: normalizedInputSchema.schema,
42
48
  ...(tool.strict != null ? { strict: tool.strict } : {}),
43
49
  },
44
50
  });
45
51
  break;
52
+ }
46
53
  default:
47
54
  toolWarnings.push({
48
55
  type: 'unsupported',
@@ -0,0 +1,161 @@
1
+ import {
2
+ UnsupportedFunctionalityError,
3
+ type JSONSchema7,
4
+ type JSONSchema7Definition,
5
+ type SharedV4Warning,
6
+ } from '@ai-sdk/provider';
7
+
8
+ /**
9
+ * Normalizes JSON Schema for OpenAI structured outputs.
10
+ *
11
+ * OpenAI does not support the JSON Schema `propertyNames` keyword. Property
12
+ * names in JSON objects are always strings, so string-based constraints can be
13
+ * left to client-side validation after removing the keyword. This
14
+ * compatibility layer does not rewrite non-string property name schemas.
15
+ */
16
+ export function normalizeOpenAIJsonSchema(schema: JSONSchema7): {
17
+ schema: JSONSchema7;
18
+ warnings: SharedV4Warning[];
19
+ } {
20
+ let removedPropertyNames = false;
21
+
22
+ const normalizedSchema = normalizeSchema(schema);
23
+
24
+ return {
25
+ schema: normalizedSchema,
26
+ warnings: removedPropertyNames
27
+ ? [
28
+ {
29
+ type: 'compatibility',
30
+ feature: 'JSON Schema propertyNames',
31
+ details:
32
+ 'OpenAI does not support JSON Schema propertyNames. It was removed before sending the schema, so OpenAI will not enforce property-name constraints.',
33
+ },
34
+ ]
35
+ : [],
36
+ };
37
+
38
+ function normalizeSchema(schema: JSONSchema7): JSONSchema7 {
39
+ const propertyNames = schema.propertyNames;
40
+
41
+ if (propertyNames != null) {
42
+ if (
43
+ typeof propertyNames === 'boolean' ||
44
+ propertyNames.type !== 'string'
45
+ ) {
46
+ throw new UnsupportedFunctionalityError({
47
+ functionality:
48
+ 'JSON Schema propertyNames that does not use a string schema',
49
+ });
50
+ }
51
+
52
+ removedPropertyNames = true;
53
+ }
54
+
55
+ const normalizedSchema = { ...schema };
56
+ delete normalizedSchema.propertyNames;
57
+
58
+ if (normalizedSchema.properties != null) {
59
+ normalizedSchema.properties = normalizeSchemaRecord(
60
+ normalizedSchema.properties,
61
+ );
62
+ }
63
+
64
+ if (normalizedSchema.patternProperties != null) {
65
+ normalizedSchema.patternProperties = normalizeSchemaRecord(
66
+ normalizedSchema.patternProperties,
67
+ );
68
+ }
69
+
70
+ if (normalizedSchema.additionalProperties != null) {
71
+ normalizedSchema.additionalProperties = normalizeDefinition(
72
+ normalizedSchema.additionalProperties,
73
+ );
74
+ }
75
+
76
+ if (normalizedSchema.additionalItems != null) {
77
+ normalizedSchema.additionalItems = normalizeDefinition(
78
+ normalizedSchema.additionalItems,
79
+ );
80
+ }
81
+
82
+ if (normalizedSchema.items != null) {
83
+ normalizedSchema.items = Array.isArray(normalizedSchema.items)
84
+ ? normalizedSchema.items.map(normalizeDefinition)
85
+ : normalizeDefinition(normalizedSchema.items);
86
+ }
87
+
88
+ if (normalizedSchema.contains != null) {
89
+ normalizedSchema.contains = normalizeDefinition(
90
+ normalizedSchema.contains,
91
+ );
92
+ }
93
+
94
+ if (normalizedSchema.not != null) {
95
+ normalizedSchema.not = normalizeDefinition(normalizedSchema.not);
96
+ }
97
+
98
+ if (normalizedSchema.allOf != null) {
99
+ normalizedSchema.allOf = normalizedSchema.allOf.map(normalizeDefinition);
100
+ }
101
+
102
+ if (normalizedSchema.anyOf != null) {
103
+ normalizedSchema.anyOf = normalizedSchema.anyOf.map(normalizeDefinition);
104
+ }
105
+
106
+ if (normalizedSchema.oneOf != null) {
107
+ normalizedSchema.oneOf = normalizedSchema.oneOf.map(normalizeDefinition);
108
+ }
109
+
110
+ if (normalizedSchema.definitions != null) {
111
+ normalizedSchema.definitions = normalizeSchemaRecord(
112
+ normalizedSchema.definitions,
113
+ );
114
+ }
115
+
116
+ if (normalizedSchema.$defs != null) {
117
+ normalizedSchema.$defs = normalizeSchemaRecord(normalizedSchema.$defs);
118
+ }
119
+
120
+ if (normalizedSchema.dependencies != null) {
121
+ normalizedSchema.dependencies = Object.fromEntries(
122
+ Object.entries(normalizedSchema.dependencies).map(
123
+ ([key, dependency]) => [
124
+ key,
125
+ Array.isArray(dependency)
126
+ ? dependency
127
+ : normalizeDefinition(dependency),
128
+ ],
129
+ ),
130
+ );
131
+ }
132
+
133
+ for (const keyword of ['if', 'then', 'else'] as const) {
134
+ const conditionalSchema = normalizedSchema[keyword];
135
+ if (conditionalSchema != null) {
136
+ normalizedSchema[keyword] = normalizeDefinition(conditionalSchema);
137
+ }
138
+ }
139
+
140
+ return normalizedSchema;
141
+ }
142
+
143
+ function normalizeSchemaRecord(
144
+ schemas: Record<string, JSONSchema7Definition>,
145
+ ): Record<string, JSONSchema7Definition> {
146
+ return Object.fromEntries(
147
+ Object.entries(schemas).map(([key, schema]) => [
148
+ key,
149
+ normalizeDefinition(schema),
150
+ ]),
151
+ );
152
+ }
153
+
154
+ function normalizeDefinition(
155
+ definition: JSONSchema7Definition,
156
+ ): JSONSchema7Definition {
157
+ return typeof definition === 'boolean'
158
+ ? definition
159
+ : normalizeSchema(definition);
160
+ }
161
+ }
@@ -1,11 +1,15 @@
1
1
  import {
2
2
  InvalidArgumentError,
3
3
  InvalidResponseDataError,
4
+ UnsupportedFunctionalityError,
4
5
  type Experimental_BatchV4 as BatchV4,
6
+ type Experimental_BatchV4CancelResult as BatchV4CancelResult,
5
7
  type Experimental_BatchV4StartResult as BatchV4StartResult,
6
8
  type Experimental_BatchV4Error as BatchV4Error,
7
9
  type Experimental_BatchV4ItemResult as BatchV4ItemResult,
8
10
  type Experimental_BatchV4OperationOptions as BatchV4OperationOptions,
11
+ type Experimental_BatchV4ListOptions as BatchV4ListOptions,
12
+ type Experimental_BatchV4ListResult as BatchV4ListResult,
9
13
  type Experimental_BatchV4Status as BatchV4Status,
10
14
  type Experimental_TextBatchV4ItemResult as TextBatchV4ItemResult,
11
15
  type Experimental_BatchV4StartOptions as BatchV4StartOptions,
@@ -86,36 +90,51 @@ type OpenAIBatchResultConversion =
86
90
  | { success: true; result: LanguageModelV4GenerateResult }
87
91
  | { success: false; error: BatchV4Error };
88
92
 
93
+ function assertTextBatchRequests(
94
+ requests: BatchV4StartOptions['requests'],
95
+ ): asserts requests is ReadonlyArray<OpenAIBatchRequest> {
96
+ for (const request of requests) {
97
+ const requestType = request.type;
98
+ if (requestType !== 'text') {
99
+ throw new UnsupportedFunctionalityError({
100
+ functionality: `batch request type: ${requestType}`,
101
+ message: `The OpenAI Batch API does not support batch requests with type "${requestType}".`,
102
+ });
103
+ }
104
+ }
105
+ }
106
+
107
+ const openaiBatchResponseZodSchema = () =>
108
+ z.object({
109
+ id: z.string(),
110
+ status: z.string(),
111
+ output_file_id: z.string().nullish(),
112
+ error_file_id: z.string().nullish(),
113
+ created_at: z.number().nullish(),
114
+ expires_at: z.number().nullish(),
115
+ request_counts: z
116
+ .object({
117
+ total: z.number().nullish(),
118
+ completed: z.number().nullish(),
119
+ failed: z.number().nullish(),
120
+ })
121
+ .nullish(),
122
+ errors: z
123
+ .object({
124
+ data: z
125
+ .array(
126
+ z.object({
127
+ code: z.string().nullish(),
128
+ message: z.string().nullish(),
129
+ }),
130
+ )
131
+ .nullish(),
132
+ })
133
+ .nullish(),
134
+ });
135
+
89
136
  const openaiBatchResponseSchema = lazySchema(() =>
90
- zodSchema(
91
- z.object({
92
- id: z.string(),
93
- status: z.string(),
94
- output_file_id: z.string().nullish(),
95
- error_file_id: z.string().nullish(),
96
- created_at: z.number().nullish(),
97
- expires_at: z.number().nullish(),
98
- request_counts: z
99
- .object({
100
- total: z.number().nullish(),
101
- completed: z.number().nullish(),
102
- failed: z.number().nullish(),
103
- })
104
- .nullish(),
105
- errors: z
106
- .object({
107
- data: z
108
- .array(
109
- z.object({
110
- code: z.string().nullish(),
111
- message: z.string().nullish(),
112
- }),
113
- )
114
- .nullish(),
115
- })
116
- .nullish(),
117
- }),
118
- ),
137
+ zodSchema(openaiBatchResponseZodSchema()),
119
138
  );
120
139
 
121
140
  type OpenAIBatchResponse = InferSchema<typeof openaiBatchResponseSchema>;
@@ -141,6 +160,16 @@ const openaiBatchResultLineSchema = lazySchema(() =>
141
160
  ),
142
161
  );
143
162
 
163
+ const openaiBatchListResponseSchema = lazySchema(() =>
164
+ zodSchema(
165
+ z.object({
166
+ data: z.array(openaiBatchResponseZodSchema()),
167
+ has_more: z.boolean(),
168
+ last_id: z.string().nullish(),
169
+ }),
170
+ ),
171
+ );
172
+
144
173
  type OpenAIBatchResultLine = InferSchema<typeof openaiBatchResultLineSchema>;
145
174
 
146
175
  export class OpenAIBatch implements BatchV4<OpenAIBatchModelIds> {
@@ -160,6 +189,7 @@ export class OpenAIBatch implements BatchV4<OpenAIBatchModelIds> {
160
189
  async doStartBatch(
161
190
  options: BatchV4StartOptions<OpenAIBatchModelIds>,
162
191
  ): Promise<BatchV4StartResult> {
192
+ assertTextBatchRequests(options.requests);
163
193
  validateSingleModel(options.requests);
164
194
 
165
195
  const fileParts: string[] = [];
@@ -319,6 +349,58 @@ export class OpenAIBatch implements BatchV4<OpenAIBatchModelIds> {
319
349
  return convertOpenAIBatchStatus(batch);
320
350
  }
321
351
 
352
+ async doCancelBatch(
353
+ options: BatchV4OperationOptions,
354
+ ): Promise<BatchV4CancelResult> {
355
+ await postJsonToApi({
356
+ url: this.getUrl(
357
+ `/batches/${encodeURIComponent(options.batchId)}/cancel`,
358
+ ),
359
+ headers: combineHeaders(this.options.config.headers?.(), options.headers),
360
+ body: {},
361
+ failedResponseHandler: openaiFailedResponseHandler,
362
+ successfulResponseHandler: createJsonResponseHandler(
363
+ openaiBatchResponseSchema,
364
+ ),
365
+ abortSignal: options.abortSignal,
366
+ fetch: this.options.config.fetch,
367
+ });
368
+
369
+ return {};
370
+ }
371
+
372
+ async doListBatches(options: BatchV4ListOptions): Promise<BatchV4ListResult> {
373
+ const url = new URL(this.getUrl('/batches'));
374
+ if (options.limit != null) {
375
+ url.searchParams.set('limit', String(options.limit));
376
+ }
377
+ if (options.cursor != null) {
378
+ url.searchParams.set('after', options.cursor);
379
+ }
380
+
381
+ const { value: page } = await getFromApi({
382
+ url: url.toString(),
383
+ headers: combineHeaders(this.options.config.headers?.(), options.headers),
384
+ failedResponseHandler: openaiFailedResponseHandler,
385
+ successfulResponseHandler: createJsonResponseHandler(
386
+ openaiBatchListResponseSchema,
387
+ ),
388
+ abortSignal: options.abortSignal,
389
+ fetch: this.options.config.fetch,
390
+ validateUrl: false,
391
+ });
392
+
393
+ return {
394
+ batches: page.data.map(batch => ({
395
+ batchId: batch.id,
396
+ ...convertOpenAIBatchStatus(batch),
397
+ })),
398
+ ...(page.has_more && page.last_id != null
399
+ ? { nextCursor: page.last_id }
400
+ : {}),
401
+ };
402
+ }
403
+
322
404
  async doGetBatchResults(
323
405
  options: BatchV4OperationOptions,
324
406
  ): Promise<ReadableStream<BatchV4ItemResult>> {
@@ -24,6 +24,13 @@ export type OpenAIConfig = {
24
24
  * @see https://github.com/vercel/ai/issues/20180
25
25
  */
26
26
  explicitMessageItemType?: boolean;
27
+ /**
28
+ * Whether the provider supports the
29
+ * `web_search_call.action.sources` Responses API include value.
30
+ *
31
+ * Defaults to `true`.
32
+ */
33
+ supportsWebSearchSourcesInclude?: boolean;
27
34
  /**
28
35
  * This is soft-deprecated. Use provider references (e.g. `{ openai: 'file-abc123' }`)
29
36
  * in file part data instead. File ID prefixes used to identify file IDs
@@ -179,6 +179,15 @@ export const openaiLanguageModelResponsesOptionsSchema = lazySchema(() =>
179
179
  )
180
180
  .nullish(),
181
181
 
182
+ /**
183
+ * Whether to automatically include web search action sources in the
184
+ * response. Disable this for OpenAI-compatible providers that do not
185
+ * support the `web_search_call.action.sources` include value.
186
+ *
187
+ * Defaults to `true`.
188
+ */
189
+ includeWebSearchSources: z.boolean().optional(),
190
+
182
191
  /**
183
192
  * Instructions for the model.
184
193
  * They can be used to change the system or developer message when continuing a conversation using the `previousResponseId` option.
@@ -70,6 +70,7 @@ import {
70
70
  isUndeclaredParallelToolCall,
71
71
  } from './expand-parallel-tool-call';
72
72
  import { mapOpenAIResponseFinishReason } from './map-openai-responses-finish-reason';
73
+ import { normalizeOpenAIJsonSchema } from '../normalize-openai-json-schema';
73
74
  import {
74
75
  openaiResponsesChunkSchema,
75
76
  openaiResponsesResponseSchema,
@@ -444,6 +445,14 @@ export class OpenAIResponsesLanguageModel implements LanguageModelV4 {
444
445
  }
445
446
 
446
447
  const strictJsonSchema = openaiOptions?.strictJsonSchema ?? true;
448
+ const normalizedResponseFormatSchema =
449
+ responseFormat?.type === 'json' && responseFormat.schema != null
450
+ ? normalizeOpenAIJsonSchema(responseFormat.schema)
451
+ : undefined;
452
+
453
+ if (normalizedResponseFormatSchema != null) {
454
+ warnings.push(...normalizedResponseFormatSchema.warnings);
455
+ }
447
456
 
448
457
  let include: OpenAIResponsesIncludeOptions = openaiOptions?.include;
449
458
 
@@ -486,7 +495,11 @@ export class OpenAIResponsesLanguageModel implements LanguageModelV4 {
486
495
  ) as LanguageModelV4ProviderTool | undefined
487
496
  )?.name;
488
497
 
489
- if (webSearchToolName) {
498
+ if (
499
+ webSearchToolName &&
500
+ config.supportsWebSearchSourcesInclude !== false &&
501
+ openaiOptions?.includeWebSearchSources !== false
502
+ ) {
490
503
  addInclude('web_search_call.action.sources');
491
504
  }
492
505
 
@@ -513,13 +526,13 @@ export class OpenAIResponsesLanguageModel implements LanguageModelV4 {
513
526
  text: {
514
527
  ...(responseFormat?.type === 'json' && {
515
528
  format:
516
- responseFormat.schema != null
529
+ normalizedResponseFormatSchema != null
517
530
  ? {
518
531
  type: 'json_schema',
519
532
  strict: strictJsonSchema,
520
533
  name: responseFormat.name ?? 'response',
521
534
  description: responseFormat.description,
522
- schema: responseFormat.schema,
535
+ schema: normalizedResponseFormatSchema.schema,
523
536
  }
524
537
  : { type: 'json_object' },
525
538
  }),
@@ -21,6 +21,7 @@ import { shellArgsSchema } from '../tool/shell';
21
21
  import { toolSearchArgsSchema } from '../tool/tool-search';
22
22
  import { webSearchArgsSchema } from '../tool/web-search';
23
23
  import { webSearchPreviewArgsSchema } from '../tool/web-search-preview';
24
+ import { normalizeOpenAIJsonSchema } from '../normalize-openai-json-schema';
24
25
  import type {
25
26
  OpenAIResponsesAllowedTool,
26
27
  OpenAIResponsesFunctionTool,
@@ -147,6 +148,7 @@ export async function prepareResponsesTools({
147
148
  const openaiFunctionTool = prepareFunctionTool({
148
149
  tool,
149
150
  options: openaiOptions,
151
+ toolWarnings,
150
152
  async: resolveAsyncToolOption({
151
153
  value: openaiOptions?.async,
152
154
  supportsAsyncToolCalling,
@@ -627,19 +629,31 @@ function toAllowedToolResolution(
627
629
  function prepareFunctionTool({
628
630
  tool,
629
631
  options,
632
+ toolWarnings,
630
633
  async,
631
634
  }: {
632
635
  tool: LanguageModelV4FunctionTool;
633
636
  options: OpenAIToolOptions | undefined;
637
+ toolWarnings: SharedV4Warning[];
634
638
  async: boolean | undefined;
635
639
  }): OpenAIResponsesFunctionTool {
636
640
  const deferLoading = options?.deferLoading;
641
+ const normalizedInputSchema = normalizeOpenAIJsonSchema(tool.inputSchema);
642
+ const normalizedOutputSchema =
643
+ options?.outputSchema != null
644
+ ? normalizeOpenAIJsonSchema(options.outputSchema as JSONSchema7)
645
+ : undefined;
646
+
647
+ toolWarnings.push(
648
+ ...normalizedInputSchema.warnings,
649
+ ...(normalizedOutputSchema?.warnings ?? []),
650
+ );
637
651
 
638
652
  return {
639
653
  type: 'function',
640
654
  name: tool.name,
641
655
  description: tool.description,
642
- parameters: tool.inputSchema,
656
+ parameters: normalizedInputSchema.schema,
643
657
  ...(async != null ? { async } : {}),
644
658
  ...(tool.strict != null ? { strict: tool.strict } : {}),
645
659
  ...(deferLoading != null ? { defer_loading: deferLoading } : {}),
@@ -647,7 +661,7 @@ function prepareFunctionTool({
647
661
  ? { allowed_callers: options.allowedCallers }
648
662
  : {}),
649
663
  ...(options?.outputSchema != null
650
- ? { output_schema: options.outputSchema as JSONSchema7 }
664
+ ? { output_schema: normalizedOutputSchema?.schema }
651
665
  : {}),
652
666
  };
653
667
  }