coding-tool-x 3.9.4 → 3.9.5-261012

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.
@@ -19,14 +19,15 @@ const {
19
19
  const { persistProxyRequestSnapshot, persistClaudeRequestTemplate } = require('../../../server/services/request-logger');
20
20
  const { redirectModel, normalizeGatewaySourceType } = require('../../../shared/proxy-utils');
21
21
  const { handleClaudeOpenAiGatewayRequest } = require('./openai-gateway');
22
- const { attachServerShutdownHandling, expediteServerShutdown } = require('../../../server/services/server-shutdown');
22
+ const { bindProxyRequest, prepareProxyResponse } = require('../../../shared/proxy-transport');
23
+ const { attachServerShutdownHandling, expediteServerShutdown, PROXY_IDLE_TIMEOUT_MS } = require('../../../server/services/server-shutdown');
23
24
 
24
25
  let proxyServer = null;
25
26
  let proxyApp = null;
26
27
  let currentPort = null;
27
28
 
28
29
  // 用于存储每个请求的元数据(用于 WebSocket 日志)
29
- const requestMetadata = new Map();
30
+ let requestMetadata = new WeakMap();
30
31
 
31
32
  // 用于缓存已打印过的模型重定向规则,避免重复打印
32
33
  // 格式: { channelId: { "originalModel": "redirectedModel", ... } }
@@ -35,7 +36,7 @@ const printedRedirectCache = new Map();
35
36
  const jsonBodyParser = express.json({
36
37
  limit: '100mb',
37
38
  verify: (req, res, buf) => {
38
- req.rawBody = Buffer.from(buf);
39
+ req.rawBody = buf;
39
40
  }
40
41
  });
41
42
 
@@ -174,6 +175,7 @@ async function startProxyServer(options = {}) {
174
175
  const proxy = httpProxy.createProxyServer({});
175
176
 
176
177
  proxy.on('proxyReq', (proxyReq, req, res) => {
178
+ bindProxyRequest(proxyReq, res);
177
179
  const selectedChannel = req.selectedChannel;
178
180
  if (selectedChannel) {
179
181
  const requestId = `${Date.now()}-${Math.random()}`;
@@ -238,9 +240,10 @@ async function startProxyServer(options = {}) {
238
240
  };
239
241
 
240
242
  req.__releaseChannel = release;
241
-
243
+ res.on('finish', release);
242
244
  res.on('close', release);
243
245
  res.on('error', release);
246
+ if (res.destroyed || res.writableEnded) { release(); return; }
244
247
 
245
248
  const effectiveKey = getEffectiveApiKey(channel);
246
249
  if (!effectiveKey) {
@@ -306,8 +309,8 @@ async function startProxyServer(options = {}) {
306
309
  const proxyOptions = {
307
310
  target: channel.baseUrl,
308
311
  changeOrigin: true,
309
- proxyTimeout: 120000, // 代理连接超时 2 分钟
310
- timeout: 120000 // 请求超时 2 分钟
312
+ proxyTimeout: PROXY_IDLE_TIMEOUT_MS, // Allow long reasoning gaps without an unbounded upstream wait
313
+ timeout: 0 // The upstream timeout bounds idle waits; streaming clients may pause
311
314
  };
312
315
 
313
316
  if (channel.proxyUrl) {
@@ -329,6 +332,7 @@ async function startProxyServer(options = {}) {
329
332
  }
330
333
  });
331
334
  } catch (error) {
335
+ req.__releaseChannel?.();
332
336
  console.error('Channel allocation error:', error);
333
337
  if (!res.headersSent) {
334
338
  res.status(503).json({
@@ -341,6 +345,7 @@ async function startProxyServer(options = {}) {
341
345
 
342
346
  // 代理响应只负责维护传输健康状态;实时日志和 usage 来自 CLI 原生日志。
343
347
  proxy.on('proxyRes', (proxyRes, req, res) => {
348
+ prepareProxyResponse(proxyRes, res);
344
349
  const metadata = requestMetadata.get(req);
345
350
  if (!metadata) return;
346
351
 
@@ -425,9 +430,9 @@ async function stopProxyServer(options = {}) {
425
430
  return { success: true, message: 'Proxy server not running' };
426
431
  }
427
432
 
428
- requestMetadata.clear();
433
+ requestMetadata = new WeakMap();
429
434
 
430
- const shutdownTimer = expediteServerShutdown(proxyServer);
435
+ const shutdownTimer = expediteServerShutdown(proxyServer, { forceAfterMs: options.forceAfterMs });
431
436
 
432
437
  return new Promise((resolve) => {
433
438
  proxyServer.close(() => {
@@ -15,14 +15,15 @@ const {
15
15
  isChatCompletionsPath,
16
16
  ensureOpenAiStreamUsage
17
17
  } = require('../../../shared/proxy-utils');
18
- const { attachServerShutdownHandling, expediteServerShutdown } = require('../../../server/services/server-shutdown');
18
+ const { bindProxyRequest, prepareProxyResponse } = require('../../../shared/proxy-transport');
19
+ const { attachServerShutdownHandling, expediteServerShutdown, PROXY_IDLE_TIMEOUT_MS } = require('../../../server/services/server-shutdown');
19
20
 
20
21
  let proxyServer = null;
21
22
  let proxyApp = null;
22
23
  let currentPort = null;
23
24
 
24
25
  // 用于存储每个请求的元数据
25
- const requestMetadata = new Map();
26
+ let requestMetadata = new WeakMap();
26
27
 
27
28
  // 用于缓存已打印过的模型重定向规则,避免重复打印
28
29
  // 格式: { channelId: { "originalModel": "redirectedModel", ... } }
@@ -37,7 +38,7 @@ const resolveCodexTarget = resolveTargetUrl;
37
38
  const jsonBodyParser = express.json({
38
39
  limit: '100mb',
39
40
  verify: (req, res, buf) => {
40
- req.rawBody = Buffer.from(buf);
41
+ req.rawBody = buf;
41
42
  }
42
43
  });
43
44
 
@@ -77,7 +78,8 @@ async function startCodexProxyServer(options = {}) {
77
78
 
78
79
  const proxy = httpProxy.createProxyServer({});
79
80
 
80
- proxy.on('proxyReq', (proxyReq, req) => {
81
+ proxy.on('proxyReq', (proxyReq, req, res) => {
82
+ bindProxyRequest(proxyReq, res);
81
83
  const activeChannel = req.selectedChannel;
82
84
  if (!activeChannel) return;
83
85
 
@@ -127,8 +129,11 @@ async function startCodexProxyServer(options = {}) {
127
129
  };
128
130
  })();
129
131
 
132
+ req.__releaseChannel = release;
133
+ res.on('finish', release);
130
134
  res.on('close', release);
131
135
  res.on('error', release);
136
+ if (res.destroyed || res.writableEnded) { release(); return; }
132
137
 
133
138
  broadcastSchedulerState('codex', getSchedulerState('codex'));
134
139
 
@@ -200,8 +205,8 @@ async function startCodexProxyServer(options = {}) {
200
205
  proxy.web(req, res, {
201
206
  target,
202
207
  changeOrigin: true,
203
- proxyTimeout: 120000, // 代理连接超时 2 分钟
204
- timeout: 120000 // 请求超时 2 分钟
208
+ proxyTimeout: PROXY_IDLE_TIMEOUT_MS, // Allow long reasoning gaps without an unbounded upstream wait
209
+ timeout: 0 // The upstream timeout bounds idle waits; streaming clients may pause
205
210
  }, (err) => {
206
211
  release();
207
212
  if (err) {
@@ -218,6 +223,7 @@ async function startCodexProxyServer(options = {}) {
218
223
  }
219
224
  });
220
225
  } catch (error) {
226
+ req.__releaseChannel?.();
221
227
  console.error('Codex channel allocation error:', error);
222
228
  if (!res.headersSent) {
223
229
  res.status(503).json({
@@ -232,6 +238,7 @@ async function startCodexProxyServer(options = {}) {
232
238
 
233
239
  // 代理响应只负责维护传输健康状态;实时日志和 usage 来自 CLI 原生日志。
234
240
  proxy.on('proxyRes', (proxyRes, req, res) => {
241
+ prepareProxyResponse(proxyRes, res);
235
242
  const metadata = requestMetadata.get(req);
236
243
  if (!metadata) return;
237
244
 
@@ -277,8 +284,7 @@ async function startCodexProxyServer(options = {}) {
277
284
  console.error('Codex proxy error:', err);
278
285
  if (req && req.selectedChannel) {
279
286
  recordFailure(req.selectedChannel.id, 'codex', err);
280
- releaseChannel(req.selectedChannel.id, 'codex');
281
- broadcastSchedulerState('codex', getSchedulerState('codex'));
287
+ req.__releaseChannel?.();
282
288
  }
283
289
  if (res && !res.headersSent) {
284
290
  res.status(502).json({
@@ -331,9 +337,9 @@ async function stopCodexProxyServer(options = {}) {
331
337
  return { success: true, message: 'Codex proxy server not running' };
332
338
  }
333
339
 
334
- requestMetadata.clear();
340
+ requestMetadata = new WeakMap();
335
341
 
336
- const shutdownTimer = expediteServerShutdown(proxyServer);
342
+ const shutdownTimer = expediteServerShutdown(proxyServer, { forceAfterMs: options.forceAfterMs });
337
343
 
338
344
  return new Promise((resolve) => {
339
345
  proxyServer.close(() => {
@@ -9,6 +9,7 @@ const { installPlugin, uninstallPlugin, updatePlugin } = require('./plugin-manag
9
9
  const { createDriver: createChannelsDriver } = require('./channels');
10
10
  const { createDriver: createProxyDriver } = require('./proxy');
11
11
  const { createDriver: createStatisticsDriver } = require('./statistics');
12
+ const { importMcpServer } = require('../../../shared/mcp-import');
12
13
 
13
14
  function requiredId(value, label) {
14
15
  const id = String(value || '').trim();
@@ -153,18 +154,34 @@ function dshMcpSpecToConfig(spec = {}, serverName) {
153
154
  return config;
154
155
  }
155
156
 
156
- function readDshMcpEntries(context = {}) {
157
+ function dshMcpServerName(server, fallbackId) {
158
+ const requested = String(server?.name || '').trim();
159
+ if (/^[A-Za-z0-9_-]{1,32}$/.test(requested)) return requested;
160
+ return String(fallbackId || '')
161
+ .replace(/[^A-Za-z0-9_-]/g, '_')
162
+ .slice(0, 32) || 'mcp';
163
+ }
164
+
165
+ function readDshMcpRecords(context = {}) {
157
166
  const profile = resolveProfileName(context);
158
- if (!profile) return {};
167
+ if (!profile) return [];
159
168
  const result = listProfileMcp(context, profile);
160
- const entries = {};
169
+ const entries = [];
161
170
  for (const server of result?.servers || []) {
162
171
  if (!server?.id || server.disabled) continue;
163
- entries[server.id] = dshMcpConfigToSpec(server.config || {});
172
+ entries.push({
173
+ id: server.id,
174
+ name: server.config?.serverName || server.id,
175
+ spec: dshMcpConfigToSpec(server.config || {})
176
+ });
164
177
  }
165
178
  return entries;
166
179
  }
167
180
 
181
+ function readDshMcpEntries(context = {}) {
182
+ return Object.fromEntries(readDshMcpRecords(context).map(({ id, spec }) => [id, spec]));
183
+ }
184
+
168
185
  function promptContentFromDshConfig(config = {}) {
169
186
  for (const key of ['content', 'prompt', 'text']) {
170
187
  if (typeof config[key] === 'string') return config[key];
@@ -359,47 +376,33 @@ function createDriver(context = {}) {
359
376
  driver.read = () => resultFor(mcpContext('read'), 'ok', { mcpServers: readDshMcpEntries(context) });
360
377
  driver.normalize = spec => resultFor(mcpContext('normalize'), 'ok', spec);
361
378
  driver.sync = async server => {
362
- const profileName = profileNameForRequest(context, server);
363
- const id = String(server?.id || '').trim();
364
- if (!id) throw new Error('MCP server id is required');
365
- const patch = requirePatchResult(
366
- upsertProfilePatchRow(context, profileName, {
367
- id,
368
- name: '@deepseek-ai/dsh-mcp-client',
369
- config: dshMcpSpecToConfig(server.server || {}, id)
370
- }),
371
- profileName
372
- );
373
- return resultFor(mcpContext('sync'), 'ok', patch);
374
- };
375
- driver.remove = async serverId => {
376
- const profileName = profileNameForRequest(context);
377
- const id = String(serverId || '').trim();
378
- if (!id) throw new Error('MCP server id is required');
379
- if (!profileName) return resultFor(mcpContext('remove'), 'ok', true);
380
- const patch = requirePatchResult(deleteProfilePatchRow(context, profileName, id), profileName);
381
- return resultFor(mcpContext('remove'), 'ok', patch);
382
- };
379
+ const profileName = profileNameForRequest(context, server);
380
+ const id = String(server?.id || '').trim();
381
+ if (!id) throw new Error('MCP server id is required');
382
+ const patch = requirePatchResult(
383
+ upsertProfilePatchRow(context, profileName, {
384
+ id,
385
+ name: '@deepseek-ai/dsh-mcp-client',
386
+ config: dshMcpSpecToConfig(server.server || {}, dshMcpServerName(server, id))
387
+ }),
388
+ profileName
389
+ );
390
+ return resultFor(mcpContext('sync'), 'ok', patch);
391
+ };
392
+ driver.remove = async serverId => {
393
+ const profileName = profileNameForRequest(context);
394
+ const id = String(serverId || '').trim();
395
+ if (!id) throw new Error('MCP server id is required');
396
+ if (!profileName) return resultFor(mcpContext('remove'), 'ok', true);
397
+ const patch = requirePatchResult(deleteProfilePatchRow(context, profileName, id), profileName);
398
+ return resultFor(mcpContext('remove'), 'ok', patch);
399
+ };
383
400
  driver.import = async servers => {
384
- const entries = readDshMcpEntries(context);
385
- let count = 0;
386
- for (const [id, spec] of Object.entries(entries)) {
387
- if (servers[id]) {
388
- servers[id].apps = { ...(servers[id].apps || {}), dsh: true };
389
- continue;
401
+ let count = 0;
402
+ for (const entry of readDshMcpRecords(context)) {
403
+ if (importMcpServer(servers, { ...entry, platform: 'dsh' })) count++;
390
404
  }
391
- const now = Date.now();
392
- servers[id] = {
393
- id,
394
- name: id,
395
- server: spec,
396
- apps: { dsh: true },
397
- createdAt: now,
398
- updatedAt: now
399
- };
400
- count++;
401
- }
402
- return resultFor(mcpContext('import'), 'ok', count);
405
+ return resultFor(mcpContext('import'), 'ok', count);
403
406
  };
404
407
  }
405
408
 
@@ -20,16 +20,17 @@ const {
20
20
  const { persistProxyRequestSnapshot } = require('../../../server/services/request-logger');
21
21
  const { redirectModel, resolveTargetUrl, isChatCompletionsPath, ensureOpenAiStreamUsage } = require('../../../shared/proxy-utils');
22
22
  const { saveProxyStartTime, clearProxyStartTime, getProxyStartTime, getProxyRuntime } = require('../../../server/services/proxy-runtime');
23
- const { attachServerShutdownHandling, expediteServerShutdown } = require('../../../server/services/server-shutdown');
23
+ const { bindProxyRequest, prepareProxyResponse } = require('../../../shared/proxy-transport');
24
+ const { attachServerShutdownHandling, expediteServerShutdown, PROXY_IDLE_TIMEOUT_MS } = require('../../../server/services/server-shutdown');
24
25
 
25
26
  let proxyServer = null;
26
27
  let currentPort = null;
27
- const requestMetadata = new Map();
28
+ let requestMetadata = new WeakMap();
28
29
 
29
30
  const jsonBodyParser = express.json({
30
31
  limit: '100mb',
31
32
  verify: (req, _res, buffer) => {
32
- req.rawBody = Buffer.from(buffer);
33
+ req.rawBody = buffer;
33
34
  }
34
35
  });
35
36
 
@@ -84,8 +85,11 @@ async function startDshProxyServer(options = {}) {
84
85
  });
85
86
  req.selectedChannel = channel;
86
87
  release = releaseOnce(channel);
88
+ req.__releaseChannel = release;
89
+ res.on('finish', release);
87
90
  res.on('close', release);
88
91
  res.on('error', release);
92
+ if (res.destroyed || res.writableEnded) { release(); return; }
89
93
  broadcastSchedulerState('dsh', getSchedulerState('dsh'));
90
94
 
91
95
  const apiKey = getEffectiveApiKey(channel);
@@ -119,8 +123,8 @@ async function startDshProxyServer(options = {}) {
119
123
  proxy.web(req, res, {
120
124
  target: resolveTargetUrl(channel.baseUrl, req.url),
121
125
  changeOrigin: true,
122
- proxyTimeout: 120000,
123
- timeout: 120000
126
+ proxyTimeout: PROXY_IDLE_TIMEOUT_MS,
127
+ timeout: 0
124
128
  }, error => {
125
129
  release?.();
126
130
  if (!error) return;
@@ -137,7 +141,8 @@ async function startDshProxyServer(options = {}) {
137
141
  }
138
142
  });
139
143
 
140
- proxy.on('proxyReq', (proxyReq, req) => {
144
+ proxy.on('proxyReq', (proxyReq, req, res) => {
145
+ bindProxyRequest(proxyReq, res);
141
146
  const channel = req.selectedChannel;
142
147
  if (!channel) return;
143
148
  const metadata = {
@@ -161,22 +166,32 @@ async function startDshProxyServer(options = {}) {
161
166
  });
162
167
 
163
168
  proxy.on('proxyRes', (proxyRes, req, res) => {
169
+ prepareProxyResponse(proxyRes, res);
164
170
  const metadata = requestMetadata.get(req);
165
171
  if (!metadata) return;
166
- if (Number(proxyRes.statusCode) >= 400) {
167
- recordFailure(metadata.channelId, 'dsh', new Error(`DSH upstream HTTP ${proxyRes.statusCode}`));
168
- } else {
169
- recordSuccess(metadata.channelId, 'dsh');
170
- }
171
- requestMetadata.delete(req);
172
- res?.on('close', () => requestMetadata.delete(req));
172
+ let completed = false;
173
+ const finish = error => {
174
+ if (completed) return;
175
+ completed = true;
176
+ requestMetadata.delete(req);
177
+ if (error || Number(proxyRes.statusCode) >= 400) {
178
+ recordFailure(metadata.channelId, 'dsh', error || new Error(`DSH upstream HTTP ${proxyRes.statusCode}`));
179
+ } else {
180
+ recordSuccess(metadata.channelId, 'dsh');
181
+ }
182
+ };
183
+ proxyRes.once('end', () => finish());
184
+ proxyRes.once('error', finish);
185
+ proxyRes.once('close', () => {
186
+ if (!completed) finish(new Error('DSH upstream response closed early'));
187
+ });
188
+ res?.once('close', () => requestMetadata.delete(req));
173
189
  });
174
190
 
175
191
  proxy.on('error', (error, req, res) => {
176
192
  if (req?.selectedChannel) {
177
193
  recordFailure(req.selectedChannel.id, 'dsh', error);
178
- releaseChannel(req.selectedChannel.id, 'dsh');
179
- broadcastSchedulerState('dsh', getSchedulerState('dsh'));
194
+ req.__releaseChannel?.();
180
195
  }
181
196
  if (res && !res.headersSent) res.status(502).json({ error: { message: `Proxy error: ${error.message}`, type: 'proxy_error' } });
182
197
  });
@@ -213,10 +228,10 @@ async function startDshProxyServer(options = {}) {
213
228
  async function stopDshProxyServer(options = {}) {
214
229
  const clearStartTime = options.clearStartTime !== false;
215
230
  const stoppedPort = currentPort;
216
- requestMetadata.clear();
231
+ requestMetadata = new WeakMap();
217
232
 
218
233
  if (proxyServer) {
219
- const shutdownTimer = expediteServerShutdown(proxyServer);
234
+ const shutdownTimer = expediteServerShutdown(proxyServer, { forceAfterMs: options.forceAfterMs });
220
235
  await new Promise(resolve => proxyServer.close(resolve));
221
236
  if (shutdownTimer) clearTimeout(shutdownTimer);
222
237
  proxyServer = null;
@@ -13,14 +13,15 @@ const {
13
13
  } = require('./channels-implementation');
14
14
  const { persistProxyRequestSnapshot } = require('../../../server/services/request-logger');
15
15
  const { redirectModel: redirectModelBase, resolveTargetUrl } = require('../../../shared/proxy-utils');
16
- const { attachServerShutdownHandling, expediteServerShutdown } = require('../../../server/services/server-shutdown');
16
+ const { bindProxyRequest, prepareProxyResponse } = require('../../../shared/proxy-transport');
17
+ const { attachServerShutdownHandling, expediteServerShutdown, PROXY_IDLE_TIMEOUT_MS } = require('../../../server/services/server-shutdown');
17
18
 
18
19
  let proxyServer = null;
19
20
  let proxyApp = null;
20
21
  let currentPort = null;
21
22
 
22
23
  // 用于存储每个请求的元数据
23
- const requestMetadata = new Map();
24
+ let requestMetadata = new WeakMap();
24
25
 
25
26
  // 用于缓存已打印过的模型重定向规则,避免重复打印
26
27
  // 格式: { channelId: { "originalModel": "redirectedModel", ... } }
@@ -30,7 +31,7 @@ const printedGeminiRedirectCache = new Map();
30
31
  const jsonBodyParser = express.json({
31
32
  limit: '100mb',
32
33
  verify: (req, res, buf) => {
33
- req.rawBody = Buffer.from(buf);
34
+ req.rawBody = buf;
34
35
  }
35
36
  });
36
37
 
@@ -198,7 +199,8 @@ async function startGeminiProxyServer(options = {}) {
198
199
 
199
200
  const proxy = httpProxy.createProxyServer({});
200
201
 
201
- proxy.on('proxyReq', (proxyReq, req) => {
202
+ proxy.on('proxyReq', (proxyReq, req, res) => {
203
+ bindProxyRequest(proxyReq, res);
202
204
  const activeChannel = req.selectedChannel;
203
205
  if (!activeChannel) return;
204
206
 
@@ -255,8 +257,11 @@ async function startGeminiProxyServer(options = {}) {
255
257
  };
256
258
  })();
257
259
 
260
+ req.__releaseChannel = release;
261
+ res.on('finish', release);
258
262
  res.on('close', release);
259
263
  res.on('error', release);
264
+ if (res.destroyed || res.writableEnded) { release(); return; }
260
265
 
261
266
  broadcastSchedulerState('gemini', getSchedulerState('gemini'));
262
267
 
@@ -335,8 +340,8 @@ async function startGeminiProxyServer(options = {}) {
335
340
  proxy.web(req, res, {
336
341
  target,
337
342
  changeOrigin: true,
338
- proxyTimeout: 120000, // 代理连接超时 2 分钟
339
- timeout: 120000 // 请求超时 2 分钟
343
+ proxyTimeout: PROXY_IDLE_TIMEOUT_MS, // Allow long reasoning gaps without an unbounded upstream wait
344
+ timeout: 0 // The upstream timeout bounds idle waits; streaming clients may pause
340
345
  }, (err) => {
341
346
  release();
342
347
  if (err) {
@@ -353,6 +358,7 @@ async function startGeminiProxyServer(options = {}) {
353
358
  }
354
359
  });
355
360
  } catch (error) {
361
+ req.__releaseChannel?.();
356
362
  console.error('Gemini channel allocation error:', error);
357
363
  if (!res.headersSent) {
358
364
  res.status(503).json({
@@ -367,6 +373,7 @@ async function startGeminiProxyServer(options = {}) {
367
373
 
368
374
  // 代理响应只负责维护传输健康状态;实时日志和 usage 来自 CLI 原生日志。
369
375
  proxy.on('proxyRes', (proxyRes, req, res) => {
376
+ prepareProxyResponse(proxyRes, res);
370
377
  const metadata = requestMetadata.get(req);
371
378
  if (!metadata) return;
372
379
 
@@ -403,8 +410,7 @@ async function startGeminiProxyServer(options = {}) {
403
410
  console.error('Gemini proxy error:', err);
404
411
  if (req && req.selectedChannel) {
405
412
  recordFailure(req.selectedChannel.id, 'gemini', err);
406
- releaseChannel(req.selectedChannel.id, 'gemini');
407
- broadcastSchedulerState('gemini', getSchedulerState('gemini'));
413
+ req.__releaseChannel?.();
408
414
  }
409
415
  if (res && !res.headersSent) {
410
416
  res.status(502).json({
@@ -457,9 +463,9 @@ async function stopGeminiProxyServer(options = {}) {
457
463
  return { success: true, message: 'Gemini proxy server not running' };
458
464
  }
459
465
 
460
- requestMetadata.clear();
466
+ requestMetadata = new WeakMap();
461
467
 
462
- const shutdownTimer = expediteServerShutdown(proxyServer);
468
+ const shutdownTimer = expediteServerShutdown(proxyServer, { forceAfterMs: options.forceAfterMs });
463
469
 
464
470
  return new Promise((resolve) => {
465
471
  proxyServer.close(() => {
@@ -1,6 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  const crypto = require('crypto');
4
+ const { downstreamHeaders } = require('../../../shared/proxy-transport');
4
5
  const http = require('http');
5
6
  const https = require('https');
6
7
  const httpProxy = require('http-proxy');
@@ -118,12 +119,20 @@ function readRequestBody(req) {
118
119
  const chunks = [];
119
120
  let size = 0;
120
121
  let settled = false;
122
+ const cleanup = () => {
123
+ chunks.length = 0;
124
+ req.removeListener('data', onData);
125
+ req.removeListener('end', onEnd);
126
+ req.removeListener('error', fail);
127
+ req.removeListener('aborted', onAborted);
128
+ };
121
129
  const fail = (error) => {
122
130
  if (settled) return;
123
131
  settled = true;
132
+ cleanup();
124
133
  reject(error);
125
134
  };
126
- req.on('data', (chunk) => {
135
+ const onData = (chunk) => {
127
136
  if (settled) return;
128
137
  size += chunk.length;
129
138
  if (size > MAX_REQUEST_BYTES) {
@@ -132,14 +141,19 @@ function readRequestBody(req) {
132
141
  return;
133
142
  }
134
143
  chunks.push(chunk);
135
- });
136
- req.on('end', () => {
144
+ };
145
+ const onEnd = () => {
137
146
  if (settled) return;
138
147
  settled = true;
139
- resolve(Buffer.concat(chunks));
140
- });
148
+ const body = Buffer.concat(chunks, size);
149
+ cleanup();
150
+ resolve(body);
151
+ };
152
+ const onAborted = () => fail(new Error('Client aborted request'));
153
+ req.on('data', onData);
154
+ req.on('end', onEnd);
141
155
  req.on('error', fail);
142
- req.on('aborted', () => fail(new Error('Client aborted request')));
156
+ req.on('aborted', onAborted);
143
157
  });
144
158
  }
145
159
 
@@ -184,6 +198,7 @@ function writeJson(res, statusCode, payload) {
184
198
  res.end(body);
185
199
  }
186
200
 
201
+
187
202
  function redactSensitiveQuery(path) {
188
203
  const url = new URL(String(path || '/'), 'http://127.0.0.1');
189
204
  for (const key of [...url.searchParams.keys()]) {
@@ -330,8 +345,13 @@ function createOmpGateway(options = {}) {
330
345
  upstreamResponse = response;
331
346
  activeUpstreams.delete(upstreamRequest);
332
347
  activeUpstreams.add(response);
348
+ // Install error handling before resolving: discarded retry responses
349
+ // must never raise an unhandled stream error and terminate the daemon.
350
+ response.on('error', () => {});
333
351
  response.once('close', () => {
352
+ activeUpstreams.delete(response);
334
353
  signal?.removeEventListener('abort', abortUpstream);
354
+ releaseOnce();
335
355
  });
336
356
  const statusCode = response.statusCode || 502;
337
357
  resolve({
@@ -387,8 +407,7 @@ function createOmpGateway(options = {}) {
387
407
  try {
388
408
  const result = await forwardAttempt(req, route, channel, body, signal);
389
409
  if (attempt === 0 && RETRYABLE_STATUS.has(result.statusCode)) {
390
- result.response.once('close', () => activeUpstreams.delete(result.response));
391
- result.response.resume();
410
+ result.response.destroy();
392
411
  result.release();
393
412
  onFailure(channel.id, 'omp');
394
413
  lastError = Object.assign(new Error(`OMP upstream returned HTTP ${result.statusCode}`), {
@@ -397,7 +416,7 @@ function createOmpGateway(options = {}) {
397
416
  continue;
398
417
  }
399
418
 
400
- res.writeHead(result.statusCode, result.response.headers);
419
+ res.writeHead(result.statusCode, downstreamHeaders(result.response.headers));
401
420
  await new Promise((resolve) => {
402
421
  let completed = false;
403
422
  const finish = (error) => {
@@ -413,7 +432,12 @@ function createOmpGateway(options = {}) {
413
432
  resolve();
414
433
  };
415
434
  result.response.once('end', () => finish());
416
- result.response.once('close', () => finish(new Error('OMP upstream response closed early')));
435
+ result.response.once('close', () => {
436
+ if (completed) return;
437
+ const error = new Error('OMP upstream response closed early');
438
+ if (!res.destroyed) res.destroy(error);
439
+ finish(error);
440
+ });
417
441
  result.response.once('error', (error) => {
418
442
  if (!res.destroyed) res.destroy(error);
419
443
  finish(error);
@@ -640,7 +664,7 @@ function createOmpGateway(options = {}) {
640
664
  handleRequest(req, res).catch((error) => {
641
665
  writeJson(res, 500, { error: error.message || 'OMP gateway request failed' });
642
666
  });
643
- }));
667
+ }), { drainOnExit: false });
644
668
  server.on('upgrade', (req, socket, head) => {
645
669
  handleUpgrade(req, socket, head).catch((error) => {
646
670
  rejectUpgrade(socket, 503, error.message || 'OMP websocket proxy failed');