@ai-sdk/openai 4.0.104 → 4.0.105

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.
@@ -3695,8 +3695,10 @@ include probability distributions. Boolean questions use the API's predicate
3695
3695
  primitive and return P(true). Apply thresholds in application code, for example
3696
3696
  `answers.requestsRefund.probability >= 0.5`.
3697
3697
 
3698
- Structured state, instructions, and criterion descriptions are serialized as JSON
3699
- text. Score levels receive zero-based index labels. Non-null Boolean criteria are
3698
+ JSON state parts that hold an object or an array of objects are sent as native
3699
+ `input_json` parts when they fit its limits (32 container levels, 10,000 values, and
3700
+ 256 KiB of compact UTF-8 JSON). Larger JSON state, other JSON state values, structured
3701
+ instructions, and criterion descriptions are serialized as JSON text. Score levels receive zero-based index labels. Non-null Boolean criteria are
3700
3702
  appended to predicate instructions under `Criteria for true:` and `Criteria for false:`.
3701
3703
  String criteria are passed through; structured criteria are serialized as JSON text.
3702
3704
 
@@ -3718,11 +3720,15 @@ state: [
3718
3720
 
3719
3721
  The factory respects `createOpenAI` settings, including custom fetch and base URL.
3720
3722
  Set `providerOptions.openai.safetyIdentifier` to an opaque end-user identifier for
3721
- safety monitoring (up to 128 characters). For example:
3723
+ safety monitoring (up to 128 characters). Set `providerOptions.openai.promptCacheKey`
3724
+ to partition prompt cache reuse across requests that share `state`. For example:
3722
3725
 
3723
3726
  ```ts
3724
3727
  providerOptions: {
3725
- openai: { safetyIdentifier: 'user-123' } satisfies OpenAIDecisionModelOptions,
3728
+ openai: {
3729
+ safetyIdentifier: 'user-123',
3730
+ promptCacheKey: 'tenant-42',
3731
+ } satisfies OpenAIDecisionModelOptions,
3726
3732
  }
3727
3733
  ```
3728
3734
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ai-sdk/openai",
3
- "version": "4.0.104",
3
+ "version": "4.0.105",
4
4
  "type": "module",
5
5
  "license": "Apache-2.0",
6
6
  "sideEffects": false,
@@ -36,7 +36,7 @@
36
36
  },
37
37
  "dependencies": {
38
38
  "@ai-sdk/provider": "4.0.34",
39
- "@ai-sdk/provider-utils": "5.0.70"
39
+ "@ai-sdk/provider-utils": "5.0.71"
40
40
  },
41
41
  "devDependencies": {
42
42
  "@ai-sdk/test-server": "2.0.4",
@@ -26,7 +26,10 @@ import {
26
26
  type ParseResult,
27
27
  } from '@ai-sdk/provider-utils';
28
28
  import { openaiFailedResponseHandler } from '../openai-error';
29
- import { getOpenAILanguageModelCapabilities } from '../openai-language-model-capabilities';
29
+ import {
30
+ getOpenAILanguageModelCapabilities,
31
+ resolveOpenAIReasoningEffort,
32
+ } from '../openai-language-model-capabilities';
30
33
  import {
31
34
  createOpenAIProviderStreamError,
32
35
  throwIfOpenAIStreamErrorBeforeOutput,
@@ -121,26 +124,38 @@ export class OpenAIChatLanguageModel implements LanguageModelV4 {
121
124
 
122
125
  const modelCapabilities = getOpenAILanguageModelCapabilities(this.modelId);
123
126
 
124
- // AI SDK reasoning values map directly to the OpenAI reasoning values.
125
- let resolvedReasoningEffort =
126
- openaiOptions.reasoningEffort ??
127
- (isCustomReasoning(reasoning) ? reasoning : undefined);
127
+ // Some models reject function tools unless reasoning is disabled, even
128
+ // when no reasoning effort is sent, so reasoning is set to 'none'.
129
+ const requiresNoReasoningEffort =
130
+ !modelCapabilities.supportsChatFunctionToolsWithReasoning &&
131
+ openaiOptions.reasoningEffort == null &&
132
+ (tools?.some(tool => tool.type === 'function') ?? false);
128
133
 
129
134
  if (
130
- resolvedReasoningEffort != null &&
131
- modelCapabilities.supportedReasoningEfforts != null &&
132
- !modelCapabilities.supportedReasoningEfforts.includes(
133
- resolvedReasoningEffort,
134
- )
135
+ requiresNoReasoningEffort &&
136
+ isCustomReasoning(reasoning) &&
137
+ reasoning !== 'none'
135
138
  ) {
136
139
  warnings.push({
137
- type: 'unsupported',
138
- feature: 'reasoningEffort',
139
- details: `${this.modelId} only supports the following reasoning efforts: ${modelCapabilities.supportedReasoningEfforts.join(', ')}`,
140
+ type: 'compatibility',
141
+ feature: 'reasoning',
142
+ details: `${this.modelId} does not support function tools with reasoning in the Chat Completions API. reasoning effort set to "none". Use the Responses API to combine reasoning with function tools.`,
140
143
  });
141
- resolvedReasoningEffort = undefined;
142
144
  }
143
145
 
146
+ const resolvedReasoningEffort = requiresNoReasoningEffort
147
+ ? 'none'
148
+ : resolveOpenAIReasoningEffort({
149
+ providerReasoningEffort: openaiOptions.reasoningEffort,
150
+ reasoning,
151
+ // Only the Responses API supports `max`.
152
+ supportedReasoningEfforts:
153
+ modelCapabilities.supportedReasoningEfforts?.filter(
154
+ effort => effort !== 'max',
155
+ ),
156
+ warnings,
157
+ });
158
+
144
159
  const isReasoningModel =
145
160
  openaiOptions.forceReasoning ?? modelCapabilities.isReasoningModel;
146
161
 
@@ -246,7 +261,7 @@ export class OpenAIChatLanguageModel implements LanguageModelV4 {
246
261
  };
247
262
 
248
263
  if (
249
- modelCapabilities.supportedReasoningEfforts != null &&
264
+ modelCapabilities.supportsConfigurationUpdate &&
250
265
  baseArgs.prompt_cache_retention != null
251
266
  ) {
252
267
  baseArgs.prompt_cache_retention = undefined;
@@ -10,6 +10,7 @@ export const openaiDecisionModelOptions = lazySchema(() =>
10
10
  z.object({
11
11
  /** An opaque end-user identifier for safety monitoring, up to 128 characters. */
12
12
  safetyIdentifier: z.string().max(128).optional(),
13
+ promptCacheKey: z.string().optional(),
13
14
  }),
14
15
  ),
15
16
  );
@@ -4,6 +4,8 @@ import {
4
4
  type Experimental_DecisionModelV4 as DecisionModelV4,
5
5
  type Experimental_DecisionModelV4Answer as DecisionModelV4Answer,
6
6
  type Experimental_DecisionModelV4Input as DecisionModelV4Input,
7
+ type JSONObject,
8
+ type JSONValue,
7
9
  } from '@ai-sdk/provider';
8
10
  import {
9
11
  lazySchema,
@@ -148,6 +150,7 @@ export class DecisionOpenAIModel implements DecisionModelV4 {
148
150
  body: {
149
151
  model: this.modelId,
150
152
  safety_identifier: openaiOptions?.safetyIdentifier,
153
+ prompt_cache_key: openaiOptions?.promptCacheKey,
151
154
  input: [
152
155
  {
153
156
  role: 'user',
@@ -155,10 +158,9 @@ export class DecisionOpenAIModel implements DecisionModelV4 {
155
158
  if (part.type === 'text')
156
159
  return { type: 'input_text', text: part.text };
157
160
  if (part.type === 'json')
158
- return {
159
- type: 'input_text',
160
- text: JSON.stringify(part.value),
161
- };
161
+ return isInputJsonData(part.value)
162
+ ? { type: 'input_json', data: part.value }
163
+ : { type: 'input_text', text: JSON.stringify(part.value) };
162
164
  if (part.data.type !== 'data') {
163
165
  throw new UnsupportedFunctionalityError({
164
166
  functionality: `OpenAI decision file input: ${part.mediaType} (${part.data.type})`,
@@ -323,7 +325,7 @@ export class DecisionOpenAIModel implements DecisionModelV4 {
323
325
  ? [{ type: 'unsupported' as const, feature: 'reasoning' }]
324
326
  : []),
325
327
  ...Object.keys(providerOptions?.openai ?? {})
326
- .filter(option => option !== 'safetyIdentifier')
328
+ .filter(option => !SUPPORTED_PROVIDER_OPTIONS.has(option))
327
329
  .map(option => ({
328
330
  type: 'unsupported' as const,
329
331
  feature: `providerOptions.openai.${option}`,
@@ -351,6 +353,45 @@ export class DecisionOpenAIModel implements DecisionModelV4 {
351
353
  }
352
354
  }
353
355
 
356
+ const SUPPORTED_PROVIDER_OPTIONS = new Set([
357
+ 'safetyIdentifier',
358
+ 'promptCacheKey',
359
+ ]);
360
+
354
361
  function toText(input: DecisionModelV4Input): string {
355
362
  return typeof input === 'string' ? input : JSON.stringify(input);
356
363
  }
364
+
365
+ const INPUT_JSON_MAX_DEPTH = 32;
366
+ const INPUT_JSON_MAX_VALUES = 10_000;
367
+ const INPUT_JSON_MAX_BYTES = 256 * 1024;
368
+
369
+ function isInputJsonData(value: JSONValue): value is JSONObject | JSONObject[] {
370
+ const hasInputJsonShape = Array.isArray(value)
371
+ ? value.length > 0 && value.every(isJSONObject)
372
+ : isJSONObject(value);
373
+ return hasInputJsonShape && fitsInputJsonLimits(value);
374
+ }
375
+
376
+ function fitsInputJsonLimits(value: JSONValue): boolean {
377
+ let count = 0;
378
+ const visit = (node: JSONValue | undefined, depth: number): boolean => {
379
+ count += 1;
380
+ if (count > INPUT_JSON_MAX_VALUES) return false;
381
+ if (node === null || typeof node !== 'object') return true;
382
+ if (depth > INPUT_JSON_MAX_DEPTH) return false;
383
+ const children: readonly (JSONValue | undefined)[] = Array.isArray(node)
384
+ ? node
385
+ : Object.values(node);
386
+ return children.every(child => visit(child, depth + 1));
387
+ };
388
+ return (
389
+ visit(value, 1) &&
390
+ new TextEncoder().encode(JSON.stringify(value)).length <=
391
+ INPUT_JSON_MAX_BYTES
392
+ );
393
+ }
394
+
395
+ function isJSONObject(value: JSONValue): value is JSONObject {
396
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
397
+ }
@@ -1,3 +1,25 @@
1
+ import type {
2
+ LanguageModelV4CallOptions,
3
+ SharedV4Warning,
4
+ } from '@ai-sdk/provider';
5
+ import { mapReasoningToProviderEffort } from '@ai-sdk/provider-utils';
6
+
7
+ type OpenAIReasoningEffort = Exclude<
8
+ LanguageModelV4CallOptions['reasoning'],
9
+ 'provider-default' | undefined
10
+ >;
11
+
12
+ // Unknown model families send the reasoning value directly.
13
+ const ALL_OPENAI_REASONING_EFFORTS: readonly OpenAIReasoningEffort[] = [
14
+ 'none',
15
+ 'minimal',
16
+ 'low',
17
+ 'medium',
18
+ 'high',
19
+ 'xhigh',
20
+ 'max',
21
+ ];
22
+
1
23
  export type OpenAILanguageModelCapabilities = {
2
24
  isReasoningModel: boolean;
3
25
  systemMessageMode: 'remove' | 'system' | 'developer';
@@ -5,7 +27,17 @@ export type OpenAILanguageModelCapabilities = {
5
27
  supportsPriorityProcessing: boolean;
6
28
  supportsConfigurationUpdate: boolean;
7
29
  supportsAsyncToolCalling: boolean;
8
- supportedReasoningEfforts: readonly string[] | undefined;
30
+ /**
31
+ * Reasoning efforts supported by the model family, or `undefined` when they
32
+ * are not known.
33
+ */
34
+ supportedReasoningEfforts: readonly OpenAIReasoningEffort[] | undefined;
35
+
36
+ /**
37
+ * Whether function tools can be combined with a reasoning effort other than
38
+ * `none` in the Chat Completions API.
39
+ */
40
+ supportsChatFunctionToolsWithReasoning: boolean;
9
41
 
10
42
  /**
11
43
  * Allow temperature, topP, logProbs when reasoningEffort is none.
@@ -24,6 +56,15 @@ export function getOpenAILanguageModelCapabilities(
24
56
  const isGptNanoModel = gptVersion?.variant?.startsWith('nano') ?? false;
25
57
  const isGpt6OrLaterModel = gptVersion != null && gptVersion.major >= 6;
26
58
 
59
+ // Pro, codex, and chat variants are excluded because their supported
60
+ // efforts are not known.
61
+ const isStandardGpt5Model =
62
+ gptVersion?.major === 5 &&
63
+ (gptVersion.variant == null ||
64
+ /^(?:(?:mini|nano|sol|luna|terra)(?:-\d{4}-\d{2}-\d{2})?|\d{4}-\d{2}-\d{2})$/.test(
65
+ gptVersion.variant,
66
+ ));
67
+
27
68
  const isGpt6OrLaterModelWithNoneEffort =
28
69
  modelId === 'gpt-6-sol' ||
29
70
  modelId === 'gpt-6-luna' ||
@@ -68,13 +109,37 @@ export function getOpenAILanguageModelCapabilities(
68
109
  ? ['none', 'low', 'medium', 'high', 'xhigh', 'max']
69
110
  : isGpt6OrLaterModel
70
111
  ? ['low', 'medium', 'high', 'xhigh', 'max']
71
- : undefined,
112
+ : isStandardGpt5Model
113
+ ? getGpt5ReasoningEfforts(gptVersion.minor ?? 0)
114
+ : undefined,
115
+ // GPT-5.6 rejects function tools with reasoning in the Chat Completions API.
116
+ supportsChatFunctionToolsWithReasoning: !(
117
+ gptVersion?.major === 5 && (gptVersion.minor ?? 0) >= 6
118
+ ),
72
119
  isReasoningModel,
73
120
  systemMessageMode,
74
121
  supportsNonReasoningParameters,
75
122
  };
76
123
  }
77
124
 
125
+ function getGpt5ReasoningEfforts(
126
+ minor: number,
127
+ ): readonly OpenAIReasoningEffort[] {
128
+ if (minor === 0) {
129
+ return ['minimal', 'low', 'medium', 'high'];
130
+ }
131
+
132
+ if (minor === 1) {
133
+ return ['none', 'low', 'medium', 'high'];
134
+ }
135
+
136
+ if (minor < 6) {
137
+ return ['none', 'low', 'medium', 'high', 'xhigh'];
138
+ }
139
+
140
+ return ['none', 'low', 'medium', 'high', 'xhigh', 'max'];
141
+ }
142
+
78
143
  function getOSeriesVersion(modelId: string): number | undefined {
79
144
  const match = /^o(\d+)(?:-|$)/.exec(modelId);
80
145
  return match == null ? undefined : Number(match[1]);
@@ -99,3 +164,33 @@ function getGptVersion(modelId: string):
99
164
  variant: match[3],
100
165
  };
101
166
  }
167
+
168
+ /**
169
+ * Resolves the reasoning effort to send to OpenAI.
170
+ *
171
+ * A reasoning effort from provider options is sent unchanged. Otherwise the
172
+ * top-level reasoning value is mapped to the closest effort that the model
173
+ * family supports, or sent directly when the supported efforts are not known.
174
+ */
175
+ export function resolveOpenAIReasoningEffort({
176
+ providerReasoningEffort,
177
+ reasoning,
178
+ supportedReasoningEfforts,
179
+ warnings,
180
+ }: {
181
+ providerReasoningEffort: string | null | undefined;
182
+ reasoning: LanguageModelV4CallOptions['reasoning'];
183
+ supportedReasoningEfforts: readonly OpenAIReasoningEffort[] | undefined;
184
+ warnings: SharedV4Warning[];
185
+ }): string | undefined {
186
+ return mapReasoningToProviderEffort({
187
+ reasoning,
188
+ providerOptionsEffort: providerReasoningEffort,
189
+ effortMap: Object.fromEntries(
190
+ (supportedReasoningEfforts ?? ALL_OPENAI_REASONING_EFFORTS).map(
191
+ effort => [effort, effort],
192
+ ),
193
+ ),
194
+ warnings,
195
+ });
196
+ }
@@ -22,7 +22,6 @@ import {
22
22
  createJsonResponseHandler,
23
23
  createToolNameMapping,
24
24
  generateId,
25
- isCustomReasoning,
26
25
  parseProviderOptions,
27
26
  postJsonToApi,
28
27
  serializeModelOptions,
@@ -37,7 +36,10 @@ import {
37
36
  type OpenAIConfig,
38
37
  } from '../openai-config';
39
38
  import { openaiFailedResponseHandler } from '../openai-error';
40
- import { getOpenAILanguageModelCapabilities } from '../openai-language-model-capabilities';
39
+ import {
40
+ getOpenAILanguageModelCapabilities,
41
+ resolveOpenAIReasoningEffort,
42
+ } from '../openai-language-model-capabilities';
41
43
  import {
42
44
  createOpenAIProviderStreamError,
43
45
  throwIfOpenAIStreamErrorBeforeOutput,
@@ -341,24 +343,12 @@ export class OpenAIResponsesLanguageModel implements LanguageModelV4 {
341
343
  });
342
344
  }
343
345
 
344
- let resolvedReasoningEffort =
345
- openaiOptions?.reasoningEffort ??
346
- (isCustomReasoning(reasoning) ? reasoning : undefined);
347
-
348
- if (
349
- resolvedReasoningEffort != null &&
350
- modelCapabilities.supportedReasoningEfforts != null &&
351
- !modelCapabilities.supportedReasoningEfforts.includes(
352
- resolvedReasoningEffort,
353
- )
354
- ) {
355
- warnings.push({
356
- type: 'unsupported',
357
- feature: 'reasoningEffort',
358
- details: `${modelId} only supports the following reasoning efforts: ${modelCapabilities.supportedReasoningEfforts.join(', ')}`,
359
- });
360
- resolvedReasoningEffort = undefined;
361
- }
346
+ const resolvedReasoningEffort = resolveOpenAIReasoningEffort({
347
+ providerReasoningEffort: openaiOptions?.reasoningEffort,
348
+ reasoning,
349
+ supportedReasoningEfforts: modelCapabilities.supportedReasoningEfforts,
350
+ warnings,
351
+ });
362
352
 
363
353
  const resolvedReasoningSummary =
364
354
  openaiOptions?.reasoningSummary !== undefined
@@ -458,10 +448,13 @@ export class OpenAIResponsesLanguageModel implements LanguageModelV4 {
458
448
 
459
449
  // The schema accepts update efforts supported by any supported model. Check
460
450
  // whether this specific model supports the requested effort.
451
+ const supportedReasoningEfforts =
452
+ modelCapabilities.supportedReasoningEfforts as
453
+ | readonly string[]
454
+ | undefined;
461
455
  const getUpdateEffortUnsupportedReason = (effort: string | undefined) =>
462
- effort != null &&
463
- modelCapabilities.supportedReasoningEfforts?.includes(effort) === false
464
- ? `${modelId} only supports the following reasoning efforts: ${modelCapabilities.supportedReasoningEfforts.join(', ')}`
456
+ effort != null && supportedReasoningEfforts?.includes(effort) === false
457
+ ? `${modelId} only supports the following reasoning efforts: ${supportedReasoningEfforts.join(', ')}`
465
458
  : undefined;
466
459
 
467
460
  // Reject configuration updates whose effort value is unsupported by the selected model.
@@ -725,7 +718,7 @@ export class OpenAIResponsesLanguageModel implements LanguageModelV4 {
725
718
  }
726
719
 
727
720
  if (
728
- modelCapabilities.supportedReasoningEfforts != null &&
721
+ modelCapabilities.supportsConfigurationUpdate &&
729
722
  (baseArgs.top_logprobs != null ||
730
723
  baseArgs.include?.includes('message.output_text.logprobs'))
731
724
  ) {