-
-
Notifications
You must be signed in to change notification settings - Fork 9.9k
Litellm oss staging 02 16 2026 #21326
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 4 commits
2129061
da8c1bd
bc2fefd
dcff326
d448682
504c70f
8f647ca
a8fbbb3
6edbeaa
a946cc4
c3b13fa
3a34b63
1c2e114
7e36d47
4bbd15f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change | ||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -9,9 +9,11 @@ | |||||||||||
| import os | ||||||||||||
| from typing import TYPE_CHECKING, Any, Dict, Literal, Optional | ||||||||||||
|
|
||||||||||||
| import httpx | ||||||||||||
|
|
||||||||||||
| from litellm._logging import verbose_proxy_logger | ||||||||||||
| from litellm._version import version as litellm_version | ||||||||||||
| from litellm.exceptions import GuardrailRaisedException | ||||||||||||
| from litellm.exceptions import GuardrailRaisedException, Timeout | ||||||||||||
| from litellm.integrations.custom_guardrail import ( | ||||||||||||
| CustomGuardrail, | ||||||||||||
| log_guardrail_information, | ||||||||||||
|
|
@@ -34,17 +36,19 @@ | |||||||||||
| GUARDRAIL_NAME = "generic_guardrail_api" | ||||||||||||
|
|
||||||||||||
| # Headers whose values are forwarded as-is (case-insensitive). Glob patterns supported (e.g. x-stainless-*, x-litellm*). | ||||||||||||
| _HEADER_VALUE_ALLOWLIST = frozenset({ | ||||||||||||
| "host", | ||||||||||||
| "accept-encoding", | ||||||||||||
| "connection", | ||||||||||||
| "accept", | ||||||||||||
| "content-type", | ||||||||||||
| "user-agent", | ||||||||||||
| "x-stainless-*", | ||||||||||||
| "x-litellm-*", | ||||||||||||
| "content-length", | ||||||||||||
| }) | ||||||||||||
| _HEADER_VALUE_ALLOWLIST = frozenset( | ||||||||||||
| { | ||||||||||||
| "host", | ||||||||||||
| "accept-encoding", | ||||||||||||
| "connection", | ||||||||||||
| "accept", | ||||||||||||
| "content-type", | ||||||||||||
| "user-agent", | ||||||||||||
| "x-stainless-*", | ||||||||||||
| "x-litellm-*", | ||||||||||||
| "content-length", | ||||||||||||
| } | ||||||||||||
| ) | ||||||||||||
|
|
||||||||||||
| # Placeholder for headers that exist but are not on the allowlist (we don't expose their value). | ||||||||||||
| _HEADER_PRESENT_PLACEHOLDER = "[present]" | ||||||||||||
|
|
@@ -166,6 +170,7 @@ def __init__( | |||||||||||
| api_base: Optional[str] = None, | ||||||||||||
| api_key: Optional[str] = None, | ||||||||||||
| additional_provider_specific_params: Optional[Dict[str, Any]] = None, | ||||||||||||
| unreachable_fallback: Literal["fail_closed", "fail_open"] = "fail_closed", | ||||||||||||
| **kwargs, | ||||||||||||
| ): | ||||||||||||
| self.async_handler = get_async_httpx_client( | ||||||||||||
|
|
@@ -196,6 +201,10 @@ def __init__( | |||||||||||
| additional_provider_specific_params or {} | ||||||||||||
| ) | ||||||||||||
|
|
||||||||||||
| self.unreachable_fallback: Literal["fail_closed", "fail_open"] = ( | ||||||||||||
| unreachable_fallback | ||||||||||||
| ) | ||||||||||||
|
|
||||||||||||
| # Set supported event hooks | ||||||||||||
| if "supported_event_hooks" not in kwargs: | ||||||||||||
| kwargs["supported_event_hooks"] = [ | ||||||||||||
|
|
@@ -259,6 +268,54 @@ def _extract_user_api_key_metadata( | |||||||||||
|
|
||||||||||||
| return result_metadata | ||||||||||||
|
|
||||||||||||
| def _fail_open_passthrough( | ||||||||||||
| self, | ||||||||||||
| *, | ||||||||||||
| inputs: GenericGuardrailAPIInputs, | ||||||||||||
| input_type: Literal["request", "response"], | ||||||||||||
| logging_obj: Optional["LiteLLMLoggingObj"], | ||||||||||||
| error: Exception, | ||||||||||||
| http_status_code: Optional[int] = None, | ||||||||||||
| ) -> GenericGuardrailAPIInputs: | ||||||||||||
| status_suffix = f" http_status_code={http_status_code}" if http_status_code else "" | ||||||||||||
| verbose_proxy_logger.critical( | ||||||||||||
| "Generic Guardrail API unreachable (fail-open). Proceeding without guardrail.%s " | ||||||||||||
| "guardrail_name=%s api_base=%s input_type=%s litellm_call_id=%s litellm_trace_id=%s", | ||||||||||||
| status_suffix, | ||||||||||||
| getattr(self, "guardrail_name", None), | ||||||||||||
| getattr(self, "api_base", None), | ||||||||||||
| input_type, | ||||||||||||
| getattr(logging_obj, "litellm_call_id", None) if logging_obj else None, | ||||||||||||
| getattr(logging_obj, "litellm_trace_id", None) if logging_obj else None, | ||||||||||||
| exc_info=error, | ||||||||||||
| ) | ||||||||||||
| # Keep flow going - treat as action=NONE (no modifications) | ||||||||||||
| return_inputs: GenericGuardrailAPIInputs = {} | ||||||||||||
| return_inputs.update(inputs) | ||||||||||||
| return return_inputs | ||||||||||||
|
|
||||||||||||
| def _build_guardrail_return_inputs( | ||||||||||||
| self, | ||||||||||||
| *, | ||||||||||||
| texts: list, | ||||||||||||
| images: Any, | ||||||||||||
| tools: Any, | ||||||||||||
| guardrail_response: GenericGuardrailAPIResponse, | ||||||||||||
| ) -> GenericGuardrailAPIInputs: | ||||||||||||
| # Action is NONE or no modifications needed | ||||||||||||
| return_inputs = GenericGuardrailAPIInputs(texts=texts) | ||||||||||||
| if guardrail_response.texts: | ||||||||||||
| return_inputs["texts"] = guardrail_response.texts | ||||||||||||
| if guardrail_response.images: | ||||||||||||
| return_inputs["images"] = guardrail_response.images | ||||||||||||
| elif images: | ||||||||||||
| return_inputs["images"] = images | ||||||||||||
| if guardrail_response.tools: | ||||||||||||
| return_inputs["tools"] = guardrail_response.tools | ||||||||||||
| elif tools: | ||||||||||||
| return_inputs["tools"] = tools | ||||||||||||
| return return_inputs | ||||||||||||
|
|
||||||||||||
| @log_guardrail_information | ||||||||||||
| async def apply_guardrail( | ||||||||||||
| self, | ||||||||||||
|
|
@@ -313,7 +370,9 @@ async def apply_guardrail( | |||||||||||
|
|
||||||||||||
| # Extract user API key metadata | ||||||||||||
| user_metadata = self._extract_user_api_key_metadata(request_data) | ||||||||||||
| inbound_headers = _extract_inbound_headers(request_data=request_data, logging_obj=logging_obj) | ||||||||||||
| inbound_headers = _extract_inbound_headers( | ||||||||||||
| request_data=request_data, logging_obj=logging_obj | ||||||||||||
| ) | ||||||||||||
|
|
||||||||||||
| # Create request payload | ||||||||||||
| guardrail_request = GenericGuardrailAPIRequest( | ||||||||||||
|
|
@@ -370,23 +429,64 @@ async def apply_guardrail( | |||||||||||
| should_wrap_with_default_message=False, | ||||||||||||
| ) | ||||||||||||
|
|
||||||||||||
| # Action is NONE or no modifications needed | ||||||||||||
| return_inputs = GenericGuardrailAPIInputs(texts=texts) | ||||||||||||
| if guardrail_response.texts: | ||||||||||||
| return_inputs["texts"] = guardrail_response.texts | ||||||||||||
| if guardrail_response.images: | ||||||||||||
| return_inputs["images"] = guardrail_response.images | ||||||||||||
| elif images: | ||||||||||||
| return_inputs["images"] = images | ||||||||||||
| if guardrail_response.tools: | ||||||||||||
| return_inputs["tools"] = guardrail_response.tools | ||||||||||||
| elif tools: | ||||||||||||
| return_inputs["tools"] = tools | ||||||||||||
| return return_inputs | ||||||||||||
| return self._build_guardrail_return_inputs( | ||||||||||||
| texts=texts, | ||||||||||||
| images=images, | ||||||||||||
| tools=tools, | ||||||||||||
| guardrail_response=guardrail_response, | ||||||||||||
| ) | ||||||||||||
|
|
||||||||||||
| except GuardrailRaisedException: | ||||||||||||
| # Re-raise guardrail exceptions as-is | ||||||||||||
| raise | ||||||||||||
| except Timeout as e: | ||||||||||||
| # AsyncHTTPHandler wraps httpx.TimeoutException into litellm.Timeout | ||||||||||||
| if self.unreachable_fallback == "fail_open": | ||||||||||||
| return self._fail_open_passthrough( | ||||||||||||
| inputs=inputs, | ||||||||||||
| input_type=input_type, | ||||||||||||
| logging_obj=logging_obj, | ||||||||||||
| error=e, | ||||||||||||
| ) | ||||||||||||
|
|
||||||||||||
| verbose_proxy_logger.error( | ||||||||||||
| "Generic Guardrail API: failed to make request: %s", str(e) | ||||||||||||
| ) | ||||||||||||
| raise Exception(f"Generic Guardrail API failed: {str(e)}") | ||||||||||||
| except httpx.HTTPStatusError as e: | ||||||||||||
| # Common reverse-proxy/LB failures can present as HTTP errors even when the backend is unreachable. | ||||||||||||
| status_code = getattr(getattr(e, "response", None), "status_code", None) | ||||||||||||
| if self.unreachable_fallback == "fail_open" and status_code in ( | ||||||||||||
| 502, | ||||||||||||
| 503, | ||||||||||||
| 504, | ||||||||||||
| ): | ||||||||||||
| return self._fail_open_passthrough( | ||||||||||||
| inputs=inputs, | ||||||||||||
| input_type=input_type, | ||||||||||||
| logging_obj=logging_obj, | ||||||||||||
| error=e, | ||||||||||||
| http_status_code=status_code, | ||||||||||||
| ) | ||||||||||||
|
|
||||||||||||
| verbose_proxy_logger.error( | ||||||||||||
| "Generic Guardrail API: failed to make request: %s", str(e) | ||||||||||||
| ) | ||||||||||||
| raise Exception(f"Generic Guardrail API failed: {str(e)}") | ||||||||||||
| except httpx.RequestError as e: | ||||||||||||
| # Guardrail endpoint is unreachable (DNS/connect/timeout/etc) | ||||||||||||
| if self.unreachable_fallback == "fail_open": | ||||||||||||
| return self._fail_open_passthrough( | ||||||||||||
| inputs=inputs, | ||||||||||||
| input_type=input_type, | ||||||||||||
| logging_obj=logging_obj, | ||||||||||||
| error=e, | ||||||||||||
| ) | ||||||||||||
|
|
||||||||||||
| verbose_proxy_logger.error( | ||||||||||||
| "Generic Guardrail API: failed to make request: %s", str(e) | ||||||||||||
| ) | ||||||||||||
| raise Exception(f"Generic Guardrail API failed: {str(e)}") | ||||||||||||
|
Comment on lines
+486
to
+489
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Exception chain lost when re-raising
Suggested change
|
||||||||||||
| except Exception as e: | ||||||||||||
| verbose_proxy_logger.error( | ||||||||||||
| "Generic Guardrail API: failed to make request: %s", str(e) | ||||||||||||
|
|
||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Non-5xx errors swallowed as generic Exception
When
unreachable_fallback="fail_closed"(or any HTTP error that isn't 502/503/504), the originalhttpx.HTTPStatusErroris caught and re-raised as a bareException(f"Generic Guardrail API failed: {str(e)}"). This discards the HTTP status code, response body, and exception chain, making debugging harder.Consider preserving the original exception with
raise ... from e, and ideally including the status code in the message: