@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.
- package/CHANGELOG.md +28 -0
- package/dist/index.d.mts +23 -2
- package/dist/index.d.ts +23 -2
- package/dist/index.js +290 -85
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +276 -69
- package/dist/index.mjs.map +1 -1
- package/dist/internal/index.d.mts +23 -2
- package/dist/internal/index.d.ts +23 -2
- package/dist/internal/index.js +289 -84
- package/dist/internal/index.js.map +1 -1
- package/dist/internal/index.mjs +275 -68
- package/dist/internal/index.mjs.map +1 -1
- package/docs/03-openai.mdx +92 -0
- package/package.json +3 -3
- package/src/chat/map-openai-finish-reason.ts +11 -4
- package/src/chat/openai-chat-api.ts +36 -0
- package/src/chat/openai-chat-language-model.ts +101 -12
- package/src/image/openai-image-model-options.ts +44 -0
- package/src/image/openai-image-model.ts +44 -4
- package/src/responses/map-openai-responses-finish-reason.ts +7 -1
- package/src/responses/openai-responses-api.ts +71 -48
- package/src/responses/openai-responses-language-model.ts +47 -0
package/docs/03-openai.mdx
CHANGED
|
@@ -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.
|
|
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
|
-
"@
|
|
48
|
-
"@ai-
|
|
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
|
|
5
|
-
|
|
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
|
-
|
|
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
|
-
|
|
427
|
-
|
|
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(
|
|
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
|
-
|
|
571
|
-
unified: mapOpenAIFinishReason(choice.finish_reason),
|
|
572
|
-
raw: choice.finish_reason,
|
|
573
|
-
};
|
|
612
|
+
rawFinishReason = choice.finish_reason;
|
|
574
613
|
}
|
|
575
614
|
|
|
576
|
-
|
|
577
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
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
|
-
|
|
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':
|