@ai-sdk/openai 4.0.109 → 4.0.111

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.
@@ -869,6 +869,8 @@ const openaiResponsesModeledChunkTypes = new Set([
869
869
  'response.output_item.done',
870
870
  'response.output_text.annotation.added',
871
871
  'response.output_text.delta',
872
+ 'response.refusal.delta',
873
+ 'response.refusal.done',
872
874
  'response.reasoning_summary_part.added',
873
875
  'response.reasoning_summary_part.done',
874
876
  'response.reasoning_summary_text.delta',
@@ -948,6 +950,20 @@ export const openaiResponsesChunkSchema = lazySchema(() =>
948
950
  )
949
951
  .nullish(),
950
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
+ }),
951
967
  z.object({
952
968
  type: z.enum(['response.completed', 'response.incomplete']),
953
969
  response: z.object({
@@ -1548,55 +1564,62 @@ export const openaiResponsesResponseSchema = lazySchema(() =>
1548
1564
  role: z.literal('assistant'),
1549
1565
  id: z.string(),
1550
1566
  phase: z.enum(['commentary', 'final_answer']).nullish(),
1567
+ // the API returns either text or a refusal as content parts:
1551
1568
  content: z.array(
1552
- z.object({
1553
- type: z.literal('output_text'),
1554
- text: z.string(),
1555
- logprobs: z
1556
- .array(
1557
- z.object({
1558
- token: z.string(),
1559
- logprob: z.number(),
1560
- top_logprobs: z.array(
1561
- z.object({
1562
- token: z.string(),
1563
- logprob: z.number(),
1564
- }),
1565
- ),
1566
- }),
1567
- )
1568
- .nullish(),
1569
- annotations: z.array(
1570
- z.discriminatedUnion('type', [
1571
- z.object({
1572
- type: z.literal('url_citation'),
1573
- start_index: z.number(),
1574
- end_index: z.number(),
1575
- url: z.string(),
1576
- title: z.string(),
1577
- }),
1578
- z.object({
1579
- type: z.literal('file_citation'),
1580
- file_id: z.string(),
1581
- filename: z.string(),
1582
- index: z.number(),
1583
- }),
1584
- z.object({
1585
- type: z.literal('container_file_citation'),
1586
- container_id: z.string(),
1587
- file_id: z.string(),
1588
- filename: z.string(),
1589
- start_index: z.number(),
1590
- end_index: z.number(),
1591
- }),
1592
- z.object({
1593
- type: z.literal('file_path'),
1594
- file_id: z.string(),
1595
- index: z.number(),
1596
- }),
1597
- ]),
1598
- ),
1599
- }),
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
+ ]),
1600
1623
  ),
1601
1624
  }),
1602
1625
  z.object({
@@ -23,8 +23,8 @@ import {
23
23
  createJsonResponseHandler,
24
24
  createToolNameMapping,
25
25
  generateId,
26
- parseProviderOptions,
27
26
  postJsonToApi,
27
+ resolveProviderOptions,
28
28
  serializeModelOptions,
29
29
  WORKFLOW_DESERIALIZE,
30
30
  WORKFLOW_SERIALIZE,
@@ -331,20 +331,15 @@ export class OpenAIResponsesLanguageModel implements LanguageModelV4 {
331
331
  const providerOptionsName = config.provider.includes('azure')
332
332
  ? 'azure'
333
333
  : 'openai';
334
- let openaiOptions = await parseProviderOptions({
335
- provider: providerOptionsName,
334
+ // The provider key (`azure`) and `openai` are alternatives: the first
335
+ // present one is used as a whole.
336
+ const { options: openaiOptions } = await resolveProviderOptions({
336
337
  providerOptions,
338
+ namespaces: [providerOptionsName, 'openai'],
339
+ resolution: 'first',
337
340
  schema: openaiLanguageModelResponsesOptionsSchema,
338
341
  });
339
342
 
340
- if (openaiOptions == null && providerOptionsName !== 'openai') {
341
- openaiOptions = await parseProviderOptions({
342
- provider: 'openai',
343
- providerOptions,
344
- schema: openaiLanguageModelResponsesOptionsSchema,
345
- });
346
- }
347
-
348
343
  const resolvedReasoningEffort = resolveOpenAIReasoningEffort({
349
344
  providerReasoningEffort: openaiOptions?.reasoningEffort,
350
345
  reasoning,
@@ -923,6 +918,9 @@ export class OpenAIResponsesLanguageModel implements LanguageModelV4 {
923
918
  let hasFunctionCall = false;
924
919
  const hostedToolSearchCallIds: string[] = [];
925
920
 
921
+ // flag that checks if the model refused to answer:
922
+ let hasRefusal = false;
923
+
926
924
  let structuredOutputText: string | undefined;
927
925
 
928
926
  // map response content to content array
@@ -1092,11 +1090,32 @@ export class OpenAIResponsesLanguageModel implements LanguageModelV4 {
1092
1090
  part.phase === 'final_answer'
1093
1091
  ) {
1094
1092
  structuredOutputText = part.content
1095
- .map(contentPart => contentPart.text)
1093
+ .map(contentPart =>
1094
+ contentPart.type === 'output_text'
1095
+ ? contentPart.text
1096
+ : contentPart.refusal,
1097
+ )
1096
1098
  .join('');
1097
1099
  }
1098
1100
 
1099
1101
  for (const contentPart of part.content) {
1102
+ if (contentPart.type === 'refusal') {
1103
+ hasRefusal = true;
1104
+
1105
+ content.push({
1106
+ type: 'text',
1107
+ text: contentPart.refusal,
1108
+ providerMetadata: {
1109
+ [providerOptionsName]: {
1110
+ itemId: part.id,
1111
+ ...(part.phase != null && { phase: part.phase }),
1112
+ } satisfies ResponsesTextProviderMetadata,
1113
+ },
1114
+ });
1115
+
1116
+ continue;
1117
+ }
1118
+
1100
1119
  if (
1101
1120
  options.providerOptions?.[providerOptionsName]?.logprobs &&
1102
1121
  contentPart.logprobs
@@ -1529,6 +1548,7 @@ export class OpenAIResponsesLanguageModel implements LanguageModelV4 {
1529
1548
  unified: mapOpenAIResponseFinishReason({
1530
1549
  finishReason: response.incomplete_details?.reason,
1531
1550
  hasFunctionCall,
1551
+ hasRefusal,
1532
1552
  }),
1533
1553
  raw: response.incomplete_details?.reason ?? undefined,
1534
1554
  },
@@ -1658,6 +1678,9 @@ export class OpenAIResponsesLanguageModel implements LanguageModelV4 {
1658
1678
  // flag that checks if there have been client-side tool calls (not executed by openai)
1659
1679
  let hasFunctionCall = false;
1660
1680
 
1681
+ // flag that checks if the model refused to answer:
1682
+ let hasRefusal = false;
1683
+
1661
1684
  const activeReasoning: Record<
1662
1685
  string,
1663
1686
  {
@@ -2772,6 +2795,20 @@ export class OpenAIResponsesLanguageModel implements LanguageModelV4 {
2772
2795
  ) {
2773
2796
  logprobs.push(value.logprobs);
2774
2797
  }
2798
+ } else if (isRefusalDeltaChunk(value)) {
2799
+ hasRefusal = true;
2800
+
2801
+ const itemId = resolveOutputItemId({
2802
+ itemId: value.item_id,
2803
+ outputIndex: value.output_index,
2804
+ });
2805
+ controller.enqueue({
2806
+ type: 'text-delta',
2807
+ id: itemId,
2808
+ delta: value.delta,
2809
+ });
2810
+ } else if (isRefusalDoneChunk(value)) {
2811
+ hasRefusal = true;
2775
2812
  } else if (value.type === 'response.reasoning_summary_part.added') {
2776
2813
  const itemId = resolveOutputItemId({
2777
2814
  itemId: value.item_id,
@@ -2872,6 +2909,7 @@ export class OpenAIResponsesLanguageModel implements LanguageModelV4 {
2872
2909
  unified: mapOpenAIResponseFinishReason({
2873
2910
  finishReason: value.response.incomplete_details?.reason,
2874
2911
  hasFunctionCall,
2912
+ hasRefusal,
2875
2913
  }),
2876
2914
  raw: value.response.incomplete_details?.reason ?? undefined,
2877
2915
  };
@@ -3014,6 +3052,18 @@ function isTextDeltaChunk(
3014
3052
  return chunk.type === 'response.output_text.delta';
3015
3053
  }
3016
3054
 
3055
+ function isRefusalDeltaChunk(
3056
+ chunk: OpenAIResponsesChunk,
3057
+ ): chunk is OpenAIResponsesChunk & { type: 'response.refusal.delta' } {
3058
+ return chunk.type === 'response.refusal.delta';
3059
+ }
3060
+
3061
+ function isRefusalDoneChunk(
3062
+ chunk: OpenAIResponsesChunk,
3063
+ ): chunk is OpenAIResponsesChunk & { type: 'response.refusal.done' } {
3064
+ return chunk.type === 'response.refusal.done';
3065
+ }
3066
+
3017
3067
  function isOpenAIChatCompletionChunk(value: unknown): boolean {
3018
3068
  const chunk = asRecord(value);
3019
3069
 
@@ -16,13 +16,18 @@ export type OpenAISpeechModelId =
16
16
  | (string & {});
17
17
 
18
18
  // https://platform.openai.com/docs/api-reference/audio/createSpeech
19
+ /**
20
+ * Shape of the OpenAI speech model options. Exported so that routers that
21
+ * also call the OpenAI speech API (e.g. Azure) can accept these options under
22
+ * their own providerOptions key.
23
+ */
24
+ export const openaiSpeechModelOptionsShape = () => ({
25
+ instructions: z.string().nullish(),
26
+ speed: z.number().min(0.25).max(4.0).nullish(),
27
+ });
28
+
19
29
  export const openaiSpeechModelOptionsSchema = lazySchema(() =>
20
- zodSchema(
21
- z.object({
22
- instructions: z.string().nullish(),
23
- speed: z.number().min(0.25).max(4.0).nullish(),
24
- }),
25
- ),
30
+ zodSchema(z.object(openaiSpeechModelOptionsShape())),
26
31
  );
27
32
 
28
33
  export type OpenAISpeechModelOptions = InferSchema<
@@ -18,91 +18,92 @@ export type OpenAITranscriptionModelId =
18
18
  | (string & {});
19
19
 
20
20
  // https://platform.openai.com/docs/api-reference/audio/createTranscription
21
- export const openAITranscriptionModelOptions = lazySchema(() =>
22
- zodSchema(
23
- z.object({
24
- /**
25
- * Additional information to include in the transcription response.
26
- */
21
+ /**
22
+ * Shape of the OpenAI transcription model options. Exported so that routers
23
+ * that also call the OpenAI transcription API (e.g. Azure) can accept these
24
+ * options under their own providerOptions key.
25
+ */
26
+ export const openAITranscriptionModelOptionsShape = () => ({
27
+ /**
28
+ * Additional information to include in the transcription response.
29
+ */
27
30
 
28
- include: z.array(z.string()).optional(),
31
+ include: z.array(z.string()).optional(),
29
32
 
30
- /**
31
- * The language of the input audio in ISO-639-1 format.
32
- */
33
- language: z.string().optional(),
33
+ /**
34
+ * The language of the input audio in ISO-639-1 format.
35
+ */
36
+ language: z.string().optional(),
34
37
 
35
- /**
36
- * Expected languages for REST transcription, e.g. with gpt-transcribe.
37
- * Use this instead of language. The API validates model support.
38
- */
39
- languages: z.array(z.string()).optional(),
38
+ /**
39
+ * Expected languages for REST transcription, e.g. with gpt-transcribe.
40
+ * Use this instead of language. The API validates model support.
41
+ */
42
+ languages: z.array(z.string()).optional(),
40
43
 
41
- /**
42
- * Vocabulary hints for REST transcription, e.g. names or technical terms.
43
- * The API validates model support.
44
- */
45
- keywords: z.array(z.string()).optional(),
44
+ /**
45
+ * Vocabulary hints for REST transcription, e.g. names or technical terms.
46
+ * The API validates model support.
47
+ */
48
+ keywords: z.array(z.string()).optional(),
46
49
 
47
- /**
48
- * An optional text to guide the model's style or continue a previous audio segment.
49
- */
50
- prompt: z.string().optional(),
50
+ /**
51
+ * An optional text to guide the model's style or continue a previous audio segment.
52
+ */
53
+ prompt: z.string().optional(),
51
54
 
52
- /**
53
- * The sampling temperature, between 0 and 1.
54
- * @default 0
55
- */
56
- temperature: z.number().min(0).max(1).default(0).optional(),
55
+ /**
56
+ * The sampling temperature, between 0 and 1.
57
+ * @default 0
58
+ */
59
+ temperature: z.number().min(0).max(1).default(0).optional(),
57
60
 
58
- /**
59
- * The timestamp granularities to populate for this transcription.
60
- * Defaults to ['segment'] when the response format is verbose_json.
61
- */
62
- timestampGranularities: z.array(z.enum(['word', 'segment'])).optional(),
61
+ /**
62
+ * The timestamp granularities to populate for this transcription.
63
+ * Defaults to ['segment'] when the response format is verbose_json.
64
+ */
65
+ timestampGranularities: z.array(z.enum(['word', 'segment'])).optional(),
63
66
 
64
- /**
65
- * The format of the transcription response.
66
- */
67
- responseFormat: z
68
- .enum(['json', 'verbose_json', 'diarized_json'])
69
- .optional(),
67
+ /**
68
+ * The format of the transcription response.
69
+ */
70
+ responseFormat: z.enum(['json', 'verbose_json', 'diarized_json']).optional(),
71
+
72
+ /**
73
+ * Controls how the audio is split into chunks before transcription.
74
+ */
75
+ chunkingStrategy: z
76
+ .union([
77
+ z.literal('auto'),
78
+ z.object({
79
+ type: z.literal('server_vad'),
80
+ threshold: z.number().min(0).max(1).optional(),
81
+ prefixPaddingMs: z.number().int().min(0).optional(),
82
+ silenceDurationMs: z.number().int().min(0).optional(),
83
+ }),
84
+ ])
85
+ .optional(),
70
86
 
87
+ /**
88
+ * Options for streaming transcription models such as `gpt-live-transcribe`.
89
+ */
90
+ streaming: z
91
+ .object({
71
92
  /**
72
- * Controls how the audio is split into chunks before transcription.
93
+ * Latency/accuracy tradeoff for realtime transcription.
73
94
  */
74
- chunkingStrategy: z
75
- .union([
76
- z.literal('auto'),
77
- z.object({
78
- type: z.literal('server_vad'),
79
- threshold: z.number().min(0).max(1).optional(),
80
- prefixPaddingMs: z.number().int().min(0).optional(),
81
- silenceDurationMs: z.number().int().min(0).optional(),
82
- }),
83
- ])
84
- .optional(),
95
+ delay: z.enum(['minimal', 'low', 'medium', 'high', 'xhigh']).optional(),
85
96
 
86
97
  /**
87
- * Options for streaming transcription models such as `gpt-live-transcribe`.
98
+ * Additional fields to include in realtime transcription events.
88
99
  */
89
- streaming: z
90
- .object({
91
- /**
92
- * Latency/accuracy tradeoff for realtime transcription.
93
- */
94
- delay: z
95
- .enum(['minimal', 'low', 'medium', 'high', 'xhigh'])
96
- .optional(),
100
+ include: z.array(z.string()).optional(),
101
+ })
102
+ .optional(),
103
+ });
97
104
 
98
- /**
99
- * Additional fields to include in realtime transcription events.
100
- */
101
- include: z.array(z.string()).optional(),
102
- })
103
- .optional(),
104
- }),
105
- ),
105
+ export const openAITranscriptionModelOptions = lazySchema(() =>
106
+ zodSchema(z.object(openAITranscriptionModelOptionsShape())),
106
107
  );
107
108
 
108
109
  export type OpenAITranscriptionModelOptions = InferSchema<