test_llm_empty_response.py 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155
  1. import json
  2. import os
  3. import tempfile
  4. import unittest
  5. from pathlib import Path
  6. from types import SimpleNamespace
  7. from unittest.mock import patch
  8. from llm_client.cache import build_cache_key
  9. from llm_client.client import LLMClient
  10. class _FakeCompletions:
  11. def __init__(self, contents):
  12. self.contents = iter(contents)
  13. self.calls = 0
  14. self.kwargs = []
  15. def create(self, **_kwargs):
  16. self.calls += 1
  17. self.kwargs.append(_kwargs)
  18. content = next(self.contents)
  19. return SimpleNamespace(
  20. choices=[SimpleNamespace(message=SimpleNamespace(content=content))]
  21. )
  22. class LlmEmptyResponseTests(unittest.TestCase):
  23. def _client(self, contents):
  24. completions = _FakeCompletions(contents)
  25. client = object.__new__(LLMClient)
  26. client.model = "test-model"
  27. client.cfg = SimpleNamespace(generate_max_tokens=65536)
  28. client.client = SimpleNamespace(
  29. chat=SimpleNamespace(completions=completions)
  30. )
  31. return client, completions
  32. @staticmethod
  33. def _config():
  34. return SimpleNamespace(
  35. llm=SimpleNamespace(
  36. generate_max_tokens=100,
  37. generate_temperature=0.4,
  38. ),
  39. max_retries=2,
  40. )
  41. def test_blank_api_response_is_retried_and_not_cached(self):
  42. client, completions = self._client([" ", "有效正文"])
  43. with tempfile.TemporaryDirectory() as temp_dir, patch.dict(
  44. os.environ,
  45. {"BID_LLM_CACHE": "1", "BID_LLM_CACHE_DIR": temp_dir},
  46. ), patch("llm_client.client.get_config", return_value=self._config()), patch(
  47. "llm_client.client.time.sleep"
  48. ):
  49. result = client.generate("system", "user", max_tokens=100)
  50. self.assertEqual(result, "有效正文")
  51. self.assertEqual(completions.calls, 2)
  52. cache_files = list(Path(temp_dir).glob("*.json"))
  53. self.assertEqual(len(cache_files), 1)
  54. cached = json.loads(cache_files[0].read_text(encoding="utf-8"))
  55. self.assertEqual(cached["content"], "有效正文")
  56. def test_existing_blank_cache_is_removed_before_request(self):
  57. client, completions = self._client(["重新生成的正文"])
  58. with tempfile.TemporaryDirectory() as temp_dir, patch.dict(
  59. os.environ,
  60. {"BID_LLM_CACHE": "1", "BID_LLM_CACHE_DIR": temp_dir},
  61. ), patch("llm_client.client.get_config", return_value=self._config()):
  62. key = build_cache_key(
  63. model="test-model",
  64. system_prompt="system",
  65. user_prompt="user",
  66. max_tokens=100,
  67. temperature=0.4,
  68. response_format=None,
  69. )
  70. cache_path = Path(temp_dir) / f"{key}.json"
  71. cache_path.write_text(
  72. json.dumps({"model": "test-model", "content": ""}),
  73. encoding="utf-8",
  74. )
  75. result = client.generate("system", "user", max_tokens=100)
  76. self.assertEqual(result, "重新生成的正文")
  77. self.assertEqual(completions.calls, 1)
  78. cached = json.loads(cache_path.read_text(encoding="utf-8"))
  79. self.assertEqual(cached["content"], "重新生成的正文")
  80. def test_blank_response_retries_double_max_tokens_four_times(self):
  81. client, completions = self._client(
  82. [" ", " ", " ", " ", "有效正文"]
  83. )
  84. with tempfile.TemporaryDirectory() as temp_dir, patch.dict(
  85. os.environ,
  86. {"BID_LLM_CACHE": "1", "BID_LLM_CACHE_DIR": temp_dir},
  87. ), patch(
  88. "llm_client.client.get_config",
  89. return_value=SimpleNamespace(
  90. llm=SimpleNamespace(
  91. generate_max_tokens=65536,
  92. generate_temperature=0.4,
  93. ),
  94. max_retries=4,
  95. ),
  96. ), patch("llm_client.client.time.sleep"):
  97. result = client.generate("system", "user", max_tokens=8192)
  98. self.assertEqual(result, "有效正文")
  99. self.assertEqual(completions.calls, 5)
  100. self.assertEqual(
  101. [kwargs["max_tokens"] for kwargs in completions.kwargs],
  102. [8192, 16384, 32768, 65536, 65536],
  103. )
  104. def test_json_parse_failure_retries_double_max_tokens_four_times(self):
  105. client, completions = self._client([
  106. "{not json",
  107. "{not json",
  108. "{not json",
  109. "{not json",
  110. "{not json",
  111. ])
  112. with tempfile.TemporaryDirectory() as temp_dir, patch.dict(
  113. os.environ,
  114. {"BID_LLM_CACHE": "1", "BID_LLM_CACHE_DIR": temp_dir},
  115. ), patch(
  116. "llm_client.client.get_config",
  117. return_value=SimpleNamespace(
  118. llm=SimpleNamespace(
  119. generate_max_tokens=65536,
  120. generate_temperature=0.4,
  121. ),
  122. max_retries=4,
  123. ),
  124. ), patch("llm_client.client.time.sleep"):
  125. result = client.extract_json(
  126. system_prompt="system",
  127. user_prompt="user",
  128. max_tokens=8192,
  129. )
  130. self.assertEqual(result, {})
  131. self.assertEqual(completions.calls, 5)
  132. self.assertEqual(
  133. [kwargs["max_tokens"] for kwargs in completions.kwargs],
  134. [8192, 16384, 32768, 65536, 65536],
  135. )
  136. if __name__ == "__main__":
  137. unittest.main()