Skip to content

Commit 3632a93

Browse files
feat: add num_retries configuration to LLMClient and LLMConfig with validation
1 parent 08ab237 commit 3632a93

2 files changed

Lines changed: 45 additions & 0 deletions

File tree

app/llm_provider.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
logger = logging.getLogger(__name__)
2020
DEFAULT_LLM_TIMEOUT_SECONDS = 60
21+
DEFAULT_LLM_RETRIES = 2
2122

2223
# Suppress noisy logging from litellm/openai unless error/warning
2324
litellm.set_verbose = False
@@ -118,11 +119,14 @@ class LLMConfig:
118119
site_name: Optional[str] = None
119120
send_site_info: bool = True
120121
timeout_seconds: int = DEFAULT_LLM_TIMEOUT_SECONDS
122+
num_retries: int = DEFAULT_LLM_RETRIES
121123

122124
def __post_init__(self):
123125
"""Validate configuration after initialization."""
124126
if not self.model:
125127
raise ValueError("Model name is required")
128+
if self.num_retries < 0:
129+
raise ValueError("Number of retries cannot be negative")
126130

127131

128132
class LLMClient:
@@ -204,6 +208,7 @@ def chat_completion(
204208
"temperature": temperature,
205209
"max_tokens": kwargs.pop("max_tokens", 4096),
206210
"timeout": kwargs.pop("timeout", self.config.timeout_seconds),
211+
"num_retries": kwargs.pop("num_retries", self.config.num_retries),
207212
**kwargs,
208213
}
209214

app/tests/test_translation.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -656,6 +656,46 @@ def test_llm_client_accepts_fenced_raw_string_batch_json(self):
656656
[("hello", "Hola"), ("goodbye", "Adiós")],
657657
)
658658
self.assertEqual(mock_completion.call_args.kwargs["timeout"], 60)
659+
self.assertEqual(mock_completion.call_args.kwargs["num_retries"], 2)
660+
661+
def test_llm_client_allows_retry_override(self):
662+
"""Callers can override the default LiteLLM retry count per request."""
663+
664+
response = SimpleNamespace(
665+
choices=[
666+
SimpleNamespace(
667+
message=SimpleNamespace(
668+
content='{"translations": [{"key": "hello", "translation": "Hola"}]}',
669+
reasoning_content=None,
670+
)
671+
)
672+
]
673+
)
674+
llm_config = LLMConfig(
675+
provider="openrouter", model="openrouter/owl-alpha", num_retries=4
676+
)
677+
678+
with patch(
679+
"llm_provider.litellm.completion", return_value=response
680+
) as mock_completion:
681+
LLMClient(llm_config).chat_completion(
682+
messages=[],
683+
response_model=StringBatchTranslation,
684+
temperature=0,
685+
num_retries=1,
686+
)
687+
688+
self.assertEqual(mock_completion.call_args.kwargs["num_retries"], 1)
689+
690+
def test_llm_config_rejects_negative_retries(self):
691+
"""Retry count must not disable validation by going negative."""
692+
693+
with self.assertRaisesRegex(ValueError, "Number of retries cannot be negative"):
694+
LLMConfig(
695+
provider="openrouter",
696+
model="openrouter/owl-alpha",
697+
num_retries=-1,
698+
)
659699

660700
def test_llm_client_accepts_dict_style_message(self):
661701
"""LiteLLM responses can expose message data with dict-style access."""

0 commit comments

Comments
 (0)