@ai-sdk/openai 3.0.137 → 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 +22 -0
- package/dist/index.d.mts +12 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +206 -66
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +206 -66
- package/dist/index.mjs.map +1 -1
- package/dist/internal/index.d.mts +12 -0
- package/dist/internal/index.d.ts +12 -0
- package/dist/internal/index.js +205 -65
- package/dist/internal/index.js.map +1 -1
- package/dist/internal/index.mjs +205 -65
- package/dist/internal/index.mjs.map +1 -1
- package/docs/03-openai.mdx +68 -0
- package/package.json +1 -1
- 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/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.
|
package/package.json
CHANGED
|
@@ -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
|
);
|
|
@@ -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':
|
|
@@ -681,6 +681,8 @@ const openaiResponsesModeledChunkTypes = new Set([
|
|
|
681
681
|
'response.output_item.done',
|
|
682
682
|
'response.output_text.annotation.added',
|
|
683
683
|
'response.output_text.delta',
|
|
684
|
+
'response.refusal.delta',
|
|
685
|
+
'response.refusal.done',
|
|
684
686
|
'response.reasoning_summary_part.added',
|
|
685
687
|
'response.reasoning_summary_part.done',
|
|
686
688
|
'response.reasoning_summary_text.delta',
|
|
@@ -756,6 +758,20 @@ export const openaiResponsesChunkSchema = lazySchema(() =>
|
|
|
756
758
|
)
|
|
757
759
|
.nullish(),
|
|
758
760
|
}),
|
|
761
|
+
z.object({
|
|
762
|
+
type: z.literal('response.refusal.delta'),
|
|
763
|
+
item_id: z.string(),
|
|
764
|
+
output_index: z.number().nullish(),
|
|
765
|
+
content_index: z.number().nullish(),
|
|
766
|
+
delta: z.string(),
|
|
767
|
+
}),
|
|
768
|
+
z.object({
|
|
769
|
+
type: z.literal('response.refusal.done'),
|
|
770
|
+
item_id: z.string(),
|
|
771
|
+
output_index: z.number().nullish(),
|
|
772
|
+
content_index: z.number().nullish(),
|
|
773
|
+
refusal: z.string(),
|
|
774
|
+
}),
|
|
759
775
|
z.object({
|
|
760
776
|
type: z.enum(['response.completed', 'response.incomplete']),
|
|
761
777
|
response: z.object({
|
|
@@ -1330,55 +1346,62 @@ export const openaiResponsesResponseSchema = lazySchema(() =>
|
|
|
1330
1346
|
role: z.literal('assistant'),
|
|
1331
1347
|
id: z.string(),
|
|
1332
1348
|
phase: z.enum(['commentary', 'final_answer']).nullish(),
|
|
1349
|
+
// the API returns either text or a refusal as content parts:
|
|
1333
1350
|
content: z.array(
|
|
1334
|
-
z.
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
z.
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
z.
|
|
1353
|
-
z.
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1351
|
+
z.discriminatedUnion('type', [
|
|
1352
|
+
z.object({
|
|
1353
|
+
type: z.literal('output_text'),
|
|
1354
|
+
text: z.string(),
|
|
1355
|
+
logprobs: z
|
|
1356
|
+
.array(
|
|
1357
|
+
z.object({
|
|
1358
|
+
token: z.string(),
|
|
1359
|
+
logprob: z.number(),
|
|
1360
|
+
top_logprobs: z.array(
|
|
1361
|
+
z.object({
|
|
1362
|
+
token: z.string(),
|
|
1363
|
+
logprob: z.number(),
|
|
1364
|
+
}),
|
|
1365
|
+
),
|
|
1366
|
+
}),
|
|
1367
|
+
)
|
|
1368
|
+
.nullish(),
|
|
1369
|
+
annotations: z.array(
|
|
1370
|
+
z.discriminatedUnion('type', [
|
|
1371
|
+
z.object({
|
|
1372
|
+
type: z.literal('url_citation'),
|
|
1373
|
+
start_index: z.number(),
|
|
1374
|
+
end_index: z.number(),
|
|
1375
|
+
url: z.string(),
|
|
1376
|
+
title: z.string(),
|
|
1377
|
+
}),
|
|
1378
|
+
z.object({
|
|
1379
|
+
type: z.literal('file_citation'),
|
|
1380
|
+
file_id: z.string(),
|
|
1381
|
+
filename: z.string(),
|
|
1382
|
+
index: z.number(),
|
|
1383
|
+
}),
|
|
1384
|
+
z.object({
|
|
1385
|
+
type: z.literal('container_file_citation'),
|
|
1386
|
+
container_id: z.string(),
|
|
1387
|
+
file_id: z.string(),
|
|
1388
|
+
filename: z.string(),
|
|
1389
|
+
start_index: z.number(),
|
|
1390
|
+
end_index: z.number(),
|
|
1391
|
+
}),
|
|
1392
|
+
z.object({
|
|
1393
|
+
type: z.literal('file_path'),
|
|
1394
|
+
file_id: z.string(),
|
|
1395
|
+
index: z.number(),
|
|
1396
|
+
}),
|
|
1397
|
+
]),
|
|
1398
|
+
),
|
|
1399
|
+
}),
|
|
1400
|
+
z.object({
|
|
1401
|
+
type: z.literal('refusal'),
|
|
1402
|
+
refusal: z.string(),
|
|
1403
|
+
}),
|
|
1404
|
+
]),
|
|
1382
1405
|
),
|
|
1383
1406
|
}),
|
|
1384
1407
|
z.object({
|
|
@@ -686,6 +686,9 @@ export class OpenAIResponsesLanguageModel implements LanguageModelV3 {
|
|
|
686
686
|
let hasFunctionCall = false;
|
|
687
687
|
const hostedToolSearchCallIds: string[] = [];
|
|
688
688
|
|
|
689
|
+
// flag that checks if the model refused to answer:
|
|
690
|
+
let hasRefusal = false;
|
|
691
|
+
|
|
689
692
|
// map response content to content array (defined when there is no error)
|
|
690
693
|
for (const part of response.output!) {
|
|
691
694
|
switch (part.type) {
|
|
@@ -842,6 +845,23 @@ export class OpenAIResponsesLanguageModel implements LanguageModelV3 {
|
|
|
842
845
|
|
|
843
846
|
case 'message': {
|
|
844
847
|
for (const contentPart of part.content) {
|
|
848
|
+
if (contentPart.type === 'refusal') {
|
|
849
|
+
hasRefusal = true;
|
|
850
|
+
|
|
851
|
+
content.push({
|
|
852
|
+
type: 'text',
|
|
853
|
+
text: contentPart.refusal,
|
|
854
|
+
providerMetadata: {
|
|
855
|
+
[providerOptionsName]: {
|
|
856
|
+
itemId: part.id,
|
|
857
|
+
...(part.phase != null && { phase: part.phase }),
|
|
858
|
+
} satisfies ResponsesTextProviderMetadata,
|
|
859
|
+
},
|
|
860
|
+
});
|
|
861
|
+
|
|
862
|
+
continue;
|
|
863
|
+
}
|
|
864
|
+
|
|
845
865
|
if (
|
|
846
866
|
options.providerOptions?.[providerOptionsName]?.logprobs &&
|
|
847
867
|
contentPart.logprobs
|
|
@@ -1200,6 +1220,7 @@ export class OpenAIResponsesLanguageModel implements LanguageModelV3 {
|
|
|
1200
1220
|
unified: mapOpenAIResponseFinishReason({
|
|
1201
1221
|
finishReason: response.incomplete_details?.reason,
|
|
1202
1222
|
hasFunctionCall,
|
|
1223
|
+
hasRefusal,
|
|
1203
1224
|
}),
|
|
1204
1225
|
raw: response.incomplete_details?.reason ?? undefined,
|
|
1205
1226
|
},
|
|
@@ -1320,6 +1341,9 @@ export class OpenAIResponsesLanguageModel implements LanguageModelV3 {
|
|
|
1320
1341
|
// flag that checks if there have been client-side tool calls (not executed by openai)
|
|
1321
1342
|
let hasFunctionCall = false;
|
|
1322
1343
|
|
|
1344
|
+
// flag that checks if the model refused to answer:
|
|
1345
|
+
let hasRefusal = false;
|
|
1346
|
+
|
|
1323
1347
|
const activeReasoning: Record<
|
|
1324
1348
|
string,
|
|
1325
1349
|
{
|
|
@@ -2265,6 +2289,16 @@ export class OpenAIResponsesLanguageModel implements LanguageModelV3 {
|
|
|
2265
2289
|
) {
|
|
2266
2290
|
logprobs.push(value.logprobs);
|
|
2267
2291
|
}
|
|
2292
|
+
} else if (isRefusalDeltaChunk(value)) {
|
|
2293
|
+
hasRefusal = true;
|
|
2294
|
+
|
|
2295
|
+
controller.enqueue({
|
|
2296
|
+
type: 'text-delta',
|
|
2297
|
+
id: value.item_id,
|
|
2298
|
+
delta: value.delta,
|
|
2299
|
+
});
|
|
2300
|
+
} else if (isRefusalDoneChunk(value)) {
|
|
2301
|
+
hasRefusal = true;
|
|
2268
2302
|
} else if (value.type === 'response.reasoning_summary_part.added') {
|
|
2269
2303
|
// the first reasoning start is pushed in isResponseOutputItemAddedReasoningChunk
|
|
2270
2304
|
if (value.summary_index > 0) {
|
|
@@ -2350,6 +2384,7 @@ export class OpenAIResponsesLanguageModel implements LanguageModelV3 {
|
|
|
2350
2384
|
unified: mapOpenAIResponseFinishReason({
|
|
2351
2385
|
finishReason: value.response.incomplete_details?.reason,
|
|
2352
2386
|
hasFunctionCall,
|
|
2387
|
+
hasRefusal,
|
|
2353
2388
|
}),
|
|
2354
2389
|
raw: value.response.incomplete_details?.reason ?? undefined,
|
|
2355
2390
|
};
|
|
@@ -2525,6 +2560,18 @@ function isTextDeltaChunk(
|
|
|
2525
2560
|
return chunk.type === 'response.output_text.delta';
|
|
2526
2561
|
}
|
|
2527
2562
|
|
|
2563
|
+
function isRefusalDeltaChunk(
|
|
2564
|
+
chunk: OpenAIResponsesChunk,
|
|
2565
|
+
): chunk is OpenAIResponsesChunk & { type: 'response.refusal.delta' } {
|
|
2566
|
+
return chunk.type === 'response.refusal.delta';
|
|
2567
|
+
}
|
|
2568
|
+
|
|
2569
|
+
function isRefusalDoneChunk(
|
|
2570
|
+
chunk: OpenAIResponsesChunk,
|
|
2571
|
+
): chunk is OpenAIResponsesChunk & { type: 'response.refusal.done' } {
|
|
2572
|
+
return chunk.type === 'response.refusal.done';
|
|
2573
|
+
}
|
|
2574
|
+
|
|
2528
2575
|
function isOpenAIChatCompletionChunk(value: unknown): boolean {
|
|
2529
2576
|
const chunk = asRecord(value);
|
|
2530
2577
|
|