@ai-sdk/openai 4.0.108 → 4.0.110
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 +54 -0
- package/dist/index.d.ts +79 -15
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +788 -423
- package/dist/index.js.map +1 -1
- package/dist/internal/index.d.ts +389 -9
- package/dist/internal/index.d.ts.map +1 -1
- package/dist/internal/index.js +629 -380
- package/dist/internal/index.js.map +1 -1
- package/docs/03-openai.mdx +224 -18
- package/package.json +3 -3
- package/src/chat/convert-to-openai-chat-messages.ts +29 -4
- package/src/chat/map-openai-finish-reason.ts +11 -4
- package/src/chat/openai-chat-api.ts +36 -0
- package/src/chat/openai-chat-language-model-options.ts +13 -0
- package/src/chat/openai-chat-language-model.ts +111 -22
- package/src/chat/openai-chat-prompt.ts +4 -0
- package/src/image/openai-image-model-options.ts +48 -0
- package/src/image/openai-image-model.ts +49 -4
- package/src/index.ts +1 -0
- package/src/internal/index.ts +4 -0
- package/src/live/openai-realtime-model-live.ts +21 -2
- package/src/openai-batch.ts +8 -0
- package/src/openai-model-catalog.ts +3 -1
- package/src/openai-tools.ts +4 -3
- package/src/realtime/openai-realtime-event-mapper.ts +172 -11
- package/src/realtime/openai-realtime-model.ts +57 -3
- package/src/responses/map-openai-responses-finish-reason.ts +7 -1
- package/src/responses/openai-responses-api.ts +90 -49
- package/src/responses/openai-responses-language-model.ts +56 -1
- package/src/responses/openai-responses-prepare-tools.ts +18 -1
- package/src/tool/code-interpreter.ts +38 -2
- package/src/tool/mcp.ts +27 -8
- package/src/transcription/openai-transcription-model.ts +43 -8
|
@@ -2,13 +2,48 @@ import type {
|
|
|
2
2
|
Experimental_RealtimeModelV4ClientEvent as RealtimeModelV4ClientEvent,
|
|
3
3
|
Experimental_RealtimeModelV4ServerEvent as RealtimeModelV4ServerEvent,
|
|
4
4
|
Experimental_RealtimeModelV4SessionConfig as RealtimeModelV4SessionConfig,
|
|
5
|
+
Experimental_RealtimeModelV4SessionIntent as RealtimeModelV4SessionIntent,
|
|
6
|
+
Experimental_RealtimeModelV4Usage as RealtimeModelV4Usage,
|
|
5
7
|
} from '@ai-sdk/provider';
|
|
6
8
|
|
|
9
|
+
type OpenAIRealtimeSessionOptions = {
|
|
10
|
+
intent?: RealtimeModelV4SessionIntent;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
type OpenAIRealtimeTokenDetails = {
|
|
14
|
+
audio_tokens?: number;
|
|
15
|
+
text_tokens?: number;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
type OpenAIRealtimeInputTokenDetails = OpenAIRealtimeTokenDetails & {
|
|
19
|
+
cached_tokens?: number;
|
|
20
|
+
cached_tokens_details?: OpenAIRealtimeTokenDetails;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
type OpenAIRealtimeResponseUsage = {
|
|
24
|
+
input_tokens?: number;
|
|
25
|
+
output_tokens?: number;
|
|
26
|
+
input_token_details?: OpenAIRealtimeInputTokenDetails;
|
|
27
|
+
output_token_details?: OpenAIRealtimeTokenDetails;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
type OpenAIRealtimeTranscriptionUsage = {
|
|
31
|
+
input_tokens?: number;
|
|
32
|
+
type?: string;
|
|
33
|
+
seconds?: number;
|
|
34
|
+
output_tokens?: number;
|
|
35
|
+
input_token_details?: OpenAIRealtimeTokenDetails;
|
|
36
|
+
};
|
|
37
|
+
|
|
7
38
|
type OpenAIRealtimeWireEvent = {
|
|
8
39
|
type: string;
|
|
9
40
|
session?: { id?: string };
|
|
10
41
|
item?: { id?: string } & Record<string, unknown>;
|
|
11
|
-
response?: {
|
|
42
|
+
response?: {
|
|
43
|
+
id?: string;
|
|
44
|
+
status?: string;
|
|
45
|
+
usage?: OpenAIRealtimeResponseUsage;
|
|
46
|
+
};
|
|
12
47
|
error?: {
|
|
13
48
|
message?: string | null;
|
|
14
49
|
code?: string | null;
|
|
@@ -25,6 +60,7 @@ type OpenAIRealtimeWireEvent = {
|
|
|
25
60
|
arguments: string;
|
|
26
61
|
message?: string;
|
|
27
62
|
code?: string;
|
|
63
|
+
usage?: OpenAIRealtimeTranscriptionUsage;
|
|
28
64
|
};
|
|
29
65
|
|
|
30
66
|
/**
|
|
@@ -80,13 +116,16 @@ export function parseOpenAIRealtimeServerEvent(
|
|
|
80
116
|
raw,
|
|
81
117
|
};
|
|
82
118
|
|
|
83
|
-
case 'conversation.item.input_audio_transcription.completed':
|
|
119
|
+
case 'conversation.item.input_audio_transcription.completed': {
|
|
120
|
+
const usage = mapTranscriptionUsage(event.usage);
|
|
84
121
|
return {
|
|
85
122
|
type: 'input-transcription-completed',
|
|
86
123
|
itemId: event.item_id,
|
|
87
124
|
transcript: event.transcript ?? '',
|
|
125
|
+
...(usage != null ? { usage } : {}),
|
|
88
126
|
raw,
|
|
89
127
|
};
|
|
128
|
+
}
|
|
90
129
|
|
|
91
130
|
// ── Response lifecycle ──────────────────────────────────────────
|
|
92
131
|
case 'response.created':
|
|
@@ -96,13 +135,16 @@ export function parseOpenAIRealtimeServerEvent(
|
|
|
96
135
|
raw,
|
|
97
136
|
};
|
|
98
137
|
|
|
99
|
-
case 'response.done':
|
|
138
|
+
case 'response.done': {
|
|
139
|
+
const usage = mapResponseUsage(event.response?.usage);
|
|
100
140
|
return {
|
|
101
141
|
type: 'response-done',
|
|
102
142
|
responseId: event.response?.id ?? event.response_id,
|
|
103
143
|
status: event.response?.status ?? 'completed',
|
|
144
|
+
...(usage != null ? { usage } : {}),
|
|
104
145
|
raw,
|
|
105
146
|
};
|
|
147
|
+
}
|
|
106
148
|
|
|
107
149
|
// ── Output item lifecycle ───────────────────────────────────────
|
|
108
150
|
case 'response.output_item.added':
|
|
@@ -231,18 +273,86 @@ export function parseOpenAIRealtimeServerEvent(
|
|
|
231
273
|
}
|
|
232
274
|
}
|
|
233
275
|
|
|
276
|
+
/**
|
|
277
|
+
* Maps OpenAI realtime `response.done` usage to normalized usage.
|
|
278
|
+
*
|
|
279
|
+
* Input buckets are reported gross (cache-inclusive) with the cached portion
|
|
280
|
+
* surfaced separately, so consumers can apply their own cached-vs-uncached
|
|
281
|
+
* billing split without re-reading `raw`.
|
|
282
|
+
*/
|
|
283
|
+
function mapResponseUsage(
|
|
284
|
+
usage: OpenAIRealtimeResponseUsage | undefined,
|
|
285
|
+
): RealtimeModelV4Usage | undefined {
|
|
286
|
+
if (usage == null) return undefined;
|
|
287
|
+
|
|
288
|
+
const input = usage.input_token_details;
|
|
289
|
+
const output = usage.output_token_details;
|
|
290
|
+
const cached = input?.cached_tokens_details;
|
|
291
|
+
|
|
292
|
+
return compactUsage({
|
|
293
|
+
inputTokens: usage.input_tokens,
|
|
294
|
+
outputTokens: usage.output_tokens,
|
|
295
|
+
cachedInputTokens: input?.cached_tokens,
|
|
296
|
+
inputAudioTokens: input?.audio_tokens,
|
|
297
|
+
inputTextTokens: input?.text_tokens,
|
|
298
|
+
outputAudioTokens: output?.audio_tokens,
|
|
299
|
+
outputTextTokens: output?.text_tokens,
|
|
300
|
+
cachedInputAudioTokens: cached?.audio_tokens,
|
|
301
|
+
cachedInputTextTokens: cached?.text_tokens,
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Maps OpenAI realtime transcription-completed usage to normalized usage.
|
|
307
|
+
* Duration-billed transcription reports `seconds`; token-billed reports tokens.
|
|
308
|
+
*/
|
|
309
|
+
function mapTranscriptionUsage(
|
|
310
|
+
usage: OpenAIRealtimeTranscriptionUsage | undefined,
|
|
311
|
+
): RealtimeModelV4Usage | undefined {
|
|
312
|
+
if (usage == null) return undefined;
|
|
313
|
+
|
|
314
|
+
if (usage.type === 'duration') {
|
|
315
|
+
return usage.seconds != null ? { seconds: usage.seconds } : undefined;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const input = usage.input_token_details;
|
|
319
|
+
return compactUsage({
|
|
320
|
+
inputTokens: usage.input_tokens,
|
|
321
|
+
outputTokens: usage.output_tokens,
|
|
322
|
+
inputAudioTokens: input?.audio_tokens,
|
|
323
|
+
inputTextTokens: input?.text_tokens,
|
|
324
|
+
outputTextTokens: usage.output_tokens,
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/** Drop `undefined` fields; return `undefined` when nothing is observable. */
|
|
329
|
+
function compactUsage(
|
|
330
|
+
usage: RealtimeModelV4Usage,
|
|
331
|
+
): RealtimeModelV4Usage | undefined {
|
|
332
|
+
const result: RealtimeModelV4Usage = {};
|
|
333
|
+
for (const [key, value] of Object.entries(usage)) {
|
|
334
|
+
if (value != null) {
|
|
335
|
+
result[key as keyof RealtimeModelV4Usage] = value;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
return Object.keys(result).length > 0 ? result : undefined;
|
|
339
|
+
}
|
|
340
|
+
|
|
234
341
|
/**
|
|
235
342
|
* Serializes a normalized client event into OpenAI's Realtime API format.
|
|
236
343
|
*/
|
|
237
344
|
export function serializeOpenAIRealtimeClientEvent(
|
|
238
345
|
event: RealtimeModelV4ClientEvent,
|
|
239
346
|
modelId: string,
|
|
347
|
+
options?: OpenAIRealtimeSessionOptions,
|
|
240
348
|
): unknown {
|
|
349
|
+
assertSupportedOpenAIIntent(options?.intent);
|
|
350
|
+
|
|
241
351
|
switch (event.type) {
|
|
242
352
|
case 'session-update':
|
|
243
353
|
return {
|
|
244
354
|
type: 'session.update',
|
|
245
|
-
session: buildOpenAISessionConfig(event.config, modelId),
|
|
355
|
+
session: buildOpenAISessionConfig(event.config, modelId, options),
|
|
246
356
|
...(event.eventId != null ? { event_id: event.eventId } : {}),
|
|
247
357
|
};
|
|
248
358
|
|
|
@@ -332,23 +442,43 @@ export function serializeOpenAIRealtimeClientEvent(
|
|
|
332
442
|
export function buildOpenAISessionConfig(
|
|
333
443
|
config: RealtimeModelV4SessionConfig,
|
|
334
444
|
modelId: string,
|
|
445
|
+
options?: OpenAIRealtimeSessionOptions,
|
|
335
446
|
): Record<string, unknown> {
|
|
447
|
+
const intent = options?.intent ?? 'conversation';
|
|
448
|
+
assertSupportedOpenAIIntent(intent);
|
|
449
|
+
if (intent === 'transcription') {
|
|
450
|
+
for (const key of [
|
|
451
|
+
'instructions',
|
|
452
|
+
'voice',
|
|
453
|
+
'outputModalities',
|
|
454
|
+
'outputAudioFormat',
|
|
455
|
+
'outputAudioTranscription',
|
|
456
|
+
'tools',
|
|
457
|
+
] as const) {
|
|
458
|
+
if (config[key] != null)
|
|
459
|
+
throw new Error(
|
|
460
|
+
`OpenAI realtime transcription sessions do not support '${key}'.`,
|
|
461
|
+
);
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
336
465
|
const session: Record<string, unknown> = {
|
|
337
|
-
type: 'realtime',
|
|
338
|
-
model: modelId,
|
|
466
|
+
type: intent === 'transcription' ? 'transcription' : 'realtime',
|
|
467
|
+
...(intent === 'conversation' ? { model: modelId } : {}),
|
|
339
468
|
};
|
|
340
469
|
|
|
341
|
-
if (config.instructions != null) {
|
|
470
|
+
if (intent === 'conversation' && config.instructions != null) {
|
|
342
471
|
session.instructions = config.instructions;
|
|
343
472
|
}
|
|
344
473
|
|
|
345
|
-
if (config.outputModalities != null) {
|
|
474
|
+
if (intent === 'conversation' && config.outputModalities != null) {
|
|
346
475
|
session.output_modalities = config.outputModalities;
|
|
347
476
|
}
|
|
348
477
|
|
|
349
478
|
const audio: Record<string, unknown> = {};
|
|
350
479
|
|
|
351
480
|
if (
|
|
481
|
+
intent === 'transcription' ||
|
|
352
482
|
config.inputAudioFormat != null ||
|
|
353
483
|
config.inputAudioTranscription != null ||
|
|
354
484
|
config.turnDetection != null
|
|
@@ -387,7 +517,17 @@ export function buildOpenAISessionConfig(
|
|
|
387
517
|
}
|
|
388
518
|
}
|
|
389
519
|
|
|
390
|
-
if (
|
|
520
|
+
if (intent === 'transcription') {
|
|
521
|
+
input.transcription = {
|
|
522
|
+
model: config.inputAudioTranscription?.model ?? modelId,
|
|
523
|
+
...(config.inputAudioTranscription?.language != null
|
|
524
|
+
? { language: config.inputAudioTranscription.language }
|
|
525
|
+
: {}),
|
|
526
|
+
...(config.inputAudioTranscription?.prompt != null
|
|
527
|
+
? { prompt: config.inputAudioTranscription.prompt }
|
|
528
|
+
: {}),
|
|
529
|
+
};
|
|
530
|
+
} else if (config.inputAudioTranscription != null) {
|
|
391
531
|
input.transcription = {
|
|
392
532
|
model: config.inputAudioTranscription.model ?? 'gpt-realtime-whisper',
|
|
393
533
|
...(config.inputAudioTranscription.language != null
|
|
@@ -402,7 +542,10 @@ export function buildOpenAISessionConfig(
|
|
|
402
542
|
audio.input = input;
|
|
403
543
|
}
|
|
404
544
|
|
|
405
|
-
if (
|
|
545
|
+
if (
|
|
546
|
+
intent === 'conversation' &&
|
|
547
|
+
(config.outputAudioFormat != null || config.voice != null)
|
|
548
|
+
) {
|
|
406
549
|
const output: Record<string, unknown> = {};
|
|
407
550
|
|
|
408
551
|
if (config.outputAudioFormat != null) {
|
|
@@ -425,7 +568,11 @@ export function buildOpenAISessionConfig(
|
|
|
425
568
|
session.audio = audio;
|
|
426
569
|
}
|
|
427
570
|
|
|
428
|
-
if (
|
|
571
|
+
if (
|
|
572
|
+
intent === 'conversation' &&
|
|
573
|
+
config.tools != null &&
|
|
574
|
+
config.tools.length > 0
|
|
575
|
+
) {
|
|
429
576
|
session.tools = config.tools.map(tool => ({
|
|
430
577
|
type: tool.type,
|
|
431
578
|
name: tool.name,
|
|
@@ -441,3 +588,17 @@ export function buildOpenAISessionConfig(
|
|
|
441
588
|
|
|
442
589
|
return session;
|
|
443
590
|
}
|
|
591
|
+
|
|
592
|
+
function assertSupportedOpenAIIntent(
|
|
593
|
+
intent: RealtimeModelV4SessionIntent | undefined,
|
|
594
|
+
): void {
|
|
595
|
+
if (
|
|
596
|
+
intent != null &&
|
|
597
|
+
intent !== 'conversation' &&
|
|
598
|
+
intent !== 'transcription'
|
|
599
|
+
) {
|
|
600
|
+
throw new Error(
|
|
601
|
+
`OpenAI realtime session intent '${intent}' is not supported.`,
|
|
602
|
+
);
|
|
603
|
+
}
|
|
604
|
+
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import {
|
|
2
2
|
type Resolvable,
|
|
3
3
|
resolve,
|
|
4
|
+
resolveWith,
|
|
5
|
+
removeUndefinedEntries,
|
|
4
6
|
type FetchFunction,
|
|
5
7
|
} from '@ai-sdk/provider-utils';
|
|
6
8
|
import type {
|
|
@@ -8,8 +10,10 @@ import type {
|
|
|
8
10
|
Experimental_RealtimeModelV4ClientEvent as RealtimeModelV4ClientEvent,
|
|
9
11
|
Experimental_RealtimeModelV4ClientSecretOptions as RealtimeModelV4ClientSecretOptions,
|
|
10
12
|
Experimental_RealtimeModelV4ClientSecretResult as RealtimeModelV4ClientSecretResult,
|
|
13
|
+
Experimental_RealtimeModelV4ServerConnection as RealtimeModelV4ServerConnection,
|
|
11
14
|
Experimental_RealtimeModelV4ServerEvent as RealtimeModelV4ServerEvent,
|
|
12
15
|
Experimental_RealtimeModelV4SessionConfig as RealtimeModelV4SessionConfig,
|
|
16
|
+
Experimental_RealtimeModelV4SessionIntent as RealtimeModelV4SessionIntent,
|
|
13
17
|
} from '@ai-sdk/provider';
|
|
14
18
|
import {
|
|
15
19
|
buildOpenAISessionConfig,
|
|
@@ -26,6 +30,11 @@ export type OpenAIRealtimeModelConfig = {
|
|
|
26
30
|
|
|
27
31
|
export class OpenAIRealtimeModel implements RealtimeModelV4 {
|
|
28
32
|
readonly specificationVersion = 'v4' as const;
|
|
33
|
+
readonly capabilities = {
|
|
34
|
+
conversation: 'turn-based',
|
|
35
|
+
transports: ['websocket'],
|
|
36
|
+
connections: ['client-secret-websocket', 'server-websocket'],
|
|
37
|
+
} as const;
|
|
29
38
|
readonly provider: string;
|
|
30
39
|
readonly modelId: string;
|
|
31
40
|
|
|
@@ -99,17 +108,62 @@ export class OpenAIRealtimeModel implements RealtimeModelV4 {
|
|
|
99
108
|
};
|
|
100
109
|
}
|
|
101
110
|
|
|
111
|
+
getServerWebSocketConfig(options?: {
|
|
112
|
+
intent?: RealtimeModelV4SessionIntent;
|
|
113
|
+
}):
|
|
114
|
+
| RealtimeModelV4ServerConnection
|
|
115
|
+
| PromiseLike<RealtimeModelV4ServerConnection> {
|
|
116
|
+
return resolveWith(this.config.headers, headers => ({
|
|
117
|
+
url: buildServerUrl(
|
|
118
|
+
this.config.baseURL,
|
|
119
|
+
this.modelId,
|
|
120
|
+
options?.intent ?? 'conversation',
|
|
121
|
+
),
|
|
122
|
+
headers: removeUndefinedEntries(headers),
|
|
123
|
+
}));
|
|
124
|
+
}
|
|
125
|
+
|
|
102
126
|
parseServerEvent(raw: unknown): RealtimeModelV4ServerEvent {
|
|
103
127
|
return parseOpenAIRealtimeServerEvent(raw);
|
|
104
128
|
}
|
|
105
129
|
|
|
106
|
-
serializeClientEvent(
|
|
107
|
-
|
|
130
|
+
serializeClientEvent(
|
|
131
|
+
event: RealtimeModelV4ClientEvent,
|
|
132
|
+
options?: { intent?: RealtimeModelV4SessionIntent },
|
|
133
|
+
): unknown {
|
|
134
|
+
return serializeOpenAIRealtimeClientEvent(event, this.modelId, options);
|
|
108
135
|
}
|
|
109
136
|
|
|
110
137
|
buildSessionConfig(
|
|
111
138
|
config: RealtimeModelV4SessionConfig,
|
|
139
|
+
options?: { intent?: RealtimeModelV4SessionIntent },
|
|
112
140
|
): Record<string, unknown> {
|
|
113
|
-
return buildOpenAISessionConfig(config, this.modelId);
|
|
141
|
+
return buildOpenAISessionConfig(config, this.modelId, options);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Builds the upstream realtime WebSocket URL for a server-initiated connection.
|
|
147
|
+
* Transcription keeps the model in `session.update`; a `?model=` here would
|
|
148
|
+
* open a voice session instead.
|
|
149
|
+
*/
|
|
150
|
+
function buildServerUrl(
|
|
151
|
+
baseURL: string,
|
|
152
|
+
modelId: string,
|
|
153
|
+
intent: RealtimeModelV4SessionIntent,
|
|
154
|
+
): string {
|
|
155
|
+
const url = new URL(baseURL);
|
|
156
|
+
const protocol = url.protocol === 'http:' ? 'ws:' : 'wss:';
|
|
157
|
+
const base = `${protocol}//${url.host}${url.pathname.replace(/\/$/, '')}`;
|
|
158
|
+
|
|
159
|
+
switch (intent) {
|
|
160
|
+
case 'transcription':
|
|
161
|
+
return `${base}/realtime?intent=transcription`;
|
|
162
|
+
case 'conversation':
|
|
163
|
+
return `${base}/realtime?model=${encodeURIComponent(modelId)}`;
|
|
164
|
+
default:
|
|
165
|
+
throw new Error(
|
|
166
|
+
`OpenAI realtime session intent '${intent}' is not supported.`,
|
|
167
|
+
);
|
|
114
168
|
}
|
|
115
169
|
}
|
|
@@ -3,15 +3,21 @@ import type { LanguageModelV4FinishReason } from '@ai-sdk/provider';
|
|
|
3
3
|
export function mapOpenAIResponseFinishReason({
|
|
4
4
|
finishReason,
|
|
5
5
|
hasFunctionCall,
|
|
6
|
+
hasRefusal = false,
|
|
6
7
|
}: {
|
|
7
8
|
finishReason: string | null | undefined;
|
|
8
9
|
// flag that checks if there have been client-side tool calls (not executed by openai)
|
|
9
10
|
hasFunctionCall: boolean;
|
|
11
|
+
// flag that checks if the model refused to answer:
|
|
12
|
+
hasRefusal?: boolean;
|
|
10
13
|
}): LanguageModelV4FinishReason['unified'] {
|
|
11
14
|
switch (finishReason) {
|
|
12
15
|
case undefined:
|
|
13
16
|
case null:
|
|
14
|
-
|
|
17
|
+
// A client-side function call must win over a refusal so that the tools
|
|
18
|
+
// are still executed. The refusal text remains part of the content.
|
|
19
|
+
if (hasFunctionCall) return 'tool-calls';
|
|
20
|
+
return hasRefusal ? 'content-filter' : 'stop';
|
|
15
21
|
case 'max_output_tokens':
|
|
16
22
|
return 'length';
|
|
17
23
|
case 'content_filter':
|
|
@@ -647,7 +647,24 @@ export type OpenAIResponsesTool =
|
|
|
647
647
|
}
|
|
648
648
|
| {
|
|
649
649
|
type: 'code_interpreter';
|
|
650
|
-
container:
|
|
650
|
+
container:
|
|
651
|
+
| string
|
|
652
|
+
| {
|
|
653
|
+
type: 'auto';
|
|
654
|
+
file_ids: string[] | undefined;
|
|
655
|
+
memory_limit?: '1g' | '4g' | '16g' | '64g';
|
|
656
|
+
network_policy?:
|
|
657
|
+
| { type: 'disabled' }
|
|
658
|
+
| {
|
|
659
|
+
type: 'allowlist';
|
|
660
|
+
allowed_domains: string[];
|
|
661
|
+
domain_secrets?: Array<{
|
|
662
|
+
domain: string;
|
|
663
|
+
name: string;
|
|
664
|
+
value: string;
|
|
665
|
+
}>;
|
|
666
|
+
};
|
|
667
|
+
};
|
|
651
668
|
}
|
|
652
669
|
| {
|
|
653
670
|
type: 'file_search';
|
|
@@ -712,6 +729,7 @@ export type OpenAIResponsesTool =
|
|
|
712
729
|
| undefined;
|
|
713
730
|
server_description: string | undefined;
|
|
714
731
|
server_url: string | undefined;
|
|
732
|
+
tunnel_id: string | undefined;
|
|
715
733
|
}
|
|
716
734
|
| {
|
|
717
735
|
type: 'custom';
|
|
@@ -851,6 +869,8 @@ const openaiResponsesModeledChunkTypes = new Set([
|
|
|
851
869
|
'response.output_item.done',
|
|
852
870
|
'response.output_text.annotation.added',
|
|
853
871
|
'response.output_text.delta',
|
|
872
|
+
'response.refusal.delta',
|
|
873
|
+
'response.refusal.done',
|
|
854
874
|
'response.reasoning_summary_part.added',
|
|
855
875
|
'response.reasoning_summary_part.done',
|
|
856
876
|
'response.reasoning_summary_text.delta',
|
|
@@ -930,6 +950,20 @@ export const openaiResponsesChunkSchema = lazySchema(() =>
|
|
|
930
950
|
)
|
|
931
951
|
.nullish(),
|
|
932
952
|
}),
|
|
953
|
+
z.object({
|
|
954
|
+
type: z.literal('response.refusal.delta'),
|
|
955
|
+
item_id: z.string(),
|
|
956
|
+
output_index: z.number().nullish(),
|
|
957
|
+
content_index: z.number().nullish(),
|
|
958
|
+
delta: z.string(),
|
|
959
|
+
}),
|
|
960
|
+
z.object({
|
|
961
|
+
type: z.literal('response.refusal.done'),
|
|
962
|
+
item_id: z.string(),
|
|
963
|
+
output_index: z.number().nullish(),
|
|
964
|
+
content_index: z.number().nullish(),
|
|
965
|
+
refusal: z.string(),
|
|
966
|
+
}),
|
|
933
967
|
z.object({
|
|
934
968
|
type: z.enum(['response.completed', 'response.incomplete']),
|
|
935
969
|
response: z.object({
|
|
@@ -1530,55 +1564,62 @@ export const openaiResponsesResponseSchema = lazySchema(() =>
|
|
|
1530
1564
|
role: z.literal('assistant'),
|
|
1531
1565
|
id: z.string(),
|
|
1532
1566
|
phase: z.enum(['commentary', 'final_answer']).nullish(),
|
|
1567
|
+
// the API returns either text or a refusal as content parts:
|
|
1533
1568
|
content: z.array(
|
|
1534
|
-
z.
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
z.
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
z.
|
|
1553
|
-
z.
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1569
|
+
z.discriminatedUnion('type', [
|
|
1570
|
+
z.object({
|
|
1571
|
+
type: z.literal('output_text'),
|
|
1572
|
+
text: z.string(),
|
|
1573
|
+
logprobs: z
|
|
1574
|
+
.array(
|
|
1575
|
+
z.object({
|
|
1576
|
+
token: z.string(),
|
|
1577
|
+
logprob: z.number(),
|
|
1578
|
+
top_logprobs: z.array(
|
|
1579
|
+
z.object({
|
|
1580
|
+
token: z.string(),
|
|
1581
|
+
logprob: z.number(),
|
|
1582
|
+
}),
|
|
1583
|
+
),
|
|
1584
|
+
}),
|
|
1585
|
+
)
|
|
1586
|
+
.nullish(),
|
|
1587
|
+
annotations: z.array(
|
|
1588
|
+
z.discriminatedUnion('type', [
|
|
1589
|
+
z.object({
|
|
1590
|
+
type: z.literal('url_citation'),
|
|
1591
|
+
start_index: z.number(),
|
|
1592
|
+
end_index: z.number(),
|
|
1593
|
+
url: z.string(),
|
|
1594
|
+
title: z.string(),
|
|
1595
|
+
}),
|
|
1596
|
+
z.object({
|
|
1597
|
+
type: z.literal('file_citation'),
|
|
1598
|
+
file_id: z.string(),
|
|
1599
|
+
filename: z.string(),
|
|
1600
|
+
index: z.number(),
|
|
1601
|
+
}),
|
|
1602
|
+
z.object({
|
|
1603
|
+
type: z.literal('container_file_citation'),
|
|
1604
|
+
container_id: z.string(),
|
|
1605
|
+
file_id: z.string(),
|
|
1606
|
+
filename: z.string(),
|
|
1607
|
+
start_index: z.number(),
|
|
1608
|
+
end_index: z.number(),
|
|
1609
|
+
}),
|
|
1610
|
+
z.object({
|
|
1611
|
+
type: z.literal('file_path'),
|
|
1612
|
+
file_id: z.string(),
|
|
1613
|
+
index: z.number(),
|
|
1614
|
+
}),
|
|
1615
|
+
]),
|
|
1616
|
+
),
|
|
1617
|
+
}),
|
|
1618
|
+
z.object({
|
|
1619
|
+
type: z.literal('refusal'),
|
|
1620
|
+
refusal: z.string(),
|
|
1621
|
+
}),
|
|
1622
|
+
]),
|
|
1582
1623
|
),
|
|
1583
1624
|
}),
|
|
1584
1625
|
z.object({
|
|
@@ -923,6 +923,9 @@ export class OpenAIResponsesLanguageModel implements LanguageModelV4 {
|
|
|
923
923
|
let hasFunctionCall = false;
|
|
924
924
|
const hostedToolSearchCallIds: string[] = [];
|
|
925
925
|
|
|
926
|
+
// flag that checks if the model refused to answer:
|
|
927
|
+
let hasRefusal = false;
|
|
928
|
+
|
|
926
929
|
let structuredOutputText: string | undefined;
|
|
927
930
|
|
|
928
931
|
// map response content to content array
|
|
@@ -1092,11 +1095,32 @@ export class OpenAIResponsesLanguageModel implements LanguageModelV4 {
|
|
|
1092
1095
|
part.phase === 'final_answer'
|
|
1093
1096
|
) {
|
|
1094
1097
|
structuredOutputText = part.content
|
|
1095
|
-
.map(contentPart =>
|
|
1098
|
+
.map(contentPart =>
|
|
1099
|
+
contentPart.type === 'output_text'
|
|
1100
|
+
? contentPart.text
|
|
1101
|
+
: contentPart.refusal,
|
|
1102
|
+
)
|
|
1096
1103
|
.join('');
|
|
1097
1104
|
}
|
|
1098
1105
|
|
|
1099
1106
|
for (const contentPart of part.content) {
|
|
1107
|
+
if (contentPart.type === 'refusal') {
|
|
1108
|
+
hasRefusal = true;
|
|
1109
|
+
|
|
1110
|
+
content.push({
|
|
1111
|
+
type: 'text',
|
|
1112
|
+
text: contentPart.refusal,
|
|
1113
|
+
providerMetadata: {
|
|
1114
|
+
[providerOptionsName]: {
|
|
1115
|
+
itemId: part.id,
|
|
1116
|
+
...(part.phase != null && { phase: part.phase }),
|
|
1117
|
+
} satisfies ResponsesTextProviderMetadata,
|
|
1118
|
+
},
|
|
1119
|
+
});
|
|
1120
|
+
|
|
1121
|
+
continue;
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1100
1124
|
if (
|
|
1101
1125
|
options.providerOptions?.[providerOptionsName]?.logprobs &&
|
|
1102
1126
|
contentPart.logprobs
|
|
@@ -1529,6 +1553,7 @@ export class OpenAIResponsesLanguageModel implements LanguageModelV4 {
|
|
|
1529
1553
|
unified: mapOpenAIResponseFinishReason({
|
|
1530
1554
|
finishReason: response.incomplete_details?.reason,
|
|
1531
1555
|
hasFunctionCall,
|
|
1556
|
+
hasRefusal,
|
|
1532
1557
|
}),
|
|
1533
1558
|
raw: response.incomplete_details?.reason ?? undefined,
|
|
1534
1559
|
},
|
|
@@ -1658,6 +1683,9 @@ export class OpenAIResponsesLanguageModel implements LanguageModelV4 {
|
|
|
1658
1683
|
// flag that checks if there have been client-side tool calls (not executed by openai)
|
|
1659
1684
|
let hasFunctionCall = false;
|
|
1660
1685
|
|
|
1686
|
+
// flag that checks if the model refused to answer:
|
|
1687
|
+
let hasRefusal = false;
|
|
1688
|
+
|
|
1661
1689
|
const activeReasoning: Record<
|
|
1662
1690
|
string,
|
|
1663
1691
|
{
|
|
@@ -2772,6 +2800,20 @@ export class OpenAIResponsesLanguageModel implements LanguageModelV4 {
|
|
|
2772
2800
|
) {
|
|
2773
2801
|
logprobs.push(value.logprobs);
|
|
2774
2802
|
}
|
|
2803
|
+
} else if (isRefusalDeltaChunk(value)) {
|
|
2804
|
+
hasRefusal = true;
|
|
2805
|
+
|
|
2806
|
+
const itemId = resolveOutputItemId({
|
|
2807
|
+
itemId: value.item_id,
|
|
2808
|
+
outputIndex: value.output_index,
|
|
2809
|
+
});
|
|
2810
|
+
controller.enqueue({
|
|
2811
|
+
type: 'text-delta',
|
|
2812
|
+
id: itemId,
|
|
2813
|
+
delta: value.delta,
|
|
2814
|
+
});
|
|
2815
|
+
} else if (isRefusalDoneChunk(value)) {
|
|
2816
|
+
hasRefusal = true;
|
|
2775
2817
|
} else if (value.type === 'response.reasoning_summary_part.added') {
|
|
2776
2818
|
const itemId = resolveOutputItemId({
|
|
2777
2819
|
itemId: value.item_id,
|
|
@@ -2872,6 +2914,7 @@ export class OpenAIResponsesLanguageModel implements LanguageModelV4 {
|
|
|
2872
2914
|
unified: mapOpenAIResponseFinishReason({
|
|
2873
2915
|
finishReason: value.response.incomplete_details?.reason,
|
|
2874
2916
|
hasFunctionCall,
|
|
2917
|
+
hasRefusal,
|
|
2875
2918
|
}),
|
|
2876
2919
|
raw: value.response.incomplete_details?.reason ?? undefined,
|
|
2877
2920
|
};
|
|
@@ -3014,6 +3057,18 @@ function isTextDeltaChunk(
|
|
|
3014
3057
|
return chunk.type === 'response.output_text.delta';
|
|
3015
3058
|
}
|
|
3016
3059
|
|
|
3060
|
+
function isRefusalDeltaChunk(
|
|
3061
|
+
chunk: OpenAIResponsesChunk,
|
|
3062
|
+
): chunk is OpenAIResponsesChunk & { type: 'response.refusal.delta' } {
|
|
3063
|
+
return chunk.type === 'response.refusal.delta';
|
|
3064
|
+
}
|
|
3065
|
+
|
|
3066
|
+
function isRefusalDoneChunk(
|
|
3067
|
+
chunk: OpenAIResponsesChunk,
|
|
3068
|
+
): chunk is OpenAIResponsesChunk & { type: 'response.refusal.done' } {
|
|
3069
|
+
return chunk.type === 'response.refusal.done';
|
|
3070
|
+
}
|
|
3071
|
+
|
|
3017
3072
|
function isOpenAIChatCompletionChunk(value: unknown): boolean {
|
|
3018
3073
|
const chunk = asRecord(value);
|
|
3019
3074
|
|