diff --git a/litellm/proxy/management_endpoints/team_callback_endpoints.py b/litellm/proxy/management_endpoints/team_callback_endpoints.py index d36be2f5d8e..3b1e8995cb3 100644 --- a/litellm/proxy/management_endpoints/team_callback_endpoints.py +++ b/litellm/proxy/management_endpoints/team_callback_endpoints.py @@ -127,10 +127,11 @@ async def add_team_callbacks( param="callback_name", ) - team_callback_settings.append(data.model_dump()) + new_entry = data.model_dump() + encrypt_logging_callback_vars({"logging": [new_entry]}) + team_callback_settings.append(new_entry) team_metadata["logging"] = team_callback_settings - encrypt_logging_callback_vars(team_metadata) team_metadata_json = json.dumps(team_metadata) # update team_metadata new_team_row = await prisma_client.db.litellm_teamtable.update( diff --git a/tests/test_litellm/proxy/management_endpoints/test_team_callback_endpoints.py b/tests/test_litellm/proxy/management_endpoints/test_team_callback_endpoints.py index cd515903452..60613aa387a 100644 --- a/tests/test_litellm/proxy/management_endpoints/test_team_callback_endpoints.py +++ b/tests/test_litellm/proxy/management_endpoints/test_team_callback_endpoints.py @@ -71,6 +71,62 @@ async def test_add_team_callback_encrypts_callback_vars(mock_prisma, monkeypatch assert stored_secret != plaintext_secret, "plaintext secret must be encrypted before DB write" +@pytest.mark.asyncio +@patch("litellm.proxy.proxy_server.prisma_client") +async def test_add_second_callback_does_not_double_encrypt_first(mock_prisma, monkeypatch): + """Adding a second callback must not re-encrypt already-encrypted entries.""" + monkeypatch.setenv("LITELLM_SALT_KEY", "test-salt-key-1234567890123456") + from litellm.proxy.common_utils.callback_utils import encrypt_logging_callback_vars + from litellm.proxy.common_utils.encrypt_decrypt_utils import decrypt_value_helper + + # Simulate a team whose first callback was already written (and encrypted) + first_entry = { + "callback_name": "langfuse", + "callback_type": "success", + "callback_vars": {"langfuse_secret_key": "sk-lf-firstsecret"}, + } + encrypt_logging_callback_vars({"logging": [first_entry]}) + encrypted_first = first_entry["callback_vars"]["langfuse_secret_key"] + + existing_team = _make_team({"logging": [first_entry]}) + mock_prisma.get_data = AsyncMock(return_value=existing_team) + + captured = {} + + async def fake_update(where, data): + captured["data"] = data + row = MagicMock() + row.metadata = data.get("metadata", "{}") + return row + + mock_prisma.db = MagicMock() + mock_prisma.db.litellm_teamtable = MagicMock() + mock_prisma.db.litellm_teamtable.update = fake_update + + data = AddTeamCallback( + callback_name="langsmith", + callback_type="success", + callback_vars={"langsmith_api_key": "ls-secondsecret"}, + ) + + await add_team_callbacks( + data=data, + http_request=_make_request(), + team_id="team-123", + user_api_key_dict=UserAPIKeyAuth(), + ) + + written_metadata = json.loads(captured["data"]["metadata"]) + # First entry must be unchanged (not re-encrypted) + assert written_metadata["logging"][0]["callback_vars"]["langfuse_secret_key"] == encrypted_first + # First entry must still decrypt correctly + recovered = decrypt_value_helper(value=encrypted_first, key="langfuse_secret_key", return_original_value=True) + assert recovered == "sk-lf-firstsecret" + # Second entry must be encrypted + stored_second = written_metadata["logging"][1]["callback_vars"]["langsmith_api_key"] + assert stored_second != "ls-secondsecret" + + # --------------------------------------------------------------------------- # GET /team/{team_id}/callback — redact on read # ---------------------------------------------------------------------------