@ai-sdk/openai 4.0.0-beta.11 → 4.0.0-beta.13

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.
@@ -257,6 +257,11 @@ The following provider options are available:
257
257
  - **forceReasoning** _boolean_
258
258
  Force treating this model as a reasoning model. This is useful for "stealth" reasoning models (e.g. via a custom baseURL) where the model ID is not recognized by the SDK's allowlist. When enabled, the SDK applies reasoning-model parameter compatibility rules and defaults `systemMessageMode` to `developer` unless overridden.
259
259
 
260
+ - **contextManagement** _Array<object>_
261
+ Enable server-side context management (compaction). When configured, the server automatically compresses conversation context when token usage crosses a specified threshold. Each object in the array should have:
262
+ - `type`: `'compaction'`
263
+ - `compactThreshold`: _number_ — the token count at which compaction is triggered
264
+
260
265
  The OpenAI responses provider also returns provider-specific metadata:
261
266
 
262
267
  For Responses models, you can type this metadata using `OpenaiResponsesProviderMetadata`:
@@ -1511,6 +1516,125 @@ for (const part of result.content) {
1511
1516
  are fields like `filename` that are directly available on the source object.
1512
1517
  </Note>
1513
1518
 
1519
+ #### Compaction
1520
+
1521
+ The OpenAI Responses API supports server-side context compaction. When enabled, the server automatically compresses conversation context when token usage crosses a configured threshold. This is useful for long-running conversations or agent loops where you want to stay within token limits without manually managing context.
1522
+
1523
+ The compaction item returned by the server is opaque and encrypted — it carries forward key prior state and reasoning into the next turn using fewer tokens. The AI SDK handles this automatically: compaction items are returned as text parts with special `providerMetadata`, and when passed back in subsequent requests they are sent as compaction input items.
1524
+
1525
+ ```ts highlight="7-11"
1526
+ import {
1527
+ openai,
1528
+ type OpenAILanguageModelResponsesOptions,
1529
+ } from '@ai-sdk/openai';
1530
+ import { generateText } from 'ai';
1531
+
1532
+ const result = await generateText({
1533
+ model: openai.responses('gpt-5.2'),
1534
+ messages: conversationHistory,
1535
+ providerOptions: {
1536
+ openai: {
1537
+ store: false,
1538
+ contextManagement: [{ type: 'compaction', compactThreshold: 50000 }],
1539
+ } satisfies OpenAILanguageModelResponsesOptions,
1540
+ },
1541
+ });
1542
+ ```
1543
+
1544
+ **Configuration:**
1545
+
1546
+ - **type** — Must be `'compaction'`
1547
+ - **compactThreshold** — The token count at which compaction is triggered. When the rendered input token count crosses this threshold, the server runs a compaction pass before continuing inference.
1548
+
1549
+ <Note>
1550
+ Server-side compaction is ZDR-friendly when you set `store: false` on your
1551
+ requests.
1552
+ </Note>
1553
+
1554
+ ##### Detecting Compaction in Streams
1555
+
1556
+ When using `streamText`, you can detect compaction by checking the `providerMetadata` on `text-start` and `text-end` events:
1557
+
1558
+ ```ts
1559
+ import {
1560
+ openai,
1561
+ type OpenAILanguageModelResponsesOptions,
1562
+ } from '@ai-sdk/openai';
1563
+ import { streamText } from 'ai';
1564
+
1565
+ const result = streamText({
1566
+ model: openai.responses('gpt-5.2'),
1567
+ messages: conversationHistory,
1568
+ providerOptions: {
1569
+ openai: {
1570
+ store: false,
1571
+ contextManagement: [{ type: 'compaction', compactThreshold: 50000 }],
1572
+ } satisfies OpenAILanguageModelResponsesOptions,
1573
+ },
1574
+ });
1575
+
1576
+ for await (const part of result.fullStream) {
1577
+ switch (part.type) {
1578
+ case 'text-start': {
1579
+ const isCompaction = part.providerMetadata?.openai?.type === 'compaction';
1580
+ if (isCompaction) {
1581
+ // ... your logic
1582
+ }
1583
+ break;
1584
+ }
1585
+ case 'text-end': {
1586
+ const isCompaction = part.providerMetadata?.openai?.type === 'compaction';
1587
+ if (isCompaction) {
1588
+ // ... your logic
1589
+ }
1590
+ break;
1591
+ }
1592
+ case 'text-delta': {
1593
+ process.stdout.write(part.text);
1594
+ break;
1595
+ }
1596
+ }
1597
+ }
1598
+ ```
1599
+
1600
+ ##### Compaction in UI Applications
1601
+
1602
+ When using `useChat` or other UI hooks, compaction items appear as text parts with `providerMetadata`. You can detect and style them differently in your UI:
1603
+
1604
+ ```tsx
1605
+ {
1606
+ message.parts.map((part, index) => {
1607
+ if (part.type === 'text') {
1608
+ const isCompaction =
1609
+ (part.providerMetadata?.openai as { type?: string } | undefined)
1610
+ ?.type === 'compaction';
1611
+
1612
+ if (isCompaction) {
1613
+ return (
1614
+ <div
1615
+ key={index}
1616
+ className="bg-yellow-100 border-l-4 border-yellow-500 p-2"
1617
+ >
1618
+ <span className="font-bold">[Context Compacted]</span>
1619
+ <p className="text-sm text-yellow-700">
1620
+ The server compressed the conversation context to reduce token
1621
+ usage.
1622
+ </p>
1623
+ </div>
1624
+ );
1625
+ }
1626
+ return <div key={index}>{part.text}</div>;
1627
+ }
1628
+ });
1629
+ }
1630
+ ```
1631
+
1632
+ The metadata includes the following fields:
1633
+
1634
+ - **type** — Always `'compaction'`
1635
+ - **itemId** _string_ — The ID of the compaction item in the Responses API
1636
+ - **encryptedContent** _string_ (optional) — The encrypted compaction state. This is automatically sent back to the API when the message is included in subsequent requests.
1637
+
1514
1638
  ### Chat Models
1515
1639
 
1516
1640
  You can create models that call the [OpenAI chat API](https://platform.openai.com/docs/api-reference/chat) using the `.chat()` factory method.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ai-sdk/openai",
3
- "version": "4.0.0-beta.11",
3
+ "version": "4.0.0-beta.13",
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": "4.0.0-beta.3",
40
- "@ai-sdk/provider-utils": "5.0.0-beta.5"
39
+ "@ai-sdk/provider": "4.0.0-beta.4",
40
+ "@ai-sdk/provider-utils": "5.0.0-beta.6"
41
41
  },
42
42
  "devDependencies": {
43
43
  "@types/node": "20.17.24",
@@ -71,9 +71,7 @@
71
71
  "build": "pnpm clean && tsup --tsconfig tsconfig.build.json",
72
72
  "build:watch": "pnpm clean && tsup --watch",
73
73
  "clean": "del-cli dist docs *.tsbuildinfo",
74
- "lint": "eslint \"./**/*.ts*\"",
75
74
  "type-check": "tsc --build",
76
- "prettier-check": "prettier --check \"./**/*.ts*\"",
77
75
  "test": "pnpm test:node && pnpm test:edge",
78
76
  "test:update": "pnpm test:node -u",
79
77
  "test:watch": "vitest --config vitest.node.config.js",
@@ -17,6 +17,7 @@ import {
17
17
  createEventSourceResponseHandler,
18
18
  createJsonResponseHandler,
19
19
  generateId,
20
+ isCustomReasoning,
20
21
  isParsableJson,
21
22
  parseProviderOptions,
22
23
  postJsonToApi,
@@ -81,6 +82,7 @@ export class OpenAIChatLanguageModel implements LanguageModelV4 {
81
82
  seed,
82
83
  tools,
83
84
  toolChoice,
85
+ reasoning,
84
86
  providerOptions,
85
87
  }: LanguageModelV4CallOptions) {
86
88
  const warnings: SharedV4Warning[] = [];
@@ -94,6 +96,12 @@ export class OpenAIChatLanguageModel implements LanguageModelV4 {
94
96
  })) ?? {};
95
97
 
96
98
  const modelCapabilities = getOpenAILanguageModelCapabilities(this.modelId);
99
+
100
+ // AI SDK reasoning values map directly to the OpenAI reasoning values.
101
+ const resolvedReasoningEffort =
102
+ openaiOptions.reasoningEffort ??
103
+ (isCustomReasoning(reasoning) ? reasoning : undefined);
104
+
97
105
  const isReasoningModel =
98
106
  openaiOptions.forceReasoning ?? modelCapabilities.isReasoningModel;
99
107
 
@@ -168,7 +176,7 @@ export class OpenAIChatLanguageModel implements LanguageModelV4 {
168
176
  store: openaiOptions.store,
169
177
  metadata: openaiOptions.metadata,
170
178
  prediction: openaiOptions.prediction,
171
- reasoning_effort: openaiOptions.reasoningEffort,
179
+ reasoning_effort: resolvedReasoningEffort,
172
180
  service_tier: openaiOptions.serviceTier,
173
181
  prompt_cache_key: openaiOptions.promptCacheKey,
174
182
  prompt_cache_retention: openaiOptions.promptCacheRetention,
@@ -184,7 +192,7 @@ export class OpenAIChatLanguageModel implements LanguageModelV4 {
184
192
  // when reasoning effort is none, gpt-5.1 models allow temperature, topP, logprobs
185
193
  // https://platform.openai.com/docs/guides/latest-model#gpt-5-1-parameter-compatibility
186
194
  if (
187
- openaiOptions.reasoningEffort !== 'none' ||
195
+ resolvedReasoningEffort !== 'none' ||
188
196
  !modelCapabilities.supportsNonReasoningParameters
189
197
  ) {
190
198
  if (baseArgs.temperature != null) {
package/src/index.ts CHANGED
@@ -15,6 +15,7 @@ export type { OpenAIEmbeddingModelOptions } from './embedding/openai-embedding-o
15
15
  export type { OpenAISpeechModelOptions } from './speech/openai-speech-options';
16
16
  export type { OpenAITranscriptionModelOptions } from './transcription/openai-transcription-options';
17
17
  export type {
18
+ OpenaiResponsesCompactionProviderMetadata,
18
19
  OpenaiResponsesProviderMetadata,
19
20
  OpenaiResponsesReasoningProviderMetadata,
20
21
  OpenaiResponsesTextProviderMetadata,
@@ -23,15 +23,16 @@ import {
23
23
  } from '../tool/local-shell';
24
24
  import { shellInputSchema, shellOutputSchema } from '../tool/shell';
25
25
  import {
26
- toolSearchInputSchema,
27
- toolSearchOutputSchema,
28
- } from '../tool/tool-search';
29
- import {
26
+ OpenAIResponsesCompactionItem,
30
27
  OpenAIResponsesCustomToolCallOutput,
31
28
  OpenAIResponsesFunctionCallOutput,
32
29
  OpenAIResponsesInput,
33
30
  OpenAIResponsesReasoning,
34
31
  } from './openai-responses-api';
32
+ import {
33
+ toolSearchInputSchema,
34
+ toolSearchOutputSchema,
35
+ } from '../tool/tool-search';
35
36
 
36
37
  /**
37
38
  * Check if a string is a file ID based on the given prefixes
@@ -543,6 +544,36 @@ export async function convertToOpenAIResponsesInput({
543
544
  }
544
545
  break;
545
546
  }
547
+
548
+ case 'custom': {
549
+ if (part.kind === 'openai-compaction') {
550
+ const providerOpts =
551
+ part.providerOptions?.[providerOptionsName];
552
+ const id = providerOpts?.itemId as string | undefined;
553
+
554
+ if (hasConversation && id != null) {
555
+ break;
556
+ }
557
+
558
+ if (store && id != null) {
559
+ input.push({ type: 'item_reference', id });
560
+ break;
561
+ }
562
+
563
+ const encryptedContent = providerOpts?.encryptedContent as
564
+ | string
565
+ | undefined;
566
+
567
+ if (id != null) {
568
+ input.push({
569
+ type: 'compaction',
570
+ id,
571
+ encrypted_content: encryptedContent!,
572
+ } satisfies OpenAIResponsesCompactionItem);
573
+ }
574
+ }
575
+ break;
576
+ }
546
577
  }
547
578
  }
548
579
 
@@ -34,7 +34,8 @@ export type OpenAIResponsesInputItem =
34
34
  | OpenAIResponsesToolSearchCall
35
35
  | OpenAIResponsesToolSearchOutput
36
36
  | OpenAIResponsesReasoning
37
- | OpenAIResponsesItemReference;
37
+ | OpenAIResponsesItemReference
38
+ | OpenAIResponsesCompactionItem;
38
39
 
39
40
  export type OpenAIResponsesIncludeValue =
40
41
  | 'web_search_call.action.sources'
@@ -235,6 +236,12 @@ export type OpenAIResponsesItemReference = {
235
236
  id: string;
236
237
  };
237
238
 
239
+ export type OpenAIResponsesCompactionItem = {
240
+ type: 'compaction';
241
+ id: string;
242
+ encrypted_content: string;
243
+ };
244
+
238
245
  /**
239
246
  * A filter used to compare a specified attribute key to a given value using a defined comparison operation.
240
247
  */
@@ -611,6 +618,11 @@ export const openaiResponsesChunkSchema = lazySchema(() =>
611
618
  commands: z.array(z.string()),
612
619
  }),
613
620
  }),
621
+ z.object({
622
+ type: z.literal('compaction'),
623
+ id: z.string(),
624
+ encrypted_content: z.string().nullish(),
625
+ }),
614
626
  z.object({
615
627
  type: z.literal('shell_call_output'),
616
628
  id: z.string(),
@@ -850,6 +862,11 @@ export const openaiResponsesChunkSchema = lazySchema(() =>
850
862
  commands: z.array(z.string()),
851
863
  }),
852
864
  }),
865
+ z.object({
866
+ type: z.literal('compaction'),
867
+ id: z.string(),
868
+ encrypted_content: z.string(),
869
+ }),
853
870
  z.object({
854
871
  type: z.literal('shell_call_output'),
855
872
  id: z.string(),
@@ -1289,6 +1306,11 @@ export const openaiResponsesResponseSchema = lazySchema(() =>
1289
1306
  commands: z.array(z.string()),
1290
1307
  }),
1291
1308
  }),
1309
+ z.object({
1310
+ type: z.literal('compaction'),
1311
+ id: z.string(),
1312
+ encrypted_content: z.string(),
1313
+ }),
1292
1314
  z.object({
1293
1315
  type: z.literal('shell_call_output'),
1294
1316
  id: z.string(),
@@ -21,6 +21,7 @@ import {
21
21
  createToolNameMapping,
22
22
  generateId,
23
23
  InferSchema,
24
+ isCustomReasoning,
24
25
  parseProviderOptions,
25
26
  ParseResult,
26
27
  postJsonToApi,
@@ -67,6 +68,7 @@ import {
67
68
  } from './openai-responses-options';
68
69
  import { prepareResponsesTools } from './openai-responses-prepare-tools';
69
70
  import {
71
+ ResponsesCompactionProviderMetadata,
70
72
  ResponsesProviderMetadata,
71
73
  ResponsesReasoningProviderMetadata,
72
74
  ResponsesSourceDocumentProviderMetadata,
@@ -129,6 +131,7 @@ export class OpenAIResponsesLanguageModel implements LanguageModelV4 {
129
131
  frequencyPenalty,
130
132
  seed,
131
133
  prompt,
134
+ reasoning,
132
135
  providerOptions,
133
136
  tools,
134
137
  toolChoice,
@@ -174,6 +177,10 @@ export class OpenAIResponsesLanguageModel implements LanguageModelV4 {
174
177
  });
175
178
  }
176
179
 
180
+ const resolvedReasoningEffort =
181
+ openaiOptions?.reasoningEffort ??
182
+ (isCustomReasoning(reasoning) ? reasoning : undefined);
183
+
177
184
  const isReasoningModel =
178
185
  openaiOptions?.forceReasoning ?? modelCapabilities.isReasoningModel;
179
186
 
@@ -337,13 +344,21 @@ export class OpenAIResponsesLanguageModel implements LanguageModelV4 {
337
344
  top_logprobs: topLogprobs,
338
345
  truncation: openaiOptions?.truncation,
339
346
 
347
+ // context management (server-side compaction):
348
+ ...(openaiOptions?.contextManagement && {
349
+ context_management: openaiOptions.contextManagement.map(cm => ({
350
+ type: cm.type,
351
+ compact_threshold: cm.compactThreshold,
352
+ })),
353
+ }),
354
+
340
355
  // model-specific settings:
341
356
  ...(isReasoningModel &&
342
- (openaiOptions?.reasoningEffort != null ||
357
+ (resolvedReasoningEffort != null ||
343
358
  openaiOptions?.reasoningSummary != null) && {
344
359
  reasoning: {
345
- ...(openaiOptions?.reasoningEffort != null && {
346
- effort: openaiOptions.reasoningEffort,
360
+ ...(resolvedReasoningEffort != null && {
361
+ effort: resolvedReasoningEffort,
347
362
  }),
348
363
  ...(openaiOptions?.reasoningSummary != null && {
349
364
  summary: openaiOptions.reasoningSummary,
@@ -359,7 +374,7 @@ export class OpenAIResponsesLanguageModel implements LanguageModelV4 {
359
374
  // https://platform.openai.com/docs/guides/latest-model#gpt-5-1-parameter-compatibility
360
375
  if (
361
376
  !(
362
- openaiOptions?.reasoningEffort === 'none' &&
377
+ resolvedReasoningEffort === 'none' &&
363
378
  modelCapabilities.supportsNonReasoningParameters
364
379
  )
365
380
  ) {
@@ -977,6 +992,21 @@ export class OpenAIResponsesLanguageModel implements LanguageModelV4 {
977
992
 
978
993
  break;
979
994
  }
995
+
996
+ case 'compaction': {
997
+ content.push({
998
+ type: 'custom',
999
+ kind: 'openai-compaction',
1000
+ providerMetadata: {
1001
+ [providerOptionsName]: {
1002
+ type: 'compaction',
1003
+ itemId: part.id,
1004
+ encryptedContent: part.encrypted_content,
1005
+ } satisfies ResponsesCompactionProviderMetadata,
1006
+ },
1007
+ });
1008
+ break;
1009
+ }
980
1010
  }
981
1011
  }
982
1012
 
@@ -1782,6 +1812,18 @@ export class OpenAIResponsesLanguageModel implements LanguageModelV4 {
1782
1812
  }
1783
1813
 
1784
1814
  delete activeReasoning[value.item.id];
1815
+ } else if (value.item.type === 'compaction') {
1816
+ controller.enqueue({
1817
+ type: 'custom',
1818
+ kind: 'openai-compaction',
1819
+ providerMetadata: {
1820
+ [providerOptionsName]: {
1821
+ type: 'compaction',
1822
+ itemId: value.item.id,
1823
+ encryptedContent: value.item.encrypted_content,
1824
+ } satisfies ResponsesCompactionProviderMetadata,
1825
+ },
1826
+ });
1785
1827
  }
1786
1828
  } else if (isResponseFunctionCallArgumentsDeltaChunk(value)) {
1787
1829
  const toolCall = ongoingToolCalls[value.output_index];
@@ -300,6 +300,18 @@ export const openaiLanguageModelResponsesOptionsSchema = lazySchema(() =>
300
300
  * and defaults `systemMessageMode` to `developer` unless overridden.
301
301
  */
302
302
  forceReasoning: z.boolean().optional(),
303
+
304
+ /**
305
+ * Enable server-side context management (compaction).
306
+ */
307
+ contextManagement: z
308
+ .array(
309
+ z.object({
310
+ type: z.literal('compaction'),
311
+ compactThreshold: z.number(),
312
+ }),
313
+ )
314
+ .nullish(),
303
315
  }),
304
316
  ),
305
317
  );
@@ -30,6 +30,16 @@ export type OpenaiResponsesProviderMetadata = {
30
30
  openai: ResponsesProviderMetadata;
31
31
  };
32
32
 
33
+ export type ResponsesCompactionProviderMetadata = {
34
+ type: 'compaction';
35
+ itemId: string;
36
+ encryptedContent?: string;
37
+ };
38
+
39
+ export type OpenaiResponsesCompactionProviderMetadata = {
40
+ openai: ResponsesCompactionProviderMetadata;
41
+ };
42
+
33
43
  export type ResponsesTextProviderMetadata = {
34
44
  itemId: string;
35
45
  phase?: 'commentary' | 'final_answer' | null;