"""Offline tests for image_inputs payload shape per LLM binding. These tests stub the underlying network clients with ``unittest.mock`` so they exercise only the message-construction layer that this repository owns. """ from __future__ import annotations import base64 from typing import Any from unittest.mock import AsyncMock, MagicMock, patch import pytest pytestmark = pytest.mark.offline PNG_BYTES = ( b"\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x01\x00\x00\x00\x01" b"\x08\x06\x00\x00\x00\x1f\x15\xc4\x89\x00\x00\x00\rIDATx\x9cc\xf8" b"\xcf\xc0\x00\x00\x00\x03\x00\x01\x5c\xcc\xd9\x9e\x00\x00\x00\x00" b"IEND\xaeB`\x82" ) PNG_B64 = base64.b64encode(PNG_BYTES).decode("ascii") @pytest.mark.asyncio async def test_openai_binding_inserts_image_url_content_block(): from lightrag.llm import openai as openai_mod fake_choice = MagicMock() fake_choice.message.content = "ok" fake_choice.message.reasoning_content = None fake_choice.finish_reason = "stop" fake_response = MagicMock() fake_response.choices = [fake_choice] fake_response.usage = None fake_client = MagicMock() fake_client.chat.completions.create = AsyncMock(return_value=fake_response) fake_client.close = AsyncMock() with patch.object( openai_mod, "create_openai_async_client", return_value=fake_client ): await openai_mod.openai_complete_if_cache( model="gpt-4o-mini", prompt="describe", api_key="dummy", image_inputs=[PNG_B64], ) _, kwargs = fake_client.chat.completions.create.call_args messages = kwargs["messages"] assert messages[-1]["role"] == "user" user_content = messages[-1]["content"] assert isinstance(user_content, list) assert user_content[0] == {"type": "text", "text": "describe"} assert user_content[1]["type"] == "image_url" assert user_content[1]["image_url"]["url"].startswith("data:image/png;base64,") @pytest.mark.asyncio async def test_openai_binding_text_only_remains_plain_string(): from lightrag.llm import openai as openai_mod fake_choice = MagicMock() fake_choice.message.content = "ok" fake_choice.message.reasoning_content = None fake_choice.finish_reason = "stop" fake_response = MagicMock() fake_response.choices = [fake_choice] fake_response.usage = None fake_client = MagicMock() fake_client.chat.completions.create = AsyncMock(return_value=fake_response) fake_client.close = AsyncMock() with patch.object( openai_mod, "create_openai_async_client", return_value=fake_client ): await openai_mod.openai_complete_if_cache( model="gpt-4o-mini", prompt="describe", api_key="dummy", ) _, kwargs = fake_client.chat.completions.create.call_args assert kwargs["messages"][-1]["content"] == "describe" @pytest.mark.asyncio async def test_ollama_binding_attaches_images_to_user_message(): from lightrag.llm import ollama as ollama_mod fake_client = MagicMock() fake_client.chat = AsyncMock(return_value={"message": {"content": "ok"}}) fake_client._client = MagicMock() fake_client._client.aclose = AsyncMock() with patch.object(ollama_mod.ollama, "AsyncClient", return_value=fake_client): await ollama_mod._ollama_model_if_cache( model="llava", prompt="describe", image_inputs=[PNG_B64], ) _, kwargs = fake_client.chat.call_args user_msg = kwargs["messages"][-1] assert user_msg["role"] == "user" assert user_msg["content"] == "describe" assert user_msg["images"] == [PNG_B64] @pytest.mark.asyncio async def test_anthropic_binding_inserts_image_content_block(): from lightrag.llm import anthropic as anthropic_mod captured: dict[str, Any] = {} class FakeMessages: async def create(self, **kwargs): captured.update(kwargs) return MagicMock(content=[MagicMock(text="")]) fake_client = MagicMock() fake_client.messages = FakeMessages() with patch.object(anthropic_mod, "AsyncAnthropic", return_value=fake_client): await anthropic_mod.anthropic_complete_if_cache( model="claude-3-opus", prompt="describe", api_key="dummy", image_inputs=[PNG_B64], ) user_content = captured["messages"][-1]["content"] assert isinstance(user_content, list) image_blocks = [b for b in user_content if b.get("type") == "image"] assert len(image_blocks) == 1 assert image_blocks[0]["source"] == { "type": "base64", "media_type": "image/png", "data": PNG_B64, } assert user_content[-1] == {"type": "text", "text": "describe"} @pytest.mark.asyncio async def test_lollms_binding_rejects_image_inputs(): from lightrag.llm import lollms as lollms_mod with pytest.raises(NotImplementedError): await lollms_mod.lollms_model_if_cache( model="unused", prompt="hi", image_inputs=[PNG_B64], ) @pytest.mark.asyncio async def test_bedrock_binding_forces_non_stream_when_image_present(): from lightrag.llm import bedrock as bedrock_mod captured: dict[str, Any] = {} class FakeBedrockClient: async def __aenter__(self): return self async def __aexit__(self, exc_type, exc, tb): return False async def converse(self, **kwargs): captured["mode"] = "converse" captured["args"] = kwargs return { "output": {"message": {"content": [{"text": "ok"}]}}, "stopReason": "end_turn", } async def converse_stream(self, **kwargs): captured["mode"] = "converse_stream" captured["args"] = kwargs return {"stream": []} class FakeSession: def client(self, *_, **__): return FakeBedrockClient() with patch.object(bedrock_mod.aioboto3, "Session", return_value=FakeSession()): await bedrock_mod.bedrock_complete_if_cache( "anthropic.claude-3-haiku-20240307-v1:0", "describe", stream=True, image_inputs=[PNG_B64], aws_region="us-east-1", ) assert captured["mode"] == "converse" user_msg = captured["args"]["messages"][-1] image_blocks = [block for block in user_msg["content"] if "image" in block] assert len(image_blocks) == 1 assert image_blocks[0]["image"]["format"] == "png" assert image_blocks[0]["image"]["source"]["bytes"] == PNG_BYTES