Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
d28a752
feat(providers): support large agent-block attachments via Files APIs…
waleedlatif1 Jun 16, 2026
6acb47e
fix(providers): clear forged file handles for inline providers too
waleedlatif1 Jun 16, 2026
7978031
fix(providers): correct OpenAI expiry serialization and Anthropic lar…
waleedlatif1 Jun 16, 2026
48f79a4
fix(providers): harden large-file path (SSRF fetch, ceiling gate, per…
waleedlatif1 Jun 16, 2026
9eda861
refactor(providers): read files-api upload bytes via storage SDK
waleedlatif1 Jun 16, 2026
e47d983
docs(providers): clarify files-api bytes are read from storage at upl…
waleedlatif1 Jun 16, 2026
d86041b
fix(providers): enforce access checks and strip forged ids in the upl…
waleedlatif1 Jun 16, 2026
9c4ad83
fix(providers): resolve UI attachment limit with the same model->prov…
waleedlatif1 Jun 16, 2026
fc9d1cd
test(providers): add new models.ts exports to provider mocks
waleedlatif1 Jun 16, 2026
d8e1c25
fix(providers): guard Gemini upload response name before polling
waleedlatif1 Jun 16, 2026
4097df2
fix(uploads): type the file-handle key list so omit preserves UserFil…
waleedlatif1 Jun 16, 2026
0b2e3a8
refactor(providers): run handle-clear + URL-mint in executeProviderRe…
waleedlatif1 Jun 16, 2026
049ec39
fix(azure-openai): guard optional attachment dataUrl in inline image …
waleedlatif1 Jun 16, 2026
0e692f6
fix(providers): upload OpenAI files via multipart and fix Buffer Blob…
waleedlatif1 Jun 16, 2026
1919059
fix(providers): forward userId from the providers API to executeProvi…
waleedlatif1 Jun 16, 2026
68849dd
fix(providers): fail clearly when a large attachment has no cloud sto…
waleedlatif1 Jun 16, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 11 additions & 3 deletions apps/sim/app/(landing)/models/(shell)/[provider]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
import { ModelTimelineChart } from '@/app/(landing)/models/components/model-timeline-chart'
import {
buildProviderFaqs,
formatFileSize,
formatPrice,
formatTokenCount,
getProviderBySlug,
Expand Down Expand Up @@ -204,9 +205,16 @@ export default async function ProviderModelsPage({
{provider.name} models
</h1>
</div>
<span className='shrink-0 font-martian-mono text-[var(--landing-text-subtle)] text-xs uppercase tracking-[0.1em]'>
{provider.modelCount} models
</span>
<div className='flex shrink-0 flex-col items-end gap-1'>
<span className='font-martian-mono text-[var(--landing-text-subtle)] text-xs uppercase tracking-[0.1em]'>
{provider.modelCount} models
</span>
{provider.maxFileAttachmentBytes ? (
<span className='font-martian-mono text-[var(--landing-text-subtle)] text-xs uppercase tracking-[0.1em]'>
{formatFileSize(provider.maxFileAttachmentBytes)} file uploads
</span>
) : null}
</div>
</div>
</div>

Expand Down
15 changes: 15 additions & 0 deletions apps/sim/app/(landing)/models/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,8 @@ export interface CatalogProvider {
color?: string
isReseller: boolean
contextInformationAvailable: boolean
/** Max agent-block file attachment size in bytes when the provider exceeds the default. */
maxFileAttachmentBytes: number | null
providerCapabilityTags: string[]
modelCount: number
models: CatalogModel[]
Expand All @@ -150,6 +152,18 @@ export function formatTokenCount(value?: number | null): string {
return value.toLocaleString('en-US')
}

export function formatFileSize(bytes?: number | null): string {
if (bytes == null) {
return 'Unknown'
}

const gb = bytes / (1024 * 1024 * 1024)
if (gb >= 1) {
return `${trimTrailingZeros(gb.toFixed(1))}GB`
}
return `${Math.round(bytes / (1024 * 1024))}MB`
}

export function formatPrice(price?: number | null): string {
if (price === undefined || price === null) {
return 'N/A'
Expand Down Expand Up @@ -507,6 +521,7 @@ const rawProviders = Object.values(PROVIDER_DEFINITIONS).map((provider) => {
color: provider.color,
isReseller: provider.isReseller ?? false,
contextInformationAvailable: provider.contextInformationAvailable !== false,
maxFileAttachmentBytes: provider.fileAttachment?.maxBytes ?? null,
providerCapabilityTags,
modelCount: models.length,
models,
Expand Down
1 change: 1 addition & 0 deletions apps/sim/app/api/providers/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
responseFormat,
workflowId,
workspaceId,
userId: auth.userId,
stream,
messages,
environmentVariables,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ import {
useWorkspaceFiles,
workspaceFilesKeys,
} from '@/hooks/queries/workspace-files'
import { getProviderAttachmentMaxBytes } from '@/providers/attachments'
import { getProviderFromModel } from '@/providers/utils'
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
import { useWorkflowStore } from '@/stores/workflows/workflow/store'

Expand Down Expand Up @@ -167,6 +169,7 @@ export function FileUpload({
}: FileUploadProps) {
const activeSearchTarget = useActiveSearchTarget()
const [storeValue, setStoreValue] = useSubBlockValue(blockId, subBlockId)
const [modelValue] = useSubBlockValue(blockId, 'model')
const [uploadingFiles, setUploadingFiles] = useState<UploadingFile[]>([])
const [uploadProgress, setUploadProgress] = useState(0)
const [uploadError, setUploadError] = useState<string | null>(null)
Expand All @@ -191,6 +194,17 @@ export function FileUpload({

const value = isPreview ? previewValue : storeValue

const maxSizeInBytes = useMemo(() => {
const fallback = maxSize * 1024 * 1024
if (typeof modelValue !== 'string' || !modelValue) return fallback
try {
return Math.max(fallback, getProviderAttachmentMaxBytes(getProviderFromModel(modelValue)))
} catch {
return fallback
}
}, [modelValue, maxSize])
Comment thread
waleedlatif1 marked this conversation as resolved.
Comment thread
waleedlatif1 marked this conversation as resolved.
const maxSizeLabel = `${Math.round(maxSizeInBytes / (1024 * 1024))}MB`

/**
* Checks if a file's MIME type matches the accepted types
* Supports exact matches, wildcard patterns (e.g., 'image/*'), and '*' for all types
Expand Down Expand Up @@ -278,25 +292,19 @@ export function FileUpload({
const files = e.target.files
if (!files || files.length === 0) return

const existingFiles = Array.isArray(value) ? value : value ? [value] : []
const existingTotalSize = existingFiles.reduce((sum, file) => sum + file.size, 0)

const maxSizeInBytes = maxSize * 1024 * 1024
const validFiles: File[] = []
let totalNewSize = 0
let sizeExceededFile: string | null = null

for (let i = 0; i < files.length; i++) {
const file = files[i]
if (existingTotalSize + totalNewSize + file.size > maxSizeInBytes) {
const errorMessage = `Adding ${file.name} would exceed the maximum size limit of ${maxSize}MB`
if (file.size > maxSizeInBytes) {
const errorMessage = `${file.name} exceeds the maximum file size of ${maxSizeLabel}`
logger.error(errorMessage, activeWorkflowId)
if (!sizeExceededFile) {
sizeExceededFile = errorMessage
}
} else {
validFiles.push(file)
totalNewSize += file.size
}
}

Expand Down
4 changes: 4 additions & 0 deletions apps/sim/blocks/utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,10 @@ vi.mock('@/lib/core/config/env-flags', () => ({
}))

vi.mock('@/providers/models', () => ({
getProviderFileAttachment: vi
.fn()
.mockReturnValue({ maxBytes: 10 * 1024 * 1024, strategy: 'inline' }),
INLINE_ATTACHMENT_MAX_BYTES: 10 * 1024 * 1024,
getHostedModels: mockGetHostedModels,
getProviderModels: mockGetProviderModels,
getProviderIcon: mockGetProviderIcon,
Expand Down
12 changes: 9 additions & 3 deletions apps/sim/executor/handlers/agent/agent-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,11 @@ import { buildAPIUrl, buildAuthHeaders } from '@/executor/utils/http'
import { stringifyJSON } from '@/executor/utils/json'
import { resolveVertexCredential } from '@/executor/utils/vertex-credential'
import { executeProviderRequest } from '@/providers'
import { getProviderAttachmentMaxBytes, supportsFileAttachments } from '@/providers/attachments'
import {
INLINE_ATTACHMENT_THRESHOLD_BYTES,
shouldUseLargeFilePath,
supportsFileAttachments,
} from '@/providers/attachments'
import { getProviderFromModel, transformBlockTool } from '@/providers/utils'
import type { SerializedBlock } from '@/serializer/types'
import { filterSchemaForLLM, type ToolSchema } from '@/tools/params'
Expand Down Expand Up @@ -760,10 +764,12 @@ export class AgentBlockHandler implements BlockHandler {
allowLargeValueWorkflowScope: ctx.allowLargeValueWorkflowScope,
userId: ctx.userId,
logger,
maxBytes: getProviderAttachmentMaxBytes(providerId),
maxBytes: INLINE_ATTACHMENT_THRESHOLD_BYTES,
})

const missingFile = hydratedFiles.find((file) => !file.base64)
const missingFile = hydratedFiles.find(
(file) => !file.base64 && !shouldUseLargeFilePath(file, providerId)
)
if (missingFile) {
throw new Error(
`File "${missingFile.name}" could not be read for provider "${providerId}". The file may exceed the attachment size limit or may no longer be accessible.`
Expand Down
6 changes: 6 additions & 0 deletions apps/sim/executor/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,12 @@ export interface UserFile {
key: string
context?: string
base64?: string
/** Provider Files API handle (OpenAI/Anthropic `file_...` id) set when a large file is uploaded instead of inlined as base64. */
providerFileId?: string
/** Provider File API uri (Gemini `fileUri`) set when a large file is uploaded instead of inlined as base64. */
providerFileUri?: string
/** Short-lived signed HTTPS URL passed to providers that fetch attachments by remote URL instead of inlining base64. */
remoteUrl?: string
}

export interface ParallelPauseScope {
Expand Down
4 changes: 4 additions & 0 deletions apps/sim/lib/api-key/byok.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,10 @@ vi.mock('@/lib/core/config/env-flags', () => ({
}))

vi.mock('@/providers/models', () => ({
getProviderFileAttachment: vi
.fn()
.mockReturnValue({ maxBytes: 10 * 1024 * 1024, strategy: 'inline' }),
INLINE_ATTACHMENT_MAX_BYTES: 10 * 1024 * 1024,
getHostedModels: vi.fn(() => []),
}))

Expand Down
35 changes: 34 additions & 1 deletion apps/sim/lib/uploads/utils/file-utils.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,16 @@
/**
* @vitest-environment node
*/
import { createLogger } from '@sim/logger'
import { describe, expect, it } from 'vitest'
import { isAbortError, isInternalFileUrl, isNetworkError } from '@/lib/uploads/utils/file-utils'
import {
isAbortError,
isInternalFileUrl,
isNetworkError,
processSingleFileToUserFile,
} from '@/lib/uploads/utils/file-utils'

const logger = createLogger('FileUtilsTest')

describe('isInternalFileUrl', () => {
it('classifies relative serve paths as internal', () => {
Expand Down Expand Up @@ -72,3 +80,28 @@ describe('isNetworkError', () => {
expect(isNetworkError(null)).toBe(false)
})
})

describe('processSingleFileToUserFile', () => {
it('strips server-only provider file handles from untrusted input', () => {
const result = processSingleFileToUserFile(
{
id: 'file-1',
name: 'doc.pdf',
url: '/api/files/serve/workspace%2Fws-1%2Fdoc.pdf?context=workspace',
size: 1024,
type: 'application/pdf',
key: 'workspace/ws-1/doc.pdf',
providerFileId: 'file-injected',
providerFileUri: 'https://injected/uri',
remoteUrl: 'http://169.254.169.254/latest/meta-data',
} as never,
'req-1',
logger
)

expect(result.providerFileId).toBeUndefined()
expect(result.providerFileUri).toBeUndefined()
expect(result.remoteUrl).toBeUndefined()
expect(result.key).toBe('workspace/ws-1/doc.pdf')
})
})
13 changes: 12 additions & 1 deletion apps/sim/lib/uploads/utils/file-utils.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { Logger } from '@sim/logger'
import { omit } from '@sim/utils/object'
import type { StorageContext } from '@/lib/uploads'
import { ACCEPTED_FILE_TYPES, SUPPORTED_DOCUMENT_EXTENSIONS } from '@/lib/uploads/utils/validation'
import { isUuid } from '@/executor/constants'
Expand Down Expand Up @@ -696,13 +697,23 @@ function resolveInternalFileUrl(file: RawFileInput): string {
return ''
}

/**
* Provider large-file handles are populated by the server pipeline and must never be
* accepted from untrusted file input (they drive server-side fetch/upload).
*/
const PROVIDER_FILE_HANDLE_FIELDS: Array<'providerFileId' | 'providerFileUri' | 'remoteUrl'> = [
'providerFileId',
'providerFileUri',
'remoteUrl',
]

/**
* Core conversion logic from RawFileInput to UserFile
*/
function convertToUserFile(file: RawFileInput, requestId: string, logger: Logger): UserFile | null {
if (isCompleteUserFile(file)) {
return {
...file,
...omit(file, PROVIDER_FILE_HANDLE_FIELDS),
url: resolveInternalFileUrl(file) || file.url,
}
}
Expand Down
Loading
Loading