@ai-sdk/openai 3.0.136 → 3.0.138

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.
@@ -2755,6 +2823,8 @@ const { images } = await generateImage({
2755
2823
 
2756
2824
  | Model | Sizes |
2757
2825
  | ------------------------ | -------------------------------------- |
2826
+ | `gpt-image-2.6-flare` | Standard presets and custom dimensions |
2827
+ | `gpt-image-2.6-sunburst` | Standard presets and custom dimensions |
2758
2828
  | `gpt-image-2.5-flare` | Standard presets and custom dimensions |
2759
2829
  | `gpt-image-2.5-sunburst` | Standard presets and custom dimensions |
2760
2830
  | `gpt-image-2` | 1024x1024, 1536x1024, 1024x1536 |
@@ -2802,6 +2872,28 @@ const { image } = await generateImage({
2802
2872
  });
2803
2873
  ```
2804
2874
 
2875
+ The `gpt-image-2.6-flare` and `gpt-image-2.6-sunburst` models support these additional options:
2876
+
2877
+ - **complexity** _'low' | 'medium' | 'high'_ - how much detail the model puts into the image (generation and editing). The API defaults to `high`.
2878
+ - **outputFormat: 'gif'** _(generation only)_ - generates an animated GIF. Use **frameCount** (2-64, API default 9) and **frameDurationMs** (20-2000, API default 200) to control the animation.
2879
+ - **markup** _string_ _(editing only)_ - a transparent PNG (HTTPS URL or base64 data URL) overlaid on the first input image to mark up the regions to change. Supported by `gpt-image-2.5-*` and `gpt-image-2.6-*` models. It cannot be combined with `mask`; providing both throws an error.
2880
+
2881
+ ```ts
2882
+ const { image } = await generateImage({
2883
+ model: openai.image('gpt-image-2.6-flare'),
2884
+ prompt: 'A red panda waving hello, flat illustration',
2885
+ providerOptions: {
2886
+ openai: {
2887
+ outputFormat: 'gif',
2888
+ frameCount: 9,
2889
+ frameDurationMs: 200,
2890
+ } satisfies OpenAIImageModelGenerationOptions,
2891
+ },
2892
+ });
2893
+ ```
2894
+
2895
+ All image options also accept `safetyIdentifier` (max 128 characters) to help OpenAI detect abuse by your end users.
2896
+
2805
2897
  For more on `generateImage()` see [Image Generation](/docs/ai-sdk-core/image-generation).
2806
2898
 
2807
2899
  OpenAI's image models return additional metadata in the response that can be
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ai-sdk/openai",
3
- "version": "3.0.136",
3
+ "version": "3.0.138",
4
4
  "license": "Apache-2.0",
5
5
  "sideEffects": false,
6
6
  "main": "./dist/index.js",
@@ -44,8 +44,8 @@
44
44
  "tsup": "^8",
45
45
  "typescript": "5.8.3",
46
46
  "zod": "3.25.76",
47
- "@vercel/ai-tsconfig": "0.0.0",
48
- "@ai-sdk/test-server": "1.0.6"
47
+ "@ai-sdk/test-server": "1.0.6",
48
+ "@vercel/ai-tsconfig": "0.0.0"
49
49
  },
50
50
  "peerDependencies": {
51
51
  "zod": "^3.25.76 || ^4.1.8"
@@ -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
  );
@@ -12,6 +12,8 @@ export type OpenAIImageModelId =
12
12
  | 'gpt-image-1-mini'
13
13
  | 'gpt-image-1.5'
14
14
  | 'gpt-image-2'
15
+ | 'gpt-image-2.6-flare'
16
+ | 'gpt-image-2.6-sunburst'
15
17
  | 'gpt-image-2.5-flare'
16
18
  | 'gpt-image-2.5-flare-2026-09-08'
17
19
  | 'gpt-image-2.5-sunburst'
@@ -27,6 +29,8 @@ export const modelMaxImagesPerCall: Record<OpenAIImageModelId, number> = {
27
29
  'gpt-image-1-mini': 10,
28
30
  'gpt-image-1.5': 10,
29
31
  'gpt-image-2': 10,
32
+ 'gpt-image-2.6-flare': 10,
33
+ 'gpt-image-2.6-sunburst': 10,
30
34
  'gpt-image-2.5-flare': 10,
31
35
  'gpt-image-2.5-flare-2026-09-08': 10,
32
36
  'gpt-image-2.5-sunburst': 10,
@@ -74,6 +78,12 @@ const baseImageModelOptionsObject = () =>
74
78
  */
75
79
  outputFormat: z.enum(['png', 'jpeg', 'webp']).optional(),
76
80
 
81
+ /**
82
+ * How much detail the model puts into the image. Supported by GPT Image
83
+ * 2.6 models. The API defaults to `high`.
84
+ */
85
+ complexity: z.enum(['low', 'medium', 'high']).optional(),
86
+
77
87
  /**
78
88
  * Compression level (0-100) for the generated image(s). Applies to the
79
89
  * `jpeg` and `webp` output formats.
@@ -85,6 +95,13 @@ const baseImageModelOptionsObject = () =>
85
95
  * to monitor and detect abuse.
86
96
  */
87
97
  user: z.string().optional(),
98
+
99
+ /**
100
+ * A stable identifier used to help detect users of your application that
101
+ * may be violating OpenAI's usage policies. Should be a hashed username
102
+ * or email address.
103
+ */
104
+ safetyIdentifier: z.string().max(128).optional(),
88
105
  });
89
106
 
90
107
  export const openaiImageModelOptions = lazySchema(() =>
@@ -105,6 +122,25 @@ export const openaiImageModelGenerationOptions = lazySchema(() =>
105
122
  */
106
123
  style: z.enum(['vivid', 'natural']).optional(),
107
124
 
125
+ /**
126
+ * Format in which the generated image(s) are returned. `gif` produces
127
+ * an animated GIF and is supported by GPT Image 2.6 models.
128
+ */
129
+ outputFormat: z.enum(['png', 'jpeg', 'webp', 'gif']).optional(),
130
+
131
+ /**
132
+ * Number of frames (2-64) of an animated GIF. Only applies to the `gif`
133
+ * output format. The API defaults to 9.
134
+ */
135
+ frameCount: z.number().int().min(2).max(64).optional(),
136
+
137
+ /**
138
+ * Duration of each frame in milliseconds (20-2000) of an animated GIF.
139
+ * The API rounds it to the nearest 10 ms. Only applies to the `gif`
140
+ * output format. The API defaults to 200.
141
+ */
142
+ frameDurationMs: z.number().int().min(20).max(2000).optional(),
143
+
108
144
  /**
109
145
  * Content moderation level for the generated image(s). `low` applies
110
146
  * less restrictive filtering.
@@ -125,6 +161,14 @@ export const openaiImageModelEditOptions = lazySchema(() =>
125
161
  * Fidelity of the output image(s) to the input image(s).
126
162
  */
127
163
  inputFidelity: z.enum(['high', 'low']).optional(),
164
+
165
+ /**
166
+ * A transparent PNG overlaid on the first input image to mark up the
167
+ * regions to change, as an HTTPS URL or a base64 data URL. It must match
168
+ * the dimensions of the first image. Supported by GPT Image 2.5 and 2.6
169
+ * models. Cannot be combined with a `mask`.
170
+ */
171
+ markup: z.string().optional(),
128
172
  }),
129
173
  ),
130
174
  );
@@ -1,7 +1,8 @@
1
- import type {
2
- ImageModelV3,
3
- ImageModelV3File,
4
- SharedV3Warning,
1
+ import {
2
+ InvalidArgumentError,
3
+ type ImageModelV3,
4
+ type ImageModelV3File,
5
+ type SharedV3Warning,
5
6
  } from '@ai-sdk/provider';
6
7
  import {
7
8
  combineHeaders,
@@ -86,6 +87,13 @@ export class OpenAIImageModel implements ImageModelV3 {
86
87
  schema: openaiImageModelEditOptions,
87
88
  })) ?? {};
88
89
 
90
+ if (mask != null && openaiOptions.markup != null) {
91
+ throw new InvalidArgumentError({
92
+ argument: 'providerOptions.openai.markup',
93
+ message: '`markup` cannot be combined with `mask`.',
94
+ });
95
+ }
96
+
89
97
  const { value: response, responseHeaders } = await postFormDataToApi({
90
98
  url: this.config.url({
91
99
  path: '/images/edits',
@@ -114,14 +122,20 @@ export class OpenAIImageModel implements ImageModelV3 {
114
122
  ),
115
123
  ),
116
124
  mask: mask != null ? await fileToBlob(mask) : undefined,
125
+ markup:
126
+ openaiOptions.markup != null
127
+ ? await markupToBlob(openaiOptions.markup)
128
+ : undefined,
117
129
  n,
118
130
  size,
119
131
  quality: openaiOptions.quality,
120
132
  background: openaiOptions.background,
121
133
  output_format: openaiOptions.outputFormat,
122
134
  output_compression: openaiOptions.outputCompression,
135
+ complexity: openaiOptions.complexity,
123
136
  input_fidelity: openaiOptions.inputFidelity,
124
137
  user: openaiOptions.user,
138
+ safety_identifier: openaiOptions.safetyIdentifier,
125
139
  }),
126
140
  failedResponseHandler: openaiFailedResponseHandler,
127
141
  successfulResponseHandler: createJsonResponseHandler(
@@ -193,7 +207,11 @@ export class OpenAIImageModel implements ImageModelV3 {
193
207
  moderation: openaiOptions.moderation,
194
208
  output_format: openaiOptions.outputFormat,
195
209
  output_compression: openaiOptions.outputCompression,
210
+ complexity: openaiOptions.complexity,
211
+ frame_count: openaiOptions.frameCount,
212
+ frame_duration_ms: openaiOptions.frameDurationMs,
196
213
  user: openaiOptions.user,
214
+ safety_identifier: openaiOptions.safetyIdentifier,
197
215
  ...(!hasDefaultResponseFormat(this.modelId)
198
216
  ? { response_format: 'b64_json' }
199
217
  : {}),
@@ -284,16 +302,38 @@ type OpenAIImageEditInput = {
284
302
  prompt?: string;
285
303
  image: Blob | Blob[];
286
304
  mask?: Blob;
305
+ markup?: Blob;
287
306
  n?: number;
288
307
  size?: `${number}x${number}`;
289
308
  quality?: OpenAIImageModelEditOptions['quality'];
290
309
  background?: OpenAIImageModelEditOptions['background'];
291
310
  output_format?: OpenAIImageModelEditOptions['outputFormat'];
292
311
  output_compression?: OpenAIImageModelEditOptions['outputCompression'];
312
+ complexity?: OpenAIImageModelEditOptions['complexity'];
293
313
  input_fidelity?: OpenAIImageModelEditOptions['inputFidelity'];
294
314
  user?: OpenAIImageModelEditOptions['user'];
315
+ safety_identifier?: OpenAIImageModelEditOptions['safetyIdentifier'];
295
316
  };
296
317
 
318
+ async function markupToBlob(markup: string): Promise<Blob> {
319
+ const dataUrl = /^data:([^;,]*)(;base64)?,(.*)$/s.exec(markup);
320
+
321
+ if (dataUrl == null) {
322
+ return downloadBlob(markup);
323
+ }
324
+
325
+ if (dataUrl[2] == null) {
326
+ throw new InvalidArgumentError({
327
+ argument: 'providerOptions.openai.markup',
328
+ message: '`markup` data URLs must be base64 encoded.',
329
+ });
330
+ }
331
+
332
+ return new Blob([convertBase64ToUint8Array(dataUrl[3]) as BlobPart], {
333
+ type: dataUrl[1] || 'image/png',
334
+ });
335
+ }
336
+
297
337
  async function fileToBlob(
298
338
  file: ImageModelV3File | undefined,
299
339
  ): Promise<Blob | undefined> {
@@ -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':