Skip to content

Commit 0e692f6

Browse files
committed
fix(providers): upload OpenAI files via multipart and fix Buffer Blob part
The installed openai SDK (4.104) does not type expires_after on files.create, so upload via POST /v1/files directly with the documented expires_after[...] form fields (gives the file an auto-expiry). Also wrap the storage Buffer in a Uint8Array for the Blob, which the production build's stricter lib types require. These two type errors were masked locally because tsc was OOMing silently without the type-check script's --max-old-space-size flag.
1 parent 049ec39 commit 0e692f6

1 file changed

Lines changed: 30 additions & 14 deletions

File tree

apps/sim/providers/file-attachments.server.ts

Lines changed: 30 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ import { FileState, GoogleGenAI } from '@google/genai'
22
import { createLogger } from '@sim/logger'
33
import { getErrorMessage } from '@sim/utils/errors'
44
import { sleep } from '@sim/utils/helpers'
5-
import OpenAI, { toFile } from 'openai'
65
import type { StorageContext } from '@/lib/uploads'
76
import { StorageService } from '@/lib/uploads'
87
import { inferContextFromKey } from '@/lib/uploads/utils/file-utils'
@@ -19,6 +18,7 @@ import type { Message, ProviderId, ProviderRequest } from '@/providers/types'
1918

2019
const logger = createLogger('ProviderFileAttachments')
2120

21+
const OPENAI_FILES_ENDPOINT = 'https://api.openai.com/v1/files'
2222
const PRESIGNED_URL_EXPIRY_SECONDS = 60 * 60
2323
/** OpenAI auto-deletes uploaded files after this window — see the "rely on provider expiry" lifecycle. */
2424
const OPENAI_FILE_EXPIRY_SECONDS = 60 * 60
@@ -113,14 +113,13 @@ export async function uploadLargeFilesToProvider(
113113
if (groups.length === 0) return
114114

115115
const maxBytes = getProviderAttachmentMaxBytes(providerId)
116-
const openai = providerId === 'openai' ? new OpenAI({ apiKey: request.apiKey }) : null
117116
const ai = providerId === 'google' ? new GoogleGenAI({ apiKey: request.apiKey }) : null
118117

119118
for (const group of groups) {
120119
const [representative] = group
121120
await assertFileAccessForUpload(representative, request.userId)
122-
if (openai) {
123-
await uploadOpenAIFile(representative, openai, maxBytes, request.abortSignal)
121+
if (providerId === 'openai') {
122+
await uploadOpenAIFile(representative, request.apiKey, maxBytes, request.abortSignal)
124123
} else if (ai) {
125124
await uploadGeminiFile(representative, ai, maxBytes, request.abortSignal)
126125
}
@@ -178,27 +177,44 @@ function groupUploadableFiles(messages: Message[] | undefined): UserFile[][] {
178177
*/
179178
async function downloadFileForUpload(file: UserFile, maxBytes: number): Promise<Blob> {
180179
const buffer = await downloadFileFromStorage(file, 'provider-file-upload', logger, { maxBytes })
181-
return new Blob([buffer], { type: file.type || inferAttachmentMimeType(file) })
180+
return new Blob([new Uint8Array(buffer)], { type: file.type || inferAttachmentMimeType(file) })
182181
}
183182

183+
/**
184+
* Uploads to `POST /v1/files` via multipart directly (not the SDK), because the installed
185+
* `openai` SDK does not type `expires_after`; the bracketed form fields are the documented
186+
* multipart encoding for the nested object and give the file an auto-expiry.
187+
*/
184188
async function uploadOpenAIFile(
185189
file: UserFile,
186-
client: OpenAI,
190+
apiKey: string | undefined,
187191
maxBytes: number,
188192
signal?: AbortSignal
189193
): Promise<void> {
190194
const mimeType = inferAttachmentMimeType(file)
191195
const blob = await downloadFileForUpload(file, maxBytes)
192196

193-
const uploaded = await client.files.create(
194-
{
195-
file: await toFile(blob, file.name, { type: mimeType }),
196-
purpose: mimeType.startsWith('image/') ? 'vision' : 'user_data',
197-
expires_after: { anchor: 'created_at', seconds: OPENAI_FILE_EXPIRY_SECONDS },
198-
},
199-
{ signal }
200-
)
197+
const form = new FormData()
198+
form.append('purpose', mimeType.startsWith('image/') ? 'vision' : 'user_data')
199+
form.append('expires_after[anchor]', 'created_at')
200+
form.append('expires_after[seconds]', String(OPENAI_FILE_EXPIRY_SECONDS))
201+
form.append('file', blob, file.name)
202+
203+
const response = await fetch(OPENAI_FILES_ENDPOINT, {
204+
method: 'POST',
205+
headers: { Authorization: `Bearer ${apiKey}` },
206+
body: form,
207+
signal,
208+
})
209+
if (!response.ok) {
210+
const detail = await response.text().catch(() => '')
211+
throw new Error(`OpenAI file upload failed for "${file.name}" (${response.status}): ${detail}`)
212+
}
201213

214+
const uploaded = (await response.json()) as { id?: string }
215+
if (!uploaded.id) {
216+
throw new Error(`OpenAI file upload for "${file.name}" returned no id`)
217+
}
202218
file.providerFileId = uploaded.id
203219
logger.info(`Uploaded "${file.name}" to OpenAI Files API`, { fileId: uploaded.id })
204220
}

0 commit comments

Comments
 (0)