@ai-sdk/openai 3.0.137 → 3.0.139

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.
@@ -1636,6 +1636,47 @@ const result = await generateText({
1636
1636
  });
1637
1637
  ```
1638
1638
 
1639
+ #### Refusals
1640
+
1641
+ The Responses API can answer a request with a `refusal` content part instead of
1642
+ `output_text`. The provider surfaces the refusal explanation as text, and
1643
+ reports a `content-filter` finish reason in both `generateText` and
1644
+ `streamText`. When the same response also contains a client-side function call,
1645
+ the finish reason is `tool-calls` instead, so the tools are still executed; the
1646
+ refusal text remains part of the content.
1647
+
1648
+ Refusal content parts mainly appear with structured outputs (`Output.object` or
1649
+ a JSON schema passed to the model). With structured output, a refusal means no
1650
+ object can be generated: `generateText` throws `NoObjectGeneratedError` with the
1651
+ refusal text on `error.text` and `error.finishReason` set to `'content-filter'`.
1652
+
1653
+ ```ts
1654
+ import { NoObjectGeneratedError, Output, generateText } from 'ai';
1655
+ import { openai } from '@ai-sdk/openai';
1656
+ import { z } from 'zod';
1657
+
1658
+ try {
1659
+ await generateText({
1660
+ model: openai.responses('gpt-4o'),
1661
+ output: Output.object({
1662
+ schema: z.object({ steps: z.array(z.string()) }),
1663
+ }),
1664
+ prompt: '...',
1665
+ });
1666
+ } catch (error) {
1667
+ if (NoObjectGeneratedError.isInstance(error)) {
1668
+ // the model's refusal explanation
1669
+ console.log(error.text);
1670
+ // 'content-filter'
1671
+ console.log(error.finishReason);
1672
+ }
1673
+ }
1674
+ ```
1675
+
1676
+ Without structured output, the model may still decline in plain text. Such a
1677
+ response arrives as ordinary text with a `stop` finish reason, so check the text
1678
+ in your application if you need to detect declines.
1679
+
1639
1680
  #### Typed providerMetadata in Text Parts
1640
1681
 
1641
1682
  When using the OpenAI Responses API, the SDK attaches OpenAI-specific metadata to output parts via `providerMetadata`.
@@ -2113,6 +2154,33 @@ You need to change Zod `.nullish()` and `.optional()` to `.nullable()`.
2113
2154
 
2114
2155
  </Note>
2115
2156
 
2157
+ #### Refusals
2158
+
2159
+ Refusals mainly happen when you request structured outputs. When OpenAI declines
2160
+ a request, the provider surfaces the refusal explanation as text and reports a
2161
+ `content-filter` finish reason in both `generateText` and `streamText`:
2162
+
2163
+ ```ts
2164
+ const { text, finishReason } = await generateText({
2165
+ model: openai.chat('gpt-4o'),
2166
+ prompt: '...',
2167
+ });
2168
+
2169
+ if (finishReason === 'content-filter') {
2170
+ // `text` contains the model's refusal explanation
2171
+ }
2172
+ ```
2173
+
2174
+ When you request structured output, a refusal means that no object can be
2175
+ generated: `generateText` throws `NoObjectGeneratedError` with the refusal text
2176
+ on `error.text`.
2177
+
2178
+ When you request logprobs, `providerMetadata.openai.logprobs` contains the
2179
+ logprobs of all emitted tokens: the content logprobs first, followed by the
2180
+ refusal logprobs. This is the same for `generateText` and `streamText`; with
2181
+ `streamText`, the logprobs of all streamed tokens are reported when the stream
2182
+ finishes.
2183
+
2116
2184
  #### Logprobs
2117
2185
 
2118
2186
  OpenAI provides logprobs information for completion/chat models.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ai-sdk/openai",
3
- "version": "3.0.137",
3
+ "version": "3.0.139",
4
4
  "license": "Apache-2.0",
5
5
  "sideEffects": false,
6
6
  "main": "./dist/index.js",
@@ -37,7 +37,7 @@
37
37
  },
38
38
  "dependencies": {
39
39
  "@ai-sdk/provider": "3.0.19",
40
- "@ai-sdk/provider-utils": "4.0.64"
40
+ "@ai-sdk/provider-utils": "4.0.65"
41
41
  },
42
42
  "devDependencies": {
43
43
  "@types/node": "20.17.24",
@@ -1,11 +1,18 @@
1
1
  import type { LanguageModelV3FinishReason } from '@ai-sdk/provider';
2
2
 
3
- export function mapOpenAIFinishReason(
4
- finishReason: string | null | undefined,
5
- ): LanguageModelV3FinishReason['unified'] {
3
+ export function mapOpenAIFinishReason({
4
+ finishReason,
5
+ hasRefusal = false,
6
+ }: {
7
+ finishReason: string | null | undefined;
8
+ // flag that checks if the model refused to answer:
9
+ hasRefusal?: boolean;
10
+ }): LanguageModelV3FinishReason['unified'] {
6
11
  switch (finishReason) {
7
12
  case 'stop':
8
- return 'stop';
13
+ // OpenAI reports a completed response for refusals, so the refusal is
14
+ // the reason the response ended:
15
+ return hasRefusal ? 'content-filter' : 'stop';
9
16
  case 'length':
10
17
  return 'length';
11
18
  case 'content_filter':
@@ -36,6 +36,8 @@ export const openaiChatResponseSchema = lazySchema(() =>
36
36
  message: z.object({
37
37
  role: z.literal('assistant').nullish(),
38
38
  content: z.string().nullish(),
39
+ // set instead of `content` when the model refuses to answer:
40
+ refusal: z.string().nullish(),
39
41
  audio: z
40
42
  .object({
41
43
  transcript: z.string().nullish(),
@@ -84,6 +86,22 @@ export const openaiChatResponseSchema = lazySchema(() =>
84
86
  }),
85
87
  )
86
88
  .nullish(),
89
+ // logprobs of the refusal tokens (set instead of `content` when
90
+ // the model refuses to answer):
91
+ refusal: z
92
+ .array(
93
+ z.object({
94
+ token: z.string(),
95
+ logprob: z.number(),
96
+ top_logprobs: z.array(
97
+ z.object({
98
+ token: z.string(),
99
+ logprob: z.number(),
100
+ }),
101
+ ),
102
+ }),
103
+ )
104
+ .nullish(),
87
105
  })
88
106
  .nullish(),
89
107
  finish_reason: z.string().nullish(),
@@ -128,6 +146,8 @@ export const openaiChatChunkSchema = lazySchema(() =>
128
146
  .object({
129
147
  role: z.enum(['assistant']).nullish(),
130
148
  content: z.string().nullish(),
149
+ // set instead of `content` when the model refuses to answer:
150
+ refusal: z.string().nullish(),
131
151
  tool_calls: z
132
152
  .array(
133
153
  z.object({
@@ -172,6 +192,22 @@ export const openaiChatChunkSchema = lazySchema(() =>
172
192
  }),
173
193
  )
174
194
  .nullish(),
195
+ // logprobs of the refusal tokens (set instead of `content` when
196
+ // the model refuses to answer):
197
+ refusal: z
198
+ .array(
199
+ z.object({
200
+ token: z.string(),
201
+ logprob: z.number(),
202
+ top_logprobs: z.array(
203
+ z.object({
204
+ token: z.string(),
205
+ logprob: z.number(),
206
+ }),
207
+ ),
208
+ }),
209
+ )
210
+ .nullish(),
175
211
  })
176
212
  .nullish(),
177
213
  finish_reason: z.string().nullish(),
@@ -43,6 +43,15 @@ import {
43
43
  } from './openai-chat-options';
44
44
  import { prepareChatTools } from './openai-chat-prepare-tools';
45
45
 
46
+ type OpenAIChatLogprob = NonNullable<
47
+ NonNullable<
48
+ Extract<
49
+ OpenAIChatChunk,
50
+ { choices: unknown }
51
+ >['choices'][number]['logprobs']
52
+ >['content']
53
+ >[number];
54
+
46
55
  type OpenAIChatConfig = {
47
56
  provider: string;
48
57
  headers: () => Record<string, string | undefined>;
@@ -381,7 +390,10 @@ export class OpenAIChatLanguageModel implements LanguageModelV3 {
381
390
 
382
391
  const content: Array<LanguageModelV3Content> = [];
383
392
 
384
- // text content:
393
+ // text content. When the model refuses to answer, the API returns the
394
+ // refusal in `refusal` and sets `content` to null, so surface it as text
395
+ // instead of dropping it. Content and refusal are both returned when the
396
+ // model refuses after answering, so keep both:
385
397
  const text =
386
398
  choice.message.content != null && choice.message.content.length > 0
387
399
  ? choice.message.content
@@ -390,6 +402,12 @@ export class OpenAIChatLanguageModel implements LanguageModelV3 {
390
402
  content.push({ type: 'text', text });
391
403
  }
392
404
 
405
+ const refusal = choice.message.refusal;
406
+ const hasRefusal = refusal != null && refusal.length > 0;
407
+ if (hasRefusal) {
408
+ content.push({ type: 'text', text: refusal });
409
+ }
410
+
393
411
  // tool calls:
394
412
  for (const toolCall of choice.message.tool_calls ?? []) {
395
413
  content.push({
@@ -423,14 +441,25 @@ export class OpenAIChatLanguageModel implements LanguageModelV3 {
423
441
  providerMetadata.openai.rejectedPredictionTokens =
424
442
  completionTokenDetails?.rejected_prediction_tokens;
425
443
  }
426
- if (choice.logprobs?.content != null) {
427
- providerMetadata.openai.logprobs = choice.logprobs.content;
444
+ // logprobs either follow the text content (content) or the refusal, so
445
+ // combine them in the order the text is emitted above. Explicitly empty
446
+ // logprobs are reported as an empty array, as before:
447
+ const contentLogprobs = choice.logprobs?.content;
448
+ const refusalLogprobs = choice.logprobs?.refusal;
449
+ if (contentLogprobs != null || refusalLogprobs != null) {
450
+ providerMetadata.openai.logprobs = [
451
+ ...(contentLogprobs ?? []),
452
+ ...(refusalLogprobs ?? []),
453
+ ];
428
454
  }
429
455
 
430
456
  return {
431
457
  content,
432
458
  finishReason: {
433
- unified: mapOpenAIFinishReason(choice.finish_reason),
459
+ unified: mapOpenAIFinishReason({
460
+ finishReason: choice.finish_reason,
461
+ hasRefusal,
462
+ }),
434
463
  raw: choice.finish_reason ?? undefined,
435
464
  },
436
465
  usage: convertOpenAIChatUsage(response.usage),
@@ -493,6 +522,13 @@ export class OpenAIChatLanguageModel implements LanguageModelV3 {
493
522
  let usage: OpenAIChatUsage | undefined = undefined;
494
523
  let metadataExtracted = false;
495
524
  let isActiveText = false;
525
+ let hasRefusal = false;
526
+ let rawFinishReason: string | undefined = undefined;
527
+ let hasLogprobs = false;
528
+
529
+ // logprobs of all streamed chunks (content token logprobs followed by
530
+ // refusal token logprobs), collected across chunks:
531
+ const streamedLogprobs: OpenAIChatLogprob[] = [];
496
532
 
497
533
  const providerMetadata: SharedV3ProviderMetadata = { openai: {} };
498
534
 
@@ -518,6 +554,7 @@ export class OpenAIChatLanguageModel implements LanguageModelV3 {
518
554
  // handle failed chunk parsing / validation:
519
555
  if (!chunk.success) {
520
556
  finishReason = { unified: 'error', raw: undefined };
557
+ rawFinishReason = undefined;
521
558
  controller.enqueue({ type: 'error', error: chunk.error });
522
559
  return;
523
560
  }
@@ -527,6 +564,7 @@ export class OpenAIChatLanguageModel implements LanguageModelV3 {
527
564
  // handle error chunks:
528
565
  if ('error' in value) {
529
566
  finishReason = { unified: 'error', raw: undefined };
567
+ rawFinishReason = undefined;
530
568
  controller.enqueue({ type: 'error', error: value.error });
531
569
  return;
532
570
  }
@@ -566,15 +604,29 @@ export class OpenAIChatLanguageModel implements LanguageModelV3 {
566
604
 
567
605
  const choice = value.choices[0];
568
606
 
607
+ // store the raw finish reason and map it in `flush`, after all
608
+ // chunks have been read: some servers send the last delta (which
609
+ // can contain a refusal) in the same chunk as the finish reason,
610
+ // and a refusal turns `stop` into `content-filter`:
569
611
  if (choice?.finish_reason != null) {
570
- finishReason = {
571
- unified: mapOpenAIFinishReason(choice.finish_reason),
572
- raw: choice.finish_reason,
573
- };
612
+ rawFinishReason = choice.finish_reason;
574
613
  }
575
614
 
576
- if (choice?.logprobs?.content != null) {
577
- providerMetadata.openai.logprobs = choice.logprobs.content;
615
+ // logprobs either follow the text content (content) or the refusal.
616
+ // They are streamed token by token, so collect the logprobs of all
617
+ // chunks (content logprobs followed by refusal logprobs) and report
618
+ // the complete list at the end of the stream:
619
+ if (choice?.logprobs != null) {
620
+ const contentLogprobs = choice.logprobs.content;
621
+ const refusalLogprobs = choice.logprobs.refusal;
622
+
623
+ if (contentLogprobs != null || refusalLogprobs != null) {
624
+ hasLogprobs = true;
625
+ streamedLogprobs.push(
626
+ ...(contentLogprobs ?? []),
627
+ ...(refusalLogprobs ?? []),
628
+ );
629
+ }
578
630
  }
579
631
 
580
632
  if (choice?.delta == null) {
@@ -583,7 +635,24 @@ export class OpenAIChatLanguageModel implements LanguageModelV3 {
583
635
 
584
636
  const delta = choice.delta;
585
637
 
586
- if (delta.content != null) {
638
+ // When the model refuses to answer, the API streams the refusal in
639
+ // `refusal` instead of `content`. Content and refusal deltas can
640
+ // both occur, so append each to the same text part instead of
641
+ // dropping one:
642
+ const textDeltas: string[] = [];
643
+ if (delta.content != null && delta.content.length > 0) {
644
+ textDeltas.push(delta.content);
645
+ }
646
+ if (delta.refusal != null && delta.refusal.length > 0) {
647
+ hasRefusal = true;
648
+ textDeltas.push(delta.refusal);
649
+ }
650
+ // empty `content: ""` deltas keep their previous behavior:
651
+ if (textDeltas.length === 0 && delta.content === '') {
652
+ textDeltas.push(delta.content);
653
+ }
654
+
655
+ for (const textDelta of textDeltas) {
587
656
  if (!isActiveText) {
588
657
  controller.enqueue({ type: 'text-start', id: '0' });
589
658
  isActiveText = true;
@@ -592,7 +661,7 @@ export class OpenAIChatLanguageModel implements LanguageModelV3 {
592
661
  controller.enqueue({
593
662
  type: 'text-delta',
594
663
  id: '0',
595
- delta: delta.content,
664
+ delta: textDelta,
596
665
  });
597
666
  }
598
667
 
@@ -623,6 +692,25 @@ export class OpenAIChatLanguageModel implements LanguageModelV3 {
623
692
 
624
693
  toolCallTracker.flush();
625
694
 
695
+ // map the stored finish reason now that all deltas (including
696
+ // refusals that arrived in the same chunk as the finish reason)
697
+ // have been read:
698
+ if (rawFinishReason != null) {
699
+ finishReason = {
700
+ unified: mapOpenAIFinishReason({
701
+ finishReason: rawFinishReason,
702
+ hasRefusal,
703
+ }),
704
+ raw: rawFinishReason,
705
+ };
706
+ }
707
+
708
+ // report the logprobs of all streamed chunks (content logprobs
709
+ // followed by refusal logprobs), not only the last chunk:
710
+ if (hasLogprobs) {
711
+ providerMetadata.openai.logprobs = streamedLogprobs;
712
+ }
713
+
626
714
  controller.enqueue({
627
715
  type: 'finish',
628
716
  finishReason,
@@ -650,6 +738,7 @@ function isOpenAIChatOutputChunk(chunk: OpenAIChatChunk): boolean {
650
738
 
651
739
  return (
652
740
  (delta?.content != null && delta.content.length > 0) ||
741
+ (delta?.refusal != null && delta.refusal.length > 0) ||
653
742
  (delta?.tool_calls != null && delta.tool_calls.length > 0) ||
654
743
  (delta?.annotations != null && delta.annotations.length > 0)
655
744
  );
@@ -3,15 +3,21 @@ import type { LanguageModelV3FinishReason } 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
  }): LanguageModelV3FinishReason['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':
@@ -681,6 +681,8 @@ const openaiResponsesModeledChunkTypes = new Set([
681
681
  'response.output_item.done',
682
682
  'response.output_text.annotation.added',
683
683
  'response.output_text.delta',
684
+ 'response.refusal.delta',
685
+ 'response.refusal.done',
684
686
  'response.reasoning_summary_part.added',
685
687
  'response.reasoning_summary_part.done',
686
688
  'response.reasoning_summary_text.delta',
@@ -756,6 +758,20 @@ export const openaiResponsesChunkSchema = lazySchema(() =>
756
758
  )
757
759
  .nullish(),
758
760
  }),
761
+ z.object({
762
+ type: z.literal('response.refusal.delta'),
763
+ item_id: z.string(),
764
+ output_index: z.number().nullish(),
765
+ content_index: z.number().nullish(),
766
+ delta: z.string(),
767
+ }),
768
+ z.object({
769
+ type: z.literal('response.refusal.done'),
770
+ item_id: z.string(),
771
+ output_index: z.number().nullish(),
772
+ content_index: z.number().nullish(),
773
+ refusal: z.string(),
774
+ }),
759
775
  z.object({
760
776
  type: z.enum(['response.completed', 'response.incomplete']),
761
777
  response: z.object({
@@ -1330,55 +1346,62 @@ export const openaiResponsesResponseSchema = lazySchema(() =>
1330
1346
  role: z.literal('assistant'),
1331
1347
  id: z.string(),
1332
1348
  phase: z.enum(['commentary', 'final_answer']).nullish(),
1349
+ // the API returns either text or a refusal as content parts:
1333
1350
  content: z.array(
1334
- z.object({
1335
- type: z.literal('output_text'),
1336
- text: z.string(),
1337
- logprobs: z
1338
- .array(
1339
- z.object({
1340
- token: z.string(),
1341
- logprob: z.number(),
1342
- top_logprobs: z.array(
1343
- z.object({
1344
- token: z.string(),
1345
- logprob: z.number(),
1346
- }),
1347
- ),
1348
- }),
1349
- )
1350
- .nullish(),
1351
- annotations: z.array(
1352
- z.discriminatedUnion('type', [
1353
- z.object({
1354
- type: z.literal('url_citation'),
1355
- start_index: z.number(),
1356
- end_index: z.number(),
1357
- url: z.string(),
1358
- title: z.string(),
1359
- }),
1360
- z.object({
1361
- type: z.literal('file_citation'),
1362
- file_id: z.string(),
1363
- filename: z.string(),
1364
- index: z.number(),
1365
- }),
1366
- z.object({
1367
- type: z.literal('container_file_citation'),
1368
- container_id: z.string(),
1369
- file_id: z.string(),
1370
- filename: z.string(),
1371
- start_index: z.number(),
1372
- end_index: z.number(),
1373
- }),
1374
- z.object({
1375
- type: z.literal('file_path'),
1376
- file_id: z.string(),
1377
- index: z.number(),
1378
- }),
1379
- ]),
1380
- ),
1381
- }),
1351
+ z.discriminatedUnion('type', [
1352
+ z.object({
1353
+ type: z.literal('output_text'),
1354
+ text: z.string(),
1355
+ logprobs: z
1356
+ .array(
1357
+ z.object({
1358
+ token: z.string(),
1359
+ logprob: z.number(),
1360
+ top_logprobs: z.array(
1361
+ z.object({
1362
+ token: z.string(),
1363
+ logprob: z.number(),
1364
+ }),
1365
+ ),
1366
+ }),
1367
+ )
1368
+ .nullish(),
1369
+ annotations: z.array(
1370
+ z.discriminatedUnion('type', [
1371
+ z.object({
1372
+ type: z.literal('url_citation'),
1373
+ start_index: z.number(),
1374
+ end_index: z.number(),
1375
+ url: z.string(),
1376
+ title: z.string(),
1377
+ }),
1378
+ z.object({
1379
+ type: z.literal('file_citation'),
1380
+ file_id: z.string(),
1381
+ filename: z.string(),
1382
+ index: z.number(),
1383
+ }),
1384
+ z.object({
1385
+ type: z.literal('container_file_citation'),
1386
+ container_id: z.string(),
1387
+ file_id: z.string(),
1388
+ filename: z.string(),
1389
+ start_index: z.number(),
1390
+ end_index: z.number(),
1391
+ }),
1392
+ z.object({
1393
+ type: z.literal('file_path'),
1394
+ file_id: z.string(),
1395
+ index: z.number(),
1396
+ }),
1397
+ ]),
1398
+ ),
1399
+ }),
1400
+ z.object({
1401
+ type: z.literal('refusal'),
1402
+ refusal: z.string(),
1403
+ }),
1404
+ ]),
1382
1405
  ),
1383
1406
  }),
1384
1407
  z.object({
@@ -686,6 +686,9 @@ export class OpenAIResponsesLanguageModel implements LanguageModelV3 {
686
686
  let hasFunctionCall = false;
687
687
  const hostedToolSearchCallIds: string[] = [];
688
688
 
689
+ // flag that checks if the model refused to answer:
690
+ let hasRefusal = false;
691
+
689
692
  // map response content to content array (defined when there is no error)
690
693
  for (const part of response.output!) {
691
694
  switch (part.type) {
@@ -842,6 +845,23 @@ export class OpenAIResponsesLanguageModel implements LanguageModelV3 {
842
845
 
843
846
  case 'message': {
844
847
  for (const contentPart of part.content) {
848
+ if (contentPart.type === 'refusal') {
849
+ hasRefusal = true;
850
+
851
+ content.push({
852
+ type: 'text',
853
+ text: contentPart.refusal,
854
+ providerMetadata: {
855
+ [providerOptionsName]: {
856
+ itemId: part.id,
857
+ ...(part.phase != null && { phase: part.phase }),
858
+ } satisfies ResponsesTextProviderMetadata,
859
+ },
860
+ });
861
+
862
+ continue;
863
+ }
864
+
845
865
  if (
846
866
  options.providerOptions?.[providerOptionsName]?.logprobs &&
847
867
  contentPart.logprobs
@@ -1200,6 +1220,7 @@ export class OpenAIResponsesLanguageModel implements LanguageModelV3 {
1200
1220
  unified: mapOpenAIResponseFinishReason({
1201
1221
  finishReason: response.incomplete_details?.reason,
1202
1222
  hasFunctionCall,
1223
+ hasRefusal,
1203
1224
  }),
1204
1225
  raw: response.incomplete_details?.reason ?? undefined,
1205
1226
  },
@@ -1320,6 +1341,9 @@ export class OpenAIResponsesLanguageModel implements LanguageModelV3 {
1320
1341
  // flag that checks if there have been client-side tool calls (not executed by openai)
1321
1342
  let hasFunctionCall = false;
1322
1343
 
1344
+ // flag that checks if the model refused to answer:
1345
+ let hasRefusal = false;
1346
+
1323
1347
  const activeReasoning: Record<
1324
1348
  string,
1325
1349
  {
@@ -2265,6 +2289,16 @@ export class OpenAIResponsesLanguageModel implements LanguageModelV3 {
2265
2289
  ) {
2266
2290
  logprobs.push(value.logprobs);
2267
2291
  }
2292
+ } else if (isRefusalDeltaChunk(value)) {
2293
+ hasRefusal = true;
2294
+
2295
+ controller.enqueue({
2296
+ type: 'text-delta',
2297
+ id: value.item_id,
2298
+ delta: value.delta,
2299
+ });
2300
+ } else if (isRefusalDoneChunk(value)) {
2301
+ hasRefusal = true;
2268
2302
  } else if (value.type === 'response.reasoning_summary_part.added') {
2269
2303
  // the first reasoning start is pushed in isResponseOutputItemAddedReasoningChunk
2270
2304
  if (value.summary_index > 0) {
@@ -2350,6 +2384,7 @@ export class OpenAIResponsesLanguageModel implements LanguageModelV3 {
2350
2384
  unified: mapOpenAIResponseFinishReason({
2351
2385
  finishReason: value.response.incomplete_details?.reason,
2352
2386
  hasFunctionCall,
2387
+ hasRefusal,
2353
2388
  }),
2354
2389
  raw: value.response.incomplete_details?.reason ?? undefined,
2355
2390
  };
@@ -2525,6 +2560,18 @@ function isTextDeltaChunk(
2525
2560
  return chunk.type === 'response.output_text.delta';
2526
2561
  }
2527
2562
 
2563
+ function isRefusalDeltaChunk(
2564
+ chunk: OpenAIResponsesChunk,
2565
+ ): chunk is OpenAIResponsesChunk & { type: 'response.refusal.delta' } {
2566
+ return chunk.type === 'response.refusal.delta';
2567
+ }
2568
+
2569
+ function isRefusalDoneChunk(
2570
+ chunk: OpenAIResponsesChunk,
2571
+ ): chunk is OpenAIResponsesChunk & { type: 'response.refusal.done' } {
2572
+ return chunk.type === 'response.refusal.done';
2573
+ }
2574
+
2528
2575
  function isOpenAIChatCompletionChunk(value: unknown): boolean {
2529
2576
  const chunk = asRecord(value);
2530
2577