fix(utils): redact credential kwargs from the set_verbose request line - #39526
Conversation
`litellm.set_verbose = True` printed the caller's kwargs verbatim to stdout, so `api_key` and its siblings landed in terminals and container log drains in plaintext while the same statement's logger emission was already redacted. Mask the kwargs at the source with a shared helper in `litellm_core_utils/sensitive_data_masker.py`, reusing the existing `SensitiveDataMasker` key classification and the `REDACTED` marker `secret_redaction.py` already owns, so both debug surfaces agree.
Greptile SummaryThis PR hardens verbose request rendering by applying shared credential redaction before formatting keyword arguments.
Confidence Score: 5/5The PR appears safe to merge. The previously reported sequence-container gap is fixed, and no blocking failure remains.
|
| Filename | Overview |
|---|---|
| litellm/litellm_core_utils/secret_redaction.py | Makes the shared redaction marker public within the package without changing its value or existing redaction behavior. |
| litellm/litellm_core_utils/sensitive_data_masker.py | Adds recursive full-value credential redaction and correctly closes the previously reported list and tuple traversal gap. |
| litellm/utils.py | Redacts keyword arguments before rendering the verbose request line while preserving ordinary argument output. |
| tests/test_litellm/litellm_core_utils/test_sensitive_data_masker.py | Verifies complete credential replacement, nested mapping traversal, and list and tuple preservation. |
| tests/test_litellm/test_utils.py | Adds end-to-end verbose-output coverage for direct credentials, headers, nested sequences, and ordinary parameters. |
Reviews (2): Last reviewed commit: "fix(utils): redact credentials nested in..." | Re-trigger Greptile
PR overviewAll previously flagged issues have been addressed. No open security concerns remain on this pull request. Security reviewNo open security issues remain on this pull request. Fixed/addressed: 1 · PR risk: 0/10 |
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
redact_credentials_in_payload only recursed into mappings, so a credential-named key one level inside a list or tuple, the shape extra_body and metadata routinely carry, still reached stdout under set_verbose. Rebuild sequences element by element too, keeping the container's own type so the printed repr is unchanged apart from the secret.
|
bugbot run |
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 0a62195. Configure here.
aa9f3d9
into
litellm_internal_staging
TLDR
Problem this solves:
litellm.set_verbose = Trueechoed the caller'sapi_keyback to stdout in plaintextHow it solves it:
Request to litellm:line is builtSensitiveDataMaskeralready carries, and the sharedREDACTEDmarker, so both debug surfaces agree on what a credential isextra_headersandextra_bodyroutinely carry onemodel,max_tokensandtemperaturestill print unchangedMasking by key name rather than by value shape is the point:
redact_stringonly catches a value that already looks like a secret (ansk-prefix and friends), so a key whose shape nobody anticipated is exactly the one that slips through.User Flow
Before: a developer who turns on verbose debugging to diagnose a failing call gets their provider key printed in full into the terminal and into whatever collects that output
litellm.set_verbose = Trueto their scriptlitellm.completion(model="gpt-4o-mini", api_key=<their provider key>, messages=[...])Request to litellm:line listing every argument they passed, with theapi_keyvalue shown verbatimAfter: the same debugging session prints the same request line with the credential replaced, so the log is safe to keep or share
litellm.set_verbose = Trueto their scriptlitellm.completion(model="gpt-4o-mini", api_key=<their provider key>, messages=[...])Request to litellm:line, now showingapi_key='REDACTED', whilemodel,max_tokensandtemperatureare still printed as beforeThe proxy is not on this flow. A proxy passes the provider key to the router after the request line is built, so
--detailed_debugnever printed it; the QA below runs all three proxy endpoints anyway as a no-regression check.Relevant issues
Linear ticket
Resolves LIT-6823
Pre-Submission checklist
Please complete all items before asking a LiteLLM maintainer to review your PR
uv run pytest tests/test_litellm/<your_test_file>.py -v. Leave the suites (make test-unit-*,make test-unit) to CI: it finishes in ~15 minutes where a laptop takes an hour or more@greptileaito 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
Both legs ran live against real OpenAI and Anthropic APIs (real spend), each in its own worktree with its own
.venv, its own.envderived from the main checkout, and its own random free high port. No mocks, no stubs, no pytest. Every credential check is agrep -ccount, so no key value is ever printed.Setup, identical on both legs apart from the commit:
The SDK script turns on verbose debugging and makes four calls, passing the provider key explicitly the way the ticket's flow does:
The proxy leg boots the same commit with
--detailed_debugand two uvicorn workers, then hits all three unified endpoints:Before, at the merge base
658f50663d19f613a3f5caf998168da019764ad8SDK, three calls (the nested-metadata case was added later and is after-leg only, which changes nothing here since no kwarg was redacted at all at this commit), script exit 0, port 38417:
sdk.logRequest to litellm:api_key='sk--F "$OPENAI_API_KEY"-F "$ANTHROPIC_API_KEY"api_key='REDACTED'litellm.completionlitellm.responseslitellm.anthropic_messagesEvery provider key the developer passed is in stdout verbatim, zero redactions, while all three calls return real provider responses.
Live proxy,
--detailed_debug, 2 workers, 563 log lines:/v1/chat/completions/v1/responses/v1/messagesproxy.logRequest to litellm:api_key='sk--F "$OPENAI_API_KEY"-F "$ANTHROPIC_API_KEY"The proxy never printed a provider key even before the fix, because the router attaches the resolved key after this line is built. That is why the User Flow above is SDK-only and the proxy leg is a no-regression check.
After, at the PR head
0a62195db25dabfd980fbd0fa50d5b0f4a33f624SDK, four calls, script exit 0, port 25341:
sdk.logRequest to litellm:api_key='sk--F "$OPENAI_API_KEY"-F "$ANTHROPIC_API_KEY"api_key='REDACTED'max_tokens=16model='gpt-4o-mini'-F 'sk-fake-lit6823-nested-in-a-list'-F "'name': 'openai'"litellm.completionlitellm.responseslitellm.anthropic_messageslitellm.completionwith a credential nested in a listThe counted grep the ticket asks about goes 3 to 0 while
api_key='REDACTED'goes 0 to 4, the nested-in-a-list credential is gone too, its ordinary sibling'name': 'openai'survives, andmax_tokensandmodelstill print. Positive control: the same-Fgreps return 1 against the leg's own.env, so the zeros are real absences rather than a broken grep.Live proxy,
--detailed_debug, 2 workers, 565 log lines:/v1/chat/completions/v1/responses/v1/messagesproxy.logRequest to litellm:api_key='sk--F "$OPENAI_API_KEY"-F "$ANTHROPIC_API_KEY"sk-substringmax_tokens=16/max_output_tokens=16model='openai/gpt-4o-mini'/model='anthropic/claude-sonnet-5'All three endpoints still answer 200 with real provider text and the request line still carries the params a developer debugs with, so nothing regressed on the surface that never leaked
Both legs' logs and response bodies were deleted after counting, both proxies were stopped, and both ports were confirmed free with no orphan workers
Observations the diff does not show:
api_keyat alllitellm.set_verbosealone does not enable the printlitellm._logging.set_verbosemust be set toometadatais stripped before the provider payloadType
🐛 Bug Fix
Caveats (if any)
extra_bodystill reaches stdout, on a different line.set_verbosealso printsFinal returned optional params: {...}fromlitellm/utils.py, and that statement is outside this fix. Probed at this head, it is the only surviving surface and only forextra_body: a top-levelapi_keyand a credential nested inmetadataboth count 0. Tracked as LIT-6835 rather than fixed here, because that call site is not behind the debug guard this one sits behind, so redacting it costs about 225 us on every request on top of the 25 us the f-string already spends, and the obvious guard is wrong: the guard helper readslitellm._logging.set_verbosewhile that print readslitellm.set_verbose, so guarding would silently drop the line for anyone using the documented flag. Changing an unguarded hot-path line on every request, for a credential someone deliberately nested underextra_body, is a worse trade than leaving it to its own PR--detailed_debugrequest line now printsuser_api_key_request_route,user_api_key_user_idanduser_api_key_spendasREDACTED. Those are proxy-injected metadata, not secrets, so a little debuggability is lost on a surface that was never leaking. The shared masker does support an exact-match exclusion set, but enumerating these names here would go stale the moment a genuinely secretuser_api_key_*field is added, and that is the exact shape of leak this PR exists to prevent. Redacting by default is the safer side of that trade, and the route is still readable from the proxy's own request logssecret_fieldsis redacted too. That one is correct rather than collateral: it carriesraw_headers, Authorization tokens included, and both the spend-tracking body scrubber and the guardrail path already strip it elsewhereFinal Attestation
Note
Medium Risk
Touches a hot debug/logging path and broad key-name redaction (including nested structures), which could over-redact proxy metadata fields but prevents credential leaks in shared logs.
Overview
Fixes plaintext credential leakage when
set_verboseprints thelitellm.completion(...)request line to stdout (terminals, CI, log drains).The verbose path now runs kwargs through a new
redact_credentials_in_payloadhelper before formatting the debug string. That helper uses the same sensitive-key rules asSensitiveDataMaskerbut replaces entire values with the sharedREDACTEDmarker (no partialsk-xxxx****reveal), including non-string secrets. It recurses into nested dicts and list/tuple elements so keys likeapi_key,Authorization, and credentials insideextra_body/extra_headersare scrubbed while ordinary params (model,max_tokens, etc.) still print unchanged.secret_redactionexportsREDACTED(renamed from private_REDACTED) so stdout redaction matches other scrubbers. Unit tests cover the new helper and the verbose request-line behavior.Reviewed by Cursor Bugbot for commit 0a62195. Bugbot is set up for automated code reviews on this repo. Configure here.