L
Artificial Intelligence & LLM • Literally Web ID

Integrasi Large Language Models (LLM) Lokal ke Backend Node.js

⏱️ 18 menit baca • 📅 16 Oct 2026 • 👁️ 1 views
Integrasi Large Language Models (LLM) Lokal ke Backend Node.js

1. Konteks Industri & Analisis Akar Masalah

Dalam lanskap pengembangan aplikasi enterprise modern, integrasi kecerdasan buatan—khususnya Large Language Models (LLM)—telah bertransformasi dari sekadar fitur eksperimental menjadi pilar operasional yang esensial. Namun, ketergantungan penuh pada Managed AI APIs pihak ketiga (seperti OpenAI GPT-4, Anthropic Claude, atau Google Gemini) memunculkan berbagai masalah mendasar yang sulit ditoleransi oleh perusahaan dengan standar keamanan data yang ketat: latensi jaringan eksternal yang tidak dapat diprediksi, biaya operasional (token-based pricing) yang membengkak seiring volume transaksi, serta risiko pelanggaran regulasi privasi data (seperti GDPR, HIPAA, atau UU PDP di Indonesia) karena data sensitif pelanggan harus dikirim melintasi batas yurisdiksi infrastruktur perusahaan.

Sebagai Principal Web & Infrastructure Systems Architect, saya melihat pergeseran besar menuju arsitektur Local LLM Inference. Dengan menjalankan model open-source (seperti Llama 3, Mistral, Gemma, atau Qwen) secara mandiri di atas infrastruktur internal menggunakan Ollama, perusahaan mendapatkan kedaulatan penuh atas data, latensi inferensi yang mendekati instan dalam jaringan lokal (on-premise/private cloud), serta kontrol penuh atas pengeluaran infrastruktur tanpa kejutan tagihan bulanan.

Namun, menghubungkan LLM lokal ke dalam backend Node.js non-blocking I/O membawa tantangan teknis tersendiri. Proses inferensi LLM bersifat komputasi intensif (compute-bound) dan membutuhkan waktu respons yang jauh lebih lama dibandingkan kueri basis data konvensional atau pemanggilan API standar. Tanpa arsitektur asinkron yang tepat, koneksi HTTP konvensional akan mengalami timeout, atau server Node.js akan kehabisan sumber daya (thread pool exhaustion) akibat menunggu token teks dihasilkan satu per satu.

  • Kondisi Sebelum Penerapan: Aplikasi backend Node.js memanggil API eksternal berbasis cloud. Biaya tinggi, rentan terhadap gangguan jaringan publik (packet loss), isu latensi tinggi, dan ketidakpatuhan terhadap regulasi kepatuhan data (data compliance).
  • Kondisi Sesudah Penerapan: Backend Node.js berkomunikasi secara lokal melalui REST/gRPC dengan server inferensi Ollama yang di-deploy pada node GPU/CPU terdedikasi. Penggunaan Server-Sent Events (SSE) memastikan pengiriman token secara real-time ke antarmuka web, sementara sistem antrean (queue) berbasis Redis mengelola lonjakan trafik tanpa menurunkan kinerja sistem secara keseluruhan.

2. Konsep Inti & Deep Dive Arsitektur

Arsitektur integrasi LLM lokal ke backend Node.js dirancang dengan memisahkan lapisan layanan aplikasi (application tier) dari lapisan inferensi model AI (inference tier). Pemisahan ini krusial untuk mencegah beban pemrosesan matriks neural network AI mengganggu siklus kejadian (event loop) dari Node.js yang menangani permintaan HTTP klien.

Berikut adalah representasi visual dari alur data dalam arsitektur sistem ini:

+-----------------------+
|     Client Browser      |
+-----------------------+
           |
           | (HTTP / EventSource - SSE)
           v
+-----------------------+
|  Nginx Reverse Proxy  | ---> (SSL Termination & Rate Limiting)
+-----------------------+
           |
           | (Reverse Proxy Pass)
           v
+-------------------------------------------------------+-
|                      Node.js Backend                    |
|                                                         |
|  +-------------------+      +------------------------+  |
|  | Express Router    | ---> | LLM Service Controller |  |
|  +-------------------+      +------------------------+  |
|                                          |              |
|                                          | (Axios /     |
|                                          |  Fetch API)  |
+------------------------------------------|--------------+
                                           |
                                           v
+---------------------------------------------------------+
|                 Ollama Inference Server                 |
|                 (Port 11434 - Localhost)                |
|                                                         |
|  +---------------------------------------------------+  |
|  | Llama-3 / Mistral / Gemma GGUF Model Weights      |
|  +---------------------------------------------------+  |
+---------------------------------------------------------+

Alur Eksekusi Data (Data Execution Flow):

  1. Inisiasi Permintaan: Klien (browser atau aplikasi frontend) mengirimkan prompt teks melalui koneksi HTTP POST ke backend Node.js.
  2. Validasi & Sanitasi: Express.js di backend menerima payload, melakukan sanitasi input untuk mencegah injeksi, dan memformat prompt sesuai chat template yang dipahami oleh model LLM lokal.
  3. Komunikasi Asinkron: Backend Node.js membuka koneksi HTTP streaming ke Ollama API (http://localhost:11434/api/generate atau /api/chat).
  4. Pemrosesan Token: Ollama mengeksekusi model menggunakan akselerasi GPU (CUDA/Metal) dan mulai menghasilkan token teks secara bertahap.
  5. Penyaluran Real-time (SSE): Begitu token pertama diterima oleh backend Node.js, backend langsung meneruskannya ke klien menggunakan protokol Server-Sent Events (SSE). Hal ini memberikan ilusi kecepatan interaksi (perceived performance) yang tinggi kepada pengguna akhir.

3. Perbandingan Komparatif / Analisis Pendekatan

Memilih metode komunikasi dan infrastruktur yang tepat sangat menentukan skalabilitas sistem. Berikut adalah analisis komparatif mendalam antara tiga pendekatan utama integrasi LLM lokal.

Parameter Komparasi Pendekatan 1: REST API Sinkron Tradisional Pendekatan 2: Server-Sent Events (SSE) (Direkomendasikan) Pendekatan 3: WebSocket Bidirectional
Latensi Respons Awal Sangat Tinggi (Menunggu seluruh teks selesai) Sangat Rendah (Streaming token per token) Sangat Rendah (Streaming token per token)
Kompleksitas Implementasi Rendah Sedang Tinggi
Penggunaan Sumber Daya Tinggi (Membebani memori server saat payload besar) Efisien (Streaming langsung tanpa buffer penuh) Efisien, tetapi membutuhkan manajemen state koneksi persisten
Kompatibilitas Firewall/Proxy Sempurna (HTTP standar) Sangat Baik (Didukung penuh HTTP/1.1 & HTTP/2) Rentan terhadap pemblokiran proxy korporat tertentu
Skenario Penggunaan Utama Pemrosesan background / Batch job Aplikasi Chatbot Web & Text Generation interaktif Aplikasi kolaboratif real-time multi-user (misal: Google Docs AI)

[!TIP] Gunakan pendekatan Server-Sent Events (SSE) untuk aplikasi chatbot standar karena kesederhanaan protokol HTTP satu arah, kemudahan reconnection otomatis oleh browser, dan kompatibilitas yang luar biasa dengan infrastruktur Reverse Proxy seperti Nginx dan Cloudflare tanpa konfigurasi upgrade handshake yang rumit seperti WebSocket.


4. Prasyarat Sistem & Persiapan Lingkungan

Sebelum melangkah ke implementasi kode, pastikan infrastruktur sistem memenuhi spesifikasi teknis minimum berikut agar proses inferensi berjalan mulus tanpa mengalami Out-Of-Memory (OOM) killer pada sistem operasi.

Spesifikasi Minimum Hardware (Development & Low-Traffic Production):

  • CPU: 8 Cores (Intel Xeon / AMD EPYC / Apple Silicon M-Series)
  • RAM: Minimum 16 GB (Direkomendasikan 32 GB untuk model 8B parameter ke atas)
  • GPU: NVIDIA GPU dengan VRAM minimum 8GB (RTX 3060/4060 atau lebih tinggi) dengan dukungan CUDA Toolkit, atau Apple Silicon (M1/M2/M3) dengan Unified Memory.
  • Storage: SSD NVMe dengan ruang kosong minimal 20 GB untuk penyimpanan bobot model (model weights).

Persiapan Software & Dependencies:

  1. Node.js: Versi LTS terbaru (v20.x atau v22.x) terpasang melalui NVM (Node Version Manager).
  2. Ollama: Terinstal pada sistem operasi (Linux Ubuntu/Debian, macOS, atau Windows WSL2).

Verifikasi instalasi Ollama melalui terminal sistem:

# Verifikasi versi Ollama dan pastikan daemon berjalan
ollama --version

# Unduh model Llama 3 8B versi terkuantisasi (Q4_K_M) yang optimal untuk hardware standar
ollama pull llama3

# Uji koneksi API lokal Ollama
curl http://localhost:11434/api/tags

5. Panduan Implementasi Langkah demi Langkah

Ikuti langkah-langkah praktis berikut untuk membangun sistem integrasi penuh dari nol. Kita akan membuat struktur proyek Node.js menggunakan Express.js, mengimplementasikan service layer untuk komunikasi dengan Ollama, serta menyajikan endpoint SSE untuk frontend streaming.

Langkah 1: Inisialisasi Proyek Node.js

Buat direktori proyek baru dan instal dependensi yang dibutuhkan:

mkdir local-llm-backend
cd local-llm-backend
npm init -y

# Instal dependensi esensial
npm install express dotenv cors
npm install -D nodemon

Perbarui package.json untuk menambahkan skrip eksekusi development:

{
  "name": "local-llm-backend",
  "version": "1.0.0",
  "description": "Enterprise Local LLM Backend Integration with Node.js and Ollama",
  "main": "server.js",
  "scripts": {
    "start": "node server.js",
    "dev": "nodemon server.js"
  },
  "dependencies": {
    "cors": "^2.8.5",
    "dotenv": "^16.4.5",
    "express": "^4.19.2"
  },
  "devDependencies": {
    "nodemon": "^3.1.0"
  }
}

Langkah 2: Konfigurasi Environment (.env)

Buat file .env di root direktori proyek:

PORT=3000
OLLAMA_HOST=http://localhost:11434
OLLAMA_MODEL=llama3

Langkah 3: Implementasi Backend Utama (server.js)

Buat file server.js dengan kode produksi lengkap berikut (tanpa placeholder):

/**
 * Enterprise Node.js & Local LLM Integration Server
 * Architecture: Express.js + Native fetch + Server-Sent Events (SSE)
 */

const express = require('express');
const cors = require('cors');
require('dotenv').config();

const app = express();
const PORT = process.env.PORT || 3000;
const OLLAMA_HOST = process.env.OLLAMA_HOST || 'http://localhost:11434';
const OLLAMA_MODEL = process.env.OLLAMA_MODEL || 'llama3';

// Middleware
app.use(cors());
app.use(express.json());
app.use(express.static('public'));

/**
 * Health Check Endpoint untuk Infrastructure Monitoring & Load Balancer
 */
app.get('/health', async (req, res) => {
    try {
        const response = await fetch(`${OLLAMA_HOST}/api/tags`);
        if (!response.ok) throw new Error('Ollama service is unresponsive');
        const data = await response.json();
        
        res.status(200).json({
            status: 'UP',
            timestamp: new Date().toISOString(),
            ollama_status: 'Connected',
            available_models: data.models.map(m => m.name)
        });
    } catch (error) {
        res.status(503).json({
            status: 'DOWN',
            timestamp: new Date().toISOString(),
            error: error.message
        });
    }
});

/**
 * Streaming Chat Endpoint menggunakan Server-Sent Events (SSE)
 */
app.post('/api/chat/stream', async (req, res) => {
    const { prompt, systemPrompt } = req.body;

    if (!prompt || typeof prompt !== 'string') {
        return res.status(400).json({ error: 'Valid prompt string is required in request body.' });
    }

    // Set headers untuk Server-Sent Events (SSE)
    res.setHeader('Content-Type', 'text/event-stream');
    res.setHeader('Cache-Control', 'no-cache');
    res.setHeader('Connection', 'keep-alive');
    res.flushHeaders();

    try {
        // Format payload untuk Ollama API
        const ollamaPayload = {
            model: OLLAMA_MODEL,
            prompt: prompt,
            system: systemPrompt || 'You are an advanced enterprise AI assistant built for secure backend processing.',
            stream: true,
            options: {
                temperature: 0.7,
                num_predict: 1024
            }
        };

        // Panggilan fetch asinkron ke server Ollama lokal
        const ollamaResponse = await fetch(`${OLLAMA_HOST}/api/generate`, {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json',
            },
            body: JSON.stringify(ollamaPayload)
        });

        if (!ollamaResponse.ok) {
            throw new Error(`Ollama API error: ${ollamaResponse.statusText}`);
        }

        // Membaca stream data chunk demi chunk dari respons Ollama
        const reader = ollamaResponse.body.getReader();
        const decoder = new TextDecoder('utf-8');

        let done = false;
        while (!done) {
            const { value, done: readerDone } = await reader.read();
            done = readerDone;

            if (value) {
                const chunkString = decoder.decode(value, { stream: true });
                
                // Ollama mengirimkan baris JSON terpisah untuk setiap token
                const lines = chunkString.split('\n');
                for (const line of lines) {
                    if (line.trim() !== '') {
                        const parsed = JSON.parse(line);
                        
                        // Kirim token ke klien via SSE
                        res.write(`data: ${JSON.stringify({ token: parsed.response, done: parsed.done })}\n\n`);

                        if (parsed.done) {
                            res.end();
                        }
                    }
                }
            }
        }
    } catch (error) {
        console.error('Error during LLM stream processing:', error);
        res.write(`data: ${JSON.stringify({ error: error.message })}\n\n`);
        res.end();
    }
});

// Menjalankan server HTTP
app.listen(PORT, () => {
    console.log(`[INFRASTRUCTURE] Enterprise Backend Server running on port ${PORT}`);
    console.log(`[AI ENGINE] Connected to Ollama at ${OLLAMA_HOST} using model ${OLLAMA_MODEL}`);
});

Langkah 4: Antarmuka Klien Sederhana (public/index.html)

Buat direktori public dan buat file index.html untuk menguji fungsionalitas SSE secara langsung:

<!DOCTYPE html>
<html lang="id">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Enterprise Local LLM Client</title>
    <style>
        body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; max-width: 800px; margin: 40px auto; padding: 20px; background: #f4f6f9; color: #333; }
        .card { background: #fff; padding: 24px; border-radius: 8px; box-shadow: 0 4px 6px rgba(0,0,0,0.05); }
        textarea { width: 100%; height: 100px; padding: 12px; border: 1px solid #ddd; border-radius: 4px; resize: vertical; margin-bottom: 12px; }
        button { background: #0052cc; color: white; border: none; padding: 10px 20px; border-radius: 4px; cursor: pointer; font-weight: bold; }
        button:hover { background: #0043a5; }
        #output { white-space: pre-wrap; background: #1e1e1e; color: #d4d4d4; padding: 16px; border-radius: 4px; margin-top: 20px; min-height: 150px; font-family: monospace; }
    </style>
</head>
<body>
    <div class="card">
        <h2>Enterprise Local LLM Gateway</h2>
        <p>Hubungkan aplikasi Anda ke model AI lokal secara aman dan real-time.</p>
        <textarea id="promptInput" placeholder="Masukkan prompt Anda di sini..."></textarea>
        <br>
        <button onclick="startStreaming()">Kirim Permintaan (Stream)</button>
        <div id="output">Menunggu respons sistem...</div>
    </div>

    <script>
        async function startStreaming() {
            const prompt = document.getElementById('promptInput').value;
            const outputDiv = document.getElementById('output');
            outputDiv.textContent = '';

            if (!prompt) {
                alert('Prompt tidak boleh kosong!');
                return;
            }

            try {
                const response = await fetch('/api/chat/stream', {
                    method: 'POST',
                    headers: { 'Content-Type': 'application/json' },
                    body: JSON.stringify({ prompt })
                });

                if (!response.ok) throw new Error('Gagal terhubung ke backend server.');

                const reader = response.body.getReader();
                const decoder = new TextDecoder();

                while (true) {
                    const { value, done } = await reader.read();
                    if (done) break;

                    const chunk = decoder.decode(value, { stream: true });
                    const lines = chunk.split('\n\n');

                    for (const line of lines) {
                        if (line.startsWith('data: ')) {
                            const jsonStr = line.replace('data: ', '').trim();
                            if (jsonStr) {
                                const data = JSON.parse(jsonStr);
                                if (data.token) {
                                    outputDiv.textContent += data.token;
                                }
                                if (data.error) {
                                    outputDiv.textContent += `\n[ERROR]: ${data.error}`;
                                }
                            }
                        }
                    }
                }
            } catch (error) {
                outputDiv.textContent += `\n[SYSTEM ERROR]: ${error.message}`;
            }
        }
    </script>
</body>
</html>

6. Bedah Kode & Penjelasan Detail Baris Kunci

Mari kita bedah beberapa komponen arsitektur paling krusial dalam kode di atas untuk memahami alasan di balik pemilihan implementasinya:

  1. Pengaturan Header SSE (res.setHeader):

    res.setHeader('Content-Type', 'text/event-stream');
    res.setHeader('Cache-Control', 'no-cache');
    res.setHeader('Connection', 'keep-alive');
    

    Baris ini memberitahu browser dan Nginx reverse proxy di tengah bahwa koneksi HTTP ini tidak boleh ditutup (keep-alive) dan respons dikirimkan secara bertahap (chunked transfer encoding). Tanpa Cache-Control: no-cache, browser korporat atau CDN mungkin akan mencoba menyimpan (cache) respons teks sebelum menampilkannya kepada pengguna, merusak efek real-time.pemrosesan.

  2. Penggunaan Native fetch dengan Streams API (response.body.getReader()): Node.js versi 18+ telah mendukung Fetch API secara native. Dengan memanggil getReader(), backend Node.js bertindak sebagai proxy transparan yang membaca byte stream dari server Ollama seketika saat token dihasilkan, tanpa perlu memuat seluruh string respons berukuran besar ke dalam memori RAM server Node.js. Ini menjaga garbage collector tetap tenang dan mencegah lonjakan konsumsi memori saat melayani ribuan pengguna bersamaan.

  3. Parsing Chunk Data Ollama (chunkString.split(' ')): Ollama mengirimkan respons dalam format Newline-Delimited JSON (ndjson). Setiap baris adalah objek JSON mandiri yang merepresentasikan satu token. Pemisahan string berdasarkan karakter baris baru ( ) memastikan setiap token dapat diekstrak secara akurat tanpa korupsi data JSON.


7. Skenario Produksi & Studi Kasus Dunia Nyata

Dalam lingkungan produksi enterprise dengan tingkat konkurensi tinggi, satu instance server Ollama akan mengalami antrean yang panjang jika menerima puluhan permintaan secara simultan, mengingat operasi matrix multiplication pada LLM memakan kapasitas penuh VRAM GPU.

Arsitektur Produksi High-Availability (HA):

  • Load Balancing di Layer Inferensi: Gunakan Nginx atau HAProxy di depan kluster server Ollama untuk mendistribusikan beban (Round Robin atau Least Connections) ke beberapa node GPU fisik.
  • Manajemen Antrean (Queue Management) di Node.js: Untuk mencegah penolakan koneksi (HTTP 503 Service Unavailable) saat lonjakan trafik, implementasikan Job Queue berbasis Redis menggunakan pustaka seperti BullMQ di backend Node.js.
[Client Requests] ---> [Node.js + BullMQ (Redis Queue)] 
                               |
       +-----------------------+-----------------------+
       |                       |                       |
       v                       v                       v
[Ollama Node 1 (GPU)]   [Ollama Node 2 (GPU)]   [Ollama Node 3 (GPU)]

Dengan pola ini, permintaan dari pengguna akan dimasukkan ke dalam antrean Redis yang aman, dan pekerja (worker) Node.js akan mengambil dan mengirimkannya ke node Ollama yang sedang idle, menjamin stabilitas sistem secara keseluruhan (graceful degradation).


8. Common Pitfalls, Anti-Patterns & Solusi

Sebagai arsitek sistem, saya sering menemukan kesalahan fatal yang dilakukan oleh tim pengembang saat mengintegrasikan AI ke dalam backend web:

  • Anti-Pattern 1: Melakukan Blokade Event Loop dengan Operasi Sinkron
    • Kesalahan: Menggunakan pustaka klien Ollama sinkron atau memproses token menggunakan operasi blocking array methods berukuran raksasa di thread utama Node.js.
    • Solusi: Selalu gunakan operasi asinkron (async/await) dan manfaatkan Streams API secara penuh.
  • Anti-Pattern 2: Mengabaikan Timeout HTTP pada Reverse Proxy (Nginx/Cloudflare)
    • Kesalahan: Mengirimkan prompt kompleks yang membutuhkan waktu inferensi 30 detik, sementara default proxy_read_timeout Nginx diset pada 60 detik atau Cloudflare edge timeout pada 100 detik, menyebabkan koneksi terputus tiba-tiba (HTTP 504 Gateway Timeout).
    • Solusi: Konfigurasi ulang Nginx reverse proxy untuk mendukung koneksi long-polling / SSE:
      location /api/chat/stream {
          proxy_pass http://node_backend_cluster;
          proxy_http_version 1.1;
          proxy_set_header Connection "";
          proxy_read_timeout 3600s;
          proxy_send_timeout 3600s;
          chunked_transfer_encoding on;
      }
      
  • Anti-Pattern 3: Tidak Melakukan Sanitasi Input (Prompt Injection Vulnerability)
    • Kesalahan: Langsung memasukkan input teks mentah dari pengguna ke dalam parameter sistem atau prompt LLM tanpa validasi.
    • Solusi: Lakukan validasi panjang string, sanitasi karakter khusus, dan terapkan batasan token length (num_predict) pada konfigurasi payload Ollama.

9. Benchmark, Metrik Kinerja & Tuning Performa

Pengujian performa sistem harus dilakukan secara berkala untuk memastikan latensi sistem tetap berada di batas toleransi SLA (Service Level Agreement). Gunakan tool load testing seperti k6 atau autocannon untuk menguji endpoint Node.js.

Skrip Uji Beban Menggunakan autocannon:

# Instal autocannon secara global
npm install -g autocannon

# Jalankan uji beban pada endpoint health check
autocannon -c 100 -d 30 http://localhost:3000/health

Parameter Tuning Lanjutan untuk Ollama (Environment Variables Linux Systemd):

Untuk performa maksimal pada server produksi Linux, edit konfigurasi systemd service Ollama (sudo systemctl edit ollama):

[Service]
Environment="OLLAMA_NUM_PARALLEL=4"
Environment="OLLAMA_MAX_LOADED_MODELS=2"
Environment="OLLAMA_FLASH_ATTENTION=1"
  • OLLAMA_NUM_PARALLEL: Memungkinkan server menangani beberapa permintaan secara bersamaan per model.
  • OLLAMA_FLASH_ATTENTION: Mengaktifkan optimalisasi memori CUDA untuk mempercepat kecepatan generasi token (tokens per second) hingga 20-30%.

10. Checklist Penerapan Produksi & Kesimpulan

Sebelum merilis sistem integrasi LLM lokal ke lingkungan produksi (Production Release), pastikan seluruh item dalam checklist berikut telah terpenuhi sepenuhnya:

  1. Keamanan Transportasi: Seluruh komunikasi antara klien, Nginx, Node.js, dan Ollama menggunakan enkripsi TLS/SSL yang valid (Let's Encrypt).
  2. Manajemen Akses & CORS: Kebijakan CORS dikonfigurasi secara ketat hanya mengizinkan domain frontend resmi perusahaan.
  3. Monitoring & Logging: Integrasi pemantauan kesehatan server menggunakan Prometheus dan Grafana untuk memantau penggunaan VRAM GPU dan CPU Node.js secara real-time.
  4. Error Handling & Fallback: Backend memiliki mekanisme fallback atau pesan error yang ramah pengguna jika server Ollama mengalami gangguan.
  5. Rotasi & Backup Model: Penyimpanan bobot model GGUF memiliki prosedur checksum integritas file dan strategi pembaruan versi model yang aman.

Dengan mengikuti panduan komprehensif ini, Anda tidak hanya berhasil menghubungkan LLM lokal ke backend Node.js, tetapi juga membangun arsitektur sistem web yang tangguh, aman, skalabel, dan siap menghadapi beban kerja enterprise tingkat tinggi.

🏷️ Tags: #Backend Architecture #Node.js #Ollama #Ai Integration #Server-Sent Events