mirror of
https://github.com/BerriAI/litellm.git
synced 2026-08-28 05:25:59 +00:00
fix(spend-tracking): keep the master key alias readable in spend logs
Master-key auth stamps the stable alias litellm_proxy_master_key instead of the raw key, so spend logs carry a readable, non-secret identifier for those rows. The new redaction path only recognized sha256 and hashed-jwt shapes, so it hashed that alias and broke continuity with every master-key row written before this change. The alias joins the recognized non-secret values, still behind the same provenance gate, so a caller who sends the alias string as their own bearer token still gets it hashed.
This commit is contained in:
parent
c7b34da079
commit
a50590f324
2 changed files with 75 additions and 16 deletions
|
|
@ -10,6 +10,7 @@ from pydantic import BaseModel
|
|||
import litellm
|
||||
from litellm._logging import verbose_proxy_logger
|
||||
from litellm.constants import (
|
||||
LITELLM_PROXY_MASTER_KEY_ALIAS,
|
||||
LITELLM_TRUNCATED_PAYLOAD_FIELD,
|
||||
LITELLM_TRUNCATION_DB_SAFEGUARD_NOTE,
|
||||
REDACTED_BY_LITELM_STRING,
|
||||
|
|
@ -67,17 +68,21 @@ def _is_master_key(api_key: str | None, _master_key: str | None) -> bool:
|
|||
_HASHED_JWT_RE = re.compile(r"hashed-jwt-[a-fA-F0-9]{64}")
|
||||
|
||||
|
||||
def _is_prehashed_key_shape(value: str) -> bool:
|
||||
return is_valid_sha256_hash(value) or _HASHED_JWT_RE.fullmatch(value) is not None
|
||||
def _is_non_secret_key_value(value: str) -> bool:
|
||||
return (
|
||||
value == LITELLM_PROXY_MASTER_KEY_ALIAS
|
||||
or is_valid_sha256_hash(value)
|
||||
or _HASHED_JWT_RE.fullmatch(value) is not None
|
||||
)
|
||||
|
||||
|
||||
def _redact_logged_api_key(value: str | None, *, already_hashed: bool = False) -> str | None:
|
||||
def _redact_logged_api_key(value: str | None, *, already_redacted: bool = False) -> str | None:
|
||||
if not isinstance(value, str) or not value:
|
||||
return None
|
||||
stripped: Final = re.sub(r"(?i)^bearer ", "", value)
|
||||
if not stripped:
|
||||
return None
|
||||
if already_hashed and _is_prehashed_key_shape(stripped):
|
||||
if already_redacted and _is_non_secret_key_value(stripped):
|
||||
return stripped
|
||||
return hash_token(stripped)
|
||||
|
||||
|
|
@ -137,10 +142,10 @@ def _get_spend_logs_metadata(
|
|||
clean_metadata: Final = SpendLogsMetadata(**{key: metadata.get(key) for key in SpendLogsMetadata.__annotations__})
|
||||
_raw_key: Final = clean_metadata.get("user_api_key")
|
||||
_trusted_hash: Final = metadata.get("user_api_key_hash")
|
||||
_already_hashed: Final = (
|
||||
isinstance(_trusted_hash, str) and _is_prehashed_key_shape(_trusted_hash) and _trusted_hash == _raw_key
|
||||
_already_redacted: Final = (
|
||||
isinstance(_trusted_hash, str) and _is_non_secret_key_value(_trusted_hash) and _trusted_hash == _raw_key
|
||||
)
|
||||
clean_metadata["user_api_key"] = _redact_logged_api_key(_raw_key, already_hashed=_already_hashed)
|
||||
clean_metadata["user_api_key"] = _redact_logged_api_key(_raw_key, already_redacted=_already_redacted)
|
||||
clean_metadata["applied_guardrails"] = applied_guardrails
|
||||
clean_metadata["batch_models"] = batch_models
|
||||
clean_metadata["mcp_tool_call_metadata"] = mcp_tool_call_metadata
|
||||
|
|
@ -297,10 +302,10 @@ def get_logging_payload(kwargs, response_obj, start_time, end_time) -> SpendLogs
|
|||
standard_logging_completion_tokens = standard_logging_payload.get("completion_tokens", 0)
|
||||
standard_logging_total_tokens = standard_logging_payload.get("total_tokens", 0)
|
||||
_trusted_hash = metadata.get("user_api_key_hash")
|
||||
_key_already_hashed = (
|
||||
isinstance(_trusted_hash, str) and _is_prehashed_key_shape(_trusted_hash) and _trusted_hash == api_key
|
||||
_key_already_redacted = (
|
||||
isinstance(_trusted_hash, str) and _is_non_secret_key_value(_trusted_hash) and _trusted_hash == api_key
|
||||
)
|
||||
api_key = _redact_logged_api_key(api_key, already_hashed=_key_already_hashed) or ""
|
||||
api_key = _redact_logged_api_key(api_key, already_redacted=_key_already_redacted) or ""
|
||||
|
||||
if (
|
||||
standard_logging_payload is not None
|
||||
|
|
@ -308,7 +313,7 @@ def get_logging_payload(kwargs, response_obj, start_time, end_time) -> SpendLogs
|
|||
api_key = (
|
||||
api_key
|
||||
or _redact_logged_api_key(
|
||||
standard_logging_payload["metadata"].get("user_api_key_hash"), already_hashed=True
|
||||
standard_logging_payload["metadata"].get("user_api_key_hash"), already_redacted=True
|
||||
)
|
||||
or ""
|
||||
)
|
||||
|
|
|
|||
|
|
@ -2625,7 +2625,7 @@ def test_redact_logged_api_key_non_sk_raw_key_is_hashed():
|
|||
def test_redact_logged_api_key_already_valid_sha256_passes_through_with_flag():
|
||||
already_hashed = hash_token("sk-some-key")
|
||||
assert len(already_hashed) == 64
|
||||
result = _redact_logged_api_key(already_hashed, already_hashed=True)
|
||||
result = _redact_logged_api_key(already_hashed, already_redacted=True)
|
||||
assert result == already_hashed
|
||||
assert hash_token(already_hashed) != result # no double-hash
|
||||
|
||||
|
|
@ -2653,7 +2653,7 @@ def test_redact_logged_api_key_long_opaque_token_is_hashed():
|
|||
|
||||
def test_redact_logged_api_key_hashed_jwt_passes_through():
|
||||
jwt_hash = "hashed-jwt-" + "a" * 64
|
||||
result = _redact_logged_api_key(jwt_hash, already_hashed=True)
|
||||
result = _redact_logged_api_key(jwt_hash, already_redacted=True)
|
||||
assert result == jwt_hash
|
||||
|
||||
|
||||
|
|
@ -2666,7 +2666,7 @@ def test_redact_logged_api_key_hashed_jwt_shape_without_provenance_is_hashed():
|
|||
|
||||
def test_redact_logged_api_key_hashed_jwt_trailing_newline_is_hashed():
|
||||
trailing = "hashed-jwt-" + "a" * 64 + "\n"
|
||||
result = _redact_logged_api_key(trailing, already_hashed=True)
|
||||
result = _redact_logged_api_key(trailing, already_redacted=True)
|
||||
assert result == hash_token(trailing)
|
||||
assert result != trailing
|
||||
|
||||
|
|
@ -2680,6 +2680,33 @@ def test_redact_logged_api_key_hashed_jwt_short_suffix_is_hashed():
|
|||
assert result == hash_token(short_jwt)
|
||||
|
||||
|
||||
def test_redact_logged_api_key_master_key_alias_passes_through():
|
||||
from litellm.constants import LITELLM_PROXY_MASTER_KEY_ALIAS
|
||||
|
||||
result = _redact_logged_api_key(LITELLM_PROXY_MASTER_KEY_ALIAS, already_redacted=True)
|
||||
assert result == LITELLM_PROXY_MASTER_KEY_ALIAS
|
||||
|
||||
|
||||
def test_redact_logged_api_key_master_key_alias_without_provenance_is_hashed():
|
||||
from litellm.constants import LITELLM_PROXY_MASTER_KEY_ALIAS
|
||||
|
||||
result = _redact_logged_api_key(LITELLM_PROXY_MASTER_KEY_ALIAS)
|
||||
assert result == hash_token(LITELLM_PROXY_MASTER_KEY_ALIAS)
|
||||
assert result != LITELLM_PROXY_MASTER_KEY_ALIAS
|
||||
|
||||
|
||||
def test_get_spend_logs_metadata_keeps_master_key_alias_readable():
|
||||
from litellm.constants import LITELLM_PROXY_MASTER_KEY_ALIAS
|
||||
|
||||
meta = _get_spend_logs_metadata(
|
||||
{
|
||||
"user_api_key": LITELLM_PROXY_MASTER_KEY_ALIAS,
|
||||
"user_api_key_hash": LITELLM_PROXY_MASTER_KEY_ALIAS,
|
||||
}
|
||||
)
|
||||
assert meta["user_api_key"] == LITELLM_PROXY_MASTER_KEY_ALIAS
|
||||
|
||||
|
||||
def test_redact_logged_api_key_bearer_only_returns_none():
|
||||
# "bearer " with nothing after stripping is equivalent to no key
|
||||
assert _redact_logged_api_key("bearer ") is None
|
||||
|
|
@ -2948,7 +2975,7 @@ class TestSpendLogKeyRedaction:
|
|||
|
||||
def test_already_hashed_key_unchanged(self):
|
||||
hashed = "bcfe8173f5447f10be0e7fb37aaa8b97829d5c9e0498232152f9d123456789ab"
|
||||
assert _redact_logged_api_key(hashed, already_hashed=True) == hashed
|
||||
assert _redact_logged_api_key(hashed, already_redacted=True) == hashed
|
||||
|
||||
def test_bearer_prefixed_non_sk_key_is_hashed(self):
|
||||
raw = "Bearer some-other-token-format"
|
||||
|
|
@ -2995,6 +3022,33 @@ def test_get_logging_payload_non_sk_raw_key_both_fields_hashed():
|
|||
assert len(parsed_meta["user_api_key"]) == 64
|
||||
|
||||
|
||||
def test_get_logging_payload_keeps_master_key_alias_readable():
|
||||
from litellm.constants import LITELLM_PROXY_MASTER_KEY_ALIAS
|
||||
|
||||
kwargs = {
|
||||
"model": "openai/gpt-4.1",
|
||||
"messages": [{"role": "user", "content": "Hello"}],
|
||||
"call_type": "acompletion",
|
||||
"litellm_params": {
|
||||
"metadata": {
|
||||
"user_api_key": LITELLM_PROXY_MASTER_KEY_ALIAS,
|
||||
"user_api_key_hash": LITELLM_PROXY_MASTER_KEY_ALIAS,
|
||||
"user_api_key_user_id": "test_user",
|
||||
}
|
||||
},
|
||||
}
|
||||
payload = get_logging_payload(
|
||||
kwargs=kwargs,
|
||||
response_obj=Exception("error"),
|
||||
start_time=datetime.datetime.now(timezone.utc),
|
||||
end_time=datetime.datetime.now(timezone.utc),
|
||||
)
|
||||
|
||||
assert payload["api_key"] == LITELLM_PROXY_MASTER_KEY_ALIAS
|
||||
parsed_meta = json.loads(payload["metadata"])
|
||||
assert parsed_meta["user_api_key"] == LITELLM_PROXY_MASTER_KEY_ALIAS
|
||||
|
||||
|
||||
@patch("litellm.proxy.proxy_server.master_key", None)
|
||||
@patch("litellm.proxy.proxy_server.general_settings", {})
|
||||
def test_get_logging_payload_hashes_bearer_prefixed_api_key():
|
||||
|
|
@ -3503,7 +3557,7 @@ def test_redact_logged_api_key_partial_sha256_is_hashed():
|
|||
def test_redact_logged_api_key_bearer_already_hashed_passes_through_with_flag():
|
||||
already_hashed = hash_token("sk-some-key")
|
||||
assert len(already_hashed) == 64
|
||||
result = _redact_logged_api_key(f"Bearer {already_hashed}", already_hashed=True)
|
||||
result = _redact_logged_api_key(f"Bearer {already_hashed}", already_redacted=True)
|
||||
assert result == already_hashed
|
||||
assert hash_token(already_hashed) != result
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue