| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207 |
- """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
|