Skip to content

fix(guardrails): send only new messages since last assistant turn to CrowdStrike AIDR - #31230

Open
kenany wants to merge 1 commit into
BerriAI:litellm_internal_stagingfrom
kenany:kenany/crowdstrike-aidr-relevant-content
Open

fix(guardrails): send only new messages since last assistant turn to CrowdStrike AIDR#31230
kenany wants to merge 1 commit into
BerriAI:litellm_internal_stagingfrom
kenany:kenany/crowdstrike-aidr-relevant-content

Conversation

@kenany

@kenany kenany commented Jun 24, 2026

Copy link
Copy Markdown
Contributor

Pre-Submission checklist

Please complete all items before asking a LiteLLM maintainer to review your PR

  • I have added meaningful tests
  • My PR passes all unit tests on make test-unit
  • My PR's scope is as isolated as possible; it only solves 1 specific problem
  • I have requested a Greptile review by commenting @greptileai and received a Confidence Score of at least 4/5 before requesting a maintainer review

Delays in PR merge?

If you're seeing a delay in your PR being merged, ping the LiteLLM Team on Slack (#pr-review).

CI (LiteLLM team)

CI status guideline:

  • 50-55 passing tests: main is stable with minor issues.
  • 45-49 passing tests: acceptable but needs attention
  • <= 40 passing tests: unstable; be careful with your merges and assess the risk.
  • Branch creation CI run
    Link:

  • CI run for the last commit
    Link:

  • Merge / cherry-pick CI run
    Links:

Screenshots / Proof of Fix

Details
================================================================= test session starts ==================================================================
platform linux -- Python 3.13.12, pytest-9.0.3, pluggy-1.6.0 -- litellm/.devenv/state/venv/bin/python
cachedir: .pytest_cache
rootdir: litellm
configfile: pyproject.toml
plugins: rerunfailures-15.1, anyio-4.13.0, asyncio-1.3.0, postgresql-7.0.2, recording-0.13.4, timeout-2.4.0, mock-3.15.1, cov-5.0.0, xdist-3.8.0, respx-0.22.0, requests-mock-1.12.1
asyncio: mode=Mode.AUTO, debug=False, asyncio_default_fixture_loop_scope=session, asyncio_default_test_loop_scope=function
collected 29 items

tests/test_litellm/proxy/guardrails/guardrail_hooks/test_crowdstrike_aidr.py::test_apply_guardrail_empty_extra_info_when_no_email PASSED [ 3%]
tests/test_litellm/proxy/guardrails/guardrail_hooks/test_crowdstrike_aidr.py::test_apply_guardrail_no_metadata_skips_user_fields PASSED [ 6%]
tests/test_litellm/proxy/guardrails/guardrail_hooks/test_crowdstrike_aidr.py::test_apply_guardrail_reads_identity_from_either_metadata_bag[identity_in_litellm_metadata] PASSED [ 10%]
tests/test_litellm/proxy/guardrails/guardrail_hooks/test_crowdstrike_aidr.py::test_apply_guardrail_reads_identity_from_either_metadata_bag[identity_in_metadata_llm_non_mapping] PASSED [ 13%]
tests/test_litellm/proxy/guardrails/guardrail_hooks/test_crowdstrike_aidr.py::test_apply_guardrail_reads_identity_from_either_metadata_bag[identity_in_metadata_llm_none] PASSED [ 17%]
tests/test_litellm/proxy/guardrails/guardrail_hooks/test_crowdstrike_aidr.py::test_apply_guardrail_reads_identity_from_either_metadata_bag[identity_in_metadata_llm_user_dict] PASSED [ 20%]
tests/test_litellm/proxy/guardrails/guardrail_hooks/test_crowdstrike_aidr.py::test_apply_guardrail_request_blocked PASSED [ 24%]
tests/test_litellm/proxy/guardrails/guardrail_hooks/test_crowdstrike_aidr.py::test_apply_guardrail_request_last_is_assistant_sends_only_that PASSED [ 27%]
tests/test_litellm/proxy/guardrails/guardrail_hooks/test_crowdstrike_aidr.py::test_apply_guardrail_request_ok PASSED [ 31%]
tests/test_litellm/proxy/guardrails/guardrail_hooks/test_crowdstrike_aidr.py::test_apply_guardrail_request_sends_only_new_messages PASSED [ 34%]
tests/test_litellm/proxy/guardrails/guardrail_hooks/test_crowdstrike_aidr.py::test_apply_guardrail_request_skipped_messages_stay_aligned PASSED [ 37%]
tests/test_litellm/proxy/guardrails/guardrail_hooks/test_crowdstrike_aidr.py::test_apply_guardrail_request_stitches_transformed_texts PASSED [ 41%]
tests/test_litellm/proxy/guardrails/guardrail_hooks/test_crowdstrike_aidr.py::test_apply_guardrail_request_transformed PASSED [ 44%]
tests/test_litellm/proxy/guardrails/guardrail_hooks/test_crowdstrike_aidr.py::test_apply_guardrail_response_blocked PASSED [ 48%]
tests/test_litellm/proxy/guardrails/guardrail_hooks/test_crowdstrike_aidr.py::test_apply_guardrail_response_drops_history PASSED [ 51%]
tests/test_litellm/proxy/guardrails/guardrail_hooks/test_crowdstrike_aidr.py::test_apply_guardrail_response_ok PASSED [ 55%]
tests/test_litellm/proxy/guardrails/guardrail_hooks/test_crowdstrike_aidr.py::test_apply_guardrail_response_one_message_per_output_text PASSED [ 58%]
tests/test_litellm/proxy/guardrails/guardrail_hooks/test_crowdstrike_aidr.py::test_apply_guardrail_response_transform_extracts_assistant_only PASSED [ 62%]
tests/test_litellm/proxy/guardrails/guardrail_hooks/test_crowdstrike_aidr.py::test_apply_guardrail_response_transformed PASSED [ 65%]
tests/test_litellm/proxy/guardrails/guardrail_hooks/test_crowdstrike_aidr.py::test_apply_guardrail_sends_user_id_model_and_extra_info PASSED [ 68%]
tests/test_litellm/proxy/guardrails/guardrail_hooks/test_crowdstrike_aidr.py::test_crowdstrike_aidr_guardrail_config PASSED [ 72%]
tests/test_litellm/proxy/guardrails/guardrail_hooks/test_crowdstrike_aidr.py::test_crowdstrike_aidr_guardrail_config_no_api_base PASSED [ 75%]
tests/test_litellm/proxy/guardrails/guardrail_hooks/test_crowdstrike_aidr.py::test_crowdstrike_aidr_guardrail_config_no_api_key PASSED [ 79%]
tests/test_litellm/proxy/guardrails/guardrail_hooks/test_crowdstrike_aidr.py::TestMessageFiltering::test_last_message_is_assistant_sends_system_plus_that_message PASSED [ 82%]
tests/test_litellm/proxy/guardrails/guardrail_hooks/test_crowdstrike_aidr.py::TestMessageFiltering::test_last_message_is_user_sends_system_plus_messages_after_assistant PASSED [ 86%]
tests/test_litellm/proxy/guardrails/guardrail_hooks/test_crowdstrike_aidr.py::TestMessageFiltering::test_multiple_user_messages_after_assistant PASSED [ 89%]
tests/test_litellm/proxy/guardrails/guardrail_hooks/test_crowdstrike_aidr.py::TestMessageFiltering::test_no_prior_assistant_sends_all_messages PASSED [ 93%]
tests/test_litellm/proxy/guardrails/guardrail_hooks/test_crowdstrike_aidr.py::TestMessageFiltering::test_no_system_messages PASSED [ 96%]
tests/test_litellm/proxy/guardrails/guardrail_hooks/test_crowdstrike_aidr.py::TestMessageFiltering::test_system_message_after_assistant_included PASSED [100%]

=================================================================== warnings summary ===================================================================
.devenv/state/venv/lib/python3.13/site-packages/fastapi/testclient.py:1
litellm/.devenv/state/venv/lib/python3.13/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using httpx with starlette.testclient is deprecated; install httpx2 instead.
from starlette.testclient import TestClient as TestClient # noqa

-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
============================================================ 29 passed, 1 warning in 2.01s =============================================================

Type

🐛 Bug Fix

Changes

Previously, every guardrail request forwarded the full conversation history to CrowdStrike AIDR. In a multi-turn conversation this means every prior message gets re-scanned on every new call, even though those messages were already evaluated in earlier turns.

CrowdStrike AIDR internally has a conversation boundary optimization in place for just this scenario (ref. https://aidr-docs.crowdstrike.com/docs/aidr/apis#messages-array-optional---array-of-message-objects-containing-a-conversation-segment-with-the-ai-system). However, it is nevertheless wasteful to send so much data to the API when only a subset of it will be processed. It also risks hitting the documented 1 MiB request size limit.

So now we filter down to system messages plus either the messages after the last assistant turn, or the last assistant message itself when that is what is being guarded. We also preserve the original, full message history within the guardrail in order to stitch back any transformations.

@kenany

kenany commented Jun 24, 2026

Copy link
Copy Markdown
Contributor Author

@greptileai

@greptile-apps

greptile-apps Bot commented Jun 24, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR optimizes the CrowdStrike AIDR guardrail by filtering the conversation to only send new messages since the last assistant turn (plus all system messages), rather than the full history. When the API returns a transformation, the redacted content is stitched back into the complete message list using index tracking, and the response guardrail no longer forwards request history at all.

  • Message filtering (_messages_since_last_assistant): collects all system messages plus the tail after the last assistant turn (or just the final assistant message when that is what is being guarded), tracking original indices to enable lossless merge.
  • Transform stitching (_merge_request_transforms, _redacted_messages): maps the filtered-subset positions back to the full structured_messages array so downstream consumers always see a consistently sized, correctly redacted message list.
  • Response guardrail simplification (_build_guard_input_for_response): drops the prior conversation history from response calls, sending only the new assistant output texts.

Confidence Score: 5/5

Safe to merge — the change is scoped to the CrowdStrike AIDR guardrail hook, all edge cases are covered by new tests, and the 29-test suite passes cleanly.

The filtering and stitching logic is correct across all traced paths: system messages are always retained, index bookkeeping maps filtered positions back to the full array without off-by-one errors, the id()-based writeback correctly falls back to structured_messages when identity cannot be confirmed, and the response-path simplification is intentional and well-tested. No regressions or behavior drift were identified.

No files require special attention.

Important Files Changed

Filename Overview
litellm/proxy/guardrails/guardrail_hooks/crowdstrike_aidr/crowdstrike_aidr.py Core guardrail handler refactored to filter conversation to only new messages since the last assistant turn before calling CrowdStrike AIDR; adds index-tracking and stitching logic to correctly merge transformed texts and structured messages back into the full conversation.
tests/test_litellm/proxy/guardrails/guardrail_hooks/test_crowdstrike_aidr.py Existing tests updated to reflect the new behavior (response guardrail no longer forwards conversation history; skipped-message assertion now validates redacted output); comprehensive new tests added covering message-filtering edge cases, transform stitching, and writeback logic.

Reviews (5): Last reviewed commit: "fix(guardrails): send only new messages ..." | Re-trigger Greptile

Comment thread litellm/proxy/guardrails/guardrail_hooks/crowdstrike_aidr/crowdstrike_aidr.py Outdated
@greptile-apps

greptile-apps Bot commented Jun 24, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR optimises the CrowdStrike AIDR guardrail integration by filtering the messages sent to the API on each turn: for request guardrails, only system messages plus the messages since the last assistant reply are forwarded; for response guardrails, only the assistant output texts are sent (conversation history is dropped). A new _messages_since_last_assistant helper computes which messages to send and records their original indices so that any transformations returned by the API can be stitched back into the full texts array.

  • Adds _FilteredMessages / _GuardInputWithIndices named tuples and a _messages_since_last_assistant function to select the relevant message subset and preserve its positional mapping.
  • Refactors _build_guard_input_for_response to send only assistant output messages (drops conversation history); also refactors _call_crowdstrike_aidr_guard to parse the response into a typed _GuardChatCompletionsResult model instead of raw dict access.
  • Adds 11 new unit tests covering the filtering and stitching behaviour, and updates existing response-path tests to match the new payload shape.

Confidence Score: 3/5

Safe to merge for the common path, but the response guardrail can now silently fire an unnecessary API call (with an empty messages array) when no output text is present, and the warning that previously caught this case was removed.

The filtering logic and transformation stitching are well-tested and the core multi-turn optimisation is correct. The main concern is that _build_guard_input_for_response no longer returns None for empty output texts, making the None-guard in apply_guardrail dead code and silently changing behaviour for tool-call-only responses (an API request with an empty messages array is sent instead of an early return with a diagnostic warning). There is no test covering this edge case.

crowdstrike_aidr.py — specifically _build_guard_input_for_response and the dead if guard_input is None branch in apply_guardrail.

Important Files Changed

Filename Overview
litellm/proxy/guardrails/guardrail_hooks/crowdstrike_aidr/crowdstrike_aidr.py Adds _messages_since_last_assistant filtering and index-based transformation stitching; response path now silently makes an API call with empty messages when no output texts exist (dead-code early-return guard).
tests/test_litellm/proxy/guardrails/guardrail_hooks/test_crowdstrike_aidr.py Existing response-path tests updated to remove history from expected payloads (matching new behaviour); 6 new TestMessageFiltering tests and 5 new top-level tests cover the filtering and stitching logic. No test for empty output_texts on the response path.

Reviews (2): Last reviewed commit: "fix(guardrails): send only new messages ..." | Re-trigger Greptile

Comment thread litellm/proxy/guardrails/guardrail_hooks/crowdstrike_aidr/crowdstrike_aidr.py Outdated
@codecov

codecov Bot commented Jun 24, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 92.30769% with 8 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
...ardrail_hooks/crowdstrike_aidr/crowdstrike_aidr.py 92.30% 8 Missing ⚠️

📢 Thoughts on this report? Let us know!

@kenany
kenany force-pushed the kenany/crowdstrike-aidr-relevant-content branch 2 times, most recently from 0348923 to a9a9241 Compare June 24, 2026 20:53
@veria-ai

veria-ai Bot commented Jun 24, 2026

Copy link
Copy Markdown
Contributor

PR overview

All previously flagged issues have been addressed. No open security concerns remain on this pull request.

Security review

No open security issues remain on this pull request.

Fixed/addressed: 3 · PR risk: 0/10

@Sameerlite

Copy link
Copy Markdown
Contributor

Thanks for this optimization — the message filtering logic looks well thought out and the test coverage is thorough. The Greptile review is stale (it was run against an earlier commit). Triggering a fresh review against the current HEAD now:\n\n@greptileai

@kenany

kenany commented Jun 25, 2026

Copy link
Copy Markdown
Contributor Author

@greptileai

@kenany
kenany force-pushed the kenany/crowdstrike-aidr-relevant-content branch 2 times, most recently from 3a14077 to 13163b7 Compare June 29, 2026 16:14
@kenany

kenany commented Jun 29, 2026

Copy link
Copy Markdown
Contributor Author

@greptileai

@kenany
kenany force-pushed the kenany/crowdstrike-aidr-relevant-content branch from 13163b7 to 4cc94e3 Compare June 29, 2026 18:29
Comment thread litellm/proxy/guardrails/guardrail_hooks/crowdstrike_aidr/crowdstrike_aidr.py Outdated
@kenany
kenany force-pushed the kenany/crowdstrike-aidr-relevant-content branch from 4cc94e3 to ae61dd3 Compare June 30, 2026 02:22
…CrowdStrike AIDR

Previously, every guardrail request forwarded the full conversation
history to CrowdStrike AIDR. In a multi-turn conversation this means
every prior message gets re-scanned on every new call, even though those
messages were already evaluated in earlier turns.

CrowdStrike AIDR internally has a conversation boundary optimization in
place for just this scenario (ref. <https://aidr-docs.crowdstrike.com/docs/aidr/apis#messages-array-optional---array-of-message-objects-containing-a-conversation-segment-with-the-ai-system>).
However, it is nevertheless wasteful to send so much data to the API
when only a subset of it will be processed. It also risks hitting the
documented 1 MiB request size limit.

So now we filter down to system messages plus either the messages after
the last assistant turn, or the last assistant message itself when that
is what is being guarded. We also preserve the original, full message
history within the guardrail in order to stitch back any
transformations.
@kenany
kenany force-pushed the kenany/crowdstrike-aidr-relevant-content branch from ae61dd3 to b5d63b9 Compare June 30, 2026 02:39
@kenany

kenany commented Jun 30, 2026

Copy link
Copy Markdown
Contributor Author

@greptileai

@ryan-crabbe-berri

Copy link
Copy Markdown
Contributor

hey thanks for this, lgtm. there's an awkward quirk in our ci pipeline rn for oss as we are moving to different system, so i will need to port this to a internal staging branch!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants