From 8d462dd550213651de67b45284ac889979dc1fe3 Mon Sep 17 00:00:00 2001 From: AkhileshNegi Date: Wed, 9 Sep 2026 02:30:29 +0530 Subject: [PATCH 1/3] fix(collections): lowercase file extension for OpenAI upload MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A knowledge base document uploaded as `report.PDF` is accepted at upload — `get_file_format` lowercases the suffix — and `fname` is stored with its original case, since it drives the download attachment name. OpenAI file search may not match an uppercase suffix in every ingestion path, and a failed file in a batch is a hard failure that rolls back the whole vector store. Send `files.create` the stem plus a lowercased extension; the stored `fname` is untouched. Co-Authored-By: Claude Opus 5 (1M context) --- .../services/collections/providers/openai.py | 7 ++++++- .../providers/test_openai_provider.py | 21 +++++++++++++++++++ docs/wiki/modules/knowledge-base.md | 1 + 3 files changed, 28 insertions(+), 1 deletion(-) diff --git a/backend/app/services/collections/providers/openai.py b/backend/app/services/collections/providers/openai.py index 770e0773f..9626f52e4 100644 --- a/backend/app/services/collections/providers/openai.py +++ b/backend/app/services/collections/providers/openai.py @@ -1,5 +1,6 @@ import logging import tempfile +from pathlib import PurePosixPath from typing import IO, List, cast from openai import OpenAI @@ -47,8 +48,12 @@ def upload_files( if doc.file_size_kb is None: doc.file_size_kb = round(tmp.tell() / 1024, 2) tmp.seek(0) + # Stored fname keeps its case (drives the download attachment + # name); file search may not match an uppercase suffix. + name = PurePosixPath(doc.fname) uploaded = self.client.files.create( - file=(doc.fname, tmp), purpose="assistants" + file=(f"{name.stem}{name.suffix.lower()}", tmp), + purpose="assistants", ) except Exception as err: logger.error( diff --git a/backend/app/tests/services/collections/providers/test_openai_provider.py b/backend/app/tests/services/collections/providers/test_openai_provider.py index d31fb0ae3..84a1a5bba 100644 --- a/backend/app/tests/services/collections/providers/test_openai_provider.py +++ b/backend/app/tests/services/collections/providers/test_openai_provider.py @@ -317,6 +317,27 @@ def test_upload_files_file_name_matches_doc_fname() -> None: assert fname == "report.pdf" +def test_upload_files_lowercases_extension() -> None: + """Only the extension is lowercased; the stem keeps its case.""" + client = MagicMock() + client.files.create.return_value = MagicMock(id="file-abc") + provider = OpenAIProvider(client=client) + + storage = _make_storage(b"data") + doc = _make_doc(file_size_kb=1.0) + doc.fname = "Report.PDF" + + session_p, crud_p = _patch_session_and_crud() + with session_p as MockSession, crud_p: + MockSession.return_value.__enter__.return_value = MagicMock() + MockSession.return_value.__exit__.return_value = False + provider.upload_files(storage, [doc], project_id=1) + + _, kwargs = client.files.create.call_args + fname, _ = kwargs["file"] + assert fname == "Report.pdf" + + def test_upload_files_raises_on_db_update_failure() -> None: client = MagicMock() client.files.create.return_value = MagicMock(id="file-ok") diff --git a/docs/wiki/modules/knowledge-base.md b/docs/wiki/modules/knowledge-base.md index 4cefd5719..179b0c69d 100644 --- a/docs/wiki/modules/knowledge-base.md +++ b/docs/wiki/modules/knowledge-base.md @@ -35,6 +35,7 @@ All paths relative to `backend/app/`. ## Gotchas - `signed_url` behaviour is chosen per request by the `download` query param (default false = inline, as before). `put()` stores the upload's `Content-Type`, so a bare presigned URL for a PDF opens in a tab (CSV/XLSX datasets happen to download, which is why only KB docs looked broken). `get_signed_url(..., filename=...)` adds `ResponseContentDisposition: attachment`; `_signed_url()` in `services/documents/helpers.py` passes `fname` only when `download` is set, and the flag threads through `build_document_schema(s)` / `build_job_schema(s)` from `api/routes/documents.py`, `doc_transformation_job.py` and `collections.py`. An attachment URL will not render in an `