@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.
@@ -2005,6 +2005,47 @@ message into `result.output`. This prevents commentary or repeated final answers
2005
2005
  from being joined into invalid JSON. The returned `text`, `content`, response
2006
2006
  messages, and token usage still include the complete response as before.
2007
2007
 
2008
+ #### Refusals
2009
+
2010
+ The Responses API can answer a request with a `refusal` content part instead of
2011
+ `output_text`. The provider surfaces the refusal explanation as text, and
2012
+ reports a `content-filter` finish reason in both `generateText` and
2013
+ `streamText`. When the same response also contains a client-side function call,
2014
+ the finish reason is `tool-calls` instead, so the tools are still executed; the
2015
+ refusal text remains part of the content.
2016
+
2017
+ Refusal content parts mainly appear with structured outputs (`Output.object` or
2018
+ a JSON schema passed to the model). With structured output, a refusal means no
2019
+ object can be generated: `generateText` throws `NoObjectGeneratedError` with the
2020
+ refusal text on `error.text` and `error.finishReason` set to `'content-filter'`.
2021
+
2022
+ ```ts
2023
+ import { NoObjectGeneratedError, Output, generateText } from 'ai';
2024
+ import { openai } from '@ai-sdk/openai';
2025
+ import { z } from 'zod';
2026
+
2027
+ try {
2028
+ await generateText({
2029
+ model: openai.responses('gpt-4o'),
2030
+ output: Output.object({
2031
+ schema: z.object({ steps: z.array(z.string()) }),
2032
+ }),
2033
+ prompt: '...',
2034
+ });
2035
+ } catch (error) {
2036
+ if (NoObjectGeneratedError.isInstance(error)) {
2037
+ // the model's refusal explanation
2038
+ console.log(error.text);
2039
+ // 'content-filter'
2040
+ console.log(error.finishReason);
2041
+ }
2042
+ }
2043
+ ```
2044
+
2045
+ Without structured output, the model may still decline in plain text. Such a
2046
+ response arrives as ordinary text with a `stop` finish reason, so check the text
2047
+ in your application if you need to detect declines.
2048
+
2008
2049
  #### Typed providerMetadata in Text Parts
2009
2050
 
2010
2051
  When using the OpenAI Responses API, the SDK attaches OpenAI-specific metadata to output parts via `providerMetadata`.
@@ -2705,6 +2746,33 @@ You need to change Zod `.nullish()` and `.optional()` to `.nullable()`.
2705
2746
 
2706
2747
  </Note>
2707
2748
 
2749
+ #### Refusals
2750
+
2751
+ Refusals mainly happen when you request structured outputs. When OpenAI declines
2752
+ a request, the provider surfaces the refusal explanation as text and reports a
2753
+ `content-filter` finish reason in both `generateText` and `streamText`:
2754
+
2755
+ ```ts
2756
+ const { text, finishReason } = await generateText({
2757
+ model: openai.chat('gpt-4o'),
2758
+ prompt: '...',
2759
+ });
2760
+
2761
+ if (finishReason === 'content-filter') {
2762
+ // `text` contains the model's refusal explanation
2763
+ }
2764
+ ```
2765
+
2766
+ When you request structured output, a refusal means that no object can be
2767
+ generated: `generateText` throws `NoObjectGeneratedError` with the refusal text
2768
+ on `error.text`.
2769
+
2770
+ When you request logprobs, `providerMetadata.openai.logprobs` contains the
2771
+ logprobs of all emitted tokens: the content logprobs first, followed by the
2772
+ refusal logprobs. This is the same for `generateText` and `streamText`; with
2773
+ `streamText`, the logprobs of all streamed tokens are reported when the stream
2774
+ finishes.
2775
+
2708
2776
  #### Logprobs
2709
2777
 
2710
2778
  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": "4.0.109",
3
+ "version": "4.0.111",
4
4
  "type": "module",
5
5
  "license": "Apache-2.0",
6
6
  "sideEffects": false,
@@ -35,8 +35,8 @@
35
35
  }
36
36
  },
37
37
  "dependencies": {
38
- "@ai-sdk/provider": "4.0.36",
39
- "@ai-sdk/provider-utils": "5.0.75"
38
+ "@ai-sdk/provider": "4.0.37",
39
+ "@ai-sdk/provider-utils": "5.0.76"
40
40
  },
41
41
  "devDependencies": {
42
42
  "@ai-sdk/test-server": "2.0.4",
@@ -1,11 +1,18 @@
1
1
  import type { LanguageModelV4FinishReason } from '@ai-sdk/provider';
2
2
 
3
- export function mapOpenAIFinishReason(
4
- finishReason: string | null | undefined,
5
- ): LanguageModelV4FinishReason['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
+ }): LanguageModelV4FinishReason['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(),
@@ -19,8 +19,8 @@ import {
19
19
  createJsonResponseHandler,
20
20
  generateId,
21
21
  isCustomReasoning,
22
- parseProviderOptions,
23
22
  postJsonToApi,
23
+ resolveProviderOptions,
24
24
  serializeModelOptions,
25
25
  WORKFLOW_DESERIALIZE,
26
26
  WORKFLOW_SERIALIZE,
@@ -53,6 +53,15 @@ import {
53
53
  import { prepareChatTools } from './openai-chat-prepare-tools';
54
54
  import { normalizeOpenAIJsonSchema } from '../normalize-openai-json-schema';
55
55
 
56
+ type OpenAIChatLogprob = NonNullable<
57
+ NonNullable<
58
+ Extract<
59
+ OpenAIChatChunk,
60
+ { choices: unknown }
61
+ >['choices'][number]['logprobs']
62
+ >['content']
63
+ >[number];
64
+
56
65
  type OpenAIChatConfig = {
57
66
  provider: string;
58
67
  /** Model ID sent to the API. Defaults to the model's reported ID. */
@@ -114,13 +123,20 @@ export class OpenAIChatLanguageModel implements LanguageModelV4 {
114
123
  }: LanguageModelV4CallOptions) {
115
124
  const warnings: SharedV4Warning[] = [];
116
125
 
117
- // Parse provider options
126
+ // `openai` and the Azure key are merged; the Azure key wins (like Azure
127
+ // completion models).
128
+ const providerOptionsNames = this.config.provider.includes('azure')
129
+ ? (['openai', 'azure'] as const)
130
+ : (['openai'] as const);
118
131
  const openaiOptions =
119
- (await parseProviderOptions({
120
- provider: 'openai',
121
- providerOptions,
122
- schema: openaiLanguageModelChatOptions,
123
- })) ?? {};
132
+ (
133
+ await resolveProviderOptions({
134
+ providerOptions,
135
+ namespaces: providerOptionsNames,
136
+ resolution: 'merge',
137
+ schema: openaiLanguageModelChatOptions,
138
+ })
139
+ ).options ?? {};
124
140
 
125
141
  const {
126
142
  capabilities: { language: modelCapabilities },
@@ -169,7 +185,11 @@ export class OpenAIChatLanguageModel implements LanguageModelV4 {
169
185
 
170
186
  // reasoningSummary is a Responses API option that the chat options schema strips,
171
187
  // so check the raw provider options to avoid silently ignoring it:
172
- if (providerOptions?.openai?.reasoningSummary != null) {
188
+ if (
189
+ providerOptionsNames.some(
190
+ name => providerOptions?.[name]?.reasoningSummary != null,
191
+ )
192
+ ) {
173
193
  warnings.push({
174
194
  type: 'unsupported',
175
195
  feature: 'reasoningSummary',
@@ -447,7 +467,10 @@ export class OpenAIChatLanguageModel implements LanguageModelV4 {
447
467
 
448
468
  const content: Array<LanguageModelV4Content> = [];
449
469
 
450
- // text content:
470
+ // text content. When the model refuses to answer, the API returns the
471
+ // refusal in `refusal` and sets `content` to null, so surface it as text
472
+ // instead of dropping it. Content and refusal are both returned when the
473
+ // model refuses after answering, so keep both:
451
474
  const text =
452
475
  choice.message.content != null && choice.message.content.length > 0
453
476
  ? choice.message.content
@@ -456,6 +479,12 @@ export class OpenAIChatLanguageModel implements LanguageModelV4 {
456
479
  content.push({ type: 'text', text });
457
480
  }
458
481
 
482
+ const refusal = choice.message.refusal;
483
+ const hasRefusal = refusal != null && refusal.length > 0;
484
+ if (hasRefusal) {
485
+ content.push({ type: 'text', text: refusal });
486
+ }
487
+
459
488
  // tool calls:
460
489
  for (const toolCall of choice.message.tool_calls ?? []) {
461
490
  content.push({
@@ -488,14 +517,25 @@ export class OpenAIChatLanguageModel implements LanguageModelV4 {
488
517
  providerMetadata.openai.rejectedPredictionTokens =
489
518
  completionTokenDetails?.rejected_prediction_tokens;
490
519
  }
491
- if (choice.logprobs?.content != null) {
492
- providerMetadata.openai.logprobs = choice.logprobs.content;
520
+ // logprobs either follow the text content (content) or the refusal, so
521
+ // combine them in the order the text is emitted above. Explicitly empty
522
+ // logprobs are reported as an empty array, as before:
523
+ const contentLogprobs = choice.logprobs?.content;
524
+ const refusalLogprobs = choice.logprobs?.refusal;
525
+ if (contentLogprobs != null || refusalLogprobs != null) {
526
+ providerMetadata.openai.logprobs = [
527
+ ...(contentLogprobs ?? []),
528
+ ...(refusalLogprobs ?? []),
529
+ ];
493
530
  }
494
531
 
495
532
  return {
496
533
  content,
497
534
  finishReason: {
498
- unified: mapOpenAIFinishReason(choice.finish_reason),
535
+ unified: mapOpenAIFinishReason({
536
+ finishReason: choice.finish_reason,
537
+ hasRefusal,
538
+ }),
499
539
  raw: choice.finish_reason ?? undefined,
500
540
  },
501
541
  usage: convertOpenAIChatUsage(response.usage),
@@ -562,6 +602,13 @@ export class OpenAIChatLanguageModel implements LanguageModelV4 {
562
602
  let usage: OpenAIChatUsage | undefined = undefined;
563
603
  let metadataExtracted = false;
564
604
  let isActiveText = false;
605
+ let hasRefusal = false;
606
+ let rawFinishReason: string | undefined = undefined;
607
+ let hasLogprobs = false;
608
+
609
+ // logprobs of all streamed chunks (content token logprobs followed by
610
+ // refusal token logprobs), collected across chunks:
611
+ const streamedLogprobs: OpenAIChatLogprob[] = [];
565
612
 
566
613
  const providerMetadata: SharedV4ProviderMetadata = { openai: {} };
567
614
 
@@ -587,6 +634,7 @@ export class OpenAIChatLanguageModel implements LanguageModelV4 {
587
634
  // handle failed chunk parsing / validation:
588
635
  if (!chunk.success) {
589
636
  finishReason = { unified: 'error', raw: undefined };
637
+ rawFinishReason = undefined;
590
638
  controller.enqueue({ type: 'error', error: chunk.error });
591
639
  return;
592
640
  }
@@ -596,6 +644,7 @@ export class OpenAIChatLanguageModel implements LanguageModelV4 {
596
644
  // handle error chunks:
597
645
  if ('error' in value) {
598
646
  finishReason = { unified: 'error', raw: undefined };
647
+ rawFinishReason = undefined;
599
648
  controller.enqueue({
600
649
  type: 'error',
601
650
  error:
@@ -639,15 +688,29 @@ export class OpenAIChatLanguageModel implements LanguageModelV4 {
639
688
 
640
689
  const choice = value.choices[0];
641
690
 
691
+ // store the raw finish reason and map it in `flush`, after all
692
+ // chunks have been read: some servers send the last delta (which
693
+ // can contain a refusal) in the same chunk as the finish reason,
694
+ // and a refusal turns `stop` into `content-filter`:
642
695
  if (choice?.finish_reason != null) {
643
- finishReason = {
644
- unified: mapOpenAIFinishReason(choice.finish_reason),
645
- raw: choice.finish_reason,
646
- };
696
+ rawFinishReason = choice.finish_reason;
647
697
  }
648
698
 
649
- if (choice?.logprobs?.content != null) {
650
- providerMetadata.openai.logprobs = choice.logprobs.content;
699
+ // logprobs either follow the text content (content) or the refusal.
700
+ // They are streamed token by token, so collect the logprobs of all
701
+ // chunks (content logprobs followed by refusal logprobs) and report
702
+ // the complete list at the end of the stream:
703
+ if (choice?.logprobs != null) {
704
+ const contentLogprobs = choice.logprobs.content;
705
+ const refusalLogprobs = choice.logprobs.refusal;
706
+
707
+ if (contentLogprobs != null || refusalLogprobs != null) {
708
+ hasLogprobs = true;
709
+ streamedLogprobs.push(
710
+ ...(contentLogprobs ?? []),
711
+ ...(refusalLogprobs ?? []),
712
+ );
713
+ }
651
714
  }
652
715
 
653
716
  if (choice?.delta == null) {
@@ -656,7 +719,24 @@ export class OpenAIChatLanguageModel implements LanguageModelV4 {
656
719
 
657
720
  const delta = choice.delta;
658
721
 
659
- if (delta.content != null) {
722
+ // When the model refuses to answer, the API streams the refusal in
723
+ // `refusal` instead of `content`. Content and refusal deltas can
724
+ // both occur, so append each to the same text part instead of
725
+ // dropping one:
726
+ const textDeltas: string[] = [];
727
+ if (delta.content != null && delta.content.length > 0) {
728
+ textDeltas.push(delta.content);
729
+ }
730
+ if (delta.refusal != null && delta.refusal.length > 0) {
731
+ hasRefusal = true;
732
+ textDeltas.push(delta.refusal);
733
+ }
734
+ // empty `content: ""` deltas keep their previous behavior:
735
+ if (textDeltas.length === 0 && delta.content === '') {
736
+ textDeltas.push(delta.content);
737
+ }
738
+
739
+ for (const textDelta of textDeltas) {
660
740
  if (!isActiveText) {
661
741
  controller.enqueue({ type: 'text-start', id: '0' });
662
742
  isActiveText = true;
@@ -665,7 +745,7 @@ export class OpenAIChatLanguageModel implements LanguageModelV4 {
665
745
  controller.enqueue({
666
746
  type: 'text-delta',
667
747
  id: '0',
668
- delta: delta.content,
748
+ delta: textDelta,
669
749
  });
670
750
  }
671
751
 
@@ -696,6 +776,25 @@ export class OpenAIChatLanguageModel implements LanguageModelV4 {
696
776
 
697
777
  toolCallTracker.flush();
698
778
 
779
+ // map the stored finish reason now that all deltas (including
780
+ // refusals that arrived in the same chunk as the finish reason)
781
+ // have been read:
782
+ if (rawFinishReason != null) {
783
+ finishReason = {
784
+ unified: mapOpenAIFinishReason({
785
+ finishReason: rawFinishReason,
786
+ hasRefusal,
787
+ }),
788
+ raw: rawFinishReason,
789
+ };
790
+ }
791
+
792
+ // report the logprobs of all streamed chunks (content logprobs
793
+ // followed by refusal logprobs), not only the last chunk:
794
+ if (hasLogprobs) {
795
+ providerMetadata.openai.logprobs = streamedLogprobs;
796
+ }
797
+
699
798
  controller.enqueue({
700
799
  type: 'finish',
701
800
  finishReason,
@@ -723,6 +822,7 @@ function isOpenAIChatOutputChunk(chunk: OpenAIChatChunk): boolean {
723
822
 
724
823
  return (
725
824
  (delta?.content != null && delta.content.length > 0) ||
825
+ (delta?.refusal != null && delta.refusal.length > 0) ||
726
826
  (delta?.tool_calls != null && delta.tool_calls.length > 0) ||
727
827
  (delta?.annotations != null && delta.annotations.length > 0)
728
828
  );
@@ -14,8 +14,8 @@ import {
14
14
  combineHeaders,
15
15
  createEventSourceResponseHandler,
16
16
  createJsonResponseHandler,
17
- parseProviderOptions,
18
17
  postJsonToApi,
18
+ resolveProviderOptions,
19
19
  serializeModelOptions,
20
20
  WORKFLOW_DESERIALIZE,
21
21
  WORKFLOW_SERIALIZE,
@@ -111,18 +111,16 @@ export class OpenAICompletionLanguageModel implements LanguageModelV4 {
111
111
  const warnings: SharedV4Warning[] = [];
112
112
 
113
113
  // Parse provider options
114
- const openaiOptions = {
115
- ...(await parseProviderOptions({
116
- provider: 'openai',
117
- providerOptions,
118
- schema: openaiLanguageModelCompletionOptions,
119
- })),
120
- ...(await parseProviderOptions({
121
- provider: this.providerOptionsName,
122
- providerOptions,
123
- schema: openaiLanguageModelCompletionOptions,
124
- })),
125
- };
114
+ // `openai` and the provider key are merged; the provider key wins.
115
+ const openaiOptions =
116
+ (
117
+ await resolveProviderOptions({
118
+ providerOptions,
119
+ namespaces: ['openai', this.providerOptionsName],
120
+ resolution: 'merge',
121
+ schema: openaiLanguageModelCompletionOptions,
122
+ })
123
+ ).options ?? {};
126
124
 
127
125
  if (topK != null) {
128
126
  warnings.push({ type: 'unsupported', feature: 'topK' });
@@ -100,71 +100,82 @@ export type OpenAIImageModelOptions = InferSchema<
100
100
  typeof openaiImageModelOptions
101
101
  >;
102
102
 
103
+ /**
104
+ * Shape of the OpenAI image generation options. Exported so that routers
105
+ * that also call the OpenAI image API (e.g. Azure) can accept these options
106
+ * under their own providerOptions key.
107
+ */
108
+ export const openaiImageModelGenerationOptionsShape = () =>
109
+ baseImageModelOptionsObject().extend({
110
+ /**
111
+ * Style of the generated image. `vivid` produces hyper-real and
112
+ * dramatic images; `natural` produces more subdued, less hyper-real
113
+ * looking images.
114
+ */
115
+ style: z.enum(['vivid', 'natural']).optional(),
116
+
117
+ /**
118
+ * Format in which the generated image(s) are returned. `gif` produces
119
+ * an animated GIF and is supported by GPT Image 2.6 models.
120
+ */
121
+ outputFormat: z.enum(['png', 'jpeg', 'webp', 'gif']).optional(),
122
+
123
+ /**
124
+ * Number of frames (2-64) of an animated GIF. Only applies to the `gif`
125
+ * output format. The API defaults to 9.
126
+ */
127
+ frameCount: z.number().int().min(2).max(64).optional(),
128
+
129
+ /**
130
+ * Duration of each frame in milliseconds (20-2000) of an animated GIF.
131
+ * The API rounds it to the nearest 10 ms. Only applies to the `gif`
132
+ * output format. The API defaults to 200.
133
+ */
134
+ frameDurationMs: z.number().int().min(20).max(2000).optional(),
135
+
136
+ /**
137
+ * Content moderation level for the generated image(s). `low` applies
138
+ * less restrictive filtering.
139
+ */
140
+ moderation: z.enum(['auto', 'low']).optional(),
141
+ }).shape;
142
+
103
143
  export const openaiImageModelGenerationOptions = lazySchema(() =>
104
- zodSchema(
105
- baseImageModelOptionsObject().extend({
106
- /**
107
- * Style of the generated image. `vivid` produces hyper-real and
108
- * dramatic images; `natural` produces more subdued, less hyper-real
109
- * looking images.
110
- */
111
- style: z.enum(['vivid', 'natural']).optional(),
112
-
113
- /**
114
- * Format in which the generated image(s) are returned. `gif` produces
115
- * an animated GIF and is supported by GPT Image 2.6 models.
116
- */
117
- outputFormat: z.enum(['png', 'jpeg', 'webp', 'gif']).optional(),
118
-
119
- /**
120
- * Number of frames (2-64) of an animated GIF. Only applies to the `gif`
121
- * output format. The API defaults to 9.
122
- */
123
- frameCount: z.number().int().min(2).max(64).optional(),
124
-
125
- /**
126
- * Duration of each frame in milliseconds (20-2000) of an animated GIF.
127
- * The API rounds it to the nearest 10 ms. Only applies to the `gif`
128
- * output format. The API defaults to 200.
129
- */
130
- frameDurationMs: z.number().int().min(20).max(2000).optional(),
131
-
132
- /**
133
- * Content moderation level for the generated image(s). `low` applies
134
- * less restrictive filtering.
135
- */
136
- moderation: z.enum(['auto', 'low']).optional(),
137
- }),
138
- ),
144
+ zodSchema(z.object(openaiImageModelGenerationOptionsShape())),
139
145
  );
140
146
 
141
147
  export type OpenAIImageModelGenerationOptions = InferSchema<
142
148
  typeof openaiImageModelGenerationOptions
143
149
  >;
144
150
 
151
+ /**
152
+ * Shape of the OpenAI image edit options. Exported so that routers can accept
153
+ * these options under their own providerOptions key.
154
+ */
155
+ export const openaiImageModelEditOptionsShape = () =>
156
+ baseImageModelOptionsObject().extend({
157
+ /**
158
+ * Fidelity of the output image(s) to the input image(s).
159
+ */
160
+ inputFidelity: z.enum(['high', 'low']).optional(),
161
+
162
+ /**
163
+ * A transparent PNG overlaid on the first input image to mark up the
164
+ * regions to change, as an HTTPS URL or a base64 data URL. It must match
165
+ * the dimensions of the first image. Supported by GPT Image 2.5 and 2.6
166
+ * models. Cannot be combined with a `mask`.
167
+ */
168
+ markup: z.string().optional(),
169
+
170
+ /**
171
+ * Content moderation level for the edited image(s). `low` applies
172
+ * less restrictive filtering. Supported by GPT Image models.
173
+ */
174
+ moderation: z.enum(['auto', 'low']).optional(),
175
+ }).shape;
176
+
145
177
  export const openaiImageModelEditOptions = lazySchema(() =>
146
- zodSchema(
147
- baseImageModelOptionsObject().extend({
148
- /**
149
- * Fidelity of the output image(s) to the input image(s).
150
- */
151
- inputFidelity: z.enum(['high', 'low']).optional(),
152
-
153
- /**
154
- * A transparent PNG overlaid on the first input image to mark up the
155
- * regions to change, as an HTTPS URL or a base64 data URL. It must match
156
- * the dimensions of the first image. Supported by GPT Image 2.5 and 2.6
157
- * models. Cannot be combined with a `mask`.
158
- */
159
- markup: z.string().optional(),
160
-
161
- /**
162
- * Content moderation level for the edited image(s). `low` applies
163
- * less restrictive filtering. Supported by GPT Image models.
164
- */
165
- moderation: z.enum(['auto', 'low']).optional(),
166
- }),
167
- ),
178
+ zodSchema(z.object(openaiImageModelEditOptionsShape())),
168
179
  );
169
180
 
170
181
  export type OpenAIImageModelEditOptions = InferSchema<
@@ -22,3 +22,5 @@ export * from '../tool/shell';
22
22
  export * from '../tool/tool-search';
23
23
  export * from '../tool/web-search';
24
24
  export * from '../tool/web-search-preview';
25
+
26
+ export { openaiModelCatalog } from '../openai-model-catalog';
@@ -748,6 +748,7 @@ async function convertOpenAIBatchResult(
748
748
  const content: LanguageModelV4GenerateResult['content'] = [];
749
749
  const logprobs: Array<NonNullable<OpenAIResponsesLogprobs>> = [];
750
750
  let hasFunctionCall = false;
751
+ let hasRefusal = false;
751
752
 
752
753
  for (const part of response.output) {
753
754
  switch (part.type) {
@@ -774,6 +775,12 @@ async function convertOpenAIBatchResult(
774
775
 
775
776
  case 'message': {
776
777
  for (const contentPart of part.content) {
778
+ if (contentPart.type === 'refusal') {
779
+ hasRefusal = true;
780
+ content.push({ type: 'text', text: contentPart.refusal });
781
+ continue;
782
+ }
783
+
777
784
  content.push({ type: 'text', text: contentPart.text });
778
785
  if (contentPart.logprobs != null) {
779
786
  logprobs.push(contentPart.logprobs);
@@ -916,6 +923,7 @@ async function convertOpenAIBatchResult(
916
923
  unified: mapOpenAIResponseFinishReason({
917
924
  finishReason: response.incomplete_details?.reason,
918
925
  hasFunctionCall,
926
+ hasRefusal,
919
927
  }),
920
928
  raw: response.incomplete_details?.reason ?? undefined,
921
929
  },
@@ -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':