import asyncio import importlib import sys from pathlib import Path import pytest BACKEND_DIR = Path(__file__).resolve().parents[1] if str(BACKEND_DIR) not in sys.path: sys.path.insert(0, str(BACKEND_DIR)) try: llm = importlib.import_module("llm") except ModuleNotFoundError: pytest.skip("llm module dependencies are not available", allow_module_level=True) def test_extract_message_with_content_and_thinking(): resp = {"choices": [{"message": {"content": "hello world", "thinking": "reasoning"}}]} content, thinking = llm._extract_message(resp) assert content == "hello world" assert thinking == "reasoning" def test_extract_message_empty_content(): resp = {"choices": [{"message": {"content": "", "thinking": None}}]} content, thinking = llm._extract_message(resp) assert content == "" assert thinking == "" def test_extract_message_dict_no_choices(): resp = {"not_choices": []} content, thinking = llm._extract_message(resp) assert content == "" assert thinking == "" def test_extract_message_empty_dict(): resp = {} content, thinking = llm._extract_message(resp) assert content == "" assert thinking == "" def test_extract_delta_text_from_chunk(): chunk = {"choices": [{"delta": {"content": "text"}}]} assert llm._extract_delta_text(chunk) == "text" def test_extract_delta_thinking_from_chunk(): chunk = {"choices": [{"delta": {"thinking": "thought"}}]} assert llm._extract_delta_thinking(chunk) == "thought" def test_call_ollama_no_system(monkeypatch): captured = {} async def fake_post(url, json=None): captured["json"] = json class FakeResp: def raise_for_status(self): pass def json(self): return {"choices": [{"message": {"content": "ok"}}]} return FakeResp() async def fake_client(*args, **kwargs): class Ctx: async def __aenter__(self2): return self2 async def __aexit__(*a): pass post = fake_post return Ctx() monkeypatch.setattr(llm.httpx, "AsyncClient", fake_client) monkeypatch.setattr(llm.asyncio, "wait_for", lambda coro, **kw: coro) result = asyncio.run( llm.call_ollama("user prompt", system_prompt=None, tag="no-system") ) assert result["content"] == "ok" # Should only have user message, no system assert len(captured["json"]["messages"]) == 1 def test_call_ollama_with_system(monkeypatch): captured = {} async def fake_post(url, json=None): captured["json"] = json class FakeResp: def raise_for_status(self): pass def json(self): return {"choices": [{"message": {"content": "ok"}}]} return FakeResp() async def fake_client(*args, **kwargs): class Ctx: async def __aenter__(self2): return self2 async def __aexit__(*a): pass post = fake_post return Ctx() monkeypatch.setattr(llm.httpx, "AsyncClient", fake_client) monkeypatch.setattr(llm.asyncio, "wait_for", lambda coro, **kw: coro) result = asyncio.run( llm.call_ollama("user prompt", system_prompt="sys prompt", tag="with-system") ) assert result["content"] == "ok" # Should have both system and user messages msgs = captured["json"]["messages"] assert len(msgs) == 2 assert msgs[0]["role"] == "system" def test_call_ollama_with_custom_model(monkeypatch): captured = {} async def fake_post(url, json=None): captured["json"] = json class FakeResp: def raise_for_status(self): pass def json(self): return {"choices": [{"message": {"content": "ok"}}]} return FakeResp() async def fake_client(*args, **kwargs): class Ctx: async def __aenter__(self2): return self2 async def __aexit__(*a): pass post = fake_post return Ctx() monkeypatch.setattr(llm.httpx, "AsyncClient", fake_client) monkeypatch.setattr(llm.asyncio, "wait_for", lambda coro, **kw: coro) result = asyncio.run( llm.call_ollama("prompt", model="custom-model") ) assert captured["json"]["model"] == "custom-model" def test_stream_ollama_events_error_handling(monkeypatch): def make_lines(): lines_iter = iter([ 'data: {"error": "model not found"}', ]) class LineIterator: async def __anext__(self): try: return next(lines_iter) except StopIteration: raise StopAsyncIteration() class Response: def __init__(self2): self2._lines = LineIterator() async def raise_for_status(self2): pass async def aiter_lines(self2): return self2._lines class StreamCtx: async def __aenter__(self2): return Response() async def __aexit__(*a): pass class Client: stream = lambda self2, *args, **kw: StreamCtx() return Client() async def fake_client(*args, **kwargs): return make_lines() monkeypatch.setattr(llm.httpx, "AsyncClient", fake_client) monkeypatch.setattr(llm.asyncio, "wait_for", lambda coro, **kw: coro) async def collect(): try: async for _ in llm.stream_ollama_events("prompt", tag="err"): pass except RuntimeError as e: return str(e) result = asyncio.run(collect()) assert "model not found" in str(result) def test_call_vlm_ocr_payload_format(monkeypatch): captured = {} async def fake_post(url, json=None): captured["json"] = json class FakeResp: def raise_for_status(self): pass def json(self): return {"choices": [{"message": {"content": "ocr result"}}]} return FakeResp() async def fake_client(*args, **kwargs): class Ctx: async def __aenter__(self2): return self2 async def __aexit__(*a): pass post = fake_post return Ctx() monkeypatch.setattr(llm.httpx, "AsyncClient", fake_client) monkeypatch.setattr(llm.asyncio, "wait_for", lambda coro, **kw: coro) result = asyncio.run(llm.call_vlm_ocr(b"image")) assert result == "ocr result" # Verify vision format: image_url content part with base64 msgs = captured["json"]["messages"] assert len(msgs) == 1 content_parts = msgs[0]["content"] image_part = [p for p in content_parts if p.get("type") == "image_url"] assert len(image_part) == 1