From 3fbae908392158675720ec99012b2ecec0d47021 Mon Sep 17 00:00:00 2001 From: mubashir1osmani Date: Sat, 6 Jun 2026 14:01:52 -0700 Subject: [PATCH] reiterability + test --- litellm/llms/custom_httpx/llm_http_handler.py | 5 +++++ litellm/llms/vertex_ai/files/transformation.py | 7 +++++++ .../files/test_vertex_ai_files_streaming.py | 13 +++++++++++++ 3 files changed, 25 insertions(+) diff --git a/litellm/llms/custom_httpx/llm_http_handler.py b/litellm/llms/custom_httpx/llm_http_handler.py index 74e58cd043c..c08161fd0af 100644 --- a/litellm/llms/custom_httpx/llm_http_handler.py +++ b/litellm/llms/custom_httpx/llm_http_handler.py @@ -3217,6 +3217,7 @@ class BaseLLMHTTPHandler: timeout=timeout, ) except Exception as e: + verbose_logger.exception(f"Error creating file: {e}") raise self._handle_error(e=e, provider_config=provider_config) elif isinstance(transformed_request, str) or isinstance( transformed_request, bytes @@ -3562,6 +3563,8 @@ class BaseLLMHTTPHandler: resp = httpx_client.send(req, follow_redirects=False) resp.read() if resp.status_code not in ((200, 201) if is_final else (308,)): + # 4xx/5xx raise here; the ValueError catches an unexpected success + # status (e.g. a 200 where the protocol expects a 308 between chunks). resp.raise_for_status() raise ValueError(f"resumable upload: unexpected status {resp.status_code}") return resp @@ -3642,6 +3645,8 @@ class BaseLLMHTTPHandler: resp = await httpx_client.send(req, follow_redirects=False) await resp.aread() if resp.status_code not in ((200, 201) if is_final else (308,)): + # 4xx/5xx raise here; the ValueError catches an unexpected success + # status (e.g. a 200 where the protocol expects a 308 between chunks). resp.raise_for_status() raise ValueError(f"resumable upload: unexpected status {resp.status_code}") return resp diff --git a/litellm/llms/vertex_ai/files/transformation.py b/litellm/llms/vertex_ai/files/transformation.py index 1d6a2071c53..3966bd9a018 100644 --- a/litellm/llms/vertex_ai/files/transformation.py +++ b/litellm/llms/vertex_ai/files/transformation.py @@ -228,6 +228,13 @@ def _iter_openai_jsonl_lines(openai_file_content: FileTypes) -> Iterator[str]: return if hasattr(content, "read"): + # Rewind seekable handles (BytesIO, temp files) so the body can be + # replayed on a retry; a non-seekable stream cannot be re-read. + if hasattr(content, "seek"): + try: + content.seek(0) + except (OSError, ValueError): + pass for raw in content: line = raw.decode("utf-8") if isinstance(raw, bytes) else raw line = line.strip() diff --git a/tests/test_litellm/llms/vertex_ai/files/test_vertex_ai_files_streaming.py b/tests/test_litellm/llms/vertex_ai/files/test_vertex_ai_files_streaming.py index ac4a84e5cb3..65b12191d0a 100644 --- a/tests/test_litellm/llms/vertex_ai/files/test_vertex_ai_files_streaming.py +++ b/tests/test_litellm/llms/vertex_ai/files/test_vertex_ai_files_streaming.py @@ -433,6 +433,19 @@ class TestResumableStreamBody: second = b"".join(stream.iter_bytes()) assert first == second and len(first) > 0 + def test_stream_is_reiterable_for_seekable_file_like_input(self): + # A seekable handle (BytesIO, temp file) must be rewound between calls; + # otherwise the first iter_bytes() exhausts it and a retry would upload + # an empty body silently. + cfg = VertexAIFilesConfig() + raw = _make_openai_jsonl_bytes(40) + stream = _OpenAIToVertexBatchUploadStream( + io.BytesIO(raw), cfg._map_openai_to_vertex_params + ) + first = b"".join(stream.iter_bytes()) + second = b"".join(stream.iter_bytes()) + assert first == second and len(first) > 0 + class TestResumableChunking: def test_intermediate_chunks_are_exactly_chunk_size(self):