@ai-sdk/openai 4.0.0-beta.10 → 4.0.0-beta.12

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.10",
3
+ "version": "4.0.0-beta.12",
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.2",
40
- "@ai-sdk/provider-utils": "5.0.0-beta.4"
39
+ "@ai-sdk/provider": "4.0.0-beta.3",
40
+ "@ai-sdk/provider-utils": "5.0.0-beta.5"
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",
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(),
@@ -67,6 +67,7 @@ import {
67
67
  } from './openai-responses-options';
68
68
  import { prepareResponsesTools } from './openai-responses-prepare-tools';
69
69
  import {
70
+ ResponsesCompactionProviderMetadata,
70
71
  ResponsesProviderMetadata,
71
72
  ResponsesReasoningProviderMetadata,
72
73
  ResponsesSourceDocumentProviderMetadata,
@@ -337,6 +338,14 @@ export class OpenAIResponsesLanguageModel implements LanguageModelV4 {
337
338
  top_logprobs: topLogprobs,
338
339
  truncation: openaiOptions?.truncation,
339
340
 
341
+ // context management (server-side compaction):
342
+ ...(openaiOptions?.contextManagement && {
343
+ context_management: openaiOptions.contextManagement.map(cm => ({
344
+ type: cm.type,
345
+ compact_threshold: cm.compactThreshold,
346
+ })),
347
+ }),
348
+
340
349
  // model-specific settings:
341
350
  ...(isReasoningModel &&
342
351
  (openaiOptions?.reasoningEffort != null ||
@@ -977,6 +986,21 @@ export class OpenAIResponsesLanguageModel implements LanguageModelV4 {
977
986
 
978
987
  break;
979
988
  }
989
+
990
+ case 'compaction': {
991
+ content.push({
992
+ type: 'custom',
993
+ kind: 'openai-compaction',
994
+ providerMetadata: {
995
+ [providerOptionsName]: {
996
+ type: 'compaction',
997
+ itemId: part.id,
998
+ encryptedContent: part.encrypted_content,
999
+ } satisfies ResponsesCompactionProviderMetadata,
1000
+ },
1001
+ });
1002
+ break;
1003
+ }
980
1004
  }
981
1005
  }
982
1006
 
@@ -1782,6 +1806,18 @@ export class OpenAIResponsesLanguageModel implements LanguageModelV4 {
1782
1806
  }
1783
1807
 
1784
1808
  delete activeReasoning[value.item.id];
1809
+ } else if (value.item.type === 'compaction') {
1810
+ controller.enqueue({
1811
+ type: 'custom',
1812
+ kind: 'openai-compaction',
1813
+ providerMetadata: {
1814
+ [providerOptionsName]: {
1815
+ type: 'compaction',
1816
+ itemId: value.item.id,
1817
+ encryptedContent: value.item.encrypted_content,
1818
+ } satisfies ResponsesCompactionProviderMetadata,
1819
+ },
1820
+ });
1785
1821
  }
1786
1822
  } else if (isResponseFunctionCallArgumentsDeltaChunk(value)) {
1787
1823
  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;