Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
ecc2a94
fix(passthrough): relay upstream error streams while the log preview …
yucheng-berri Sep 24, 2026
b0d6574
fix(passthrough): finish the upstream error report when the client di…
yucheng-berri Sep 24, 2026
e45275a
fix(passthrough): hand the upstream error report to the logging worke…
yucheng-berri Sep 24, 2026
b6ba92a
test(integration): audit cells for passthrough error relay across rou…
yucheng-berri Sep 24, 2026
db83435
fix(passthrough): await the shielded upstream error report at stream …
yucheng-berri Sep 25, 2026
1f8714c
test(integration): assert the distinct read-failure and report warnin…
yucheng-berri Sep 25, 2026
3d2bb74
fix(passthrough): re-raise post-preview upstream aborts and own error…
yucheng-berri Sep 25, 2026
f7c1570
fix(passthrough): clamp report concurrency and drain reports before s…
yucheng-berri Sep 25, 2026
9045a2d
test(passthrough): drop the fastapi import from the normalization tests
yucheng-berri Sep 25, 2026
bf803cd
Merge remote-tracking branch 'origin/main' into litellm_passthrough_e…
yucheng-berri Sep 25, 2026
391d8f0
fix(passthrough): await own report without a shared limiter, register…
yucheng-berri Sep 25, 2026
a614d85
test(proxy): cover shutdown flush ordering functionally
yucheng-berri Sep 25, 2026
3f708fa
fix(passthrough): run the upstream error report as a response backgro…
yucheng-berri Sep 25, 2026
58d02d5
Merge remote-tracking branch 'origin/main' into litellm_passthrough_e…
yucheng-berri Sep 25, 2026
d12afe1
Merge remote-tracking branch 'origin/main' into litellm_passthrough_e…
yucheng-berri Sep 26, 2026
6a4a17f
test(passthrough): poll for the parked hook and assert disconnect row…
yucheng-berri Sep 26, 2026
3da1a16
test(integration): let gated replies wait as long as the spend-row polls
yucheng-berri Sep 26, 2026
4cc5a8c
test(integration): release the upstream gate if the disconnect burst …
yucheng-berri Sep 26, 2026
9767391
fix(passthrough): report upstream errors from the relay stream and wa…
yucheng-berri Sep 26, 2026
08c3bf6
fix(proxy): run late-relay background tasks for non-streaming respons…
yucheng-berri Sep 26, 2026
dfec7c2
test(integration): shape the round 4 chaos cells after the gauntlet r…
yucheng-berri Sep 26, 2026
12d0145
test(passthrough): build the relayed error tail without rebinding a F…
yucheng-berri Sep 26, 2026
18cd44c
fix(passthrough): dispatch the upstream error report at the preview b…
yucheng-berri Sep 26, 2026
b04c983
test(passthrough): cover round 5 dispatch, bounded teardown, and boun…
yucheng-berri Sep 26, 2026
e4fb417
fix(passthrough): mask a private key block that straddles the redacti…
yucheng-berri Sep 26, 2026
7267f32
fix(passthrough): redact the upstream error preview in original coord…
yucheng-berri Sep 26, 2026
0005428
fix(passthrough): mask dangling private keys after escaped newlines a…
yucheng-berri Sep 26, 2026
b84ae93
fix(passthrough): mask dangling private keys by base64 shape instead …
yucheng-berri Sep 26, 2026
2cace25
fix(passthrough): mask private keys wrapped into short base64 lines
yucheng-berri Sep 26, 2026
cd8aabd
test(passthrough): pin the 16 char token floor of the wrapped key mask
yucheng-berri Sep 26, 2026
823c5c8
chore: merge main into litellm_passthrough_error_preview_stream
yucheng-berri Sep 26, 2026
cf5659f
fix(passthrough): close the upstream and log the right reason when th…
yucheng-berri Sep 26, 2026
5beef60
fix(passthrough): keep the original exception when closing an abandon…
yucheng-berri Sep 26, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions litellm/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -1529,6 +1529,11 @@
MCP_TOOL_NAME_PREFIX: Final = "mcp_tool"
MAXIMUM_TRACEBACK_LINES_TO_LOG: Final = int(os.getenv("MAXIMUM_TRACEBACK_LINES_TO_LOG", 100))
PASSTHROUGH_UPSTREAM_ERROR_BODY_MAX_LOG_CHARS: Final = 4096
PASSTHROUGH_UPSTREAM_ERROR_REPORT_TASK_NAME: Final = "passthrough-upstream-error-report"
PASSTHROUGH_UPSTREAM_ERROR_REPORT_SHUTDOWN_WAIT_SECONDS: Final = 10.0
PASSTHROUGH_UPSTREAM_ERROR_REDACT_MARGIN_CHARS: Final = 256
PASSTHROUGH_UPSTREAM_ERROR_BODY_MAX_SCAN_BYTES: Final = 65536
SSE_LATE_RELAY_BACKGROUND_WAIT_SECONDS: Final = 10.0

# Headers to control callbacks
X_LITELLM_DISABLE_CALLBACKS: Final = "x-litellm-disable-callbacks"
Expand Down
9 changes: 7 additions & 2 deletions litellm/litellm_core_utils/error_normalization.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,9 @@ class _HasProxyErrorType(Protocol):
type: str


_UPSTREAM_PASSTHROUGH_PATTERN: Final = re.compile(r"upstream passthrough request failed", re.IGNORECASE)

_MESSAGE_PATTERNS: Final[tuple[tuple[re.Pattern[str], str], ...]] = (
(re.compile(r"upstream passthrough request failed", re.IGNORECASE), UPSTREAM_PASSTHROUGH),
(
re.compile(r"budget has been exceeded|max budget|crossed budget", re.IGNORECASE),
BUDGET_EXCEEDED,
Expand Down Expand Up @@ -192,7 +193,11 @@ def normalize_error(exc: Exception | None, status_code: str, message: str) -> st
if by_proxy_type is not None:
return by_proxy_type
by_message: Final = (
BUDGET_EXCEEDED if _exceeded_before_budget(message) else _classify_by_message(message, _MESSAGE_PATTERNS)
UPSTREAM_PASSTHROUGH
if _UPSTREAM_PASSTHROUGH_PATTERN.search(message)
else BUDGET_EXCEEDED
if _exceeded_before_budget(message)
else _classify_by_message(message, _MESSAGE_PATTERNS)
)
if by_message is not None:
return by_message
Expand Down
45 changes: 29 additions & 16 deletions litellm/proxy/common_request_processing.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
MAX_PAYLOAD_SIZE_FOR_DEBUG_LOG,
NON_INFERENCE_CALL_TYPES,
RETURN_RAW_MODEL_NAME_METADATA_KEY,
SSE_LATE_RELAY_BACKGROUND_WAIT_SECONDS,
STREAM_SSE_DATA_PREFIX,
STREAM_SSE_KEEPALIVE_PING_BYTES,
UNSAFE_PROXY_RESPONSE_HEADERS,
Expand Down Expand Up @@ -1141,27 +1142,39 @@
return resolve_ttft_keepalive_interval(deployments, litellm.sse_keepalive_ping_interval_seconds)


async def _aclose_late_response(produced: Response) -> None:
async def _aclose_late_response(
produced: Response, *, background_wait_seconds: float = SSE_LATE_RELAY_BACKGROUND_WAIT_SECONDS
) -> None:
"""Run the cleanup Starlette would have run, for a response it never called.

Closing an already-closed async generator is a no-op, so this is safe to call
from both the relay's own teardown and the outer one.
"""
if not isinstance(produced, StreamingResponse):
return
targets: Final = (
(produced.body_iterator, produced.upstream_generator)
if isinstance(produced, _UpstreamClosingStreamingResponse)
else (produced.body_iterator,)
)
for target in targets:
aclose = getattr(target, "aclose", None)
if aclose is None:
continue
try:
await aclose()
except BaseException as exc: # noqa: BLE001 # teardown must not mask why the stream ended
verbose_proxy_logger.debug("error closing relayed streaming generator: %s", exc)
if isinstance(produced, StreamingResponse):
targets: Final = (
(produced.body_iterator, produced.upstream_generator)
if isinstance(produced, _UpstreamClosingStreamingResponse)
else (produced.body_iterator,)
)
for target in targets:
aclose = getattr(target, "aclose", None)
if aclose is None:
continue
try:
await aclose()
except BaseException as exc: # noqa: BLE001 # teardown must not mask why the stream ended
Comment thread
devin-ai-integration[bot] marked this conversation as resolved.
verbose_proxy_logger.debug("error closing relayed streaming generator: %s", exc)
if produced.background is not None:
with anyio.move_on_after(background_wait_seconds) as scope:
try:
await produced.background()
except BaseException as exc: # noqa: BLE001 # teardown must not mask why the stream ended
Comment thread
devin-ai-integration[bot] marked this conversation as resolved.
verbose_proxy_logger.debug("error running relayed response background task: %s", exc)
if scope.cancel_called:
verbose_proxy_logger.warning(
"relayed response background task still running after %.0fs, leaving it to shutdown",
background_wait_seconds,
)


async def _relay_late_response(produced: Response) -> AsyncGenerator[bytes, None]:
Expand Down
Loading
Loading