diff --git a/litellm/litellm_core_utils/bug_report.py b/litellm/litellm_core_utils/bug_report.py index 54502423b0c..ba1a78e8c07 100644 --- a/litellm/litellm_core_utils/bug_report.py +++ b/litellm/litellm_core_utils/bug_report.py @@ -1,5 +1,6 @@ from __future__ import annotations +import dataclasses import os import platform import traceback @@ -15,8 +16,10 @@ from litellm._version import version as litellm_version ISSUE_URL_BASE: Final = "https://github.com/BerriAI/litellm/issues/new" MAX_URL_LENGTH: Final = 6000 MAX_MESSAGE_CHARS: Final = 600 +MAX_CONTEXT_CHARS: Final = 200 MAX_FRAMES: Final = 12 DISABLE_ENV_VAR: Final = "LITELLM_DISABLE_BUG_REPORT_LINK" +NOTICE_PREFIX: Final = "This looks like a bug in LiteLLM rather than in your request." _SHORTENED_MESSAGE_LENGTHS: Final = (480, 360, 240, 120, 0) Surface = Literal["sdk", "proxy"] @@ -66,7 +69,7 @@ def _get_litellm_frames(exc: BaseException) -> tuple[str, ...]: def _redact_context(value: str | None) -> str | None: if value is None: return None - return redact_secrets(value)[:MAX_MESSAGE_CHARS] + return redact_secrets(value)[:MAX_CONTEXT_CHARS] def build_bug_report( @@ -142,11 +145,19 @@ def bug_report_issue_url(report: BugReport) -> str: *(_issue_url(report, message, frames[index:]) for index in range(len(frames) + 1)), *(_issue_url(report, message[:length], ()) for length in _SHORTENED_MESSAGE_LENGTHS if length < len(message)), ) - return next((candidate for candidate in candidates if len(candidate) <= MAX_URL_LENGTH), _issue_url(report, "", ())) + return next( + (candidate for candidate in candidates if len(candidate) <= MAX_URL_LENGTH), + _issue_url(dataclasses.replace(report, call_type=None, model=None, custom_llm_provider=None), "", ()), + ) + + +def strip_bug_report_notice(message: str) -> str: + index: Final = message.find(NOTICE_PREFIX) + return message if index == -1 else message[:index].rstrip() def bug_report_notice(report: BugReport) -> str: return ( - "This looks like a bug in LiteLLM rather than in your request. File it with one click " + f"{NOTICE_PREFIX} File it with one click " f"(prefilled and redacted, review before submitting): {bug_report_issue_url(report)}" ) diff --git a/litellm/proxy/common_request_processing.py b/litellm/proxy/common_request_processing.py index f9602a0f663..ed867d900ea 100644 --- a/litellm/proxy/common_request_processing.py +++ b/litellm/proxy/common_request_processing.py @@ -50,6 +50,7 @@ from litellm.litellm_core_utils.bug_report import ( bug_report_enabled, bug_report_notice, build_bug_report, + strip_bug_report_notice, ) from litellm.litellm_core_utils.core_helpers import ( get_or_create_metadata_bucket, @@ -3681,8 +3682,11 @@ class ProxyBaseLLMRequestProcessing: ) ) ) + client_message: Final = getattr(e, "message", error_msg) raise ProxyException( - message=redact_internal_details_from_client_message(getattr(e, "message", error_msg)), + message=redact_internal_details_from_client_message( + strip_bug_report_notice(client_message) if isinstance(client_message, str) else error_msg + ), type=openai_error_type(e, _code), param=openai_error_param(e), openai_code=getattr(e, "code", None), diff --git a/litellm/proxy/utils.py b/litellm/proxy/utils.py index 95f958c6c01..3216c370126 100644 --- a/litellm/proxy/utils.py +++ b/litellm/proxy/utils.py @@ -59,6 +59,7 @@ from litellm.litellm_core_utils.bug_report import ( bug_report_enabled, bug_report_notice, build_bug_report, + strip_bug_report_notice, ) from litellm.proxy._types import ( CommonProxyErrors, @@ -7844,7 +7845,7 @@ def handle_exception_on_proxy(e: Exception, litellm_call_id: str | None = None) if _status_code == status.HTTP_500_INTERNAL_SERVER_ERROR and bug_report_enabled() and isinstance(e, BaseException): verbose_proxy_logger.error(bug_report_notice(build_bug_report(e, surface="proxy"))) return ProxyException( - message=str(e), + message=strip_bug_report_notice(str(e)), type=ProxyErrorTypes.internal_server_error, param=openai_error_param(e), headers=headers, diff --git a/tests/test_litellm/litellm_core_utils/test_bug_report.py b/tests/test_litellm/litellm_core_utils/test_bug_report.py index 18c36308473..bf4fe92a934 100644 --- a/tests/test_litellm/litellm_core_utils/test_bug_report.py +++ b/tests/test_litellm/litellm_core_utils/test_bug_report.py @@ -13,7 +13,9 @@ from litellm.litellm_core_utils.bug_report import ( MAX_URL_LENGTH, bug_report_enabled, bug_report_issue_url, + bug_report_notice, build_bug_report, + strip_bug_report_notice, ) from litellm.litellm_core_utils.get_llm_provider_logic import get_llm_provider @@ -54,6 +56,18 @@ def test_issue_url_is_bounded_for_long_messages(): assert "RuntimeError" in query["description"][0] +def test_issue_url_is_bounded_for_long_non_ascii_context(): + report = build_bug_report( + ValueError("故障" * 10_000), + surface="sdk", + model="模型" * 300, + custom_llm_provider="供給" * 300, + call_type="呼出" * 300, + ) + + assert len(bug_report_issue_url(report)) <= MAX_URL_LENGTH + + def test_issue_url_builds_without_a_traceback(): exc = RuntimeError("no traceback") assert exc.__traceback__ is None @@ -80,3 +94,11 @@ def test_proxy_provider_uses_translation_domain(): query = parse_qs(urlparse(bug_report_issue_url(report)).query) assert query["domain"] == ["LLM translation: a specific provider's request or response"] + + +def test_strip_bug_report_notice(): + report = build_bug_report(RuntimeError("boom"), surface="sdk") + notice = bug_report_notice(report) + + assert strip_bug_report_notice(f"boom\n{notice}") == "boom" + assert strip_bug_report_notice("boom") == "boom" diff --git a/tests/test_litellm/proxy/test_common_request_processing.py b/tests/test_litellm/proxy/test_common_request_processing.py index 1f698f179aa..cdae971b838 100644 --- a/tests/test_litellm/proxy/test_common_request_processing.py +++ b/tests/test_litellm/proxy/test_common_request_processing.py @@ -14,7 +14,12 @@ from fastapi.responses import JSONResponse, StreamingResponse import litellm from litellm._uuid import uuid -from litellm.litellm_core_utils.bug_report import DISABLE_ENV_VAR, ISSUE_URL_BASE +from litellm.litellm_core_utils.bug_report import ( + DISABLE_ENV_VAR, + ISSUE_URL_BASE, + bug_report_notice, + build_bug_report, +) from litellm.constants import ( CLIENT_REQUESTED_MODEL_SCOPE_KEY, MAX_LITELLM_CALL_ID_LENGTH, @@ -4259,6 +4264,21 @@ class TestHandleLLMApiExceptionRetryAfter: proxy_exc = await self._invoke(ValueError("some other failure")) assert "retry-after" not in proxy_exc.headers + async def test_handle_llm_api_exception_strips_bug_report_notice_from_client_message(self, caplog): + report = build_bug_report(RuntimeError("boom"), surface="sdk") + notice = bug_report_notice(report) + exc = litellm.APIConnectionError( + message=f"boom\n{notice}", + model="gpt-4o", + llm_provider="openai", + ) + + with caplog.at_level("ERROR"): + proxy_exc = await self._invoke(exc) + + assert ISSUE_URL_BASE not in proxy_exc.message + assert ISSUE_URL_BASE in caplog.text + async def test_handle_llm_api_exception_retry_after_survives_callback_headers(self): from litellm.types.router import RouterRateLimitError