@ai-sdk/openai 4.0.84 → 4.0.86

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.
@@ -36,6 +36,15 @@ const openai = createOpenAI({
36
36
 
37
37
  You can use the following optional settings to customize the OpenAI provider instance:
38
38
 
39
+ - **batchResultDownloads** _object_
40
+
41
+ Settings for downloading JSON Lines batch results.
42
+ - **maxLineBytes** _number_
43
+
44
+ Maximum UTF-8 bytes per row, excluding the LF delimiter. Defaults to 64 MiB
45
+ (67,108,864 bytes). Must be a positive safe integer. Oversized rows throw a
46
+ `DownloadError` and cancel the download.
47
+
39
48
  - **baseURL** _string_
40
49
 
41
50
  Use a different URL prefix for API calls, e.g. to use proxy servers.
@@ -3472,8 +3481,8 @@ const result = await generateSpeech({
3472
3481
  ## Decision Models
3473
3482
 
3474
3483
  Create an experimental decision model with `openai.decisionModel(modelId)`.
3475
- It uses the Responses API with structured output to answer Choice, Score, and
3476
- Boolean questions. Choose a model that supports structured output, such as `gpt-6-luna`.
3484
+ It uses the native [Decisions API](https://developers.openai.com/api/docs/guides/decisions)
3485
+ to answer Choice, Score, and Boolean questions with `gpt-6-luna`.
3477
3486
 
3478
3487
  ```ts
3479
3488
  import { openai } from '@ai-sdk/openai';
@@ -3496,17 +3505,37 @@ const { answers } = await experimental_decide({
3496
3505
  });
3497
3506
  ```
3498
3507
 
3499
- Scores are finite fractional positions on the ordered rubric. Choice labels are
3500
- returned exactly as supplied. These are model judgments: no probability
3501
- distributions are returned for Choice or Score. Boolean answers contain a prompted
3502
- estimate of P(true), validated to be finite and in `[0, 1]`. These estimates are
3503
- not guaranteed to be calibrated. Apply thresholds in application code, for example
3508
+ Scores are probability-weighted fractional positions on the ordered rubric, starting
3509
+ at zero. Choice labels are returned exactly as supplied. Choice and Score answers
3510
+ include probability distributions. Boolean questions use the API's predicate
3511
+ primitive and return P(true). Apply thresholds in application code, for example
3504
3512
  `answers.requestsRefund.probability >= 0.5`.
3505
3513
 
3514
+ Structured state, instructions, and criterion descriptions are serialized as JSON
3515
+ text. Score levels receive zero-based index labels. Non-null Boolean criteria are
3516
+ appended to predicate instructions under `Criteria for true:` and `Criteria for false:`.
3517
+ String criteria are passed through; structured criteria are serialized as JSON text.
3518
+
3506
3519
  The factory respects `createOpenAI` settings, including custom fetch and base URL.
3507
- Pass Responses options through `providerOptions.openai`. Usage, warnings,
3508
- response information, and provider metadata are preserved. Refusals, truncated
3509
- output, or invalid answers fail the entire call. See [Decisions](/docs/ai-sdk-core/decisions).
3520
+ Set `providerOptions.openai.safetyIdentifier` to an opaque end-user identifier for
3521
+ safety monitoring (up to 128 characters). For example:
3522
+
3523
+ ```ts
3524
+ providerOptions: {
3525
+ openai: { safetyIdentifier: 'user-123' } satisfies OpenAIDecisionModelOptions,
3526
+ }
3527
+ ```
3528
+
3529
+ Import `OpenAIDecisionModelOptions` from `@ai-sdk/openai` when using the type above.
3530
+ Other OpenAI options are unsupported and produce warnings. Native confidence
3531
+ values are available in `providerMetadata.openai.confidence`, keyed by question ID.
3532
+ Input and output token counts are available in `usage`. The full native token usage
3533
+ (including cache, reasoning, and total counts) is preserved in
3534
+ `providerMetadata.openai.usage`. Response headers, the returned model ID, and the
3535
+ raw response body are preserved. If OpenAI refuses a question, the entire call
3536
+ throws `InvalidResponseDataError` with the question name and the raw response in
3537
+ `error.data`. Invalid answers also fail the entire decision.
3538
+ See [Decisions](/docs/ai-sdk-core/decisions).
3510
3539
 
3511
3540
  Decision models can also be accessed through `customProvider` aliases or
3512
3541
  `createProviderRegistry().decisionModel('provider:model')`. Direct string IDs
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ai-sdk/openai",
3
- "version": "4.0.84",
3
+ "version": "4.0.86",
4
4
  "type": "module",
5
5
  "license": "Apache-2.0",
6
6
  "sideEffects": false,
@@ -35,15 +35,15 @@
35
35
  }
36
36
  },
37
37
  "dependencies": {
38
- "@ai-sdk/provider": "4.0.22",
39
- "@ai-sdk/provider-utils": "5.0.54"
38
+ "@ai-sdk/provider": "4.0.24",
39
+ "@ai-sdk/provider-utils": "5.0.56"
40
40
  },
41
41
  "devDependencies": {
42
- "@ai-sdk/test-server": "2.0.3",
42
+ "@ai-sdk/test-server": "2.0.4",
43
43
  "@types/node": "22.19.19",
44
44
  "@vercel/ai-tsconfig": "0.0.0",
45
45
  "tsdown": "^0.23.0",
46
- "typescript": "5.8.3",
46
+ "typescript": "6.0.3",
47
47
  "zod": "3.25.76"
48
48
  },
49
49
  "peerDependencies": {
@@ -148,7 +148,7 @@ export class OpenAIFiles implements FilesV4 {
148
148
  } else {
149
149
  const fileBytes = convertInlineFileDataToUint8Array(data);
150
150
 
151
- const blob = new Blob([fileBytes], {
151
+ const blob = new Blob([fileBytes as Uint8Array<ArrayBuffer>], {
152
152
  type: mediaType,
153
153
  });
154
154
 
package/src/index.ts CHANGED
@@ -60,3 +60,5 @@ export type {
60
60
  OpenaiResponsesSourceDocumentProviderMetadata,
61
61
  } from './responses/openai-responses-provider-metadata';
62
62
  export { VERSION } from './version';
63
+ export type { OpenAIDecisionModelId } from './openai-decision-model';
64
+ export type { OpenAIDecisionModelOptions } from './openai-decision-model-options';
@@ -181,6 +181,7 @@ export class OpenAIBatch implements BatchV4<OpenAIBatchModelIds> {
181
181
  private readonly options: {
182
182
  provider: string;
183
183
  config: OpenAIConfig;
184
+ maxLineBytes?: number;
184
185
  },
185
186
  ) {
186
187
  this.provider = options.provider;
@@ -466,6 +467,7 @@ export class OpenAIBatch implements BatchV4<OpenAIBatchModelIds> {
466
467
  failedResponseHandler: openaiFailedResponseHandler,
467
468
  successfulResponseHandler: createJsonLinesResponseHandler(
468
469
  openaiBatchResultLineSchema,
470
+ { maxLineBytes: this.options.maxLineBytes },
469
471
  ),
470
472
  abortSignal: options.abortSignal,
471
473
  fetch: this.options.config.fetch,
@@ -0,0 +1,19 @@
1
+ import {
2
+ lazySchema,
3
+ zodSchema,
4
+ type InferSchema,
5
+ } from '@ai-sdk/provider-utils';
6
+ import { z } from 'zod/v4';
7
+
8
+ export const openaiDecisionModelOptions = lazySchema(() =>
9
+ zodSchema(
10
+ z.object({
11
+ /** An opaque end-user identifier for safety monitoring, up to 128 characters. */
12
+ safetyIdentifier: z.string().max(128).optional(),
13
+ }),
14
+ ),
15
+ );
16
+
17
+ export type OpenAIDecisionModelOptions = InferSchema<
18
+ typeof openaiDecisionModelOptions
19
+ >;
@@ -0,0 +1,285 @@
1
+ import {
2
+ InvalidResponseDataError,
3
+ type Experimental_DecisionModelV4 as DecisionModelV4,
4
+ type Experimental_DecisionModelV4Answer as DecisionModelV4Answer,
5
+ type Experimental_DecisionModelV4Input as DecisionModelV4Input,
6
+ } from '@ai-sdk/provider';
7
+ import {
8
+ combineHeaders,
9
+ createJsonResponseHandler,
10
+ postJsonToApi,
11
+ parseProviderOptions,
12
+ serializeModelOptions,
13
+ WORKFLOW_DESERIALIZE,
14
+ WORKFLOW_SERIALIZE,
15
+ } from '@ai-sdk/provider-utils';
16
+ import { z } from 'zod/v4';
17
+ import {
18
+ prepareOpenAIConfigForWorkflowDeserialize,
19
+ type OpenAIConfig,
20
+ } from './openai-config';
21
+ import { openaiFailedResponseHandler } from './openai-error';
22
+ import { openaiDecisionModelOptions } from './openai-decision-model-options';
23
+
24
+ export type OpenAIDecisionModelId = 'gpt-6-luna' | (string & {});
25
+
26
+ const probability = z.number().min(0).max(1);
27
+ const responseSchema = z.object({
28
+ model: z.string().nullish(),
29
+ usage: z
30
+ .object({
31
+ input_tokens: z.number().nullish(),
32
+ input_tokens_details: z
33
+ .object({
34
+ cached_tokens: z.number().nullish(),
35
+ cache_write_tokens: z.number().nullish(),
36
+ })
37
+ .nullish(),
38
+ output_tokens: z.number().nullish(),
39
+ output_tokens_details: z
40
+ .object({
41
+ reasoning_tokens: z.number().nullish(),
42
+ })
43
+ .nullish(),
44
+ total_tokens: z.number().nullish(),
45
+ })
46
+ .nullish(),
47
+ answers: z.array(
48
+ z.discriminatedUnion('type', [
49
+ z.object({ type: z.literal('refusal'), name: z.string().nullable() }),
50
+ z.object({
51
+ type: z.literal('predicate'),
52
+ name: z.string(),
53
+ probability,
54
+ }),
55
+ z.object({
56
+ type: z.literal('choice'),
57
+ name: z.string(),
58
+ choice: z.string(),
59
+ confidence: probability.nullish(),
60
+ probabilities: z.array(z.object({ value: z.string(), probability })),
61
+ }),
62
+ z.object({
63
+ type: z.literal('score'),
64
+ name: z.string(),
65
+ score: z.number(),
66
+ confidence: probability.nullish(),
67
+ probabilities: z.array(
68
+ z.object({ value: z.number().int().nonnegative(), probability }),
69
+ ),
70
+ }),
71
+ ]),
72
+ ),
73
+ });
74
+
75
+ function toText(input: DecisionModelV4Input): string {
76
+ return typeof input === 'string' ? input : JSON.stringify(input);
77
+ }
78
+
79
+ export class DecisionOpenAIModel implements DecisionModelV4 {
80
+ readonly specificationVersion = 'v4';
81
+ readonly supportedQuestionTypes = ['choice', 'score', 'boolean'] as const;
82
+
83
+ constructor(
84
+ readonly modelId: OpenAIDecisionModelId,
85
+ private readonly config: OpenAIConfig,
86
+ ) {}
87
+
88
+ get provider() {
89
+ return this.config.provider;
90
+ }
91
+
92
+ static [WORKFLOW_SERIALIZE](model: DecisionOpenAIModel) {
93
+ return serializeModelOptions({
94
+ modelId: model.modelId,
95
+ config: model.config,
96
+ });
97
+ }
98
+
99
+ static [WORKFLOW_DESERIALIZE](options: {
100
+ modelId: OpenAIDecisionModelId;
101
+ config: Parameters<typeof prepareOpenAIConfigForWorkflowDeserialize>[0];
102
+ }) {
103
+ return new DecisionOpenAIModel(
104
+ options.modelId,
105
+ prepareOpenAIConfigForWorkflowDeserialize(options.config),
106
+ );
107
+ }
108
+
109
+ /** @deprecated Use `doDecide` instead. */
110
+ doEvaluate(options: Parameters<DecisionModelV4['doDecide']>[0]) {
111
+ return this.doDecide(options);
112
+ }
113
+
114
+ async doDecide({
115
+ state,
116
+ questions,
117
+ headers,
118
+ abortSignal,
119
+ providerOptions,
120
+ }: Parameters<DecisionModelV4['doDecide']>[0]): Promise<
121
+ Awaited<ReturnType<DecisionModelV4['doDecide']>>
122
+ > {
123
+ const openaiOptions = await parseProviderOptions({
124
+ provider: 'openai',
125
+ providerOptions,
126
+ schema: openaiDecisionModelOptions,
127
+ });
128
+ const {
129
+ value: response,
130
+ rawValue,
131
+ responseHeaders,
132
+ } = await postJsonToApi({
133
+ url: this.config.url({ path: '/decisions', modelId: this.modelId }),
134
+ headers: combineHeaders(this.config.headers?.(), headers),
135
+ body: {
136
+ model: this.modelId,
137
+ safety_identifier: openaiOptions?.safetyIdentifier,
138
+ input: toText(state),
139
+ questions: Object.entries(questions).map(([name, question]) => {
140
+ const instructions = toText(question.instructions);
141
+ switch (question.type) {
142
+ case 'boolean':
143
+ return {
144
+ type: 'predicate',
145
+ name,
146
+ instructions: [
147
+ instructions,
148
+ question.criteria?.true == null
149
+ ? undefined
150
+ : `Criteria for true:\n${toText(question.criteria.true)}`,
151
+ question.criteria?.false == null
152
+ ? undefined
153
+ : `Criteria for false:\n${toText(question.criteria.false)}`,
154
+ ]
155
+ .filter(part => part !== undefined)
156
+ .join('\n\n'),
157
+ };
158
+ case 'choice':
159
+ return {
160
+ type: 'choice',
161
+ name,
162
+ instructions,
163
+ choices: Object.entries(question.criteria).map(
164
+ ([value, description]) => ({
165
+ value,
166
+ ...(description == null
167
+ ? {}
168
+ : { description: toText(description) }),
169
+ }),
170
+ ),
171
+ };
172
+ case 'score':
173
+ return {
174
+ type: 'score',
175
+ name,
176
+ instructions,
177
+ levels: question.criteria.map((description, index) => ({
178
+ // Score criteria have no separate labels; indices identify each level.
179
+ label: String(index),
180
+ ...(description == null
181
+ ? {}
182
+ : { description: toText(description) }),
183
+ })),
184
+ };
185
+ }
186
+ }),
187
+ },
188
+ abortSignal,
189
+ fetch: this.config.fetch,
190
+ failedResponseHandler: openaiFailedResponseHandler,
191
+ successfulResponseHandler: createJsonResponseHandler(responseSchema),
192
+ });
193
+
194
+ const answers = Object.fromEntries(
195
+ response.answers.map((answer): [string, DecisionModelV4Answer] => {
196
+ if (answer.type === 'refusal') {
197
+ if (answer.name === null) {
198
+ throw new InvalidResponseDataError({
199
+ data: rawValue,
200
+ message: 'OpenAI Decisions refused an unnamed question.',
201
+ });
202
+ }
203
+ return [answer.name, { type: 'refusal' }];
204
+ }
205
+ if (answer.type === 'predicate') {
206
+ return [
207
+ answer.name,
208
+ { type: 'boolean', probability: answer.probability },
209
+ ];
210
+ }
211
+ const probabilities = Object.fromEntries(
212
+ answer.probabilities.map(({ value, probability }) => [
213
+ value,
214
+ probability,
215
+ ]),
216
+ );
217
+ if (Object.keys(probabilities).length !== answer.probabilities.length) {
218
+ throw new InvalidResponseDataError({
219
+ data: rawValue,
220
+ message: 'Decisions returned duplicate probability values.',
221
+ });
222
+ }
223
+ return [
224
+ answer.name,
225
+ answer.type === 'choice'
226
+ ? { type: 'choice', choice: answer.choice, probabilities }
227
+ : { type: 'score', score: answer.score, probabilities },
228
+ ];
229
+ }),
230
+ );
231
+
232
+ const names = response.answers.map(answer => answer.name);
233
+ if (
234
+ names.length !== Object.keys(questions).length ||
235
+ new Set(names).size !== names.length ||
236
+ names.some(
237
+ name =>
238
+ typeof name !== 'string' ||
239
+ !Object.prototype.hasOwnProperty.call(questions, name),
240
+ )
241
+ ) {
242
+ throw new InvalidResponseDataError({
243
+ data: rawValue,
244
+ message: 'Decisions must return exactly one answer for every question.',
245
+ });
246
+ }
247
+
248
+ return {
249
+ answers,
250
+ usage:
251
+ response.usage == null
252
+ ? undefined
253
+ : {
254
+ inputTokens: response.usage.input_tokens ?? undefined,
255
+ outputTokens: response.usage.output_tokens ?? undefined,
256
+ },
257
+ // The Decisions API reports probabilities and scores to two decimal places.
258
+ rounding: { probabilityDecimals: 2, scoreDecimals: 2 },
259
+ warnings: Object.keys(providerOptions?.openai ?? {})
260
+ .filter(option => option !== 'safetyIdentifier')
261
+ .map(option => ({
262
+ type: 'unsupported',
263
+ feature: `providerOptions.openai.${option}`,
264
+ })),
265
+ providerMetadata: {
266
+ openai: {
267
+ ...(response.usage == null ? {} : { usage: response.usage }),
268
+ confidence: Object.fromEntries(
269
+ response.answers.flatMap(answer =>
270
+ (answer.type === 'choice' || answer.type === 'score') &&
271
+ answer.confidence != null
272
+ ? [[answer.name, answer.confidence]]
273
+ : [],
274
+ ),
275
+ ),
276
+ },
277
+ },
278
+ response: {
279
+ modelId: response.model ?? this.modelId,
280
+ headers: responseHeaders,
281
+ body: rawValue,
282
+ },
283
+ };
284
+ }
285
+ }
@@ -20,7 +20,10 @@ import {
20
20
  type FetchFunction,
21
21
  type WebSocketConstructor,
22
22
  } from '@ai-sdk/provider-utils';
23
- import { Experimental_DecisionLanguageModel as DecisionLanguageModel } from '@ai-sdk/provider-utils/experimental-decision';
23
+ import {
24
+ DecisionOpenAIModel,
25
+ type OpenAIDecisionModelId,
26
+ } from './openai-decision-model';
24
27
  import { OpenAIChatLanguageModel } from './chat/openai-chat-language-model';
25
28
  import type { OpenAIChatModelId } from './chat/openai-chat-language-model-options';
26
29
  import { OpenAICompletionLanguageModel } from './completion/openai-completion-language-model';
@@ -50,10 +53,10 @@ import { VERSION } from './version';
50
53
  export interface OpenAIProvider extends ProviderV4 {
51
54
  (modelId: OpenAIResponsesModelId): LanguageModelV4;
52
55
 
53
- /** Creates an experimental Choice/Score/Boolean decision model using the Responses API. */
54
- decisionModel(modelId: OpenAIResponsesModelId): DecisionModelV4;
56
+ /** Creates an experimental Choice/Score/Boolean decision model using the Decisions API. */
57
+ decisionModel(modelId: OpenAIDecisionModelId): DecisionModelV4;
55
58
  /** @deprecated Use `decisionModel` instead. */
56
- evaluationModel(modelId: OpenAIResponsesModelId): DecisionModelV4 & {
59
+ evaluationModel(modelId: OpenAIDecisionModelId): DecisionModelV4 & {
57
60
  doEvaluate: DecisionModelV4['doDecide'];
58
61
  };
59
62
 
@@ -159,6 +162,12 @@ export interface OpenAIProvider extends ProviderV4 {
159
162
  }
160
163
 
161
164
  export interface OpenAIProviderSettings {
165
+ /** Settings for downloading JSON Lines batch results. */
166
+ batchResultDownloads?: {
167
+ /** Maximum UTF-8 bytes per row, excluding LF. Defaults to 64 MiB. */
168
+ maxLineBytes?: number;
169
+ };
170
+
162
171
  /**
163
172
  * Base URL for the OpenAI API calls.
164
173
  */
@@ -336,6 +345,7 @@ export function createOpenAI(
336
345
  const createBatch = () =>
337
346
  new OpenAIBatch({
338
347
  provider: `${providerName}.batch`,
348
+ maxLineBytes: options.batchResultDownloads?.maxLineBytes,
339
349
  config: {
340
350
  provider: `${providerName}.responses`,
341
351
  baseURL,
@@ -356,9 +366,12 @@ export function createOpenAI(
356
366
  provider.chat = createChatModel;
357
367
  provider.completion = createCompletionModel;
358
368
  provider.responses = createResponsesModel;
359
- provider.decisionModel = (modelId: OpenAIResponsesModelId) =>
360
- new DecisionLanguageModel({
361
- model: createResponsesModel(modelId),
369
+ provider.decisionModel = (modelId: OpenAIDecisionModelId) =>
370
+ new DecisionOpenAIModel(modelId, {
371
+ baseURL,
372
+ url: ({ path }) => `${baseURL}${path}`,
373
+ headers: getHeaders,
374
+ fetch: options.fetch,
362
375
  provider: `${providerName}.decision`,
363
376
  });
364
377
  provider.evaluationModel =
@@ -41,7 +41,11 @@ export class OpenAISkills implements SkillsV4 {
41
41
 
42
42
  for (const file of params.files) {
43
43
  const content = convertInlineFileDataToUint8Array(file.data);
44
- formData.append('files[]', new Blob([content]), file.path);
44
+ formData.append(
45
+ 'files[]',
46
+ new Blob([content as Uint8Array<ArrayBuffer>]),
47
+ file.path,
48
+ );
45
49
  }
46
50
 
47
51
  const { value: response } = await postFormDataToApi({
@@ -180,7 +180,7 @@ export class OpenAITranscriptionModel implements TranscriptionModelV4 {
180
180
  const formData = new FormData();
181
181
  const blob =
182
182
  audio instanceof Uint8Array
183
- ? new Blob([audio])
183
+ ? new Blob([audio as Uint8Array<ArrayBuffer>])
184
184
  : new Blob([convertBase64ToUint8Array(audio)]);
185
185
 
186
186
  formData.append('model', this.modelId);