fix(proxy): warn at submit when a body-selected post_call policy is deferred

This commit is contained in:
mateo-berri 2026-09-09 13:45:26 -07:00
parent c4bd3763e8
commit c5e93aff13
2 changed files with 46 additions and 2 deletions

View file

@ -622,6 +622,15 @@ def _defer_post_call_pipelines(
response.id,
", ".join(tag_matched),
)
body_selected: Final = _body_selected_deferrals(data, deferred)
if body_selected:
verbose_proxy_logger.warning(
"Policy engine: background response %s matched post_call policies through the request body's policies "
"list at submit; retrieval carries no request body, so those policies do not govern the completed "
"response: %s",
response.id,
", ".join(body_selected),
)
_withdraw_deferred_claims(data, deferred)
@ -638,6 +647,14 @@ def _tag_matched_deferrals(
)
def _body_selected_deferrals(
data: Mapping[str, object], deferred: Sequence[tuple[str, "GuardrailPipeline"]]
) -> tuple[str, ...]:
sources: Final = _policy_state_metadata(data).get("policy_sources")
attributed: Final = frozenset(sources) if isinstance(sources, dict) else frozenset()
return tuple(policy_name for policy_name, _pipeline in deferred if policy_name not in attributed)
def _pipeline_is_streamable(policy_name: str, pipeline: "GuardrailPipeline") -> bool:
unsupported: Final = tuple(
dict.fromkeys(

View file

@ -941,6 +941,7 @@ def _post_call_pipeline_data(
"metadata": {
"_guardrail_pipelines": [("response-governance", pipeline)],
"_pipeline_managed_guardrails": {guardrail},
"policy_sources": {"response-governance": "model:m"},
},
**extra,
}
@ -1468,7 +1469,7 @@ def _output_passing_callbacks() -> list[CustomGuardrail]:
def _claimed_post_call_pipeline_data(
*policy_names: str, extra_guardrails: dict[str, list[str]] | None = None, policy_source: str = "model:m"
*policy_names: str, extra_guardrails: dict[str, list[str]] | None = None, policy_source: str | None = "model:m"
):
from litellm.proxy.policy_engine.policy_registry import get_policy_registry
@ -1491,7 +1492,7 @@ def _claimed_post_call_pipeline_data(
"_pipeline_managed_guardrails": {"gr-post"},
"applied_policies": list(policy_names),
"applied_guardrails": ["gr-post", *(g for gs in (extra_guardrails or {}).values() for g in gs)],
"policy_sources": {policy_name: policy_source for policy_name in policy_names},
"policy_sources": {policy_name: policy_source for policy_name in policy_names if policy_source is not None},
},
}
@ -1551,6 +1552,32 @@ async def test_pending_background_response_warns_when_the_deferred_policy_was_ma
]
@pytest.mark.asyncio
async def test_pending_background_response_warns_when_the_deferred_policy_came_from_the_request_body(
proxy_logging: ProxyLogging,
make_user_api_key_auth: Callable[..., UserAPIKeyAuth],
monkeypatch: pytest.MonkeyPatch,
clear_policy_registry: None,
caplog: pytest.LogCaptureFixture,
) -> None:
monkeypatch.setattr(litellm, "callbacks", _output_blocking_callbacks({}))
monkeypatch.setattr("litellm.proxy.proxy_server.llm_router", None, raising=False)
data = _claimed_post_call_pipeline_data("body-governance", policy_source=None)
with caplog.at_level(logging.WARNING, logger="LiteLLM Proxy"):
out = await proxy_logging.post_call_success_hook(
data=data, response=_background_response("queued"), user_api_key_dict=make_user_api_key_auth()
)
assert out.status == "queued"
assert "policy_sources" not in data["metadata"]
assert _warnings(caplog) == [
"Policy engine: background response resp_bg matched post_call policies through the request body's policies "
"list at submit; retrieval carries no request body, so those policies do not govern the completed "
"response: body-governance"
]
@pytest.mark.asyncio
async def test_pending_background_response_matched_through_its_model_does_not_warn(
proxy_logging: ProxyLogging,