@ai-sdk/openai 4.0.0-beta.20 → 4.0.0-beta.21

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ai-sdk/openai",
3
- "version": "4.0.0-beta.20",
3
+ "version": "4.0.0-beta.21",
4
4
  "license": "Apache-2.0",
5
5
  "sideEffects": false,
6
6
  "main": "./dist/index.js",
@@ -36,8 +36,8 @@
36
36
  }
37
37
  },
38
38
  "dependencies": {
39
- "@ai-sdk/provider": "4.0.0-beta.5",
40
- "@ai-sdk/provider-utils": "5.0.0-beta.9"
39
+ "@ai-sdk/provider": "4.0.0-beta.6",
40
+ "@ai-sdk/provider-utils": "5.0.0-beta.10"
41
41
  },
42
42
  "devDependencies": {
43
43
  "@types/node": "20.17.24",
@@ -4,7 +4,11 @@ import {
4
4
  UnsupportedFunctionalityError,
5
5
  } from '@ai-sdk/provider';
6
6
  import { OpenAIChatPrompt } from './openai-chat-prompt';
7
- import { convertToBase64 } from '@ai-sdk/provider-utils';
7
+ import {
8
+ convertToBase64,
9
+ isProviderReference,
10
+ resolveProviderReference,
11
+ } from '@ai-sdk/provider-utils';
8
12
 
9
13
  export function convertToOpenAIChatMessages({
10
14
  prompt,
@@ -62,6 +66,18 @@ export function convertToOpenAIChatMessages({
62
66
  return { type: 'text', text: part.text };
63
67
  }
64
68
  case 'file': {
69
+ if (isProviderReference(part.data)) {
70
+ return {
71
+ type: 'file',
72
+ file: {
73
+ file_id: resolveProviderReference({
74
+ reference: part.data,
75
+ provider: 'openai',
76
+ }),
77
+ },
78
+ };
79
+ }
80
+
65
81
  if (part.mediaType.startsWith('image/')) {
66
82
  const mediaType =
67
83
  part.mediaType === 'image/*'
@@ -76,7 +92,6 @@ export function convertToOpenAIChatMessages({
76
92
  ? part.data.toString()
77
93
  : `data:${mediaType};base64,${convertToBase64(part.data)}`,
78
94
 
79
- // OpenAI specific extension: image detail
80
95
  detail: part.providerOptions?.openai?.imageDetail,
81
96
  },
82
97
  };
@@ -123,14 +138,10 @@ export function convertToOpenAIChatMessages({
123
138
 
124
139
  return {
125
140
  type: 'file',
126
- file:
127
- typeof part.data === 'string' &&
128
- part.data.startsWith('file-')
129
- ? { file_id: part.data }
130
- : {
131
- filename: part.filename ?? `part-${index}.pdf`,
132
- file_data: `data:application/pdf;base64,${convertToBase64(part.data)}`,
133
- },
141
+ file: {
142
+ filename: part.filename ?? `part-${index}.pdf`,
143
+ file_data: `data:application/pdf;base64,${convertToBase64(part.data)}`,
144
+ },
134
145
  };
135
146
  } else {
136
147
  throw new UnsupportedFunctionalityError({
@@ -0,0 +1,17 @@
1
+ import { lazySchema, zodSchema } from '@ai-sdk/provider-utils';
2
+ import { z } from 'zod/v4';
3
+
4
+ export const openaiFilesResponseSchema = lazySchema(() =>
5
+ zodSchema(
6
+ z.object({
7
+ id: z.string(),
8
+ object: z.string().nullish(),
9
+ bytes: z.number().nullish(),
10
+ created_at: z.number().nullish(),
11
+ filename: z.string().nullish(),
12
+ purpose: z.string().nullish(),
13
+ status: z.string().nullish(),
14
+ expires_at: z.number().nullish(),
15
+ }),
16
+ ),
17
+ );
@@ -0,0 +1,18 @@
1
+ import { InferSchema, lazySchema, zodSchema } from '@ai-sdk/provider-utils';
2
+ import { z } from 'zod/v4';
3
+
4
+ export const openaiFilesOptionsSchema = lazySchema(() =>
5
+ zodSchema(
6
+ z.object({
7
+ /*
8
+ * Required by the OpenAI API, but optional here because
9
+ * the SDK defaults to "assistants" — by far the most common
10
+ * purpose when uploading files in this context.
11
+ */
12
+ purpose: z.string().optional(),
13
+ expiresAfter: z.number().optional(),
14
+ }),
15
+ ),
16
+ );
17
+
18
+ export type OpenAIFilesOptions = InferSchema<typeof openaiFilesOptionsSchema>;
@@ -0,0 +1,102 @@
1
+ import {
2
+ FilesV4,
3
+ FilesV4UploadFileCallOptions,
4
+ FilesV4UploadFileResult,
5
+ } from '@ai-sdk/provider';
6
+ import {
7
+ combineHeaders,
8
+ convertBase64ToUint8Array,
9
+ createJsonResponseHandler,
10
+ FetchFunction,
11
+ parseProviderOptions,
12
+ postFormDataToApi,
13
+ } from '@ai-sdk/provider-utils';
14
+ import { openaiFailedResponseHandler } from '../openai-error';
15
+ import { openaiFilesResponseSchema } from './openai-files-api';
16
+ import {
17
+ openaiFilesOptionsSchema,
18
+ OpenAIFilesOptions,
19
+ } from './openai-files-options';
20
+
21
+ interface OpenAIFilesConfig {
22
+ provider: string;
23
+ baseURL: string;
24
+ headers: () => Record<string, string | undefined>;
25
+ fetch?: FetchFunction;
26
+ }
27
+
28
+ export class OpenAIFiles implements FilesV4 {
29
+ readonly specificationVersion = 'v4';
30
+
31
+ get provider(): string {
32
+ return this.config.provider;
33
+ }
34
+
35
+ constructor(private readonly config: OpenAIFilesConfig) {}
36
+
37
+ async uploadFile({
38
+ data,
39
+ mediaType,
40
+ filename,
41
+ providerOptions,
42
+ }: FilesV4UploadFileCallOptions): Promise<FilesV4UploadFileResult> {
43
+ const openaiOptions = (await parseProviderOptions({
44
+ provider: 'openai',
45
+ providerOptions,
46
+ schema: openaiFilesOptionsSchema,
47
+ })) as OpenAIFilesOptions | undefined;
48
+
49
+ const fileBytes =
50
+ data instanceof Uint8Array ? data : convertBase64ToUint8Array(data);
51
+
52
+ const blob = new Blob([fileBytes], {
53
+ type: mediaType,
54
+ });
55
+
56
+ const formData = new FormData();
57
+ if (filename != null) {
58
+ formData.append('file', blob, filename);
59
+ } else {
60
+ formData.append('file', blob);
61
+ }
62
+ formData.append('purpose', openaiOptions?.purpose ?? 'assistants');
63
+
64
+ if (openaiOptions?.expiresAfter != null) {
65
+ formData.append('expires_after', String(openaiOptions.expiresAfter));
66
+ }
67
+
68
+ const { value: response } = await postFormDataToApi({
69
+ url: `${this.config.baseURL}/files`,
70
+ headers: combineHeaders(this.config.headers()),
71
+ formData,
72
+ failedResponseHandler: openaiFailedResponseHandler,
73
+ successfulResponseHandler: createJsonResponseHandler(
74
+ openaiFilesResponseSchema,
75
+ ),
76
+ fetch: this.config.fetch,
77
+ });
78
+
79
+ return {
80
+ warnings: [],
81
+ providerReference: { openai: response.id },
82
+ ...((response.filename ?? filename)
83
+ ? { filename: response.filename ?? filename }
84
+ : {}),
85
+ ...(mediaType != null ? { mediaType } : {}),
86
+ providerMetadata: {
87
+ openai: {
88
+ ...(response.filename != null ? { filename: response.filename } : {}),
89
+ ...(response.purpose != null ? { purpose: response.purpose } : {}),
90
+ ...(response.bytes != null ? { bytes: response.bytes } : {}),
91
+ ...(response.created_at != null
92
+ ? { createdAt: response.created_at }
93
+ : {}),
94
+ ...(response.status != null ? { status: response.status } : {}),
95
+ ...(response.expires_at != null
96
+ ? { expiresAt: response.expires_at }
97
+ : {}),
98
+ },
99
+ },
100
+ };
101
+ }
102
+ }
package/src/index.ts CHANGED
@@ -14,6 +14,7 @@ export type { OpenAILanguageModelCompletionOptions } from './completion/openai-c
14
14
  export type { OpenAIEmbeddingModelOptions } from './embedding/openai-embedding-options';
15
15
  export type { OpenAISpeechModelOptions } from './speech/openai-speech-options';
16
16
  export type { OpenAITranscriptionModelOptions } from './transcription/openai-transcription-options';
17
+ export type { OpenAIFilesOptions } from './files/openai-files-options';
17
18
  export type {
18
19
  OpenaiResponsesCompactionProviderMetadata,
19
20
  OpenaiResponsesProviderMetadata,
@@ -7,12 +7,12 @@ export type OpenAIConfig = {
7
7
  fetch?: FetchFunction;
8
8
  generateId?: () => string;
9
9
  /**
10
- * File ID prefixes used to identify file IDs in Responses API.
11
- * When undefined, all file data is treated as base64 content.
10
+ * This is soft-deprecated. Use provider references (e.g. `{ openai: 'file-abc123' }`)
11
+ * in file part data instead. File ID prefixes used to identify file IDs
12
+ * in Responses API. When undefined, all string file data is treated as
13
+ * base64 content.
12
14
  *
13
- * Examples:
14
- * - OpenAI: ['file-'] for IDs like 'file-abc123'
15
- * - Azure OpenAI: ['assistant-'] for IDs like 'assistant-abc123'
15
+ * TODO: remove in v8
16
16
  */
17
17
  fileIdPrefixes?: readonly string[];
18
18
  };
@@ -1,5 +1,6 @@
1
1
  import {
2
2
  EmbeddingModelV4,
3
+ FilesV4,
3
4
  ImageModelV4,
4
5
  LanguageModelV4,
5
6
  ProviderV4,
@@ -18,6 +19,7 @@ import { OpenAIChatModelId } from './chat/openai-chat-options';
18
19
  import { OpenAICompletionLanguageModel } from './completion/openai-completion-language-model';
19
20
  import { OpenAICompletionModelId } from './completion/openai-completion-options';
20
21
  import { OpenAIEmbeddingModel } from './embedding/openai-embedding-model';
22
+ import { OpenAIFiles } from './files/openai-files';
21
23
  import { OpenAIEmbeddingModelId } from './embedding/openai-embedding-options';
22
24
  import { OpenAIImageModel } from './image/openai-image-model';
23
25
  import { OpenAIImageModelId } from './image/openai-image-options';
@@ -93,6 +95,11 @@ export interface OpenAIProvider extends ProviderV4 {
93
95
  */
94
96
  speech(modelId: OpenAISpeechModelId): SpeechModelV4;
95
97
 
98
+ /**
99
+ * Returns a FilesV4 interface for uploading files to OpenAI.
100
+ */
101
+ files(): FilesV4;
102
+
96
103
  /**
97
104
  * OpenAI-specific tools.
98
105
  */
@@ -216,6 +223,14 @@ export function createOpenAI(
216
223
  fetch: options.fetch,
217
224
  });
218
225
 
226
+ const createFiles = () =>
227
+ new OpenAIFiles({
228
+ provider: `${providerName}.files`,
229
+ baseURL,
230
+ headers: getHeaders,
231
+ fetch: options.fetch,
232
+ });
233
+
219
234
  const createLanguageModel = (modelId: OpenAIResponsesModelId) => {
220
235
  if (new.target) {
221
236
  throw new Error(
@@ -232,6 +247,7 @@ export function createOpenAI(
232
247
  url: ({ path }) => `${baseURL}${path}`,
233
248
  headers: getHeaders,
234
249
  fetch: options.fetch,
250
+ // Soft-deprecated. TODO: remove in v8
235
251
  fileIdPrefixes: ['file-'],
236
252
  });
237
253
  };
@@ -259,6 +275,8 @@ export function createOpenAI(
259
275
  provider.speech = createSpeechModel;
260
276
  provider.speechModel = createSpeechModel;
261
277
 
278
+ provider.files = createFiles;
279
+
262
280
  provider.tools = openaiTools;
263
281
 
264
282
  return provider as OpenAIProvider;
@@ -7,8 +7,10 @@ import {
7
7
  import {
8
8
  convertToBase64,
9
9
  isNonNullable,
10
+ isProviderReference,
10
11
  parseJSON,
11
12
  parseProviderOptions,
13
+ resolveProviderReference,
12
14
  ToolNameMapping,
13
15
  validateTypes,
14
16
  } from '@ai-sdk/provider-utils';
@@ -35,8 +37,10 @@ import {
35
37
  } from '../tool/tool-search';
36
38
 
37
39
  /**
38
- * Check if a string is a file ID based on the given prefixes
39
- * Returns false if prefixes is undefined (disables file ID detection)
40
+ * This is soft-deprecated. Use provider references instead. Kept for backward compatibility
41
+ * with the `fileIdPrefixes` option.
42
+ *
43
+ * TODO: remove in v8
40
44
  */
41
45
  function isFileId(data: string, prefixes?: readonly string[]): boolean {
42
46
  if (!prefixes) return false;
@@ -60,6 +64,7 @@ export async function convertToOpenAIResponsesInput({
60
64
  toolNameMapping: ToolNameMapping;
61
65
  systemMessageMode: 'system' | 'developer' | 'remove';
62
66
  providerOptionsName: string;
67
+ /** @deprecated Use provider references instead. */
63
68
  fileIdPrefixes?: readonly string[];
64
69
  store: boolean;
65
70
  hasConversation?: boolean; // when true, skip assistant messages that already have item IDs
@@ -113,6 +118,28 @@ export async function convertToOpenAIResponsesInput({
113
118
  return { type: 'input_text', text: part.text };
114
119
  }
115
120
  case 'file': {
121
+ if (isProviderReference(part.data)) {
122
+ const fileId = resolveProviderReference({
123
+ reference: part.data,
124
+ provider: providerOptionsName,
125
+ });
126
+
127
+ if (part.mediaType.startsWith('image/')) {
128
+ return {
129
+ type: 'input_image',
130
+ file_id: fileId,
131
+ detail:
132
+ part.providerOptions?.[providerOptionsName]
133
+ ?.imageDetail,
134
+ };
135
+ }
136
+
137
+ return {
138
+ type: 'input_file',
139
+ file_id: fileId,
140
+ };
141
+ }
142
+
116
143
  if (part.mediaType.startsWith('image/')) {
117
144
  const mediaType =
118
145
  part.mediaType === 'image/*'