mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-06 02:48:13 +00:00
fix(proxy): keep request-body aws credentials out of stored spend-log requests
This commit is contained in:
parent
7e383c9f6a
commit
f298c9f696
5 changed files with 83 additions and 3 deletions
|
|
@ -186,6 +186,7 @@ def _sanitize_for_log(value: object) -> str:
|
|||
from litellm.router import Router
|
||||
from litellm.secret_managers.main import get_secret_bool
|
||||
from litellm.types.llms.anthropic import ANTHROPIC_API_HEADERS
|
||||
from litellm.types.llms.bedrock import AWS_CREDENTIAL_VALUE_PARAM_KEYS
|
||||
from litellm.types.services import ServiceTypes
|
||||
from litellm.types.utils import (
|
||||
CustomPricingLiteLLMParams,
|
||||
|
|
@ -1964,6 +1965,7 @@ def refresh_proxy_server_request_body_snapshot(
|
|||
frozenset({"secret_fields", "proxy_server_request", "litellm_logging_obj"})
|
||||
| _TRANSPORT_ONLY_CREDENTIAL_KEYS
|
||||
| _CALLBACK_CREDENTIAL_KEYS
|
||||
| AWS_CREDENTIAL_VALUE_PARAM_KEYS
|
||||
)
|
||||
body: Final = { # mutable-ok: audit JSON serialization requires a dict with shared nested messages
|
||||
k: v for k, v in data.items() if k not in _body_snapshot_exclude
|
||||
|
|
|
|||
|
|
@ -48,6 +48,7 @@ from litellm.proxy._types import SpendLogsMetadata, SpendLogsPayload, SpendLogsR
|
|||
from litellm.proxy.route_llm_request import ProxyModelNotFoundError
|
||||
from litellm.proxy.spend_tracking.spend_log_error_logger import spend_log_error
|
||||
from litellm.proxy.utils import PrismaClient, hash_token
|
||||
from litellm.types.llms.bedrock import AWS_CREDENTIAL_VALUE_PARAM_KEYS
|
||||
from litellm.types.router import DeploymentTypedDict, LiteLLM_Params
|
||||
from litellm.types.utils import (
|
||||
PROMPT_CARRYING_GUARDRAIL_FIELDS,
|
||||
|
|
@ -1094,8 +1095,9 @@ def _sanitize_request_body_for_spend_logs_payload(
|
|||
Recursively sanitize request body to prevent logging large base64 strings or other large values.
|
||||
Truncates strings longer than MAX_STRING_LENGTH_PROMPT_IN_DB characters and handles nested dictionaries.
|
||||
|
||||
Also strips keys listed in _SENSITIVE_REQUEST_BODY_KEYS (e.g. secret_fields
|
||||
which contains raw HTTP headers including Authorization tokens).
|
||||
At every nesting level, also strips keys listed in _SENSITIVE_REQUEST_BODY_KEYS (e.g. secret_fields,
|
||||
which holds raw HTTP headers including Authorization tokens) and masks string values of AWS
|
||||
credential keys with REDACTED_BY_LITELM_STRING.
|
||||
"""
|
||||
from litellm.constants import (
|
||||
LITELLM_TRUNCATED_PAYLOAD_FIELD,
|
||||
|
|
@ -1152,7 +1154,13 @@ def _sanitize_request_body_for_spend_logs_payload(
|
|||
return value
|
||||
return value
|
||||
|
||||
return {k: _sanitize_value(v) for k, v in request_body.items() if k not in _SENSITIVE_REQUEST_BODY_KEYS}
|
||||
return {
|
||||
k: REDACTED_BY_LITELM_STRING
|
||||
if k in AWS_CREDENTIAL_VALUE_PARAM_KEYS and isinstance(v, str)
|
||||
else _sanitize_value(v)
|
||||
for k, v in request_body.items()
|
||||
if k not in _SENSITIVE_REQUEST_BODY_KEYS
|
||||
}
|
||||
|
||||
|
||||
# Quoted-key form: ``"input"`` / ``'messages'`` / ``"prompt"`` followed by
|
||||
|
|
|
|||
|
|
@ -1132,6 +1132,9 @@ class AwsAuthParams(BaseModel):
|
|||
|
||||
|
||||
AWS_AUTH_PARAM_KEYS: Final[tuple[str, ...]] = tuple(AwsAuthParams.model_fields)
|
||||
AWS_CREDENTIAL_VALUE_PARAM_KEYS: Final = frozenset(
|
||||
{"aws_access_key_id", "aws_secret_access_key", "aws_session_token", "aws_web_identity_token"}
|
||||
)
|
||||
|
||||
|
||||
class BedrockCreateBatchRequest(TypedDict, total=False):
|
||||
|
|
|
|||
|
|
@ -2691,6 +2691,42 @@ def test_sanitize_request_body_strips_secret_fields():
|
|||
assert sanitized["messages"] == [{"role": "user", "content": "hi"}]
|
||||
|
||||
|
||||
@patch("litellm.proxy.spend_tracking.spend_tracking_utils.should_store_prompts_and_responses_in_spend_logs")
|
||||
def test_proxy_server_request_payload_strips_nested_aws_credentials(mock_should_store: MagicMock) -> None:
|
||||
mock_should_store.return_value = True
|
||||
credentials: Final = {
|
||||
"aws_access_key_id": "AKIA-canary",
|
||||
"aws_secret_access_key": "secret-canary",
|
||||
"aws_session_token": "token-canary",
|
||||
"aws_web_identity_token": "wit-canary",
|
||||
}
|
||||
tool_parameters: Final = {"type": "object", "properties": {"aws_secret_access_key": {"type": "string"}}}
|
||||
litellm_params: Final = {
|
||||
"proxy_server_request": {
|
||||
"body": {
|
||||
"model": "bedrock-claude",
|
||||
"messages": [{"role": "user", "content": "hello"}],
|
||||
"fallbacks": [{"model": "bedrock-b", "aws_region_name": "us-west-2", **credentials}],
|
||||
"extra_body": {"aws_role_name": "arn:aws:iam::123456789012:role/r", **credentials},
|
||||
"tools": [{"type": "function", "function": {"name": "f", "parameters": tool_parameters}}],
|
||||
**credentials,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
parsed: Final = json.loads(
|
||||
_get_proxy_server_request_for_spend_logs_payload(metadata={}, litellm_params=litellm_params, kwargs={})
|
||||
)
|
||||
|
||||
assert "canary" not in json.dumps(parsed)
|
||||
masked: Final = dict.fromkeys(credentials, REDACTED_BY_LITELM_STRING)
|
||||
assert parsed["fallbacks"] == [{"model": "bedrock-b", "aws_region_name": "us-west-2", **masked}]
|
||||
assert parsed["extra_body"] == {"aws_role_name": "arn:aws:iam::123456789012:role/r", **masked}
|
||||
assert {name: parsed[name] for name in credentials} == masked
|
||||
assert parsed["tools"][0]["function"]["parameters"] == tool_parameters
|
||||
assert parsed["messages"] == [{"role": "user", "content": "hello"}]
|
||||
|
||||
|
||||
@patch("litellm.proxy.spend_tracking.spend_tracking_utils.should_store_prompts_and_responses_in_spend_logs")
|
||||
def test_proxy_server_request_payload_excludes_secret_fields(mock_should_store):
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -895,6 +895,37 @@ def test_body_snapshot_excludes_team_callback_credentials() -> None:
|
|||
}, proxy_request
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_add_litellm_data_to_request_body_snapshot_excludes_aws_credentials() -> None:
|
||||
from litellm.proxy.litellm_pre_call_utils import add_litellm_data_to_request
|
||||
from litellm.types.llms.bedrock import AWS_CREDENTIAL_VALUE_PARAM_KEYS
|
||||
|
||||
aws_credentials: Final = {name: f"canary-{name}" for name in AWS_CREDENTIAL_VALUE_PARAM_KEYS}
|
||||
data: Final = {
|
||||
"model": "bedrock-claude",
|
||||
"messages": [{"role": "user", "content": "hello"}],
|
||||
"aws_region_name": "us-east-1",
|
||||
"aws_role_name": "arn:aws:iam::123456789012:role/bedrock",
|
||||
**aws_credentials,
|
||||
}
|
||||
|
||||
updated: Final = await add_litellm_data_to_request(
|
||||
data=data,
|
||||
request=_make_request_mock("/v1/chat/completions", {"Content-Type": "application/json"}),
|
||||
user_api_key_dict=UserAPIKeyAuth(api_key="hashed-key", user_id="test-user"),
|
||||
proxy_config=MagicMock(),
|
||||
general_settings={},
|
||||
version="test-version",
|
||||
)
|
||||
|
||||
snapshot_body: Final = updated["proxy_server_request"]["body"]
|
||||
assert "canary-" not in json.dumps(snapshot_body, default=str)
|
||||
assert snapshot_body["aws_region_name"] == "us-east-1"
|
||||
assert snapshot_body["aws_role_name"] == "arn:aws:iam::123456789012:role/bedrock"
|
||||
assert snapshot_body["messages"] == [{"role": "user", "content": "hello"}]
|
||||
assert {name: updated[name] for name in AWS_CREDENTIAL_VALUE_PARAM_KEYS} == aws_credentials
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize("pre_call_ran", [False, True])
|
||||
async def test_post_guardrail_snapshot_preserves_logging_only_masking_in_spend_logs(
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue