fix(proxy): omit litellm_batch_guardrail when no guardrail acted - #37964
Merged
yucheng-berri merged 1 commit intoAug 22, 2026
Merged
Conversation
The field is declared optional on OpenAIFileObject and its own docstring says it is absent on every upload guardrails did not touch, but the /v1/files routes have no response_model, so FastAPI falls through to jsonable_encoder with exclude_none off and serialises the unset default as an explicit null. Every create and retrieve response on a proxy with no guardrails configured at all picked up a litellm_batch_guardrail: null it never had before, and so did every row of a file list, since those rows are the same object. A wrap serializer drops the key only when nothing set it, so the populated report still reaches the wire intact, including a record whose guardrail is null. The managed-files list route spreads a stored file_object blob rather than the model, so rows persisted before this lands keep their null until it is dropped there too.
Contributor
Author
Contributor
Author
|
bugbot run |
Contributor
There was a problem hiding this comment.
✅ Bugbot reviewed your changes and found no new issues!
Comment @cursor review or bugbot run to trigger another review on this PR
Reviewed by Cursor Bugbot for commit 8aad40a. Configure here.
Contributor
Greptile SummaryThis PR restores the OpenAI-compatible file response shape by omitting
Confidence Score: 5/5The PR appears safe to merge No blocking failure remains
|
| Filename | Overview |
|---|---|
| litellm/types/llms/openai.py | Adds targeted serialization behavior that omits an unset batch guardrail report while retaining populated report data |
| litellm/proxy/pass_through_endpoints/managed_id_rewriter.py | Removes the legacy null extension field from locally constructed managed-file list items |
| tests/test_litellm/proxy/openai_files_endpoint/test_files_endpoint.py | Covers file creation with no configured guardrail and with a no-op guardrail |
| tests/test_litellm/proxy/pass_through_endpoints/test_managed_id_rewriter.py | Verifies database-backed file lists omit legacy persisted null guardrail fields |
| tests/test_litellm/types/llms/test_types_llms_openai.py | Covers absent and populated reports, nested null preservation, aliases, schemas, round trips, and nested list serialization |
Reviews (2): Last reviewed commit: "fix(proxy): omit litellm_batch_guardrail..." | Re-trigger Greptile
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
Contributor
Contributor
Author
|
@greptileai review latest head and rescore |
yucheng-berri
merged commit Aug 22, 2026
8a7c873
into
litellm_internal_staging
77 of 78 checks passed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
TLDR
Problem this solves:
litellm_batch_guardrail: nullHow it solves it:
User Flow
Before: a developer uploading a plain file to the gateway suddenly gets a field back that OpenAI never sends, on a proxy with no guardrails turned on at all
purpose=user_dataand a text file"litellm_batch_guardrail": null, where OpenAI's own API returns nineAfter: the same upload returns exactly what OpenAI returns, and the guardrail report only shows up when a guardrail actually did something
purpose=user_dataand a text filelitellm_batch_guardrailpurpose=batchand a JSONL file where one row trips itlitellm_batch_guardrail, reportingsubmitted_recordsand every changed row bycustom_idand line, exactly as beforeRelevant issues
Linear ticket
Refs LIT-5276
Pre-Submission checklist
uv run pytest tests/test_litellm/<your_test_file>.py -vScreenshots / Proof of Fix
End to end against a live proxy pair, real OpenAI files API, one shared Postgres. Port 4762 is the current build, 4761 is this branch. The guardrail report survives untouched, and the key disappears from the plain upload, the file list, and the retrieve
Shared setup, one proxy config, one plain file, one batch file. Two guardrails are on by default so the batch case has something to report, and the plain upload proves the key stays away even when guardrails exist.
Both sides ran against the real OpenAI files API with a real key, same config, same order.
The list route is served from the DB rather than forwarded, so it needs a second config with a database and the managed-id flag on. Same two proxies, same Postgres, so one row is written by the old build and one by the new.
Before (490c9f9)
Plain upload, no guardrail can act
curl -sS -X POST http://127.0.0.1:4762/v1/files -H "Authorization: Bearer $KEY" -F purpose=user_data -F file=@plain.txt{"id":"file-LsWJYXvJgfzWoZjUR2QCgJ","bytes":12,"created_at":1787425081,"filename":"plain.txt","object":"file","purpose":"user_data","status":"processed","expires_at":null,"status_details":null,"litellm_batch_guardrail":null}curl -sS http://127.0.0.1:4762/v1/files/file-LsWJYXvJgfzWoZjUR2QCgJ -H "Authorization: Bearer $KEY"{"id":"file-LsWJYXvJgfzWoZjUR2QCgJ","bytes":12,"created_at":1787425081,"filename":"plain.txt","object":"file","purpose":"user_data","status":"processed","expires_at":null,"status_details":null,"litellm_batch_guardrail":null}bytes, created_at, expires_at, filename, id, object, purpose, status, status_detailsBatch upload, guardrails act on two rows
curl -sS -X POST http://127.0.0.1:4762/v1/files -H "Authorization: Bearer $KEY" -F purpose=batch -F file=@batch.jsonl{ "id": "file-HMSed3EWMHm9NKmj3ed6BN", "bytes": 331, "created_at": 1787425082, "filename": "batch.jsonl", "object": "file", "purpose": "batch", "status": "processed", "expires_at": 1790017082, "status_details": null, "litellm_batch_guardrail": { "submitted_records": 2, "modified_records": [ {"line": 2, "custom_id": "dirty", "action": "redacted", "guardrail": null}, {"line": 3, "custom_id": "blocked", "action": "dropped", "guardrail": null} ] } }File list, one row persisted by an older build
curl -sS http://127.0.0.1:4762/openai_passthrough/v1/files -H "Authorization: Bearer $KEY"{ "object": "list", "data": [ {"id": "bGl0ZWxsbV9wcm94eTpwYXNzdGhyb3VnaDtwcm92aWRlcjpvcGVuYWk7dW5pZmllZF9pZCw0Nzk5ZDBmOS1mMWIzLTRkMDgtODE3MS1lZGUyNTc1ZjA2YTc7cmF3X2lkLGZpbGUtR0ZLNUNCOVVuWk1KWVo0M2FKUmUxZw", "object": "file", "created_at": 1787427769, "bytes": 481, "status": "processed", "purpose": "batch", "filename": "batch.jsonl", "expires_at": 1790019769, "status_details": null}, {"id": "bGl0ZWxsbV9wcm94eTpwYXNzdGhyb3VnaDtwcm92aWRlcjpvcGVuYWk7dW5pZmllZF9pZCxmYTQ0ZjA4Mi0wZWQ3LTQ5OGUtYjc1NC04MTM4OTk3MmQ0YzY7cmF3X2lkLGZpbGUtOHdnQnZWOThRdVlBQUQxTGJHSnp5dA", "object": "file", "created_at": 1787427725, "bytes": 481, "status": "processed", "purpose": "batch", "filename": "batch.jsonl", "expires_at": 1790019725, "status_details": null, "litellm_batch_guardrail": null} ], "has_more": false }curl -sS http://127.0.0.1:4762/v1/files -H "Authorization: Bearer $KEY"returns both rows carrying"litellm_batch_guardrail": nullAfter (8aad40a)
Plain upload, no guardrail can act
curl -sS -X POST http://127.0.0.1:4761/v1/files -H "Authorization: Bearer $KEY" -F purpose=user_data -F file=@plain.txt{"id":"file-TZUb1n69n6KvpyPQFRLrNt","bytes":12,"created_at":1787425082,"filename":"plain.txt","object":"file","purpose":"user_data","status":"processed","expires_at":null,"status_details":null}curl -sS http://127.0.0.1:4761/v1/files/file-TZUb1n69n6KvpyPQFRLrNt -H "Authorization: Bearer $KEY"{"id":"file-TZUb1n69n6KvpyPQFRLrNt","bytes":12,"created_at":1787425082,"filename":"plain.txt","object":"file","purpose":"user_data","status":"processed","expires_at":null,"status_details":null}Batch upload, guardrails act on two rows
curl -sS -X POST http://127.0.0.1:4761/v1/files -H "Authorization: Bearer $KEY" -F purpose=batch -F file=@batch.jsonl{ "id": "file-AFXB9VPkGVa1kB3uvmTmyo", "bytes": 331, "created_at": 1787425083, "filename": "batch.jsonl", "object": "file", "purpose": "batch", "status": "processed", "expires_at": 1790017083, "status_details": null, "litellm_batch_guardrail": { "submitted_records": 2, "modified_records": [ {"line": 2, "custom_id": "dirty", "action": "redacted", "guardrail": null}, {"line": 3, "custom_id": "blocked", "action": "dropped", "guardrail": null} ] } }File list, one row persisted by an older build
curl -sS http://127.0.0.1:4761/openai_passthrough/v1/files -H "Authorization: Bearer $KEY"{ "object": "list", "data": [ {"id": "bGl0ZWxsbV9wcm94eTpwYXNzdGhyb3VnaDtwcm92aWRlcjpvcGVuYWk7dW5pZmllZF9pZCw0Nzk5ZDBmOS1mMWIzLTRkMDgtODE3MS1lZGUyNTc1ZjA2YTc7cmF3X2lkLGZpbGUtR0ZLNUNCOVVuWk1KWVo0M2FKUmUxZw", "object": "file", "created_at": 1787427769, "bytes": 481, "status": "processed", "purpose": "batch", "filename": "batch.jsonl", "expires_at": 1790019769, "status_details": null}, {"id": "bGl0ZWxsbV9wcm94eTpwYXNzdGhyb3VnaDtwcm92aWRlcjpvcGVuYWk7dW5pZmllZF9pZCxmYTQ0ZjA4Mi0wZWQ3LTQ5OGUtYjc1NC04MTM4OTk3MmQ0YzY7cmF3X2lkLGZpbGUtOHdnQnZWOThRdVlBQUQxTGJHSnp5dA", "object": "file", "created_at": 1787427725, "bytes": 481, "status": "processed", "purpose": "batch", "filename": "batch.jsonl", "expires_at": 1790019725, "status_details": null} ], "has_more": false }The second row is the one the old build wrote, and it comes back clean without any data migration
curl -sS http://127.0.0.1:4761/v1/files -H "Authorization: Bearer $KEY"returns both rows with the nine OpenAI keysByte-identical to the Before run apart from the ids and timestamps OpenAI assigns, including the nested
"guardrail": null, which is why the fix targets the one top-level key rather than reaching forexclude_noneType
🐛 Bug Fix
Caveats (if any)
_hidden_params, since moving it would change the create route's public body shape a second timeOpenAIFileObject.json()is left alone: it returns a dict from a method contracted to return a string, but it has no callers in this repo and any SDK caller relying on it would breakFinal Attestation