@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.
- package/CHANGELOG.md +35 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +206 -68
- package/dist/index.js.map +1 -1
- package/dist/internal/index.d.ts +316 -6
- package/dist/internal/index.d.ts.map +1 -1
- package/dist/internal/index.js +195 -67
- package/dist/internal/index.js.map +1 -1
- package/docs/03-openai.mdx +68 -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 +120 -20
- package/src/completion/openai-completion-language-model.ts +11 -13
- package/src/image/openai-image-model-options.ts +68 -57
- package/src/internal/index.ts +2 -0
- package/src/openai-batch.ts +8 -0
- 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 +62 -12
- package/src/speech/openai-speech-model-options.ts +11 -6
- package/src/transcription/openai-transcription-model-options.ts +71 -70
package/docs/03-openai.mdx
CHANGED
|
@@ -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.
|
|
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.
|
|
39
|
-
"@ai-sdk/provider-utils": "5.0.
|
|
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
|
|
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
|
+
}): LanguageModelV4FinishReason['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(),
|
|
@@ -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
|
-
//
|
|
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
|
-
(
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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 (
|
|
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
|
-
|
|
492
|
-
|
|
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(
|
|
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
|
-
|
|
644
|
-
unified: mapOpenAIFinishReason(choice.finish_reason),
|
|
645
|
-
raw: choice.finish_reason,
|
|
646
|
-
};
|
|
696
|
+
rawFinishReason = choice.finish_reason;
|
|
647
697
|
}
|
|
648
698
|
|
|
649
|
-
|
|
650
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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<
|
package/src/internal/index.ts
CHANGED
package/src/openai-batch.ts
CHANGED
|
@@ -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
|
-
|
|
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':
|