@ai-sdk/open-responses 2.0.31 → 2.0.35

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.
@@ -239,10 +239,124 @@ file's media type. Provider file references, such as OpenAI file IDs, and
239
239
  file data in `{ type: 'text', text: '...' }` format are not supported by this
240
240
  provider.
241
241
 
242
+ ## Experimental Extensions
243
+
244
+ Open Responses implementations can add namespaced tools, items, and streaming
245
+ events. Use `Experimental_OpenResponsesExtension` to encode and decode an
246
+ implementation's extension wire formats. Unregistered provider tools are
247
+ omitted from requests with an `unsupported` warning.
248
+
249
+ This API is experimental and may change in a future release.
250
+
251
+ The AI SDK provider-tool ID uses dot notation (`acme.document_search`), while
252
+ Open Responses wire types use colon notation (`acme:document_search`). The
253
+ namespace in `id` must match the namespace in every registered wire type.
254
+
255
+ ```ts
256
+ import {
257
+ createOpenResponses,
258
+ type Experimental_OpenResponsesExtension,
259
+ } from '@ai-sdk/open-responses';
260
+ import { generateText, tool } from 'ai';
261
+ import { z } from 'zod';
262
+
263
+ const documentSearchExtension: Experimental_OpenResponsesExtension = {
264
+ id: 'acme.document_search',
265
+ toolType: 'acme:document_search',
266
+ itemTypes: ['acme:document_search_receipt'],
267
+ encodeTool: ({ name, args }) => ({
268
+ name,
269
+ index: args.index as string,
270
+ }),
271
+
272
+ decodeItem: ({ item }) => [
273
+ {
274
+ type: 'tool-call',
275
+ toolCallId: item.call_id as string,
276
+ toolName: item.name as string,
277
+ input: JSON.stringify(item.query),
278
+ providerExecuted: true,
279
+ },
280
+ {
281
+ type: 'tool-result',
282
+ toolCallId: item.call_id as string,
283
+ toolName: item.name as string,
284
+ result: item.result!,
285
+ },
286
+ ],
287
+ };
288
+
289
+ const acme = createOpenResponses({
290
+ name: 'acme',
291
+ url: 'https://api.acme.example/v1/responses',
292
+ experimental_extensions: [documentSearchExtension],
293
+ });
294
+
295
+ const documentSearch = tool({
296
+ type: 'provider',
297
+ id: 'acme.document_search',
298
+ args: { index: 'documentation' },
299
+ isProviderExecuted: true,
300
+ inputSchema: z.object({ text: z.string() }),
301
+ outputSchema: z.object({
302
+ documents: z.array(
303
+ z.object({
304
+ id: z.string(),
305
+ title: z.string(),
306
+ }),
307
+ ),
308
+ }),
309
+ });
310
+
311
+ const result = await generateText({
312
+ model: acme('your-model-id'),
313
+ prompt: 'Find the extension documentation.',
314
+ tools: { documentSearch },
315
+ });
316
+ ```
317
+
318
+ Set `providerExecuted` on each decoded `tool-call` or `tool-input-start` part.
319
+ It can vary by item or event.
320
+
321
+ An extension codec can define:
322
+
323
+ - `encodeTool`: Encodes a provider tool. Returning `undefined` omits the tool
324
+ with an `unsupported` warning. A `toolChoice` that selects the omitted tool is
325
+ also omitted.
326
+ - `encodeToolChoice`: Encodes a specific `toolChoice`. The default is
327
+ `{ type: toolType }`.
328
+ - `decodeItem`: Maps a completed item to AI SDK content parts. `mode` is either
329
+ `'generate'` or `'stream'`.
330
+ - `encodeInputItem`: Encodes tool-call or tool-result history when the original
331
+ wire item is unavailable. Returned items must include `id`, `status`, and a
332
+ registered namespaced `type`.
333
+ - `decodeEvent`: Maps a streaming event to AI SDK stream parts. Its `state` map
334
+ lasts for one response stream.
335
+
336
+ Tool, item, and event capabilities are independent. An item-only extension, for
337
+ example, defines `itemTypes` and `decodeItem` without `toolType` or `encodeTool`.
338
+ Capability fields must be provided in pairs.
339
+
340
+ Each decoded item adds an `open-responses.extension-replay` custom part that
341
+ stores the original JSON in provider metadata. Other decoded parts reference
342
+ that custom part instead of copying the item. Passing `result.response.messages`
343
+ to a later call replays the original item once, including opaque fields. This
344
+ also preserves items that decode only to response-only source parts.
345
+
346
+ The adapter validates only the extension discriminator fields. Validate other
347
+ item and event fields in the codec before returning AI SDK parts. The original
348
+ item is stored in provider metadata and may appear in persisted messages,
349
+ telemetry, logs, or UI payloads.
350
+
351
+ Extension callbacks cannot be serialized across workflow step boundaries.
352
+ Create the provider inside the workflow step. Serializing a model configured
353
+ with extensions throws a `SerializationError`.
354
+
242
355
  ## Limitations
243
356
 
244
357
  - Stop sequences, `topK`, and `seed` are not supported and are ignored with warnings.
245
358
  - The provider supports language models only. It does not provide embedding or
246
359
  image generation models.
247
- - AI SDK function tools are supported. Provider-specific built-in tools and
248
- options require a dedicated provider implementation.
360
+ - AI SDK function tools and registered Open Responses extensions are supported.
361
+ Other provider-specific tools and options require a dedicated provider
362
+ implementation.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ai-sdk/open-responses",
3
- "version": "2.0.31",
3
+ "version": "2.0.35",
4
4
  "type": "module",
5
5
  "license": "Apache-2.0",
6
6
  "sideEffects": false,
@@ -30,7 +30,7 @@
30
30
  },
31
31
  "dependencies": {
32
32
  "@ai-sdk/provider": "4.0.8",
33
- "@ai-sdk/provider-utils": "5.0.30"
33
+ "@ai-sdk/provider-utils": "5.0.33"
34
34
  },
35
35
  "devDependencies": {
36
36
  "@ai-sdk/test-server": "2.0.1",
package/src/index.ts CHANGED
@@ -4,6 +4,16 @@ export type {
4
4
  OpenResponsesProvider,
5
5
  OpenResponsesProviderSettings,
6
6
  } from './open-responses-provider';
7
+ export type {
8
+ OpenResponsesExtension as Experimental_OpenResponsesExtension,
9
+ OpenResponsesExtensionContentPart as Experimental_OpenResponsesExtensionContentPart,
10
+ OpenResponsesExtensionEvent as Experimental_OpenResponsesExtensionEvent,
11
+ OpenResponsesExtensionInputPart as Experimental_OpenResponsesExtensionInputPart,
12
+ OpenResponsesExtensionItem as Experimental_OpenResponsesExtensionItem,
13
+ OpenResponsesExtensionRecord as Experimental_OpenResponsesExtensionRecord,
14
+ OpenResponsesExtensionStreamPart as Experimental_OpenResponsesExtensionStreamPart,
15
+ OpenResponsesNamespacedType as Experimental_OpenResponsesNamespacedType,
16
+ } from './open-responses-extension';
7
17
  export type {
8
18
  OpenResponsesLanguageModelOptions,
9
19
  /** @deprecated Use `OpenResponsesLanguageModelOptions` instead. */
@@ -0,0 +1,387 @@
1
+ import type {
2
+ JSONObject,
3
+ LanguageModelV4Content,
4
+ LanguageModelV4ProviderTool,
5
+ LanguageModelV4StreamPart,
6
+ LanguageModelV4ToolCallPart,
7
+ LanguageModelV4ToolResultPart,
8
+ } from '@ai-sdk/provider';
9
+ import type { MaybePromiseLike } from '@ai-sdk/provider-utils';
10
+
11
+ export type OpenResponsesNamespacedType = `${string}:${string}`;
12
+
13
+ export type OpenResponsesExtensionRecord = JSONObject & {
14
+ type: OpenResponsesNamespacedType;
15
+ };
16
+
17
+ export type OpenResponsesExtensionItem = OpenResponsesExtensionRecord & {
18
+ id: string;
19
+ status: string;
20
+ };
21
+
22
+ export type OpenResponsesExtensionEvent = OpenResponsesExtensionRecord & {
23
+ sequence_number: number;
24
+ };
25
+
26
+ export type OpenResponsesExtensionInputPart =
27
+ | LanguageModelV4ToolCallPart
28
+ | LanguageModelV4ToolResultPart;
29
+
30
+ export type OpenResponsesExtensionContentPart = Extract<
31
+ LanguageModelV4Content,
32
+ LanguageModelV4StreamPart
33
+ >;
34
+
35
+ export type OpenResponsesExtensionStreamPart = Exclude<
36
+ LanguageModelV4StreamPart,
37
+ {
38
+ type: 'error' | 'finish' | 'raw' | 'response-metadata' | 'stream-start';
39
+ }
40
+ >;
41
+
42
+ /**
43
+ * Defines how the provider encodes and decodes an Open Responses extension.
44
+ *
45
+ * Tool, item, and event codecs can be registered independently.
46
+ */
47
+ export interface OpenResponsesExtension {
48
+ /**
49
+ * Extension ID in `<implementor>.<extension>` format. For extensions that
50
+ * encode a provider tool, this is also the AI SDK provider-tool ID.
51
+ */
52
+ id: LanguageModelV4ProviderTool['id'];
53
+
54
+ /**
55
+ * Namespaced Open Responses tool type, in `<implementor>:<tool>` format.
56
+ * Must be provided together with `encodeTool`.
57
+ */
58
+ toolType?: OpenResponsesNamespacedType;
59
+
60
+ /**
61
+ * Namespaced item types decoded by this extension. Must be provided together
62
+ * with `decodeItem`.
63
+ */
64
+ itemTypes?: readonly OpenResponsesNamespacedType[];
65
+
66
+ /**
67
+ * Namespaced streaming event types decoded by this extension. Must be
68
+ * provided together with `decodeEvent`.
69
+ */
70
+ eventTypes?: readonly OpenResponsesNamespacedType[];
71
+
72
+ /**
73
+ * Encodes an AI SDK provider tool. Return `undefined` when its arguments
74
+ * cannot be encoded. The adapter adds `toolType` and omits a specific tool
75
+ * choice that selects the omitted tool.
76
+ */
77
+ encodeTool?(options: {
78
+ name: string;
79
+ args: Record<string, unknown>;
80
+ }): MaybePromiseLike<JSONObject | undefined>;
81
+
82
+ /**
83
+ * Encodes a specific `toolChoice`. The adapter adds `toolType`. The default
84
+ * is an object containing only `toolType`.
85
+ */
86
+ encodeToolChoice?(options: {
87
+ name: string;
88
+ args: Record<string, unknown>;
89
+ }): MaybePromiseLike<JSONObject | undefined>;
90
+
91
+ /**
92
+ * Decodes a completed namespaced item into AI SDK content parts. The adapter
93
+ * adds a custom replay part containing the original item and reference
94
+ * metadata to the returned parts.
95
+ */
96
+ decodeItem?(options: {
97
+ item: OpenResponsesExtensionItem;
98
+ mode: 'generate' | 'stream';
99
+ }): MaybePromiseLike<OpenResponsesExtensionContentPart[] | undefined>;
100
+
101
+ /**
102
+ * Encodes an AI SDK history part when no original wire item is available.
103
+ * Every returned item must use one of `itemTypes`.
104
+ */
105
+ encodeInputItem?(options: {
106
+ part: OpenResponsesExtensionInputPart;
107
+ tool: LanguageModelV4ProviderTool;
108
+ }): MaybePromiseLike<
109
+ OpenResponsesExtensionItem | OpenResponsesExtensionItem[] | undefined
110
+ >;
111
+
112
+ /**
113
+ * Decodes a namespaced streaming event.
114
+ *
115
+ * `state` persists for the lifetime of one response stream.
116
+ */
117
+ decodeEvent?(options: {
118
+ event: OpenResponsesExtensionEvent;
119
+ state: Map<string, unknown>;
120
+ }): MaybePromiseLike<OpenResponsesExtensionStreamPart[] | undefined>;
121
+ }
122
+
123
+ export type OpenResponsesExtensionRegistry = {
124
+ byEventType: Map<OpenResponsesNamespacedType, OpenResponsesEventExtension>;
125
+ byExtensionId: Map<LanguageModelV4ProviderTool['id'], OpenResponsesExtension>;
126
+ byItemType: Map<OpenResponsesNamespacedType, OpenResponsesItemExtension>;
127
+ byProviderToolId: Map<
128
+ LanguageModelV4ProviderTool['id'],
129
+ OpenResponsesToolExtension
130
+ >;
131
+ byToolType: Map<OpenResponsesNamespacedType, OpenResponsesToolExtension>;
132
+ };
133
+
134
+ type OpenResponsesToolExtension = OpenResponsesExtension & {
135
+ toolType: OpenResponsesNamespacedType;
136
+ encodeTool: NonNullable<OpenResponsesExtension['encodeTool']>;
137
+ };
138
+
139
+ type OpenResponsesItemExtension = OpenResponsesExtension & {
140
+ itemTypes: readonly OpenResponsesNamespacedType[];
141
+ decodeItem: NonNullable<OpenResponsesExtension['decodeItem']>;
142
+ };
143
+
144
+ type OpenResponsesEventExtension = OpenResponsesExtension & {
145
+ eventTypes: readonly OpenResponsesNamespacedType[];
146
+ decodeEvent: NonNullable<OpenResponsesExtension['decodeEvent']>;
147
+ };
148
+
149
+ export function createOpenResponsesExtensionRegistry(
150
+ extensions?: readonly OpenResponsesExtension[],
151
+ ): OpenResponsesExtensionRegistry {
152
+ const registry: OpenResponsesExtensionRegistry = {
153
+ byEventType: new Map(),
154
+ byExtensionId: new Map(),
155
+ byItemType: new Map(),
156
+ byProviderToolId: new Map(),
157
+ byToolType: new Map(),
158
+ };
159
+
160
+ for (const extension of extensions ?? []) {
161
+ const namespaceSeparatorIndex = extension.id.indexOf('.');
162
+ if (namespaceSeparatorIndex <= 0) {
163
+ throw new Error(
164
+ `Open Responses extension ID ${extension.id} must use <implementor>.<extension> format.`,
165
+ );
166
+ }
167
+ const namespace = extension.id.slice(0, namespaceSeparatorIndex);
168
+
169
+ registerUnique({
170
+ map: registry.byExtensionId,
171
+ key: extension.id,
172
+ extension,
173
+ field: 'id',
174
+ });
175
+
176
+ const hasToolType = extension.toolType != null;
177
+ const hasToolEncoder = extension.encodeTool != null;
178
+ if (hasToolType !== hasToolEncoder) {
179
+ throw new Error(
180
+ `Open Responses extension ${extension.id} must provide toolType and encodeTool together.`,
181
+ );
182
+ }
183
+
184
+ if (extension.encodeToolChoice != null && !hasToolEncoder) {
185
+ throw new Error(
186
+ `Open Responses extension ${extension.id} cannot provide encodeToolChoice without toolType and encodeTool.`,
187
+ );
188
+ }
189
+
190
+ if (hasToolType && hasToolEncoder) {
191
+ const toolExtension = extension as OpenResponsesToolExtension;
192
+ assertNamespacedType({
193
+ extensionId: extension.id,
194
+ namespace,
195
+ type: toolExtension.toolType,
196
+ field: 'toolType',
197
+ });
198
+ registerUnique({
199
+ map: registry.byProviderToolId,
200
+ key: extension.id,
201
+ extension: toolExtension,
202
+ field: 'provider-tool id',
203
+ });
204
+ registerUnique({
205
+ map: registry.byToolType,
206
+ key: toolExtension.toolType,
207
+ extension: toolExtension,
208
+ field: 'toolType',
209
+ });
210
+ }
211
+
212
+ const hasItemTypes = extension.itemTypes != null;
213
+ const hasItemDecoder = extension.decodeItem != null;
214
+ if (hasItemTypes !== hasItemDecoder) {
215
+ throw new Error(
216
+ `Open Responses extension ${extension.id} must provide itemTypes and decodeItem together.`,
217
+ );
218
+ }
219
+
220
+ if (extension.encodeInputItem != null && !hasItemDecoder) {
221
+ throw new Error(
222
+ `Open Responses extension ${extension.id} cannot provide encodeInputItem without itemTypes and decodeItem.`,
223
+ );
224
+ }
225
+
226
+ if (hasItemTypes && hasItemDecoder) {
227
+ const itemExtension = extension as OpenResponsesItemExtension;
228
+ if (itemExtension.itemTypes.length === 0) {
229
+ throw new Error(
230
+ `Open Responses extension ${extension.id} must register at least one item type.`,
231
+ );
232
+ }
233
+ for (const itemType of itemExtension.itemTypes) {
234
+ assertNamespacedType({
235
+ extensionId: extension.id,
236
+ namespace,
237
+ type: itemType,
238
+ field: 'itemTypes',
239
+ });
240
+ registerUnique({
241
+ map: registry.byItemType,
242
+ key: itemType,
243
+ extension: itemExtension,
244
+ field: 'item type',
245
+ });
246
+ }
247
+ }
248
+
249
+ const hasEventTypes = extension.eventTypes != null;
250
+ const hasEventDecoder = extension.decodeEvent != null;
251
+ if (hasEventTypes !== hasEventDecoder) {
252
+ throw new Error(
253
+ `Open Responses extension ${extension.id} must provide eventTypes and decodeEvent together.`,
254
+ );
255
+ }
256
+
257
+ if (hasEventTypes && hasEventDecoder) {
258
+ const eventExtension = extension as OpenResponsesEventExtension;
259
+ if (eventExtension.eventTypes.length === 0) {
260
+ throw new Error(
261
+ `Open Responses extension ${extension.id} must register at least one event type.`,
262
+ );
263
+ }
264
+ for (const eventType of eventExtension.eventTypes) {
265
+ assertNamespacedType({
266
+ extensionId: extension.id,
267
+ namespace,
268
+ type: eventType,
269
+ field: 'eventTypes',
270
+ });
271
+ registerUnique({
272
+ map: registry.byEventType,
273
+ key: eventType,
274
+ extension: eventExtension,
275
+ field: 'event type',
276
+ });
277
+ }
278
+ }
279
+
280
+ if (!hasToolEncoder && !hasItemDecoder && !hasEventDecoder) {
281
+ throw new Error(
282
+ `Open Responses extension ${extension.id} must register a tool, item, or event capability.`,
283
+ );
284
+ }
285
+ }
286
+
287
+ return registry;
288
+ }
289
+
290
+ function assertNamespacedType({
291
+ extensionId,
292
+ namespace,
293
+ type,
294
+ field,
295
+ }: {
296
+ extensionId: string;
297
+ namespace: string;
298
+ type: string;
299
+ field: string;
300
+ }) {
301
+ if (!type.includes(':') || type.slice(0, type.indexOf(':')) !== namespace) {
302
+ throw new Error(
303
+ `Open Responses extension ${extensionId} has invalid ${field} value ${type}. Extension wire types must use the ${namespace}: namespace.`,
304
+ );
305
+ }
306
+ }
307
+
308
+ function registerUnique<K, Extension extends OpenResponsesExtension>({
309
+ map,
310
+ key,
311
+ extension,
312
+ field,
313
+ }: {
314
+ map: Map<K, Extension>;
315
+ key: K;
316
+ extension: Extension;
317
+ field: string;
318
+ }) {
319
+ const existing = map.get(key);
320
+ if (existing != null) {
321
+ throw new Error(
322
+ `Open Responses extension ${extension.id} cannot register ${field} ${String(key)} because it is already registered by ${existing.id}.`,
323
+ );
324
+ }
325
+ map.set(key, extension);
326
+ }
327
+
328
+ export function isOpenResponsesNamespacedType(
329
+ value: unknown,
330
+ ): value is OpenResponsesNamespacedType {
331
+ return typeof value === 'string' && value.includes(':');
332
+ }
333
+
334
+ export function isOpenResponsesExtensionRecord(
335
+ value: unknown,
336
+ ): value is OpenResponsesExtensionRecord {
337
+ return (
338
+ isOpenResponsesJSONObject(value) &&
339
+ isOpenResponsesNamespacedType((value as { type?: unknown }).type)
340
+ );
341
+ }
342
+
343
+ export function isOpenResponsesJSONObject(value: unknown): value is JSONObject {
344
+ return (
345
+ value != null &&
346
+ typeof value === 'object' &&
347
+ !Array.isArray(value) &&
348
+ Object.getPrototypeOf(value) === Object.prototype &&
349
+ Object.values(value).every(isOpenResponsesJSONValue)
350
+ );
351
+ }
352
+
353
+ export function isOpenResponsesExtensionItem(
354
+ value: unknown,
355
+ ): value is OpenResponsesExtensionItem {
356
+ return (
357
+ isOpenResponsesExtensionRecord(value) &&
358
+ typeof value.id === 'string' &&
359
+ typeof value.status === 'string'
360
+ );
361
+ }
362
+
363
+ export function isOpenResponsesExtensionEvent(
364
+ value: unknown,
365
+ ): value is OpenResponsesExtensionEvent {
366
+ return (
367
+ isOpenResponsesExtensionRecord(value) &&
368
+ typeof value.sequence_number === 'number'
369
+ );
370
+ }
371
+
372
+ function isOpenResponsesJSONValue(value: unknown): boolean {
373
+ if (
374
+ value == null ||
375
+ typeof value === 'string' ||
376
+ typeof value === 'number' ||
377
+ typeof value === 'boolean'
378
+ ) {
379
+ return true;
380
+ }
381
+
382
+ if (Array.isArray(value)) {
383
+ return value.every(isOpenResponsesJSONValue);
384
+ }
385
+
386
+ return isOpenResponsesJSONObject(value);
387
+ }
@@ -8,6 +8,10 @@ import {
8
8
  withUserAgentSuffix,
9
9
  type FetchFunction,
10
10
  } from '@ai-sdk/provider-utils';
11
+ import {
12
+ createOpenResponsesExtensionRegistry,
13
+ type OpenResponsesExtension,
14
+ } from './open-responses-extension';
11
15
  import { OpenResponsesLanguageModel } from './responses/open-responses-language-model';
12
16
  import { VERSION } from './version';
13
17
 
@@ -41,12 +45,22 @@ export interface OpenResponsesProviderSettings {
41
45
  * or to provide a custom fetch implementation for e.g. testing.
42
46
  */
43
47
  fetch?: FetchFunction;
48
+
49
+ /**
50
+ * Codecs for Open Responses extension tools, items, and streaming events.
51
+ *
52
+ * @experimental This API may change in a future release.
53
+ */
54
+ experimental_extensions?: readonly OpenResponsesExtension[];
44
55
  }
45
56
 
46
57
  export function createOpenResponses(
47
58
  options: OpenResponsesProviderSettings,
48
59
  ): OpenResponsesProvider {
49
60
  const providerName = options.name;
61
+ const extensionRegistry = createOpenResponsesExtensionRegistry(
62
+ options.experimental_extensions,
63
+ );
50
64
 
51
65
  const getHeaders = () =>
52
66
  withUserAgentSuffix(
@@ -69,6 +83,7 @@ export function createOpenResponses(
69
83
  url: options.url,
70
84
  fetch: options.fetch,
71
85
  generateId: () => generateId(),
86
+ extensionRegistry,
72
87
  });
73
88
  };
74
89