Files
hkuds--lightrag/tests/llm/test_llm_binding_image_payload.py
2026-07-13 12:08:54 +08:00

208 lines
6.5 KiB
Python

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