diff --git a/litellm/proxy/_types.py b/litellm/proxy/_types.py index bfc894798d3..09c99969ba2 100644 --- a/litellm/proxy/_types.py +++ b/litellm/proxy/_types.py @@ -2767,7 +2767,8 @@ class ConfigGeneralSettings(LiteLLMPydanticObjectBase): description=( "controls run on every upload that lands in a vector store (/v1/rag/ingest and /v1/files with purpose " "assistants or user_data); rag_ingest.malware_scanner names the scanner instance as . " - "the way custom_auth does, and unset runs the EICAR test scanner - https://docs.litellm.ai/docs/rag_ingest" + "the way custom_auth does, and unset runs the EICAR test scanner; rag_ingest.files_api_controls: false lets " + "the /v1/files uploads through unchecked - https://docs.litellm.ai/docs/rag_ingest" ), ) max_parallel_requests: int | None = Field( diff --git a/litellm/proxy/openai_files_endpoints/files_endpoints.py b/litellm/proxy/openai_files_endpoints/files_endpoints.py index 4a6ad8adc47..1dead8ac504 100644 --- a/litellm/proxy/openai_files_endpoints/files_endpoints.py +++ b/litellm/proxy/openai_files_endpoints/files_endpoints.py @@ -94,7 +94,12 @@ from litellm.proxy.openai_files_endpoints.general_upload_validation import ( coerce_optional_str_list_setting, raise_upload_validation_failure, ) -from litellm.proxy.rag_endpoints.upload_security import MalwareScanner, RejectedUpload, validate_upload +from litellm.proxy.rag_endpoints.upload_security import ( + MAX_UPLOAD_SIZE_BYTES, + MalwareScanner, + RejectedUpload, + validate_upload, +) from litellm.proxy.utils import PrismaClient, ProxyLogging, is_known_model from litellm.repositories.table_repositories import ManagedFileRepository from litellm.router import Router @@ -105,6 +110,7 @@ from litellm.types.llms.openai import ( OpenAIFileObject, OpenAIFilesPurpose, ) +from litellm.types.proxy.rag_ingest import RagIngestSettings router: Final = APIRouter() @@ -531,14 +537,33 @@ async def route_create_file( _VECTOR_STORE_UPLOAD_PURPOSES: Final[frozenset[str]] = frozenset({"assistants", "user_data"}) -async def _reject_unsafe_vector_store_upload( - purpose: str, - file_source: bytes | BinaryIO, - scanner: MalwareScanner, -) -> None: - if purpose not in _VECTOR_STORE_UPLOAD_PURPOSES or not isinstance(file_source, bytes): - return - validation: Final = await asyncio.to_thread(validate_upload, content=file_source, scanner=scanner) +def _vector_store_upload_controls_apply(purpose: str, settings: RagIngestSettings) -> bool: + return purpose in _VECTOR_STORE_UPLOAD_PURPOSES and settings.files_api_controls + + +def _upload_read_limit_bytes(max_file_size_mb: int | None, controls_apply: bool) -> int | None: + control_limit: Final = MAX_UPLOAD_SIZE_BYTES + 1 if controls_apply else None + if max_file_size_mb is None or max_file_size_mb <= 0: + return control_limit + admin_limit: Final = max_file_size_mb * MB + 1 + return admin_limit if control_limit is None else min(admin_limit, control_limit) + + +def _in_memory_upload(file_source: bytes | BinaryIO) -> bytes: + if isinstance(file_source, bytes): + return file_source + raise ProxyException( + message="Uploads with purpose assistants or user_data must be read into memory before the upload controls run.", + type=ProxyErrorTypes.internal_server_error.value, + param="file", + code=500, + ) + + +async def _reject_unsafe_vector_store_upload(content: bytes, scanner: MalwareScanner) -> None: + validation: Final = await asyncio.to_thread( + validate_upload, content=content, scanner=scanner, max_size_bytes=MAX_UPLOAD_SIZE_BYTES + ) if isinstance(validation, RejectedUpload): raise ProxyException( message=f"{validation.message} Rejection reason: {validation.reason.value}.", @@ -598,6 +623,7 @@ async def create_file( llm_router, proxy_config, proxy_logging_obj, + rag_ingest_settings, rag_upload_malware_scanner, version, ) @@ -613,18 +639,21 @@ async def create_file( raise_upload_validation_failure(unsafe_filename_failure) max_file_size_mb: Final = coerce_optional_int_setting(general_settings.get("max_file_size_mb")) + controls_apply: Final = _vector_store_upload_controls_apply(purpose, rag_ingest_settings) + read_limit_bytes: Final = _upload_read_limit_bytes(max_file_size_mb, controls_apply) # Batch uploads can be gigabytes. Starlette has already spooled the upload # to disk, so stream from that handle instead of reading it into memory. - # Other uploads stay in-memory bytes, bounded to max_file_size_mb (plus one - # byte, to still tell "exactly at the limit" from "over it") when it is set, - # so an oversized upload cannot be read to completion before it is rejected. + # Other uploads stay in-memory bytes, bounded to the smaller of max_file_size_mb + # and the vector store cap (plus one byte, to still tell "exactly at the limit" + # from "over it") when either applies, so an oversized upload cannot be read to + # completion before it is rejected. file_source: bytes | BinaryIO if purpose == "batch": await file.seek(0) file_source = file.file - elif max_file_size_mb is not None and max_file_size_mb > 0: - file_source = await file.read(max_file_size_mb * MB + 1) + elif read_limit_bytes is not None: + file_source = await file.read(read_limit_bytes) else: file_source = await file.read() custom_llm_provider = ( @@ -677,7 +706,8 @@ async def create_file( if blocked_extension_failure is not None: raise_upload_validation_failure(blocked_extension_failure) - await _reject_unsafe_vector_store_upload(purpose, file_source, rag_upload_malware_scanner) + if controls_apply: + await _reject_unsafe_vector_store_upload(_in_memory_upload(file_source), rag_upload_malware_scanner) if passthrough: _validate_passthrough_upload( diff --git a/litellm/proxy/proxy_server.py b/litellm/proxy/proxy_server.py index 22ba313ed0f..58f7b436ba3 100644 --- a/litellm/proxy/proxy_server.py +++ b/litellm/proxy/proxy_server.py @@ -745,7 +745,8 @@ from litellm.proxy.rag_endpoints.endpoints import router as rag_router from litellm.proxy.rag_endpoints.upload_security import ( EicarTestMalwareScanner, MalwareScanner, - MalwareScannerConfigError, + RagIngestConfigError, + parse_rag_ingest_settings, resolve_malware_scanner, ) from litellm.proxy.rerank_endpoints.endpoints import router as rerank_router @@ -867,6 +868,7 @@ from litellm.types.proxy.model_deprecation import ( DEFAULT_DEPRECATION_WARN_DAYS, ModelDeprecationResponse, ) +from litellm.types.proxy.rag_ingest import RagIngestSettings from litellm.types.proxy.spend_capture_rate import SpendCaptureProvider, SpendCaptureRateCheckSettings from litellm.types.realtime import RealtimeQueryParams from litellm.types.router import ( @@ -1042,6 +1044,7 @@ def cleanup_router_config_variables(): otel_logging, \ user_custom_auth, \ rag_upload_malware_scanner, \ + rag_ingest_settings, \ user_custom_auth_path, \ user_custom_key_generate, \ user_custom_key_update, \ @@ -1061,6 +1064,7 @@ def cleanup_router_config_variables(): otel_logging = None user_custom_auth = None rag_upload_malware_scanner = EicarTestMalwareScanner() + rag_ingest_settings = RagIngestSettings() user_custom_auth_path = None user_custom_key_generate = None user_custom_key_update = None @@ -2573,6 +2577,7 @@ native_background_mode: list[str] = [] # Models that should use native provider polling_cache_ttl: int = 3600 # Default 1 hour TTL for polling cache user_custom_auth = None rag_upload_malware_scanner: MalwareScanner = EicarTestMalwareScanner() +rag_ingest_settings: RagIngestSettings = RagIngestSettings() user_custom_key_generate = None # Sentinel: prevents PKCE-no-Redis advisory from re-logging on config hot-reload. # Tests that need to reset it can patch 'litellm.proxy.proxy_server._pkce_no_redis_warning_emitted'. @@ -5950,6 +5955,7 @@ class ProxyConfig: otel_logging, \ user_custom_auth, \ rag_upload_malware_scanner, \ + rag_ingest_settings, \ user_custom_auth_path, \ user_custom_key_generate, \ user_custom_key_update, \ @@ -6366,13 +6372,17 @@ class ProxyConfig: TypeAdapter(MCPAdvertisedVersions).validate_python(general_settings["mcp_advertised_versions"]) + parsed_rag_ingest_settings: Final = parse_rag_ingest_settings(general_settings.get("rag_ingest")) + if isinstance(parsed_rag_ingest_settings, RagIngestConfigError): + raise ValueError(parsed_rag_ingest_settings.message) resolved_malware_scanner: Final = resolve_malware_scanner( - general_settings.get("rag_ingest"), + parsed_rag_ingest_settings, config_file_path=config_file_path, load_instance=get_instance_fn, ) - if isinstance(resolved_malware_scanner, MalwareScannerConfigError): + if isinstance(resolved_malware_scanner, RagIngestConfigError): raise ValueError(resolved_malware_scanner.message) + rag_ingest_settings = parsed_rag_ingest_settings rag_upload_malware_scanner = resolved_malware_scanner if os.getenv("NUM_WORKERS", "1") != "1" and redis_usage_cache is None: diff --git a/litellm/proxy/rag_endpoints/upload_security.py b/litellm/proxy/rag_endpoints/upload_security.py index e7f6a5e7fef..4579fd65cca 100644 --- a/litellm/proxy/rag_endpoints/upload_security.py +++ b/litellm/proxy/rag_endpoints/upload_security.py @@ -123,39 +123,42 @@ class EicarTestMalwareScanner: @dataclass(frozen=True, slots=True) -class MalwareScannerConfigError: +class RagIngestConfigError: message: str InstanceLoader: TypeAlias = Callable[[str, str | None], object] +def parse_rag_ingest_settings(rag_ingest: object) -> RagIngestSettings | RagIngestConfigError: + if rag_ingest is None: + return RagIngestSettings() + try: + return RagIngestSettings.model_validate(rag_ingest) + except ValidationError as e: + return RagIngestConfigError(f"general_settings.rag_ingest is invalid: {e}") + + def resolve_malware_scanner( - rag_ingest: object, + settings: RagIngestSettings, *, config_file_path: str | None, load_instance: InstanceLoader, -) -> MalwareScanner | MalwareScannerConfigError: - try: - settings: Final = RagIngestSettings() if rag_ingest is None else RagIngestSettings.model_validate(rag_ingest) - except ValidationError as e: - return MalwareScannerConfigError(f"general_settings.rag_ingest is invalid: {e}") +) -> MalwareScanner | RagIngestConfigError: if settings.malware_scanner is None: return EicarTestMalwareScanner() try: loaded: Final = load_instance(settings.malware_scanner, config_file_path) except Exception as e: - return MalwareScannerConfigError( - f"{MALWARE_SCANNER_OPTION}={settings.malware_scanner!r} could not be loaded: {e}" - ) + return RagIngestConfigError(f"{MALWARE_SCANNER_OPTION}={settings.malware_scanner!r} could not be loaded: {e}") if isinstance(loaded, type): - return MalwareScannerConfigError( + return RagIngestConfigError( f"{MALWARE_SCANNER_OPTION}={settings.malware_scanner!r} names the class {loaded.__qualname__}; " "name an instance of it instead" ) if isinstance(loaded, MalwareScanner) and callable(loaded.scan): return loaded - return MalwareScannerConfigError( + return RagIngestConfigError( f"{MALWARE_SCANNER_OPTION}={settings.malware_scanner!r} resolved to a {type(loaded).__qualname__}, " "which has no scan(content: bytes) -> ScanResult method" ) diff --git a/litellm/types/proxy/rag_ingest.py b/litellm/types/proxy/rag_ingest.py index e091c6a0434..01ebf070bf6 100644 --- a/litellm/types/proxy/rag_ingest.py +++ b/litellm/types/proxy/rag_ingest.py @@ -7,7 +7,7 @@ class RagIngestSettings(BaseModel): model_config = ConfigDict(frozen=True, extra="forbid") malware_scanner: str | None = Field( - None, + default=None, description=( "The scanner every vector-store upload goes through, as . where the module sits " "next to config.yaml or is importable, resolved the way custom_auth is. The instance must expose " @@ -15,3 +15,12 @@ class RagIngestSettings(BaseModel): "the EICAR test scanner, which flags only the EICAR test file" ), ) + files_api_controls: bool = Field( + default=True, + description=( + "Whether /v1/files uploads with purpose assistants or user_data run the upload controls (format, size " + "and malware checks) before reaching the provider. false lets those uploads through unchecked, for a " + "deployment that sends the provider a format the controls reject on that route, such as DOCX or " + "images. /v1/rag/ingest runs the controls whatever this is set to" + ), + ) diff --git a/tests/test_litellm/proxy/openai_files_endpoint/test_files_endpoint.py b/tests/test_litellm/proxy/openai_files_endpoint/test_files_endpoint.py index 23f4cc56371..b02de1346d9 100644 --- a/tests/test_litellm/proxy/openai_files_endpoint/test_files_endpoint.py +++ b/tests/test_litellm/proxy/openai_files_endpoint/test_files_endpoint.py @@ -18,7 +18,9 @@ from litellm.proxy.management_endpoints.internal_user_endpoints import ui_view_u from litellm.proxy.openai_files_endpoints.file_content_streaming_handler import ( FileContentStreamingHandler, ) +from litellm.proxy.openai_files_endpoints.general_upload_validation import MB from litellm.proxy.proxy_server import app +from litellm.proxy.rag_endpoints.upload_security import MAX_UPLOAD_SIZE_BYTES from litellm.types.llms.openai import ( FileListPage, HttpxBinaryResponseContent, @@ -6033,3 +6035,70 @@ def test_create_file_other_purposes_skip_the_vector_store_upload_controls( ) assert resp.status_code == 200, resp.text assert captured["file"][0] == name + + +def test_create_file_for_vector_store_purposes_lets_uploads_through_when_files_api_controls_is_off( + monkeypatch, llm_router: Router +): + from litellm.proxy.rag_endpoints.upload_security import ScanResult, ScanVerdict + from litellm.types.proxy.rag_ingest import RagIngestSettings + + class _InfectedScanner: + def scan(self, content: bytes) -> ScanResult: + return ScanResult(verdict=ScanVerdict.INFECTED, signature="Custom.Sig") + + monkeypatch.setattr("litellm.proxy.proxy_server.rag_upload_malware_scanner", _InfectedScanner()) + monkeypatch.setattr("litellm.proxy.proxy_server.rag_ingest_settings", RagIngestSettings(files_api_controls=False)) + zip_bytes: Final = b"PK\x03\x04\x14\x00\x00\x00payload" + resp, captured = _post_file_as_admin( + monkeypatch, llm_router, purpose="assistants", name="deck.zip", content=zip_bytes, ctype="application/zip" + ) + assert resp.status_code == 200, resp.text + assert captured["file"][0] == "deck.zip" + assert captured["file"][1] == zip_bytes + + +def test_create_file_for_vector_store_purposes_reads_at_most_the_cap_plus_one_byte(monkeypatch, llm_router: Router): + from litellm.proxy.openai_files_endpoints import files_endpoints as fe + + monkeypatch.setattr(fe, "MAX_UPLOAD_SIZE_BYTES", 10) + resp, captured = _post_file_as_admin( + monkeypatch, llm_router, purpose="assistants", name="notes.txt", content=b"x" * 20, ctype="text/plain" + ) + assert resp.status_code == 400, resp.text + message = resp.json()["error"]["message"] + assert "Uploaded file is 11 bytes" in message + assert "Rejection reason: file_too_large." in message + assert captured == {} + + +@pytest.mark.parametrize( + "max_file_size_mb, controls_apply, expected", + [ + (None, True, MAX_UPLOAD_SIZE_BYTES + 1), + (0, True, MAX_UPLOAD_SIZE_BYTES + 1), + (1, True, MB + 1), + (1024, True, MAX_UPLOAD_SIZE_BYTES + 1), + (1, False, MB + 1), + (None, False, None), + ], +) +def test_upload_read_limit_is_the_smaller_of_the_admin_limit_and_the_vector_store_cap( + max_file_size_mb, controls_apply, expected +): + from litellm.proxy.openai_files_endpoints.files_endpoints import _upload_read_limit_bytes + + assert _upload_read_limit_bytes(max_file_size_mb, controls_apply) == expected + + +def test_vector_store_upload_controls_refuse_a_source_that_was_not_read_into_memory(): + from io import BytesIO + + from litellm.proxy._types import ProxyException + from litellm.proxy.openai_files_endpoints.files_endpoints import _in_memory_upload + + assert _in_memory_upload(b"in memory") == b"in memory" + with pytest.raises(ProxyException) as raised: + _in_memory_upload(BytesIO(b"spooled to disk")) + assert raised.value.code == "500" + assert raised.value.param == "file" diff --git a/tests/test_litellm/proxy/rag_endpoints/test_upload_security.py b/tests/test_litellm/proxy/rag_endpoints/test_upload_security.py index 1347fea8c1f..e31387bce12 100644 --- a/tests/test_litellm/proxy/rag_endpoints/test_upload_security.py +++ b/tests/test_litellm/proxy/rag_endpoints/test_upload_security.py @@ -15,7 +15,7 @@ from litellm.proxy.rag_endpoints.upload_security import ( EICAR_TEST_SIGNATURE, DetectedFormat, EicarTestMalwareScanner, - MalwareScannerConfigError, + RagIngestConfigError, RejectedUpload, RejectionReason, ScanResult, @@ -23,10 +23,12 @@ from litellm.proxy.rag_endpoints.upload_security import ( SecuredUpload, generate_safe_filename, inspect_content, + parse_rag_ingest_settings, resolve_malware_scanner, safe_download_headers, validate_upload, ) +from litellm.types.proxy.rag_ingest import RagIngestSettings @dataclass(frozen=True) @@ -196,20 +198,47 @@ def _loader_raising(error: Exception): return load_instance +def test_parse_rag_ingest_settings_unset_block_runs_every_control_with_the_eicar_scanner(): + for rag_ingest in (None, {}, {"malware_scanner": None}): + assert parse_rag_ingest_settings(rag_ingest) == RagIngestSettings( + malware_scanner=None, files_api_controls=True + ), rag_ingest + + +def test_parse_rag_ingest_settings_reads_the_files_api_opt_out(): + parsed = parse_rag_ingest_settings({"malware_scanner": "custom_scanner.scanner", "files_api_controls": False}) + assert parsed == RagIngestSettings(malware_scanner="custom_scanner.scanner", files_api_controls=False) + + +@pytest.mark.parametrize( + "rag_ingest", + [ + {"malware_scaner": "custom_scanner.scanner"}, + {"malware_scanner": 42}, + {"files_api_controls": "sometimes"}, + "custom_scanner.scanner", + ["x"], + ], +) +def test_parse_rag_ingest_settings_rejects_an_invalid_block(rag_ingest): + parsed = parse_rag_ingest_settings(rag_ingest) + assert isinstance(parsed, RagIngestConfigError) + assert "general_settings.rag_ingest" in parsed.message + + def test_resolve_malware_scanner_unset_runs_the_eicar_test_scanner(): load_instance, calls = _loader_returning(_CLEAN_SCANNER) - for rag_ingest in (None, {}, {"malware_scanner": None}): - resolved = resolve_malware_scanner( - rag_ingest, config_file_path="/etc/litellm/config.yaml", load_instance=load_instance - ) - assert isinstance(resolved, EicarTestMalwareScanner), rag_ingest + resolved = resolve_malware_scanner( + RagIngestSettings(), config_file_path="/etc/litellm/config.yaml", load_instance=load_instance + ) + assert isinstance(resolved, EicarTestMalwareScanner) assert calls == [] def test_resolve_malware_scanner_loads_the_configured_instance_next_to_the_config(): load_instance, calls = _loader_returning(_INFECTED_SCANNER) resolved = resolve_malware_scanner( - {"malware_scanner": "custom_scanner.scanner"}, + RagIngestSettings(malware_scanner="custom_scanner.scanner"), config_file_path="/etc/litellm/config.yaml", load_instance=load_instance, ) @@ -230,11 +259,11 @@ def test_resolve_malware_scanner_loads_the_configured_instance_next_to_the_confi def test_resolve_malware_scanner_rejects_an_object_that_is_not_a_scanner(loaded, expected_fragment): load_instance, _calls = _loader_returning(loaded) resolved = resolve_malware_scanner( - {"malware_scanner": "custom_scanner.scanner"}, + RagIngestSettings(malware_scanner="custom_scanner.scanner"), config_file_path=None, load_instance=load_instance, ) - assert isinstance(resolved, MalwareScannerConfigError) + assert isinstance(resolved, RagIngestConfigError) assert "general_settings.rag_ingest.malware_scanner" in resolved.message assert "custom_scanner.scanner" in resolved.message assert expected_fragment in resolved.message @@ -251,22 +280,11 @@ def test_resolve_malware_scanner_rejects_an_object_that_is_not_a_scanner(loaded, ) def test_resolve_malware_scanner_reports_a_failed_load_with_the_option_name(error): resolved = resolve_malware_scanner( - {"malware_scanner": "custom_scanner.scanner"}, + RagIngestSettings(malware_scanner="custom_scanner.scanner"), config_file_path=None, load_instance=_loader_raising(error), ) - assert isinstance(resolved, MalwareScannerConfigError) + assert isinstance(resolved, RagIngestConfigError) assert "general_settings.rag_ingest.malware_scanner" in resolved.message assert str(error) in resolved.message - -@pytest.mark.parametrize( - "rag_ingest", - [{"malware_scaner": "custom_scanner.scanner"}, {"malware_scanner": 42}, "custom_scanner.scanner", ["x"]], -) -def test_resolve_malware_scanner_rejects_an_invalid_rag_ingest_block(rag_ingest): - load_instance, calls = _loader_returning(_CLEAN_SCANNER) - resolved = resolve_malware_scanner(rag_ingest, config_file_path=None, load_instance=load_instance) - assert isinstance(resolved, MalwareScannerConfigError) - assert "general_settings.rag_ingest" in resolved.message - assert calls == [] diff --git a/tests/test_litellm/proxy/test_proxy_server.py b/tests/test_litellm/proxy/test_proxy_server.py index ecf19e362e4..7e8647d69ab 100644 --- a/tests/test_litellm/proxy/test_proxy_server.py +++ b/tests/test_litellm/proxy/test_proxy_server.py @@ -3522,8 +3522,10 @@ async def test_load_config_role_permissions_usable_by_jwt_auth(tmp_path): async def test_load_config_wires_the_configured_malware_scanner_into_uploads(tmp_path, monkeypatch): from litellm.proxy.proxy_server import ProxyConfig from litellm.proxy.rag_endpoints.upload_security import EicarTestMalwareScanner, ScanVerdict + from litellm.types.proxy.rag_ingest import RagIngestSettings monkeypatch.setattr(proxy_server_module, "rag_upload_malware_scanner", EicarTestMalwareScanner()) + monkeypatch.setattr(proxy_server_module, "rag_ingest_settings", RagIngestSettings(files_api_controls=False)) (tmp_path / "boot_scanner.py").write_text( "from litellm.proxy.rag_endpoints.upload_security import ScanResult, ScanVerdict\n" "class FlagEverything:\n" @@ -3539,10 +3541,26 @@ async def test_load_config_wires_the_configured_malware_scanner_into_uploads(tmp ) await ProxyConfig().load_config(router=MagicMock(), config_file_path=str(config_file)) assert proxy_server_module.rag_upload_malware_scanner.scan(b"plain text").verdict is ScanVerdict.INFECTED + assert proxy_server_module.rag_ingest_settings.files_api_controls is True + + config_file.write_text( + yaml.dump( + { + "model_list": [], + "general_settings": { + "rag_ingest": {"malware_scanner": "boot_scanner.scanner", "files_api_controls": False} + }, + } + ) + ) + await ProxyConfig().load_config(router=MagicMock(), config_file_path=str(config_file)) + assert proxy_server_module.rag_ingest_settings.files_api_controls is False + assert proxy_server_module.rag_upload_malware_scanner.scan(b"plain text").verdict is ScanVerdict.INFECTED config_file.write_text(yaml.dump({"model_list": []})) await ProxyConfig().load_config(router=MagicMock(), config_file_path=str(config_file)) assert isinstance(proxy_server_module.rag_upload_malware_scanner, EicarTestMalwareScanner) + assert proxy_server_module.rag_ingest_settings.files_api_controls is True config_file.write_text( yaml.dump( diff --git a/ui/litellm-dashboard/src/lib/http/schema.d.ts b/ui/litellm-dashboard/src/lib/http/schema.d.ts index f63e4657a97..508e2f71248 100644 --- a/ui/litellm-dashboard/src/lib/http/schema.d.ts +++ b/ui/litellm-dashboard/src/lib/http/schema.d.ts @@ -28451,7 +28451,7 @@ export interface components { * @default 30 */ proxy_config_reload_interval_seconds: number; - /** @description controls run on every upload that lands in a vector store (/v1/rag/ingest and /v1/files with purpose assistants or user_data); rag_ingest.malware_scanner names the scanner instance as . the way custom_auth does, and unset runs the EICAR test scanner - https://docs.litellm.ai/docs/rag_ingest */ + /** @description controls run on every upload that lands in a vector store (/v1/rag/ingest and /v1/files with purpose assistants or user_data); rag_ingest.malware_scanner names the scanner instance as . the way custom_auth does, and unset runs the EICAR test scanner; rag_ingest.files_api_controls: false lets the /v1/files uploads through unchecked - https://docs.litellm.ai/docs/rag_ingest */ rag_ingest?: components["schemas"]["RagIngestSettings"] | null; /** * Reject Clientside Metadata Tags @@ -39055,6 +39055,12 @@ export interface components { }; /** RagIngestSettings */ RagIngestSettings: { + /** + * Files Api Controls + * @description Whether /v1/files uploads with purpose assistants or user_data run the upload controls (format, size and malware checks) before reaching the provider. false lets those uploads through unchecked, for a deployment that sends the provider a format the controls reject on that route, such as DOCX or images. /v1/rag/ingest runs the controls whatever this is set to + * @default true + */ + files_api_controls: boolean; /** * Malware Scanner * @description The scanner every vector-store upload goes through, as . where the module sits next to config.yaml or is importable, resolved the way custom_auth is. The instance must expose scan(content: bytes) -> ScanResult and be safe to call from several threads at once. Unset runs the EICAR test scanner, which flags only the EICAR test file