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,16 +1,19 @@
|
|
|
1
1
|
import * as plugins from './plugins.js';
|
|
2
2
|
import { inferenceProviders, type TInferenceProvider } from './functions.proxycontract.js';
|
|
3
|
-
import type { IControllerUsageBucket, TControllerUsageSpan } from '../ts_interfaces/index.js';
|
|
3
|
+
import type { IControllerUsageBucket, IControllerUsageCounts, TControllerUsageSpan } from '../ts_interfaces/index.js';
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* The tokens one answer used, by kind. Input counts only the tokens read neither from nor written to the provider's
|
|
7
7
|
* prompt cache; the cache reads and writes are counted on their own, so the four add up to everything the answer used.
|
|
8
|
+
* `cacheReadTokens` and `cacheWriteTokens` are `null` when the answer did not state them, which is not the same as
|
|
9
|
+
* stating none. Whether the input count then holds them depends on the provider (`controllerUsageTotalIsLowerBound`):
|
|
10
|
+
* an OpenAI account's input counts every input token, an Anthropic account's leaves the cache out.
|
|
8
11
|
*/
|
|
9
12
|
export interface IInferenceTokenCounts {
|
|
10
13
|
inputTokens: number;
|
|
11
14
|
outputTokens: number;
|
|
12
|
-
cacheReadTokens: number;
|
|
13
|
-
cacheWriteTokens: number;
|
|
15
|
+
cacheReadTokens: number | null;
|
|
16
|
+
cacheWriteTokens: number | null;
|
|
14
17
|
}
|
|
15
18
|
|
|
16
19
|
/**
|
|
@@ -33,8 +36,12 @@ export interface IInferenceUsageLedger {
|
|
|
33
36
|
record(recordArg: IInferenceUsageRecord): void;
|
|
34
37
|
}
|
|
35
38
|
|
|
39
|
+
/** The counts a bucket stored before unknown cache counts were kept lacks. */
|
|
40
|
+
type TUnknownCountKey = 'cacheReadUnknownCalls' | 'cacheWriteUnknownCalls';
|
|
41
|
+
const unknownCountKeys: readonly TUnknownCountKey[] = ['cacheReadUnknownCalls', 'cacheWriteUnknownCalls'];
|
|
42
|
+
|
|
36
43
|
/** One minute or hour of one model's tokens on one harness, account and session, as the proxy keeps it. */
|
|
37
|
-
export interface IInferenceUsageBucket extends
|
|
44
|
+
export interface IInferenceUsageBucket extends IControllerUsageCounts {
|
|
38
45
|
/** The bucket's digest: SHA-256 of its span, start and dimensions. */
|
|
39
46
|
id: string;
|
|
40
47
|
span: TControllerUsageSpan;
|
|
@@ -45,16 +52,29 @@ export interface IInferenceUsageBucket extends IInferenceTokenCounts {
|
|
|
45
52
|
harness: string | null;
|
|
46
53
|
accountId: string;
|
|
47
54
|
session: string | null;
|
|
48
|
-
/** The answers counted. */
|
|
49
|
-
calls: number;
|
|
50
55
|
/** When a flush last added to the bucket. */
|
|
51
56
|
updatedAt: Date;
|
|
52
57
|
}
|
|
53
58
|
|
|
54
|
-
|
|
59
|
+
/**
|
|
60
|
+
* A stored bucket. One written before unknown cache counts were kept has neither `cacheReadUnknownCalls` nor
|
|
61
|
+
* `cacheWriteUnknownCalls`: it did not record whether its answers stated their cache reads and writes, so each of its
|
|
62
|
+
* answers reads as one that stated neither.
|
|
63
|
+
*/
|
|
64
|
+
export interface IInferenceUsageDocument extends Omit<IInferenceUsageBucket, TUnknownCountKey>,
|
|
65
|
+
Partial<Pick<IInferenceUsageBucket, TUnknownCountKey>> {
|
|
55
66
|
expiresAt: Date;
|
|
56
67
|
}
|
|
57
68
|
|
|
69
|
+
/** A stored bucket's counts, those of a bucket written before unknown cache counts were kept with every answer unknown. */
|
|
70
|
+
const storedCounts = (documentArg: Omit<IInferenceUsageDocument, 'id' | 'span' | 'start' | 'provider' | 'model' | 'harness'
|
|
71
|
+
| 'accountId' | 'session' | 'updatedAt' | 'expiresAt'>): IControllerUsageCounts => ({
|
|
72
|
+
calls: documentArg.calls, inputTokens: documentArg.inputTokens, outputTokens: documentArg.outputTokens,
|
|
73
|
+
cacheReadTokens: documentArg.cacheReadTokens, cacheWriteTokens: documentArg.cacheWriteTokens,
|
|
74
|
+
cacheReadUnknownCalls: documentArg.cacheReadUnknownCalls ?? documentArg.calls,
|
|
75
|
+
cacheWriteUnknownCalls: documentArg.cacheWriteUnknownCalls ?? documentArg.calls,
|
|
76
|
+
});
|
|
77
|
+
|
|
58
78
|
/** A usage flush that failed, as the proxy reports it: no session id, prompt or account id. */
|
|
59
79
|
export interface IInferenceUsageFlushFailure {
|
|
60
80
|
/** The buckets kept for the next flush. */
|
|
@@ -96,10 +116,14 @@ const pageDocuments = 128;
|
|
|
96
116
|
|
|
97
117
|
const validDate = (valueArg: unknown): valueArg is Date => valueArg instanceof Date && Number.isFinite(valueArg.getTime());
|
|
98
118
|
const validCount = (valueArg: unknown): valueArg is number => Number.isSafeInteger(valueArg) && (valueArg as number) >= 0;
|
|
119
|
+
/** A count of a bucket's answers: whole, and at most the answers it counts. */
|
|
120
|
+
const countWithin = (countArg: unknown, callsArg: unknown): boolean => validCount(countArg) && validCount(callsArg) && countArg <= callsArg;
|
|
99
121
|
|
|
100
122
|
const assertDocument: (valueArg: unknown) => asserts valueArg is IInferenceUsageDocument = (valueArg) => {
|
|
101
123
|
const value = valueArg as Partial<IInferenceUsageDocument> | null;
|
|
102
|
-
|
|
124
|
+
// A bucket written before unknown cache counts were kept has neither of them; every bucket written since has both.
|
|
125
|
+
const kept = !!value && typeof value === 'object' && Object.hasOwn(value, 'cacheWriteUnknownCalls');
|
|
126
|
+
if (!value || typeof value !== 'object' || Object.keys(value).length !== 15 + (kept ? unknownCountKeys.length : 0)
|
|
103
127
|
|| typeof value.id !== 'string' || !hex64.test(value.id)
|
|
104
128
|
|| !usageSpans.includes(value.span as TControllerUsageSpan)
|
|
105
129
|
|| !validDate(value.start) || value.start.getTime() % spanMs[value.span as TControllerUsageSpan] !== 0
|
|
@@ -111,6 +135,7 @@ const assertDocument: (valueArg: unknown) => asserts valueArg is IInferenceUsage
|
|
|
111
135
|
|| !validCount(value.calls) || value.calls < 1
|
|
112
136
|
|| !validCount(value.inputTokens) || !validCount(value.outputTokens)
|
|
113
137
|
|| !validCount(value.cacheReadTokens) || !validCount(value.cacheWriteTokens)
|
|
138
|
+
|| (kept && !unknownCountKeys.every((keyArg) => countWithin(value[keyArg], value.calls)))
|
|
114
139
|
|| !validDate(value.updatedAt) || !validDate(value.expiresAt)) {
|
|
115
140
|
throw new Error('Invalid inference usage bucket.');
|
|
116
141
|
}
|
|
@@ -134,6 +159,9 @@ export class InferenceUsageModel extends plugins.nosqldb.SmartDataDbDoc<Inferenc
|
|
|
134
159
|
@plugins.nosqldb.svDb() public outputTokens!: number;
|
|
135
160
|
@plugins.nosqldb.svDb() public cacheReadTokens!: number;
|
|
136
161
|
@plugins.nosqldb.svDb() public cacheWriteTokens!: number;
|
|
162
|
+
/** These two are absent only on a bucket written before unknown cache counts were kept; every write sets them. */
|
|
163
|
+
@plugins.nosqldb.svDb() public cacheReadUnknownCalls?: number;
|
|
164
|
+
@plugins.nosqldb.svDb() public cacheWriteUnknownCalls?: number;
|
|
137
165
|
@plugins.nosqldb.svDb()
|
|
138
166
|
@plugins.nosqldb.index() public updatedAt!: Date;
|
|
139
167
|
@plugins.nosqldb.svDb()
|
|
@@ -259,9 +287,11 @@ export class InferenceUsageStore implements IInferenceUsageLedger {
|
|
|
259
287
|
public record(recordArg: IInferenceUsageRecord): void {
|
|
260
288
|
try {
|
|
261
289
|
if (this.stopped) return;
|
|
262
|
-
const counts = [recordArg.inputTokens, recordArg.outputTokens, recordArg.cacheReadTokens, recordArg.cacheWriteTokens];
|
|
290
|
+
const counts = [recordArg.inputTokens, recordArg.outputTokens, recordArg.cacheReadTokens ?? 0, recordArg.cacheWriteTokens ?? 0];
|
|
263
291
|
if (!inferenceProviders.includes(recordArg.provider) || !usageModelPattern.test(recordArg.model)
|
|
264
292
|
|| (recordArg.harness !== null && !usageHarnessPattern.test(recordArg.harness)) || !hex64.test(recordArg.accountId)
|
|
293
|
+
|| (recordArg.cacheReadTokens !== null && !validCount(recordArg.cacheReadTokens))
|
|
294
|
+
|| (recordArg.cacheWriteTokens !== null && !validCount(recordArg.cacheWriteTokens))
|
|
265
295
|
|| !counts.every(validCount) || counts.every((countArg) => countArg === 0)) {
|
|
266
296
|
this.droppedRecords += 1;
|
|
267
297
|
return;
|
|
@@ -280,12 +310,12 @@ export class InferenceUsageStore implements IInferenceUsageLedger {
|
|
|
280
310
|
this.droppedRecords += 1;
|
|
281
311
|
return;
|
|
282
312
|
}
|
|
313
|
+
const answer = answerCounts(recordArg);
|
|
283
314
|
for (const { span, start, id } of buckets) {
|
|
284
315
|
const held = this.pending.get(id);
|
|
285
|
-
if (held) { addCounts(held,
|
|
316
|
+
if (held) { addCounts(held, answer); continue; }
|
|
286
317
|
this.pending.set(id, { id, span, start: new Date(start), provider: recordArg.provider, model: recordArg.model,
|
|
287
|
-
harness: recordArg.harness, accountId: recordArg.accountId, session,
|
|
288
|
-
outputTokens: recordArg.outputTokens, cacheReadTokens: recordArg.cacheReadTokens, cacheWriteTokens: recordArg.cacheWriteTokens });
|
|
318
|
+
harness: recordArg.harness, accountId: recordArg.accountId, session, ...answer });
|
|
289
319
|
}
|
|
290
320
|
} catch {
|
|
291
321
|
this.droppedRecords += 1;
|
|
@@ -364,7 +394,7 @@ export class InferenceUsageStore implements IInferenceUsageLedger {
|
|
|
364
394
|
// The buckets that were not written join those recorded since, as far as the pending limit allows.
|
|
365
395
|
for (const bucket of failed) {
|
|
366
396
|
const held = this.pending.get(bucket.id);
|
|
367
|
-
if (held) addCounts(held, bucket
|
|
397
|
+
if (held) addCounts(held, bucket);
|
|
368
398
|
else if (this.pending.size < maximumPendingBuckets) this.pending.set(bucket.id, bucket);
|
|
369
399
|
else this.droppedRecords += bucket.calls;
|
|
370
400
|
}
|
|
@@ -388,7 +418,10 @@ export class InferenceUsageStore implements IInferenceUsageLedger {
|
|
|
388
418
|
return;
|
|
389
419
|
}
|
|
390
420
|
const result = await InferenceUsageModel.exact.transition({ current, change: (modelArg) => {
|
|
391
|
-
|
|
421
|
+
// A bucket written before unknown cache counts were kept gets them, its own answers as unknown.
|
|
422
|
+
const counts = storedCounts(modelArg);
|
|
423
|
+
addCounts(counts, bucketArg);
|
|
424
|
+
Object.assign(modelArg, counts);
|
|
392
425
|
modelArg.updatedAt = updatedAt;
|
|
393
426
|
} }, { signal: signalArg });
|
|
394
427
|
if (result.status === 'transitioned') {
|
|
@@ -418,15 +451,21 @@ export class InferenceUsageStore implements IInferenceUsageLedger {
|
|
|
418
451
|
}
|
|
419
452
|
|
|
420
453
|
/** A stored bucket as a reader receives it: its start in Unix milliseconds, without its bookkeeping. */
|
|
421
|
-
const wireBucket = (documentArg:
|
|
454
|
+
const wireBucket = (documentArg: IInferenceUsageDocument): IControllerUsageBucket => ({ id: documentArg.id, span: documentArg.span,
|
|
422
455
|
start: documentArg.start.getTime(), provider: documentArg.provider, model: documentArg.model, harness: documentArg.harness,
|
|
423
|
-
accountId: documentArg.accountId, session: documentArg.session,
|
|
424
|
-
|
|
456
|
+
accountId: documentArg.accountId, session: documentArg.session, ...storedCounts(documentArg) });
|
|
457
|
+
|
|
458
|
+
/** One answer as a bucket counts it: the cache counts it stated, and for each it did not, one answer unknown. */
|
|
459
|
+
const answerCounts = (countsArg: IInferenceTokenCounts): IControllerUsageCounts => ({ calls: 1, inputTokens: countsArg.inputTokens,
|
|
460
|
+
outputTokens: countsArg.outputTokens, cacheReadTokens: countsArg.cacheReadTokens ?? 0, cacheWriteTokens: countsArg.cacheWriteTokens ?? 0,
|
|
461
|
+
cacheReadUnknownCalls: countsArg.cacheReadTokens === null ? 1 : 0, cacheWriteUnknownCalls: countsArg.cacheWriteTokens === null ? 1 : 0 });
|
|
425
462
|
|
|
426
|
-
const addCounts = (targetArg:
|
|
427
|
-
targetArg.calls +=
|
|
463
|
+
const addCounts = (targetArg: IControllerUsageCounts, addArg: IControllerUsageCounts): void => {
|
|
464
|
+
targetArg.calls += addArg.calls;
|
|
428
465
|
targetArg.inputTokens += addArg.inputTokens;
|
|
429
466
|
targetArg.outputTokens += addArg.outputTokens;
|
|
430
467
|
targetArg.cacheReadTokens += addArg.cacheReadTokens;
|
|
431
468
|
targetArg.cacheWriteTokens += addArg.cacheWriteTokens;
|
|
469
|
+
targetArg.cacheReadUnknownCalls += addArg.cacheReadUnknownCalls;
|
|
470
|
+
targetArg.cacheWriteUnknownCalls += addArg.cacheWriteUnknownCalls;
|
|
432
471
|
};
|
|
@@ -29,6 +29,16 @@ interface IStatedUsage {
|
|
|
29
29
|
ephemeral_1h_input_tokens?: number;
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
+
/**
|
|
33
|
+
* Which cache counts an Anthropic Messages answer stated: `reads` its `cache_read_input_tokens`, `writes` its
|
|
34
|
+
* `cache_creation_input_tokens` or its `cache_creation` split. Anthropic documents both as `number or null`, and a
|
|
35
|
+
* translated answer gives null for a count the model did not state.
|
|
36
|
+
*/
|
|
37
|
+
export interface IInferenceClaudeCacheStated {
|
|
38
|
+
reads: boolean;
|
|
39
|
+
writes: boolean;
|
|
40
|
+
}
|
|
41
|
+
|
|
32
42
|
/** What the relay hands each chunk of an answer it passed on, and the end of the answer. */
|
|
33
43
|
export interface IInferenceUsageTap {
|
|
34
44
|
chunk(bytesArg: Uint8Array): void;
|
|
@@ -43,7 +53,9 @@ export interface IInferenceUsageTap {
|
|
|
43
53
|
* A stream (`sse`) states usage in `message_start` (`message.usage`, `message.model`) and in `message_delta` (`usage`,
|
|
44
54
|
* cumulative), so only those events' data lines are parsed and every other line is skipped unread; a cut stream reports
|
|
45
55
|
* what arrived. A JSON answer states `usage` and `model` at its top level and is read at its end. Anything unreadable
|
|
46
|
-
* reports nothing: the estimate is the authority's, and the answer is never at stake.
|
|
56
|
+
* reports nothing: the estimate is the authority's, and the answer is never at stake. The report says which cache
|
|
57
|
+
* counts the answer stated (`IInferenceClaudeCacheStated`): one it did not state is unknown, not none. The usage
|
|
58
|
+
* reported for the estimate counts an unknown one as none, as the account service's token usage has no unknown.
|
|
47
59
|
*
|
|
48
60
|
* `answered`, given for an answer an API key served, takes at the end what the answer stated for its price: the model
|
|
49
61
|
* that answered and its usage object as sent, a stream's merged field by field from `message_start` and the
|
|
@@ -66,7 +78,8 @@ export class InferenceClaudeUsageTap implements IInferenceUsageTap {
|
|
|
66
78
|
private stopped = false;
|
|
67
79
|
private ended = false;
|
|
68
80
|
|
|
69
|
-
constructor(private readonly format: 'sse' | 'json',
|
|
81
|
+
constructor(private readonly format: 'sse' | 'json',
|
|
82
|
+
private readonly report: (usageArg: TTokenUsage, cacheStatedArg: IInferenceClaudeCacheStated) => void,
|
|
70
83
|
private readonly answered?: (answeredArg: IInferenceAnsweredUsage) => void) {}
|
|
71
84
|
|
|
72
85
|
/** Reads a chunk the relay passed on; never throws. */
|
|
@@ -102,7 +115,11 @@ export class InferenceClaudeUsageTap implements IInferenceUsageTap {
|
|
|
102
115
|
}
|
|
103
116
|
this.stopped = true;
|
|
104
117
|
const usage = this.usage();
|
|
105
|
-
if (usage)
|
|
118
|
+
if (usage) {
|
|
119
|
+
this.report(usage, { reads: this.stated.cache_read_input_tokens !== undefined,
|
|
120
|
+
writes: this.stated.cache_creation_input_tokens !== undefined || this.stated.ephemeral_5m_input_tokens !== undefined
|
|
121
|
+
|| this.stated.ephemeral_1h_input_tokens !== undefined });
|
|
122
|
+
}
|
|
106
123
|
} catch { this.stopped = true; } finally {
|
|
107
124
|
try {
|
|
108
125
|
this.answered?.({ ...(this.model === undefined ? {} : { model: this.model }), ...(this.raw ? { usage: this.raw } : {}),
|
|
@@ -207,7 +224,8 @@ const searchCallTypes: ReadonlyMap<string, 'web' | 'file'> = new Map([['web_sear
|
|
|
207
224
|
* `response.output_item.done`; a data-only frame is kept up to the frame limit and parsed only when it names a terminal
|
|
208
225
|
* type, or a finished output item that is a search call. A JSON answer states `usage` at its top level. `input_tokens`
|
|
209
226
|
* counts every input token, so the cached reads (`cached_tokens`) and cache writes (`cache_write_tokens`) of
|
|
210
|
-
* `input_tokens_details` are taken out of it.
|
|
227
|
+
* `input_tokens_details` are taken out of it. An answer that does not state `cached_tokens` or `cache_write_tokens`
|
|
228
|
+
* reports those as unknown (`null`), never as none, and its input count then holds whatever it read or wrote.
|
|
211
229
|
*
|
|
212
230
|
* `answered`, given for an answer an OpenAI API key served, takes at the end what the answer stated for its price
|
|
213
231
|
* (`IInferenceResponsesAnswered`): whether its terminal event or its JSON body was read, arrived too large to read, or
|
|
@@ -380,10 +398,11 @@ export class InferenceResponsesUsageTap implements IInferenceUsageTap {
|
|
|
380
398
|
const output = count(usage?.output_tokens);
|
|
381
399
|
if (input === undefined && output === undefined) return undefined;
|
|
382
400
|
const details = record(usage?.input_tokens_details);
|
|
383
|
-
const cacheRead = count(details?.cached_tokens) ??
|
|
384
|
-
const cacheWrite = count(details?.cache_write_tokens) ??
|
|
385
|
-
const counts: IInferenceTokenCounts = { inputTokens: Math.max(0, (input ?? 0) - cacheRead - cacheWrite),
|
|
401
|
+
const cacheRead = count(details?.cached_tokens) ?? null;
|
|
402
|
+
const cacheWrite = count(details?.cache_write_tokens) ?? null;
|
|
403
|
+
const counts: IInferenceTokenCounts = { inputTokens: Math.max(0, (input ?? 0) - (cacheRead ?? 0) - (cacheWrite ?? 0)),
|
|
386
404
|
outputTokens: output ?? 0, cacheReadTokens: cacheRead, cacheWriteTokens: cacheWrite };
|
|
387
|
-
return counts.inputTokens + counts.outputTokens + counts.cacheReadTokens + counts.cacheWriteTokens
|
|
405
|
+
return counts.inputTokens + counts.outputTokens + (counts.cacheReadTokens ?? 0) + (counts.cacheWriteTokens ?? 0) > 0
|
|
406
|
+
? counts : undefined;
|
|
388
407
|
}
|
|
389
408
|
}
|
|
@@ -483,7 +483,8 @@ const readEffectiveArgvPort = (argumentsArg: string[]): number => {
|
|
|
483
483
|
export const processIdentityHasCliCommand = async (
|
|
484
484
|
identityArg: IControllerProcessIdentity,
|
|
485
485
|
cliPathArg: string,
|
|
486
|
-
|
|
486
|
+
/** The CLI command the process runs; an upgrade owner may name a later release's (`IUpgradeProcessOwner`). */
|
|
487
|
+
commandArg: string,
|
|
487
488
|
optionsArg: { allowMissingAbsolutePath?: boolean } = {},
|
|
488
489
|
): Promise<boolean> => {
|
|
489
490
|
const cliIndex = await findCliArgumentIndex(
|
|
@@ -72,6 +72,8 @@ export interface IAglProxyStatus {
|
|
|
72
72
|
export interface IAglProxyConfinedRunStatus {
|
|
73
73
|
/** The run's id: the first 12 hex characters of its credential's SHA-256 digest, never the credential. */
|
|
74
74
|
id: string;
|
|
75
|
+
/** The pool the run is served from: Claude accounts (`anthropic`) or ChatGPT accounts (`openai`). */
|
|
76
|
+
provider: TInferenceProvider;
|
|
75
77
|
createdAt: string;
|
|
76
78
|
/** False while a restarted proxy serves the credential for its reclaim window, waiting for its holder to take it back. */
|
|
77
79
|
held: boolean;
|
|
@@ -148,24 +150,35 @@ export interface IReq_AglProxyReclaim extends ITypedRequest {
|
|
|
148
150
|
|
|
149
151
|
/**
|
|
150
152
|
* A confined run's credential: accepted on the confined listener alone, never on the loopback one, and served there from
|
|
151
|
-
* the Claude pool exactly as the loopback listener serves
|
|
152
|
-
*
|
|
153
|
-
*
|
|
154
|
-
*
|
|
153
|
+
* the pool `provider` names -- the Claude pool when it names none -- exactly as the loopback listener serves Claude Code on
|
|
154
|
+
* that pool, its sessions those of the client `aglProxyConfinedClient` on that pool. A ChatGPT-pool run is served by the
|
|
155
|
+
* pool's ChatGPT accounts alone, since a translated Messages request never goes to an API key. Like a lease it lives as
|
|
156
|
+
* long as the connection that asked for it, a restarted proxy keeps it for its holder to take back
|
|
157
|
+
* (`agl.proxy.reclaim`), and the proxy keeps only its digest. `agl.proxy.confined.end` ends it at once.
|
|
155
158
|
*/
|
|
156
159
|
export interface IReq_AglProxyConfined extends ITypedRequest {
|
|
157
160
|
method: 'agl.proxy.confined';
|
|
158
|
-
request
|
|
161
|
+
/** An empty request asks for a Claude-pool run, as every proxy since confined runs takes it. */
|
|
162
|
+
request: { provider?: TInferenceProvider };
|
|
159
163
|
response: IAglProxyConfinedGrant;
|
|
160
164
|
}
|
|
161
165
|
|
|
162
|
-
/**
|
|
166
|
+
/**
|
|
167
|
+
* What a confined run is given: its id, its pool, its credential, the confined listener's socket and the origin to name
|
|
168
|
+
* with it, and, for a ChatGPT-pool run, the pool's models as the proxy read them when it issued the credential, which the
|
|
169
|
+
* launcher configures Claude Code with as `agl proxy run claude --provider openai` does. A Claude-pool run gets none:
|
|
170
|
+
* Claude Code reads that pool's models through the confined listener.
|
|
171
|
+
*/
|
|
163
172
|
export interface IAglProxyConfinedGrant {
|
|
164
173
|
/** See `IAglProxyConfinedRunStatus.id`. */
|
|
165
174
|
id: string;
|
|
175
|
+
/** The run's pool; a proxy older than ChatGPT-pool runs names none, and issues Claude-pool runs alone. */
|
|
176
|
+
provider?: TInferenceProvider;
|
|
166
177
|
token: string;
|
|
167
178
|
socketPath: string;
|
|
168
179
|
baseUrl: string;
|
|
180
|
+
/** A ChatGPT-pool run's models, the pool's default among them; absent for a Claude-pool run. */
|
|
181
|
+
models?: TInferenceModel[];
|
|
169
182
|
}
|
|
170
183
|
|
|
171
184
|
/**
|
|
@@ -207,7 +220,7 @@ export interface IAglProxyControlHandlers {
|
|
|
207
220
|
catalog(requestArg: IReq_AglProxyCatalog['request'], connectionArg: IAglProxyConnection): Promise<IReq_AglProxyCatalog['response']>;
|
|
208
221
|
lease(requestArg: IReq_AglProxyLease['request'], connectionArg: IAglProxyConnection): Promise<IReq_AglProxyLease['response']>;
|
|
209
222
|
reclaim(requestArg: IReq_AglProxyReclaim['request'], connectionArg: IAglProxyConnection): Promise<IReq_AglProxyReclaim['response']>;
|
|
210
|
-
confined(connectionArg: IAglProxyConnection): Promise<IReq_AglProxyConfined['response']>;
|
|
223
|
+
confined(requestArg: IReq_AglProxyConfined['request'], connectionArg: IAglProxyConnection): Promise<IReq_AglProxyConfined['response']>;
|
|
211
224
|
confinedEnd(requestArg: IReq_AglProxyConfinedEnd['request']): Promise<IReq_AglProxyConfinedEnd['response']>;
|
|
212
225
|
usage(requestArg: IReq_AglProxyUsage['request'], connectionArg: IAglProxyConnection): Promise<IReq_AglProxyUsage['response']>;
|
|
213
226
|
}
|
|
@@ -303,8 +316,11 @@ export class AglProxyControlServer {
|
|
|
303
316
|
return this.handlers!.reclaim(requestArg, connectionArg);
|
|
304
317
|
}));
|
|
305
318
|
this.router.addTypedHandler(handler<IReq_AglProxyConfined>('agl.proxy.confined', (requestArg, connectionArg) => {
|
|
306
|
-
if (!isRecord(requestArg) || Object.keys(requestArg).
|
|
307
|
-
|
|
319
|
+
if (!isRecord(requestArg) || Object.keys(requestArg).some((keyArg) => keyArg !== 'provider')
|
|
320
|
+
|| (requestArg.provider !== undefined && !inferenceProviders.includes(requestArg.provider))) {
|
|
321
|
+
throw new AglProxyControlRefusal('Invalid proxy confined request.');
|
|
322
|
+
}
|
|
323
|
+
return this.handlers!.confined(requestArg, connectionArg);
|
|
308
324
|
}));
|
|
309
325
|
this.router.addTypedHandler(handler<IReq_AglProxyConfinedEnd>('agl.proxy.confined.end', (requestArg) => {
|
|
310
326
|
if (!isRecord(requestArg) || typeof requestArg.id !== 'string' || !aglProxyConfinedIdPattern.test(requestArg.id)
|
|
@@ -190,12 +190,15 @@ export class AglProxyControlClient {
|
|
|
190
190
|
}
|
|
191
191
|
|
|
192
192
|
/**
|
|
193
|
-
* A confined run's credential, the confined listener's socket and the origin to name
|
|
194
|
-
* long as the returned connection (see `IReq_AglProxyConfined`).
|
|
193
|
+
* A confined run's credential on the pool `providerArg` names, the confined listener's socket and the origin to name
|
|
194
|
+
* with it; the credential lives as long as the returned connection (see `IReq_AglProxyConfined`). A Claude-pool run is
|
|
195
|
+
* asked for with the empty request every proxy since confined runs takes.
|
|
195
196
|
*/
|
|
196
|
-
public async confined(
|
|
197
|
+
public async confined(providerArg: TInferenceProvider,
|
|
198
|
+
signalArg: AbortSignal): Promise<IReq_AglProxyConfined['response'] & IAglProxyLeaseConnection> {
|
|
197
199
|
let held: { socket: net.Socket; rest: Buffer } | undefined;
|
|
198
|
-
const
|
|
200
|
+
const request: IReq_AglProxyConfined['request'] = providerArg === 'anthropic' ? {} : { provider: providerArg };
|
|
201
|
+
const response = await exchange<IReq_AglProxyConfined['response']>(this.socketPath, 'agl.proxy.confined', request,
|
|
199
202
|
signalArg, (socketArg, restArg) => { held = { socket: socketArg, rest: restArg }; });
|
|
200
203
|
return { ...response, ...leaseConnection(held) };
|
|
201
204
|
}
|
|
@@ -8,7 +8,7 @@ import { InferenceRoutingError, type IInferenceRoutingPolicy } from './classes.i
|
|
|
8
8
|
import { aglProxyConfinedClient, aglProxyConfinedOrigin, proxyClientNamePattern, type TInferenceProvider,
|
|
9
9
|
type TProxyCredentialDelivery } from './functions.proxycontract.js';
|
|
10
10
|
import { AglProxyControlRefusal, AglProxyControlServer, type IAglProxyConfinedGrant, type IAglProxyConnection,
|
|
11
|
-
type IAglProxyStatus, type IReq_AglProxyCatalog, type IReq_AglProxyReclaim,
|
|
11
|
+
type IAglProxyStatus, type IReq_AglProxyCatalog, type IReq_AglProxyConfined, type IReq_AglProxyReclaim,
|
|
12
12
|
type TAglProxyLeaseCatalog } from './classes.proxycontrol.js';
|
|
13
13
|
import { ProxyStore, type IProxySecretCodec } from './classes.proxystore.js';
|
|
14
14
|
import type { InferenceUsageStore } from './classes.inferenceusage.js';
|
|
@@ -51,8 +51,16 @@ const catalogRefusal = (errorArg: unknown): AglProxyControlRefusal => new AglPro
|
|
|
51
51
|
errorArg instanceof InferenceRoutingError ? errorArg.code : undefined,
|
|
52
52
|
);
|
|
53
53
|
|
|
54
|
-
/**
|
|
55
|
-
|
|
54
|
+
/**
|
|
55
|
+
* The policy of a confined run's credential, by its pool: the pool, unrestricted, as Claude Code's own lease on the Claude
|
|
56
|
+
* pool and `agl proxy run claude --provider openai`'s on the ChatGPT pool.
|
|
57
|
+
*/
|
|
58
|
+
const confinedPolicies: Readonly<Record<TInferenceProvider, IInferenceRoutingPolicy>> = Object.freeze({
|
|
59
|
+
anthropic: Object.freeze({ provider: 'anthropic' }),
|
|
60
|
+
// A ChatGPT-pool run's policy is the one `agl proxy run claude --provider openai` leases: its translated Messages
|
|
61
|
+
// requests go to the pool's ChatGPT accounts alone, never to an API key (see `IReq_AglProxyConfined`).
|
|
62
|
+
openai: Object.freeze({ provider: 'openai' }),
|
|
63
|
+
});
|
|
56
64
|
|
|
57
65
|
/** A confined run's id, as the operator names it: the first 12 hex characters of its credential's digest. */
|
|
58
66
|
const confinedRunId = (digestArg: string): string => digestArg.slice(0, 12);
|
|
@@ -61,6 +69,8 @@ const confinedRunId = (digestArg: string): string => digestArg.slice(0, 12);
|
|
|
61
69
|
interface IHeldLease {
|
|
62
70
|
grant: IInferenceGatewayGrant;
|
|
63
71
|
route: TInferenceGatewayRoute;
|
|
72
|
+
/** The pool the lease is served from; a policy that names none is the ChatGPT pool's, as the router reads it. */
|
|
73
|
+
provider: TInferenceProvider;
|
|
64
74
|
createdAt: Date;
|
|
65
75
|
/** The connection the lease lives as long as, while a holder has it. */
|
|
66
76
|
holder?: IAglProxyConnection;
|
|
@@ -146,7 +156,7 @@ export class AglProxyDaemon {
|
|
|
146
156
|
catalog: (requestArg, connectionArg) => this.catalog(requestArg, connectionArg),
|
|
147
157
|
lease: (requestArg, connectionArg) => this.lease(requestArg.policy, requestArg.client, connectionArg),
|
|
148
158
|
reclaim: async (requestArg, connectionArg) => this.reclaim(requestArg.token, connectionArg),
|
|
149
|
-
confined: (connectionArg) => this.confined(connectionArg),
|
|
159
|
+
confined: (requestArg, connectionArg) => this.confined(requestArg, connectionArg),
|
|
150
160
|
confinedEnd: async (requestArg) => this.confinedEnd(requestArg.id),
|
|
151
161
|
usage: async (requestArg, connectionArg) => 'since' in requestArg ? this.usage!.changes(requestArg)
|
|
152
162
|
: await this.usage!.snapshot(requestArg, connectionArg.signal),
|
|
@@ -168,8 +178,8 @@ export class AglProxyDaemon {
|
|
|
168
178
|
recentPlacements: status.recentPlacements, confinedSocketPath: this.options.confinedSocketPath ?? null,
|
|
169
179
|
confinedRuns: [...this.leases].filter(([, heldArg]) => heldArg.route === 'confined')
|
|
170
180
|
.sort(([, left], [, right]) => left.createdAt.getTime() - right.createdAt.getTime())
|
|
171
|
-
.map(([digestArg, heldArg]) => ({ id: confinedRunId(digestArg),
|
|
172
|
-
held: heldArg.reclaimTimer === undefined })),
|
|
181
|
+
.map(([digestArg, heldArg]) => ({ id: confinedRunId(digestArg), provider: heldArg.provider,
|
|
182
|
+
createdAt: heldArg.createdAt.toISOString(), held: heldArg.reclaimTimer === undefined })),
|
|
173
183
|
};
|
|
174
184
|
}
|
|
175
185
|
|
|
@@ -289,7 +299,8 @@ export class AglProxyDaemon {
|
|
|
289
299
|
// The lease continues the sessions of the client it is taken for (see `IReq_AglProxyLease`).
|
|
290
300
|
try { lease = await service.open(policyArg, { sessionScope: clientArg }); }
|
|
291
301
|
catch (errorArg) { throw new AglProxyControlRefusal(errorArg instanceof Error ? errorArg.message : 'The lease could not be opened.'); }
|
|
292
|
-
const held: IHeldLease = { grant: lease, route: 'loopback', createdAt: new Date(),
|
|
302
|
+
const held: IHeldLease = { grant: lease, route: 'loopback', provider: policyArg.provider ?? 'openai', createdAt: new Date(),
|
|
303
|
+
holder: connectionArg };
|
|
293
304
|
this.leases.set(lease.digest, held);
|
|
294
305
|
try {
|
|
295
306
|
await this.trackLeaseWrite(this.store!.recordLease({ digest: lease.digest, client: clientArg, policy: policyArg,
|
|
@@ -319,30 +330,45 @@ export class AglProxyDaemon {
|
|
|
319
330
|
* proves the caller holds the credential the lease was issued with.
|
|
320
331
|
*/
|
|
321
332
|
/**
|
|
322
|
-
* A confined run's credential (see `IReq_AglProxyConfined`): a lease of the
|
|
323
|
-
*
|
|
324
|
-
*
|
|
325
|
-
*
|
|
333
|
+
* A confined run's credential (see `IReq_AglProxyConfined`): a lease of the pool the request names -- the Claude pool
|
|
334
|
+
* when it names none -- that continues the confined client's sessions on that pool and is accepted on the confined
|
|
335
|
+
* listener alone. It is kept, as its digest, before it is handed out, and ends when its holder's connection closes or
|
|
336
|
+
* the operator ends it (`confinedEnd`). A Claude-pool run gets no model list: Claude Code reads the pool's models
|
|
337
|
+
* through the confined listener, and the pool is judged at each request. A ChatGPT-pool run gets the pool's models, as
|
|
338
|
+
* an `agl proxy run` lease does, for its launcher to configure Claude Code with.
|
|
326
339
|
*/
|
|
327
|
-
private async confined(
|
|
340
|
+
private async confined(requestArg: IReq_AglProxyConfined['request'],
|
|
341
|
+
connectionArg: IAglProxyConnection): Promise<IAglProxyConfinedGrant> {
|
|
328
342
|
const socketPath = this.options.confinedSocketPath;
|
|
329
343
|
if (socketPath === undefined) throw new AglProxyControlRefusal('This AGL proxy has no confined listener.');
|
|
344
|
+
const provider = requestArg.provider ?? 'anthropic';
|
|
345
|
+
const policy = confinedPolicies[provider];
|
|
346
|
+
const service = this.service!;
|
|
330
347
|
let lease: IInferenceGatewayLease;
|
|
331
|
-
try { lease = await
|
|
348
|
+
try { lease = await service.open(policy, { sessionScope: aglProxyConfinedClient, route: 'confined' }); }
|
|
332
349
|
catch (errorArg) {
|
|
333
350
|
throw new AglProxyControlRefusal(errorArg instanceof Error ? errorArg.message : 'The confined credential could not be issued.');
|
|
334
351
|
}
|
|
335
|
-
const held: IHeldLease = { grant: lease, route: 'confined', createdAt: new Date(), holder: connectionArg };
|
|
352
|
+
const held: IHeldLease = { grant: lease, route: 'confined', provider, createdAt: new Date(), holder: connectionArg };
|
|
336
353
|
this.leases.set(lease.digest, held);
|
|
337
354
|
try {
|
|
338
355
|
await this.trackLeaseWrite(this.store!.recordLease({ digest: lease.digest, client: aglProxyConfinedClient,
|
|
339
|
-
policy
|
|
356
|
+
policy, createdAt: held.createdAt, route: 'confined' }));
|
|
340
357
|
} catch (errorArg) {
|
|
341
358
|
this.endLease(lease.digest, held);
|
|
342
359
|
throw new AglProxyControlRefusal(errorArg instanceof Error ? errorArg.message : 'The confined credential could not be recorded.');
|
|
343
360
|
}
|
|
344
361
|
connectionArg.onClose(() => this.endLease(lease.digest, held));
|
|
345
|
-
|
|
362
|
+
const grant: IAglProxyConfinedGrant = { id: confinedRunId(lease.digest), provider, token: lease.token, socketPath,
|
|
363
|
+
baseUrl: aglProxyConfinedOrigin };
|
|
364
|
+
if (provider === 'anthropic') return grant;
|
|
365
|
+
try {
|
|
366
|
+
// A launcher that goes away stops the model list read it asked for.
|
|
367
|
+
return { ...grant, models: await service.router.models(policy, AbortSignal.any([connectionArg.signal, AbortSignal.timeout(60_000)])) };
|
|
368
|
+
} catch (errorArg) {
|
|
369
|
+
this.endLease(lease.digest, held);
|
|
370
|
+
throw catalogRefusal(errorArg);
|
|
371
|
+
}
|
|
346
372
|
}
|
|
347
373
|
|
|
348
374
|
/**
|
|
@@ -414,7 +440,7 @@ export class AglProxyDaemon {
|
|
|
414
440
|
await this.forgetLease(kept.digest);
|
|
415
441
|
continue;
|
|
416
442
|
}
|
|
417
|
-
const held: IHeldLease = { grant, route: kept.route, createdAt: kept.createdAt };
|
|
443
|
+
const held: IHeldLease = { grant, route: kept.route, provider: kept.policy.provider ?? 'openai', createdAt: kept.createdAt };
|
|
418
444
|
held.reclaimTimer = setTimeout(() => this.endLease(kept.digest, held), Math.max(0, deadline - Date.now()));
|
|
419
445
|
this.leases.set(kept.digest, held);
|
|
420
446
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import * as plugins from './plugins.js';
|
|
2
|
-
import type { IInferenceRoutingPolicy } from './classes.inferencerouter.js';
|
|
3
|
-
import type { IReq_AglProxyReclaim, TAglProxyLeaseCatalog } from './classes.proxycontrol.js';
|
|
2
|
+
import type { IInferenceRoutingPolicy, TInferenceModel } from './classes.inferencerouter.js';
|
|
3
|
+
import type { IAglProxyConfinedGrant, IReq_AglProxyReclaim, TAglProxyLeaseCatalog } from './classes.proxycontrol.js';
|
|
4
|
+
import type { TInferenceProvider } from './functions.proxycontract.js';
|
|
4
5
|
import { AglProxyControlClient, AglProxyRefusedError, type IAglProxyLeaseConnection } from './classes.proxycontrolclient.js';
|
|
5
6
|
|
|
6
7
|
/** The delays between attempts to take a lease back from a restarting proxy; the last one repeats. */
|
|
@@ -159,28 +160,56 @@ export class AglProxyLeaseHolder extends AglProxyHeldCredential {
|
|
|
159
160
|
}
|
|
160
161
|
}
|
|
161
162
|
|
|
163
|
+
export interface IAglProxyConfinedHolderOptions extends IAglProxyHolderOptions {
|
|
164
|
+
/** The run's pool: Claude accounts (`anthropic`) or, translated, the ChatGPT accounts (`openai`). */
|
|
165
|
+
provider: TInferenceProvider;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Why a confined grant does not serve the pool it was asked for, or `undefined` when it does: a proxy older than
|
|
170
|
+
* ChatGPT-pool runs names no pool and issues Claude-pool runs alone, and a ChatGPT-pool run comes with the pool's models.
|
|
171
|
+
*/
|
|
172
|
+
const confinedGrantMismatch = (providerArg: TInferenceProvider, grantArg: IAglProxyConfinedGrant): string | undefined => {
|
|
173
|
+
if (providerArg === 'anthropic') {
|
|
174
|
+
return grantArg.provider === undefined || grantArg.provider === 'anthropic' ? undefined
|
|
175
|
+
: 'The AGL proxy issued a confined run of another pool than the Claude pool asked for.';
|
|
176
|
+
}
|
|
177
|
+
return grantArg.provider === 'openai' && Array.isArray(grantArg.models) ? undefined
|
|
178
|
+
: 'The AGL proxy issued no ChatGPT-pool confined run with the pool\'s models; it may run an AGL older than ChatGPT-pool runs.';
|
|
179
|
+
};
|
|
180
|
+
|
|
162
181
|
/**
|
|
163
182
|
* A confined run's credential (see `IReq_AglProxyConfined`), held for as long as its container runs. It is configured
|
|
164
183
|
* with the confined listener's socket, so one taken back on another socket cannot continue.
|
|
165
184
|
*/
|
|
166
185
|
export class AglProxyConfinedHolder extends AglProxyHeldCredential {
|
|
167
186
|
/** Takes a new confined run's credential of the proxy on `optionsArg.socketPath`, its control socket. */
|
|
168
|
-
public static async take(optionsArg:
|
|
187
|
+
public static async take(optionsArg: IAglProxyConfinedHolderOptions, signalArg: AbortSignal): Promise<AglProxyConfinedHolder> {
|
|
169
188
|
const control = new AglProxyControlClient(optionsArg.socketPath);
|
|
170
|
-
const grant = await control.confined(signalArg);
|
|
189
|
+
const grant = await control.confined(optionsArg.provider, signalArg);
|
|
190
|
+
const mismatch = confinedGrantMismatch(optionsArg.provider, grant);
|
|
191
|
+
if (mismatch !== undefined) {
|
|
192
|
+
grant.close();
|
|
193
|
+
throw new Error(mismatch);
|
|
194
|
+
}
|
|
171
195
|
return new AglProxyConfinedHolder(control, grant, optionsArg);
|
|
172
196
|
}
|
|
173
197
|
|
|
174
198
|
/** The run's id, as `agl proxy confined end` and `agl proxy status` name it. */
|
|
175
199
|
public readonly id: string;
|
|
200
|
+
/** The run's pool. */
|
|
201
|
+
public readonly provider: TInferenceProvider;
|
|
202
|
+
/** A ChatGPT-pool run's models as the proxy read them when it issued the credential; empty for a Claude-pool run. */
|
|
203
|
+
public readonly models: readonly TInferenceModel[];
|
|
176
204
|
/** The confined listener's socket, on the host. */
|
|
177
205
|
public readonly confinedSocketPath: string;
|
|
178
206
|
|
|
179
|
-
private constructor(controlArg: AglProxyControlClient,
|
|
180
|
-
|
|
181
|
-
optionsArg: IAglProxyHolderOptions) {
|
|
207
|
+
private constructor(controlArg: AglProxyControlClient, grantArg: IAglProxyConfinedGrant & IAglProxyLeaseConnection,
|
|
208
|
+
optionsArg: IAglProxyConfinedHolderOptions) {
|
|
182
209
|
super(controlArg, grantArg, optionsArg);
|
|
183
210
|
this.id = grantArg.id;
|
|
211
|
+
this.provider = optionsArg.provider;
|
|
212
|
+
this.models = grantArg.models ?? [];
|
|
184
213
|
this.confinedSocketPath = grantArg.socketPath;
|
|
185
214
|
}
|
|
186
215
|
|
package/ts/classes.proxystore.ts
CHANGED
|
@@ -151,8 +151,9 @@ export interface IProxyLeaseDocument {
|
|
|
151
151
|
*/
|
|
152
152
|
orphanedAt?: Date;
|
|
153
153
|
/**
|
|
154
|
-
* `confined` for a confined run's credential, accepted on the confined listener alone, with the
|
|
155
|
-
* and the confined client's sessions; absent for a
|
|
154
|
+
* `confined` for a confined run's credential, accepted on the confined listener alone, with the policy of its pool,
|
|
155
|
+
* the Claude pool's or the ChatGPT pool's, unrestricted, and the confined client's sessions on that pool; absent for a
|
|
156
|
+
* lease of the loopback listener.
|
|
156
157
|
*/
|
|
157
158
|
route?: 'confined';
|
|
158
159
|
}
|
|
@@ -174,6 +175,10 @@ const assertLeasePolicy = (valueArg: unknown): IProxyLeasePolicyDocument => {
|
|
|
174
175
|
return value;
|
|
175
176
|
};
|
|
176
177
|
|
|
178
|
+
/** A confined run's policy, as kept: one pool, unrestricted (see `IProxyLeaseDocument.route`). */
|
|
179
|
+
const confinedLeasePolicies: ReadonlySet<string> = new Set([JSON.stringify({ provider: 'anthropic' }),
|
|
180
|
+
JSON.stringify({ provider: 'openai' })]);
|
|
181
|
+
|
|
177
182
|
const assertLease: (valueArg: unknown) => asserts valueArg is IProxyLeaseDocument = (valueArg) => {
|
|
178
183
|
const value = valueArg as Partial<IProxyLeaseDocument> | null;
|
|
179
184
|
if (!value || typeof value !== 'object'
|
|
@@ -182,7 +187,7 @@ const assertLease: (valueArg: unknown) => asserts valueArg is IProxyLeaseDocumen
|
|
|
182
187
|
|| typeof value.client !== 'string' || !proxyClientNamePattern.test(value.client)
|
|
183
188
|
|| !isDate(value.createdAt) || (Object.hasOwn(value, 'orphanedAt') && !isDate(value.orphanedAt))
|
|
184
189
|
|| (Object.hasOwn(value, 'route') && (value.route !== 'confined' || value.client !== aglProxyConfinedClient
|
|
185
|
-
|| JSON.stringify(value.policy)
|
|
190
|
+
|| !confinedLeasePolicies.has(JSON.stringify(value.policy))))) {
|
|
186
191
|
throw new Error('Invalid proxy lease.');
|
|
187
192
|
}
|
|
188
193
|
assertLeasePolicy(value.policy);
|