Skip to content

fix(proxy): sanitize per-key callback config out of logged metadata - #32583

Merged
yuneng-berri merged 1 commit into
litellm_internal_stagingfrom
litellm_/redact-langsmith-api-key-c92cc3
Jul 27, 2026
Merged

fix(proxy): sanitize per-key callback config out of logged metadata#32583
yuneng-berri merged 1 commit into
litellm_internal_stagingfrom
litellm_/redact-langsmith-api-key-c92cc3

Conversation

@yuneng-berri

@yuneng-berri yuneng-berri commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

TLDR

Problem this solves:

  • redact_user_api_key_info never applied to LangSmith run inputs
  • per-key callback config rode along in every logger's metadata
  • three call sites stamped raw key metadata into request metadata

How it solves it:

  • sanitize key/team metadata at those three sites
  • one redaction helper shared by LangSmith inputs and extra

Relevant issues

Linear ticket

Resolves LIT-4306

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

Verified against a live proxy making real Anthropic claude-sonnet-5 calls, with litellm_settings.redact_user_api_key_info: true and a per-key LangSmith callback whose langsmith_base_url points at a local sink that records the exact /runs/batch body. Before = b9b27c2beb (the parent commit), after = 5e34e0460b (this PR). The chat completion response is identical either way; the observable difference is what LiteLLM POSTs to LangSmith. Values below are placeholders

Setup, identical for both runs. Create a team and a key whose metadata carries per-key LangSmith callback config, matching the reported shape

TEAM=$(curl -s http://localhost:4001/team/new \
  -H "Authorization: Bearer sk-1234" -H 'Content-Type: application/json' \
  -d '{"team_alias":"repro-team","metadata":{"logging":[{"callback_name":"langsmith","callback_type":"success_and_failure","callback_vars":{"langsmith_api_key":"lsv2_sk_TEAMLEVEL_SECRET","langsmith_project":"team-proj","langsmith_base_url":"http://127.0.0.1:4002"}}]}}' | jq -r .team_id)

KEY=$(curl -s http://localhost:4001/key/generate \
  -H "Authorization: Bearer sk-1234" -H 'Content-Type: application/json' \
  -d "{\"models\":[\"claude-sonnet-5\"],\"team_id\":\"$TEAM\",\"metadata\":{\"logging\":[{\"callback_name\":\"langsmith\",\"callback_type\":\"success_and_failure\",\"callback_vars\":{\"langsmith_api_key\":\"lsv2_sk_KEYLEVEL_SECRET\",\"langsmith_project\":\"key-proj\",\"langsmith_base_url\":\"http://127.0.0.1:4002\"}}],\"langsmith_provisioning\":{\"api_key_id\":\"prov-uuid-1\",\"api_key_short\":\"lsv2_sk_a365...33b2\"},\"priority\":\"high\"}}" | jq -r .key)

Chat with that key. HTTP 200 in both runs, response unchanged by this PR

curl -s http://localhost:4001/v1/chat/completions \
  -H "Authorization: Bearer $KEY" -H 'Content-Type: application/json' \
  -d '{"model":"claude-sonnet-5","messages":[{"role":"user","content":"say hi in one word"}],"max_tokens":16}'
{"id":"chatcmpl-...","object":"chat.completion","model":"claude-sonnet-5","choices":[{"index":0,"message":{"role":"assistant","content":"Hi!"}}],"usage":{...}}

Before, on b9b27c2beb. Every user_api_key_* field ships under inputs.metadata even though the flag is on, and extra is already clean, so the two disagree

jq -r '.body.post[0] | "inputs user_api_key_* : \([.inputs.metadata|keys[]|select(startswith("user_api_key"))]|length)",
                        "extra  user_api_key_* : \([.extra|keys[]|select(startswith("user_api_key"))]|length)"' sink-capture.jsonl
inputs user_api_key_* : 20
extra  user_api_key_* : 0
jq -c '.body.post[0].inputs.metadata.user_api_key_auth_metadata' sink-capture.jsonl
{"logging":[{"callback_name":"langsmith","callback_type":"success_and_failure","callback_vars":{"langsmith_api_key":"litellm_enc::pt71BYmrVlJk...","langsmith_project":"key-proj","langsmith_base_url":"http://127.0.0.1:4002"}}],"priority":"high","langsmith_provisioning":{"api_key_id":"prov-uuid-1","api_key_short":"lsv2_sk_a365...33b2"}}

After, on 5e34e0460b. inputs now agrees with extra, and the key's callback config is gone from what is logged

inputs user_api_key_* : 0
extra  user_api_key_* : 0
jq -c '.body.post[0].inputs.metadata | {team_id, team_alias, model: .model, requester_metadata}' sink-capture.jsonl
{"team_id":"d842a11f-b749-4722-9227-0d8b1a1c5536","team_alias":"repro-team","requester_metadata":{}}

The second half is independent of the flag. Re-running with redact_user_api_key_info: false, the identity fields stay (that is what the flag governs) but the callback config no longer reaches the logged payload, and priority, which the dynamic rate limiter reads back off this exact field, is preserved

jq -c '.body.post[0].inputs.metadata.user_api_key_auth_metadata' sink-capture.jsonl
{"priority":"high","langsmith_provisioning":{"api_key_id":"prov-uuid-1","api_key_short":"lsv2_sk_a365...33b2"}}

A key with no per-key logging config, and the team-level callback, both still log and still return 200 on the after run

Type

🐛 Bug Fix

Changes

LiteLLMProxyRequestSetup.get_sanitized_user_information_from_key copied UserAPIKeyAuth.metadata verbatim into user_api_key_auth_metadata, so the key's callback configuration, including the values inside callback_vars, reached the StandardLoggingPayload that every integration receives. add_litellm_data_to_request and async_queue_request stamped the same raw metadata into user_api_key_metadata and user_api_key_team_metadata. The existing scrub in scrub_sensitive_keys_in_metadata only matched the literal key logging under one of the two field names and never covered callback_settings, so it missed the field that actually reaches the payload

This adds strip_callback_config next to the callback_vars traversal that already lives in common_utils/callback_utils.py, and routes all three sites through it. It drops the logging and callback_settings slots and leaves everything else untouched, so priority for the dynamic rate limiter, guardrails for the guardrail hooks, and the Arize/Phoenix project overrides all keep working. Those two slots are resolved from UserAPIKeyAuth during pre-call setup and are never read back off the logged copies, so nothing downstream loses input. litellm/proxy/utils.py builds its payload through the same helper on the proxy-error path and inherits this. With the source sanitized the old scrub is dead, so it is removed

Separately, LangSmith set the run's inputs to the raw StandardLoggingPayload while redacting only extra. redact_user_api_key_info therefore left the whole user_api_key_* family in inputs.metadata, and the nested requester_metadata handling that extra had was not applied there either. Both now go through one _redact_metadata helper, which is what keeps them from drifting apart again

Tests cover both halves and fail on the parent commit: strip_callback_config drops the two slots without mutating the caller's dict, get_sanitized_user_information_from_key keeps priority while dropping the callback config, and _prepare_log_data with the flag on leaves no user_api_key_* field in either inputs.metadata or its nested requester_metadata while leaving the shared standard_logging_object unmutated

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

@greptile-apps

greptile-apps Bot commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR sanitizes per-key and per-team callback configuration (the logging and callback_settings metadata slots that carry integration credentials) out of the three call sites that stamp raw UserAPIKeyAuth metadata into request metadata, and separately fixes LangSmith's inputs payload which previously shipped the raw StandardLoggingPayload while extra was already redacted.

  • Adds strip_callback_config in callback_utils.py and applies it at get_sanitized_user_information_from_key, add_litellm_data_to_request, and async_queue_request; removes the now-superseded partial scrub in scrub_sensitive_keys_in_metadata.
  • Extracts _redact_metadata as a shared helper in LangsmithLogger and uses it for both extra and the new inputs construction, so both fields go through identical redaction (including nested requester_metadata) when redact_user_api_key_info is set.
  • Tests cover strip_callback_config immutability, get_sanitized_user_information_from_key preservation of priority, and the LangSmith redact-on/off paths including mutation-safety of the shared standard_logging_object.

Confidence Score: 5/5

This PR is safe to merge — it fixes a data-leakage issue in logging paths without touching request routing or auth logic, and all changes are covered by targeted unit tests.

All three call sites that stamp raw key/team metadata into request metadata are correctly updated. The old partial scrub is removed only after the upstream fix covers more fields. The LangSmith inputs/extra divergence is closed by a shared helper. Tests verify non-mutation of the live auth object and preservation of fields consumed by downstream features (rate limiter, guardrails).

Files Needing Attention: No files require special attention.

Important Files Changed

Filename Overview
litellm/proxy/common_utils/callback_utils.py Adds strip_callback_config which returns a new dict without logging/callback_settings slots, leaving priority, guardrails, and other application fields intact.
litellm/integrations/langsmith.py Extracts _redact_metadata helper shared by _build_extra_metadata and the new inputs construction; inputs now carries a redacted copy of payload.metadata instead of the raw payload.
litellm/proxy/litellm_pre_call_utils.py Routes user_api_key_auth_metadata, user_api_key_metadata, and user_api_key_team_metadata through strip_callback_config at the two main call sites that build request metadata.
litellm/proxy/proxy_server.py Applies strip_callback_config to user_api_key_metadata in async_queue_request, the third call site that stamps raw key metadata into request metadata.
litellm/litellm_core_utils/litellm_logging.py Removes the legacy scrub_sensitive_keys_in_metadata block that only scrubbed logging under user_api_key_metadata; now fully superseded by the upstream strip_callback_config calls.
tests/test_litellm/integrations/test_langsmith_init.py Adds two new tests covering the redact-enabled/disabled paths for inputs.metadata, including mutation-safety of the shared standard_logging_object. No real network calls.
tests/test_litellm/proxy/common_utils/test_callback_utils.py Adds unit tests for strip_callback_config: verifies credential-bearing slots are dropped, other fields survive, original dict is not mutated, and non-dict values pass through unchanged.
tests/test_litellm/proxy/test_litellm_pre_call_utils.py Adds a regression test for get_sanitized_user_information_from_key confirming callback config is stripped while priority survives and the source UserAPIKeyAuth is not mutated.

Reviews (2): Last reviewed commit: "fix(proxy): sanitize per-key callback co..." | Re-trigger Greptile

@codecov

codecov Bot commented Jul 9, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@codspeed-hq

codspeed-hq Bot commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Merging this PR will not alter performance

✅ 31 untouched benchmarks


Comparing litellm_/redact-langsmith-api-key-c92cc3 (5e34e04) with litellm_internal_staging (b9b27c2)

Open in CodSpeed

get_sanitized_user_information_from_key copied UserAPIKeyAuth.metadata
verbatim into user_api_key_auth_metadata, so the key's callback
configuration - including the integration credentials inside callback_vars -
reached the StandardLoggingPayload every integration receives. The two other
sites that stamp key/team metadata into request metadata did the same.
Sanitize at those sources with strip_callback_config, which drops the
`logging` and `callback_settings` slots and leaves everything else (notably
`priority`, read back by the dynamic rate limiter) untouched. Those slots are
resolved from UserAPIKeyAuth during pre-call setup and never read off the
logged copies, so nothing downstream loses input.

This makes the scrub in scrub_sensitive_keys_in_metadata dead - it only
matched the string "logging" under one of the two field names and never
covered callback_settings - so it is removed.

Separately, LangSmith set the run's `inputs` to the raw StandardLoggingPayload
while redacting only `extra`, so redact_user_api_key_info left every
user_api_key_* field in inputs.metadata. Both now go through one
_redact_metadata helper, which also covers the nested requester_metadata copy.
@yuneng-berri
yuneng-berri force-pushed the litellm_/redact-langsmith-api-key-c92cc3 branch from 038b791 to 5e34e04 Compare July 25, 2026 05:20
@yuneng-berri yuneng-berri changed the title fix(langsmith): redact user_api_key_auth_metadata from run inputs, not only extra fix(proxy): sanitize per-key callback config out of logged metadata Jul 25, 2026
@yuneng-berri

Copy link
Copy Markdown
Contributor Author

@grepile

@yuneng-berri

Copy link
Copy Markdown
Contributor Author

@greptile

@devin-ai-integration

Copy link
Copy Markdown
Contributor

QA of this PR

Independent QA against a live proxy on a local Postgres, hitting the real Anthropic API with claude-sonnet-5. litellm_settings.redact_user_api_key_info was toggled between runs, and the per-key LangSmith callback pointed langsmith_base_url at a local sink on 127.0.0.1:4002 that records the exact /runs/batch body LiteLLM sends. Before is b9b27c2beb (parent), after is 5e34e0460b (this PR); each run used its own checkout so the two builds could not shadow each other

Setup for every run: a team whose metadata carries a team-level LangSmith callback, a key on that team whose metadata carries a key-level LangSmith callback plus priority and langsmith_provisioning, and a second key on the same team with no per-key logging config so the team-level callback is exercised too

TEAM=$(curl -s http://localhost:4001/team/new \
  -H "Authorization: Bearer sk-1234" -H 'Content-Type: application/json' \
  -d '{"team_alias":"qa-team","metadata":{"logging":[{"callback_name":"langsmith","callback_type":"success_and_failure","callback_vars":{"langsmith_api_key":"lsv2_sk_TEAMLEVEL_SECRET","langsmith_project":"team-proj","langsmith_base_url":"http://127.0.0.1:4002"}}]}}' | jq -r .team_id)

KEY=$(curl -s http://localhost:4001/key/generate \
  -H "Authorization: Bearer sk-1234" -H 'Content-Type: application/json' \
  -d "{\"models\":[\"claude-sonnet-5\"],\"team_id\":\"$TEAM\",\"metadata\":{\"logging\":[{\"callback_name\":\"langsmith\",\"callback_type\":\"success_and_failure\",\"callback_vars\":{\"langsmith_api_key\":\"lsv2_sk_KEYLEVEL_SECRET\",\"langsmith_project\":\"key-proj\",\"langsmith_base_url\":\"http://127.0.0.1:4002\"}}],\"langsmith_provisioning\":{\"api_key_id\":\"prov-uuid-1\",\"api_key_short\":\"lsv2_sk_a365...33b2\"},\"priority\":\"high\"}}" | jq -r .key)

PLAINKEY=$(curl -s http://localhost:4001/key/generate \
  -H "Authorization: Bearer sk-1234" -H 'Content-Type: application/json' \
  -d "{\"models\":[\"claude-sonnet-5\"],\"team_id\":\"$TEAM\"}" | jq -r .key)

curl -s http://localhost:4001/v1/chat/completions \
  -H "Authorization: Bearer $KEY" -H 'Content-Type: application/json' \
  -d '{"model":"claude-sonnet-5","messages":[{"role":"user","content":"say hi in one word"}],"max_tokens":16}' \
  | jq -c '{model, content: .choices[0].message.content, prompt_tokens: .usage.prompt_tokens}'
{"model":"claude-sonnet-5","content":"Hi!","prompt_tokens":12}

The chat responses are HTTP 200 and identical on both builds, and both keys keep logging (the run for KEY lands in key-proj, the run for PLAINKEY lands in team-proj), so the per-key and team-level callback resolution is untouched. What changes is the body LiteLLM POSTs to LangSmith

jq -r '.body.post[] | "project=\(.session_name) inputs_user_api_key_fields=\([.inputs.metadata|keys[]|select(startswith("user_api_key"))]|length) extra_user_api_key_fields=\([.extra|keys[]|select(startswith("user_api_key"))]|length) callback_config_logged=\((.inputs.metadata.user_api_key_auth_metadata // {})|has("logging"))"' sink-capture.jsonl
run inputs.metadata user_api_key_* extra user_api_key_* callback config in logged metadata
before, redact_user_api_key_info: true 20 0 yes
after, redact_user_api_key_info: true 0 0 no
before, redact_user_api_key_info: false 20 20 yes
after, redact_user_api_key_info: false 20 20 no

So both halves of the fix reproduce. With the flag on, inputs used to disagree with extra and shipped the whole user_api_key_* family; it now agrees. Independently of the flag, the key's callback configuration no longer reaches the payload

jq -c '.body.post[0].inputs.metadata.user_api_key_auth_metadata' sink-capture.jsonl

before, flag off

{"logging":[{"callback_name":"langsmith","callback_type":"success_and_failure","callback_vars":{"langsmith_api_key":"litellm_enc::bb11vqvWPXqRsZmL2_VCJ...","langsmith_project":"key-proj","langsmith_base_url":"http://127.0.0.1:4002"}}],"priority":"high","langsmith_provisioning":{"api_key_id":"prov-uuid-1","api_key_short":"lsv2_sk_a365...33b2"}}

after, flag off

{"priority":"high","langsmith_provisioning":{"api_key_id":"prov-uuid-1","api_key_short":"lsv2_sk_a365...33b2"}}

A full recursive scan of the after-run captures finds no lsv2_sk_* or litellm_enc::* value anywhere in the run body; the only occurrence left is the x-api-key request header the logger uses to authenticate to LangSmith itself, which is expected

priority survives, which is what dynamic_rate_limiter_v3 reads back off user_api_key_auth_metadata, and the other slots that live next to logging in key metadata still work. Rate limits configured through key metadata are still enforced on the after build

K=$(curl -s http://localhost:4001/key/generate -H "Authorization: Bearer sk-1234" -H 'Content-Type: application/json' \
  -d '{"models":["claude-sonnet-5"],"metadata":{"model_rpm_limit":{"claude-sonnet-5":1},"priority":"high"}}' | jq -r .key)
for i in 1 2 3; do curl -s -o /tmp/r$i.json -w "req$i http=%{http_code}\n" http://localhost:4001/v1/chat/completions \
  -H "Authorization: Bearer $K" -H 'Content-Type: application/json' \
  -d '{"model":"claude-sonnet-5","messages":[{"role":"user","content":"hi"}],"max_tokens":8}'; done
req1 http=200
req2 http=429   Rate limit exceeded for model_per_key
req3 http=429   Rate limit exceeded for model_per_key

On the test side, the three touched files pass on this branch (238 tests), and the two new behavioral tests fail when only litellm/ is reverted to the parent, so they are real regression tests rather than coverage filler: test_get_sanitized_user_information_from_key_drops_callback_config and TestLangsmithRedactUserApiKeyInfo::test_redact_enabled_strips_user_api_key_info_from_inputs

CI is green apart from osv-scan, which fails on gitpython 3.1.52, brace-expansion 5.0.7 and postcss 8.5.13 advisories coming from uv.lock and ui/litellm-dashboard/package-lock.json. This PR changes no lockfile, and litellm_internal_staging pins the same gitpython 3.1.52, so that failure is pre-existing and unrelated

One thing outside the scope of this PR, worth a follow-up rather than a change here: litellm/proxy/search_endpoints/endpoints.py still stamps user_api_key_team_metadata from UserAPIKeyAuth verbatim, so the /search path is a fourth site that does not go through strip_callback_config. It does not reach the StandardLoggingPayload (the field is not part of StandardLoggingMetadata), so it is not the leak this PR is about, but it is the same shape of raw copy

QA verdict: passes

@yuneng-berri
yuneng-berri merged commit 38ea85b into litellm_internal_staging Jul 27, 2026
83 of 85 checks passed
@yuneng-berri
yuneng-berri deleted the litellm_/redact-langsmith-api-key-c92cc3 branch July 27, 2026 22:25
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