from types import SimpleNamespace from typing import Any import pytest from pydantic import BaseModel from resume_agent.llm import LLMError, OpenAILLM class LoggedResult(BaseModel): answer: str def fake_client(parsed: BaseModel) -> Any: class Completions: def parse(self, **kwargs: Any) -> Any: return SimpleNamespace( choices=[SimpleNamespace(message=SimpleNamespace(parsed=parsed))] ) return SimpleNamespace( base_url="https://llm.example/v1/", chat=SimpleNamespace(completions=Completions()), ) def test_custom_endpoint_defaults_to_chat(monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setenv("RESUME_AGENT_API_KEY", "test-token") monkeypatch.setenv("RESUME_AGENT_BASE_URL", "https://llm.example/v1") monkeypatch.setenv("RESUME_AGENT_MODEL", "example-model") monkeypatch.delenv("RESUME_AGENT_API_STYLE", raising=False) monkeypatch.delenv("RESUME_AGENT_LLM_TIMEOUT_SECONDS", raising=False) llm = OpenAILLM() assert llm.model == "example-model" assert llm.api_style == "chat" assert llm.timeout_seconds == 1800 assert str(llm.client.base_url) == "https://llm.example/v1/" def test_custom_endpoint_allows_model_discovery(monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setenv("RESUME_AGENT_API_KEY", "test-token") monkeypatch.setenv("RESUME_AGENT_BASE_URL", "https://llm.example/v1") monkeypatch.delenv("RESUME_AGENT_MODEL", raising=False) llm = OpenAILLM() assert llm.model is None def test_parse_requires_model(monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setenv("RESUME_AGENT_API_KEY", "test-token") monkeypatch.setenv("RESUME_AGENT_BASE_URL", "https://llm.example/v1") monkeypatch.delenv("RESUME_AGENT_MODEL", raising=False) with pytest.raises(LLMError, match="RESUME_AGENT_MODEL"): OpenAILLM().parse(dict, "instructions", "input") # type: ignore[type-var] def test_custom_llm_timeout(monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setenv("RESUME_AGENT_API_KEY", "test-token") monkeypatch.setenv("RESUME_AGENT_BASE_URL", "https://llm.example/v1") monkeypatch.setenv("RESUME_AGENT_MODEL", "example-model") monkeypatch.setenv("RESUME_AGENT_LLM_TIMEOUT_SECONDS", "2700") llm = OpenAILLM() assert llm.timeout_seconds == 2700 assert llm.client.timeout.read == 2700 assert llm.client.timeout.connect == 30 def test_llm_timeout_must_be_positive(monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setenv("RESUME_AGENT_API_KEY", "test-token") monkeypatch.setenv("RESUME_AGENT_BASE_URL", "https://llm.example/v1") monkeypatch.setenv("RESUME_AGENT_MODEL", "example-model") monkeypatch.setenv("RESUME_AGENT_LLM_TIMEOUT_SECONDS", "0") with pytest.raises(LLMError, match="greater than zero"): OpenAILLM() def test_model_queries_log_payloads_to_stdout( monkeypatch: pytest.MonkeyPatch, caplog: pytest.LogCaptureFixture, ) -> None: monkeypatch.setenv("RESUME_AGENT_API_KEY", "test-token") monkeypatch.setenv("RESUME_AGENT_BASE_URL", "https://llm.example/v1") monkeypatch.setenv("RESUME_AGENT_MODEL", "example-model") monkeypatch.setenv("RESUME_AGENT_LOG_MODEL_PAYLOADS", "true") llm = OpenAILLM() llm.client = fake_client(LoggedResult(answer="done")) with caplog.at_level("INFO", logger="resume_agent.model"): llm.parse(LoggedResult, "System logging test", "Private resume input") output = caplog.text assert "event=start" in output assert "System logging test" in output assert "Private resume input" in output assert '"answer": "done"' in output assert "test-token" not in output def test_model_payload_logging_can_be_disabled( monkeypatch: pytest.MonkeyPatch, caplog: pytest.LogCaptureFixture, ) -> None: monkeypatch.setenv("RESUME_AGENT_API_KEY", "test-token") monkeypatch.setenv("RESUME_AGENT_BASE_URL", "https://llm.example/v1") monkeypatch.setenv("RESUME_AGENT_MODEL", "example-model") monkeypatch.setenv("RESUME_AGENT_LOG_MODEL_PAYLOADS", "false") llm = OpenAILLM() llm.client = fake_client(LoggedResult(answer="done")) with caplog.at_level("INFO", logger="resume_agent.model"): llm.parse(LoggedResult, "Hidden system prompt", "Hidden resume input") output = caplog.text assert "event=start" in output assert "Hidden system prompt" not in output assert "Hidden resume input" not in output