llm-simple-router 0.9.25 → 0.9.27
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/dist/admin/upgrade.js +19 -5
- package/dist/index.js +11 -0
- package/dist/proxy/transport/stream.js +26 -9
- package/package.json +1 -1
package/dist/admin/upgrade.js
CHANGED
|
@@ -11,6 +11,8 @@ const GITHUB_CONFIG_BASE = 'https://raw.githubusercontent.com/zhushanwen321/llm-
|
|
|
11
11
|
const GITEE_CONFIG_BASE = 'https://gitee.com/zzzzswszzzz/llm-simple-router/raw/main/router/config';
|
|
12
12
|
const CHECK_INTERVAL_MS = 60 * 60 * 1000; // eslint-disable-line no-magic-numbers
|
|
13
13
|
const JSON_INDENT = 2;
|
|
14
|
+
const RESTART_FORCE_EXIT_MS = 3_000;
|
|
15
|
+
const RESTART_RESPONSE_FLUSH_MS = 300;
|
|
14
16
|
// 模块级单例:checker、configDir 和定时器
|
|
15
17
|
let checker = null;
|
|
16
18
|
let configDir = '';
|
|
@@ -87,13 +89,12 @@ export const adminUpgradeRoutes = (app, options, done) => {
|
|
|
87
89
|
// 先回复客户端,再执行重启(否则客户端收不到响应)
|
|
88
90
|
reply.send({ ok: true, method });
|
|
89
91
|
// 给响应发送窗口
|
|
90
|
-
await new Promise((resolve) => setTimeout(resolve,
|
|
92
|
+
await new Promise((resolve) => setTimeout(resolve, RESTART_RESPONSE_FLUSH_MS));
|
|
91
93
|
try {
|
|
92
94
|
req.log.info({ method, managed }, 'Restarting server...');
|
|
93
|
-
// 优雅关闭(释放端口、等待活跃请求完成)
|
|
94
|
-
await options.closeFn();
|
|
95
95
|
if (!managed) {
|
|
96
|
-
// 无进程管理器(npx / 手动 node
|
|
96
|
+
// 无进程管理器(npx / 手动 node):先 spawn 新进程,再关闭旧进程。
|
|
97
|
+
// 必须在 closeFn 之前 spawn,否则 closeFn 可能因活跃 SSE 连接卡住导致新进程永远不启动。
|
|
97
98
|
const binPath = resolveRestartBinPath();
|
|
98
99
|
const args = process.argv.slice(2); // eslint-disable-line no-magic-numbers
|
|
99
100
|
req.log.info({ binPath, args }, 'Spawning new process before exit');
|
|
@@ -102,15 +103,28 @@ export const adminUpgradeRoutes = (app, options, done) => {
|
|
|
102
103
|
stdio: 'ignore',
|
|
103
104
|
env: { ...process.env },
|
|
104
105
|
});
|
|
106
|
+
child.on('error', (err) => {
|
|
107
|
+
req.log.error({ err, binPath }, 'Failed to spawn new process');
|
|
108
|
+
});
|
|
105
109
|
child.unref();
|
|
106
110
|
}
|
|
111
|
+
// 强制退出兜底:即使 closeFn 卡住(如活跃代理 SSE 流),也能确保进程退出。
|
|
112
|
+
const forceExitTimer = setTimeout(() => {
|
|
113
|
+
req.log.warn('Graceful shutdown timed out during restart, forcing exit');
|
|
114
|
+
process.exit(0);
|
|
115
|
+
}, RESTART_FORCE_EXIT_MS);
|
|
116
|
+
forceExitTimer.unref();
|
|
117
|
+
// 尝试优雅关闭(closeFn 内部有 2s 优雅等待 + closeAllConnections 兜底)
|
|
118
|
+
await options.closeFn();
|
|
119
|
+
clearTimeout(forceExitTimer);
|
|
107
120
|
req.log.info('Exiting current process');
|
|
108
121
|
process.exit(0);
|
|
109
122
|
}
|
|
110
123
|
catch (err) {
|
|
111
|
-
//
|
|
124
|
+
// 优雅关闭失败时也必须退出:新进程已经 spawn,旧进程必须让出端口
|
|
112
125
|
const msg = err instanceof Error ? err.message : String(err);
|
|
113
126
|
req.log.error({ err }, `Restart failed: ${msg}`);
|
|
127
|
+
process.exit(1);
|
|
114
128
|
}
|
|
115
129
|
});
|
|
116
130
|
app.post('/admin/api/upgrade/sync-config', async (req, reply) => {
|
package/dist/index.js
CHANGED
|
@@ -294,7 +294,18 @@ export async function buildApp(options) {
|
|
|
294
294
|
proxyAgentFactory.invalidateAll();
|
|
295
295
|
const sessionTracker = container.resolve(SERVICE_KEYS.sessionTracker);
|
|
296
296
|
sessionTracker.stop();
|
|
297
|
+
// 等待活跃代理请求自然完成,超时后强制关闭所有连接。
|
|
298
|
+
// 先调用 app.close() 停止接受新连接并等待现有连接结束,
|
|
299
|
+
// 如果 2 秒内未完成则调用 closeAllConnections() 强制断开,防止 SSE 长连接导致无限等待。
|
|
300
|
+
const CLOSE_GRACE_PERIOD_MS = 2_000;
|
|
301
|
+
const forceClose = typeof app.server.closeAllConnections === 'function'
|
|
302
|
+
? setTimeout(() => app.server.closeAllConnections(), CLOSE_GRACE_PERIOD_MS)
|
|
303
|
+
: null;
|
|
304
|
+
if (forceClose)
|
|
305
|
+
forceClose.unref();
|
|
297
306
|
await app.close();
|
|
307
|
+
if (forceClose)
|
|
308
|
+
clearTimeout(forceClose);
|
|
298
309
|
db.close();
|
|
299
310
|
};
|
|
300
311
|
// 文件压缩和清理任务(仅非 :memory: 模式)
|
|
@@ -4,6 +4,7 @@ import { buildUpstreamUrl } from "../proxy-core.js";
|
|
|
4
4
|
import { _transportInternals, buildRequestOptions, } from "./http.js";
|
|
5
5
|
const UPSTREAM_BAD_GATEWAY = 502;
|
|
6
6
|
const BUFFER_SIZE_LIMIT = 4096;
|
|
7
|
+
const END_REPLY_TIMEOUT_MS = 1000;
|
|
7
8
|
class StreamProxy {
|
|
8
9
|
statusCode;
|
|
9
10
|
sentUpstreamHeaders;
|
|
@@ -278,18 +279,34 @@ class StreamProxy {
|
|
|
278
279
|
// 这保证了 inject() 返回时日志已经写入 DB。
|
|
279
280
|
const metrics = this.collectMetrics(true);
|
|
280
281
|
this.terminal("stream_success", { metrics }, true);
|
|
281
|
-
// 延迟结束管道和响应,属于 reply 层面操作,不属于 StreamProxy
|
|
282
|
+
// 延迟结束管道和响应,属于 reply 层面操作,不属于 StreamProxy 状态管理。
|
|
283
|
+
//
|
|
284
|
+
// 当 formatTransform 存在时(如 OpenAI→Anthropic 转换),Transform 链的 flush
|
|
285
|
+
// 通过 process.nextTick 异步传播。pipeEntry.end() 后不能同步调用 reply.raw.end(),
|
|
286
|
+
// 否则 formatTransform._flush() 中的 ensureTerminated()(发送 message_stop)
|
|
287
|
+
// 尚未执行,连接就被关闭,导致客户端收到 "stream ended before message_stop"。
|
|
282
288
|
setImmediate(() => {
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
289
|
+
const endReply = () => {
|
|
290
|
+
if (this.headersSent) {
|
|
291
|
+
try {
|
|
292
|
+
this.reply.raw.end();
|
|
293
|
+
}
|
|
294
|
+
catch { // eslint-disable-line taste/no-silent-catch
|
|
295
|
+
// reply 可能已 destroyed,安全忽略
|
|
296
|
+
}
|
|
290
297
|
}
|
|
298
|
+
this.cleanup();
|
|
299
|
+
};
|
|
300
|
+
this.pipeEntry.end();
|
|
301
|
+
if (this.formatTransform) {
|
|
302
|
+
// 等 passThrough end 事件触发(整个 transform 链 flush 完成后),再关闭 reply
|
|
303
|
+
this.passThrough.once("end", endReply);
|
|
304
|
+
// 安全超时兜底,防止 end 事件未触发导致连接挂起
|
|
305
|
+
setTimeout(endReply, END_REPLY_TIMEOUT_MS).unref();
|
|
306
|
+
}
|
|
307
|
+
else {
|
|
308
|
+
endReply();
|
|
291
309
|
}
|
|
292
|
-
this.cleanup();
|
|
293
310
|
});
|
|
294
311
|
}
|
|
295
312
|
onUpstreamError(err) {
|
package/package.json
CHANGED