Skip to content

fix: trigger pre-call guardrail hooks for batch file#21933

Open
Harshit28j wants to merge 3 commits intomainfrom
litellm_fix_pre-call-hook
Open

fix: trigger pre-call guardrail hooks for batch file#21933
Harshit28j wants to merge 3 commits intomainfrom
litellm_fix_pre-call-hook

Conversation

@Harshit28j
Copy link
Collaborator

@Harshit28j Harshit28j commented Feb 23, 2026

Relevant issues

Fixes LIT-2026 [Pre-call guardrail hook not triggered for batch requests]

Pre-Submission checklist

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

  • I have Added testing in the tests/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

Changes

Issue:
Pre-call guardrail hooks (async_pre_call_hook) were not being triggered for individual items inside batch requests. When a user uploads a JSONL file via /v1/files and creates a batch via /v1/batches, the guardrail never inspected the actual messages content of the individual requests.

Fix:
This PR intercepts the batch file at upload time (POST /v1/files where purpose="batch") to run pre-call guardrails on every line inside the JSONL before the upload is completed and sent to the provider.

  • Created litellm/proxy/batches_endpoints/batch_guardrail_utils.py containing a new run_pre_call_guardrails_on_batch_file utility function to parse JSONL batch files and run proxy_logging_obj.pre_call_hook() for each item.
  • Modified litellm/proxy/openai_files_endpoints/files_endpoints.py to trigger this utility during the create_file sequence when purpose="batch". If any guardrail raises an exception (e.g., content violation), the request is aborted and the file is rejected with the custom_id and line number reported.
  • Documented in litellm/proxy/batches_endpoints/endpoints.py that guardrails run at file upload time rather than at the /v1/batches batch creation time.
  • Added comprehensive unit tests in tests/test_litellm/proxy/batches_endpoints/test_batch_guardrail_utils.py.
  • Added an integration test in tests/test_litellm/proxy/openai_files_endpoint/test_files_endpoint.py.

@vercel
Copy link

vercel bot commented Feb 23, 2026

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

Project Deployment Actions Updated (UTC)
litellm Ready Ready Preview, Comment Feb 24, 2026 8:41am

Request Review

@greptile-apps
Copy link
Contributor

greptile-apps bot commented Feb 23, 2026

Greptile Summary

This PR adds pre-call guardrail enforcement for batch file uploads by intercepting JSONL files at /v1/files upload time (when purpose="batch") and running each line's request body through registered guardrail callbacks. The approach of checking content at upload time rather than at /v1/batches creation time is reasonable since file contents are only available during upload.

  • Tests are broken: The unit tests in test_batch_guardrail_utils.py pass proxy_logging_obj as a keyword argument, but the function signature expects user_api_key_cache. All unit tests will fail with TypeError. The integration test mocks proxy_logging_obj.pre_call_hook, but the implementation iterates litellm.callbacks directly, so the mock is never triggered and assertions will fail.
  • String-registered callbacks skipped: The implementation only checks isinstance(callback, CustomGuardrail) and isinstance(callback, CustomLogger), missing the string-to-object callback resolution that ProxyLogging.pre_call_hook performs. Guardrails registered as strings in litellm.callbacks will be silently bypassed.
  • The majority of changes in files_endpoints.py are whitespace/formatting cleanup (trailing spaces, line wrapping), with the actual functional change being ~10 lines at lines 465-475.

Confidence Score: 1/5

  • This PR has broken tests and a callback resolution gap that could allow guardrail bypass — not safe to merge as-is.
  • Score of 1 reflects that: (1) all unit tests will crash with TypeError due to wrong parameter names, (2) the integration test mocks the wrong object and assertions will fail, (3) string-registered guardrail callbacks are silently skipped, creating a potential bypass. The core idea is sound but the implementation and tests need significant rework before merging.
  • tests/test_litellm/proxy/batches_endpoints/test_batch_guardrail_utils.py (broken function call signatures), tests/test_litellm/proxy/openai_files_endpoint/test_files_endpoint.py (mock targets wrong object), litellm/proxy/batches_endpoints/batch_guardrail_utils.py (missing string callback resolution)

Important Files Changed

Filename Overview
litellm/proxy/batches_endpoints/batch_guardrail_utils.py New utility for running pre-call guardrails on batch JSONL files. Skips string-registered callbacks (diverges from ProxyLogging.pre_call_hook behavior), and the exception wrapping loses HTTP status codes.
litellm/proxy/batches_endpoints/endpoints.py Minor documentation addition explaining guardrails run at file upload time. No functional changes.
litellm/proxy/openai_files_endpoints/files_endpoints.py Adds guardrail hook invocation for batch file uploads. Most changes are whitespace reformatting. The functional change (lines 465-475) correctly integrates the guardrail call but uses an inline import.
tests/test_litellm/proxy/batches_endpoints/test_batch_guardrail_utils.py Unit tests pass wrong parameter name (proxy_logging_obj instead of user_api_key_cache) and assert on a method the implementation never calls. All tests will fail with TypeError at runtime.
tests/test_litellm/proxy/openai_files_endpoint/test_files_endpoint.py Integration test mocks proxy_logging_obj.pre_call_hook, but the implementation iterates litellm.callbacks directly. The mock is never invoked, so assertions will fail.

Sequence Diagram

sequenceDiagram
    participant Client
    participant FilesEndpoint as /v1/files endpoint
    participant GuardrailUtils as batch_guardrail_utils
    participant Callbacks as litellm.callbacks
    participant Provider as LLM Provider

    Client->>FilesEndpoint: POST /v1/files (purpose=batch, file=batch.jsonl)
    FilesEndpoint->>FilesEndpoint: Read file content
    FilesEndpoint->>FilesEndpoint: Check purpose == "batch"
    
    alt purpose is "batch"
        FilesEndpoint->>GuardrailUtils: run_pre_call_guardrails_on_batch_file(file_content, cache, key_dict)
        loop For each JSONL line
            GuardrailUtils->>GuardrailUtils: Parse JSON line, extract body
            alt body has "messages"
                GuardrailUtils->>Callbacks: Iterate litellm.callbacks
                Callbacks-->>GuardrailUtils: CustomGuardrail.async_pre_call_hook()
                alt Guardrail rejects
                    GuardrailUtils-->>FilesEndpoint: Raise Exception
                    FilesEndpoint-->>Client: 500 Error (guardrail rejection)
                end
            end
        end
        GuardrailUtils-->>FilesEndpoint: All items passed
    end
    
    FilesEndpoint->>Provider: route_create_file()
    Provider-->>FilesEndpoint: File created
    FilesEndpoint-->>Client: 200 OK (OpenAIFileObject)
Loading

Last reviewed commit: 208ac5a

Copy link
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

5 files reviewed, 4 comments

Edit Code Review Agent Settings | Greptile

Comment on lines +32 to +35
try:
json_obj = json.loads(line)
except json.JSONDecodeError:
continue
Copy link
Contributor

Choose a reason for hiding this comment

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

Silently skipping malformed JSON is a guardrail bypass vector

Malformed or invalid JSON lines are silently skipped without running guardrails on them. If the provider's batch parser is more lenient than json.loads (or handles encoding differently), a malicious user could craft lines that fail Python's JSON parsing but succeed on the provider side, thereby bypassing guardrails entirely.

Consider logging a warning for malformed lines, or rejecting the entire file if any line fails to parse, to prevent guardrail evasion.

Comment on lines +39 to +40
if not body or "messages" not in body:
continue
Copy link
Contributor

Choose a reason for hiding this comment

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

Embedding requests bypass guardrails

Lines without a messages key are skipped, but batch JSONL files can also contain embedding requests that use input instead of messages (e.g., {"url": "/v1/embeddings", "body": {"input": "sensitive text", "model": "text-embedding-ada-002"}}). The _get_call_type_from_endpoint function already maps embeddings to "aembedding", but those lines will never reach the guardrail check because they're filtered out here.

Consider also checking for "input" in body, or passing all lines with a non-empty body through the guardrail hook.

@Harshit28j
Copy link
Collaborator Author

@greptile can you please review this PR ?

Copy link
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

5 files reviewed, 3 comments

Edit Code Review Agent Settings | Greptile

Comment on lines +25 to +29
await run_pre_call_guardrails_on_batch_file(
file_content=file_content,
proxy_logging_obj=mock_proxy_logging_obj,
user_api_key_dict=mock_user_api_key_dict
)
Copy link
Contributor

Choose a reason for hiding this comment

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

Tests pass wrong parameter name — will crash at runtime

All unit tests call run_pre_call_guardrails_on_batch_file with proxy_logging_obj=..., but the actual function signature accepts user_api_key_cache (a DualCache), not proxy_logging_obj. Every test in this file will fail with TypeError: run_pre_call_guardrails_on_batch_file() got an unexpected keyword argument 'proxy_logging_obj'.

Additionally, the tests assert on mock_proxy_logging_obj.pre_call_hook.call_count, but the implementation never calls proxy_logging_obj.pre_call_hook — it directly iterates litellm.callbacks. This means even if the parameter issue were fixed, the mock would never be invoked and the assertions would fail.

The tests need to be rewritten to match the actual implementation: either mock litellm.callbacks with CustomGuardrail/CustomLogger instances, or change the implementation to use proxy_logging_obj.pre_call_hook() and pass it as a parameter.

Comment on lines +1151 to +1155
mock_pre_call_hook = mocker.patch.object(
proxy_logging_obj,
"pre_call_hook",
side_effect=Exception("Guardrail blocked this batch item")
)
Copy link
Contributor

Choose a reason for hiding this comment

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

Mock targets the wrong object — assertion will fail

This test mocks proxy_logging_obj.pre_call_hook, but run_pre_call_guardrails_on_batch_file never calls that method. Instead, it directly iterates litellm.callbacks and invokes callback.async_pre_call_hook() on each CustomGuardrail/CustomLogger instance.

The call at files_endpoints.py:471 passes user_api_key_cache, not the proxy_logging_obj itself. So the assertion on line 1175 that mock_pre_call_hook.call_count == 1 will always be 0, and the test won't validate the guardrail behavior.

To properly test this, mock litellm.callbacks with a CustomGuardrail instance whose async_pre_call_hook raises the exception.

Comment on lines +57 to +58
try:
for callback in litellm.callbacks:
Copy link
Contributor

Choose a reason for hiding this comment

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

String-registered callbacks are silently skipped

The original pre_call_hook in ProxyLogging (see litellm/proxy/utils.py:1311-1316) resolves string callbacks via get_custom_logger_compatible_class() before checking their type. This implementation only checks isinstance(callback, CustomGuardrail) and isinstance(callback, CustomLogger), so guardrails registered as strings in litellm.callbacks (which is a common pattern) will be silently skipped.

Consider adding the same string-to-object resolution logic:

for callback in litellm.callbacks:
    _callback = callback
    if isinstance(callback, str):
        _callback = litellm.litellm_core_utils.litellm_logging.get_custom_logger_compatible_class(callback)
    if _callback is None:
        continue
    if isinstance(_callback, CustomGuardrail):
        ...

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.

1 participant