Skip to content

feat: imageblock and imgurblock ergonomics#1377

Open
AngeloDanducci wants to merge 2 commits into
generative-computing:mainfrom
AngeloDanducci:ad-1312
Open

feat: imageblock and imgurblock ergonomics#1377
AngeloDanducci wants to merge 2 commits into
generative-computing:mainfrom
AngeloDanducci:ad-1312

Conversation

@AngeloDanducci

@AngeloDanducci AngeloDanducci commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

Pull Request

Issue

Fixes #1312

Description

Adds ImageBlock and ImageUrBlock ergonomics.

Note: I chose make_image_block to avoid image_block and ImageBlock being a bit too close when reading quickly.

Testing

  • Tests added to the respective file if code was changed
  • New code has 100% coverage if code was added
  • Ensure existing tests and github automation passes (a maintainer will kick off the github automation when the rest of the PR is populated)

Attribution

  • AI coding assistants used

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.

  • Component
  • Requirement
  • Sampling Strategy
  • Tool

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.

Signed-off-by: AngeloDanducci <angelo.danducci.ii@ibm.com>
@AngeloDanducci AngeloDanducci requested review from a team, jakelorocco and nrfulton as code owners July 10, 2026 18:48
@AngeloDanducci AngeloDanducci requested a review from psschwei July 10, 2026 18:48
@github-actions github-actions Bot added the enhancement New feature or request label Jul 10, 2026
Comment thread mellea/core/base.py
return ImageBlock.pil_to_base64(image)


def make_image_block(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is it worth debug logging when a conversion happens?

@psschwei psschwei left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 e preserves the chain, but the surfaced message omits why (404 vs DNS vs bad image). Include {e}.
  • Sequential downloads per message (ollama.py:429-434) — N ImageUrlBlocks = N sequential blocking round-trips. Once #1 is async, asyncio.gather them (keep return_exceptions=False per AGENTS.md §9).
  • Raises: docstring is padded with prose (ollama.py:390-393) — the "fetched and encoded automatically" sentence belongs in the body, not the Raises: entry.
  • Naming — issue #1312 suggested image_block; PR ships make_image_block. Fine (repo has make_execution_environment precedent), 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_base64 through the Ollama path. Consider one that patches only urllib.request.urlopen (as test_base.py:219 already does) to catch import-alias regressions.

✅ Done well

  • make_image_block tests cover all four dispatch branches, both error paths, and meta propagation — thorough.
  • Ollama change is a minimal diff that reuses _strip_data_uri_prefix and 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 against openai_compatible_helpers.py:200-206, which passes ImageUrlBlock.value straight through as image_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>
@AngeloDanducci

AngeloDanducci commented Jul 13, 2026

Copy link
Copy Markdown
Contributor Author

Should be ready for re-review, I think the only thing not changed was regarding naming.

I chose make_image_block to avoid image_block and ImageBlock being a bit too close when reading quickly.

@AngeloDanducci AngeloDanducci requested a review from psschwei July 13, 2026 17:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add ImageBlock/ImageUrlBlock Ergonomics

2 participants