@openai/agents-extensions 0.0.1
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/LICENSE +21 -0
- package/README.md +13 -0
- package/dist/TwilioRealtimeTransport.d.ts +46 -0
- package/dist/TwilioRealtimeTransport.js +165 -0
- package/dist/TwilioRealtimeTransport.js.map +1 -0
- package/dist/TwilioRealtimeTransport.mjs +165 -0
- package/dist/TwilioRealtimeTransport.mjs.map +1 -0
- package/dist/aiSdk.d.ts +61 -0
- package/dist/aiSdk.js +604 -0
- package/dist/aiSdk.js.map +1 -0
- package/dist/aiSdk.mjs +604 -0
- package/dist/aiSdk.mjs.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +3 -0
- package/dist/index.mjs.map +1 -0
- package/dist/metadata.d.ts +9 -0
- package/dist/metadata.js +11 -0
- package/dist/metadata.js.map +1 -0
- package/dist/metadata.mjs +11 -0
- package/dist/metadata.mjs.map +1 -0
- package/package.json +50 -0
package/LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
MIT License
|
2
|
+
|
3
|
+
Copyright (c) 2025 OpenAI
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
13
|
+
copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
# OpenAI Agents SDK Extensions
|
2
|
+
|
3
|
+
This package contains a collection of extension features for the OpenAI Agents SDK and is intended to be used alongside it.
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
```bash
|
8
|
+
npm install @openai/agents @openai/agents-extensions
|
9
|
+
```
|
10
|
+
|
11
|
+
## License
|
12
|
+
|
13
|
+
MIT
|
@@ -0,0 +1,46 @@
|
|
1
|
+
import { OpenAIRealtimeWebSocket, OpenAIRealtimeWebSocketOptions, RealtimeTransportLayerConnectOptions, TransportLayerAudio, RealtimeSessionConfig } from '@openai/agents/realtime';
|
2
|
+
import type { WebSocket } from 'ws';
|
3
|
+
/**
|
4
|
+
* The options for the Twilio Realtime Transport Layer.
|
5
|
+
*/
|
6
|
+
export type TwilioRealtimeTransportLayerOptions = OpenAIRealtimeWebSocketOptions & {
|
7
|
+
/**
|
8
|
+
* The websocket that is receiving messages from Twilio's Media Streams API. Typically the
|
9
|
+
* connection gets passed into your request handler when running your WebSocket server.
|
10
|
+
*/
|
11
|
+
twilioWebSocket: WebSocket;
|
12
|
+
};
|
13
|
+
/**
|
14
|
+
* An adapter to connect a websocket that is receiving messages from Twilio's Media Streams API to
|
15
|
+
* the OpenAI Realtime API via WebSocket.
|
16
|
+
*
|
17
|
+
* It automatically handles setting the right audio format for the input and output audio, passing
|
18
|
+
* the data along and handling the timing for interruptions using Twilio's `mark` events.
|
19
|
+
*
|
20
|
+
* It does require you to run your own WebSocket server that is receiving connection requests from
|
21
|
+
* Twilio.
|
22
|
+
*
|
23
|
+
* It will emit all Twilio received messages as `twilio_message` type messages on the `*` handler.
|
24
|
+
* If you are using a `RealtimeSession` you can listen to the `transport_event`.
|
25
|
+
*
|
26
|
+
* @example
|
27
|
+
* ```ts
|
28
|
+
* const transport = new TwilioRealtimeTransportLayer({
|
29
|
+
* twilioWebSocket: twilioWebSocket,
|
30
|
+
* });
|
31
|
+
*
|
32
|
+
* transport.on('*', (event) => {
|
33
|
+
* if (event.type === 'twilio_message') {
|
34
|
+
* console.log('Twilio message:', event.data);
|
35
|
+
* }
|
36
|
+
* });
|
37
|
+
* ```
|
38
|
+
*/
|
39
|
+
export declare class TwilioRealtimeTransportLayer extends OpenAIRealtimeWebSocket {
|
40
|
+
#private;
|
41
|
+
constructor(options: TwilioRealtimeTransportLayerOptions);
|
42
|
+
_setInputAndOutputAudioFormat(partialConfig?: Partial<RealtimeSessionConfig>): Partial<RealtimeSessionConfig>;
|
43
|
+
connect(options: RealtimeTransportLayerConnectOptions): Promise<void>;
|
44
|
+
_interrupt(_elapsedTime: number): void;
|
45
|
+
protected _onAudio(audioEvent: TransportLayerAudio): void;
|
46
|
+
}
|
@@ -0,0 +1,165 @@
|
|
1
|
+
import { OpenAIRealtimeWebSocket, utils, } from '@openai/agents/realtime';
|
2
|
+
import { getLogger } from '@openai/agents';
|
3
|
+
/**
|
4
|
+
* An adapter to connect a websocket that is receiving messages from Twilio's Media Streams API to
|
5
|
+
* the OpenAI Realtime API via WebSocket.
|
6
|
+
*
|
7
|
+
* It automatically handles setting the right audio format for the input and output audio, passing
|
8
|
+
* the data along and handling the timing for interruptions using Twilio's `mark` events.
|
9
|
+
*
|
10
|
+
* It does require you to run your own WebSocket server that is receiving connection requests from
|
11
|
+
* Twilio.
|
12
|
+
*
|
13
|
+
* It will emit all Twilio received messages as `twilio_message` type messages on the `*` handler.
|
14
|
+
* If you are using a `RealtimeSession` you can listen to the `transport_event`.
|
15
|
+
*
|
16
|
+
* @example
|
17
|
+
* ```ts
|
18
|
+
* const transport = new TwilioRealtimeTransportLayer({
|
19
|
+
* twilioWebSocket: twilioWebSocket,
|
20
|
+
* });
|
21
|
+
*
|
22
|
+
* transport.on('*', (event) => {
|
23
|
+
* if (event.type === 'twilio_message') {
|
24
|
+
* console.log('Twilio message:', event.data);
|
25
|
+
* }
|
26
|
+
* });
|
27
|
+
* ```
|
28
|
+
*/
|
29
|
+
export class TwilioRealtimeTransportLayer extends OpenAIRealtimeWebSocket {
|
30
|
+
#twilioWebSocket;
|
31
|
+
#streamSid = null;
|
32
|
+
#audioChunkCount = 0;
|
33
|
+
#lastPlayedChunkCount = 0;
|
34
|
+
#previousItemId = null;
|
35
|
+
#logger = getLogger('openai-agents:extensions:twilio');
|
36
|
+
constructor(options) {
|
37
|
+
super(options);
|
38
|
+
this.#twilioWebSocket = options.twilioWebSocket;
|
39
|
+
}
|
40
|
+
_setInputAndOutputAudioFormat(partialConfig) {
|
41
|
+
let newConfig = {};
|
42
|
+
if (!partialConfig) {
|
43
|
+
newConfig.inputAudioFormat = 'g711_ulaw';
|
44
|
+
newConfig.outputAudioFormat = 'g711_ulaw';
|
45
|
+
}
|
46
|
+
else {
|
47
|
+
newConfig = {
|
48
|
+
...partialConfig,
|
49
|
+
inputAudioFormat: partialConfig.inputAudioFormat ?? 'g711_ulaw',
|
50
|
+
outputAudioFormat: partialConfig.outputAudioFormat ?? 'g711_ulaw',
|
51
|
+
};
|
52
|
+
}
|
53
|
+
return newConfig;
|
54
|
+
}
|
55
|
+
async connect(options) {
|
56
|
+
options.initialSessionConfig = this._setInputAndOutputAudioFormat(options.initialSessionConfig);
|
57
|
+
// listen to Twilio messages as quickly as possible
|
58
|
+
this.#twilioWebSocket.on('message', (message) => {
|
59
|
+
try {
|
60
|
+
const data = JSON.parse(message.toString());
|
61
|
+
if (this.#logger.dontLogModelData) {
|
62
|
+
this.#logger.debug('Twilio message:', data.event);
|
63
|
+
}
|
64
|
+
else {
|
65
|
+
this.#logger.debug('Twilio message:', data);
|
66
|
+
}
|
67
|
+
this.emit('*', {
|
68
|
+
type: 'twilio_message',
|
69
|
+
message: data,
|
70
|
+
});
|
71
|
+
switch (data.event) {
|
72
|
+
case 'media':
|
73
|
+
if (this.status === 'connected') {
|
74
|
+
this.sendAudio(utils.base64ToArrayBuffer(data.media.payload));
|
75
|
+
}
|
76
|
+
break;
|
77
|
+
case 'mark':
|
78
|
+
if (!data.mark.name.startsWith('done:') &&
|
79
|
+
data.mark.name.includes(':')) {
|
80
|
+
// keeping track of what the last chunk was that the user heard fully
|
81
|
+
const count = Number(data.mark.name.split(':')[1]);
|
82
|
+
if (Number.isFinite(count)) {
|
83
|
+
this.#lastPlayedChunkCount = count;
|
84
|
+
}
|
85
|
+
else {
|
86
|
+
this.#logger.warn('Invalid mark name received:', data.mark.name);
|
87
|
+
}
|
88
|
+
}
|
89
|
+
else if (data.mark.name.startsWith('done:')) {
|
90
|
+
this.#lastPlayedChunkCount = 0;
|
91
|
+
}
|
92
|
+
break;
|
93
|
+
case 'start':
|
94
|
+
this.#streamSid = data.start.streamSid;
|
95
|
+
break;
|
96
|
+
default:
|
97
|
+
break;
|
98
|
+
}
|
99
|
+
}
|
100
|
+
catch (error) {
|
101
|
+
this.#logger.error('Error parsing message:', error, 'Message:', message);
|
102
|
+
this.emit('error', {
|
103
|
+
type: 'error',
|
104
|
+
error,
|
105
|
+
});
|
106
|
+
}
|
107
|
+
});
|
108
|
+
this.#twilioWebSocket.on('close', () => {
|
109
|
+
if (this.status !== 'disconnected') {
|
110
|
+
this.close();
|
111
|
+
}
|
112
|
+
});
|
113
|
+
this.#twilioWebSocket.on('error', (error) => {
|
114
|
+
this.emit('error', {
|
115
|
+
type: 'error',
|
116
|
+
error,
|
117
|
+
});
|
118
|
+
this.close();
|
119
|
+
});
|
120
|
+
this.on('audio_done', () => {
|
121
|
+
this.#twilioWebSocket.send(JSON.stringify({
|
122
|
+
event: 'mark',
|
123
|
+
mark: {
|
124
|
+
name: `done:${this.currentItemId}`,
|
125
|
+
},
|
126
|
+
streamSid: this.#streamSid,
|
127
|
+
}));
|
128
|
+
});
|
129
|
+
await super.connect(options);
|
130
|
+
}
|
131
|
+
_interrupt(_elapsedTime) {
|
132
|
+
const elapsedTime = this.#lastPlayedChunkCount + 50; /* 50ms buffer */
|
133
|
+
this.#logger.debug(`Interruption detected, clearing Twilio audio and truncating OpenAI audio after ${elapsedTime}ms`);
|
134
|
+
this.#twilioWebSocket.send(JSON.stringify({
|
135
|
+
event: 'clear',
|
136
|
+
streamSid: this.#streamSid,
|
137
|
+
}));
|
138
|
+
super._interrupt(elapsedTime);
|
139
|
+
}
|
140
|
+
_onAudio(audioEvent) {
|
141
|
+
this.#logger.debug(`Sending audio to Twilio ${audioEvent.responseId}: (${audioEvent.data.byteLength} bytes)`);
|
142
|
+
const audioDelta = {
|
143
|
+
event: 'media',
|
144
|
+
streamSid: this.#streamSid,
|
145
|
+
media: {
|
146
|
+
payload: utils.arrayBufferToBase64(audioEvent.data),
|
147
|
+
},
|
148
|
+
};
|
149
|
+
if (this.#previousItemId !== this.currentItemId && this.currentItemId) {
|
150
|
+
this.#previousItemId = this.currentItemId;
|
151
|
+
this.#audioChunkCount = 0;
|
152
|
+
}
|
153
|
+
this.#audioChunkCount += audioEvent.data.byteLength / 8;
|
154
|
+
this.#twilioWebSocket.send(JSON.stringify(audioDelta));
|
155
|
+
this.#twilioWebSocket.send(JSON.stringify({
|
156
|
+
event: 'mark',
|
157
|
+
streamSid: this.#streamSid,
|
158
|
+
mark: {
|
159
|
+
name: `${this.currentItemId}:${this.#audioChunkCount}`,
|
160
|
+
},
|
161
|
+
}));
|
162
|
+
this.emit('audio', audioEvent);
|
163
|
+
}
|
164
|
+
}
|
165
|
+
//# sourceMappingURL=TwilioRealtimeTransport.js.map
|
@@ -0,0 +1 @@
|
|
1
|
+
{"version":3,"file":"TwilioRealtimeTransport.js","sourceRoot":"","sources":["../src/TwilioRealtimeTransport.ts"],"names":[],"mappings":"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"}
|
@@ -0,0 +1,165 @@
|
|
1
|
+
import { OpenAIRealtimeWebSocket, utils, } from '@openai/agents/realtime';
|
2
|
+
import { getLogger } from '@openai/agents';
|
3
|
+
/**
|
4
|
+
* An adapter to connect a websocket that is receiving messages from Twilio's Media Streams API to
|
5
|
+
* the OpenAI Realtime API via WebSocket.
|
6
|
+
*
|
7
|
+
* It automatically handles setting the right audio format for the input and output audio, passing
|
8
|
+
* the data along and handling the timing for interruptions using Twilio's `mark` events.
|
9
|
+
*
|
10
|
+
* It does require you to run your own WebSocket server that is receiving connection requests from
|
11
|
+
* Twilio.
|
12
|
+
*
|
13
|
+
* It will emit all Twilio received messages as `twilio_message` type messages on the `*` handler.
|
14
|
+
* If you are using a `RealtimeSession` you can listen to the `transport_event`.
|
15
|
+
*
|
16
|
+
* @example
|
17
|
+
* ```ts
|
18
|
+
* const transport = new TwilioRealtimeTransportLayer({
|
19
|
+
* twilioWebSocket: twilioWebSocket,
|
20
|
+
* });
|
21
|
+
*
|
22
|
+
* transport.on('*', (event) => {
|
23
|
+
* if (event.type === 'twilio_message') {
|
24
|
+
* console.log('Twilio message:', event.data);
|
25
|
+
* }
|
26
|
+
* });
|
27
|
+
* ```
|
28
|
+
*/
|
29
|
+
export class TwilioRealtimeTransportLayer extends OpenAIRealtimeWebSocket {
|
30
|
+
#twilioWebSocket;
|
31
|
+
#streamSid = null;
|
32
|
+
#audioChunkCount = 0;
|
33
|
+
#lastPlayedChunkCount = 0;
|
34
|
+
#previousItemId = null;
|
35
|
+
#logger = getLogger('openai-agents:extensions:twilio');
|
36
|
+
constructor(options) {
|
37
|
+
super(options);
|
38
|
+
this.#twilioWebSocket = options.twilioWebSocket;
|
39
|
+
}
|
40
|
+
_setInputAndOutputAudioFormat(partialConfig) {
|
41
|
+
let newConfig = {};
|
42
|
+
if (!partialConfig) {
|
43
|
+
newConfig.inputAudioFormat = 'g711_ulaw';
|
44
|
+
newConfig.outputAudioFormat = 'g711_ulaw';
|
45
|
+
}
|
46
|
+
else {
|
47
|
+
newConfig = {
|
48
|
+
...partialConfig,
|
49
|
+
inputAudioFormat: partialConfig.inputAudioFormat ?? 'g711_ulaw',
|
50
|
+
outputAudioFormat: partialConfig.outputAudioFormat ?? 'g711_ulaw',
|
51
|
+
};
|
52
|
+
}
|
53
|
+
return newConfig;
|
54
|
+
}
|
55
|
+
async connect(options) {
|
56
|
+
options.initialSessionConfig = this._setInputAndOutputAudioFormat(options.initialSessionConfig);
|
57
|
+
// listen to Twilio messages as quickly as possible
|
58
|
+
this.#twilioWebSocket.on('message', (message) => {
|
59
|
+
try {
|
60
|
+
const data = JSON.parse(message.toString());
|
61
|
+
if (this.#logger.dontLogModelData) {
|
62
|
+
this.#logger.debug('Twilio message:', data.event);
|
63
|
+
}
|
64
|
+
else {
|
65
|
+
this.#logger.debug('Twilio message:', data);
|
66
|
+
}
|
67
|
+
this.emit('*', {
|
68
|
+
type: 'twilio_message',
|
69
|
+
message: data,
|
70
|
+
});
|
71
|
+
switch (data.event) {
|
72
|
+
case 'media':
|
73
|
+
if (this.status === 'connected') {
|
74
|
+
this.sendAudio(utils.base64ToArrayBuffer(data.media.payload));
|
75
|
+
}
|
76
|
+
break;
|
77
|
+
case 'mark':
|
78
|
+
if (!data.mark.name.startsWith('done:') &&
|
79
|
+
data.mark.name.includes(':')) {
|
80
|
+
// keeping track of what the last chunk was that the user heard fully
|
81
|
+
const count = Number(data.mark.name.split(':')[1]);
|
82
|
+
if (Number.isFinite(count)) {
|
83
|
+
this.#lastPlayedChunkCount = count;
|
84
|
+
}
|
85
|
+
else {
|
86
|
+
this.#logger.warn('Invalid mark name received:', data.mark.name);
|
87
|
+
}
|
88
|
+
}
|
89
|
+
else if (data.mark.name.startsWith('done:')) {
|
90
|
+
this.#lastPlayedChunkCount = 0;
|
91
|
+
}
|
92
|
+
break;
|
93
|
+
case 'start':
|
94
|
+
this.#streamSid = data.start.streamSid;
|
95
|
+
break;
|
96
|
+
default:
|
97
|
+
break;
|
98
|
+
}
|
99
|
+
}
|
100
|
+
catch (error) {
|
101
|
+
this.#logger.error('Error parsing message:', error, 'Message:', message);
|
102
|
+
this.emit('error', {
|
103
|
+
type: 'error',
|
104
|
+
error,
|
105
|
+
});
|
106
|
+
}
|
107
|
+
});
|
108
|
+
this.#twilioWebSocket.on('close', () => {
|
109
|
+
if (this.status !== 'disconnected') {
|
110
|
+
this.close();
|
111
|
+
}
|
112
|
+
});
|
113
|
+
this.#twilioWebSocket.on('error', (error) => {
|
114
|
+
this.emit('error', {
|
115
|
+
type: 'error',
|
116
|
+
error,
|
117
|
+
});
|
118
|
+
this.close();
|
119
|
+
});
|
120
|
+
this.on('audio_done', () => {
|
121
|
+
this.#twilioWebSocket.send(JSON.stringify({
|
122
|
+
event: 'mark',
|
123
|
+
mark: {
|
124
|
+
name: `done:${this.currentItemId}`,
|
125
|
+
},
|
126
|
+
streamSid: this.#streamSid,
|
127
|
+
}));
|
128
|
+
});
|
129
|
+
await super.connect(options);
|
130
|
+
}
|
131
|
+
_interrupt(_elapsedTime) {
|
132
|
+
const elapsedTime = this.#lastPlayedChunkCount + 50; /* 50ms buffer */
|
133
|
+
this.#logger.debug(`Interruption detected, clearing Twilio audio and truncating OpenAI audio after ${elapsedTime}ms`);
|
134
|
+
this.#twilioWebSocket.send(JSON.stringify({
|
135
|
+
event: 'clear',
|
136
|
+
streamSid: this.#streamSid,
|
137
|
+
}));
|
138
|
+
super._interrupt(elapsedTime);
|
139
|
+
}
|
140
|
+
_onAudio(audioEvent) {
|
141
|
+
this.#logger.debug(`Sending audio to Twilio ${audioEvent.responseId}: (${audioEvent.data.byteLength} bytes)`);
|
142
|
+
const audioDelta = {
|
143
|
+
event: 'media',
|
144
|
+
streamSid: this.#streamSid,
|
145
|
+
media: {
|
146
|
+
payload: utils.arrayBufferToBase64(audioEvent.data),
|
147
|
+
},
|
148
|
+
};
|
149
|
+
if (this.#previousItemId !== this.currentItemId && this.currentItemId) {
|
150
|
+
this.#previousItemId = this.currentItemId;
|
151
|
+
this.#audioChunkCount = 0;
|
152
|
+
}
|
153
|
+
this.#audioChunkCount += audioEvent.data.byteLength / 8;
|
154
|
+
this.#twilioWebSocket.send(JSON.stringify(audioDelta));
|
155
|
+
this.#twilioWebSocket.send(JSON.stringify({
|
156
|
+
event: 'mark',
|
157
|
+
streamSid: this.#streamSid,
|
158
|
+
mark: {
|
159
|
+
name: `${this.currentItemId}:${this.#audioChunkCount}`,
|
160
|
+
},
|
161
|
+
}));
|
162
|
+
this.emit('audio', audioEvent);
|
163
|
+
}
|
164
|
+
}
|
165
|
+
//# sourceMappingURL=TwilioRealtimeTransport.mjs.map
|
@@ -0,0 +1 @@
|
|
1
|
+
{"version":3,"file":"TwilioRealtimeTransport.mjs","sourceRoot":"","sources":["../src/TwilioRealtimeTransport.ts"],"names":[],"mappings":"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"}
|
package/dist/aiSdk.d.ts
ADDED
@@ -0,0 +1,61 @@
|
|
1
|
+
import type { LanguageModelV1 } from '@ai-sdk/provider';
|
2
|
+
import { Model, ModelRequest, ResponseStreamEvent, Usage } from '@openai/agents';
|
3
|
+
/**
|
4
|
+
* Wraps a model from the AI SDK that adheres to the LanguageModelV1 spec to be used used as a model
|
5
|
+
* in the OpenAI Agents SDK to use other models.
|
6
|
+
*
|
7
|
+
* While you can use this with the OpenAI models, it is recommended to use the default OpenAI model
|
8
|
+
* provider instead.
|
9
|
+
*
|
10
|
+
* If tracing is enabled, the model will send generation spans to your traces processor.
|
11
|
+
*
|
12
|
+
* ```ts
|
13
|
+
* import { aisdk } from '@openai/agents-extensions';
|
14
|
+
* import { openai } from '@ai-sdk/openai';
|
15
|
+
*
|
16
|
+
* const model = aisdk(openai('gpt-4o'));
|
17
|
+
*
|
18
|
+
* const agent = new Agent({
|
19
|
+
* name: 'My Agent',
|
20
|
+
* model
|
21
|
+
* });
|
22
|
+
* ```
|
23
|
+
*
|
24
|
+
* @param model - The Vercel AI SDK model to wrap.
|
25
|
+
* @returns The wrapped model.
|
26
|
+
*/
|
27
|
+
export declare class AiSdkModel implements Model {
|
28
|
+
#private;
|
29
|
+
constructor(model: LanguageModelV1);
|
30
|
+
getResponse(request: ModelRequest): Promise<{
|
31
|
+
responseId: string;
|
32
|
+
usage: Usage;
|
33
|
+
output: import("@openai/agents").AgentOutputItem[];
|
34
|
+
}>;
|
35
|
+
getStreamedResponse(request: ModelRequest): AsyncIterable<ResponseStreamEvent>;
|
36
|
+
}
|
37
|
+
/**
|
38
|
+
* Wraps a model from the AI SDK that adheres to the LanguageModelV1 spec to be used used as a model
|
39
|
+
* in the OpenAI Agents SDK to use other models.
|
40
|
+
*
|
41
|
+
* While you can use this with the OpenAI models, it is recommended to use the default OpenAI model
|
42
|
+
* provider instead.
|
43
|
+
*
|
44
|
+
* If tracing is enabled, the model will send generation spans to your traces processor.
|
45
|
+
*
|
46
|
+
* ```ts
|
47
|
+
* import { aisdk } from '@openai/agents-extensions';
|
48
|
+
* import { openai } from '@ai-sdk/openai';
|
49
|
+
*
|
50
|
+
* const model = aisdk(openai('gpt-4o'));
|
51
|
+
*
|
52
|
+
* const agent = new Agent({
|
53
|
+
* name: 'My Agent',
|
54
|
+
* model
|
55
|
+
* });
|
56
|
+
* ```
|
57
|
+
*
|
58
|
+
* @param model - The Vercel AI SDK model to wrap.
|
59
|
+
* @returns The wrapped model.
|
60
|
+
*/
|
61
|
+
export declare function aisdk(model: LanguageModelV1): AiSdkModel;
|