Skip to content

fix(logging): stop deepcopying results redaction cannot redact - #36638

Merged
mateo-berri merged 2 commits into
BerriAI:litellm_internal_stagingfrom
cu-aaii:litellm_redaction_deepcopy_aborts_success_logging
Aug 17, 2026
Merged

fix(logging): stop deepcopying results redaction cannot redact#36638
mateo-berri merged 2 commits into
BerriAI:litellm_internal_stagingfrom
cu-aaii:litellm_redaction_deepcopy_aborts_success_logging

Conversation

@marty-sullivan

@marty-sullivan marty-sullivan commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

TLDR

Problem this solves:

  • Redaction deepcopies binary response bodies it cannot redact
  • Those hold a thread lock, so the copy raises
  • The raise aborts success logging for that request

How it solves it:

  • Decide redactability before copying, not after
  • Unrecognized shapes already discard the copy anyway

User Flow

Before: an admin who has message redaction turned on sees a logging error for every download of a completed batch's results, and that request never reaches wherever they send their logs

  1. The admin turns on message redaction for the proxy and restarts it
  2. A developer runs a batch job to completion and downloads the results with GET https://litellm-domain/v1/files/{output_file_id}/content
  3. The download itself succeeds with HTTP 200 and the records come back
  4. The proxy log shows LiteLLM.LoggingError: [Non-Blocking] Exception occurred while success logging cannot pickle '_thread.lock' object for that request
  5. Handling of that request stops at the point the error was raised, so everything the admin configured to happen once a request succeeds does not happen for it, and the request is missing from where they collect their logs

After: the same download produces no error and completes its logging

  1. Same admin setting and restart
  2. Same batch job and same GET https://litellm-domain/v1/files/{output_file_id}/content
  3. Same HTTP 200 with the records
  4. No logging error appears for that request
  5. The request finishes its logging, so it lands where the admin collects their logs like any other request

Relevant issues

Same failure class as #6631, which fixed an unpicklable value reaching copy.deepcopy in the Langfuse integration. This is the equivalent site in message redaction, which never got the same treatment.

Linear ticket

Resolves LIT-5665

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 CI/CD checks (e.g., lint, format, unit tests)
  • My PR's scope is as isolated as possible; it only solves 1 specific problem
  • I have received a Greptile Confidence Score of at least 4/5 before requesting a maintainer review (Greptile reviews automatically once the PR is opened; only comment @greptileai to re-request a review after pushing changes)

Delays in PR merge?

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

Screenshots / Proof of Fix

QA ran two live proxies from clean worktrees, one per commit, each with its own Postgres, message redaction on (turn_off_message_logging: true), files routed to OpenAI, and real OpenAI and Anthropic calls (models gpt-5.4-nano and claude-haiku-4-5). The lock-bearing shape comes from Bedrock and Vertex batch downloads on the author's gateway; at this repo's current dependency pins OpenAI, Anthropic, and shared-client binary responses all deepcopy cleanly, so the abort itself is shown with the redaction call the failing success handler makes plus the author's gateway logs, and the live-proxy legs prove the surrounding flow and all three unified endpoints are unchanged

Before (02b0ee7)

Redaction of a lock-bearing response body

  1. Run the redaction call the success handler makes on such a body:
$ .venv/bin/python -c "
import threading
from litellm.litellm_core_utils.redact_messages import perform_redaction
class Body: pass
b = Body(); b.lock = threading.Lock()
print(perform_redaction({'litellm_params': {}}, b))"
TypeError: cannot pickle '_thread.lock' object
  1. On the author's live gateway at this commit, real Bedrock and Vertex batch jobs, every results download logged
04:52:27  ERROR  LiteLLM.LoggingError: [Non-Blocking] Exception occurred while success logging cannot pickle '_thread.lock' object
04:52:27  ERROR  LiteLLM.LoggingError: [Non-Blocking] Exception occurred while success logging cannot pickle '_thread.lock' object

with the frames naming this path: success_handler -> redact_message_input_output_from_logging -> perform_redaction -> copy.deepcopy

File content download through a live proxy

  1. Upload: curl -sS -X POST 'http://localhost:22513/v1/files?provider=openai' -H 'Authorization: Bearer sk-qa36638' -F purpose=batch -F file=@input.jsonl returns 200, id file-7hoXvfK3Af9sfFgU4NxPzm
  2. Real batch run to completion: POST /v1/batches?provider=openai returns batch_6a8385a77ebc8190b1b790214fab7b00, polled to completed, output file-HETeNHf3FZyKSSQDn5Nigx
  3. Download: curl -sS -D - 'http://localhost:22513/v1/files/file-HETeNHf3FZyKSSQDn5Nigx/content?provider=openai' -H 'Authorization: Bearer sk-qa36638' returns HTTP/1.1 200 OK with the real completion records (22 tokens of real OpenAI output)
  4. grep -c -e 'LoggingError' -e 'cannot pickle' proxy.log returns 0: OpenAI's binary body deepcopies cleanly at this pin, so the abort does not fire on this provider pair; the lock-bearing case above is what aborts

Unified endpoints with redaction on

  1. POST /v1/chat/completions (gpt-5.4-nano) returns 200 "Hi there" (chatcmpl-EDzkZabBs9vNcbeFz3f5EgBmkBpdH); spend row 8.65e-06 USD, messages and response stored as {}
  2. POST /v1/responses (gpt-5.4-nano) returns 200 (resp_5SsTnawn6skz...); spend row 2.035e-05 USD, redacted
  3. POST /v1/messages (claude-haiku-4-5) returns 200 "Hi there." (msg_011Ce95an1FZfyj6c1JLFuXy); spend row 4.3e-05 USD, redacted

After (b048ce4)

Redaction of a lock-bearing response body

  1. The same call returns the placeholder instead of raising:
$ .venv/bin/python -c "
import threading
from litellm.litellm_core_utils.redact_messages import perform_redaction
class Body: pass
b = Body(); b.lock = threading.Lock()
print(perform_redaction({'litellm_params': {}}, b))"
{'text': 'redacted-by-litellm'}
  1. On the author's live gateway with the fix, 100 Bedrock records and 6 Vertex records downloaded across several retrievals: zero cannot pickle lines and zero ERROR-level lines

File content download through a live proxy

  1. Upload: curl -sS -X POST 'http://localhost:31647/v1/files?provider=openai' -H 'Authorization: Bearer sk-qa36638' -F purpose=batch -F file=@qa36638_input.jsonl returns 200, id file-H18VTAMLdtdPDwG211Mdco
  2. Download twice: curl -sS -i 'http://localhost:31647/v1/files/file-H18VTAMLdtdPDwG211Mdco/content?provider=openai' -H 'Authorization: Bearer sk-qa36638' returns HTTP/1.1 200 OK with the 177 jsonl bytes, both times
  3. grep -n -i -E "LoggingError|cannot pickle|Traceback" proxy.log finds no matches across the whole run

Unified endpoints with redaction on

  1. POST /v1/chat/completions (gpt-5.4-nano) returns 200 "Hi, nice to meet you." (chatcmpl-EDzMWNLzxRnDt3F69DaeQStOWNhQF); spend row 1.47e-05 USD, messages and response stored as {}
  2. POST /v1/responses (gpt-5.4-nano) returns 200 "Hi! How can I help you today?" (resp_gm9gIk3L9CEY...); spend row 2.035e-05 USD, redacted
  3. POST /v1/messages (claude-haiku-4-5) returns 200 "Hi! How's it going?" (msg_011Ce93hfjBgqWjZb7EXVh2b); spend row 7.9e-05 USD, redacted

QA observations, all pre-existing behavior this PR neither causes nor worsens:

  • File content downloads never write spend rows; stream no-op
  • That gap persists with redaction disabled per-request
  • Redacted upload rows share one request_id, silently deduped
  • Spend rows keyed by response id, not call id
  • Download responses lack x-litellm-call-id header

Type

🐛 Bug Fix

Caveats (if any)

  • Unrecognized shapes still log a placeholder, unchanged from before
  • Which integrations missed those requests was not enumerated
  • Also reachable when a custom logger sets message_logging off

Final Attestation

  • The tests check the right things, including the edge cases, and regressions in the respective real-world customer use-cases are not possible after this PR

perform_redaction deepcopies the result before inspecting it, but every shape it does not
recognize falls through to the placeholder return at the end of that block, so the copy is
built and then discarded. Binary and HTTP response bodies land in exactly that case: batch
output, file content and audio responses hold an unpicklable `_thread.lock`, so
copy.deepcopy raises TypeError

The raise lands inside the try in Logging.success_handler that also wraps the callback
loop, so the handler body aborts at the redaction call and everything after it is skipped.
It surfaces only as "[Non-Blocking] Exception occurred while success logging cannot pickle
'_thread.lock' object", which is why it can run unnoticed. The async handler body reaches
perform_redaction the same way. Only deployments with message redaction enabled are
affected, since perform_redaction runs only when turn_off_message_logging resolves true

Deciding redactability before copying fixes the crash as a consequence rather than catching
it, and keeps the deepcopy off large batch bodies it was never going to help. Behaviour for
every recognized shape is unchanged: the copy still shields the caller's object from
in-place redaction

Observed on a live gateway with turn_off_message_logging enabled, where every managed-batch
output download logged that error; after this change the error no longer appears
@marty-sullivan

Copy link
Copy Markdown
Contributor Author

@greptileai

@greptile-apps

greptile-apps Bot commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

The PR avoids deep-copying response shapes that redaction cannot process, preventing lock-bearing binary responses from interrupting success logging

  • Adds an early redactability check before copy.deepcopy
  • Adds regression coverage for uncopyable responses and recognized response shapes

Confidence Score: 5/5

The PR appears safe to merge

No blocking failure remains

Important Files Changed

Filename Overview
litellm/litellm_core_utils/redact_messages.py Adds the early shape gate that returns the existing redaction placeholder before attempting a deep copy
tests/test_litellm/litellm_core_utils/test_redact_messages.py Adds deterministic regression tests for lock-bearing responses and verifies recognized response objects remain copied and redacted

Reviews (3): Last reviewed commit: "refactor(logging): drop the type-gate co..." | Re-trigger Greptile

Comment thread litellm/litellm_core_utils/redact_messages.py Outdated
@codecov

codecov Bot commented Aug 12, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

The comment restated what the gate does and carried incident detail that would drift,
including a claim about downstream callbacks that the evidence does not support. The
rationale belongs in the regression test, which fails if the copy is ever reintroduced
ahead of the gate, rather than in prose that can rot silently

Also corrects that test's docstring for the same overclaim: the raise aborts the handler
body at the redaction call, and what that costs a given deployment was not established
@marty-sullivan

Copy link
Copy Markdown
Contributor Author

Agreed, removed. The rationale now lives in the regression test, which fails if the copy is reintroduced ahead of the gate.

@greptileai

@marty-sullivan
marty-sullivan marked this pull request as ready for review August 12, 2026 08:20
@codspeed-hq

codspeed-hq Bot commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Merging this PR will not alter performance

✅ 31 untouched benchmarks


Comparing cu-aaii:litellm_redaction_deepcopy_aborts_success_logging (b048ce4) with litellm_internal_staging (9cc5a81)1

Open in CodSpeed

Footnotes

  1. No successful run was found on litellm_internal_staging (f8caaf4) during the generation of this report, so 9cc5a81 was used instead as the comparison base. There might be some changes unrelated to this pull request in this report.

@mateo-berri

Copy link
Copy Markdown
Contributor

bugbot run

@cursor cursor Bot left a comment

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.

✅ 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 b048ce4. Configure here.

@mateo-berri mateo-berri left a comment

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.

LGTM. Thanks for the contribution!

@mateo-berri
mateo-berri merged commit 67635d8 into BerriAI:litellm_internal_staging Aug 17, 2026
87 checks passed
@marty-sullivan
marty-sullivan deleted the litellm_redaction_deepcopy_aborts_success_logging branch August 18, 2026 16:19
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