@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.
Files changed (34) hide show
  1. package/CHANGELOG.md +54 -0
  2. package/dist/index.d.ts +79 -15
  3. package/dist/index.d.ts.map +1 -1
  4. package/dist/index.js +788 -423
  5. package/dist/index.js.map +1 -1
  6. package/dist/internal/index.d.ts +389 -9
  7. package/dist/internal/index.d.ts.map +1 -1
  8. package/dist/internal/index.js +629 -380
  9. package/dist/internal/index.js.map +1 -1
  10. package/docs/03-openai.mdx +224 -18
  11. package/package.json +3 -3
  12. package/src/chat/convert-to-openai-chat-messages.ts +29 -4
  13. package/src/chat/map-openai-finish-reason.ts +11 -4
  14. package/src/chat/openai-chat-api.ts +36 -0
  15. package/src/chat/openai-chat-language-model-options.ts +13 -0
  16. package/src/chat/openai-chat-language-model.ts +111 -22
  17. package/src/chat/openai-chat-prompt.ts +4 -0
  18. package/src/image/openai-image-model-options.ts +48 -0
  19. package/src/image/openai-image-model.ts +49 -4
  20. package/src/index.ts +1 -0
  21. package/src/internal/index.ts +4 -0
  22. package/src/live/openai-realtime-model-live.ts +21 -2
  23. package/src/openai-batch.ts +8 -0
  24. package/src/openai-model-catalog.ts +3 -1
  25. package/src/openai-tools.ts +4 -3
  26. package/src/realtime/openai-realtime-event-mapper.ts +172 -11
  27. package/src/realtime/openai-realtime-model.ts +57 -3
  28. package/src/responses/map-openai-responses-finish-reason.ts +7 -1
  29. package/src/responses/openai-responses-api.ts +90 -49
  30. package/src/responses/openai-responses-language-model.ts +56 -1
  31. package/src/responses/openai-responses-prepare-tools.ts +18 -1
  32. package/src/tool/code-interpreter.ts +38 -2
  33. package/src/tool/mcp.ts +27 -8
  34. 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?: { id?: string; status?: string };
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 (config.inputAudioTranscription != null) {
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 (config.outputAudioFormat != null || config.voice != null) {
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 (config.tools != null && config.tools.length > 0) {
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(event: RealtimeModelV4ClientEvent): unknown {
107
- return serializeOpenAIRealtimeClientEvent(event, this.modelId);
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
- return hasFunctionCall ? 'tool-calls' : 'stop';
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: string | { type: 'auto'; file_ids: string[] | undefined };
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.object({
1535
- type: z.literal('output_text'),
1536
- text: z.string(),
1537
- logprobs: z
1538
- .array(
1539
- z.object({
1540
- token: z.string(),
1541
- logprob: z.number(),
1542
- top_logprobs: z.array(
1543
- z.object({
1544
- token: z.string(),
1545
- logprob: z.number(),
1546
- }),
1547
- ),
1548
- }),
1549
- )
1550
- .nullish(),
1551
- annotations: z.array(
1552
- z.discriminatedUnion('type', [
1553
- z.object({
1554
- type: z.literal('url_citation'),
1555
- start_index: z.number(),
1556
- end_index: z.number(),
1557
- url: z.string(),
1558
- title: z.string(),
1559
- }),
1560
- z.object({
1561
- type: z.literal('file_citation'),
1562
- file_id: z.string(),
1563
- filename: z.string(),
1564
- index: z.number(),
1565
- }),
1566
- z.object({
1567
- type: z.literal('container_file_citation'),
1568
- container_id: z.string(),
1569
- file_id: z.string(),
1570
- filename: z.string(),
1571
- start_index: z.number(),
1572
- end_index: z.number(),
1573
- }),
1574
- z.object({
1575
- type: z.literal('file_path'),
1576
- file_id: z.string(),
1577
- index: z.number(),
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 => contentPart.text)
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