refactor(errors): scope bug report link to unmapped exceptions

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
ryan 2026-09-20 01:26:04 +00:00
parent 38475d7414
commit 2ab2cdd87a
4 changed files with 18 additions and 76 deletions

View file

@ -17,6 +17,7 @@ MAX_URL_LENGTH: Final = 6000
MAX_MESSAGE_CHARS: Final = 600
MAX_FRAMES: Final = 12
DISABLE_ENV_VAR: Final = "LITELLM_DISABLE_BUG_REPORT_LINK"
_SHORTENED_MESSAGE_LENGTHS: Final = (480, 360, 240, 120, 0)
Surface = Literal["sdk", "proxy"]
@ -51,7 +52,7 @@ def _format_frame(frame: traceback.FrameSummary, package_dir: Path, package_pare
def _get_litellm_frames(exc: BaseException) -> tuple[str, ...]:
package_file: Final = getattr(litellm, "__file__", None)
if package_file is None or exc.__traceback__ is None:
if not isinstance(package_file, str) or exc.__traceback__ is None:
return ()
package_dir: Final = Path(package_file).resolve().parent
package_parent: Final = package_dir.parent
@ -116,7 +117,7 @@ def _description(report: BugReport, message: str, frames: tuple[str, ...]) -> st
def _issue_url(report: BugReport, message: str, frames: tuple[str, ...]) -> str:
deployment: tuple[tuple[str, str], ...] = (
deployment: Final[tuple[tuple[str, str], ...]] = (
(("deployment", "pip / Python SDK"),)
if report.surface == "sdk"
else (("deployment", "Docker"),)
@ -135,33 +136,17 @@ def _issue_url(report: BugReport, message: str, frames: tuple[str, ...]) -> str:
def bug_report_issue_url(report: BugReport) -> str:
frame_candidates: Final = tuple(report.litellm_frames[index:] for index in range(len(report.litellm_frames) + 1))
message_lengths: Final = (
len(report.exception_message),
480,
360,
240,
120,
0,
frames: Final = report.litellm_frames
message: Final = report.exception_message
candidates: Final = (
*(_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)
),
)
frame_candidates_with_full_message: Final = (
_issue_url(report, report.exception_message[:message_length], frames)
for frames in frame_candidates
for message_length in (len(report.exception_message),)
)
shortest_candidate: Final = _issue_url(report, "", ())
full_message_candidate: Final = next(
(candidate for candidate in frame_candidates_with_full_message if len(candidate) <= MAX_URL_LENGTH),
None,
)
if full_message_candidate is not None:
return full_message_candidate
shortened_candidates: Final = (
_issue_url(report, report.exception_message[:message_length], ())
for message_length in message_lengths
if message_length <= len(report.exception_message)
)
return next((candidate for candidate in shortened_candidates if len(candidate) <= MAX_URL_LENGTH), shortest_candidate)
return next((candidate for candidate in candidates if len(candidate) <= MAX_URL_LENGTH), _issue_url(report, "", ()))
def bug_report_notice(report: BugReport) -> str:

View file

@ -515,24 +515,8 @@ def _map_openai_exception(
else:
# if no status code then it is an APIConnectionError: https://github.com/openai/openai-python#handling-errors
# exception_mapping_worked = True
bug_report_message: Final = (
f"{exception_provider} - {message}"
+ (
"\n"
+ bug_report_notice(
build_bug_report(
original_exception,
surface="sdk",
model=model,
custom_llm_provider=custom_llm_provider,
)
)
if not hasattr(original_exception, "request") and bug_report_enabled()
else ""
)
)
raise APIConnectionError(
message=f"APIConnectionError: {bug_report_message}",
message=f"APIConnectionError: {exception_provider} - {message}",
llm_provider=custom_llm_provider,
model=model,
litellm_debug_info=extra_information,
@ -2702,11 +2686,11 @@ def exception_type(
build_bug_report(
original_exception,
surface="sdk",
model=model,
custom_llm_provider=custom_llm_provider,
model=cast(str | None, model),
custom_llm_provider=cast(str | None, custom_llm_provider),
)
)
if bug_report_enabled()
if bug_report_enabled() and isinstance(original_exception, BaseException)
else ""
)
),

View file

@ -11,10 +11,9 @@ from litellm.litellm_core_utils.bug_report import (
DISABLE_ENV_VAR,
ISSUE_URL_BASE,
MAX_URL_LENGTH,
BugReport,
build_bug_report,
bug_report_enabled,
bug_report_issue_url,
build_bug_report,
)
from litellm.litellm_core_utils.get_llm_provider_logic import get_llm_provider

View file

@ -3,8 +3,6 @@ import openai
import pytest
import litellm
from litellm.litellm_core_utils.bug_report import DISABLE_ENV_VAR, ISSUE_URL_BASE
from litellm.litellm_core_utils.exception_mapping_utils import (
ExceptionCheckers,
_get_body_error_code,
@ -974,30 +972,6 @@ def test_an_unmapped_exception_with_no_model_or_provider_is_a_connection_error(q
assert "boom" in raised.value.message
def test_unmapped_sdk_exception_includes_bug_report_link(quiet_exception_mapping):
with pytest.raises(litellm.APIConnectionError) as raised:
exception_type(
model="gpt-4",
original_exception=ValueError("boom"),
custom_llm_provider="openai",
)
assert ISSUE_URL_BASE in str(raised.value)
def test_unmapped_sdk_exception_bug_report_link_can_be_disabled(quiet_exception_mapping, monkeypatch):
monkeypatch.setenv(DISABLE_ENV_VAR, "true")
with pytest.raises(litellm.APIConnectionError) as raised:
exception_type(
model="gpt-4",
original_exception=ValueError("boom"),
custom_llm_provider="openai",
)
assert ISSUE_URL_BASE not in str(raised.value)
def _raise_and_map(model: str | None, original_exception: Exception, custom_llm_provider: str | None) -> None:
"""Calls exception_type() from inside the except block, as litellm/main.py does,
so traceback.format_exc() has a real stack."""