From cca2c71e8eacb2b9329752b1722ec6476869b970 Mon Sep 17 00:00:00 2001 From: Vineeth Sai Date: Tue, 25 Aug 2026 12:59:48 -0700 Subject: [PATCH 1/2] fix(files): read the file extension from the URL path, not the query string infer_content_type_from_url_and_content took the extension with `url.split(".")[-1].lower().split("?")[0]`, which splits on "." first, so the last dot-segment of the query string wins: "report.pdf?v=1.0" reads as "0" and falls through to the image sniff, which raises ValueError on a PDF. Parse the URL and take the extension off its path, the way _get_image_mime_type_from_url in the same module already does. Rebased onto current staging: the source file merged clean, and the test file's conflict was both sides appending at the end, so both blocks are kept. --- .../prompt_templates/common_utils.py | 7 +++- ...ore_utils_prompt_templates_common_utils.py | 35 +++++++++++++++++++ 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/litellm/litellm_core_utils/prompt_templates/common_utils.py b/litellm/litellm_core_utils/prompt_templates/common_utils.py index 72ea85dfa33..b42fbabe1a2 100644 --- a/litellm/litellm_core_utils/prompt_templates/common_utils.py +++ b/litellm/litellm_core_utils/prompt_templates/common_utils.py @@ -1230,7 +1230,12 @@ def infer_content_type_from_url_and_content( # Try to infer from URL extension if url: - extension: Final = url.split(".")[-1].lower().split("?")[0] # Remove query params + # Strip the query string before taking the extension, the way + # _get_image_mime_type_from_url does: splitting on "." first takes the + # last dot-segment of the query instead, so "report.pdf?v=1.0" reads as "0". + from urllib.parse import urlparse + + extension: Final = urlparse(url).path.split(".")[-1].lower() inferred_type: Final = extension_to_mime.get(extension) if inferred_type: return inferred_type diff --git a/tests/test_litellm/litellm_core_utils/prompt_templates/test_litellm_core_utils_prompt_templates_common_utils.py b/tests/test_litellm/litellm_core_utils/prompt_templates/test_litellm_core_utils_prompt_templates_common_utils.py index 44f91c98d81..398e0c1c994 100644 --- a/tests/test_litellm/litellm_core_utils/prompt_templates/test_litellm_core_utils_prompt_templates_common_utils.py +++ b/tests/test_litellm/litellm_core_utils/prompt_templates/test_litellm_core_utils_prompt_templates_common_utils.py @@ -1027,3 +1027,38 @@ def test_update_messages_xlitellm_decode_does_not_override_mapping(): updated = update_messages_with_model_file_ids(messages, "model-A", mapping) assert updated[0]["content"][0]["file"]["file_id"] == "provider-explicit-id" + + +class TestInferContentTypeQueryString: + """A dot in the query string must not be mistaken for the file extension.""" + + def _infer(self, url: str, content: bytes): + from litellm.litellm_core_utils.prompt_templates.common_utils import ( + infer_content_type_from_url_and_content, + ) + + return infer_content_type_from_url_and_content( + url=url, content=content, current_content_type="binary/octet-stream" + ) + + @pytest.mark.parametrize( + "url, content, expected", + [ + # No query string, and a query string without a dot, both already worked. + ("https://bucket.s3.amazonaws.com/report.pdf", b"%PDF-1.7", "application/pdf"), + ("https://bucket.s3.amazonaws.com/report.pdf?v=1", b"%PDF-1.7", "application/pdf"), + # A dotted query string is the regression: the extension used to be + # read out of the query, so these raised ValueError. + ("https://bucket.s3.amazonaws.com/report.pdf?v=1.0", b"%PDF-1.7", "application/pdf"), + ("https://bucket.s3.amazonaws.com/data.csv?X-Amz-Expires=3.6", b"a,b\n1,2", "text/csv"), + ("https://cdn.example.com/page.html?cb=1.2.3", b"", "text/html"), + ], + ) + def test_extension_is_read_from_the_path_not_the_query(self, url, content, expected): + assert self._infer(url, content) == expected + + def test_a_url_with_no_usable_extension_still_raises(self): + # The fallback is unchanged: non-image bytes with no known extension + # have nothing left to infer from. + with pytest.raises(ValueError): + self._infer("https://cdn.example.com/download?id=1.2", b"not-an-image") From da727c90f4cd1322752d5f1be2f3ca44b432f083 Mon Sep 17 00:00:00 2001 From: Vineeth Sai Date: Tue, 25 Aug 2026 13:10:09 -0700 Subject: [PATCH 2/2] chore(tests): match the ValueError message in the fallback assertion PT011: a bare pytest.raises(ValueError) is too broad. Match the message the fallback actually raises so the test cannot pass on some unrelated ValueError. --- .../test_litellm_core_utils_prompt_templates_common_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_litellm/litellm_core_utils/prompt_templates/test_litellm_core_utils_prompt_templates_common_utils.py b/tests/test_litellm/litellm_core_utils/prompt_templates/test_litellm_core_utils_prompt_templates_common_utils.py index 398e0c1c994..3b5b420be98 100644 --- a/tests/test_litellm/litellm_core_utils/prompt_templates/test_litellm_core_utils_prompt_templates_common_utils.py +++ b/tests/test_litellm/litellm_core_utils/prompt_templates/test_litellm_core_utils_prompt_templates_common_utils.py @@ -1060,5 +1060,5 @@ class TestInferContentTypeQueryString: def test_a_url_with_no_usable_extension_still_raises(self): # The fallback is unchanged: non-image bytes with no known extension # have nothing left to infer from. - with pytest.raises(ValueError): + with pytest.raises(ValueError, match="Unable to determine content type from URL"): self._infer("https://cdn.example.com/download?id=1.2", b"not-an-image")