Skip to content

Commit 7978031

Browse files
committed
fix(providers): correct OpenAI expiry serialization and Anthropic large-text-doc handling
- OpenAI upload now uses the SDK (client.files.create) so expires_after is serialized as a real nested object; the prior expires_after[anchor] bracket FormData keys were ignored by OpenAI's server, leaving files un-expiring. - Anthropic url document source only supports PDFs/images; large non-PDF text docs now throw a clear error instead of emitting an unsupported url source. - Warn when an oversized file can't be sent because cloud storage is unavailable.
1 parent 6acb47e commit 7978031

3 files changed

Lines changed: 58 additions & 25 deletions

File tree

apps/sim/providers/attachments.test.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -361,6 +361,40 @@ describe('provider large-file capability', () => {
361361
])
362362
})
363363

364+
it('rejects oversized non-PDF text documents on Anthropic (url source supports PDFs/images only)', () => {
365+
expect(() =>
366+
buildAnthropicMessageContent(
367+
'Analyze',
368+
[
369+
{
370+
...markdownFile,
371+
type: 'text/csv',
372+
name: 'data.csv',
373+
base64: undefined,
374+
remoteUrl: 'https://signed/data.csv',
375+
},
376+
],
377+
'anthropic'
378+
)
379+
).toThrow('Only PDFs and images are supported')
380+
})
381+
382+
it('references large Anthropic PDFs via a url document source', () => {
383+
const content = buildAnthropicMessageContent(
384+
'Analyze',
385+
[{ ...pdfFile, base64: undefined, remoteUrl: 'https://signed/doc.pdf' }],
386+
'anthropic'
387+
)
388+
expect(content).toEqual([
389+
{ type: 'text', text: 'Analyze' },
390+
{
391+
type: 'document',
392+
source: { type: 'url', url: 'https://signed/doc.pdf' },
393+
title: 'example.pdf',
394+
},
395+
])
396+
})
397+
364398
it('rejects files above the provider ceiling', () => {
365399
const huge = {
366400
...imageFile,

apps/sim/providers/attachments.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -484,6 +484,11 @@ export function buildAnthropicMessageContent(
484484
} satisfies Anthropic.Messages.Base64ImageSource),
485485
} satisfies Anthropic.Messages.ImageBlockParam)
486486
} else if (attachment.remoteUrl) {
487+
if (attachment.mimeType !== PDF_MIME_TYPE) {
488+
throw new Error(
489+
`Document "${attachment.filename}" (${attachment.mimeType}) is too large to send to provider "${providerId}". Only PDFs and images are supported above the inline limit — convert it to PDF or reduce its size.`
490+
)
491+
}
487492
parts.push({
488493
type: 'document',
489494
source: { type: 'url', url: attachment.remoteUrl },

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

Lines changed: 19 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ 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'
56
import type { StorageContext } from '@/lib/uploads'
67
import { StorageService } from '@/lib/uploads'
78
import { inferContextFromKey } from '@/lib/uploads/utils/file-utils'
@@ -16,7 +17,6 @@ import type { Message, ProviderId, ProviderRequest } from '@/providers/types'
1617

1718
const logger = createLogger('ProviderFileAttachments')
1819

19-
const OPENAI_FILES_ENDPOINT = 'https://api.openai.com/v1/files'
2020
const PRESIGNED_URL_EXPIRY_SECONDS = 60 * 60
2121
/** OpenAI auto-deletes uploaded files after this window — see the "rely on provider expiry" lifecycle. */
2222
const OPENAI_FILE_EXPIRY_SECONDS = 60 * 60
@@ -54,7 +54,12 @@ export async function attachLargeFileRemoteUrls(
5454

5555
for (const file of files) {
5656
if (!file.key || !shouldUseLargeFilePath(file, providerId)) continue
57-
if (!StorageService.hasCloudStorage()) continue
57+
if (!StorageService.hasCloudStorage()) {
58+
logger.warn(
59+
`[${ctx.requestId}] "${file.name}" exceeds the inline limit for "${providerId}" but cloud storage is unavailable; it cannot be sent`
60+
)
61+
continue
62+
}
5863

5964
if (!ctx.userId) {
6065
throw new Error(
@@ -90,12 +95,13 @@ export async function uploadLargeFilesToProvider(
9095
const groups = groupUploadableFiles(request.messages)
9196
if (groups.length === 0) return
9297

98+
const openai = providerId === 'openai' ? new OpenAI({ apiKey: request.apiKey }) : null
9399
const ai = providerId === 'google' ? new GoogleGenAI({ apiKey: request.apiKey }) : null
94100

95101
for (const group of groups) {
96102
const [representative] = group
97-
if (providerId === 'openai') {
98-
await uploadOpenAIFile(representative, request.apiKey, request.abortSignal)
103+
if (openai) {
104+
await uploadOpenAIFile(representative, openai, request.abortSignal)
99105
} else if (ai) {
100106
await uploadGeminiFile(representative, ai, request.abortSignal)
101107
}
@@ -134,33 +140,21 @@ async function fetchRemoteFileBlob(file: UserFile, signal?: AbortSignal): Promis
134140

135141
async function uploadOpenAIFile(
136142
file: UserFile,
137-
apiKey: string | undefined,
143+
client: OpenAI,
138144
signal?: AbortSignal
139145
): Promise<void> {
140146
const mimeType = inferAttachmentMimeType(file)
141147
const blob = await fetchRemoteFileBlob(file, signal)
142148

143-
const form = new FormData()
144-
form.append('purpose', mimeType.startsWith('image/') ? 'vision' : 'user_data')
145-
form.append('expires_after[anchor]', 'created_at')
146-
form.append('expires_after[seconds]', String(OPENAI_FILE_EXPIRY_SECONDS))
147-
form.append('file', blob, file.name)
148-
149-
const response = await fetch(OPENAI_FILES_ENDPOINT, {
150-
method: 'POST',
151-
headers: { Authorization: `Bearer ${apiKey}` },
152-
body: form,
153-
signal,
154-
})
155-
if (!response.ok) {
156-
const detail = await response.text().catch(() => '')
157-
throw new Error(`OpenAI file upload failed for "${file.name}" (${response.status}): ${detail}`)
158-
}
149+
const uploaded = await client.files.create(
150+
{
151+
file: await toFile(blob, file.name, { type: mimeType }),
152+
purpose: mimeType.startsWith('image/') ? 'vision' : 'user_data',
153+
expires_after: { anchor: 'created_at', seconds: OPENAI_FILE_EXPIRY_SECONDS },
154+
},
155+
{ signal }
156+
)
159157

160-
const uploaded = (await response.json()) as { id?: string }
161-
if (!uploaded.id) {
162-
throw new Error(`OpenAI file upload for "${file.name}" returned no id`)
163-
}
164158
file.providerFileId = uploaded.id
165159
logger.info(`Uploaded "${file.name}" to OpenAI Files API`, { fileId: uploaded.id })
166160
}

0 commit comments

Comments
 (0)