fix: stream vision downloads before size checks - #10440
Conversation
The vision analyzer accepts arbitrary remote image URLs, so the size cap has to apply before response bodies are buffered in memory. The downloader now streams into a temporary file, counts bytes as chunks arrive, removes partial files on failure, and still uses Content-Length as an early rejection when it is valid. Constraint: Remote image servers may omit or lie about Content-Length. Rejected: Trust Content-Length plus len(response.content) | response.content buffers the full body before the actual cap can run. Confidence: high Scope-risk: narrow Tested: source venv/bin/activate && pytest tests/tools/test_vision_tools.py -q Tested: source venv/bin/activate && python -m compileall tools/vision_tools.py tests/tools/test_vision_tools.py Tested: git diff --check Not-tested: Live vision download against external CDNs.
4c1f30f to
667b4e3
Compare
|
Rebased onto current upstream Validation:
|
teknium1
left a comment
There was a problem hiding this comment.
Thanks for the focused image-path fix. The reported image issue is present on current main: tools/vision_tools.py:435 performs client.get(), and the effective cap is only checked after response.content at :457-460.
Problems
- The same unbounded-buffer pattern remains in
_download_video()attools/vision_tools.py:1583-1608. A complete remote-vision media fix should cover that sibling path as well; #55028 is tracking an equivalent video change. - Current-main retry tests still mock and assert
client.get()intests/tools/test_vision_tools.py:1093-1158. Salvage needs to migrate those tests to aclient.stream()async-context-manager mock. - The added tests are mock-only.
AGENTS.md:84-87calls for real-path coverage of file/network I/O; add a local streaming-server regression test for a chunked over-cap response and partial-file cleanup.
The streaming loop and temporary-file replacement in PR head 667b4e3d:tools/vision_tools.py:179-236 are directionally sound for the image path. This is an automated hermes-sweeper review.
| event_hooks={"response": [_ssrf_redirect_guard]}, | ||
| ) as client: | ||
| response = await client.get( | ||
| async with client.stream( |
There was a problem hiding this comment.
Current main has retry-classification coverage at tests/tools/test_vision_tools.py:1093-1158 that mocks client.get() and asserts get.await_count. When salvaging this switch to client.stream(), update that helper and its assertions to use an async stream context manager; otherwise the current retry tests no longer exercise the intended status-error path.
|
Closing as stale — the PR is 14923 commits behind
The core streaming approach is correct and we'll implement it fresh on current Thanks for the PR! |
…e cap _download_image() and _download_video() both used client.get() + response.content, buffering the entire media body into memory before checking the size cap. A server that omits Content-Length could send an arbitrarily large payload, causing OOM. Extract _stream_download_to_file() shared helper: streams via client.stream() + aiter_bytes(), writes chunks to a temp file, enforces the running byte count against the cap after each chunk, and atomically replaces onto the destination on success. Cleans up the temp file on failure. Uses utils.atomic_replace() for cross-device/symlink safety. Malformed Content-Length values are now caught and ignored instead of crashing with ValueError; the streaming cap is the authoritative guard. Approach adapted from PR #10440 by @WuKongAI-CMU (closed as stale — 14923 commits behind, reverted 32 commits of vision_tools.py evolution including SSRF-safe client, retry classification, and lazy imports). Closes #10440
…e cap _download_image() and _download_video() both used client.get() + response.content, buffering the entire media body into memory before checking the size cap. A server that omits Content-Length could send an arbitrarily large payload, causing OOM. Extract _stream_download_to_file() shared helper: streams via client.stream() + aiter_bytes(), writes chunks to a temp file, enforces the running byte count against the cap after each chunk, and atomically replaces onto the destination on success. Cleans up the temp file on failure. Uses utils.atomic_replace() for cross-device/symlink safety. Malformed Content-Length values are now caught and ignored instead of crashing with ValueError; the streaming cap is the authoritative guard. Approach adapted from PR NousResearch#10440 by @WuKongAI-CMU (closed as stale — 14923 commits behind, reverted 32 commits of vision_tools.py evolution including SSRF-safe client, retry classification, and lazy imports). Closes NousResearch#10440
…e cap _download_image() and _download_video() both used client.get() + response.content, buffering the entire media body into memory before checking the size cap. A server that omits Content-Length could send an arbitrarily large payload, causing OOM. Extract _stream_download_to_file() shared helper: streams via client.stream() + aiter_bytes(), writes chunks to a temp file, enforces the running byte count against the cap after each chunk, and atomically replaces onto the destination on success. Cleans up the temp file on failure. Uses utils.atomic_replace() for cross-device/symlink safety. Malformed Content-Length values are now caught and ignored instead of crashing with ValueError; the streaming cap is the authoritative guard. Approach adapted from PR NousResearch#10440 by @WuKongAI-CMU (closed as stale — 14923 commits behind, reverted 32 commits of vision_tools.py evolution including SSRF-safe client, retry classification, and lazy imports). Closes NousResearch#10440
…e cap _download_image() and _download_video() both used client.get() + response.content, buffering the entire media body into memory before checking the size cap. A server that omits Content-Length could send an arbitrarily large payload, causing OOM. Extract _stream_download_to_file() shared helper: streams via client.stream() + aiter_bytes(), writes chunks to a temp file, enforces the running byte count against the cap after each chunk, and atomically replaces onto the destination on success. Cleans up the temp file on failure. Uses utils.atomic_replace() for cross-device/symlink safety. Malformed Content-Length values are now caught and ignored instead of crashing with ValueError; the streaming cap is the authoritative guard. Approach adapted from PR NousResearch#10440 by @WuKongAI-CMU (closed as stale — 14923 commits behind, reverted 32 commits of vision_tools.py evolution including SSRF-safe client, retry classification, and lazy imports). Closes NousResearch#10440
Summary
response.content_VISION_MAX_DOWNLOAD_BYTESchunk-by-chunk and delete partial files on failureContent-Lengthas an early rejection, but ignore malformed values and rely on the streaming capTests
source venv/bin/activate && pytest tests/tools/test_vision_tools.py -qsource venv/bin/activate && python -m compileall tools/vision_tools.py tests/tools/test_vision_tools.pygit diff --check