@ai-sdk/openai 4.0.109 → 4.0.110
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 +23 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +159 -36
- package/dist/index.js.map +1 -1
- package/dist/internal/index.d.ts +146 -6
- package/dist/internal/index.d.ts.map +1 -1
- package/dist/internal/index.js +148 -35
- package/dist/internal/index.js.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/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 +56 -1
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,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(),
|
|
@@ -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. */
|
|
@@ -447,7 +456,10 @@ export class OpenAIChatLanguageModel implements LanguageModelV4 {
|
|
|
447
456
|
|
|
448
457
|
const content: Array<LanguageModelV4Content> = [];
|
|
449
458
|
|
|
450
|
-
// text content
|
|
459
|
+
// text content. When the model refuses to answer, the API returns the
|
|
460
|
+
// refusal in `refusal` and sets `content` to null, so surface it as text
|
|
461
|
+
// instead of dropping it. Content and refusal are both returned when the
|
|
462
|
+
// model refuses after answering, so keep both:
|
|
451
463
|
const text =
|
|
452
464
|
choice.message.content != null && choice.message.content.length > 0
|
|
453
465
|
? choice.message.content
|
|
@@ -456,6 +468,12 @@ export class OpenAIChatLanguageModel implements LanguageModelV4 {
|
|
|
456
468
|
content.push({ type: 'text', text });
|
|
457
469
|
}
|
|
458
470
|
|
|
471
|
+
const refusal = choice.message.refusal;
|
|
472
|
+
const hasRefusal = refusal != null && refusal.length > 0;
|
|
473
|
+
if (hasRefusal) {
|
|
474
|
+
content.push({ type: 'text', text: refusal });
|
|
475
|
+
}
|
|
476
|
+
|
|
459
477
|
// tool calls:
|
|
460
478
|
for (const toolCall of choice.message.tool_calls ?? []) {
|
|
461
479
|
content.push({
|
|
@@ -488,14 +506,25 @@ export class OpenAIChatLanguageModel implements LanguageModelV4 {
|
|
|
488
506
|
providerMetadata.openai.rejectedPredictionTokens =
|
|
489
507
|
completionTokenDetails?.rejected_prediction_tokens;
|
|
490
508
|
}
|
|
491
|
-
|
|
492
|
-
|
|
509
|
+
// logprobs either follow the text content (content) or the refusal, so
|
|
510
|
+
// combine them in the order the text is emitted above. Explicitly empty
|
|
511
|
+
// logprobs are reported as an empty array, as before:
|
|
512
|
+
const contentLogprobs = choice.logprobs?.content;
|
|
513
|
+
const refusalLogprobs = choice.logprobs?.refusal;
|
|
514
|
+
if (contentLogprobs != null || refusalLogprobs != null) {
|
|
515
|
+
providerMetadata.openai.logprobs = [
|
|
516
|
+
...(contentLogprobs ?? []),
|
|
517
|
+
...(refusalLogprobs ?? []),
|
|
518
|
+
];
|
|
493
519
|
}
|
|
494
520
|
|
|
495
521
|
return {
|
|
496
522
|
content,
|
|
497
523
|
finishReason: {
|
|
498
|
-
unified: mapOpenAIFinishReason(
|
|
524
|
+
unified: mapOpenAIFinishReason({
|
|
525
|
+
finishReason: choice.finish_reason,
|
|
526
|
+
hasRefusal,
|
|
527
|
+
}),
|
|
499
528
|
raw: choice.finish_reason ?? undefined,
|
|
500
529
|
},
|
|
501
530
|
usage: convertOpenAIChatUsage(response.usage),
|
|
@@ -562,6 +591,13 @@ export class OpenAIChatLanguageModel implements LanguageModelV4 {
|
|
|
562
591
|
let usage: OpenAIChatUsage | undefined = undefined;
|
|
563
592
|
let metadataExtracted = false;
|
|
564
593
|
let isActiveText = false;
|
|
594
|
+
let hasRefusal = false;
|
|
595
|
+
let rawFinishReason: string | undefined = undefined;
|
|
596
|
+
let hasLogprobs = false;
|
|
597
|
+
|
|
598
|
+
// logprobs of all streamed chunks (content token logprobs followed by
|
|
599
|
+
// refusal token logprobs), collected across chunks:
|
|
600
|
+
const streamedLogprobs: OpenAIChatLogprob[] = [];
|
|
565
601
|
|
|
566
602
|
const providerMetadata: SharedV4ProviderMetadata = { openai: {} };
|
|
567
603
|
|
|
@@ -587,6 +623,7 @@ export class OpenAIChatLanguageModel implements LanguageModelV4 {
|
|
|
587
623
|
// handle failed chunk parsing / validation:
|
|
588
624
|
if (!chunk.success) {
|
|
589
625
|
finishReason = { unified: 'error', raw: undefined };
|
|
626
|
+
rawFinishReason = undefined;
|
|
590
627
|
controller.enqueue({ type: 'error', error: chunk.error });
|
|
591
628
|
return;
|
|
592
629
|
}
|
|
@@ -596,6 +633,7 @@ export class OpenAIChatLanguageModel implements LanguageModelV4 {
|
|
|
596
633
|
// handle error chunks:
|
|
597
634
|
if ('error' in value) {
|
|
598
635
|
finishReason = { unified: 'error', raw: undefined };
|
|
636
|
+
rawFinishReason = undefined;
|
|
599
637
|
controller.enqueue({
|
|
600
638
|
type: 'error',
|
|
601
639
|
error:
|
|
@@ -639,15 +677,29 @@ export class OpenAIChatLanguageModel implements LanguageModelV4 {
|
|
|
639
677
|
|
|
640
678
|
const choice = value.choices[0];
|
|
641
679
|
|
|
680
|
+
// store the raw finish reason and map it in `flush`, after all
|
|
681
|
+
// chunks have been read: some servers send the last delta (which
|
|
682
|
+
// can contain a refusal) in the same chunk as the finish reason,
|
|
683
|
+
// and a refusal turns `stop` into `content-filter`:
|
|
642
684
|
if (choice?.finish_reason != null) {
|
|
643
|
-
|
|
644
|
-
unified: mapOpenAIFinishReason(choice.finish_reason),
|
|
645
|
-
raw: choice.finish_reason,
|
|
646
|
-
};
|
|
685
|
+
rawFinishReason = choice.finish_reason;
|
|
647
686
|
}
|
|
648
687
|
|
|
649
|
-
|
|
650
|
-
|
|
688
|
+
// logprobs either follow the text content (content) or the refusal.
|
|
689
|
+
// They are streamed token by token, so collect the logprobs of all
|
|
690
|
+
// chunks (content logprobs followed by refusal logprobs) and report
|
|
691
|
+
// the complete list at the end of the stream:
|
|
692
|
+
if (choice?.logprobs != null) {
|
|
693
|
+
const contentLogprobs = choice.logprobs.content;
|
|
694
|
+
const refusalLogprobs = choice.logprobs.refusal;
|
|
695
|
+
|
|
696
|
+
if (contentLogprobs != null || refusalLogprobs != null) {
|
|
697
|
+
hasLogprobs = true;
|
|
698
|
+
streamedLogprobs.push(
|
|
699
|
+
...(contentLogprobs ?? []),
|
|
700
|
+
...(refusalLogprobs ?? []),
|
|
701
|
+
);
|
|
702
|
+
}
|
|
651
703
|
}
|
|
652
704
|
|
|
653
705
|
if (choice?.delta == null) {
|
|
@@ -656,7 +708,24 @@ export class OpenAIChatLanguageModel implements LanguageModelV4 {
|
|
|
656
708
|
|
|
657
709
|
const delta = choice.delta;
|
|
658
710
|
|
|
659
|
-
|
|
711
|
+
// When the model refuses to answer, the API streams the refusal in
|
|
712
|
+
// `refusal` instead of `content`. Content and refusal deltas can
|
|
713
|
+
// both occur, so append each to the same text part instead of
|
|
714
|
+
// dropping one:
|
|
715
|
+
const textDeltas: string[] = [];
|
|
716
|
+
if (delta.content != null && delta.content.length > 0) {
|
|
717
|
+
textDeltas.push(delta.content);
|
|
718
|
+
}
|
|
719
|
+
if (delta.refusal != null && delta.refusal.length > 0) {
|
|
720
|
+
hasRefusal = true;
|
|
721
|
+
textDeltas.push(delta.refusal);
|
|
722
|
+
}
|
|
723
|
+
// empty `content: ""` deltas keep their previous behavior:
|
|
724
|
+
if (textDeltas.length === 0 && delta.content === '') {
|
|
725
|
+
textDeltas.push(delta.content);
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
for (const textDelta of textDeltas) {
|
|
660
729
|
if (!isActiveText) {
|
|
661
730
|
controller.enqueue({ type: 'text-start', id: '0' });
|
|
662
731
|
isActiveText = true;
|
|
@@ -665,7 +734,7 @@ export class OpenAIChatLanguageModel implements LanguageModelV4 {
|
|
|
665
734
|
controller.enqueue({
|
|
666
735
|
type: 'text-delta',
|
|
667
736
|
id: '0',
|
|
668
|
-
delta:
|
|
737
|
+
delta: textDelta,
|
|
669
738
|
});
|
|
670
739
|
}
|
|
671
740
|
|
|
@@ -696,6 +765,25 @@ export class OpenAIChatLanguageModel implements LanguageModelV4 {
|
|
|
696
765
|
|
|
697
766
|
toolCallTracker.flush();
|
|
698
767
|
|
|
768
|
+
// map the stored finish reason now that all deltas (including
|
|
769
|
+
// refusals that arrived in the same chunk as the finish reason)
|
|
770
|
+
// have been read:
|
|
771
|
+
if (rawFinishReason != null) {
|
|
772
|
+
finishReason = {
|
|
773
|
+
unified: mapOpenAIFinishReason({
|
|
774
|
+
finishReason: rawFinishReason,
|
|
775
|
+
hasRefusal,
|
|
776
|
+
}),
|
|
777
|
+
raw: rawFinishReason,
|
|
778
|
+
};
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
// report the logprobs of all streamed chunks (content logprobs
|
|
782
|
+
// followed by refusal logprobs), not only the last chunk:
|
|
783
|
+
if (hasLogprobs) {
|
|
784
|
+
providerMetadata.openai.logprobs = streamedLogprobs;
|
|
785
|
+
}
|
|
786
|
+
|
|
699
787
|
controller.enqueue({
|
|
700
788
|
type: 'finish',
|
|
701
789
|
finishReason,
|
|
@@ -723,6 +811,7 @@ function isOpenAIChatOutputChunk(chunk: OpenAIChatChunk): boolean {
|
|
|
723
811
|
|
|
724
812
|
return (
|
|
725
813
|
(delta?.content != null && delta.content.length > 0) ||
|
|
814
|
+
(delta?.refusal != null && delta.refusal.length > 0) ||
|
|
726
815
|
(delta?.tool_calls != null && delta.tool_calls.length > 0) ||
|
|
727
816
|
(delta?.annotations != null && delta.annotations.length > 0)
|
|
728
817
|
);
|
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':
|
|
@@ -869,6 +869,8 @@ const openaiResponsesModeledChunkTypes = new Set([
|
|
|
869
869
|
'response.output_item.done',
|
|
870
870
|
'response.output_text.annotation.added',
|
|
871
871
|
'response.output_text.delta',
|
|
872
|
+
'response.refusal.delta',
|
|
873
|
+
'response.refusal.done',
|
|
872
874
|
'response.reasoning_summary_part.added',
|
|
873
875
|
'response.reasoning_summary_part.done',
|
|
874
876
|
'response.reasoning_summary_text.delta',
|
|
@@ -948,6 +950,20 @@ export const openaiResponsesChunkSchema = lazySchema(() =>
|
|
|
948
950
|
)
|
|
949
951
|
.nullish(),
|
|
950
952
|
}),
|
|
953
|
+
z.object({
|
|
954
|
+
type: z.literal('response.refusal.delta'),
|
|
955
|
+
item_id: z.string(),
|
|
956
|
+
output_index: z.number().nullish(),
|
|
957
|
+
content_index: z.number().nullish(),
|
|
958
|
+
delta: z.string(),
|
|
959
|
+
}),
|
|
960
|
+
z.object({
|
|
961
|
+
type: z.literal('response.refusal.done'),
|
|
962
|
+
item_id: z.string(),
|
|
963
|
+
output_index: z.number().nullish(),
|
|
964
|
+
content_index: z.number().nullish(),
|
|
965
|
+
refusal: z.string(),
|
|
966
|
+
}),
|
|
951
967
|
z.object({
|
|
952
968
|
type: z.enum(['response.completed', 'response.incomplete']),
|
|
953
969
|
response: z.object({
|
|
@@ -1548,55 +1564,62 @@ export const openaiResponsesResponseSchema = lazySchema(() =>
|
|
|
1548
1564
|
role: z.literal('assistant'),
|
|
1549
1565
|
id: z.string(),
|
|
1550
1566
|
phase: z.enum(['commentary', 'final_answer']).nullish(),
|
|
1567
|
+
// the API returns either text or a refusal as content parts:
|
|
1551
1568
|
content: z.array(
|
|
1552
|
-
z.
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
z.
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
z.
|
|
1571
|
-
z.
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1569
|
+
z.discriminatedUnion('type', [
|
|
1570
|
+
z.object({
|
|
1571
|
+
type: z.literal('output_text'),
|
|
1572
|
+
text: z.string(),
|
|
1573
|
+
logprobs: z
|
|
1574
|
+
.array(
|
|
1575
|
+
z.object({
|
|
1576
|
+
token: z.string(),
|
|
1577
|
+
logprob: z.number(),
|
|
1578
|
+
top_logprobs: z.array(
|
|
1579
|
+
z.object({
|
|
1580
|
+
token: z.string(),
|
|
1581
|
+
logprob: z.number(),
|
|
1582
|
+
}),
|
|
1583
|
+
),
|
|
1584
|
+
}),
|
|
1585
|
+
)
|
|
1586
|
+
.nullish(),
|
|
1587
|
+
annotations: z.array(
|
|
1588
|
+
z.discriminatedUnion('type', [
|
|
1589
|
+
z.object({
|
|
1590
|
+
type: z.literal('url_citation'),
|
|
1591
|
+
start_index: z.number(),
|
|
1592
|
+
end_index: z.number(),
|
|
1593
|
+
url: z.string(),
|
|
1594
|
+
title: z.string(),
|
|
1595
|
+
}),
|
|
1596
|
+
z.object({
|
|
1597
|
+
type: z.literal('file_citation'),
|
|
1598
|
+
file_id: z.string(),
|
|
1599
|
+
filename: z.string(),
|
|
1600
|
+
index: z.number(),
|
|
1601
|
+
}),
|
|
1602
|
+
z.object({
|
|
1603
|
+
type: z.literal('container_file_citation'),
|
|
1604
|
+
container_id: z.string(),
|
|
1605
|
+
file_id: z.string(),
|
|
1606
|
+
filename: z.string(),
|
|
1607
|
+
start_index: z.number(),
|
|
1608
|
+
end_index: z.number(),
|
|
1609
|
+
}),
|
|
1610
|
+
z.object({
|
|
1611
|
+
type: z.literal('file_path'),
|
|
1612
|
+
file_id: z.string(),
|
|
1613
|
+
index: z.number(),
|
|
1614
|
+
}),
|
|
1615
|
+
]),
|
|
1616
|
+
),
|
|
1617
|
+
}),
|
|
1618
|
+
z.object({
|
|
1619
|
+
type: z.literal('refusal'),
|
|
1620
|
+
refusal: z.string(),
|
|
1621
|
+
}),
|
|
1622
|
+
]),
|
|
1600
1623
|
),
|
|
1601
1624
|
}),
|
|
1602
1625
|
z.object({
|