"""llm.complete: `max_tokens` and `timeout` are threaded into the backend call. The OpenAI client is faked so nothing hits a network. We assert the generation cap reaches the create() call and the fast-fail timeout reaches the client (with max_retries=0 so summary.py owns the retry policy, not the SDK). """ from __future__ import annotations import types import pytest from lyra import llm @pytest.fixture def fake_openai(monkeypatch): recorded: dict = {} class FakeCompletions: def create(self, **kwargs): recorded["create"] = kwargs msg = types.SimpleNamespace(content="ok") return types.SimpleNamespace(choices=[types.SimpleNamespace(message=msg)]) class FakeClient: def __init__(self, **kwargs): recorded["client"] = kwargs self.chat = types.SimpleNamespace(completions=FakeCompletions()) monkeypatch.setattr(llm, "OpenAI", FakeClient) monkeypatch.setattr(llm, "load", lambda: types.SimpleNamespace( mi50_base_url="http://mi50/v1", mi50_model="local-gpu", cloud_model="gpt-4o-mini", openai_api_key="sk-test", local_model="l", )) return recorded def test_mi50_threads_max_tokens_and_timeout(fake_openai): out = llm.complete([{"role": "user", "content": "hi"}], backend="mi50", max_tokens=768, timeout=150) assert out == "ok" assert fake_openai["create"]["max_tokens"] == 768 assert fake_openai["client"]["timeout"] == 150 assert fake_openai["client"]["max_retries"] == 0 def test_cloud_threads_max_tokens_and_timeout(fake_openai): llm.complete([{"role": "user", "content": "hi"}], backend="cloud", max_tokens=768, timeout=150) assert fake_openai["create"]["max_tokens"] == 768 assert fake_openai["client"]["timeout"] == 150 assert fake_openai["client"]["max_retries"] == 0 def test_fallback_uses_primary_when_it_succeeds(monkeypatch): seen = [] monkeypatch.setattr(llm, "complete", lambda messages, backend="local", model=None, **k: seen.append(backend) or f"{backend}-ok") out = llm.complete_with_fallback([{"role": "user", "content": "x"}], backend="local", model="dolphin3:8b") assert out == "local-ok" assert seen == ["local"] # no fallback when the primary works def test_fallback_to_cloud_when_primary_errors(monkeypatch): monkeypatch.setattr(llm, "load", lambda: types.SimpleNamespace(openai_api_key="sk")) seen = [] def fake(messages, backend="local", model=None, **k): seen.append(backend) if backend == "local": raise RuntimeError("3090 is powered off") return "cloud-ok" monkeypatch.setattr(llm, "complete", fake) out = llm.complete_with_fallback([{"role": "user", "content": "x"}], backend="local", model="dolphin3:8b") assert out == "cloud-ok" assert seen == ["local", "cloud"] def test_fallback_reraises_when_primary_is_already_cloud(monkeypatch): monkeypatch.setattr(llm, "load", lambda: types.SimpleNamespace(openai_api_key="sk")) monkeypatch.setattr(llm, "complete", lambda *a, **k: (_ for _ in ()).throw(RuntimeError("boom"))) with pytest.raises(RuntimeError): llm.complete_with_fallback([{"role": "user", "content": "x"}], backend="cloud") def test_fallback_reraises_without_openai_key(monkeypatch): monkeypatch.setattr(llm, "load", lambda: types.SimpleNamespace(openai_api_key="")) monkeypatch.setattr(llm, "complete", lambda *a, **k: (_ for _ in ()).throw(RuntimeError("down"))) with pytest.raises(RuntimeError): llm.complete_with_fallback([{"role": "user", "content": "x"}], backend="local") def test_default_bounds_calls_even_without_explicit_timeout(fake_openai): # No cap / timeout passed -> still bounded: 300s default + no SDK retries, so # no call can silently inherit the SDK's 600s x2 (~30 min). No length cap # unless asked, though. llm.complete([{"role": "user", "content": "hi"}], backend="mi50") assert "max_tokens" not in fake_openai["create"] assert fake_openai["client"]["timeout"] == 300 assert fake_openai["client"]["max_retries"] == 0