coding-tool-x 3.7.10 → 3.7.11
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/web/assets/{Analytics-CkUtnoz9.js → Analytics-Ck_00Brj.js} +1 -1
- package/dist/web/assets/{ConfigTemplates-BwPqhE-N.js → ConfigTemplates-Cpw2vUa8.js} +1 -1
- package/dist/web/assets/Home-Bft3Kztl.js +1 -0
- package/dist/web/assets/Home-CHZJkkjH.css +1 -0
- package/dist/web/assets/{PluginManager-JMXWl37b.js → PluginManager-2RlvEJCT.js} +1 -1
- package/dist/web/assets/{ProjectList-CZwDgVvt.js → ProjectList-_ecCngpl.js} +1 -1
- package/dist/web/assets/{SessionList-2fZmN7Au.js → SessionList-DlWcw3hO.js} +1 -1
- package/dist/web/assets/{SkillManager-Bhzhkeis.js → SkillManager-CnY7IXkX.js} +1 -1
- package/dist/web/assets/{WorkspaceManager-B3lhhBD0.js → WorkspaceManager-3mqghi09.js} +1 -1
- package/dist/web/assets/{index-C5nLTbOk.css → index-BceXgSSe.css} +1 -1
- package/dist/web/assets/index-k5oP7c-A.js +2 -0
- package/dist/web/index.html +2 -2
- package/package.json +1 -1
- package/src/commands/toggle-proxy.js +7 -8
- package/src/commands/ui.js +11 -1
- package/src/config/default.js +1 -1
- package/src/index.js +41 -15
- package/src/server/api/omp-channels.js +6 -4
- package/src/server/api/omp-config.js +1 -1
- package/src/server/api/omp-proxy.js +13 -42
- package/src/server/index.js +3 -3
- package/src/server/omp-proxy-server.js +129 -10
- package/src/server/services/channel-scheduler.js +59 -9
- package/src/server/services/omp-auth-gateway-client.js +62 -0
- package/src/server/services/omp-channels.js +109 -12
- package/src/server/services/omp-gateway-routing.js +295 -0
- package/src/server/services/omp-gateway.js +919 -0
- package/src/server/services/omp-session-log-observer.js +138 -0
- package/src/server/services/omp-sessions.js +80 -0
- package/src/server/services/omp-settings-manager.js +335 -86
- package/dist/web/assets/Home-BANl91zG.js +0 -1
- package/dist/web/assets/Home-CqnBXFxj.css +0 -1
- package/dist/web/assets/index-LYvv7wtx.js +0 -2
|
@@ -0,0 +1,919 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const crypto = require('crypto');
|
|
4
|
+
const http = require('http');
|
|
5
|
+
const https = require('https');
|
|
6
|
+
const httpProxy = require('http-proxy');
|
|
7
|
+
const { URL } = require('url');
|
|
8
|
+
const { allocateChannel, releaseChannel } = require('./channel-scheduler');
|
|
9
|
+
const { recordSuccess, recordFailure } = require('./channel-health');
|
|
10
|
+
const { recordRequest: recordOmpRequest } = require('./omp-statistics-service');
|
|
11
|
+
const {
|
|
12
|
+
createTokenData,
|
|
13
|
+
mergeUsageIntoTokenData,
|
|
14
|
+
parseNonStreamingUsage,
|
|
15
|
+
parseSSEEventText,
|
|
16
|
+
parseSSEUsage,
|
|
17
|
+
splitSSEEvents
|
|
18
|
+
} = require('./base/response-usage-parser');
|
|
19
|
+
const {
|
|
20
|
+
publishFailureLog: publishSharedFailureLog,
|
|
21
|
+
publishUsageLog: publishSharedUsageLog
|
|
22
|
+
} = require('./proxy-log-helper');
|
|
23
|
+
const { attachServerShutdownHandling, expediteServerShutdown } = require('./server-shutdown');
|
|
24
|
+
const { resolveOmpGatewayRoute } = require('./omp-gateway-routing');
|
|
25
|
+
const { broadcastLog } = require('../websocket-server');
|
|
26
|
+
|
|
27
|
+
const RETRYABLE_STATUS = new Set([401, 403, 429, 502, 503, 504]);
|
|
28
|
+
const MAX_REQUEST_BYTES = 100 * 1024 * 1024;
|
|
29
|
+
|
|
30
|
+
function createAbortError(message = 'OMP downstream client disconnected') {
|
|
31
|
+
return Object.assign(new Error(message), { name: 'AbortError' });
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function normalizeHeaderMap(value) {
|
|
35
|
+
return value && typeof value === 'object' && !Array.isArray(value) ? value : {};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function collectChannelHeaders(channel = {}) {
|
|
39
|
+
return {
|
|
40
|
+
...normalizeHeaderMap(channel.providerConfig?.headers),
|
|
41
|
+
...normalizeHeaderMap(channel.headers)
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function buildUpstreamRequestPath(baseUrl, requestUrl) {
|
|
46
|
+
const base = new URL(baseUrl);
|
|
47
|
+
const request = new URL(requestUrl, 'http://127.0.0.1');
|
|
48
|
+
const basePath = base.pathname === '/' ? '' : base.pathname.replace(/\/+$/, '');
|
|
49
|
+
const requestedPath = request.pathname || '/';
|
|
50
|
+
const pathAlreadyIncludesBase = basePath
|
|
51
|
+
&& (requestedPath === basePath || requestedPath.startsWith(`${basePath}/`));
|
|
52
|
+
const pathname = pathAlreadyIncludesBase
|
|
53
|
+
? requestedPath
|
|
54
|
+
: `${basePath}/${requestedPath.replace(/^\/+/, '')}` || '/';
|
|
55
|
+
const search = new URLSearchParams(base.searchParams);
|
|
56
|
+
request.searchParams.forEach((value, name) => search.set(name, value));
|
|
57
|
+
const query = search.toString();
|
|
58
|
+
return `${pathname.startsWith('/') ? pathname : `/${pathname}`}${query ? `?${query}` : ''}`;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function rewriteCredential(headers, requestUrl, route, channel, options = {}) {
|
|
62
|
+
const next = { ...headers };
|
|
63
|
+
delete next.host;
|
|
64
|
+
delete next['content-length'];
|
|
65
|
+
delete next['accept-encoding'];
|
|
66
|
+
if (!options.preserveConnection) {
|
|
67
|
+
delete next.connection;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const authHeaders = ['authorization', 'x-api-key', 'x-goog-api-key', 'api-key'];
|
|
71
|
+
const upstreamKey = String(channel.apiKey || '');
|
|
72
|
+
for (const name of authHeaders) {
|
|
73
|
+
delete next[name];
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const url = new URL(requestUrl, 'http://127.0.0.1');
|
|
77
|
+
for (const name of ['key', 'api_key', 'access_token', 'token']) {
|
|
78
|
+
url.searchParams.delete(name);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (channel.authMode !== 'none' && upstreamKey) {
|
|
82
|
+
if (route.authMode === 'oauth') {
|
|
83
|
+
next.authorization = `Bearer ${upstreamKey}`;
|
|
84
|
+
} else if (route.providerApi === 'anthropic-messages') {
|
|
85
|
+
next['x-api-key'] = upstreamKey;
|
|
86
|
+
} else if (route.providerApi === 'azure-openai-responses') {
|
|
87
|
+
next['api-key'] = upstreamKey;
|
|
88
|
+
} else if (route.providerApi.startsWith('google-')) {
|
|
89
|
+
next['x-goog-api-key'] = upstreamKey;
|
|
90
|
+
} else {
|
|
91
|
+
next.authorization = `Bearer ${upstreamKey}`;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
Object.entries(collectChannelHeaders(channel)).forEach(([name, value]) => {
|
|
96
|
+
next[String(name).toLowerCase()] = String(value);
|
|
97
|
+
});
|
|
98
|
+
return { headers: next, path: `${url.pathname}${url.search}` };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function readRequestBody(req) {
|
|
102
|
+
return new Promise((resolve, reject) => {
|
|
103
|
+
const chunks = [];
|
|
104
|
+
let size = 0;
|
|
105
|
+
let settled = false;
|
|
106
|
+
const fail = (error) => {
|
|
107
|
+
if (settled) return;
|
|
108
|
+
settled = true;
|
|
109
|
+
reject(error);
|
|
110
|
+
};
|
|
111
|
+
req.on('data', (chunk) => {
|
|
112
|
+
if (settled) return;
|
|
113
|
+
size += chunk.length;
|
|
114
|
+
if (size > MAX_REQUEST_BYTES) {
|
|
115
|
+
fail(Object.assign(new Error('OMP gateway request body is too large'), { statusCode: 413 }));
|
|
116
|
+
req.destroy();
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
chunks.push(chunk);
|
|
120
|
+
});
|
|
121
|
+
req.on('end', () => {
|
|
122
|
+
if (settled) return;
|
|
123
|
+
settled = true;
|
|
124
|
+
resolve(Buffer.concat(chunks));
|
|
125
|
+
});
|
|
126
|
+
req.on('error', fail);
|
|
127
|
+
req.on('aborted', () => fail(new Error('Client aborted request')));
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function extractModelId(body, contentType = '') {
|
|
132
|
+
if (!body?.length || !String(contentType).includes('json')) return '';
|
|
133
|
+
try {
|
|
134
|
+
const parsed = JSON.parse(body.toString('utf8'));
|
|
135
|
+
if (typeof parsed.model === 'string') return parsed.model.trim();
|
|
136
|
+
if (parsed.model && typeof parsed.model === 'object') {
|
|
137
|
+
return String(parsed.model.id || parsed.model.name || '').trim();
|
|
138
|
+
}
|
|
139
|
+
return String(parsed.modelId || parsed.model_id || '').trim();
|
|
140
|
+
} catch {
|
|
141
|
+
return '';
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function normalizeModelId(value) {
|
|
146
|
+
return String(value || '').trim().replace(/:(minimal|low|medium|high|xhigh|off)$/, '');
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function channelSupportsModel(channel, modelId) {
|
|
150
|
+
const requested = normalizeModelId(modelId).toLowerCase();
|
|
151
|
+
if (!requested) return true;
|
|
152
|
+
const declared = [];
|
|
153
|
+
(Array.isArray(channel.allowedModels) ? channel.allowedModels : []).forEach(value => declared.push(value));
|
|
154
|
+
(Array.isArray(channel.models) ? channel.models : []).forEach((model) => {
|
|
155
|
+
declared.push(typeof model === 'string' ? model : (model?.id || model?.name));
|
|
156
|
+
});
|
|
157
|
+
if (channel.model) declared.push(channel.model);
|
|
158
|
+
const normalized = [...new Set(declared.map(normalizeModelId).filter(Boolean).map(value => value.toLowerCase()))];
|
|
159
|
+
return normalized.length === 0 || normalized.includes(requested);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function writeJson(res, statusCode, payload) {
|
|
163
|
+
if (res.headersSent || res.destroyed) return;
|
|
164
|
+
const body = Buffer.from(JSON.stringify(payload));
|
|
165
|
+
res.writeHead(statusCode, {
|
|
166
|
+
'content-type': 'application/json; charset=utf-8',
|
|
167
|
+
'content-length': body.length
|
|
168
|
+
});
|
|
169
|
+
res.end(body);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function constantTimeEqual(left, right) {
|
|
173
|
+
const leftBuffer = Buffer.from(String(left || ''));
|
|
174
|
+
const rightBuffer = Buffer.from(String(right || ''));
|
|
175
|
+
return leftBuffer.length === rightBuffer.length
|
|
176
|
+
&& crypto.timingSafeEqual(leftBuffer, rightBuffer);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function hasValidCapability(req, parsedUrl, route) {
|
|
180
|
+
if (route.authMode === 'none') return true;
|
|
181
|
+
const candidates = [
|
|
182
|
+
req.headers.authorization,
|
|
183
|
+
req.headers['x-api-key'],
|
|
184
|
+
req.headers['x-goog-api-key'],
|
|
185
|
+
req.headers['api-key'],
|
|
186
|
+
parsedUrl.searchParams.get('key'),
|
|
187
|
+
parsedUrl.searchParams.get('api_key'),
|
|
188
|
+
parsedUrl.searchParams.get('access_token'),
|
|
189
|
+
parsedUrl.searchParams.get('token')
|
|
190
|
+
].flatMap((value) => Array.isArray(value) ? value : [value])
|
|
191
|
+
.filter(value => value !== undefined && value !== null)
|
|
192
|
+
.map((value) => String(value).replace(/^Bearer\s+/i, '').trim());
|
|
193
|
+
return candidates.some(candidate => constantTimeEqual(candidate, route.capability));
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function mergeParsedUsage(tokenData, value, eventType = '') {
|
|
197
|
+
if (Array.isArray(value)) {
|
|
198
|
+
value.forEach(item => mergeParsedUsage(tokenData, item, eventType));
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
mergeUsageIntoTokenData(
|
|
202
|
+
tokenData,
|
|
203
|
+
eventType ? parseSSEUsage(value, eventType) : parseNonStreamingUsage(value)
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function createResponseUsageMonitor(contentType = '') {
|
|
208
|
+
const tokenData = createTokenData();
|
|
209
|
+
const streaming = String(contentType).toLowerCase().includes('text/event-stream');
|
|
210
|
+
const chunks = [];
|
|
211
|
+
let bufferedBytes = 0;
|
|
212
|
+
let sseBuffer = '';
|
|
213
|
+
let observationDisabled = false;
|
|
214
|
+
|
|
215
|
+
const parseEvent = (eventText) => {
|
|
216
|
+
const event = parseSSEEventText(eventText);
|
|
217
|
+
if (!event) return;
|
|
218
|
+
try {
|
|
219
|
+
mergeParsedUsage(tokenData, JSON.parse(event.data), event.eventType);
|
|
220
|
+
} catch {
|
|
221
|
+
// Usage observation must never alter or reject the transparent response.
|
|
222
|
+
}
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
return {
|
|
226
|
+
observe(chunk) {
|
|
227
|
+
if (observationDisabled) return;
|
|
228
|
+
if (streaming) {
|
|
229
|
+
if (Buffer.byteLength(sseBuffer) + chunk.length > MAX_REQUEST_BYTES) {
|
|
230
|
+
observationDisabled = true;
|
|
231
|
+
sseBuffer = '';
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
sseBuffer += chunk.toString('utf8');
|
|
235
|
+
const split = splitSSEEvents(sseBuffer);
|
|
236
|
+
split.events.forEach(parseEvent);
|
|
237
|
+
sseBuffer = split.remainder;
|
|
238
|
+
} else if (bufferedBytes + chunk.length <= MAX_REQUEST_BYTES) {
|
|
239
|
+
chunks.push(Buffer.from(chunk));
|
|
240
|
+
bufferedBytes += chunk.length;
|
|
241
|
+
} else {
|
|
242
|
+
observationDisabled = true;
|
|
243
|
+
chunks.length = 0;
|
|
244
|
+
}
|
|
245
|
+
},
|
|
246
|
+
finish() {
|
|
247
|
+
if (observationDisabled) {
|
|
248
|
+
return {
|
|
249
|
+
model: tokenData.model,
|
|
250
|
+
tokens: {
|
|
251
|
+
input: tokenData.inputTokens,
|
|
252
|
+
output: tokenData.outputTokens,
|
|
253
|
+
cacheCreation: tokenData.cacheCreation,
|
|
254
|
+
cacheRead: tokenData.cacheRead,
|
|
255
|
+
cached: tokenData.cachedTokens,
|
|
256
|
+
reasoning: tokenData.reasoningTokens,
|
|
257
|
+
total: tokenData.totalTokens
|
|
258
|
+
}
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
if (streaming) {
|
|
262
|
+
parseEvent(sseBuffer);
|
|
263
|
+
} else if (chunks.length > 0) {
|
|
264
|
+
try {
|
|
265
|
+
mergeParsedUsage(tokenData, JSON.parse(Buffer.concat(chunks).toString('utf8')));
|
|
266
|
+
} catch {
|
|
267
|
+
// Non-JSON responses are valid passthrough responses without usage.
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
return {
|
|
271
|
+
model: tokenData.model,
|
|
272
|
+
tokens: {
|
|
273
|
+
input: tokenData.inputTokens,
|
|
274
|
+
output: tokenData.outputTokens,
|
|
275
|
+
cacheCreation: tokenData.cacheCreation,
|
|
276
|
+
cacheRead: tokenData.cacheRead,
|
|
277
|
+
cached: tokenData.cachedTokens,
|
|
278
|
+
reasoning: tokenData.reasoningTokens,
|
|
279
|
+
total: tokenData.totalTokens
|
|
280
|
+
}
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function closeServer(server, forceAfterMs = 1000) {
|
|
287
|
+
if (!server) return Promise.resolve();
|
|
288
|
+
return new Promise((resolve, reject) => {
|
|
289
|
+
const timer = expediteServerShutdown(server, { forceAfterMs });
|
|
290
|
+
server.close((error) => {
|
|
291
|
+
if (timer) clearTimeout(timer);
|
|
292
|
+
if (error && error.code !== 'ERR_SERVER_NOT_RUNNING') {
|
|
293
|
+
reject(error);
|
|
294
|
+
} else {
|
|
295
|
+
resolve();
|
|
296
|
+
}
|
|
297
|
+
});
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function createOmpGateway(options = {}) {
|
|
302
|
+
const getChannels = options.getChannels;
|
|
303
|
+
const allocate = options.allocateChannel || allocateChannel;
|
|
304
|
+
const release = options.releaseChannel || releaseChannel;
|
|
305
|
+
const onSuccess = options.recordSuccess || recordSuccess;
|
|
306
|
+
const onFailure = options.recordFailure || recordFailure;
|
|
307
|
+
const publishUsage = options.publishUsageLog || ((data) => {
|
|
308
|
+
const enrich = (payload) => broadcastLog({
|
|
309
|
+
...payload,
|
|
310
|
+
originalProvider: data.originalProvider,
|
|
311
|
+
providerApi: data.providerApi,
|
|
312
|
+
routingGroup: data.routingGroup,
|
|
313
|
+
actualChannelId: data.channelId,
|
|
314
|
+
upstreamModel: data.model,
|
|
315
|
+
switchReason: data.switchReason || undefined,
|
|
316
|
+
attemptedChannels: data.attemptedChannels
|
|
317
|
+
});
|
|
318
|
+
return publishSharedUsageLog({
|
|
319
|
+
source: 'omp',
|
|
320
|
+
metadata: {
|
|
321
|
+
id: data.requestId,
|
|
322
|
+
channel: data.channel,
|
|
323
|
+
channelId: data.channelId,
|
|
324
|
+
originalModel: data.originalModel,
|
|
325
|
+
startTime: data.startTime
|
|
326
|
+
},
|
|
327
|
+
model: data.model || data.originalModel,
|
|
328
|
+
tokens: data.tokens,
|
|
329
|
+
broadcastLog: enrich,
|
|
330
|
+
recordRequest: recordOmpRequest,
|
|
331
|
+
recordSuccess: null
|
|
332
|
+
});
|
|
333
|
+
});
|
|
334
|
+
const publishFailure = options.publishFailureLog || ((data) => {
|
|
335
|
+
const enrich = (payload) => broadcastLog({
|
|
336
|
+
...payload,
|
|
337
|
+
originalProvider: data.originalProvider,
|
|
338
|
+
providerApi: data.providerApi,
|
|
339
|
+
routingGroup: data.routingGroup,
|
|
340
|
+
actualChannelId: data.channelId,
|
|
341
|
+
switchReason: data.switchReason || undefined,
|
|
342
|
+
attemptedChannels: data.attemptedChannels
|
|
343
|
+
});
|
|
344
|
+
return publishSharedFailureLog({
|
|
345
|
+
source: 'omp',
|
|
346
|
+
metadata: {
|
|
347
|
+
id: data.requestId,
|
|
348
|
+
channel: data.channel,
|
|
349
|
+
model: data.model
|
|
350
|
+
},
|
|
351
|
+
channel: data.channel,
|
|
352
|
+
model: data.model,
|
|
353
|
+
message: data.message,
|
|
354
|
+
error: data.error,
|
|
355
|
+
statusCode: data.statusCode,
|
|
356
|
+
stage: data.stage,
|
|
357
|
+
broadcastLog: enrich
|
|
358
|
+
});
|
|
359
|
+
});
|
|
360
|
+
const websocketProxy = httpProxy.createProxyServer({
|
|
361
|
+
ws: true,
|
|
362
|
+
changeOrigin: true,
|
|
363
|
+
xfwd: false
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
let server = null;
|
|
367
|
+
let host = '127.0.0.1';
|
|
368
|
+
let port = null;
|
|
369
|
+
let secret = '';
|
|
370
|
+
let draining = false;
|
|
371
|
+
let inflightRequests = 0;
|
|
372
|
+
const activeUpstreams = new Set();
|
|
373
|
+
|
|
374
|
+
function gatewayDescriptor() {
|
|
375
|
+
return { host, port, secret };
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
function currentChannels() {
|
|
379
|
+
const value = typeof getChannels === 'function' ? getChannels() : [];
|
|
380
|
+
return Array.isArray(value?.channels) ? value.channels : (Array.isArray(value) ? value : []);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
async function allocateForRoute(route, excludedIds = [], modelId = '', signal = null) {
|
|
384
|
+
const channels = currentChannels();
|
|
385
|
+
const candidateIds = route.channelIds.filter((channelId) => {
|
|
386
|
+
const channel = channels.find(item => item.id === channelId);
|
|
387
|
+
return channel && channelSupportsModel(channel, modelId);
|
|
388
|
+
});
|
|
389
|
+
if (candidateIds.length === 0) {
|
|
390
|
+
throw Object.assign(
|
|
391
|
+
new Error(`No OMP channel in routing group "${route.routingGroup}" supports model "${modelId}"`),
|
|
392
|
+
{ statusCode: 422 }
|
|
393
|
+
);
|
|
394
|
+
}
|
|
395
|
+
return allocate({
|
|
396
|
+
source: 'omp',
|
|
397
|
+
enableSessionBinding: false,
|
|
398
|
+
candidateIds,
|
|
399
|
+
excludeChannelIds: [...excludedIds],
|
|
400
|
+
routingGroup: route.routingGroup,
|
|
401
|
+
providerKey: route.providerKey,
|
|
402
|
+
providerApi: route.providerApi,
|
|
403
|
+
modelId: normalizeModelId(modelId),
|
|
404
|
+
signal
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
function forwardAttempt(req, route, channel, body, signal) {
|
|
409
|
+
return new Promise((resolve, reject) => {
|
|
410
|
+
let released = false;
|
|
411
|
+
const releaseOnce = () => {
|
|
412
|
+
if (released) return;
|
|
413
|
+
released = true;
|
|
414
|
+
release(channel.id, 'omp');
|
|
415
|
+
};
|
|
416
|
+
if (signal?.aborted) {
|
|
417
|
+
releaseOnce();
|
|
418
|
+
reject(createAbortError());
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
let upstream;
|
|
423
|
+
try {
|
|
424
|
+
upstream = new URL(channel.baseUrl);
|
|
425
|
+
} catch (error) {
|
|
426
|
+
releaseOnce();
|
|
427
|
+
reject(error);
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
const upstreamPath = buildUpstreamRequestPath(channel.baseUrl, route.upstreamPath);
|
|
432
|
+
const rewritten = rewriteCredential(req.headers, upstreamPath, route, channel);
|
|
433
|
+
const transport = upstream.protocol === 'https:' ? https : http;
|
|
434
|
+
let upstreamResponse = null;
|
|
435
|
+
const upstreamRequest = transport.request({
|
|
436
|
+
protocol: upstream.protocol,
|
|
437
|
+
hostname: upstream.hostname,
|
|
438
|
+
port: upstream.port || undefined,
|
|
439
|
+
method: req.method,
|
|
440
|
+
path: rewritten.path,
|
|
441
|
+
headers: {
|
|
442
|
+
...rewritten.headers,
|
|
443
|
+
host: upstream.host,
|
|
444
|
+
'content-length': body.length
|
|
445
|
+
}
|
|
446
|
+
}, (response) => {
|
|
447
|
+
upstreamResponse = response;
|
|
448
|
+
activeUpstreams.delete(upstreamRequest);
|
|
449
|
+
activeUpstreams.add(response);
|
|
450
|
+
response.once('close', () => {
|
|
451
|
+
signal?.removeEventListener('abort', abortUpstream);
|
|
452
|
+
});
|
|
453
|
+
const statusCode = response.statusCode || 502;
|
|
454
|
+
resolve({
|
|
455
|
+
statusCode,
|
|
456
|
+
response,
|
|
457
|
+
channel,
|
|
458
|
+
release: releaseOnce
|
|
459
|
+
});
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
activeUpstreams.add(upstreamRequest);
|
|
463
|
+
const abortUpstream = () => {
|
|
464
|
+
const error = createAbortError();
|
|
465
|
+
if (upstreamResponse) {
|
|
466
|
+
upstreamResponse.destroy(error);
|
|
467
|
+
} else {
|
|
468
|
+
upstreamRequest.destroy(error);
|
|
469
|
+
}
|
|
470
|
+
};
|
|
471
|
+
signal?.addEventListener('abort', abortUpstream, { once: true });
|
|
472
|
+
upstreamRequest.on('error', (error) => {
|
|
473
|
+
signal?.removeEventListener('abort', abortUpstream);
|
|
474
|
+
activeUpstreams.delete(upstreamRequest);
|
|
475
|
+
releaseOnce();
|
|
476
|
+
reject(error);
|
|
477
|
+
});
|
|
478
|
+
upstreamRequest.end(body);
|
|
479
|
+
});
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
async function handleProxyRequest(req, res, route, signal) {
|
|
483
|
+
const body = await readRequestBody(req);
|
|
484
|
+
if (signal?.aborted) throw createAbortError();
|
|
485
|
+
const modelId = extractModelId(body, req.headers['content-type']);
|
|
486
|
+
const attemptedIds = [];
|
|
487
|
+
const attemptedChannels = [];
|
|
488
|
+
const requestId = `omp-${crypto.randomUUID()}`;
|
|
489
|
+
const startTime = Date.now();
|
|
490
|
+
let lastError = null;
|
|
491
|
+
let switchReason = '';
|
|
492
|
+
|
|
493
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
494
|
+
let channel;
|
|
495
|
+
try {
|
|
496
|
+
channel = await allocateForRoute(route, attemptedIds, modelId, signal);
|
|
497
|
+
} catch (error) {
|
|
498
|
+
lastError = error;
|
|
499
|
+
break;
|
|
500
|
+
}
|
|
501
|
+
if (signal?.aborted) {
|
|
502
|
+
release(channel.id, 'omp');
|
|
503
|
+
lastError = createAbortError();
|
|
504
|
+
break;
|
|
505
|
+
}
|
|
506
|
+
attemptedIds.push(channel.id);
|
|
507
|
+
attemptedChannels.push(channel.name || channel.id);
|
|
508
|
+
|
|
509
|
+
try {
|
|
510
|
+
const result = await forwardAttempt(req, route, channel, body, signal);
|
|
511
|
+
if (attempt === 0 && RETRYABLE_STATUS.has(result.statusCode)) {
|
|
512
|
+
result.response.once('close', () => activeUpstreams.delete(result.response));
|
|
513
|
+
result.response.resume();
|
|
514
|
+
result.release();
|
|
515
|
+
onFailure(channel.id, 'omp');
|
|
516
|
+
switchReason = `http-${result.statusCode}`;
|
|
517
|
+
publishFailure({
|
|
518
|
+
source: 'omp',
|
|
519
|
+
requestId,
|
|
520
|
+
channel: channel.name || channel.id,
|
|
521
|
+
channelId: channel.id,
|
|
522
|
+
originalProvider: route.providerKey,
|
|
523
|
+
providerApi: route.providerApi,
|
|
524
|
+
routingGroup: route.routingGroup,
|
|
525
|
+
model: modelId,
|
|
526
|
+
statusCode: result.statusCode,
|
|
527
|
+
stage: 'dynamic-switch',
|
|
528
|
+
switchReason,
|
|
529
|
+
attemptedChannels: [...attemptedChannels],
|
|
530
|
+
message: `OMP upstream returned HTTP ${result.statusCode}`
|
|
531
|
+
});
|
|
532
|
+
lastError = Object.assign(new Error(`OMP upstream returned HTTP ${result.statusCode}`), {
|
|
533
|
+
statusCode: result.statusCode
|
|
534
|
+
});
|
|
535
|
+
continue;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
res.writeHead(result.statusCode, result.response.headers);
|
|
539
|
+
const usageMonitor = createResponseUsageMonitor(result.response.headers['content-type']);
|
|
540
|
+
await new Promise((resolve) => {
|
|
541
|
+
let completed = false;
|
|
542
|
+
const finish = (error) => {
|
|
543
|
+
if (completed) return;
|
|
544
|
+
completed = true;
|
|
545
|
+
activeUpstreams.delete(result.response);
|
|
546
|
+
result.release();
|
|
547
|
+
if (result.statusCode < 400 && !error) {
|
|
548
|
+
const usage = usageMonitor.finish();
|
|
549
|
+
onSuccess(channel.id, 'omp');
|
|
550
|
+
publishUsage({
|
|
551
|
+
source: 'omp',
|
|
552
|
+
requestId,
|
|
553
|
+
startTime,
|
|
554
|
+
channel: channel.name || channel.id,
|
|
555
|
+
channelId: channel.id,
|
|
556
|
+
originalProvider: route.providerKey,
|
|
557
|
+
providerApi: route.providerApi,
|
|
558
|
+
routingGroup: route.routingGroup,
|
|
559
|
+
originalModel: modelId,
|
|
560
|
+
model: usage.model || modelId,
|
|
561
|
+
tokens: usage.tokens,
|
|
562
|
+
switchReason,
|
|
563
|
+
attemptedChannels: [...attemptedChannels]
|
|
564
|
+
});
|
|
565
|
+
} else {
|
|
566
|
+
onFailure(channel.id, 'omp');
|
|
567
|
+
publishFailure({
|
|
568
|
+
source: 'omp',
|
|
569
|
+
requestId,
|
|
570
|
+
channel: channel.name || channel.id,
|
|
571
|
+
channelId: channel.id,
|
|
572
|
+
originalProvider: route.providerKey,
|
|
573
|
+
providerApi: route.providerApi,
|
|
574
|
+
routingGroup: route.routingGroup,
|
|
575
|
+
model: modelId,
|
|
576
|
+
error,
|
|
577
|
+
statusCode: result.statusCode,
|
|
578
|
+
stage: error ? 'response-stream' : 'upstream-response',
|
|
579
|
+
switchReason,
|
|
580
|
+
attemptedChannels: [...attemptedChannels],
|
|
581
|
+
message: error?.message || `OMP upstream returned HTTP ${result.statusCode}`
|
|
582
|
+
});
|
|
583
|
+
}
|
|
584
|
+
resolve();
|
|
585
|
+
};
|
|
586
|
+
result.response.on('data', chunk => usageMonitor.observe(chunk));
|
|
587
|
+
result.response.once('end', () => finish());
|
|
588
|
+
result.response.once('close', () => finish(new Error('OMP upstream response closed early')));
|
|
589
|
+
result.response.once('error', (error) => {
|
|
590
|
+
if (!res.destroyed) res.destroy(error);
|
|
591
|
+
finish(error);
|
|
592
|
+
});
|
|
593
|
+
res.once('close', () => {
|
|
594
|
+
if (!res.writableEnded) {
|
|
595
|
+
result.response.destroy(new Error('OMP downstream client disconnected'));
|
|
596
|
+
}
|
|
597
|
+
});
|
|
598
|
+
result.response.pipe(res);
|
|
599
|
+
});
|
|
600
|
+
return;
|
|
601
|
+
} catch (error) {
|
|
602
|
+
lastError = error;
|
|
603
|
+
if (error?.name === 'AbortError') {
|
|
604
|
+
break;
|
|
605
|
+
}
|
|
606
|
+
onFailure(channel.id, 'omp');
|
|
607
|
+
switchReason = error.code || error.message || 'connection-error';
|
|
608
|
+
publishFailure({
|
|
609
|
+
source: 'omp',
|
|
610
|
+
requestId,
|
|
611
|
+
channel: channel.name || channel.id,
|
|
612
|
+
channelId: channel.id,
|
|
613
|
+
originalProvider: route.providerKey,
|
|
614
|
+
providerApi: route.providerApi,
|
|
615
|
+
routingGroup: route.routingGroup,
|
|
616
|
+
model: modelId,
|
|
617
|
+
error,
|
|
618
|
+
statusCode: error.statusCode,
|
|
619
|
+
stage: attempt === 0 ? 'dynamic-switch' : 'upstream-connect',
|
|
620
|
+
switchReason,
|
|
621
|
+
attemptedChannels: [...attemptedChannels]
|
|
622
|
+
});
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
throw lastError || new Error('No compatible OMP channel is available');
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
async function handleRequest(req, res) {
|
|
630
|
+
if (req.method === 'GET' && req.url === '/healthz') {
|
|
631
|
+
writeJson(res, 200, {
|
|
632
|
+
ok: true,
|
|
633
|
+
service: 'omp-gateway',
|
|
634
|
+
draining,
|
|
635
|
+
inflightRequests
|
|
636
|
+
});
|
|
637
|
+
return;
|
|
638
|
+
}
|
|
639
|
+
if (draining) {
|
|
640
|
+
writeJson(res, 503, { error: 'OMP gateway is draining' });
|
|
641
|
+
return;
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
let parsed;
|
|
645
|
+
try {
|
|
646
|
+
parsed = new URL(req.url, `http://${host}`);
|
|
647
|
+
} catch {
|
|
648
|
+
writeJson(res, 400, { error: 'Invalid OMP gateway URL' });
|
|
649
|
+
return;
|
|
650
|
+
}
|
|
651
|
+
let route;
|
|
652
|
+
try {
|
|
653
|
+
route = resolveOmpGatewayRoute(parsed.pathname, currentChannels(), gatewayDescriptor());
|
|
654
|
+
} catch (error) {
|
|
655
|
+
writeJson(res, 503, { error: error.message });
|
|
656
|
+
return;
|
|
657
|
+
}
|
|
658
|
+
if (!route) {
|
|
659
|
+
writeJson(res, 404, { error: 'Unknown OMP gateway route' });
|
|
660
|
+
return;
|
|
661
|
+
}
|
|
662
|
+
if (!hasValidCapability(req, parsed, route)) {
|
|
663
|
+
writeJson(res, 401, { error: 'Invalid OMP gateway capability' });
|
|
664
|
+
return;
|
|
665
|
+
}
|
|
666
|
+
route.upstreamPath = `${route.upstreamPath}${parsed.search}`;
|
|
667
|
+
|
|
668
|
+
inflightRequests++;
|
|
669
|
+
const abortController = new AbortController();
|
|
670
|
+
const abortDownstream = () => abortController.abort();
|
|
671
|
+
const abortClosedResponse = () => {
|
|
672
|
+
if (!res.writableEnded) abortDownstream();
|
|
673
|
+
};
|
|
674
|
+
req.once('aborted', abortDownstream);
|
|
675
|
+
res.once('close', abortClosedResponse);
|
|
676
|
+
try {
|
|
677
|
+
await handleProxyRequest(req, res, route, abortController.signal);
|
|
678
|
+
} catch (error) {
|
|
679
|
+
if (error?.name !== 'AbortError') {
|
|
680
|
+
writeJson(res, error.statusCode || 502, { error: error.message || 'OMP upstream request failed' });
|
|
681
|
+
}
|
|
682
|
+
} finally {
|
|
683
|
+
req.removeListener('aborted', abortDownstream);
|
|
684
|
+
res.removeListener('close', abortClosedResponse);
|
|
685
|
+
inflightRequests = Math.max(0, inflightRequests - 1);
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
function rejectUpgrade(socket, statusCode, message) {
|
|
690
|
+
if (!socket || socket.destroyed) return;
|
|
691
|
+
const body = JSON.stringify({ error: message });
|
|
692
|
+
const statusText = {
|
|
693
|
+
401: 'Unauthorized',
|
|
694
|
+
404: 'Not Found',
|
|
695
|
+
503: 'Service Unavailable'
|
|
696
|
+
}[statusCode] || 'Bad Gateway';
|
|
697
|
+
socket.end([
|
|
698
|
+
`HTTP/1.1 ${statusCode} ${statusText}`,
|
|
699
|
+
'Content-Type: application/json; charset=utf-8',
|
|
700
|
+
`Content-Length: ${Buffer.byteLength(body)}`,
|
|
701
|
+
'Connection: close',
|
|
702
|
+
'',
|
|
703
|
+
body
|
|
704
|
+
].join('\r\n'));
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
async function handleUpgrade(req, socket, head) {
|
|
708
|
+
if (draining) {
|
|
709
|
+
rejectUpgrade(socket, 503, 'OMP gateway is draining');
|
|
710
|
+
return;
|
|
711
|
+
}
|
|
712
|
+
let parsed;
|
|
713
|
+
try {
|
|
714
|
+
parsed = new URL(req.url, `http://${host}`);
|
|
715
|
+
} catch {
|
|
716
|
+
rejectUpgrade(socket, 404, 'Invalid OMP gateway URL');
|
|
717
|
+
return;
|
|
718
|
+
}
|
|
719
|
+
let route;
|
|
720
|
+
try {
|
|
721
|
+
route = resolveOmpGatewayRoute(parsed.pathname, currentChannels(), gatewayDescriptor());
|
|
722
|
+
} catch (error) {
|
|
723
|
+
rejectUpgrade(socket, 503, error.message);
|
|
724
|
+
return;
|
|
725
|
+
}
|
|
726
|
+
if (!route) {
|
|
727
|
+
rejectUpgrade(socket, 404, 'Unknown OMP gateway route');
|
|
728
|
+
return;
|
|
729
|
+
}
|
|
730
|
+
if (!hasValidCapability(req, parsed, route)) {
|
|
731
|
+
rejectUpgrade(socket, 401, 'Invalid OMP gateway capability');
|
|
732
|
+
return;
|
|
733
|
+
}
|
|
734
|
+
route.upstreamPath = `${route.upstreamPath}${parsed.search}`;
|
|
735
|
+
|
|
736
|
+
let channel;
|
|
737
|
+
try {
|
|
738
|
+
channel = await allocateForRoute(route);
|
|
739
|
+
} catch (error) {
|
|
740
|
+
rejectUpgrade(socket, 503, error.message || 'No compatible OMP channel is available');
|
|
741
|
+
return;
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
inflightRequests++;
|
|
745
|
+
const requestId = `omp-${crypto.randomUUID()}`;
|
|
746
|
+
const startTime = Date.now();
|
|
747
|
+
const websocketModel = normalizeModelId(channel.model || '');
|
|
748
|
+
let released = false;
|
|
749
|
+
let failed = false;
|
|
750
|
+
let failureRecorded = false;
|
|
751
|
+
const markFailedOnce = (error) => {
|
|
752
|
+
failed = true;
|
|
753
|
+
if (failureRecorded) return;
|
|
754
|
+
failureRecorded = true;
|
|
755
|
+
onFailure(channel.id, 'omp');
|
|
756
|
+
publishFailure({
|
|
757
|
+
source: 'omp',
|
|
758
|
+
requestId,
|
|
759
|
+
channel: channel.name || channel.id,
|
|
760
|
+
channelId: channel.id,
|
|
761
|
+
originalProvider: route.providerKey,
|
|
762
|
+
providerApi: route.providerApi,
|
|
763
|
+
routingGroup: route.routingGroup,
|
|
764
|
+
model: websocketModel,
|
|
765
|
+
error,
|
|
766
|
+
stage: 'websocket',
|
|
767
|
+
attemptedChannels: [channel.name || channel.id]
|
|
768
|
+
});
|
|
769
|
+
};
|
|
770
|
+
const releaseOnce = () => {
|
|
771
|
+
if (released) return;
|
|
772
|
+
released = true;
|
|
773
|
+
inflightRequests = Math.max(0, inflightRequests - 1);
|
|
774
|
+
if (!failed) {
|
|
775
|
+
onSuccess(channel.id, 'omp');
|
|
776
|
+
publishUsage({
|
|
777
|
+
source: 'omp',
|
|
778
|
+
requestId,
|
|
779
|
+
startTime,
|
|
780
|
+
channel: channel.name || channel.id,
|
|
781
|
+
channelId: channel.id,
|
|
782
|
+
originalProvider: route.providerKey,
|
|
783
|
+
providerApi: route.providerApi,
|
|
784
|
+
routingGroup: route.routingGroup,
|
|
785
|
+
originalModel: websocketModel,
|
|
786
|
+
model: websocketModel,
|
|
787
|
+
tokens: {},
|
|
788
|
+
attemptedChannels: [channel.name || channel.id]
|
|
789
|
+
});
|
|
790
|
+
}
|
|
791
|
+
release(channel.id, 'omp');
|
|
792
|
+
};
|
|
793
|
+
socket.once('close', releaseOnce);
|
|
794
|
+
socket.once('error', (error) => {
|
|
795
|
+
markFailedOnce(error);
|
|
796
|
+
releaseOnce();
|
|
797
|
+
});
|
|
798
|
+
|
|
799
|
+
try {
|
|
800
|
+
const upstream = new URL(channel.baseUrl);
|
|
801
|
+
const upstreamPath = buildUpstreamRequestPath(channel.baseUrl, route.upstreamPath);
|
|
802
|
+
const rewritten = rewriteCredential(req.headers, upstreamPath, route, channel, {
|
|
803
|
+
preserveConnection: true
|
|
804
|
+
});
|
|
805
|
+
req.url = rewritten.path;
|
|
806
|
+
req.headers = {
|
|
807
|
+
...rewritten.headers,
|
|
808
|
+
host: upstream.host
|
|
809
|
+
};
|
|
810
|
+
req.__ompGatewayRelease = releaseOnce;
|
|
811
|
+
req.__ompGatewayFail = markFailedOnce;
|
|
812
|
+
websocketProxy.ws(req, socket, head, {
|
|
813
|
+
target: upstream.origin,
|
|
814
|
+
changeOrigin: true,
|
|
815
|
+
secure: upstream.protocol === 'https:'
|
|
816
|
+
});
|
|
817
|
+
} catch (error) {
|
|
818
|
+
markFailedOnce(error);
|
|
819
|
+
releaseOnce();
|
|
820
|
+
rejectUpgrade(socket, 503, error.message || 'OMP websocket proxy failed');
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
websocketProxy.on('error', (error, req, socket) => {
|
|
825
|
+
req?.__ompGatewayFail?.(error);
|
|
826
|
+
req?.__ompGatewayRelease?.();
|
|
827
|
+
if (socket && !socket.destroyed) {
|
|
828
|
+
socket.destroy(error);
|
|
829
|
+
}
|
|
830
|
+
});
|
|
831
|
+
|
|
832
|
+
async function start(startOptions = {}) {
|
|
833
|
+
if (server?.listening) return status();
|
|
834
|
+
host = startOptions.host || '127.0.0.1';
|
|
835
|
+
port = Number.isInteger(startOptions.port) ? startOptions.port : 20092;
|
|
836
|
+
if (port < 0 || port > 65535) {
|
|
837
|
+
throw new Error(`Invalid OMP gateway port: ${port}`);
|
|
838
|
+
}
|
|
839
|
+
secret = String(startOptions.secret || '');
|
|
840
|
+
if (!secret) {
|
|
841
|
+
throw new Error('OMP gateway secret is required');
|
|
842
|
+
}
|
|
843
|
+
draining = false;
|
|
844
|
+
server = attachServerShutdownHandling(http.createServer((req, res) => {
|
|
845
|
+
handleRequest(req, res).catch((error) => {
|
|
846
|
+
writeJson(res, 500, { error: error.message || 'OMP gateway request failed' });
|
|
847
|
+
});
|
|
848
|
+
}));
|
|
849
|
+
server.on('upgrade', (req, socket, head) => {
|
|
850
|
+
handleUpgrade(req, socket, head).catch((error) => {
|
|
851
|
+
rejectUpgrade(socket, 503, error.message || 'OMP websocket proxy failed');
|
|
852
|
+
});
|
|
853
|
+
});
|
|
854
|
+
|
|
855
|
+
try {
|
|
856
|
+
await new Promise((resolve, reject) => {
|
|
857
|
+
const onError = (error) => {
|
|
858
|
+
server.removeListener('listening', onListening);
|
|
859
|
+
reject(error);
|
|
860
|
+
};
|
|
861
|
+
const onListening = () => {
|
|
862
|
+
server.removeListener('error', onError);
|
|
863
|
+
resolve();
|
|
864
|
+
};
|
|
865
|
+
server.once('error', onError);
|
|
866
|
+
server.once('listening', onListening);
|
|
867
|
+
server.listen(port, host);
|
|
868
|
+
});
|
|
869
|
+
} catch (error) {
|
|
870
|
+
server = null;
|
|
871
|
+
port = null;
|
|
872
|
+
throw error;
|
|
873
|
+
}
|
|
874
|
+
port = server.address().port;
|
|
875
|
+
return status();
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
async function stop(stopOptions = {}) {
|
|
879
|
+
if (!server) return status();
|
|
880
|
+
draining = true;
|
|
881
|
+
await closeServer(server, stopOptions.forceAfterMs);
|
|
882
|
+
activeUpstreams.clear();
|
|
883
|
+
server = null;
|
|
884
|
+
port = null;
|
|
885
|
+
draining = false;
|
|
886
|
+
inflightRequests = 0;
|
|
887
|
+
return status();
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
function cancelDraining() {
|
|
891
|
+
draining = false;
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
function beginDraining() {
|
|
895
|
+
draining = true;
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
function status() {
|
|
899
|
+
return {
|
|
900
|
+
listening: Boolean(server?.listening),
|
|
901
|
+
draining,
|
|
902
|
+
port,
|
|
903
|
+
inflightRequests
|
|
904
|
+
};
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
return {
|
|
908
|
+
start,
|
|
909
|
+
stop,
|
|
910
|
+
status,
|
|
911
|
+
beginDraining,
|
|
912
|
+
cancelDraining,
|
|
913
|
+
descriptor: gatewayDescriptor
|
|
914
|
+
};
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
module.exports = {
|
|
918
|
+
createOmpGateway
|
|
919
|
+
};
|