chat-nest-server 1.0.1 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +30 -11
- package/dist/index.js +45 -5
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
# chat-nest-server
|
|
2
2
|
|
|
3
|
-
> Streaming AI backend server for Chat Nest with built-in cost protection and cancellation propagation.
|
|
3
|
+
> Streaming AI backend server for Chat Nest with built-in cost protection and cancellation propagation using Server-Side Events (SSE).
|
|
4
4
|
|
|
5
5
|
This package exposes an Express-compatible request handler that:
|
|
6
|
-
- Streams AI responses
|
|
6
|
+
- Streams AI responses using Server-Side Events (SSE)
|
|
7
|
+
- Sends real-time tokens via SSE protocol
|
|
7
8
|
- Enforces rate limits and budgets
|
|
8
9
|
- Supports abort propagation
|
|
9
10
|
- Protects against runaway usage
|
|
@@ -12,7 +13,10 @@ This package exposes an Express-compatible request handler that:
|
|
|
12
13
|
|
|
13
14
|
## ✨ Features
|
|
14
15
|
|
|
15
|
-
-
|
|
16
|
+
- Server-Side Events (SSE) streaming over HTTP
|
|
17
|
+
- Real-time token streaming via SSE protocol
|
|
18
|
+
- SSE event types: `start`, `token`, `done`, `error`, `ping`
|
|
19
|
+
- Heartbeat pings to keep connection alive
|
|
16
20
|
- End-to-end cancellation support
|
|
17
21
|
- Daily token budget enforcement
|
|
18
22
|
- Rate limiting
|
|
@@ -31,6 +35,8 @@ npm install chat-nest-server
|
|
|
31
35
|
## 🚀 Usage
|
|
32
36
|
Express Integration
|
|
33
37
|
|
|
38
|
+
The handler automatically uses Server-Side Events (SSE) for streaming responses:
|
|
39
|
+
|
|
34
40
|
```
|
|
35
41
|
import express from "express";
|
|
36
42
|
import cors from "cors";
|
|
@@ -53,6 +59,13 @@ app.listen(3001, () => {
|
|
|
53
59
|
});
|
|
54
60
|
```
|
|
55
61
|
|
|
62
|
+
The handler sends SSE-formatted events:
|
|
63
|
+
- `event: start\ndata: \n\n` - Stream started
|
|
64
|
+
- `event: token\ndata: <token>\n\n` - Each token chunk
|
|
65
|
+
- `event: done\ndata: \n\n` - Stream completed
|
|
66
|
+
- `event: error\ndata: <error_json>\n\n` - Error occurred
|
|
67
|
+
- `event: ping\ndata: \n\n` - Heartbeat (every 15s)
|
|
68
|
+
|
|
56
69
|
---
|
|
57
70
|
|
|
58
71
|
## 🔐 Environment Variables
|
|
@@ -63,21 +76,27 @@ app.listen(3001, () => {
|
|
|
63
76
|
|
|
64
77
|
## 💰 Cost Controls
|
|
65
78
|
|
|
66
|
-
```
|
|
67
79
|
The server enforces:
|
|
68
80
|
|
|
69
|
-
Maximum tokens per request
|
|
81
|
+
- Maximum tokens per request
|
|
82
|
+
- Daily token budget
|
|
83
|
+
- Request rate limiting
|
|
84
|
+
- Prompt size trimming
|
|
85
|
+
- Retry classification
|
|
70
86
|
|
|
71
|
-
|
|
87
|
+
This prevents accidental overspending and abuse.
|
|
72
88
|
|
|
73
|
-
|
|
89
|
+
## 🔄 Server-Side Events (SSE)
|
|
74
90
|
|
|
75
|
-
|
|
91
|
+
This package uses SSE protocol for efficient streaming:
|
|
76
92
|
|
|
77
|
-
|
|
93
|
+
- **Content-Type**: `text/event-stream`
|
|
94
|
+
- **Connection**: `keep-alive`
|
|
95
|
+
- **Cache-Control**: `no-cache`
|
|
96
|
+
- **Heartbeat**: Ping every 15 seconds to keep connection alive
|
|
97
|
+
- **Event Format**: `event: <type>\ndata: <data>\n\n`
|
|
78
98
|
|
|
79
|
-
|
|
80
|
-
```
|
|
99
|
+
SSE provides better efficiency and real-time streaming compared to traditional polling or chunked responses.
|
|
81
100
|
|
|
82
101
|
---
|
|
83
102
|
|
package/dist/index.js
CHANGED
|
@@ -64,7 +64,19 @@ function createChatHandler(config) {
|
|
|
64
64
|
return async function handler(req, res) {
|
|
65
65
|
const abortController = new AbortController();
|
|
66
66
|
let streamStarted = false;
|
|
67
|
+
let heartbeatInterval = null;
|
|
68
|
+
const writeSSE = (event, data) => {
|
|
69
|
+
if (abortController.signal.aborted) return;
|
|
70
|
+
res.write(`event: ${event}
|
|
71
|
+
data: ${data}
|
|
72
|
+
|
|
73
|
+
`);
|
|
74
|
+
};
|
|
67
75
|
res.on("close", () => {
|
|
76
|
+
if (heartbeatInterval) {
|
|
77
|
+
clearInterval(heartbeatInterval);
|
|
78
|
+
heartbeatInterval = null;
|
|
79
|
+
}
|
|
68
80
|
if (streamStarted && !abortController.signal.aborted) {
|
|
69
81
|
abortController.abort();
|
|
70
82
|
}
|
|
@@ -90,8 +102,16 @@ function createChatHandler(config) {
|
|
|
90
102
|
});
|
|
91
103
|
return;
|
|
92
104
|
}
|
|
93
|
-
res.setHeader("Content-Type", "text/
|
|
94
|
-
res.setHeader("
|
|
105
|
+
res.setHeader("Content-Type", "text/event-stream");
|
|
106
|
+
res.setHeader("Cache-Control", "no-cache");
|
|
107
|
+
res.setHeader("Connection", "keep-alive");
|
|
108
|
+
res.flushHeaders();
|
|
109
|
+
writeSSE("start", "");
|
|
110
|
+
heartbeatInterval = setInterval(() => {
|
|
111
|
+
if (!abortController.signal.aborted) {
|
|
112
|
+
writeSSE("ping", "");
|
|
113
|
+
}
|
|
114
|
+
}, 15e3);
|
|
95
115
|
const stream = await client.chat.completions.create(
|
|
96
116
|
{
|
|
97
117
|
model: AI_MODEL,
|
|
@@ -115,25 +135,45 @@ function createChatHandler(config) {
|
|
|
115
135
|
}
|
|
116
136
|
const token = chunk.choices[0]?.delta?.content;
|
|
117
137
|
if (token) {
|
|
118
|
-
|
|
138
|
+
writeSSE("token", JSON.stringify(token));
|
|
119
139
|
}
|
|
120
140
|
}
|
|
141
|
+
writeSSE("done", "");
|
|
121
142
|
} catch (error) {
|
|
122
143
|
if (abortController.signal.aborted) {
|
|
123
144
|
console.log("Stream aborted by client");
|
|
124
145
|
} else {
|
|
125
|
-
|
|
146
|
+
const errorData = JSON.stringify({
|
|
147
|
+
message: error instanceof Error ? error.message : "Unknown error"
|
|
148
|
+
});
|
|
149
|
+
writeSSE("error", errorData);
|
|
126
150
|
}
|
|
127
151
|
} finally {
|
|
152
|
+
if (heartbeatInterval) {
|
|
153
|
+
clearInterval(heartbeatInterval);
|
|
154
|
+
heartbeatInterval = null;
|
|
155
|
+
}
|
|
128
156
|
recordTokenUsage(estimatedTotalTokens);
|
|
129
157
|
res.end();
|
|
130
158
|
}
|
|
131
159
|
} catch (error) {
|
|
132
160
|
if (error?.name === "AbortError") {
|
|
161
|
+
if (heartbeatInterval) {
|
|
162
|
+
clearInterval(heartbeatInterval);
|
|
163
|
+
heartbeatInterval = null;
|
|
164
|
+
}
|
|
133
165
|
return;
|
|
134
166
|
}
|
|
135
167
|
console.error("AI error:", error);
|
|
136
|
-
res.
|
|
168
|
+
if (!res.headersSent) {
|
|
169
|
+
res.status(500).json({ error: "AI request failed" });
|
|
170
|
+
} else {
|
|
171
|
+
const errorData = JSON.stringify({
|
|
172
|
+
message: "AI request failed"
|
|
173
|
+
});
|
|
174
|
+
writeSSE("error", errorData);
|
|
175
|
+
res.end();
|
|
176
|
+
}
|
|
137
177
|
}
|
|
138
178
|
};
|
|
139
179
|
}
|
package/package.json
CHANGED