Files
WeHub Mirror 6bf8bebf51
CI / Test and Build (push) Failing after 1s
CI / Migrate Dev DB (push) Has been skipped
CI / Migrate DB (push) Has been skipped
CodeQL / Analyze actions (push) Has been cancelled
CodeQL / Analyze javascript-typescript (push) Has been cancelled
CI / Detect Version (push) Has been cancelled
CI / Detect Desktop Changes (push) Has been cancelled
CI / Build AMD64 (blacksmith-2vcpu-ubuntu-2404, ./docker/cron.Dockerfile, ubuntu-latest, ghcr.io/simstudioai/cron) (push) Has been cancelled
CI / Build AMD64 (blacksmith-2vcpu-ubuntu-2404, ./docker/db.Dockerfile, ECR_MIGRATIONS, ubuntu-latest, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (blacksmith-4vcpu-ubuntu-2404, ./docker/pii.Dockerfile, ECR_PII, ubuntu-latest, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (blacksmith-4vcpu-ubuntu-2404, ./docker/realtime.Dockerfile, ECR_REALTIME, ubuntu-latest, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build AMD64 (blacksmith-8vcpu-ubuntu-2404, ./docker/app.Dockerfile, ECR_APP, linux-x64-8-core, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (blacksmith-4vcpu-ubuntu-2404-arm, ./docker/cron.Dockerfile, ubuntu-24.04-arm, ghcr.io/simstudioai/cron) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (blacksmith-4vcpu-ubuntu-2404-arm, ./docker/db.Dockerfile, ubuntu-24.04-arm, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (blacksmith-4vcpu-ubuntu-2404-arm, ./docker/pii.Dockerfile, ubuntu-24.04-arm, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (blacksmith-4vcpu-ubuntu-2404-arm, ./docker/realtime.Dockerfile, ubuntu-24.04-arm, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (blacksmith-8vcpu-ubuntu-2404-arm, ./docker/app.Dockerfile, linux-arm64-8-core, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
Publish CLI Package / publish-npm (push) Has been cancelled
Publish Python SDK / publish-pypi (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
Helm Chart / Lint, test, and validate chart (push) Has been cancelled
Helm Chart / Chart version bumped (push) Has been cancelled
Publish TypeScript SDK / publish-npm (push) Has been cancelled
CI / Build Dev ECR (blacksmith-8vcpu-ubuntu-2404, ./docker/app.Dockerfile, ECR_APP, linux-x64-8-core) (push) Has been cancelled
CI / Promote Images (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/cron) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build Dev ECR (blacksmith-2vcpu-ubuntu-2404, ./docker/db.Dockerfile, ECR_MIGRATIONS, ubuntu-latest) (push) Has been cancelled
CI / Build Dev ECR (blacksmith-4vcpu-ubuntu-2404, ./docker/pii.Dockerfile, ECR_PII, ubuntu-latest) (push) Has been cancelled
CI / Build Dev ECR (blacksmith-4vcpu-ubuntu-2404, ./docker/realtime.Dockerfile, ECR_REALTIME, ubuntu-latest) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Check Desktop Signing Secrets (push) Has been cancelled
CI / Desktop Release (push) Has been cancelled
CI / Create Desktop Prerelease (push) Has been cancelled
CI / Desktop Prerelease Build (push) Has been cancelled
CI / Publish Desktop Prerelease (push) Has been cancelled
CI / Prune Desktop Prereleases (push) Has been cancelled
Helm Chart / Install on kind and run helm test (push) Has been cancelled
WeHub snapshot of cb28d14c6f2c081de7a0d8729a8c816c9adef67a
2026-08-10 11:17:50 +08:00

588 lines
18 KiB
TypeScript

/**
* @vitest-environment node
*/
import { describe, expect, it, vi } from 'vitest'
import { LARGE_VALUE_THRESHOLD_BYTES } from '@/lib/execution/payloads/large-value-ref'
import type { UserFile } from '@/executor/types'
import {
buildAnthropicMessageContent,
buildBedrockMessageContent,
buildGeminiMessageParts,
buildOpenAICompatibleChatContent,
buildOpenAIMessageContent,
buildOpenRouterMessageContent,
formatAttachmentSizes,
formatMessagesForProvider,
getProviderAttachmentMaxBytes,
getProviderFileStrategy,
INLINE_ATTACHMENT_THRESHOLD_BYTES,
inferAttachmentMimeType,
isProviderAttachmentFilenameModelBound,
LARGE_FILE_PATH_THRESHOLD_BYTES,
prepareProviderAttachments,
shouldUseLargeFilePath,
} from '@/providers/attachments'
const imageFile: UserFile = {
id: 'file-1',
name: 'example.png',
url: '/api/files/serve/workspace%2Fws-1%2Fexample.png?context=workspace',
size: 128,
type: 'image/png',
key: 'workspace/ws-1/example.png',
base64: 'iVBORw0KGgo=',
}
const pdfFile: UserFile = {
id: 'file-2',
name: 'example.pdf',
url: '/api/files/serve/workspace%2Fws-1%2Fexample.pdf?context=workspace',
size: 256,
type: 'application/pdf',
key: 'workspace/ws-1/example.pdf',
base64: 'cGRm',
}
const markdownFile: UserFile = {
id: 'file-3',
name: 'notes.md',
url: '/api/files/serve/workspace%2Fws-1%2Fnotes.md?context=workspace',
size: 17,
type: 'text/markdown',
key: 'workspace/ws-1/notes.md',
base64: Buffer.from('# Notes\n\nHello').toString('base64'),
}
describe('provider attachments', () => {
it('infers MIME type from filename when file type is generic', () => {
expect(
inferAttachmentMimeType({
...imageFile,
type: 'application/octet-stream',
})
).toBe('image/png')
})
it('infers MIME type from filename when file type is a generated-doc source marker', () => {
expect(
inferAttachmentMimeType({
...pdfFile,
type: 'text/x-python-pdf',
})
).toBe('application/pdf')
})
it('formats OpenAI Responses content with text, image, and file parts', () => {
const content = buildOpenAIMessageContent(
'Analyze these files',
[imageFile, pdfFile, markdownFile],
'openai'
)
expect(content).toEqual([
{ type: 'input_text', text: 'Analyze these files' },
{
type: 'input_image',
image_url: 'data:image/png;base64,iVBORw0KGgo=',
detail: 'auto',
},
{
type: 'input_file',
filename: 'example.pdf',
file_data: 'data:application/pdf;base64,cGRm',
},
{
type: 'input_file',
filename: 'notes.md',
file_data: `data:text/markdown;base64,${markdownFile.base64}`,
},
])
})
it('keeps image filenames raw for MIME inference and does not project unsent names', () => {
const source: UserFile = {
...imageFile,
name: 'secret-image.png',
type: 'application/octet-stream',
}
const projectFilename = vi.fn(() => '{{IMAGE_NAME}}.png')
const content = buildOpenAIMessageContent('Analyze', [source], 'openai', projectFilename)
expect(projectFilename).not.toHaveBeenCalled()
expect(source.name).toBe('secret-image.png')
expect(content).toEqual([
{ type: 'input_text', text: 'Analyze' },
{
type: 'input_image',
image_url: 'data:image/png;base64,iVBORw0KGgo=',
detail: 'auto',
},
])
})
it('treats an image filename as provider-bound only when its upload path is active', () => {
const largeImage = { ...imageFile, size: LARGE_FILE_PATH_THRESHOLD_BYTES + 1 }
expect(isProviderAttachmentFilenameModelBound(largeImage, 'openai')).toBe(false)
expect(
isProviderAttachmentFilenameModelBound(largeImage, 'openai', {
largeFilePathAvailable: true,
})
).toBe(true)
})
it('projects a document filename only after raw MIME and extension inference', () => {
const source: UserFile = {
...pdfFile,
name: 'report.pdf',
type: 'application/octet-stream',
}
const projectFilename = vi.fn(() => '{{FILE_NAME}}.pdf')
const content = buildOpenAIMessageContent('Analyze', [source], 'openai', projectFilename)
expect(projectFilename).toHaveBeenCalledOnce()
expect(projectFilename).toHaveBeenCalledWith('report.pdf', 'pdf')
expect(source.name).toBe('report.pdf')
expect(content).toEqual([
{ type: 'input_text', text: 'Analyze' },
{
type: 'input_file',
filename: '{{FILE_NAME}}.pdf',
file_data: 'data:application/pdf;base64,cGRm',
},
])
})
it('uses a neutral Bedrock document name when projection changes the original', () => {
const content = buildBedrockMessageContent(
'Analyze',
[{ ...markdownFile, name: 'TOKEN.md' }],
'bedrock',
() => '{{TOKEN}}.md'
)
expect(content).toEqual([
{ text: 'Analyze' },
{
document: {
format: 'md',
name: 'Document',
source: { bytes: Buffer.from(markdownFile.base64, 'base64') },
},
},
])
})
it('formats Anthropic content with image, PDF document, and text document blocks', () => {
const content = buildAnthropicMessageContent(
'Analyze these files',
[imageFile, pdfFile, markdownFile],
'anthropic'
)
expect(content).toEqual([
{ type: 'text', text: 'Analyze these files' },
{
type: 'image',
source: {
type: 'base64',
media_type: 'image/png',
data: 'iVBORw0KGgo=',
},
},
{
type: 'document',
source: {
type: 'base64',
media_type: 'application/pdf',
data: 'cGRm',
},
title: 'example.pdf',
},
{
type: 'document',
source: {
type: 'text',
media_type: 'text/plain',
data: '# Notes\n\nHello',
},
title: 'notes.md',
},
])
})
it('formats Gemini content with text and inline data parts', () => {
const parts = buildGeminiMessageParts('Analyze this file', [imageFile, markdownFile], 'google')
expect(parts).toEqual([
{ text: 'Analyze this file' },
{
inlineData: {
mimeType: 'image/png',
data: 'iVBORw0KGgo=',
},
},
{
inlineData: {
mimeType: 'text/plain',
data: markdownFile.base64,
},
},
])
})
it('formats Bedrock content with native document blocks', () => {
const parts = buildBedrockMessageContent('Analyze this file', [markdownFile], 'bedrock')
expect(parts).toEqual([
{ text: 'Analyze this file' },
{
document: {
format: 'md',
name: 'notes',
source: {
bytes: Buffer.from(markdownFile.base64, 'base64'),
},
},
},
])
})
it('formats OpenRouter images and PDFs with native multimodal message parts', () => {
const content = buildOpenRouterMessageContent(
'Analyze these files',
[imageFile, pdfFile],
'openrouter'
)
expect(content).toEqual([
{ type: 'text', text: 'Analyze these files' },
{
type: 'image_url',
image_url: { url: 'data:image/png;base64,iVBORw0KGgo=' },
},
{
type: 'file',
file: {
filename: 'example.pdf',
file_data: 'data:application/pdf;base64,cGRm',
},
},
])
})
it('formats image-only provider messages and strips file fields', () => {
const messages = formatMessagesForProvider(
[{ role: 'user', content: 'Analyze this image', files: [imageFile] }],
'groq'
)
expect(messages).toEqual([
{
role: 'user',
content: [
{ type: 'text', text: 'Analyze this image' },
{
type: 'image_url',
image_url: { url: 'data:image/png;base64,iVBORw0KGgo=' },
},
],
},
])
})
it('fails fast for unsupported MIME types', () => {
expect(() =>
prepareProviderAttachments(
[
{
...imageFile,
name: 'archive.zip',
type: 'application/zip',
},
],
'openai'
)
).toThrow('application/zip')
})
it('sniffs image bytes and corrects a wrong declared image MIME type', () => {
const content = buildAnthropicMessageContent(
'Analyze this image',
[
{
...imageFile,
name: 'wrong.ico',
type: 'image/x-icon',
},
],
'anthropic'
)
expect(content[1]).toEqual({
type: 'image',
source: {
type: 'base64',
media_type: 'image/png',
data: 'iVBORw0KGgo=',
},
})
})
it('rejects image attachments when the bytes are not a supported image format', () => {
expect(() =>
prepareProviderAttachments(
[
{
...imageFile,
name: 'not-an-image.png',
base64: Buffer.from('not an image').toString('base64'),
},
],
'anthropic'
)
).toThrow('not a supported model image format')
})
it('rejects documents for image-only providers', () => {
expect(() =>
formatMessagesForProvider(
[{ role: 'user', content: 'Analyze this file', files: [pdfFile] }],
'groq'
)
).toThrow('Supported attachments: images')
})
it('rejects providers without file attachment support', () => {
expect(() =>
formatMessagesForProvider(
[{ role: 'user', content: 'Analyze this file', files: [imageFile] }],
'deepseek'
)
).toThrow('not supported')
})
})
describe('attachment limit formatting', () => {
/**
* Guards both directions of the unit bug: dividing every ceiling by 1024² reported OpenAI's
* decimal 50 MB as "48MB", and dividing every ceiling by 10⁶ reported Anthropic's 50 MiB as
* "52MB". Each vendor's number must come back as that vendor publishes it.
*/
it('reports each ceiling in the unit its vendor publishes', () => {
expect(formatAttachmentSizes(0, 50_000_000).limit).toBe('50')
expect(formatAttachmentSizes(0, 50 * 1024 * 1024).limit).toBe('50')
expect(formatAttachmentSizes(0, 20 * 1024 * 1024).limit).toBe('20')
expect(formatAttachmentSizes(0, 25 * 1024 * 1024).limit).toBe('25')
expect(formatAttachmentSizes(0, 10 * 1024 * 1024).limit).toBe('10')
})
/**
* Exercises `limit + 1` for every ceiling in the registry, which is the only input that can
* expose this: rounding both figures to the nearest hundredth rendered a file one byte over a
* 20 MiB cap as "20.00MB exceeds the 20MB limit". The previous version of this test asserted
* a file 0.03MB over and an openai file *under* the limit, so it passed while that was live.
*/
it('never renders an over-limit file as equal to the limit', () => {
const ceilings = [
50 * 1024 * 1024,
25 * 1024 * 1024,
20 * 1024 * 1024,
10 * 1024 * 1024,
6 * 1024 * 1024,
50_000_000,
]
for (const limit of ceilings) {
const justOver = formatAttachmentSizes(limit + 1, limit)
expect(justOver.size).not.toBe(justOver.limit)
}
/**
* Every ceiling above divides to an exact integer, so floor/round/ceil are indistinguishable
* on them — only a ceiling with a fractional remainder pins the limit-side rounding.
*/
const fractional = formatAttachmentSizes(12_345_679, 12_345_678)
expect(fractional.size).not.toBe(fractional.limit)
})
it('keeps a comfortably over-limit size readable', () => {
const groq = formatAttachmentSizes(21_000_000, 20 * 1024 * 1024)
expect(groq.limit).toBe('20')
expect(groq.size).toBe('20.03')
})
})
describe('provider large-file capability', () => {
/**
* Guards the regression where every 6-10 MB attachment died with "Execution memory limit
* exceeded": past this size the base64 copy no longer fits the payload store, so an upload
* has to take over wherever one is reachable.
*/
it('starts preferring an upload before base64 outgrows the payload store', () => {
const encodedBytes = Math.ceil(LARGE_FILE_PATH_THRESHOLD_BYTES / 3) * 4
expect(encodedBytes).toBeLessThanOrEqual(LARGE_VALUE_THRESHOLD_BYTES)
expect(LARGE_FILE_PATH_THRESHOLD_BYTES).toBeLessThan(INLINE_ATTACHMENT_THRESHOLD_BYTES)
})
/**
* A `remote-url` provider only fetches images and PDFs, so it must not take over from base64
* early — text documents in the 6-10 MB band inline fine today and would start failing.
*/
/** A size we cannot read must still reach the uploader, which enforces the ceiling itself. */
it('routes an unknown-size file to a files-api upload rather than stranding it', () => {
const unknown = { size: 0, type: 'text/csv' }
expect(shouldUseLargeFilePath(unknown, 'openai')).toBe(true)
expect(shouldUseLargeFilePath(unknown, 'anthropic')).toBe(false)
expect(shouldUseLargeFilePath({ size: Number.NaN, type: 'text/csv' }, 'openai')).toBe(true)
})
it('crosses over to an upload at different sizes for files-api and remote-url', () => {
const midBand = { size: LARGE_FILE_PATH_THRESHOLD_BYTES + 1, type: 'text/plain' }
expect(shouldUseLargeFilePath(midBand, 'openai')).toBe(true)
expect(shouldUseLargeFilePath(midBand, 'anthropic')).toBe(false)
const aboveInline = { size: INLINE_ATTACHMENT_THRESHOLD_BYTES + 1, type: 'application/pdf' }
expect(shouldUseLargeFilePath(aboveInline, 'anthropic')).toBe(true)
})
it('reports per-provider strategy and ceiling, defaulting others to inline', () => {
expect(getProviderFileStrategy('openai')).toBe('files-api')
expect(getProviderFileStrategy('google')).toBe('files-api')
expect(getProviderFileStrategy('anthropic')).toBe('remote-url')
expect(getProviderFileStrategy('groq')).toBe('remote-url')
expect(getProviderFileStrategy('bedrock')).toBe('inline')
expect(getProviderFileStrategy('azure-openai')).toBe('inline')
expect(getProviderFileStrategy('vertex')).toBe('inline')
expect(getProviderAttachmentMaxBytes('openai')).toBeGreaterThan(
INLINE_ATTACHMENT_THRESHOLD_BYTES
)
expect(getProviderAttachmentMaxBytes('bedrock')).toBe(INLINE_ATTACHMENT_THRESHOLD_BYTES)
expect(getProviderAttachmentMaxBytes('azure-openai')).toBe(INLINE_ATTACHMENT_THRESHOLD_BYTES)
})
it('routes only oversized files on capable providers to the large-file path', () => {
const small = { ...imageFile, size: 1024 }
const large = { ...imageFile, size: LARGE_FILE_PATH_THRESHOLD_BYTES + 1 }
expect(shouldUseLargeFilePath(small, 'openai')).toBe(false)
expect(shouldUseLargeFilePath(large, 'openai')).toBe(true)
expect(shouldUseLargeFilePath(large, 'bedrock')).toBe(false)
})
it('does not expose generated source through a remote-url large-file path', () => {
const generated = {
...pdfFile,
size: LARGE_FILE_PATH_THRESHOLD_BYTES + 1,
type: 'text/x-python-pdf',
}
expect(shouldUseLargeFilePath(generated, 'openai')).toBe(true)
expect(shouldUseLargeFilePath(generated, 'anthropic')).toBe(false)
})
it('references uploaded OpenAI files by file_id instead of inlining base64', () => {
const content = buildOpenAIMessageContent(
'Analyze',
[
{ ...imageFile, base64: undefined, providerFileId: 'file-img' },
{ ...pdfFile, base64: undefined, providerFileId: 'file-doc' },
],
'openai'
)
expect(content).toEqual([
{ type: 'input_text', text: 'Analyze' },
{ type: 'input_image', file_id: 'file-img', detail: 'auto' },
{ type: 'input_file', file_id: 'file-doc' },
])
})
it('references large Anthropic files via url content-block sources', () => {
const content = buildAnthropicMessageContent(
'Analyze',
[
{ ...imageFile, base64: undefined, remoteUrl: 'https://signed/img.png' },
{ ...pdfFile, base64: undefined, remoteUrl: 'https://signed/doc.pdf' },
],
'anthropic'
)
expect(content).toEqual([
{ type: 'text', text: 'Analyze' },
{ type: 'image', source: { type: 'url', url: 'https://signed/img.png' } },
{
type: 'document',
source: { type: 'url', url: 'https://signed/doc.pdf' },
title: 'example.pdf',
},
])
})
it('references uploaded Gemini files via fileData uri', () => {
const parts = buildGeminiMessageParts(
'Analyze',
[{ ...imageFile, base64: undefined, providerFileUri: 'https://files/abc' }],
'google'
)
expect(parts).toEqual([
{ text: 'Analyze' },
{ fileData: { fileUri: 'https://files/abc', mimeType: 'image/png' } },
])
})
it('passes a remote url to OpenAI-compatible providers instead of a data url', () => {
const content = buildOpenAICompatibleChatContent(
'Analyze',
[{ ...imageFile, base64: undefined, remoteUrl: 'https://signed/img.png' }],
'groq'
)
expect(content).toEqual([
{ type: 'text', text: 'Analyze' },
{ type: 'image_url', image_url: { url: 'https://signed/img.png' } },
])
})
it('rejects oversized non-PDF text documents on Anthropic (url source supports PDFs/images only)', () => {
expect(() =>
buildAnthropicMessageContent(
'Analyze',
[
{
...markdownFile,
type: 'text/csv',
name: 'data.csv',
base64: undefined,
remoteUrl: 'https://signed/data.csv',
},
],
'anthropic'
)
).toThrow('Only PDFs and images are supported')
})
it('references large Anthropic PDFs via a url document source', () => {
const content = buildAnthropicMessageContent(
'Analyze',
[{ ...pdfFile, base64: undefined, remoteUrl: 'https://signed/doc.pdf' }],
'anthropic'
)
expect(content).toEqual([
{ type: 'text', text: 'Analyze' },
{
type: 'document',
source: { type: 'url', url: 'https://signed/doc.pdf' },
title: 'example.pdf',
},
])
})
it('rejects files above the provider ceiling', () => {
const huge = {
...imageFile,
size: getProviderAttachmentMaxBytes('openai') + 1,
base64: undefined,
providerFileId: 'file-img',
}
expect(() => buildOpenAIMessageContent('Analyze', [huge], 'openai')).toThrow('exceeds the')
})
})