fix(proxy): keep bug report notice out of client error bodies

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
ryan 2026-09-20 01:42:38 +00:00
parent 919d8a59c3
commit fd0398d837
5 changed files with 64 additions and 6 deletions

View file

@ -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)}"
)

View file

@ -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),

View file

@ -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,

View file

@ -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"

View file

@ -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