Skip to content

Hiddenlayer Integration: Add V2 Integration - #22708

Merged
krrish-berri-2 merged 9 commits into
BerriAI:litellm_oss_staging_04_13_2026_p1from
Ashton-Sidhu:hiddenlayer-guardrail-integration-update
Apr 14, 2026
Merged

Hiddenlayer Integration: Add V2 Integration#22708
krrish-berri-2 merged 9 commits into
BerriAI:litellm_oss_staging_04_13_2026_p1from
Ashton-Sidhu:hiddenlayer-guardrail-integration-update

Conversation

@Ashton-Sidhu

@Ashton-Sidhu Ashton-Sidhu commented Mar 3, 2026

Copy link
Copy Markdown
Contributor

Relevant issues

Fixes some issues with the hiddenlayer guardrails:

  • Serialize the block message to a proper string
  • only scan the last message in the message array
  • Add block reason
  • Add V2 integration

Pre-Submission checklist

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

  • I have Added testing in the tests/test_litellm/ directory, Adding at least 1 test is a hard requirement - see details
  • 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

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:

Type

🐛 Bug Fix

@vercel

vercel Bot commented Mar 3, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
litellm Error Error Apr 13, 2026 6:12pm

Request Review

@greptile-apps

greptile-apps Bot commented Mar 3, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds a V2 HiddenLayer guardrail implementation (HiddenlayerGuardrailV2) using the new /detection/v2/request-evaluations and /detection/v2/response-evaluations endpoints, fixes block-message serialization, and restricts V1 scanning to the last message only. V2 is wired as the default (via version: 2 in HiddenlayerGuardrailConfigModel), with V1 still reachable by setting version: 1.

  • The documentation (hiddenlayer.md) still describes the integration as routing to /detection/v1/interactions, which is now incorrect for any user who doesn't explicitly pin version: 1 — this should be updated to reflect V2 as the default and clarify the version field.
  • test_apply_guardrail_api_error_handling creates GenericGuardrailAPIInputs() with no content, so _call_hiddenlayer is never invoked and the mocked Exception(\"Connection timeout\") never fires — the test passes trivially and provides no coverage of the error-handling path it describes.

Confidence Score: 4/5

Functionally correct for the happy path; a few edge cases flagged in prior rounds remain open but were acknowledged by the maintainer, and the new V2 class is well-tested overall.

Prior review rounds surfaced several robustness issues (missing isinstance guard on last_msg, potential IndexError on empty choices list, null fields in V2 payload, empty-payload API call on response path) — some were replied to as intentional, others were not addressed. New findings in this round are limited to a misleading test and outdated docs, both P2. The core V2 logic and routing are sound, but the open prior-round items prevent a confident 5/5.

litellm/proxy/guardrails/guardrail_hooks/hiddenlayer/hiddenlayer.py (V2 edge cases), tests/test_litellm/proxy/guardrails/guardrail_hooks/test_hiddenlayer.py (no-op error test), docs/my-website/docs/proxy/guardrails/hiddenlayer.md (stale V1 reference)

Important Files Changed

Filename Overview
litellm/proxy/guardrails/guardrail_hooks/hiddenlayer/hiddenlayer.py Adds V2 guardrail class with new API endpoints; several edge cases remain from prior review rounds (empty choices IndexError, null fields in payload, empty-payload API call on response path, missing isinstance guard on last_msg).
litellm/proxy/guardrails/guardrail_hooks/hiddenlayer/init.py Routes to V1 or V2 based on version field; guardrail_class_registry still maps to V1 class only while V2 is the new default.
litellm/types/proxy/guardrails/guardrail_hooks/hiddenlayer.py Adds version field defaulting to 2; silently migrates existing users to V2 API on upgrade (accepted by maintainer as intentional).
tests/test_litellm/proxy/guardrails/guardrail_hooks/test_hiddenlayer.py Good coverage for V1 and V2 happy paths and block/redact cases; test_apply_guardrail_api_error_handling is a no-op (empty inputs → API never called, mock exception never fires).
litellm/types/guardrails.py Adds HiddenlayerGuardrailConfigModel to LitellmParams inheritance chain; straightforward import addition.
docs/my-website/docs/proxy/guardrails/hiddenlayer.md Documentation still references the V1 /detection/v1/interactions endpoint as the integration point, but V2 is now the default — misleading for new users.

Sequence Diagram

sequenceDiagram
    participant Client
    participant LiteLLM
    participant HL as HiddenLayer API
    participant LLM

    Note over LiteLLM: apply_guardrail (pre_call)
    Client->>LiteLLM: POST /chat/completions

    alt V1 (version < 2)
        LiteLLM->>HL: POST /detection/v1/interactions
        HL-->>LiteLLM: evaluation + analysis + modified_data
        alt action == Block
            LiteLLM-->>Client: 400 HTTPException (block_reasons, threat_level)
        else action == Redact
            LiteLLM->>LLM: modified messages
        else Allow
            LiteLLM->>LLM: original messages
        end
    else V2 (version >= 2, default)
        LiteLLM->>HL: POST /detection/v2/request-evaluations
        HL-->>LiteLLM: response + hl-runtime-action header
        alt header == block
            LiteLLM-->>Client: 400 HTTPException
        else pass
            LiteLLM->>LLM: possibly modified messages
        end
    end

    LLM-->>LiteLLM: response

    Note over LiteLLM: apply_guardrail (post_call)
    alt V1
        LiteLLM->>HL: POST /detection/v1/interactions
        HL-->>LiteLLM: evaluation result
    else V2
        LiteLLM->>HL: POST /detection/v2/response-evaluations
        HL-->>LiteLLM: response + hl-runtime-action header
    end

    LiteLLM-->>Client: final response
Loading

Reviews (6): Last reviewed commit: "Add image support" | Re-trigger Greptile

Comment thread litellm/proxy/guardrails/guardrail_hooks/hiddenlayer/hiddenlayer.py
…er.py

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
Comment on lines 153 to 160
if scan_params := inputs.get("structured_messages"):
# Convert AllMessageValues to simple dict format for HiddenLayer API
messages = [
{"role": msg.get("role", "user"), "content": msg.get("content", "")}
for msg in scan_params
if isinstance(msg, dict)
]
last_msg = scan_params[-1]
result = await self._call_hiddenlayer(
project_id, hl_request_metadata, {"messages": messages}, input_type
project_id,
hl_request_metadata,
{"messages": [{"role": last_msg.get("role", "user"), "content": last_msg.get("content", "")}]},
input_type,
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing test coverage for new behavior

The existing test test_apply_guardrail_request_with_violations mocks the HiddenLayer response as {"evaluation": {"action": "Block"}} without an analysis key, so it doesn't verify the new block_reasons or threat_level fields in the error detail. Similarly, no test covers the change from scanning all messages to scanning only the last message.

Consider adding a test that:

  1. Mocks a response with analysis entries (e.g., {"analysis": [{"name": "prompt_injection", "detected": True}], "evaluation": {"action": "Block", "threat_level": "high"}})
  2. Verifies that exc_info.value.detail["block_reasons"] contains ["prompt_injection"] and threat_level is "high"
  3. Uses structured_messages with multiple messages and asserts only the last one is sent to the API

description="The Hiddenlayer Secret Key for the Hiddenlayer API.. If not provided, the `HIDDENLAYER_CLIENT_SECRET` environment variable is checked.",
)

version: Optional[int] = Field(default=2, description="Hiddenlayer guardrail version to use.")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Backwards-incompatible default breaks existing V1 users

Setting default=2 means every existing HiddenLayer user who has not explicitly set a version field in their config will be silently upgraded from HiddenlayerGuardrail (V1) to HiddenlayerGuardrailV2 when they upgrade litellm. V1 calls /detection/v1/interactions while V2 calls /detection/v2/request-evaluations and /detection/v2/response-evaluations — these are completely different APIs with different request/response shapes. This will silently break any user on the V1 API.

Per the project's backwards-compatibility rule, the default should preserve existing behavior (1), and users should opt in to V2 explicitly:

Suggested change
version: Optional[int] = Field(default=2, description="Hiddenlayer guardrail version to use.")
version: Optional[int] = Field(default=1, description="Hiddenlayer guardrail version to use.")

Rule Used: What: avoid backwards-incompatible changes without... (source)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not an issue in our use case!

Comment thread litellm/proxy/guardrails/guardrail_hooks/hiddenlayer/hiddenlayer.py
Comment thread litellm/proxy/guardrails/guardrail_hooks/hiddenlayer/hiddenlayer.py Outdated
Comment thread litellm/proxy/guardrails/guardrail_hooks/hiddenlayer/hiddenlayer.py Outdated
@Ashton-Sidhu Ashton-Sidhu changed the title Hiddenlayer Integration: Serialize error message to a string; only scan last message Hiddenlayer Integration: Add V2 Integration Mar 18, 2026
…er.py

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
@codspeed-hq

codspeed-hq Bot commented Mar 18, 2026

Copy link
Copy Markdown
Contributor

Merging this PR will not alter performance

✅ 16 untouched benchmarks


Comparing Ashton-Sidhu:hiddenlayer-guardrail-integration-update (ab68592) with main (d319cd8)

Open in CodSpeed


new_texts = []
if input_type == "request":
inputs["structured_messages"] = output

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 structured_messages set to full response dict instead of messages list

inputs["structured_messages"] = output assigns the entire JSON response object from HiddenLayer (e.g. {"messages": [...], "model": "...", "tools": [...]}) to structured_messages. But structured_messages is expected to be a list of message dicts. Any downstream litellm code that reads inputs["structured_messages"] to build the LLM request will receive a dict instead of a list, which will likely fail.

The intent appears to be to propagate the (potentially redacted/modified) messages from HiddenLayer's response. Only the messages array should be assigned:

inputs["structured_messages"] = output.get("messages", inputs.get("structured_messages", []))

This also avoids silently dropping the original messages if HiddenLayer returns a response without a messages key.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the hiddenlayer api returns the exact same api shape it receives, so if we send up messages, there will be a messages coming back

Comment thread litellm/proxy/guardrails/guardrail_hooks/hiddenlayer/hiddenlayer.py Outdated
Comment on lines +362 to +366
payload = {
"messages": inputs.get("structured_messages"),
"model": inputs.get("model"),
"tools": inputs.get("tools")
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 V2 request payload includes null fields for model and tools

When inputs.get("model") or inputs.get("tools") is None (e.g. when tools are not part of the request), the payload is serialized as {"messages": [...], "model": null, "tools": null}. Many REST APIs distinguish between an absent field and an explicit null — sending null for tools could cause a validation error on the HiddenLayer V2 side, whereas simply omitting the key would be safe.

Consider only including keys with non-None values:

payload: dict[str, Any] = {}
if messages := inputs.get("structured_messages"):
    payload["messages"] = messages
if model := inputs.get("model"):
    payload["model"] = model
if tools := inputs.get("tools"):
    payload["tools"] = tools

Comment on lines +383 to +390
else:
payload = {}

response = await self._call_hiddenlayer(
payload, # ty:ignore[invalid-argument-type]
input_type,
hl_headers
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 V2 unconditionally calls the API even when there is nothing to scan

When input_type == "response" and inputs has neither texts nor tool_calls, payload is set to {} and _call_hiddenlayer is still invoked, sending an empty body to detection/v2/response-evaluations. Compare this with V1's behaviour, which short-circuits with result = {} (no API call) in the same situation.

Sending an empty payload to the evaluation endpoint is wasteful and may trigger a validation error from HiddenLayer. The response path should mirror V1 and skip the API call when there is no content to scan:

else:
    return inputs  # nothing to scan, skip API call

@codecov

codecov Bot commented Apr 13, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 78.30189% with 23 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
...rdrails/guardrail_hooks/hiddenlayer/hiddenlayer.py 77.77% 22 Missing ⚠️
...guardrails/guardrail_hooks/hiddenlayer/__init__.py 80.00% 1 Missing ⚠️

📢 Thoughts on this report? Let us know!

@krrish-berri-2
krrish-berri-2 changed the base branch from main to litellm_oss_staging_04_13_2026_p1 April 14, 2026 02:28
@krrish-berri-2
krrish-berri-2 merged commit 59a9047 into BerriAI:litellm_oss_staging_04_13_2026_p1 Apr 14, 2026
49 of 51 checks passed
Sameerlite pushed a commit that referenced this pull request Apr 14, 2026
* Serialize error message to a string; only scan last message

* Update litellm/proxy/guardrails/guardrail_hooks/hiddenlayer/hiddenlayer.py

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>

* Add v2 of hiddenlayer guardrail implementation

* Update litellm/proxy/guardrails/guardrail_hooks/hiddenlayer/hiddenlayer.py

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>

* Fix potential header issue

* linting

* Add image support

---------

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
fzowl pushed a commit to fzowl/litellm that referenced this pull request Jun 24, 2026
* Serialize error message to a string; only scan last message

* Update litellm/proxy/guardrails/guardrail_hooks/hiddenlayer/hiddenlayer.py

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>

* Add v2 of hiddenlayer guardrail implementation

* Update litellm/proxy/guardrails/guardrail_hooks/hiddenlayer/hiddenlayer.py

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>

* Fix potential header issue

* linting

* Add image support

---------

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
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.

2 participants