agl 41.0.1 → 41.2.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/changelog.md +24 -0
- package/dist_serve/bundle.js +1448 -1448
- package/dist_serve/bundle.js.map +2 -2
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/aglapi/aglapisurface.document.js +7 -3
- package/dist_ts/classes.authmodels.d.ts +15 -0
- package/dist_ts/classes.authmodels.js +78 -7
- package/dist_ts/classes.authstore.js +23 -6
- package/dist_ts/classes.cli.js +8 -3
- package/dist_ts/classes.controller.d.ts +13 -0
- package/dist_ts/classes.controller.js +51 -14
- package/dist_ts/classes.controllermanagement.d.ts +53 -6
- package/dist_ts/classes.controllermanagement.js +180 -72
- package/dist_ts/classes.inferencegateway.d.ts +6 -6
- package/dist_ts/classes.inferencegateway.js +28 -15
- package/dist_ts/classes.inferenceusage.d.ts +19 -7
- package/dist_ts/classes.inferenceusage.js +49 -13
- package/dist_ts/classes.inferenceusagetap.d.ts +15 -3
- package/dist_ts/classes.inferenceusagetap.js +16 -9
- package/dist_ts/classes.processinspection.d.ts +3 -1
- package/dist_ts/classes.processinspection.js +4 -2
- package/dist_ts/classes.proxycontrol.d.ts +22 -7
- package/dist_ts/classes.proxycontrol.js +5 -3
- package/dist_ts/classes.proxycontrolclient.d.ts +4 -3
- package/dist_ts/classes.proxycontrolclient.js +7 -5
- package/dist_ts/classes.proxydaemon.d.ts +6 -4
- package/dist_ts/classes.proxydaemon.js +42 -17
- package/dist_ts/classes.proxyleaseholder.d.ts +11 -2
- package/dist_ts/classes.proxyleaseholder.js +25 -2
- package/dist_ts/classes.proxystore.d.ts +3 -2
- package/dist_ts/classes.proxystore.js +5 -2
- package/dist_ts/classes.upgradecoordinator.d.ts +2 -1
- package/dist_ts/classes.upgradecoordinator.js +13 -10
- package/dist_ts/classes.upgradetransaction.d.ts +6 -0
- package/dist_ts/classes.upgradetransaction.js +38 -13
- package/dist_ts/classes.usagefeed.js +3 -2
- package/dist_ts/functions.controllermanagementsurface.d.ts +37 -0
- package/dist_ts/functions.controllermanagementsurface.js +110 -0
- package/dist_ts/functions.inferencecli.d.ts +6 -3
- package/dist_ts/functions.inferencecli.js +9 -5
- package/dist_ts/functions.inferencecliarguments.d.ts +1 -1
- package/dist_ts/functions.inferencecliarguments.js +19 -7
- package/dist_ts/functions.inferencelaunch.d.ts +30 -9
- package/dist_ts/functions.inferencelaunch.js +49 -23
- package/dist_ts/interfaces.auth.d.ts +14 -3
- package/dist_ts/interfaces.auth.js +4 -3
- package/dist_ts_interfaces/index.d.ts +1 -0
- package/dist_ts_interfaces/index.js +2 -1
- package/dist_ts_interfaces/interfaces.d.ts +6 -1
- package/dist_ts_interfaces/interfaces.js +2 -2
- package/dist_ts_interfaces/management.d.ts +33 -0
- package/dist_ts_interfaces/management.js +35 -0
- package/dist_ts_interfaces/metrics.d.ts +29 -7
- package/dist_ts_interfaces/metrics.js +14 -1
- package/package.json +6 -5
- package/readme.md +123 -28
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/aglapi/aglapisurface.document.ts +6 -2
- package/ts/classes.authmodels.ts +74 -5
- package/ts/classes.authstore.ts +24 -4
- package/ts/classes.cli.ts +9 -2
- package/ts/classes.controller.ts +98 -59
- package/ts/classes.controllermanagement.ts +284 -107
- package/ts/classes.inferencegateway.ts +31 -19
- package/ts/classes.inferenceusage.ts +58 -19
- package/ts/classes.inferenceusagetap.ts +27 -8
- package/ts/classes.processinspection.ts +2 -1
- package/ts/classes.proxycontrol.ts +25 -9
- package/ts/classes.proxycontrolclient.ts +7 -4
- package/ts/classes.proxydaemon.ts +43 -17
- package/ts/classes.proxyleaseholder.ts +36 -7
- package/ts/classes.proxystore.ts +8 -3
- package/ts/classes.upgradecoordinator.ts +20 -11
- package/ts/classes.upgradetransaction.ts +53 -2
- package/ts/classes.usagefeed.ts +2 -1
- package/ts/functions.controllermanagementsurface.ts +149 -0
- package/ts/functions.inferencecli.ts +15 -7
- package/ts/functions.inferencecliarguments.ts +16 -6
- package/ts/functions.inferencelaunch.ts +57 -23
- package/ts/interfaces.auth.ts +14 -3
- package/ts_interfaces/index.ts +1 -0
- package/ts_interfaces/interfaces.ts +6 -1
- package/ts_interfaces/management.ts +52 -0
- package/ts_interfaces/metrics.ts +34 -7
- package/ts_web/00_commitinfo_data.ts +1 -1
- package/ts_web/classes.controllersocketclient.ts +5 -2
- package/ts_web/elements.metricsboard.ts +68 -18
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import * as plugins from './plugins.js';
|
|
2
2
|
import { parseUpgradeBlockers } from './classes.upgradecoordinator.js';
|
|
3
3
|
import {
|
|
4
|
+
controllerManagementResponseMaximumBytes,
|
|
5
|
+
controllerManagementTypedRequestPath,
|
|
4
6
|
controllerPackageName,
|
|
5
7
|
controllerProtocolVersion,
|
|
6
8
|
controllerServiceUnitName,
|
|
@@ -9,6 +11,7 @@ import {
|
|
|
9
11
|
type IReq_ControllerUpgradeLaunch,
|
|
10
12
|
type IReq_ControllerUpgradePrepareBegin,
|
|
11
13
|
type IReq_ControllerUpgradeFinalize,
|
|
14
|
+
type TControllerManagementTypedRequest,
|
|
12
15
|
type TControllerSessionHarnessId,
|
|
13
16
|
} from '../ts_interfaces/index.js';
|
|
14
17
|
import {
|
|
@@ -27,6 +30,33 @@ export interface IControllerStatusExpectation {
|
|
|
27
30
|
upgradeManagementVersion: number;
|
|
28
31
|
}
|
|
29
32
|
|
|
33
|
+
/**
|
|
34
|
+
* A declared management protocol (`controllerManagementProtocolVersion` in a package, `managementProtocolVersion`
|
|
35
|
+
* in a status): a safe integer of 1 or more.
|
|
36
|
+
*/
|
|
37
|
+
export const isControllerManagementProtocolVersion = (valueArg: unknown): valueArg is number => (
|
|
38
|
+
typeof valueArg === 'number' && Number.isSafeInteger(valueArg) && valueArg >= 1
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
/** A controller a management call is addressed to, by what it declares about its management endpoint. */
|
|
42
|
+
export interface IControllerManagementTarget {
|
|
43
|
+
/**
|
|
44
|
+
* The management protocol the controller declares (`controllerManagementProtocolVersion`): the status's own
|
|
45
|
+
* `managementProtocolVersion` once the controller runs, or what the installed package that runs it declares in
|
|
46
|
+
* its `package.json`. Absent when it declares none, as every release older than the endpoint.
|
|
47
|
+
*/
|
|
48
|
+
managementProtocolVersion?: number;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* A status read: the identity the status must report, and the declaration that decides how it is read, which comes
|
|
53
|
+
* from the package expected to run the controller, since there is no status yet. Every caller states it, absent
|
|
54
|
+
* included.
|
|
55
|
+
*/
|
|
56
|
+
export interface IControllerStatusQuery extends IControllerStatusExpectation {
|
|
57
|
+
managementProtocolVersion: number | undefined;
|
|
58
|
+
}
|
|
59
|
+
|
|
30
60
|
const harnessStates = ['starting', 'ready', 'stopping', 'stopped', 'failed'] as const;
|
|
31
61
|
/** The controller protocol version that added each harness to the status document. */
|
|
32
62
|
const controllerHarnessIntroductions: Record<TControllerSessionHarnessId, number> = {
|
|
@@ -165,6 +195,8 @@ export const assertControllerStatus = (
|
|
|
165
195
|
|| (expectationArg.protocolVersion !== undefined
|
|
166
196
|
&& status.protocolVersion !== expectationArg.protocolVersion)
|
|
167
197
|
|| status.upgradeManagementVersion !== expectationArg.upgradeManagementVersion
|
|
198
|
+
|| (status.managementProtocolVersion !== undefined
|
|
199
|
+
&& !isControllerManagementProtocolVersion(status.managementProtocolVersion))
|
|
168
200
|
|| !Number.isSafeInteger(status.controllerPid)
|
|
169
201
|
|| !Number.isSafeInteger(status.processGroupId)
|
|
170
202
|
|| typeof status.processFingerprint !== 'string'
|
|
@@ -192,109 +224,273 @@ export const assertControllerStatus = (
|
|
|
192
224
|
return readableStatus as IControllerStatus;
|
|
193
225
|
};
|
|
194
226
|
|
|
195
|
-
|
|
227
|
+
/**
|
|
228
|
+
* How this build reaches a controller's management methods, decided by the controller's declaration alone: over its
|
|
229
|
+
* loopback management endpoint when it declares a management protocol, which every later release keeps whatever
|
|
230
|
+
* transport its browser speaks, and over its controller socket when it declares none (a release older than the
|
|
231
|
+
* endpoint, such as a source an upgrade recovery restores), which serves them in every release up to this one.
|
|
232
|
+
* There is no second attempt over the other transport.
|
|
233
|
+
*/
|
|
234
|
+
export const controllerManagementTransport = (
|
|
235
|
+
targetArg: IControllerManagementTarget,
|
|
236
|
+
): 'http' | 'socket' => (
|
|
237
|
+
isControllerManagementProtocolVersion(targetArg.managementProtocolVersion) ? 'http' : 'socket'
|
|
238
|
+
);
|
|
239
|
+
|
|
240
|
+
/** A management request the controller answered with an error, or whose answer this build cannot read. */
|
|
241
|
+
export class ControllerManagementError extends Error {
|
|
242
|
+
constructor(messageArg: string, public readonly code?: string) {
|
|
243
|
+
super(messageArg);
|
|
244
|
+
this.name = 'ControllerManagementError';
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const maximumManagementErrorTextBytes = 4_096;
|
|
249
|
+
|
|
250
|
+
const isPlainRecord = (valueArg: unknown): valueArg is Record<string, unknown> => (
|
|
251
|
+
typeof valueArg === 'object' && valueArg !== null && !Array.isArray(valueArg)
|
|
252
|
+
);
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Reads one answer of the management endpoint: the envelope must answer this very request (method, request
|
|
256
|
+
* instance and correlation) and carry `response`; an `error` is the controller's refusal. Keys this build does not
|
|
257
|
+
* read are left alone, so a later release may add some.
|
|
258
|
+
*/
|
|
259
|
+
const readControllerManagementAnswer = (
|
|
260
|
+
answerArg: unknown,
|
|
261
|
+
requestArg: { method: string; requestInstanceId: string; correlationId: string },
|
|
262
|
+
): unknown => {
|
|
263
|
+
if (
|
|
264
|
+
!isPlainRecord(answerArg)
|
|
265
|
+
|| answerArg.method !== requestArg.method
|
|
266
|
+
|| answerArg.requestInstanceId !== requestArg.requestInstanceId
|
|
267
|
+
|| !isPlainRecord(answerArg.correlation)
|
|
268
|
+
|| answerArg.correlation.id !== requestArg.correlationId
|
|
269
|
+
|| answerArg.correlation.phase !== 'response'
|
|
270
|
+
|| !Object.hasOwn(answerArg, 'response')
|
|
271
|
+
) throw new ControllerManagementError('The controller returned an invalid management answer.', 'protocol_mismatch');
|
|
272
|
+
if (answerArg.error === undefined) return answerArg.response;
|
|
273
|
+
if (!isPlainRecord(answerArg.error) || typeof answerArg.error.text !== 'string') {
|
|
274
|
+
throw new ControllerManagementError('The controller returned an invalid management answer.', 'protocol_mismatch');
|
|
275
|
+
}
|
|
276
|
+
const data = answerArg.error.data;
|
|
277
|
+
const code = isPlainRecord(data) && typeof data.code === 'string' && data.code.length <= 128 ? data.code : undefined;
|
|
278
|
+
const text = Buffer.byteLength(answerArg.error.text, 'utf8') <= maximumManagementErrorTextBytes
|
|
279
|
+
? answerArg.error.text
|
|
280
|
+
: 'The controller refused the management request.';
|
|
281
|
+
throw new ControllerManagementError(text, code);
|
|
282
|
+
};
|
|
283
|
+
|
|
284
|
+
/** Sends one envelope to the management endpoint on 127.0.0.1 and returns the answer's bytes. */
|
|
285
|
+
const postControllerManagementEnvelope = (
|
|
196
286
|
portArg: number,
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
287
|
+
bodyArg: Buffer,
|
|
288
|
+
signalArg: AbortSignal,
|
|
289
|
+
): Promise<Buffer> => new Promise<Buffer>((resolve, reject) => {
|
|
290
|
+
const request = plugins.http.request({
|
|
291
|
+
protocol: 'http:',
|
|
292
|
+
hostname: '127.0.0.1',
|
|
293
|
+
port: portArg,
|
|
294
|
+
path: controllerManagementTypedRequestPath,
|
|
295
|
+
method: 'POST',
|
|
296
|
+
agent: false,
|
|
297
|
+
signal: signalArg,
|
|
298
|
+
headers: {
|
|
299
|
+
Host: `127.0.0.1:${portArg}`,
|
|
300
|
+
'Content-Type': 'application/json',
|
|
301
|
+
Accept: 'application/json',
|
|
302
|
+
'Content-Length': String(bodyArg.byteLength),
|
|
303
|
+
Connection: 'close',
|
|
304
|
+
},
|
|
305
|
+
}, (responseArg) => {
|
|
306
|
+
const statusCode = responseArg.statusCode ?? 0;
|
|
307
|
+
const contentType = responseArg.headers['content-type'];
|
|
308
|
+
if (statusCode !== 200) {
|
|
309
|
+
responseArg.resume();
|
|
310
|
+
reject(new ControllerManagementError(
|
|
311
|
+
`The controller refused the management request with HTTP ${statusCode}.`,
|
|
312
|
+
'http_refused',
|
|
313
|
+
));
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
if (typeof contentType !== 'string' || !/^application\/json(?:;|$)/iu.test(contentType)) {
|
|
317
|
+
responseArg.resume();
|
|
318
|
+
reject(new ControllerManagementError('The controller returned an invalid management answer.', 'protocol_mismatch'));
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
const chunks: Buffer[] = [];
|
|
322
|
+
let byteLength = 0;
|
|
323
|
+
responseArg.on('data', (chunkArg: Buffer | string) => {
|
|
324
|
+
const chunk = Buffer.isBuffer(chunkArg) ? chunkArg : Buffer.from(chunkArg);
|
|
325
|
+
byteLength += chunk.byteLength;
|
|
326
|
+
if (byteLength > controllerManagementResponseMaximumBytes) {
|
|
327
|
+
responseArg.destroy();
|
|
328
|
+
reject(new ControllerManagementError('The controller management answer exceeded its size limit.', 'response_limit'));
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
chunks.push(chunk);
|
|
332
|
+
});
|
|
333
|
+
responseArg.once('end', () => resolve(Buffer.concat(chunks, byteLength)));
|
|
334
|
+
responseArg.once('error', reject);
|
|
335
|
+
});
|
|
336
|
+
request.once('error', reject);
|
|
337
|
+
request.end(bodyArg);
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Sends one management request over loopback HTTP: a TypedRequest envelope of exactly `method`, `request`,
|
|
342
|
+
* `response: null`, `correlation` and `requestInstanceId`, as a TypedRequest client sends it, with no credential
|
|
343
|
+
* beside what the request itself carries.
|
|
344
|
+
*/
|
|
345
|
+
export const requestControllerManagement = async <T extends TControllerManagementTypedRequest>(
|
|
346
|
+
portArg: number,
|
|
347
|
+
methodArg: T['method'],
|
|
348
|
+
requestArg: T['request'],
|
|
349
|
+
optionsArg: { timeoutMs: number },
|
|
350
|
+
): Promise<unknown> => {
|
|
351
|
+
const identity = {
|
|
352
|
+
method: methodArg,
|
|
353
|
+
requestInstanceId: plugins.crypto.randomUUID(),
|
|
354
|
+
correlationId: plugins.crypto.randomUUID(),
|
|
355
|
+
};
|
|
356
|
+
const body = Buffer.from(JSON.stringify({
|
|
357
|
+
method: methodArg,
|
|
358
|
+
request: requestArg,
|
|
359
|
+
response: null,
|
|
360
|
+
correlation: { id: identity.correlationId, phase: 'request' },
|
|
361
|
+
requestInstanceId: identity.requestInstanceId,
|
|
362
|
+
}), 'utf8');
|
|
363
|
+
const timeoutSignal = AbortSignal.timeout(optionsArg.timeoutMs);
|
|
364
|
+
let answerBytes: Buffer;
|
|
365
|
+
try {
|
|
366
|
+
answerBytes = await postControllerManagementEnvelope(portArg, body, timeoutSignal);
|
|
367
|
+
} catch (errorArg) {
|
|
368
|
+
if (timeoutSignal.aborted && !(errorArg instanceof ControllerManagementError)) {
|
|
369
|
+
throw new ControllerManagementError(
|
|
370
|
+
`The controller did not answer ${methodArg} within ${optionsArg.timeoutMs} ms.`,
|
|
371
|
+
'timeout',
|
|
372
|
+
);
|
|
373
|
+
}
|
|
374
|
+
throw errorArg;
|
|
375
|
+
}
|
|
376
|
+
let answer: unknown;
|
|
377
|
+
try {
|
|
378
|
+
const text = answerBytes.toString('utf8');
|
|
379
|
+
if (!Buffer.from(text, 'utf8').equals(answerBytes)) throw new Error('invalid UTF-8');
|
|
380
|
+
answer = JSON.parse(text);
|
|
381
|
+
} catch {
|
|
382
|
+
throw new ControllerManagementError('The controller returned an invalid management answer.', 'protocol_mismatch');
|
|
383
|
+
}
|
|
384
|
+
return readControllerManagementAnswer(answer, identity);
|
|
385
|
+
};
|
|
386
|
+
|
|
387
|
+
/** Fires one management request over the controller socket, for a controller that declares no management protocol. */
|
|
388
|
+
const requestControllerManagementOverSocket = async <T extends TControllerManagementTypedRequest>(
|
|
389
|
+
portArg: number,
|
|
390
|
+
methodArg: T['method'],
|
|
391
|
+
requestArg: T['request'],
|
|
392
|
+
optionsArg: { timeoutMs: number },
|
|
393
|
+
): Promise<unknown> => {
|
|
394
|
+
const signal = AbortSignal.timeout(optionsArg.timeoutMs);
|
|
206
395
|
let socket: plugins.typedsocket.TypedSocket | undefined;
|
|
207
396
|
try {
|
|
208
397
|
socket = await plugins.typedsocket.TypedSocket.createClient(
|
|
209
398
|
new plugins.typedrequest.TypedRouter(),
|
|
210
399
|
`http://127.0.0.1:${portArg}`,
|
|
211
|
-
{
|
|
212
|
-
autoReconnect: false,
|
|
213
|
-
maxRetries: 0,
|
|
214
|
-
abortSignal: abortController.signal,
|
|
215
|
-
},
|
|
400
|
+
{ autoReconnect: false, maxRetries: 0, abortSignal: signal },
|
|
216
401
|
);
|
|
217
|
-
|
|
218
|
-
|
|
402
|
+
return await socket.createTypedRequest<T>(
|
|
403
|
+
methodArg,
|
|
219
404
|
undefined,
|
|
220
|
-
{
|
|
221
|
-
|
|
222
|
-
abortSignal: abortController.signal,
|
|
223
|
-
},
|
|
224
|
-
).fire({ token: tokenArg, targetVersion: targetVersionArg, gracePeriodMs: gracePeriodMsArg });
|
|
225
|
-
if (
|
|
226
|
-
!response
|
|
227
|
-
|| typeof response !== 'object'
|
|
228
|
-
|| Array.isArray(response)
|
|
229
|
-
|| Object.keys(response).length !== 1
|
|
230
|
-
|| response.accepted !== true
|
|
231
|
-
) throw new Error('The controller returned an invalid upgrade preparation response.');
|
|
232
|
-
return response;
|
|
405
|
+
{ timeoutMs: optionsArg.timeoutMs, abortSignal: signal },
|
|
406
|
+
).fire(requestArg);
|
|
233
407
|
} finally {
|
|
234
|
-
clearTimeout(timeout);
|
|
235
408
|
await socket?.stop();
|
|
236
409
|
}
|
|
237
410
|
};
|
|
238
411
|
|
|
412
|
+
const requestManagedController = <T extends TControllerManagementTypedRequest>(
|
|
413
|
+
portArg: number,
|
|
414
|
+
controllerArg: IControllerManagementTarget,
|
|
415
|
+
methodArg: T['method'],
|
|
416
|
+
requestArg: T['request'],
|
|
417
|
+
optionsArg: { timeoutMs: number },
|
|
418
|
+
): Promise<unknown> => controllerManagementTransport(controllerArg) === 'http'
|
|
419
|
+
? requestControllerManagement<T>(portArg, methodArg, requestArg, optionsArg)
|
|
420
|
+
: requestControllerManagementOverSocket<T>(portArg, methodArg, requestArg, optionsArg);
|
|
421
|
+
|
|
422
|
+
export const requestControllerUpgradePrepareBegin = async (
|
|
423
|
+
portArg: number,
|
|
424
|
+
controllerArg: IControllerManagementTarget,
|
|
425
|
+
tokenArg: string,
|
|
426
|
+
targetVersionArg: string,
|
|
427
|
+
gracePeriodMsArg: number,
|
|
428
|
+
): Promise<IReq_ControllerUpgradePrepareBegin['response']> => {
|
|
429
|
+
const response = await requestManagedController<IReq_ControllerUpgradePrepareBegin>(
|
|
430
|
+
portArg,
|
|
431
|
+
controllerArg,
|
|
432
|
+
'controller.upgrade.prepare.begin',
|
|
433
|
+
{ token: tokenArg, targetVersion: targetVersionArg, gracePeriodMs: gracePeriodMsArg },
|
|
434
|
+
{ timeoutMs: controllerUpgradePrepareBeginRequestTimeoutMs },
|
|
435
|
+
);
|
|
436
|
+
if (
|
|
437
|
+
!isPlainRecord(response)
|
|
438
|
+
|| Object.keys(response).length !== 1
|
|
439
|
+
|| response.accepted !== true
|
|
440
|
+
) throw new Error('The controller returned an invalid upgrade preparation response.');
|
|
441
|
+
return { accepted: true };
|
|
442
|
+
};
|
|
443
|
+
|
|
239
444
|
export const requestControllerUpgradeFinalize = async (
|
|
240
445
|
portArg: number,
|
|
446
|
+
controllerArg: IControllerManagementTarget,
|
|
241
447
|
tokenArg: string,
|
|
242
448
|
modeArg: IReq_ControllerUpgradeFinalize['request']['mode'],
|
|
243
449
|
timeoutMsArg = 60_000,
|
|
244
450
|
): Promise<IReq_ControllerUpgradeFinalize['response']> => {
|
|
245
|
-
const
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
451
|
+
const response = await requestManagedController<IReq_ControllerUpgradeFinalize>(
|
|
452
|
+
portArg,
|
|
453
|
+
controllerArg,
|
|
454
|
+
'controller.upgrade.finalize',
|
|
455
|
+
{ token: tokenArg, mode: modeArg },
|
|
456
|
+
{ timeoutMs: timeoutMsArg },
|
|
249
457
|
);
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
'controller.upgrade.finalize',
|
|
253
|
-
undefined,
|
|
254
|
-
{ timeoutMs: timeoutMsArg },
|
|
255
|
-
).fire({ token: tokenArg, mode: modeArg });
|
|
256
|
-
} finally {
|
|
257
|
-
await socket.stop();
|
|
458
|
+
if (!isPlainRecord(response) || response.finalized !== true) {
|
|
459
|
+
throw new Error('The controller returned an invalid upgrade finalization response.');
|
|
258
460
|
}
|
|
461
|
+
const { continued, failed } = response;
|
|
462
|
+
if (
|
|
463
|
+
typeof continued !== 'number' || !Number.isSafeInteger(continued) || continued < 0
|
|
464
|
+
|| typeof failed !== 'number' || !Number.isSafeInteger(failed) || failed < 0
|
|
465
|
+
) throw new Error('The controller returned an invalid upgrade finalization response.');
|
|
466
|
+
return { finalized: true, continued, failed };
|
|
259
467
|
};
|
|
260
468
|
|
|
469
|
+
/**
|
|
470
|
+
* Reads a controller's status over the transport the query's declaration picks. The upgrade calls that follow take
|
|
471
|
+
* the status itself as their target, so from then on the controller's own declaration decides.
|
|
472
|
+
*/
|
|
261
473
|
export const queryControllerStatus = async (
|
|
262
474
|
portArg: number,
|
|
263
|
-
|
|
475
|
+
queryArg: IControllerStatusQuery,
|
|
264
476
|
timeoutMsArg = 2_500,
|
|
265
|
-
): Promise<IControllerStatus> =>
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
autoReconnect: false,
|
|
276
|
-
maxRetries: 0,
|
|
277
|
-
abortSignal: abortController.signal,
|
|
278
|
-
},
|
|
279
|
-
);
|
|
280
|
-
const request = socket.createTypedRequest<IReq_ControllerStatus>(
|
|
281
|
-
'controller.status',
|
|
282
|
-
undefined,
|
|
283
|
-
{ timeoutMs: timeoutMsArg, abortSignal: abortController.signal },
|
|
284
|
-
);
|
|
285
|
-
return assertControllerStatus(await request.fire({}), expectationArg);
|
|
286
|
-
} finally {
|
|
287
|
-
clearTimeout(timeout);
|
|
288
|
-
await socket?.stop();
|
|
289
|
-
}
|
|
290
|
-
};
|
|
477
|
+
): Promise<IControllerStatus> => assertControllerStatus(
|
|
478
|
+
await requestManagedController<IReq_ControllerStatus>(
|
|
479
|
+
portArg,
|
|
480
|
+
queryArg,
|
|
481
|
+
'controller.status',
|
|
482
|
+
{},
|
|
483
|
+
{ timeoutMs: timeoutMsArg },
|
|
484
|
+
),
|
|
485
|
+
queryArg,
|
|
486
|
+
);
|
|
291
487
|
|
|
292
488
|
export const tryQueryControllerStatus = async (
|
|
293
489
|
portArg: number,
|
|
294
|
-
|
|
490
|
+
queryArg: IControllerStatusQuery,
|
|
295
491
|
): Promise<IControllerStatus | undefined> => {
|
|
296
492
|
try {
|
|
297
|
-
return await queryControllerStatus(portArg,
|
|
493
|
+
return await queryControllerStatus(portArg, queryArg);
|
|
298
494
|
} catch {
|
|
299
495
|
return undefined;
|
|
300
496
|
}
|
|
@@ -322,44 +518,25 @@ export const isLoopbackPortListening = async (
|
|
|
322
518
|
|
|
323
519
|
export const requestControllerUpgradeLaunch = async (
|
|
324
520
|
portArg: number,
|
|
521
|
+
controllerArg: IControllerManagementTarget,
|
|
325
522
|
tokenArg: string,
|
|
326
523
|
timeoutMsArg = 15_000,
|
|
327
524
|
): Promise<IReq_ControllerUpgradeLaunch['response']> => {
|
|
328
|
-
const
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
'controller.upgrade.launch',
|
|
344
|
-
undefined,
|
|
345
|
-
{ timeoutMs: timeoutMsArg, abortSignal: abortController.signal },
|
|
346
|
-
).fire({ token: tokenArg });
|
|
347
|
-
if (
|
|
348
|
-
!response
|
|
349
|
-
|| typeof response !== 'object'
|
|
350
|
-
|| response.accepted !== true
|
|
351
|
-
|| !Number.isSafeInteger(response.workerPid)
|
|
352
|
-
|| response.workerPid < 2
|
|
353
|
-
|| typeof response.logFilePath !== 'string'
|
|
354
|
-
|| !plugins.path.isAbsolute(response.logFilePath)
|
|
355
|
-
) {
|
|
356
|
-
throw new Error(`The controller returned an invalid ${currentCliName} upgrade launch response.`);
|
|
357
|
-
}
|
|
358
|
-
return response;
|
|
359
|
-
} finally {
|
|
360
|
-
clearTimeout(timeout);
|
|
361
|
-
await socket?.stop();
|
|
362
|
-
}
|
|
525
|
+
const response = await requestManagedController<IReq_ControllerUpgradeLaunch>(
|
|
526
|
+
portArg,
|
|
527
|
+
controllerArg,
|
|
528
|
+
'controller.upgrade.launch',
|
|
529
|
+
{ token: tokenArg },
|
|
530
|
+
{ timeoutMs: timeoutMsArg },
|
|
531
|
+
);
|
|
532
|
+
const invalid = new Error(`The controller returned an invalid ${currentCliName} upgrade launch response.`);
|
|
533
|
+
if (!isPlainRecord(response) || response.accepted !== true) throw invalid;
|
|
534
|
+
const { workerPid, logFilePath } = response;
|
|
535
|
+
if (
|
|
536
|
+
typeof workerPid !== 'number' || !Number.isSafeInteger(workerPid) || workerPid < 2
|
|
537
|
+
|| typeof logFilePath !== 'string' || !plugins.path.isAbsolute(logFilePath)
|
|
538
|
+
) throw invalid;
|
|
539
|
+
return { accepted: true, workerPid, logFilePath };
|
|
363
540
|
};
|
|
364
541
|
|
|
365
542
|
export const waitUntilControllerExited = async (
|
|
@@ -66,9 +66,9 @@ export interface IInferenceGatewayOptions {
|
|
|
66
66
|
usageLedger: IInferenceUsageLedger;
|
|
67
67
|
/**
|
|
68
68
|
* The confined listener's Unix socket, bound beside the loopback port and made this user's alone (0600, in a private
|
|
69
|
-
* directory that holds nothing else). It accepts confined credentials alone and serves them the
|
|
70
|
-
*
|
|
71
|
-
* no confined listener and admits no confined credential.
|
|
69
|
+
* directory that holds nothing else). It accepts confined credentials alone and serves them the three endpoints Claude
|
|
70
|
+
* Code uses (`confinedEndpoints`) of their pool, the Claude or the ChatGPT pool, exactly as the loopback listener serves
|
|
71
|
+
* Claude Code on that pool; unset, the gateway has no confined listener and admits no confined credential.
|
|
72
72
|
*/
|
|
73
73
|
confinedSocketPath?: string;
|
|
74
74
|
}
|
|
@@ -159,8 +159,8 @@ export interface IInferenceGatewayClientOptions {
|
|
|
159
159
|
*/
|
|
160
160
|
sessionScope?: string;
|
|
161
161
|
/**
|
|
162
|
-
* Where the credential is accepted; `loopback` when unset. A confined credential is a lease of the Claude
|
|
163
|
-
* on the confined listener alone.
|
|
162
|
+
* Where the credential is accepted; `loopback` when unset. A confined credential is a lease of the Claude or the ChatGPT
|
|
163
|
+
* pool, accepted on the confined listener alone, and stays on the pool it was issued for.
|
|
164
164
|
*/
|
|
165
165
|
route?: TInferenceGatewayRoute;
|
|
166
166
|
}
|
|
@@ -233,8 +233,9 @@ const maximumErrorBodyBytes = 64 * 1024;
|
|
|
233
233
|
const catalogTag = (modelsArg: unknown): string =>
|
|
234
234
|
`"${plugins.crypto.createHash('sha256').update(JSON.stringify(modelsArg)).digest('base64url')}"`;
|
|
235
235
|
/**
|
|
236
|
-
* What the confined listener serves, by method and path: the
|
|
237
|
-
* method, path
|
|
236
|
+
* What the confined listener serves, by method and path: the endpoints Claude Code uses, of the Claude pool or, translated,
|
|
237
|
+
* of the ChatGPT pool. Every other method and path, every `/openai/` path among them, is refused there before anything is
|
|
238
|
+
* read or routed.
|
|
238
239
|
*/
|
|
239
240
|
const confinedEndpoints: ReadonlySet<string> = new Set(['GET /anthropic/v1/models', 'POST /anthropic/v1/messages',
|
|
240
241
|
'POST /anthropic/v1/messages/count_tokens']);
|
|
@@ -476,18 +477,24 @@ export const assertInferenceRoutingPolicy = (policyArg: IInferenceRoutingPolicy)
|
|
|
476
477
|
}
|
|
477
478
|
};
|
|
478
479
|
|
|
479
|
-
/**
|
|
480
|
+
/**
|
|
481
|
+
* A confined credential is a lease of the Claude or the ChatGPT pool, admitted only by a gateway with a confined listener;
|
|
482
|
+
* `issuedArg` is the policy it was issued with, which a new policy may not move to another pool.
|
|
483
|
+
*/
|
|
480
484
|
const assertConfinedClient = (policyArg: IInferenceRoutingPolicy, nameArg: string | undefined,
|
|
481
|
-
socketPathArg: string | undefined): void => {
|
|
485
|
+
socketPathArg: string | undefined, issuedArg?: IInferenceRoutingPolicy): void => {
|
|
482
486
|
if (socketPathArg === undefined) throw new Error('The gateway has no confined listener.');
|
|
483
|
-
if (nameArg !== undefined || policyArg.provider !== 'anthropic') {
|
|
484
|
-
throw new Error('A confined credential is a lease of the Claude pool.');
|
|
487
|
+
if (nameArg !== undefined || (policyArg.provider !== 'anthropic' && policyArg.provider !== 'openai')) {
|
|
488
|
+
throw new Error('A confined credential is a lease of the Claude or the ChatGPT pool.');
|
|
489
|
+
}
|
|
490
|
+
if (issuedArg !== undefined && policyArg.provider !== issuedArg.provider) {
|
|
491
|
+
throw new Error('A confined credential stays on the pool it was issued for.');
|
|
485
492
|
}
|
|
486
493
|
};
|
|
487
494
|
|
|
488
495
|
/**
|
|
489
496
|
* Authenticated Responses and Messages endpoints with bounded, cancelable streaming, on a loopback listener and, for
|
|
490
|
-
* confined credentials, a Unix socket that serves
|
|
497
|
+
* confined credentials, a Unix socket that serves Claude Code's endpoints alone.
|
|
491
498
|
*/
|
|
492
499
|
export class InferenceGateway {
|
|
493
500
|
private readonly clients = new Set<IClient>();
|
|
@@ -583,7 +590,7 @@ export class InferenceGateway {
|
|
|
583
590
|
this.clients.add(client);
|
|
584
591
|
return { baseUrl: `http://${this.host}`, digest: digestArg.toString('hex'), setPolicy: (nextArg) => {
|
|
585
592
|
assertInferenceRoutingPolicy(nextArg);
|
|
586
|
-
if (routeArg === 'confined') assertConfinedClient(nextArg, nameArg, this.options.confinedSocketPath);
|
|
593
|
+
if (routeArg === 'confined') assertConfinedClient(nextArg, nameArg, this.options.confinedSocketPath, client.policy);
|
|
587
594
|
client.policy = structuredClone(nextArg);
|
|
588
595
|
}, close: () => {
|
|
589
596
|
this.clients.delete(client);
|
|
@@ -713,11 +720,15 @@ export class InferenceGateway {
|
|
|
713
720
|
try {
|
|
714
721
|
if (routeArg === 'confined') protocol = 'claude';
|
|
715
722
|
client = this.authenticate(requestArg, routeArg);
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
723
|
+
// On either pool the confined listener serves Claude Code's three endpoints alone, as the loopback listener serves
|
|
724
|
+
// Claude Code on that pool; a ChatGPT-pool run never reaches a Responses or OpenAI models endpoint.
|
|
725
|
+
if (routeArg === 'confined' && !confinedEndpoints.has(`${requestArg.method} ${new URL(requestArg.url).pathname}`)) {
|
|
726
|
+
throw new InferenceRoutingError(404, 'not_found', 'The confined listener serves GET /anthropic/v1/models, '
|
|
727
|
+
+ 'POST /anthropic/v1/messages and POST /anthropic/v1/messages/count_tokens alone.');
|
|
720
728
|
}
|
|
729
|
+
// From here on a confined request is answered exactly as the loopback listener answers Claude Code on its pool, its
|
|
730
|
+
// errors included; only the refusals above are the confined listener's own, in Claude's shape.
|
|
731
|
+
if (routeArg === 'confined' && client.policy.provider !== 'anthropic') protocol = undefined;
|
|
721
732
|
if (this.inFlight >= this.limits.total) throw new InferenceRoutingError(429, 'concurrency_limit', 'The gateway has too many requests in flight.', 1);
|
|
722
733
|
if (client.pending >= this.limits.perClient) throw new InferenceRoutingError(429, 'concurrency_limit', 'This gateway client has too many requests.', 1);
|
|
723
734
|
client.pending += 1;
|
|
@@ -998,10 +1009,11 @@ export class InferenceGateway {
|
|
|
998
1009
|
if (spent) this.options.router.recordSpend(accountId, route.loginId, spent);
|
|
999
1010
|
else this.options.router.uncountedSpend(accountId, answeredArg.model ?? route.model.modelId);
|
|
1000
1011
|
} : undefined)
|
|
1001
|
-
: new InferenceClaudeUsageTap(answerFormat, (usageArg) => {
|
|
1012
|
+
: new InferenceClaudeUsageTap(answerFormat, (usageArg, cacheStatedArg) => {
|
|
1002
1013
|
if (route.provider === 'anthropic') this.options.router.countTokens(accountId, usageArg);
|
|
1003
1014
|
recordUsage({ inputTokens: usageArg.inputTokens, outputTokens: usageArg.outputTokens,
|
|
1004
|
-
cacheReadTokens:
|
|
1015
|
+
cacheReadTokens: cacheStatedArg.reads ? usageArg.cacheReadTokens : null,
|
|
1016
|
+
cacheWriteTokens: cacheStatedArg.writes ? usageArg.cacheWrite5mTokens + usageArg.cacheWrite1hTokens : null });
|
|
1005
1017
|
// An API key's answer is priced as it ends, and its spend counts against the key's limit from then on.
|
|
1006
1018
|
}, route.provider === 'anthropic' && route.credential === 'api_key' ? (answeredArg) => {
|
|
1007
1019
|
const spent = inferenceAnthropicSpend(answeredArg, route.model.modelId, Date.now(), inferenceAnthropicServerToolTypes(body));
|