From ac7446bb76169f7b080a78265751ddb12f54b78d Mon Sep 17 00:00:00 2001 From: yuneng-jiang Date: Fri, 20 Feb 2026 18:13:27 -0800 Subject: [PATCH] fix: don't mask cost_per_token fields in SensitiveDataMasker Fields like input_cost_per_token contain "token" as a key segment, which incorrectly matched the sensitive patterns list and caused values like 3.6e-06 to be displayed as "3.60*******e-06" in the model info UI. Add a non_sensitive_overrides set (defaulting to {"cost"}) so that any key containing "cost" as a segment is never masked, regardless of other matching patterns. --- .../sensitive_data_masker.py | 12 ++++++++ .../test_sensitive_data_masker.py | 30 +++++++++++++++++++ 2 files changed, 42 insertions(+) diff --git a/litellm/litellm_core_utils/sensitive_data_masker.py b/litellm/litellm_core_utils/sensitive_data_masker.py index 8b6ae74463..3ec34e6d9e 100644 --- a/litellm/litellm_core_utils/sensitive_data_masker.py +++ b/litellm/litellm_core_utils/sensitive_data_masker.py @@ -8,6 +8,7 @@ class SensitiveDataMasker: def __init__( self, sensitive_patterns: Optional[Set[str]] = None, + non_sensitive_overrides: Optional[Set[str]] = None, visible_prefix: int = 4, visible_suffix: int = 4, mask_char: str = "*", @@ -26,6 +27,10 @@ class SensitiveDataMasker: "fingerprint", "tenancy", } + # If any key segment matches one of these, the key is not considered sensitive + # even if it also matches a sensitive pattern. For example, "input_cost_per_token" + # contains "token" but "cost" overrides that — it's a pricing field, not a secret. + self.non_sensitive_overrides = non_sensitive_overrides or {"cost"} self.visible_prefix = visible_prefix self.visible_suffix = visible_suffix @@ -56,6 +61,13 @@ class SensitiveDataMasker: # This avoids false positives like "max_tokens" matching "token" # but still catches "api_key", "access_token", etc. key_segments = key_lower.replace("-", "_").split("_") + + # If any segment matches a non-sensitive override, the key is not sensitive. + # For example, "input_cost_per_token" contains "token" but also "cost", + # so it should not be masked — it's a pricing field, not a secret. + if any(override in key_segments for override in self.non_sensitive_overrides): + return False + result = any(pattern in key_segments for pattern in self.sensitive_patterns) return result diff --git a/tests/test_litellm/litellm_core_utils/test_sensitive_data_masker.py b/tests/test_litellm/litellm_core_utils/test_sensitive_data_masker.py index b9f17aa4c7..5a731cbfa9 100644 --- a/tests/test_litellm/litellm_core_utils/test_sensitive_data_masker.py +++ b/tests/test_litellm/litellm_core_utils/test_sensitive_data_masker.py @@ -121,3 +121,33 @@ def test_lists_with_sensitive_keys_are_masked(): # non-sensitive list should remain unchanged assert masked["tags"] == ["prod", "test"] + + +def test_cost_per_token_fields_not_masked(): + """ + Regression test: cost fields like input_cost_per_token contain "token" in their name + but should NOT be masked — they are pricing fields, not secrets. + Previously, these would be displayed as e.g. "3.60*******e-06" in the UI. + """ + masker = SensitiveDataMasker() + data = { + "input_cost_per_token": 3.6e-06, + "output_cost_per_token": 1.2e-05, + "cache_read_input_token_cost": 9.0e-07, + "cache_creation_input_token_cost": 3.75e-06, + # Real secret fields should still be masked + "api_key": "sk-1234567890abcdef", + "access_token": "my-secret-token", + } + + masked = masker.mask_dict(data) + + # Cost fields must not be masked + assert masked["input_cost_per_token"] == 3.6e-06 + assert masked["output_cost_per_token"] == 1.2e-05 + assert masked["cache_read_input_token_cost"] == 9.0e-07 + assert masked["cache_creation_input_token_cost"] == 3.75e-06 + + # Actual secrets must still be masked + assert "*" in masked["api_key"] + assert "*" in masked["access_token"]