diff --git a/strix/llm/request_log.py b/strix/llm/request_log.py index eda0d222e..89b421689 100644 --- a/strix/llm/request_log.py +++ b/strix/llm/request_log.py @@ -779,17 +779,24 @@ def _litellm_success( cost = _float_or_none(kwargs.get("response_cost")) if cost is None: cost = _float_or_none(hidden.get("response_cost")) + finish_reason = _litellm_finish_reason(response) + provider_filtered = finish_reason == "content_filter" return replace( base, + outcome="error" if provider_filtered else base.outcome, status_code=200, response_id=_str_or_none(getattr(response, "id", None)), + error_type="ProviderContentFilter" if provider_filtered else None, + error_message=( + "Response withheld by the provider's content filter." if provider_filtered else None + ), input_tokens=usage.get("input_tokens"), output_tokens=usage.get("output_tokens"), cached_input_tokens=usage.get("cached_input_tokens"), total_tokens=usage.get("total_tokens"), cost_usd=cost, response_bytes=json_size(response), - finish_reason=_litellm_finish_reason(response), + finish_reason=finish_reason, details=merge_details( ("request", _litellm_request_details(kwargs)), ("response", _litellm_response_details(response)), diff --git a/tests/test_llm_request_log.py b/tests/test_llm_request_log.py index 41bbc8843..50f0305bc 100644 --- a/tests/test_llm_request_log.py +++ b/tests/test_llm_request_log.py @@ -216,6 +216,25 @@ def test_litellm_success_falls_back_to_response_hidden_headers() -> None: assert event.provider_request_id == "req_hidden_1" +def test_litellm_content_filter_success_callback_is_logged_as_provider_error() -> None: + class _Choice: + finish_reason = "content_filter" + + class _Response(_FakeResponse): + choices: ClassVar[list[Any]] = [_Choice()] + + event = request_log.event_from_litellm( + _anthropic_kwargs(None), _Response(), None, None, outcome="success" + ) + + assert event.outcome == "error" + assert event.status_code == 200 + assert event.finish_reason == "content_filter" + assert event.error_type == "ProviderContentFilter" + assert event.error_message == "Response withheld by the provider's content filter." + assert (event.input_tokens, event.output_tokens, event.total_tokens) == (120, 30, 150) + + def test_litellm_anthropic_block_prefers_header_id_over_body_id() -> None: exc = _anthropic_error(400, ANTHROPIC_BLOCK_BODY, {"request-id": "req_HeaderWins01"}) kwargs = _anthropic_kwargs(exc, headers={"request-id": "req_HeaderWins01"})