@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.
- package/CHANGELOG.md +31 -0
- package/dist/index.d.ts +96 -3
- package/dist/index.js +560 -14
- package/dist/index.js.map +1 -1
- package/docs/06-open-responses.mdx +116 -2
- package/package.json +2 -2
- package/src/index.ts +10 -0
- package/src/open-responses-extension.ts +387 -0
- package/src/open-responses-provider.ts +15 -0
- package/src/responses/convert-to-open-responses-input.ts +193 -0
- package/src/responses/open-responses-api.ts +14 -4
- package/src/responses/open-responses-config.ts +2 -0
- package/src/responses/open-responses-language-model.ts +313 -17
|
@@ -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
|
|
248
|
-
options require a dedicated provider
|
|
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.
|
|
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.
|
|
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
|
|