LibreChat/packages/data-provider/src/file-config.spec.ts
Danny Avila 046e92217f
🧩 feat: OpenDocument Format File Upload and Native ODS Parsing (#11959)
*  feat: Add support for OpenDocument MIME types in file configuration

Updated the applicationMimeTypes regex to include support for OASIS OpenDocument formats, enhancing the file type recognition capabilities of the data provider.

* feat: document processing with OpenDocument support

Added support for OpenDocument Spreadsheet (ODS) MIME type in the file processing service and updated the document parser to handle ODS files. Included tests to verify correct parsing of ODS documents and updated file configuration to recognize OpenDocument formats.

* refactor: Enhance document processing to support additional Excel MIME types

Updated the document processing logic to utilize a regex for matching Excel MIME types, improving flexibility in handling various Excel file formats. Added tests to ensure correct parsing of new MIME types, including multiple Excel variants and OpenDocument formats. Adjusted file configuration to include these MIME types for better recognition in the file processing service.

* feat: Add support for additional OpenDocument MIME types in file processing

Enhanced the document processing service to support ODT, ODP, and ODG MIME types. Updated tests to verify correct routing through the OCR strategy for these new formats. Adjusted documentation to reflect changes in handled MIME types for improved clarity.
2026-02-26 14:39:49 -05:00

1210 lines
34 KiB
TypeScript

import type { FileConfig } from './types/files';
import {
fileConfig as baseFileConfig,
getEndpointFileConfig,
mergeFileConfig,
applicationMimeTypes,
defaultOCRMimeTypes,
documentParserMimeTypes,
supportedMimeTypes,
} from './file-config';
import { EModelEndpoint } from './schemas';
describe('applicationMimeTypes', () => {
const odfTypes = [
'application/vnd.oasis.opendocument.text',
'application/vnd.oasis.opendocument.spreadsheet',
'application/vnd.oasis.opendocument.presentation',
'application/vnd.oasis.opendocument.graphics',
];
it.each(odfTypes)('matches ODF type: %s', (mimeType) => {
expect(applicationMimeTypes.test(mimeType)).toBe(true);
});
const existingTypes = [
'application/pdf',
'application/json',
'application/csv',
'application/msword',
'application/xml',
'application/zip',
'application/epub+zip',
'application/x-tar',
'application/x-sh',
'application/typescript',
'application/sql',
'application/yaml',
'application/x-parquet',
'application/vnd.apache.parquet',
'application/vnd.coffeescript',
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'application/vnd.openxmlformats-officedocument.presentationml.presentation',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
];
it.each(existingTypes)('matches existing type: %s', (mimeType) => {
expect(applicationMimeTypes.test(mimeType)).toBe(true);
});
const invalidTypes = [
'application/vnd.oasis.opendocument.text-template',
'application/vnd.oasis.opendocument.texts',
'application/vnd.oasis.opendocument.chart',
'application/vnd.oasis.opendocument.formula',
'application/vnd.oasis.opendocument.image',
'application/vnd.oasis.opendocument.text-master',
'text/plain',
'image/png',
];
it.each(invalidTypes)('does not match invalid type: %s', (mimeType) => {
expect(applicationMimeTypes.test(mimeType)).toBe(false);
});
});
describe('defaultOCRMimeTypes', () => {
const checkOCRType = (mimeType: string): boolean =>
defaultOCRMimeTypes.some((regex) => regex.test(mimeType));
it.each([
'application/vnd.oasis.opendocument.text',
'application/vnd.oasis.opendocument.spreadsheet',
'application/vnd.oasis.opendocument.presentation',
'application/vnd.oasis.opendocument.graphics',
])('matches ODF type for OCR: %s', (mimeType) => {
expect(checkOCRType(mimeType)).toBe(true);
});
});
describe('supportedMimeTypes', () => {
const checkSupported = (mimeType: string): boolean =>
supportedMimeTypes.some((regex) => regex.test(mimeType));
it.each([
'application/vnd.oasis.opendocument.text',
'application/vnd.oasis.opendocument.spreadsheet',
'application/vnd.oasis.opendocument.presentation',
'application/vnd.oasis.opendocument.graphics',
])('ODF type flows through supportedMimeTypes: %s', (mimeType) => {
expect(checkSupported(mimeType)).toBe(true);
});
});
describe('documentParserMimeTypes', () => {
const check = (mimeType: string): boolean =>
documentParserMimeTypes.some((regex) => regex.test(mimeType));
it.each([
'application/pdf',
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
'application/vnd.ms-excel',
'application/msexcel',
'application/x-msexcel',
'application/x-ms-excel',
'application/vnd.oasis.opendocument.spreadsheet',
])('matches natively parseable type: %s', (mimeType) => {
expect(check(mimeType)).toBe(true);
});
it.each([
'application/vnd.oasis.opendocument.text',
'application/vnd.oasis.opendocument.presentation',
'application/vnd.oasis.opendocument.graphics',
'text/plain',
'image/png',
])('does not match OCR-only or unsupported type: %s', (mimeType) => {
expect(check(mimeType)).toBe(false);
});
});
describe('getEndpointFileConfig', () => {
describe('custom endpoint lookup', () => {
it('should find custom endpoint by direct lookup', () => {
const fileConfig: FileConfig = {
...baseFileConfig,
endpoints: {
...baseFileConfig.endpoints,
ollama: {
disabled: true,
fileLimit: 5,
},
},
};
const result = getEndpointFileConfig({
fileConfig,
endpoint: 'ollama',
endpointType: EModelEndpoint.custom,
});
expect(result.disabled).toBe(true);
expect(result.fileLimit).toBe(5);
});
it('should find custom endpoint by normalized lookup', () => {
const fileConfig: FileConfig = {
...baseFileConfig,
endpoints: {
...baseFileConfig.endpoints,
ollama: {
disabled: true,
fileLimit: 7,
},
},
};
/** Test with non-normalized name */
const result = getEndpointFileConfig({
fileConfig,
endpoint: 'Ollama',
endpointType: EModelEndpoint.custom,
});
expect(result.disabled).toBe(true);
expect(result.fileLimit).toBe(7);
});
it('should fallback to generic custom config when specific endpoint not found', () => {
const fileConfig: FileConfig = {
...baseFileConfig,
endpoints: {
...baseFileConfig.endpoints,
[EModelEndpoint.custom]: {
disabled: false,
fileLimit: 3,
},
},
};
const result = getEndpointFileConfig({
fileConfig,
endpoint: 'unknownCustomEndpoint',
endpointType: EModelEndpoint.custom,
});
expect(result.disabled).toBe(false);
expect(result.fileLimit).toBe(3);
});
it('should fallback to agents config when custom and specific endpoint not found', () => {
const fileConfig: FileConfig = {
...baseFileConfig,
endpoints: {
...baseFileConfig.endpoints,
[EModelEndpoint.agents]: {
disabled: false,
fileLimit: 8,
},
},
};
const result = getEndpointFileConfig({
fileConfig,
endpoint: 'unknownCustomEndpoint',
endpointType: EModelEndpoint.custom,
});
expect(result.disabled).toBe(false);
expect(result.fileLimit).toBe(8);
});
it('should use base agents config when only default is dynamically configured', () => {
const dynamicConfig = {
endpoints: {
default: {
disabled: false,
fileLimit: 12,
},
},
};
const merged = mergeFileConfig(dynamicConfig);
const result = getEndpointFileConfig({
fileConfig: merged,
endpoint: 'unknownCustomEndpoint',
endpointType: EModelEndpoint.custom,
});
/**
* Should use base agents config (fileLimit: 10) since it exists in baseFileConfig
* and custom endpoints fall back to agents
*/
expect(result.disabled).toBe(false);
expect(result.fileLimit).toBe(10); /** From baseFileConfig.endpoints.agents */
});
it('should prioritize specific custom endpoint over generic custom config', () => {
const fileConfig: FileConfig = {
...baseFileConfig,
endpoints: {
...baseFileConfig.endpoints,
[EModelEndpoint.custom]: {
disabled: false,
fileLimit: 20,
},
ollama: {
disabled: true,
fileLimit: 3,
},
},
};
const result = getEndpointFileConfig({
fileConfig,
endpoint: 'ollama',
endpointType: EModelEndpoint.custom,
});
/** Should use ollama config, not generic custom */
expect(result.disabled).toBe(true);
expect(result.fileLimit).toBe(3);
});
it('should skip standard endpoint keys in normalized lookup', () => {
const fileConfig: FileConfig = {
...baseFileConfig,
endpoints: {
...baseFileConfig.endpoints,
default: {
disabled: false,
fileLimit: 99,
},
},
};
/** "default" should not match via normalized lookup for custom endpoints */
const result = getEndpointFileConfig({
fileConfig,
endpoint: 'default',
endpointType: EModelEndpoint.custom,
});
/** Should not use direct lookup, should fall back to default */
expect(result.fileLimit).toBe(99);
});
it('should handle complete fallback chain: specific -> custom -> agents -> default', () => {
const customConfig: FileConfig = {
...baseFileConfig,
endpoints: {
...baseFileConfig.endpoints,
myOllama: {
disabled: true,
fileLimit: 1,
},
[EModelEndpoint.custom]: {
disabled: false,
fileLimit: 2,
},
[EModelEndpoint.agents]: {
disabled: false,
fileLimit: 3,
},
default: {
disabled: false,
fileLimit: 4,
},
},
};
/** 1. Should find specific config */
const specific = getEndpointFileConfig({
fileConfig: customConfig,
endpoint: 'myOllama',
endpointType: EModelEndpoint.custom,
});
expect(specific.fileLimit).toBe(1);
/** 2. Should fallback to custom when specific not found */
const customOnlyConfig: FileConfig = {
...baseFileConfig,
endpoints: {
...baseFileConfig.endpoints,
[EModelEndpoint.custom]: {
disabled: false,
fileLimit: 2,
},
[EModelEndpoint.agents]: {
disabled: false,
fileLimit: 3,
},
default: {
disabled: false,
fileLimit: 4,
},
},
};
const customFallback = getEndpointFileConfig({
fileConfig: customOnlyConfig,
endpoint: 'unknownCustom',
endpointType: EModelEndpoint.custom,
});
expect(customFallback.fileLimit).toBe(2);
/** 3. Should fallback to agents */
const agentsFallback = getEndpointFileConfig({
fileConfig: {
...baseFileConfig,
endpoints: {
...baseFileConfig.endpoints,
[EModelEndpoint.agents]: {
disabled: false,
fileLimit: 3,
},
default: {
disabled: false,
fileLimit: 4,
},
},
},
endpoint: 'unknownCustom',
endpointType: EModelEndpoint.custom,
});
expect(agentsFallback.fileLimit).toBe(3);
/**
* 4. Should use agents even if disabled (caller decides based on disabled flag)
* getEndpointFileConfig returns the config, doesn't filter based on disabled
*/
const agentsDisabledConfig = mergeFileConfig({
endpoints: {
[EModelEndpoint.agents]: {
disabled: true,
},
default: {
disabled: false,
fileLimit: 4,
},
},
});
const agentsDisabled = getEndpointFileConfig({
fileConfig: agentsDisabledConfig,
endpoint: 'unknownCustom',
endpointType: EModelEndpoint.custom,
});
/** Should return agents config (disabled: true), not skip to default */
expect(agentsDisabled.disabled).toBe(true);
expect(agentsDisabled.fileLimit).toBe(0); /** disabled: true sets fileLimit to 0 */
});
});
describe('standard endpoint lookup', () => {
it('should find endpoint by endpointType', () => {
const fileConfig: FileConfig = {
...baseFileConfig,
endpoints: {
...baseFileConfig.endpoints,
[EModelEndpoint.openAI]: {
disabled: true,
fileLimit: 15,
},
},
};
const result = getEndpointFileConfig({
fileConfig,
endpoint: 'someOtherName',
endpointType: EModelEndpoint.openAI,
});
expect(result.disabled).toBe(true);
expect(result.fileLimit).toBe(15);
});
it('should find endpoint by direct endpoint name', () => {
const fileConfig: FileConfig = {
...baseFileConfig,
endpoints: {
...baseFileConfig.endpoints,
[EModelEndpoint.anthropic]: {
disabled: false,
fileLimit: 25,
},
},
};
const result = getEndpointFileConfig({
fileConfig,
endpoint: EModelEndpoint.anthropic,
});
expect(result.disabled).toBe(false);
expect(result.fileLimit).toBe(25);
});
it('should find endpoint by normalized name', () => {
const fileConfig: FileConfig = {
...baseFileConfig,
endpoints: {
...baseFileConfig.endpoints,
ollama: {
disabled: true,
fileLimit: 6,
},
},
};
/** Test normalization (Ollama -> ollama) */
const result = getEndpointFileConfig({
fileConfig,
endpoint: 'Ollama',
});
expect(result.disabled).toBe(true);
expect(result.fileLimit).toBe(6);
});
it('should use agents fallback for explicitly agents endpoint', () => {
const fileConfig: FileConfig = {
...baseFileConfig,
endpoints: {
...baseFileConfig.endpoints,
[EModelEndpoint.agents]: {
disabled: false,
fileLimit: 11,
},
},
};
const result = getEndpointFileConfig({
fileConfig,
endpoint: EModelEndpoint.agents,
});
expect(result.disabled).toBe(false);
expect(result.fileLimit).toBe(11);
});
it('should use agents fallback for unconfigured non-standard endpoint', () => {
const fileConfig: FileConfig = {
...baseFileConfig,
endpoints: {
...baseFileConfig.endpoints,
[EModelEndpoint.agents]: {
disabled: false,
fileLimit: 10,
},
default: {
disabled: false,
fileLimit: 100,
},
},
};
const result = getEndpointFileConfig({
fileConfig,
endpoint: 'unconfiguredEndpoint',
});
/**
* With new logic, unconfigured endpoints are treated as custom
* and fall back through: specific -> custom -> agents -> default
* So this should use agents (fileLimit: 10), not default
*/
expect(result.disabled).toBe(false);
expect(result.fileLimit).toBe(10);
});
it('should prioritize endpointType over endpoint name', () => {
const fileConfig: FileConfig = {
...baseFileConfig,
endpoints: {
...baseFileConfig.endpoints,
[EModelEndpoint.openAI]: {
disabled: true,
fileLimit: 5,
},
[EModelEndpoint.anthropic]: {
disabled: false,
fileLimit: 10,
},
},
};
/** endpointType should take priority */
const result = getEndpointFileConfig({
fileConfig,
endpoint: EModelEndpoint.openAI,
endpointType: EModelEndpoint.anthropic,
});
expect(result.disabled).toBe(false);
expect(result.fileLimit).toBe(10);
});
});
describe('edge cases', () => {
it('should return default when fileConfig is null', () => {
const result = getEndpointFileConfig({
fileConfig: null,
endpoint: EModelEndpoint.openAI,
});
expect(result).toBeDefined();
expect(result.disabled).toBe(false);
});
it('should return default when fileConfig is undefined', () => {
const result = getEndpointFileConfig({
fileConfig: undefined,
endpoint: EModelEndpoint.openAI,
});
expect(result).toBeDefined();
expect(result.disabled).toBe(false);
});
it('should handle empty endpoint gracefully', () => {
const fileConfig: FileConfig = {
...baseFileConfig,
endpoints: {
...baseFileConfig.endpoints,
default: {
disabled: false,
fileLimit: 50,
},
},
};
const result = getEndpointFileConfig({
fileConfig,
endpoint: '',
});
expect(result.disabled).toBe(false);
expect(result.fileLimit).toBe(50);
});
it('should handle null endpoint gracefully', () => {
const fileConfig: FileConfig = {
...baseFileConfig,
endpoints: {
...baseFileConfig.endpoints,
default: {
disabled: false,
fileLimit: 50,
},
},
};
const result = getEndpointFileConfig({
fileConfig,
endpoint: null,
});
expect(result.disabled).toBe(false);
expect(result.fileLimit).toBe(50);
});
it('should handle undefined endpoint gracefully', () => {
const fileConfig: FileConfig = {
...baseFileConfig,
endpoints: {
...baseFileConfig.endpoints,
default: {
disabled: false,
fileLimit: 50,
},
},
};
const result = getEndpointFileConfig({
fileConfig,
endpoint: undefined,
});
expect(result.disabled).toBe(false);
expect(result.fileLimit).toBe(50);
});
it('should not mutate the input fileConfig', () => {
const fileConfig: FileConfig = {
...baseFileConfig,
endpoints: {
...baseFileConfig.endpoints,
[EModelEndpoint.openAI]: {
disabled: false,
fileLimit: 10,
},
},
};
const originalDisabled = fileConfig.endpoints[EModelEndpoint.openAI]!.disabled;
getEndpointFileConfig({
fileConfig,
endpoint: EModelEndpoint.openAI,
});
/** Config should not be mutated */
expect(fileConfig.endpoints[EModelEndpoint.openAI]!.disabled).toBe(originalDisabled);
});
});
describe('assistants endpoint handling', () => {
it('should find assistants endpoint config', () => {
const fileConfig: FileConfig = {
...baseFileConfig,
endpoints: {
...baseFileConfig.endpoints,
[EModelEndpoint.assistants]: {
disabled: false,
fileLimit: 20,
},
},
};
const result = getEndpointFileConfig({
fileConfig,
endpoint: EModelEndpoint.assistants,
});
expect(result.disabled).toBe(false);
expect(result.fileLimit).toBe(20);
});
it('should find azureAssistants endpoint config', () => {
const fileConfig: FileConfig = {
...baseFileConfig,
endpoints: {
...baseFileConfig.endpoints,
[EModelEndpoint.azureAssistants]: {
disabled: true,
fileLimit: 15,
},
},
};
const result = getEndpointFileConfig({
fileConfig,
endpoint: EModelEndpoint.azureAssistants,
});
expect(result.disabled).toBe(true);
expect(result.fileLimit).toBe(15);
});
it('should not fallback to agents for assistants endpoints', () => {
const fileConfig: FileConfig = {
...baseFileConfig,
endpoints: {
...baseFileConfig.endpoints,
[EModelEndpoint.agents]: {
disabled: true,
fileLimit: 5,
},
default: {
disabled: false,
fileLimit: 10,
},
},
};
const result = getEndpointFileConfig({
fileConfig,
endpoint: 'unknownAssistants',
endpointType: EModelEndpoint.assistants,
});
/** Should use default, not agents */
expect(result.fileLimit).toBe(10);
});
});
describe('agents endpoint handling', () => {
it('should find agents endpoint config', () => {
const fileConfig: FileConfig = {
...baseFileConfig,
endpoints: {
...baseFileConfig.endpoints,
[EModelEndpoint.agents]: {
disabled: false,
fileLimit: 9,
},
},
};
const result = getEndpointFileConfig({
fileConfig,
endpoint: EModelEndpoint.agents,
});
expect(result.disabled).toBe(false);
expect(result.fileLimit).toBe(9);
});
});
describe('mergeFileConfig integration', () => {
it('should work with mergeFileConfig output for disabled endpoint', () => {
const dynamicConfig = {
endpoints: {
[EModelEndpoint.openAI]: {
disabled: true,
},
},
};
const merged = mergeFileConfig(dynamicConfig);
const result = getEndpointFileConfig({
fileConfig: merged,
endpoint: EModelEndpoint.openAI,
});
expect(result.disabled).toBe(true);
/** When disabled: true, merge sets these to 0 */
expect(result.fileLimit).toBe(0);
expect(result.fileSizeLimit).toBe(0);
expect(result.totalSizeLimit).toBe(0);
expect(result.supportedMimeTypes).toEqual([]);
});
it('should work with mergeFileConfig output for enabled endpoint', () => {
const dynamicConfig = {
endpoints: {
[EModelEndpoint.anthropic]: {
disabled: false,
fileLimit: 5,
fileSizeLimit: 10,
},
},
};
const merged = mergeFileConfig(dynamicConfig);
const result = getEndpointFileConfig({
fileConfig: merged,
endpoint: EModelEndpoint.anthropic,
});
expect(result.disabled).toBe(false);
expect(result.fileLimit).toBe(5);
/** Should convert MB to bytes */
expect(result.fileSizeLimit).toBe(10 * 1024 * 1024);
});
it('should preserve disabled: false in merged config', () => {
const dynamicConfig = {
endpoints: {
[EModelEndpoint.anthropic]: {
disabled: false,
fileLimit: 8,
},
},
};
const merged = mergeFileConfig(dynamicConfig);
const result = getEndpointFileConfig({
fileConfig: merged,
endpoint: EModelEndpoint.anthropic,
});
expect(result.disabled).toBe(false);
expect(result.fileLimit).toBe(8);
});
it('should not mutate base fileConfig during merge', () => {
const originalBaseAgentsConfig = { ...baseFileConfig.endpoints.agents };
const dynamicConfig = {
endpoints: {
[EModelEndpoint.agents]: {
disabled: true,
fileLimit: 1,
},
},
};
mergeFileConfig(dynamicConfig);
/** Base config should not be mutated */
expect(baseFileConfig.endpoints.agents).toEqual(originalBaseAgentsConfig);
});
});
describe('lookup priority verification', () => {
it('should check endpointType before endpoint for standard endpoints', () => {
const fileConfig: FileConfig = {
...baseFileConfig,
endpoints: {
...baseFileConfig.endpoints,
[EModelEndpoint.openAI]: {
disabled: true,
fileLimit: 1,
},
wrongEndpoint: {
disabled: false,
fileLimit: 99,
},
},
};
const result = getEndpointFileConfig({
fileConfig,
endpoint: 'wrongEndpoint',
endpointType: EModelEndpoint.openAI,
});
/** Should use endpointType config, not endpoint */
expect(result.fileLimit).toBe(1);
});
it('should check endpoint when endpointType not found', () => {
const fileConfig: FileConfig = {
...baseFileConfig,
endpoints: {
...baseFileConfig.endpoints,
myCustomEndpoint: {
disabled: true,
fileLimit: 7,
},
},
};
const result = getEndpointFileConfig({
fileConfig,
endpoint: 'myCustomEndpoint',
endpointType: 'notFound',
});
expect(result.fileLimit).toBe(7);
});
});
describe('disabled handling', () => {
it('should properly handle disabled: true', () => {
const fileConfig: FileConfig = {
...baseFileConfig,
endpoints: {
...baseFileConfig.endpoints,
[EModelEndpoint.openAI]: {
disabled: true,
fileLimit: 0,
fileSizeLimit: 0,
totalSizeLimit: 0,
supportedMimeTypes: [],
},
},
};
const result = getEndpointFileConfig({
fileConfig,
endpoint: EModelEndpoint.openAI,
});
expect(result.disabled).toBe(true);
expect(result.fileLimit).toBe(0);
expect(result.fileSizeLimit).toBe(0);
expect(result.totalSizeLimit).toBe(0);
expect(result.supportedMimeTypes).toEqual([]);
});
it('should properly handle disabled: false', () => {
const fileConfig: FileConfig = {
...baseFileConfig,
endpoints: {
...baseFileConfig.endpoints,
[EModelEndpoint.anthropic]: {
disabled: false,
fileLimit: 10,
},
},
};
const result = getEndpointFileConfig({
fileConfig,
endpoint: EModelEndpoint.anthropic,
});
expect(result.disabled).toBe(false);
expect(result.fileLimit).toBe(10);
});
it('should treat undefined disabled as enabled', () => {
const fileConfig: FileConfig = {
...baseFileConfig,
endpoints: {
...baseFileConfig.endpoints,
[EModelEndpoint.google]: {
fileLimit: 10,
},
},
};
const result = getEndpointFileConfig({
fileConfig,
endpoint: EModelEndpoint.google,
});
/** disabled should not be explicitly true */
expect(result.disabled).not.toBe(true);
});
});
describe('partial config merging', () => {
it('should merge partial endpoint config with default config', () => {
const dynamicConfig = {
endpoints: {
google: {
fileSizeLimit: 500,
/** Note: supportedMimeTypes not configured */
},
},
};
const merged = mergeFileConfig(dynamicConfig);
const result = getEndpointFileConfig({
fileConfig: merged,
endpoint: EModelEndpoint.google,
});
/** Should have the configured fileSizeLimit */
expect(result.fileSizeLimit).toBe(500 * 1024 * 1024);
/** Should have supportedMimeTypes from default config */
expect(result.supportedMimeTypes).toBeDefined();
expect(Array.isArray(result.supportedMimeTypes)).toBe(true);
expect(result.supportedMimeTypes!.length).toBeGreaterThan(0);
/** Should have other fields from default */
expect(result.fileLimit).toBeDefined();
expect(result.totalSizeLimit).toBeDefined();
});
it('should not override explicitly set fields with default', () => {
const dynamicConfig = {
endpoints: {
anthropic: {
disabled: true,
fileLimit: 3,
},
},
};
const merged = mergeFileConfig(dynamicConfig);
const result = getEndpointFileConfig({
fileConfig: merged,
endpoint: EModelEndpoint.anthropic,
});
/** Should keep explicitly configured values */
expect(result.disabled).toBe(true);
expect(result.fileLimit).toBe(0); /** disabled: true sets to 0 in merge */
/** But still get supportedMimeTypes from... wait, disabled: true clears this */
expect(result.supportedMimeTypes).toEqual([]);
});
it('should handle endpoint with only fileSizeLimit configured', () => {
const dynamicConfig = {
endpoints: {
openAI: {
fileSizeLimit: 100,
},
},
};
const merged = mergeFileConfig(dynamicConfig);
const result = getEndpointFileConfig({
fileConfig: merged,
endpoint: EModelEndpoint.openAI,
});
expect(result.fileSizeLimit).toBe(100 * 1024 * 1024);
/** Should get these from default */
expect(result.supportedMimeTypes).toBeDefined();
expect(result.fileLimit).toBeDefined();
expect(result.disabled).not.toBe(true);
});
it('should merge supportedMimeTypes from default when only fileSizeLimit is configured', () => {
/** This tests the exact scenario from the issue */
const dynamicConfig = {
endpoints: {
google: {
fileSizeLimit: 1000000024,
},
},
};
const merged = mergeFileConfig(dynamicConfig);
const result = getEndpointFileConfig({
fileConfig: merged,
endpoint: EModelEndpoint.google,
});
/** Should have the massive fileSizeLimit configured */
expect(result.fileSizeLimit).toBe(1000000024 * 1024 * 1024);
/** CRITICAL: Should have supportedMimeTypes from default, not undefined or [] */
expect(result.supportedMimeTypes).toBeDefined();
expect(Array.isArray(result.supportedMimeTypes)).toBe(true);
expect(result.supportedMimeTypes!.length).toBeGreaterThan(0);
/** Should have other default fields */
expect(result.fileLimit).toBe(10);
expect(result.disabled).toBe(false);
});
});
describe('real-world scenarios', () => {
it('should handle multi-provider custom endpoint configuration', () => {
const fileConfig: FileConfig = {
...baseFileConfig,
endpoints: {
...baseFileConfig.endpoints,
ollama: {
disabled: false,
fileLimit: 5,
},
lmstudio: {
disabled: true,
fileLimit: 3,
},
[EModelEndpoint.custom]: {
disabled: false,
fileLimit: 10,
},
},
};
const ollamaResult = getEndpointFileConfig({
fileConfig,
endpoint: 'ollama',
endpointType: EModelEndpoint.custom,
});
expect(ollamaResult.fileLimit).toBe(5);
const lmstudioResult = getEndpointFileConfig({
fileConfig,
endpoint: 'lmstudio',
endpointType: EModelEndpoint.custom,
});
expect(lmstudioResult.disabled).toBe(true);
expect(lmstudioResult.fileLimit).toBe(3);
const unknownResult = getEndpointFileConfig({
fileConfig,
endpoint: 'unknownProvider',
endpointType: EModelEndpoint.custom,
});
expect(unknownResult.fileLimit).toBe(10);
});
it('should handle switching between endpoints correctly', () => {
const fileConfig: FileConfig = {
...baseFileConfig,
endpoints: {
...baseFileConfig.endpoints,
[EModelEndpoint.openAI]: {
disabled: true,
},
[EModelEndpoint.anthropic]: {
disabled: false,
fileLimit: 15,
},
},
};
const openaiResult = getEndpointFileConfig({
fileConfig,
endpoint: EModelEndpoint.openAI,
});
expect(openaiResult.disabled).toBe(true);
const anthropicResult = getEndpointFileConfig({
fileConfig,
endpoint: EModelEndpoint.anthropic,
});
expect(anthropicResult.disabled).toBe(false);
expect(anthropicResult.fileLimit).toBe(15);
});
});
describe('user-configured default behavior', () => {
it('should use user-configured default as effective default when endpoint not found', () => {
const dynamicConfig = {
endpoints: {
default: {
fileLimit: 7,
},
},
};
const merged = mergeFileConfig(dynamicConfig);
const result = getEndpointFileConfig({
fileConfig: merged,
endpoint: EModelEndpoint.google,
});
expect(result.fileLimit).toBe(7);
expect(result.disabled).toBe(false);
expect(result.supportedMimeTypes).toBeDefined();
expect(Array.isArray(result.supportedMimeTypes)).toBe(true);
expect(result.supportedMimeTypes!.length).toBeGreaterThan(0);
});
it('should merge endpoint config against user default (not base default)', () => {
const dynamicConfig = {
endpoints: {
default: {
fileLimit: 7,
},
google: {
fileSizeLimit: 123,
},
},
};
const merged = mergeFileConfig(dynamicConfig);
const result = getEndpointFileConfig({
fileConfig: merged,
endpoint: EModelEndpoint.google,
});
/** fileLimit should come from user default */
expect(result.fileLimit).toBe(7);
/** fileSizeLimit should come from endpoint (converted to bytes) */
expect(result.fileSizeLimit).toBe(123 * 1024 * 1024);
});
it('should respect user-configured default supportedMimeTypes override', () => {
const dynamicConfig = {
endpoints: {
default: {
supportedMimeTypes: ['^text\\/plain$'],
},
},
};
const merged = mergeFileConfig(dynamicConfig);
const result = getEndpointFileConfig({
fileConfig: merged,
});
/** Only text/plain should be allowed */
expect(result.supportedMimeTypes).toBeDefined();
expect(result.supportedMimeTypes!.length).toBe(1);
const [onlyRegex] = result.supportedMimeTypes as RegExp[];
expect(onlyRegex.test('text/plain')).toBe(true);
expect(onlyRegex.test('image/png')).toBe(false);
});
it('should propagate disabled from user default across fallbacks', () => {
const dynamicConfig = {
endpoints: {
default: {
disabled: true,
},
},
};
const merged = mergeFileConfig(dynamicConfig);
const result = getEndpointFileConfig({
fileConfig: merged,
endpoint: EModelEndpoint.google,
});
expect(result.disabled).toBe(true);
expect(result.fileLimit).toBe(0);
expect(result.fileSizeLimit).toBe(0);
expect(result.totalSizeLimit).toBe(0);
expect(result.supportedMimeTypes).toEqual([]);
});
});
});