Skip to content

fix(utils): redact credential kwargs from the set_verbose request line - #39526

Merged
mateo-berri merged 2 commits into
litellm_internal_stagingfrom
litellm_mask_verbose_request_kwargs
Sep 3, 2026
Merged

fix(utils): redact credential kwargs from the set_verbose request line#39526
mateo-berri merged 2 commits into
litellm_internal_stagingfrom
litellm_mask_verbose_request_kwargs

Conversation

@mateo-berri

@mateo-berri mateo-berri commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

TLDR

Problem this solves:

  • litellm.set_verbose = True echoed the caller's api_key back to stdout in plaintext
  • Terminals, CI job logs and log drains captured a working provider key
  • The same statement's logger copy was already redacted, so the two halves of one print disagreed

How it solves it:

  • Mask credential-named kwargs before the Request to litellm: line is built
  • Reuse the sensitive-key names the shared SensitiveDataMasker already carries, and the shared REDACTED marker, so both debug surfaces agree on what a credential is
  • Cover nested dicts, lists and tuples, since extra_headers and extra_body routinely carry one
  • Ordinary params like model, max_tokens and temperature still print unchanged

Masking by key name rather than by value shape is the point: redact_string only catches a value that already looks like a secret (an sk- 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

  1. They add litellm.set_verbose = True to their script
  2. They call litellm.completion(model="gpt-4o-mini", api_key=<their provider key>, messages=[...])
  3. The console prints a Request to litellm: line listing every argument they passed, with the api_key value shown verbatim
  4. The call itself succeeds and returns a normal completion
  5. They paste that output into a bug report, or their CI job archives it, and anyone who can read that log now holds a working provider key

After: the same debugging session prints the same request line with the credential replaced, so the log is safe to keep or share

  1. They add litellm.set_verbose = True to their script
  2. They call litellm.completion(model="gpt-4o-mini", api_key=<their provider key>, messages=[...])
  3. The console prints the same Request to litellm: line, now showing api_key='REDACTED', while model, max_tokens and temperature are still printed as before
  4. The call itself succeeds and returns a normal completion
  5. They paste that output into a bug report, or their CI job archives it, and the log carries no usable credential

The proxy is not on this flow. A proxy passes the provider key to the router after the request line is built, so --detailed_debug never 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

  • I have added meaningful tests
  • The handful of test files covering my change pass locally, e.g. 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
  • My PR passes all required CI/CD checks (e.g., lint, schema.d.ts sync check, etc.)
  • 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

Both legs ran live against real OpenAI and Anthropic APIs (real spend), each in its own worktree with its own .venv, its own .env derived from the main checkout, and its own random free high port. No mocks, no stubs, no pytest. Every credential check is a grep -c count, so no key value is ever printed.

Setup, identical on both legs apart from the commit:

git -C /Users/mateo/Development/litellm worktree add <leg worktree> <commit> --detach
cd <leg worktree> && make bootstrap
grep -v -E '^(DATABASE_URL|STORE_MODEL_IN_DB)=' /Users/mateo/Development/litellm/.env > ./.env
lsof -nP -iTCP:<port> -sTCP:LISTEN          # must print nothing
./.venv/bin/python -c "import litellm; print(litellm.__file__)"   # must resolve inside the leg worktree

The SDK script turns on verbose debugging and makes four calls, passing the provider key explicitly the way the ticket's flow does:

litellm.set_verbose = True
litellm._logging.set_verbose = True
litellm.completion(model="gpt-4o-mini", api_key=OPENAI_KEY, messages=[...], max_tokens=16)
litellm.responses(model="gpt-4o-mini", api_key=OPENAI_KEY, input="say hi", max_output_tokens=16)
asyncio.run(litellm.anthropic_messages(model="claude-sonnet-5", api_key=ANTHROPIC_KEY, messages=[...], max_tokens=16))
litellm.completion(model="gpt-4o-mini", api_key=OPENAI_KEY, messages=[...], max_tokens=16,
                   metadata={"upstreams": [{"name": "openai", "api_key": "sk-fake-lit6823-nested-in-a-list"}]})
set -a; . ./.env; set +a
./.venv/bin/python sdk_verbose_qa.py > sdk.log 2>&1; echo "exit=$?"
grep -c 'Request to litellm:' sdk.log
grep -c "api_key='sk-" sdk.log
grep -c -F "$OPENAI_API_KEY" sdk.log
grep -c -F "$ANTHROPIC_API_KEY" sdk.log
grep -c "api_key='REDACTED'" sdk.log
grep -c 'max_tokens=16' sdk.log
grep -c "model='gpt-4o-mini'" sdk.log
grep -c -F 'sk-fake-lit6823-nested-in-a-list' sdk.log
grep -c -F "'name': 'openai'" sdk.log

The proxy leg boots the same commit with --detailed_debug and two uvicorn workers, then hits all three unified endpoints:

./.venv/bin/python litellm/proxy/proxy_cli.py --config lit6823.yaml --port <port> --num_workers 2 --detailed_debug > proxy.log 2>&1 &
until [ "$(curl -s -o /dev/null -m 3 -w '%{http_code}' http://127.0.0.1:<port>/health/readiness)" = "200" ]; do sleep 2; done
curl -s -o r_chat.json -w '%{http_code}\n' -X POST http://127.0.0.1:<port>/v1/chat/completions \
  -H "Authorization: Bearer $MASTER_KEY" -H 'Content-Type: application/json' \
  -d '{"model":"gpt-4o-mini","messages":[{"role":"user","content":"say hi"}],"max_tokens":16}'
curl -s -o r_resp.json -w '%{http_code}\n' -X POST http://127.0.0.1:<port>/v1/responses \
  -H "Authorization: Bearer $MASTER_KEY" -H 'Content-Type: application/json' \
  -d '{"model":"gpt-4o-mini","input":"say hi","max_output_tokens":16}'
curl -s -o r_msg.json -w '%{http_code}\n' -X POST http://127.0.0.1:<port>/v1/messages \
  -H "Authorization: Bearer $MASTER_KEY" -H 'Content-Type: application/json' \
  -d '{"model":"claude-sonnet-5","messages":[{"role":"user","content":"say hi"}],"max_tokens":16}'
# then the same greps against proxy.log

Before, at the merge base 658f50663d19f613a3f5caf998168da019764ad8

SDK, 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:

grep over sdk.log count
Request to litellm: 3
api_key='sk- 3
-F "$OPENAI_API_KEY" 2
-F "$ANTHROPIC_API_KEY" 1
api_key='REDACTED' 0
call model returned text len
litellm.completion gpt-4o-mini-2024-07-18 31
litellm.responses gpt-4o-mini-2024-07-18 37
litellm.anthropic_messages claude-sonnet-5 37

Every 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:

endpoint HTTP model assistant text
/v1/chat/completions 200 gpt-4o-mini Hi! How can I assist you today?
/v1/responses 200 gpt-4o-mini Hi there! How can I assist you today?
/v1/messages 200 claude-sonnet-5 Hi there! How can I help you today?
grep over proxy.log count
Request to litellm: 4
api_key='sk- 0
-F "$OPENAI_API_KEY" 0
-F "$ANTHROPIC_API_KEY" 0
master key, key prefixes, first 20 and last 8 chars of each key 0

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 0a62195db25dabfd980fbd0fa50d5b0f4a33f624

SDK, four calls, script exit 0, port 25341:

grep over sdk.log count
Request to litellm: 4
api_key='sk- 0
-F "$OPENAI_API_KEY" 0
-F "$ANTHROPIC_API_KEY" 0
api_key='REDACTED' 4
max_tokens=16 3
model='gpt-4o-mini' 3
-F 'sk-fake-lit6823-nested-in-a-list' 0
-F "'name': 'openai'" 1
call model returned text len
litellm.completion gpt-4o-mini-2024-07-18 37
litellm.responses gpt-4o-mini-2024-07-18 37
litellm.anthropic_messages claude-sonnet-5 29
litellm.completion with a credential nested in a list gpt-4o-mini-2024-07-18 37

The 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, and max_tokens and model still print. Positive control: the same -F greps 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:

endpoint HTTP model assistant text
/v1/chat/completions 200 gpt-4o-mini Hi there! How can I assist you today?
/v1/responses 200 gpt-4o-mini Hi there! How are you today?
/v1/messages 200 claude-sonnet-5 Hi! How can I help you today?
grep over proxy.log count
Request to litellm: 4
api_key='sk- 0
-F "$OPENAI_API_KEY" 0
-F "$ANTHROPIC_API_KEY" 0
master key anywhere, any sk- substring 0
max_tokens=16 / max_output_tokens=16 2 / 3
model='openai/gpt-4o-mini' / model='anthropic/claude-sonnet-5' 3 / 1

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:

  • Proxy request line never carried api_key at all
  • litellm.set_verbose alone does not enable the print
  • litellm._logging.set_verbose must be set too
  • Four request lines printed for three proxy requests
  • Nested metadata is stripped before the provider payload

Type

🐛 Bug Fix

Caveats (if any)

  • Low. A credential nested under extra_body still reaches stdout, on a different line. set_verbose also prints Final returned optional params: {...} from litellm/utils.py, and that statement is outside this fix. Probed at this head, it is the only surviving surface and only for extra_body: a top-level api_key and a credential nested in metadata both 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 reads litellm._logging.set_verbose while that print reads litellm.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 under extra_body, is a worse trade than leaving it to its own PR
  • Low. The proxy's --detailed_debug request line now prints user_api_key_request_route, user_api_key_user_id and user_api_key_spend as REDACTED. 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 secret user_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 logs
  • Low. secret_fields is redacted too. That one is correct rather than collateral: it carries raw_headers, Authorization tokens included, and both the spend-tracking body scrubber and the guardrail path already strip it elsewhere

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

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_verbose prints the litellm.completion(...) request line to stdout (terminals, CI, log drains).

The verbose path now runs kwargs through a new redact_credentials_in_payload helper before formatting the debug string. That helper uses the same sensitive-key rules as SensitiveDataMasker but replaces entire values with the shared REDACTED marker (no partial sk-xxxx**** reveal), including non-string secrets. It recurses into nested dicts and list/tuple elements so keys like api_key, Authorization, and credentials inside extra_body / extra_headers are scrubbed while ordinary params (model, max_tokens, etc.) still print unchanged.

secret_redaction exports REDACTED (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.

`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.
@codspeed-hq

codspeed-hq Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Merging this PR will not alter performance

✅ 31 untouched benchmarks


Comparing litellm_mask_verbose_request_kwargs (0a62195) with litellm_internal_staging (ecabfbd)1

Open in CodSpeed

Footnotes

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

@greptile-apps

greptile-apps Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR hardens verbose request rendering by applying shared credential redaction before formatting keyword arguments.

  • Exposes the shared redaction marker for reuse.
  • Recursively redacts credential-named values in mappings, lists, and tuples.
  • Adds focused coverage for nested sequence payloads and ordinary parameter preservation.

Confidence Score: 5/5

The PR appears safe to merge.

The previously reported sequence-container gap is fixed, and no blocking failure remains.

Important Files Changed

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

Comment thread litellm/litellm_core_utils/sensitive_data_masker.py
Comment thread litellm/litellm_core_utils/sensitive_data_masker.py
@veria-ai

veria-ai Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

PR overview

All previously flagged issues have been addressed. No open security concerns remain on this pull request.

Security review

No open security issues remain on this pull request.

Fixed/addressed: 1 · PR risk: 0/10

@codecov

codecov Bot commented Sep 3, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 92.85714% with 2 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
...itellm/litellm_core_utils/sensitive_data_masker.py 90.47% 2 Missing ⚠️

📢 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.
@mateo-berri

Copy link
Copy Markdown
Contributor Author

@greptileai

@mateo-berri mateo-berri added run-ci and removed run-ci labels Sep 3, 2026
@mateo-berri

Copy link
Copy Markdown
Contributor Author

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 0a62195. Configure here.

@mateo-berri
mateo-berri merged commit aa9f3d9 into litellm_internal_staging Sep 3, 2026
124 of 127 checks passed
@mateo-berri
mateo-berri deleted the litellm_mask_verbose_request_kwargs branch September 3, 2026 21:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants