feat: imageblock and imgurblock ergonomics#1377
Conversation
Signed-off-by: AngeloDanducci <angelo.danducci.ii@ibm.com>
| return ImageBlock.pil_to_base64(image) | ||
|
|
||
|
|
||
| def make_image_block( |
There was a problem hiding this comment.
is it worth debug logging when a conversion happens?
psschwei
left a comment
There was a problem hiding this comment.
The make_image_block factory is clean, well-tested, and mergeable as-is. But the Ollama auto-download change swaps an explicit opt-in ValueError for an automatic, synchronous network fetch inside an async method — with no timeout, no size cap, and no SSRF protection. That needs to be addressed (or explicitly accepted by a maintainer) before merge.
Verified locally: tests pass (16/16 in the touched suites), ruff format/ruff check clean, and the docstring quality gate passes at 100%.
🔴 Blocking
1. Blocking urlopen inside an async coroutine — stalls the event loop (mellea/core/base.py:243, called from mellea/backends/ollama.py:429)
_download_image_as_base64 does a fully synchronous urllib.request.urlopen(...).read() and is called directly inside async def generate_from_chat_context — no asyncio.to_thread, no executor. One chat request with a URL image blocks the entire event loop for the whole download, freezing every other concurrent generation sharing that loop (m serve, multi-session apps). Offload it: await asyncio.to_thread(_download_image_as_base64, url).
2. No timeout on urlopen (mellea/core/base.py:243)
urlopen(url) has no timeout=. A slow/hung remote can block indefinitely — and per #1, it blocks the shared loop while doing so. This is now reachable by default (it replaced the old ValueError). Pass an explicit timeout= and fold TimeoutError/socket.timeout into the existing ValueError wrapper.
3. No response size cap (mellea/core/base.py:243-247)
response.read() buffers the whole body into memory before PIL ever sees it. A crafted/infinite response is a memory-exhaustion DoS, triggered automatically by the Ollama path. Cap it (check Content-Length, or read(MAX + 1) and reject on overflow).
4. SSRF exposure — now automatic, not opt-in (mellea/core/base.py:225-248)
Before this PR, nothing in mellea made an outbound GET from a URL embedded in message content. Now any ImageUrlBlock routed through Ollama causes the process to fetch that URL — including URLs from untrusted sources (documents, tool results, model output echoed back). No scheme/host/IP restrictions (169.254.169.254 metadata, localhost:6379, RFC1918), and urlopen follows redirects, so an input-string host check would be bypassable via a 302. At minimum this warrants explicit maintainer/security sign-off; a reasonable mitigation is validating resolved IPs against private/loopback/link-local ranges and disabling redirects.
🟡 Suggestions (non-blocking)
- Error message loses the cause (
base.py:246) —from epreserves the chain, but the surfaced message omits why (404 vs DNS vs bad image). Include{e}. - Sequential downloads per message (
ollama.py:429-434) — NImageUrlBlocks = N sequential blocking round-trips. Once #1 is async,asyncio.gatherthem (keepreturn_exceptions=Falseper AGENTS.md §9). Raises:docstring is padded with prose (ollama.py:390-393) — the "fetched and encoded automatically" sentence belongs in the body, not theRaises:entry.- Naming — issue #1312 suggested
image_block; PR shipsmake_image_block. Fine (repo hasmake_execution_environmentprecedent), but worth a one-line note in the PR so it doesn't read as a misread spec. - Ollama failure test is fully mocked (
test_vision_ollama.py:163) — no test drives the real_download_image_as_base64through the Ollama path. Consider one that patches onlyurllib.request.urlopen(astest_base.py:219already does) to catch import-alias regressions.
✅ Done well
make_image_blocktests cover all four dispatch branches, both error paths, and meta propagation — thorough.- Ollama change is a minimal diff that reuses
_strip_data_uri_prefixand the existing payload shape. - Docstring double→single-backtick fixes correctly follow AGENTS.md §5 and exactly match what #1312 asked for.
- The
"passed directly to OpenAI-compatible backends"docstring claim is accurate — verified againstopenai_compatible_helpers.py:200-206, which passesImageUrlBlock.valuestraight through asimage_url.url. - Quality gate (§10.6) passes at 100% for the new public API, including
Raises:/Returns:.
Items #1–#4 are all in the ~20-line _download_image_as_base64 helper, so a single focused revision (thread offload + timeout + size cap + IP/redirect guard) clears the whole blocking set.
Signed-off-by: AngeloDanducci <angelo.danducci.ii@ibm.com>
|
Should be ready for re-review, I think the only thing not changed was regarding naming. I chose |
Pull Request
Issue
Fixes #1312
Description
Adds ImageBlock and ImageUrBlock ergonomics.
Note: I chose
make_image_blocktoavoid image_blockandImageBlockbeing a bit too close when reading quickly.Testing
Attribution
Adding a new component, requirement, sampling strategy, or tool?
If your PR adds or modifies one of the types below, check the matching box. A checklist of type-specific review items will be posted as a comment.
NOTE: Please ensure you have an issue that has been acknowledged by a core contributor and routed you to open a pull request against this repository. Otherwise, please open an issue before continuing with this pull request.