mirror of
https://github.com/BerriAI/litellm.git
synced 2026-08-28 05:25:59 +00:00
Merge pull request #30736 from nitishagar/litellm_fix_raw_key_log_persistence
fix(spend-tracking): hash raw api keys before persisting to spend logs
This commit is contained in:
commit
ff02d5cfc0
3 changed files with 400 additions and 41 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,
|
||||
|
|
@ -21,6 +22,7 @@ from litellm.litellm_core_utils.core_helpers import (
|
|||
get_litellm_metadata_from_kwargs,
|
||||
reconstruct_model_name,
|
||||
)
|
||||
from litellm.litellm_core_utils.litellm_logging import is_valid_sha256_hash
|
||||
from litellm.litellm_core_utils.safe_json_dumps import safe_dumps, strip_null_bytes
|
||||
from litellm.proxy._types import SpendLogsMetadata, SpendLogsPayload
|
||||
from litellm.proxy.spend_tracking.spend_log_error_logger import spend_log_error
|
||||
|
|
@ -53,13 +55,6 @@ def _get_max_string_length_prompt_in_db() -> int:
|
|||
return DEFAULT_MAX_STRING_LENGTH_PROMPT_IN_DB
|
||||
|
||||
|
||||
def _hash_api_key_for_spend_log(api_key: str) -> str:
|
||||
stripped: Final = api_key[7:] if api_key[:7].lower() == "bearer " else api_key
|
||||
if stripped.startswith("sk-"):
|
||||
return hash_token(stripped)
|
||||
return stripped
|
||||
|
||||
|
||||
def _is_master_key(api_key: str | None, _master_key: str | None) -> bool:
|
||||
"""
|
||||
Raw-only constant-time master-key comparison. The hashed form is never
|
||||
|
|
@ -70,6 +65,28 @@ def _is_master_key(api_key: str | None, _master_key: str | None) -> bool:
|
|||
return secrets.compare_digest(api_key, _master_key)
|
||||
|
||||
|
||||
_HASHED_JWT_RE = re.compile(r"hashed-jwt-[a-fA-F0-9]{64}")
|
||||
|
||||
|
||||
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_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_redacted and _is_non_secret_key_value(stripped):
|
||||
return stripped
|
||||
return hash_token(stripped)
|
||||
|
||||
|
||||
def _get_spend_logs_metadata(
|
||||
metadata: dict | None,
|
||||
applied_guardrails: list[str] | None = None,
|
||||
|
|
@ -123,9 +140,12 @@ def _get_spend_logs_metadata(
|
|||
|
||||
# Filter the metadata dictionary to include only the specified keys
|
||||
clean_metadata: Final = SpendLogsMetadata(**{key: metadata.get(key) for key in SpendLogsMetadata.__annotations__})
|
||||
raw_user_api_key: Final = clean_metadata.get("user_api_key")
|
||||
if raw_user_api_key is not None and isinstance(raw_user_api_key, str):
|
||||
clean_metadata["user_api_key"] = _hash_api_key_for_spend_log(raw_user_api_key)
|
||||
_raw_key: Final = clean_metadata.get("user_api_key")
|
||||
_trusted_hash: Final = metadata.get("user_api_key_hash")
|
||||
_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_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
|
||||
|
|
@ -281,16 +301,23 @@ def get_logging_payload(kwargs, response_obj, start_time, end_time) -> SpendLogs
|
|||
standard_logging_prompt_tokens = standard_logging_payload.get("prompt_tokens", 0)
|
||||
standard_logging_completion_tokens = standard_logging_payload.get("completion_tokens", 0)
|
||||
standard_logging_total_tokens = standard_logging_payload.get("total_tokens", 0)
|
||||
if api_key is not None and isinstance(api_key, str):
|
||||
api_key = _hash_api_key_for_spend_log(api_key)
|
||||
_trusted_hash = metadata.get("user_api_key_hash")
|
||||
_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_redacted=_key_already_redacted) or ""
|
||||
|
||||
if (
|
||||
standard_logging_payload is not None
|
||||
): # [TODO] migrate completely to sl payload. currently missing pass-through endpoint data
|
||||
api_key = api_key or standard_logging_payload["metadata"].get("user_api_key_hash") or ""
|
||||
api_key = (
|
||||
api_key
|
||||
or _redact_logged_api_key(
|
||||
standard_logging_payload["metadata"].get("user_api_key_hash"), already_redacted=True
|
||||
)
|
||||
or ""
|
||||
)
|
||||
end_user_id = end_user_id or standard_logging_payload["metadata"].get("user_api_key_end_user_id")
|
||||
# BUG FIX: Don't overwrite api_key when standard_logging_payload is None
|
||||
# The api_key was already extracted from metadata (line 243) and hashed (lines 256-259)
|
||||
request_tags = safe_dumps(metadata.get("tags", [])) if isinstance(metadata.get("tags", []), list) else "[]"
|
||||
if (
|
||||
standard_logging_payload is not None and standard_logging_payload.get("request_tags") is not None
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ from prisma.errors import ClientNotConnectedError
|
|||
_PROXY_MODULE_GLOBALS_TO_ISOLATE = (
|
||||
"master_key",
|
||||
"prisma_client",
|
||||
"llm_router",
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -56,7 +57,10 @@ def pytest_runtest_setup(item):
|
|||
|
||||
Without this, a leaked value (e.g. master_key set by a sibling test)
|
||||
flips the auth short-circuit in user_api_key_auth and causes unrelated
|
||||
tests in the same xdist worker to return 401 instead of 200.
|
||||
tests in the same xdist worker to return 401 instead of 200. A leaked
|
||||
llm_router does the same to anything that reads the running router out
|
||||
of sys.modules, such as the PTU rollup's deployment scan, which then
|
||||
counts a sibling test's deployments as if the proxy owned them.
|
||||
|
||||
This must be a hook pair, not an autouse fixture: an autouse fixture in
|
||||
the root conftest requests monkeypatch, so monkeypatch's undo stack
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ from datetime import timezone
|
|||
from typing import Any, Final, cast
|
||||
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
sys.path.insert(
|
||||
0, os.path.abspath("../../../..")
|
||||
|
|
@ -29,8 +28,8 @@ from litellm.proxy.spend_tracking.spend_tracking_utils import (
|
|||
_get_response_for_spend_logs_payload,
|
||||
_get_spend_logs_metadata,
|
||||
_get_vector_store_request_for_spend_logs_payload,
|
||||
_hash_api_key_for_spend_log,
|
||||
_is_master_key,
|
||||
_redact_logged_api_key,
|
||||
_redact_prompt_leaks_in_error_string,
|
||||
_sanitize_error_information_for_spend_logs,
|
||||
_sanitize_guardrail_information_for_spend_logs,
|
||||
|
|
@ -39,6 +38,7 @@ from litellm.proxy.spend_tracking.spend_tracking_utils import (
|
|||
get_logging_payload,
|
||||
get_spend_logs_id,
|
||||
)
|
||||
from litellm.proxy.utils import hash_token
|
||||
from litellm.types.utils import (
|
||||
StandardLoggingHiddenParams,
|
||||
StandardLoggingMetadata,
|
||||
|
|
@ -888,8 +888,6 @@ def test_get_logging_payload_api_key_preserved_when_standard_logging_payload_is_
|
|||
assert payload["model"] == "openai/gpt-4.1"
|
||||
assert payload["user"] == "test_user"
|
||||
|
||||
print(f"✅ Test passed! api_key preserved: {payload['api_key']}")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@patch("litellm.proxy.proxy_server.master_key", "sk-master-key")
|
||||
|
|
@ -1037,18 +1035,6 @@ async def test_api_key_preserved_through_failure_hook_to_database():
|
|||
assert payload.get("model") == "gpt-3.5-turbo"
|
||||
assert payload.get("user") == "test_user"
|
||||
|
||||
print("\n" + "=" * 80)
|
||||
print("✅ CRITICAL E2E TEST PASSED")
|
||||
print("=" * 80)
|
||||
print(f"Token: {data['token']}")
|
||||
print(f"Payload api_key: {payload_api_key}")
|
||||
print(f"Match: {data['token'] == payload_api_key}")
|
||||
print("=" * 80)
|
||||
print("Production incident bug is FIXED and protected:")
|
||||
print("- Failed requests preserve api_key through entire flow")
|
||||
print("- Both SpendLogs AND DailyUserSpend will have correct api_key")
|
||||
print("=" * 80 + "\n")
|
||||
|
||||
|
||||
@patch("litellm.proxy.proxy_server.master_key", None)
|
||||
@patch("litellm.proxy.proxy_server.general_settings", {})
|
||||
|
|
@ -2591,6 +2577,219 @@ def test_sanitize_error_information_redacts_pydantic_assignment_form(
|
|||
assert REDACTED_BY_LITELM_STRING in sanitized["error_message"]
|
||||
|
||||
|
||||
# ── _redact_logged_api_key unit tests ──────────────────────────────────────
|
||||
|
||||
|
||||
def test_redact_logged_api_key_none_returns_none():
|
||||
assert _redact_logged_api_key(None) is None
|
||||
|
||||
|
||||
def test_redact_logged_api_key_empty_string_returns_none():
|
||||
assert _redact_logged_api_key("") is None
|
||||
|
||||
|
||||
def test_redact_logged_api_key_sk_key_is_hashed():
|
||||
raw = "sk-1234secret"
|
||||
result = _redact_logged_api_key(raw)
|
||||
assert result == hash_token(raw)
|
||||
assert result is not None
|
||||
assert not result.startswith("sk-")
|
||||
assert len(result) == 64
|
||||
|
||||
|
||||
def test_redact_logged_api_key_bearer_sk_equals_sk_hash():
|
||||
raw = "sk-1234secret"
|
||||
result_plain = _redact_logged_api_key(raw)
|
||||
result_bearer = _redact_logged_api_key(f"Bearer {raw}")
|
||||
assert result_bearer == result_plain
|
||||
|
||||
|
||||
def test_redact_logged_api_key_bearer_case_insensitive():
|
||||
raw = "sk-1234secret"
|
||||
result_lower = _redact_logged_api_key(f"bearer {raw}")
|
||||
result_upper = _redact_logged_api_key(f"BEARER {raw}")
|
||||
expected = hash_token(raw)
|
||||
assert result_lower == expected
|
||||
assert result_upper == expected
|
||||
|
||||
|
||||
def test_redact_logged_api_key_non_sk_raw_key_is_hashed():
|
||||
raw = "anthropic-raw-key-xyz"
|
||||
result = _redact_logged_api_key(raw)
|
||||
assert result is not None
|
||||
assert result != raw
|
||||
assert len(result) == 64
|
||||
assert result == hash_token(raw)
|
||||
|
||||
|
||||
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_redacted=True)
|
||||
assert result == already_hashed
|
||||
assert hash_token(already_hashed) != result # no double-hash
|
||||
|
||||
|
||||
def test_redact_logged_api_key_sha256_without_flag_is_hashed():
|
||||
already_hashed = hash_token("sk-some-key")
|
||||
assert len(already_hashed) == 64
|
||||
result = _redact_logged_api_key(already_hashed)
|
||||
assert result is not None
|
||||
assert result != already_hashed
|
||||
assert len(result) == 64
|
||||
assert result == hash_token(already_hashed)
|
||||
|
||||
|
||||
def test_redact_logged_api_key_long_opaque_token_is_hashed():
|
||||
raw = "x1" * 450
|
||||
assert len(raw) == 900
|
||||
result = _redact_logged_api_key(raw)
|
||||
assert result is not None
|
||||
assert result != raw
|
||||
assert raw not in result
|
||||
assert len(result) == 64
|
||||
assert result == hash_token(raw)
|
||||
|
||||
|
||||
def test_redact_logged_api_key_hashed_jwt_passes_through():
|
||||
jwt_hash = "hashed-jwt-" + "a" * 64
|
||||
result = _redact_logged_api_key(jwt_hash, already_redacted=True)
|
||||
assert result == jwt_hash
|
||||
|
||||
|
||||
def test_redact_logged_api_key_hashed_jwt_shape_without_provenance_is_hashed():
|
||||
lookalike = "hashed-jwt-" + "a" * 64
|
||||
result = _redact_logged_api_key(lookalike)
|
||||
assert result == hash_token(lookalike)
|
||||
assert result != lookalike
|
||||
|
||||
|
||||
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_redacted=True)
|
||||
assert result == hash_token(trailing)
|
||||
assert result != trailing
|
||||
|
||||
|
||||
def test_redact_logged_api_key_hashed_jwt_short_suffix_is_hashed():
|
||||
short_jwt = "hashed-jwt-tooshort"
|
||||
result = _redact_logged_api_key(short_jwt)
|
||||
assert result is not None
|
||||
assert result != short_jwt
|
||||
assert len(result) == 64
|
||||
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
|
||||
assert _redact_logged_api_key("Bearer ") is None
|
||||
assert _redact_logged_api_key("BEARER ") is None
|
||||
|
||||
|
||||
# ── _get_spend_logs_metadata key-hash invariant tests ─────────────────────
|
||||
|
||||
|
||||
def test_get_spend_logs_metadata_sk_key_hashed():
|
||||
raw = "sk-1234secret"
|
||||
meta = _get_spend_logs_metadata({"user_api_key": raw})
|
||||
assert meta["user_api_key"] == hash_token(raw)
|
||||
assert meta["user_api_key"] is not None
|
||||
result = meta["user_api_key"]
|
||||
assert result is not None
|
||||
assert not result.startswith("sk-")
|
||||
assert len(result) == 64
|
||||
|
||||
|
||||
def test_get_spend_logs_metadata_bearer_sk_key_hashed_same_as_plain():
|
||||
raw = "sk-1234secret"
|
||||
meta_plain = _get_spend_logs_metadata({"user_api_key": raw})
|
||||
meta_bearer = _get_spend_logs_metadata({"user_api_key": f"Bearer {raw}"})
|
||||
assert meta_bearer["user_api_key"] == meta_plain["user_api_key"]
|
||||
|
||||
|
||||
def test_get_spend_logs_metadata_non_sk_raw_key_hashed():
|
||||
raw = "anthropic-raw-key-xyz"
|
||||
meta = _get_spend_logs_metadata({"user_api_key": raw})
|
||||
result = meta["user_api_key"]
|
||||
assert result is not None
|
||||
assert result != raw
|
||||
assert len(result) == 64
|
||||
|
||||
|
||||
def test_get_spend_logs_metadata_already_hashed_unchanged_with_provenance():
|
||||
already_hashed = hash_token("sk-some-key")
|
||||
meta = _get_spend_logs_metadata(
|
||||
{"user_api_key": already_hashed, "user_api_key_hash": already_hashed}
|
||||
)
|
||||
assert meta["user_api_key"] == already_hashed
|
||||
assert hash_token(already_hashed) != meta["user_api_key"] # no double-hash
|
||||
|
||||
|
||||
def test_get_spend_logs_metadata_already_hashed_no_provenance_is_rehashed():
|
||||
already_hashed = hash_token("sk-some-key")
|
||||
meta = _get_spend_logs_metadata({"user_api_key": already_hashed})
|
||||
assert meta["user_api_key"] != already_hashed
|
||||
assert meta["user_api_key"] == hash_token(already_hashed)
|
||||
|
||||
|
||||
def test_get_spend_logs_metadata_provenance_bypass_requires_hash_match():
|
||||
already_hashed = hash_token("sk-some-key")
|
||||
different_hash = hash_token("sk-other-key")
|
||||
meta = _get_spend_logs_metadata(
|
||||
{"user_api_key": already_hashed, "user_api_key_hash": different_hash}
|
||||
)
|
||||
assert meta["user_api_key"] == hash_token(already_hashed)
|
||||
|
||||
|
||||
def test_get_spend_logs_metadata_hashed_jwt_unchanged():
|
||||
jwt_hash = "hashed-jwt-" + "b" * 64
|
||||
meta = _get_spend_logs_metadata({"user_api_key": jwt_hash, "user_api_key_hash": jwt_hash})
|
||||
assert meta["user_api_key"] == jwt_hash
|
||||
|
||||
|
||||
def test_get_spend_logs_metadata_hashed_jwt_shape_without_provenance_is_hashed():
|
||||
lookalike = "hashed-jwt-" + "b" * 64
|
||||
meta = _get_spend_logs_metadata({"user_api_key": lookalike})
|
||||
assert meta["user_api_key"] == hash_token(lookalike)
|
||||
assert meta["user_api_key"] != lookalike
|
||||
|
||||
|
||||
def test_get_spend_logs_metadata_none_key_is_none():
|
||||
meta = _get_spend_logs_metadata({"user_api_key": None})
|
||||
assert meta["user_api_key"] is None
|
||||
|
||||
|
||||
# ── get_logging_payload key-hash invariant tests ───────────────────────────
|
||||
|
||||
|
||||
def test_get_logging_payload_uses_recovered_combined_usage_on_failure():
|
||||
"""A request that fails mid-stream has no usable response_obj usage, but the
|
||||
streaming handler recovers the usage from the chunks already delivered and
|
||||
|
|
@ -2747,44 +2946,107 @@ def test_get_logging_payload_cache_hit_keeps_raw_litellm_call_id():
|
|||
assert json.loads(payload["metadata"])["litellm_call_id"] != payload["request_id"]
|
||||
|
||||
|
||||
class TestHashApiKeyForSpendLog:
|
||||
class TestSpendLogKeyRedaction:
|
||||
"""Regression: plaintext API keys with Bearer prefix were stored in
|
||||
SpendLogs for failed requests (LIT-4121)"""
|
||||
|
||||
def test_bearer_prefixed_sk_key_is_hashed(self):
|
||||
raw = "Bearer sk-WLi4iRn4JmbVlTaYw12IOA"
|
||||
result = _hash_api_key_for_spend_log(raw)
|
||||
result = _redact_logged_api_key(raw)
|
||||
assert result is not None
|
||||
assert not result.startswith("Bearer")
|
||||
assert not result.startswith("sk-")
|
||||
assert len(result) == 64
|
||||
|
||||
def test_bare_sk_key_is_hashed(self):
|
||||
raw = "sk-WLi4iRn4JmbVlTaYw12IOA"
|
||||
result = _hash_api_key_for_spend_log(raw)
|
||||
result = _redact_logged_api_key(raw)
|
||||
assert result is not None
|
||||
assert not result.startswith("sk-")
|
||||
assert len(result) == 64
|
||||
|
||||
def test_bearer_lowercase_is_handled(self):
|
||||
raw = "bearer sk-WLi4iRn4JmbVlTaYw12IOA"
|
||||
result = _hash_api_key_for_spend_log(raw)
|
||||
result = _redact_logged_api_key(raw)
|
||||
assert result is not None
|
||||
assert not result.startswith("bearer")
|
||||
assert not result.startswith("sk-")
|
||||
assert len(result) == 64
|
||||
|
||||
def test_already_hashed_key_unchanged(self):
|
||||
hashed = "bcfe8173f5447f10be0e7fb37aaa8b97829d5c9e0498232152f9d123456789ab"
|
||||
assert _hash_api_key_for_spend_log(hashed) == hashed
|
||||
assert _redact_logged_api_key(hashed, already_redacted=True) == hashed
|
||||
|
||||
def test_bearer_prefixed_non_sk_key_strips_prefix(self):
|
||||
def test_bearer_prefixed_non_sk_key_is_hashed(self):
|
||||
raw = "Bearer some-other-token-format"
|
||||
result = _hash_api_key_for_spend_log(raw)
|
||||
assert result == "some-other-token-format"
|
||||
result = _redact_logged_api_key(raw)
|
||||
assert result == hash_token("some-other-token-format")
|
||||
assert result is not None
|
||||
assert not result.startswith("Bearer")
|
||||
|
||||
def test_bearer_and_bare_produce_same_hash(self):
|
||||
bare = "sk-WLi4iRn4JmbVlTaYw12IOA"
|
||||
bearer = "Bearer sk-WLi4iRn4JmbVlTaYw12IOA"
|
||||
assert _hash_api_key_for_spend_log(bare) == _hash_api_key_for_spend_log(bearer)
|
||||
assert _redact_logged_api_key(bare) == _redact_logged_api_key(bearer)
|
||||
|
||||
|
||||
@patch("litellm.proxy.proxy_server.master_key", None)
|
||||
@patch("litellm.proxy.proxy_server.general_settings", {})
|
||||
def test_get_logging_payload_non_sk_raw_key_both_fields_hashed():
|
||||
raw = "anthropic-raw-key-xyz"
|
||||
kwargs = {
|
||||
"model": "openai/gpt-4.1",
|
||||
"messages": [{"role": "user", "content": "Hello"}],
|
||||
"call_type": "acompletion",
|
||||
"litellm_params": {
|
||||
"metadata": {
|
||||
"user_api_key": raw,
|
||||
"user_api_key_user_id": "test_user",
|
||||
"user_api_key_team_id": "test_team",
|
||||
}
|
||||
},
|
||||
}
|
||||
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"] != raw
|
||||
assert len(payload["api_key"]) == 64
|
||||
|
||||
parsed_meta = json.loads(payload["metadata"])
|
||||
assert parsed_meta["user_api_key"] != raw
|
||||
assert parsed_meta["user_api_key"] is not None
|
||||
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)
|
||||
|
|
@ -3241,3 +3503,69 @@ def test_get_logging_payload_failed_request_without_standard_logging_payload_lea
|
|||
assert payload["model_group"] == ""
|
||||
assert payload["api_base"] == ""
|
||||
assert payload["custom_llm_provider"] == ""
|
||||
|
||||
|
||||
@patch("litellm.proxy.proxy_server.master_key", None)
|
||||
@patch("litellm.proxy.proxy_server.general_settings", {})
|
||||
def test_get_logging_payload_empty_key_slp_none_is_empty_string_not_none_literal():
|
||||
kwargs = {
|
||||
"model": "openai/gpt-4.1",
|
||||
"messages": [{"role": "user", "content": "Hello"}],
|
||||
"call_type": "acompletion",
|
||||
"litellm_params": {
|
||||
"metadata": {
|
||||
"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"] == "", (
|
||||
f"Expected empty string but got {payload['api_key']!r}; "
|
||||
"dropping _redact_logged_api_key's 'or \"\"' guard would yield 'None' here"
|
||||
)
|
||||
|
||||
|
||||
def test_get_spend_logs_metadata_sibling_fields_preserved():
|
||||
raw = "anthropic-raw-key-xyz"
|
||||
meta = _get_spend_logs_metadata(
|
||||
{
|
||||
"user_api_key": raw,
|
||||
"user_api_key_alias": "my-alias",
|
||||
"user_api_key_team_id": "team-123",
|
||||
}
|
||||
)
|
||||
assert meta["user_api_key"] == hash_token(raw)
|
||||
assert meta["user_api_key_alias"] == "my-alias"
|
||||
assert meta["user_api_key_team_id"] == "team-123"
|
||||
|
||||
|
||||
def test_redact_logged_api_key_partial_sha256_is_hashed():
|
||||
partial_hex = "a" * 63
|
||||
result = _redact_logged_api_key(partial_hex)
|
||||
assert result is not None
|
||||
assert result != partial_hex
|
||||
assert len(result) == 64
|
||||
assert result == hash_token(partial_hex)
|
||||
|
||||
|
||||
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_redacted=True)
|
||||
assert result == already_hashed
|
||||
assert hash_token(already_hashed) != result
|
||||
|
||||
|
||||
def test_redact_logged_api_key_bearer_sha256_without_flag_is_hashed():
|
||||
already_hashed = hash_token("sk-some-key")
|
||||
assert len(already_hashed) == 64
|
||||
result = _redact_logged_api_key(f"Bearer {already_hashed}")
|
||||
assert result is not None
|
||||
assert result != already_hashed
|
||||
assert result == hash_token(already_hashed)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue