fix(llm): log expected transient upstream failures at WARNING, not ERROR - #340
Conversation
A flaky upstream provider (e.g. minimax timeouts / 529 overload) was flooding ERROR-level logs/alerts for failures the callers already handle gracefully (FTS fallback for document expansion; skip-dedup for profile consolidation). - LiteLLM client: classify the request-end failure — transient upstream errors (Timeout/APITimeoutError/APIConnectionError/RateLimitError/InternalServerError, and our LLMHardTimeoutError) log at WARNING; genuinely-unexpected errors stay ERROR. Behavior is unchanged — it still raises LiteLLMClientError, so the caller still decides fatality. - Consolidator: the best-effort dedup failure (returns profiles un-deduped) is a graceful degradation → WARNING, not ERROR. Adds unit tests: transient (TimeoutError) → WARNING; unexpected (RuntimeError) → ERROR.
📝 WalkthroughWalkthroughTransient LiteLLM/provider failures now produce warning-level request-end logs, while unexpected failures remain errors. Profile deduplication failures also log warnings and return the existing best-effort fallback result. Tests cover both transient and unexpected request failures. ChangesError observability and fallback handling
Estimated code review effort: 2 (Simple) | ~10 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
🧹 Nitpick comments (1)
tests/server/llm/test_litellm_client_unit.py (1)
1471-1503: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winType-name matching path is untested.
The transient test only exercises the
isinstance(exc, TimeoutError)branch of_is_expected_transient_llm_error. Thetype(exc).__name__ in _TRANSIENT_LLM_ERROR_NAMESbranch — coveringAPITimeoutError,APIConnectionError,RateLimitError,InternalServerError,ServiceUnavailableError— has no test coverage. Consider adding a parametrized test that raises a dummy exception with one of those type names to verify the WARNING classification holds.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@tests/server/llm/test_litellm_client_unit.py` around lines 1471 - 1503, The transient logging tests do not cover the type-name fallback in _is_expected_transient_llm_error. Add a parametrized test alongside test_transient_upstream_error_logged_at_warning that raises dummy exceptions whose class names match each entry in _TRANSIENT_LLM_ERROR_NAMES, then assert the request-end failure records are WARNING and not ERROR while the client still raises LiteLLMClientError.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@tests/server/llm/test_litellm_client_unit.py`:
- Around line 1471-1503: The transient logging tests do not cover the type-name
fallback in _is_expected_transient_llm_error. Add a parametrized test alongside
test_transient_upstream_error_logged_at_warning that raises dummy exceptions
whose class names match each entry in _TRANSIENT_LLM_ERROR_NAMES, then assert
the request-end failure records are WARNING and not ERROR while the client still
raises LiteLLMClientError.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: a8110d89-9fdb-451f-aa0c-0080ea13d63f
📒 Files selected for processing (3)
reflexio/server/llm/_litellm_text_generation.pyreflexio/server/services/profile/components/consolidator.pytests/server/llm/test_litellm_client_unit.py
Log expected transient upstream LLM failures at WARNING, not ERROR
Problem. A batch Sentry alert fired on ERROR-log volume traced to minimax API
instability — intermittent
529(server overloaded) + ~25s timeouts in thedocument-expansion and profile-dedup paths. Both paths already degrade
gracefully (FTS fallback for expansion; keep profiles un-deduped for
consolidation), so there is no user-facing failure — but the underlying
timeout was logged at ERROR in the LiteLLM client's catch-all and in the
consolidator, flooding error alerts for handled fallbacks.
Fix (log-level only — no behavior change).
_litellm_text_generation.py: the request-end failure handler now classifiesthe caught exception. Expected transient upstream errors —
Timeout,APITimeoutError,APIConnectionError,RateLimitError,InternalServerError,ServiceUnavailableError, and our ownLLMHardTimeoutError(aTimeoutErrorsubclass raised when a provider hang iskilled) — log at WARNING; everything else (bugs, auth, malformed structured
output) stays ERROR. It still raises
LiteLLMClientError, so callerscontinue to own fatality. Classified by exception type name to avoid importing
the heavy
litellm/openaiexception hierarchies at module import.consolidator.py: the best-effort dedup failure (returns profiles un-deduped)is a graceful degradation → WARNING, not ERROR.
Tests. New unit tests assert a transient upstream error (
TimeoutError) logsthe request-end failure at WARNING while a genuinely-unexpected error
(
RuntimeError) stays ERROR; both still raise. Fulltest_litellm_client_unit.pyNot in scope (documented in the investigation): the document-expansion 25s
timeout is intentionally short (latency-sensitive; FTS fallback by design) and is
not extended here. If minimax overload becomes chronic, demoting it from
primary for these ops is a separate cost/quality decision.
Summary by CodeRabbit
Bug Fixes
Tests