diff --git a/packages/api/src/files/encode/document.spec.ts b/packages/api/src/files/encode/document.spec.ts index 15c2e3b716..62bd974075 100644 --- a/packages/api/src/files/encode/document.spec.ts +++ b/packages/api/src/files/encode/document.spec.ts @@ -888,6 +888,60 @@ describe('encodeAndFormatDocuments - fileConfig integration', () => { expect(mockedGetFileStream).not.toHaveBeenCalled(); }); + it('should apply Claude document restrictions through an OpenAI-compatible gateway', async () => { + const req = createMockRequest(30) as ServerRequest; + const file = createMockDocFile( + 1, + 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + 'report.xlsx', + ); + + const result = await encodeAndFormatDocuments( + req, + [file], + { provider: Providers.OPENAI, model: 'anthropic/claude-sonnet-4-6' }, + mockStrategyFunctions, + ); + + expect(result.documents).toHaveLength(0); + expect(result.files).toHaveLength(0); + expect(mockedGetFileStream).not.toHaveBeenCalled(); + }); + + it('should retain XLSX support for non-Claude OpenAI models', async () => { + const req = createMockRequest(30) as ServerRequest; + const file = createMockDocFile( + 1, + 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + 'report.xlsx', + ); + const mockContent = Buffer.from('xlsx-content').toString('base64'); + mockedGetFileStream.mockResolvedValue({ + file, + content: mockContent, + metadata: file, + }); + + const result = await encodeAndFormatDocuments( + req, + [file], + { provider: Providers.OPENAI, model: 'gpt-5.4' }, + mockStrategyFunctions, + ); + + expect(result.documents).toMatchObject([ + { + type: 'file', + file: { + filename: 'report.xlsx', + file_data: `data:${file.type};base64,${mockContent}`, + }, + }, + ]); + expect(result.files).toEqual([file]); + expect(mockedGetFileStream).toHaveBeenCalledTimes(1); + }); + it('should still encode supported Anthropic documents when mixed with unsupported ones', async () => { const req = createMockRequest(30) as ServerRequest; const docxFile = createMockDocFile(1, 'application/vnd.ms-excel', 'sheet.xls'); diff --git a/packages/api/src/files/encode/document.ts b/packages/api/src/files/encode/document.ts index 7ef4b684e5..b225841258 100644 --- a/packages/api/src/files/encode/document.ts +++ b/packages/api/src/files/encode/document.ts @@ -106,15 +106,24 @@ function formatDocumentBlock( /** * Filters out files the provider's document path cannot send to the model. - * Anthropic rejects non-PDF base64 documents with a 400 that recurs on every - * retry, so unsupported types are skipped instead of bricking the conversation. + * Claude rejects non-PDF binary documents with a 400 that recurs on every retry, + * including when it is reached through an OpenAI-compatible gateway. Unsupported + * types are skipped instead of bricking the conversation. */ -function filterProviderDocumentFiles(provider: Providers, files: IMongoFile[]): IMongoFile[] { +function filterProviderDocumentFiles( + provider: Providers, + files: IMongoFile[], + model?: string, +): IMongoFile[] { if (provider === Providers.BEDROCK) { return files.filter((file) => isBedrockDocumentType(file.type)); } - if (provider !== Providers.ANTHROPIC) { + const usesAnthropicDocumentCapabilities = + provider === Providers.ANTHROPIC || + (isOpenAILikeProvider(provider) && model?.toLowerCase().includes('claude')); + + if (!usesAnthropicDocumentCapabilities) { return files; } @@ -130,7 +139,7 @@ function filterProviderDocumentFiles(provider: Providers, files: IMongoFile[]): if (skipped.length) { console.warn( - `Skipping attachment(s) unsupported by Anthropic document input: ${skipped.join(', ')}`, + `Skipping attachment(s) unsupported by Claude document input: ${skipped.join(', ')}`, ); } @@ -182,7 +191,7 @@ export async function encodeAndFormatDocuments( return result; } - const processableFiles = filterProviderDocumentFiles(provider, files); + const processableFiles = filterProviderDocumentFiles(provider, files, model); if (!processableFiles.length) { return result;