Coverage for /home/admin/Documents/AI/applications/lexigram-dev/lexigram/experimental/ai/lexigram-ai-llm/src/lexigram/ai/llm/multimodal/fetcher.py: 40%
20 statements
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-25 07:19 +0800
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-25 07:19 +0800
1"""Async image fetcher that converts URLs to base64-encoded image parts."""
3from __future__ import annotations
5import base64
7import httpx
9from lexigram.ai.llm.exceptions import LLMError
10from lexigram.contracts.ai.multimodal import ImageBase64Part
11from lexigram.logging import (
12 get_logger,
13)
15logger = get_logger(__name__)
18async def fetch_image_as_base64(url: str, timeout: float = 10.0) -> ImageBase64Part:
19 """Fetch an image URL and return it as a base64-encoded part.
21 Used by clients that cannot pass image URLs through to the provider
22 (e.g. Ollama, AWS Bedrock). The fetch is done once per message
23 construction — callers should cache results when sending the same
24 image in multiple turns.
26 Args:
27 url: Public HTTP/HTTPS URL of the image.
28 timeout: Request timeout in seconds (default: 10).
30 Returns:
31 :class:`~lexigram.contracts.ai.multimodal.ImageBase64Part` with
32 base64-encoded data and detected media type.
34 Raises:
35 LLMError: If the HTTP request fails or times out.
36 """
37 try:
38 async with httpx.AsyncClient(timeout=timeout) as client:
39 response = await client.get(url)
40 response.raise_for_status()
41 except httpx.HTTPError as exc:
42 raise LLMError(f"Failed to fetch image from {url!r}: {exc}") from exc
44 raw = response.content
45 media_type = (
46 response.headers.get("content-type", "image/jpeg").split(";")[0].strip()
47 )
48 if not media_type.startswith("image/"):
49 media_type = "image/jpeg"
51 logger.debug("image_fetched", url=url, media_type=media_type, size_bytes=len(raw))
52 return ImageBase64Part(
53 data=base64.b64encode(raw).decode(),
54 media_type=media_type,
55 )