@ai-sdk/openai 3.0.117 → 3.0.119

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.
@@ -325,55 +325,104 @@ The following OpenAI-specific metadata may be returned:
325
325
 
326
326
  #### Changing Reasoning Effort Mid-Conversation
327
327
 
328
- GPT-6 and later models can change reasoning effort between responses without
329
- changing the request-level `reasoningEffort` setting. The provider sends
330
- `reasoningEffortUpdate` as an OpenAI `configuration_update` input item before
331
- the next user message. Keeping the request-level effort unchanged preserves the
332
- original prompt prefix for prompt caching.
328
+ You can increase reasoning effort for difficult work or reduce it for routine
329
+ follow-ups while preserving the conversation prefix for prompt caching:
333
330
 
334
- ```ts highlight="13,32,34"
331
+ - Set the initial effort with request-level `providerOptions.openai.reasoningEffort` and keep that setting
332
+ unchanged throughout the conversation.
333
+ - For later changes, add a message-level `reasoningEffortUpdate` before the next
334
+ user message, following
335
+ [OpenAI's configuration update guidance](https://developers.openai.com/api/docs/guides/reasoning#change-reasoning-mid-conversation).
336
+
337
+ The new effort applies until another update changes it. When sending conversation
338
+ history in `messages`, include earlier effort updates along with the other messages.
339
+
340
+ Represent an update as an empty system message with
341
+ `providerOptions.openai.reasoningEffortUpdate`.
342
+
343
+ Set `allowSystemInMessages: true` to explicitly allow these trusted system messages
344
+ and suppress the warning for system messages in `messages`.
345
+
346
+ In this example, the conversation starts at low effort and switches to high
347
+ effort for the follow-up. Both requests keep `providerOptions.openai.reasoningEffort: 'low'`.
348
+
349
+ ```ts
335
350
  import {
336
351
  openai,
337
- type OpenAILanguageModelResponsesOptions,
338
- type OpenaiResponsesProviderMetadata,
352
+ type OpenAIResponsesSystemMessageOptions,
339
353
  } from '@ai-sdk/openai';
340
- import { generateText } from 'ai';
354
+ import { generateText, type ModelMessage } from 'ai';
355
+
356
+ const messages: ModelMessage[] = [
357
+ { role: 'user', content: 'Draft a migration plan.' },
358
+ ];
341
359
 
342
360
  const first = await generateText({
343
361
  model: openai.responses('gpt-6-astra'),
344
- prompt: 'Draft a database migration plan.',
345
- providerOptions: {
346
- openai: {
347
- reasoningEffort: 'low',
348
- } satisfies OpenAILanguageModelResponsesOptions,
349
- },
362
+ providerOptions: { openai: { reasoningEffort: 'low' } },
363
+ messages,
350
364
  });
351
365
 
352
- const metadata = first.providerMetadata as
353
- | OpenaiResponsesProviderMetadata
354
- | undefined;
355
- const previousResponseId = metadata?.openai.responseId;
356
-
357
- if (!previousResponseId) {
358
- throw new Error('OpenAI did not return a response ID.');
359
- }
366
+ messages.push(...first.response.messages);
367
+ messages.push(
368
+ {
369
+ role: 'system',
370
+ content: '',
371
+ providerOptions: {
372
+ openai: {
373
+ reasoningEffortUpdate: 'high',
374
+ } satisfies OpenAIResponsesSystemMessageOptions,
375
+ },
376
+ },
377
+ { role: 'user', content: 'Analyze the failure modes.' },
378
+ );
360
379
 
361
380
  const second = await generateText({
362
381
  model: openai.responses('gpt-6-astra'),
363
- prompt: 'Analyze the failure modes and propose rollback steps.',
364
- providerOptions: {
365
- openai: {
366
- previousResponseId,
367
- reasoningEffort: 'low',
368
- reasoningEffortUpdate: 'high',
369
- } satisfies OpenAILanguageModelResponsesOptions,
370
- },
382
+ providerOptions: { openai: { reasoningEffort: 'low' } },
383
+ allowSystemInMessages: true,
384
+ messages,
371
385
  });
386
+
387
+ messages.push(...second.response.messages);
372
388
  ```
373
389
 
374
- The response metadata continues to report the request-level reasoning effort,
375
- not the effective effort selected by `reasoningEffortUpdate`. Configuration
376
- updates are not supported with `reasoningMode: 'pro'` or `truncation: 'auto'`.
390
+ <Note>
391
+ The OpenAI Responses provider sends each message-level effort update as a
392
+ `configuration_update` input item.
393
+ </Note>
394
+
395
+ Message-level updates also work with `streamText` and `previousResponseId`.
396
+
397
+ ##### Restrictions
398
+
399
+ Effort updates are supported on GPT-6-family models with these settings:
400
+
401
+ | Setting | Required value |
402
+ | ------------------- | ---------------------- |
403
+ | `reasoningMode` | `'standard'` (default) |
404
+ | `truncation` | `'disabled'` (default) |
405
+
406
+ - Each update **must** use exactly `content: ''`.
407
+ - Updates cannot be consecutive, even with the same effort. This also applies
408
+ across the boundary with history referenced by previousResponseId or
409
+ conversation.
410
+
411
+ Requests that violate these restrictions are rejected.
412
+
413
+ <Note>
414
+ Effort update messages are sent even if `systemMessageMode` is set to
415
+ `remove`.
416
+ </Note>
417
+
418
+ ##### Request-Level Update Option
419
+
420
+ The request-level `providerOptions.openai.reasoningEffortUpdate` option is a
421
+ convenience that prepends a configuration update to the input. When using
422
+ message-level updates, omit this option.
423
+
424
+ The request-level option continues to warn and omit updates for unsupported
425
+ models or configurations.
377
426
 
378
427
  #### Reasoning Output
379
428
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ai-sdk/openai",
3
- "version": "3.0.117",
3
+ "version": "3.0.119",
4
4
  "license": "Apache-2.0",
5
5
  "sideEffects": false,
6
6
  "main": "./dist/index.js",
@@ -36,8 +36,8 @@
36
36
  }
37
37
  },
38
38
  "dependencies": {
39
- "@ai-sdk/provider": "3.0.17",
40
- "@ai-sdk/provider-utils": "4.0.54"
39
+ "@ai-sdk/provider": "3.0.18",
40
+ "@ai-sdk/provider-utils": "4.0.55"
41
41
  },
42
42
  "devDependencies": {
43
43
  "@types/node": "20.17.24",
package/src/index.ts CHANGED
@@ -2,6 +2,7 @@ export { createOpenAI, openai } from './openai-provider';
2
2
  export type { OpenAIProvider, OpenAIProviderSettings } from './openai-provider';
3
3
  export type {
4
4
  OpenAILanguageModelResponsesOptions,
5
+ OpenAIResponsesSystemMessageOptions,
5
6
  /** @deprecated Use `OpenAILanguageModelResponsesOptions` instead. */
6
7
  OpenAILanguageModelResponsesOptions as OpenAIResponsesProviderOptions,
7
8
  } from './responses/openai-responses-options';
@@ -16,6 +16,7 @@ import {
16
16
  type ToolNameMapping,
17
17
  } from '@ai-sdk/provider-utils';
18
18
  import { z } from 'zod/v4';
19
+ import { openaiResponsesSystemMessageOptionsSchema } from './openai-responses-options';
19
20
  import {
20
21
  applyPatchInputSchema,
21
22
  applyPatchOutputSchema,
@@ -330,6 +331,7 @@ export async function convertToOpenAIResponsesInput({
330
331
  store,
331
332
  hasConversation = false,
332
333
  hasPreviousResponseId = false,
334
+ configurationUpdateUnsupportedReason,
333
335
  hasLocalShellTool = false,
334
336
  hasShellTool = false,
335
337
  hasApplyPatchTool = false,
@@ -346,6 +348,7 @@ export async function convertToOpenAIResponsesInput({
346
348
  store: boolean;
347
349
  hasConversation?: boolean; // when true, skip assistant messages that already have item IDs
348
350
  hasPreviousResponseId?: boolean; // when true, skip reasoning and function-call items that already exist in the previous response chain
351
+ configurationUpdateUnsupportedReason?: string;
349
352
  hasLocalShellTool?: boolean;
350
353
  hasShellTool?: boolean;
351
354
  hasApplyPatchTool?: boolean;
@@ -371,6 +374,42 @@ export async function convertToOpenAIResponsesInput({
371
374
  for (const { role, content, providerOptions } of prompt) {
372
375
  switch (role) {
373
376
  case 'system': {
377
+ // Keep effort updates at their original positions so they apply to
378
+ // the same parts of the conversation when the history is sent again.
379
+ let options = await parseProviderOptions({
380
+ provider: providerOptionsName,
381
+ providerOptions,
382
+ schema: openaiResponsesSystemMessageOptionsSchema,
383
+ });
384
+ if (options == null && providerOptionsName !== 'openai') {
385
+ options = await parseProviderOptions({
386
+ provider: 'openai',
387
+ providerOptions,
388
+ schema: openaiResponsesSystemMessageOptionsSchema,
389
+ });
390
+ }
391
+ const effort = options?.reasoningEffortUpdate;
392
+ if (effort != null) {
393
+ const unsupportedReason =
394
+ content !== ''
395
+ ? 'Message-level reasoningEffortUpdate requires empty system message content.'
396
+ : configurationUpdateUnsupportedReason;
397
+
398
+ if (unsupportedReason != null) {
399
+ throw new UnsupportedFunctionalityError({
400
+ functionality: 'Message-level reasoningEffortUpdate',
401
+ message: unsupportedReason,
402
+ });
403
+ }
404
+
405
+ input.push({
406
+ type: 'configuration_update',
407
+ reasoning: { effort },
408
+ });
409
+ // The control is independent of systemMessageMode's text handling.
410
+ break;
411
+ }
412
+
374
413
  switch (systemMessageMode) {
375
414
  case 'system': {
376
415
  const promptCacheBreakpoint = getPromptCacheBreakpoint(
@@ -1,4 +1,5 @@
1
1
  import {
2
+ UnsupportedFunctionalityError,
2
3
  APICallError,
3
4
  type JSONValue,
4
5
  type LanguageModelV3,
@@ -72,6 +73,7 @@ import {
72
73
  import {
73
74
  openaiLanguageModelResponsesOptionsSchema,
74
75
  TOP_LOGPROBS_MAX,
76
+ type OpenAILanguageModelResponsesOptions,
75
77
  type OpenAIResponsesModelId,
76
78
  } from './openai-responses-options';
77
79
  import { prepareResponsesTools } from './openai-responses-prepare-tools';
@@ -108,6 +110,30 @@ function extractApprovalRequestIdToToolCallIdMapping(
108
110
  return mapping;
109
111
  }
110
112
 
113
+ /**
114
+ * Enforces OpenAI's configuration update restrictions for reasoningEffortUpdate,
115
+ * returning a string describing the unsupported reason if any.
116
+ *
117
+ * @see https://developers.openai.com/api/docs/guides/reasoning#change-reasoning-mid-conversation
118
+ */
119
+ function getConfigurationUpdateUnsupportedReason({
120
+ modelCapabilities,
121
+ options,
122
+ }: {
123
+ modelCapabilities: { supportsConfigurationUpdate: boolean };
124
+ options: OpenAILanguageModelResponsesOptions | undefined;
125
+ }): string | undefined {
126
+ if (!modelCapabilities.supportsConfigurationUpdate) {
127
+ return 'reasoningEffortUpdate is only supported by GPT-6 and later models';
128
+ }
129
+
130
+ if (options?.reasoningMode === 'pro' || options?.truncation === 'auto') {
131
+ return 'reasoningEffortUpdate requires standard reasoning mode without automatic truncation';
132
+ }
133
+
134
+ return undefined;
135
+ }
136
+
111
137
  export class OpenAIResponsesLanguageModel implements LanguageModelV3 {
112
138
  readonly specificationVersion = 'v3';
113
139
 
@@ -246,9 +272,16 @@ export class OpenAIResponsesLanguageModel implements LanguageModelV3 {
246
272
  supportsAsyncToolCalling: modelCapabilities.supportsAsyncToolCalling,
247
273
  });
248
274
 
275
+ const configurationUpdateUnsupportedReason =
276
+ getConfigurationUpdateUnsupportedReason({
277
+ modelCapabilities,
278
+ options: openaiOptions,
279
+ });
280
+
249
281
  const { input, warnings: inputWarnings } =
250
282
  await convertToOpenAIResponsesInput({
251
283
  prompt,
284
+ configurationUpdateUnsupportedReason,
252
285
  toolNameMapping,
253
286
  systemMessageMode:
254
287
  openaiOptions?.systemMessageMode ??
@@ -276,25 +309,41 @@ export class OpenAIResponsesLanguageModel implements LanguageModelV3 {
276
309
  warnings.push(...inputWarnings);
277
310
 
278
311
  const reasoningEffortUpdate = openaiOptions?.reasoningEffortUpdate;
279
- const configurationUpdateIsSupported =
280
- reasoningEffortUpdate == null ||
281
- (modelCapabilities.supportsConfigurationUpdate &&
282
- openaiOptions?.reasoningMode !== 'pro' &&
283
- openaiOptions?.truncation !== 'auto');
284
-
285
- if (reasoningEffortUpdate != null && !configurationUpdateIsSupported) {
312
+ if (
313
+ reasoningEffortUpdate != null &&
314
+ configurationUpdateUnsupportedReason != null
315
+ ) {
286
316
  warnings.push({
287
317
  type: 'unsupported',
288
318
  feature: 'reasoningEffortUpdate',
289
- details: !modelCapabilities.supportsConfigurationUpdate
290
- ? 'reasoningEffortUpdate is only supported by GPT-6 and later models'
291
- : 'reasoningEffortUpdate requires standard reasoning mode without automatic truncation',
319
+ details: configurationUpdateUnsupportedReason,
292
320
  });
293
321
  } else if (reasoningEffortUpdate != null) {
294
- input.unshift({
295
- type: 'configuration_update',
296
- reasoning: { effort: reasoningEffortUpdate },
297
- });
322
+ const firstItem = input[0];
323
+ // If the first item already sets this effort, prepending another update
324
+ // would create an adjacent pair that OpenAI rejects.
325
+ if (
326
+ firstItem?.type !== 'configuration_update' ||
327
+ firstItem.reasoning.effort !== reasoningEffortUpdate
328
+ ) {
329
+ input.unshift({
330
+ type: 'configuration_update',
331
+ reasoning: { effort: reasoningEffortUpdate },
332
+ });
333
+ }
334
+ }
335
+
336
+ // Conversion and prepending can make updates adjacent, so check afterward
337
+ // to catch combinations that OpenAI would reject.
338
+ for (let i = 1; i < input.length; i++) {
339
+ if (
340
+ input[i - 1].type === 'configuration_update' &&
341
+ input[i].type === 'configuration_update'
342
+ ) {
343
+ throw new UnsupportedFunctionalityError({
344
+ functionality: 'Adjacent reasoning effort configuration updates',
345
+ });
346
+ }
298
347
  }
299
348
 
300
349
  const strictJsonSchema = openaiOptions?.strictJsonSchema ?? true;
@@ -405,3 +405,25 @@ export const openaiLanguageModelResponsesOptionsSchema = lazySchema(() =>
405
405
  export type OpenAILanguageModelResponsesOptions = InferSchema<
406
406
  typeof openaiLanguageModelResponsesOptionsSchema
407
407
  >;
408
+
409
+ export const openaiResponsesSystemMessageOptionsSchema = lazySchema(() =>
410
+ zodSchema(
411
+ z.object({
412
+ /**
413
+ * Emit a configuration update at this position in Responses history.
414
+ * Requires empty system message content and the same supported
415
+ * configuration as the request-level reasoningEffortUpdate option.
416
+ * Unsupported historical updates throw instead of being omitted.
417
+ *
418
+ * @see https://developers.openai.com/api/docs/guides/reasoning#change-reasoning-mid-conversation
419
+ */
420
+ reasoningEffortUpdate: z
421
+ .enum(['low', 'medium', 'high', 'xhigh', 'max'])
422
+ .optional(),
423
+ }),
424
+ ),
425
+ );
426
+
427
+ export type OpenAIResponsesSystemMessageOptions = InferSchema<
428
+ typeof openaiResponsesSystemMessageOptionsSchema
429
+ >;