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.
Files changed (87) hide show
  1. package/changelog.md +24 -0
  2. package/dist_serve/bundle.js +1448 -1448
  3. package/dist_serve/bundle.js.map +2 -2
  4. package/dist_ts/00_commitinfo_data.js +1 -1
  5. package/dist_ts/aglapi/aglapisurface.document.js +7 -3
  6. package/dist_ts/classes.authmodels.d.ts +15 -0
  7. package/dist_ts/classes.authmodels.js +78 -7
  8. package/dist_ts/classes.authstore.js +23 -6
  9. package/dist_ts/classes.cli.js +8 -3
  10. package/dist_ts/classes.controller.d.ts +13 -0
  11. package/dist_ts/classes.controller.js +51 -14
  12. package/dist_ts/classes.controllermanagement.d.ts +53 -6
  13. package/dist_ts/classes.controllermanagement.js +180 -72
  14. package/dist_ts/classes.inferencegateway.d.ts +6 -6
  15. package/dist_ts/classes.inferencegateway.js +28 -15
  16. package/dist_ts/classes.inferenceusage.d.ts +19 -7
  17. package/dist_ts/classes.inferenceusage.js +49 -13
  18. package/dist_ts/classes.inferenceusagetap.d.ts +15 -3
  19. package/dist_ts/classes.inferenceusagetap.js +16 -9
  20. package/dist_ts/classes.processinspection.d.ts +3 -1
  21. package/dist_ts/classes.processinspection.js +4 -2
  22. package/dist_ts/classes.proxycontrol.d.ts +22 -7
  23. package/dist_ts/classes.proxycontrol.js +5 -3
  24. package/dist_ts/classes.proxycontrolclient.d.ts +4 -3
  25. package/dist_ts/classes.proxycontrolclient.js +7 -5
  26. package/dist_ts/classes.proxydaemon.d.ts +6 -4
  27. package/dist_ts/classes.proxydaemon.js +42 -17
  28. package/dist_ts/classes.proxyleaseholder.d.ts +11 -2
  29. package/dist_ts/classes.proxyleaseholder.js +25 -2
  30. package/dist_ts/classes.proxystore.d.ts +3 -2
  31. package/dist_ts/classes.proxystore.js +5 -2
  32. package/dist_ts/classes.upgradecoordinator.d.ts +2 -1
  33. package/dist_ts/classes.upgradecoordinator.js +13 -10
  34. package/dist_ts/classes.upgradetransaction.d.ts +6 -0
  35. package/dist_ts/classes.upgradetransaction.js +38 -13
  36. package/dist_ts/classes.usagefeed.js +3 -2
  37. package/dist_ts/functions.controllermanagementsurface.d.ts +37 -0
  38. package/dist_ts/functions.controllermanagementsurface.js +110 -0
  39. package/dist_ts/functions.inferencecli.d.ts +6 -3
  40. package/dist_ts/functions.inferencecli.js +9 -5
  41. package/dist_ts/functions.inferencecliarguments.d.ts +1 -1
  42. package/dist_ts/functions.inferencecliarguments.js +19 -7
  43. package/dist_ts/functions.inferencelaunch.d.ts +30 -9
  44. package/dist_ts/functions.inferencelaunch.js +49 -23
  45. package/dist_ts/interfaces.auth.d.ts +14 -3
  46. package/dist_ts/interfaces.auth.js +4 -3
  47. package/dist_ts_interfaces/index.d.ts +1 -0
  48. package/dist_ts_interfaces/index.js +2 -1
  49. package/dist_ts_interfaces/interfaces.d.ts +6 -1
  50. package/dist_ts_interfaces/interfaces.js +2 -2
  51. package/dist_ts_interfaces/management.d.ts +33 -0
  52. package/dist_ts_interfaces/management.js +35 -0
  53. package/dist_ts_interfaces/metrics.d.ts +29 -7
  54. package/dist_ts_interfaces/metrics.js +14 -1
  55. package/package.json +6 -5
  56. package/readme.md +123 -28
  57. package/ts/00_commitinfo_data.ts +1 -1
  58. package/ts/aglapi/aglapisurface.document.ts +6 -2
  59. package/ts/classes.authmodels.ts +74 -5
  60. package/ts/classes.authstore.ts +24 -4
  61. package/ts/classes.cli.ts +9 -2
  62. package/ts/classes.controller.ts +98 -59
  63. package/ts/classes.controllermanagement.ts +284 -107
  64. package/ts/classes.inferencegateway.ts +31 -19
  65. package/ts/classes.inferenceusage.ts +58 -19
  66. package/ts/classes.inferenceusagetap.ts +27 -8
  67. package/ts/classes.processinspection.ts +2 -1
  68. package/ts/classes.proxycontrol.ts +25 -9
  69. package/ts/classes.proxycontrolclient.ts +7 -4
  70. package/ts/classes.proxydaemon.ts +43 -17
  71. package/ts/classes.proxyleaseholder.ts +36 -7
  72. package/ts/classes.proxystore.ts +8 -3
  73. package/ts/classes.upgradecoordinator.ts +20 -11
  74. package/ts/classes.upgradetransaction.ts +53 -2
  75. package/ts/classes.usagefeed.ts +2 -1
  76. package/ts/functions.controllermanagementsurface.ts +149 -0
  77. package/ts/functions.inferencecli.ts +15 -7
  78. package/ts/functions.inferencecliarguments.ts +16 -6
  79. package/ts/functions.inferencelaunch.ts +57 -23
  80. package/ts/interfaces.auth.ts +14 -3
  81. package/ts_interfaces/index.ts +1 -0
  82. package/ts_interfaces/interfaces.ts +6 -1
  83. package/ts_interfaces/management.ts +52 -0
  84. package/ts_interfaces/metrics.ts +34 -7
  85. package/ts_web/00_commitinfo_data.ts +1 -1
  86. package/ts_web/classes.controllersocketclient.ts +5 -2
  87. 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 IInferenceTokenCounts {
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
- export interface IInferenceUsageDocument extends IInferenceUsageBucket {
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
- if (!value || typeof value !== 'object' || Object.keys(value).length !== 15
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, recordArg, 1); continue; }
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, calls: 1, inputTokens: recordArg.inputTokens,
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, bucket.calls);
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
- addCounts(modelArg, bucketArg, bucketArg.calls);
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: IInferenceUsageBucket): IControllerUsageBucket => ({ id: documentArg.id, span: documentArg.span,
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, calls: documentArg.calls, inputTokens: documentArg.inputTokens,
424
- outputTokens: documentArg.outputTokens, cacheReadTokens: documentArg.cacheReadTokens, cacheWriteTokens: documentArg.cacheWriteTokens });
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: IInferenceTokenCounts & { calls: number }, addArg: IInferenceTokenCounts, callsArg: number): void => {
427
- targetArg.calls += callsArg;
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', private readonly report: (usageArg: TTokenUsage) => void,
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) this.report(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) ?? 0;
384
- const cacheWrite = count(details?.cache_write_tokens) ?? 0;
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 > 0 ? counts : undefined;
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
- commandArg: '__serve' | '__upgrade-worker' | 'upgrade' | 'start' | 'restart' | 'service' | 'foreground' | 'temp-password' | 'config',
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 a Claude client, its sessions those of the client
152
- * `aglProxyConfinedClient`. Like a lease it lives as long as the connection that asked for it, a restarted proxy keeps it
153
- * for its holder to take back (`agl.proxy.reclaim`), and the proxy keeps only its digest. `agl.proxy.confined.end` ends it
154
- * at once.
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: Record<string, never>;
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
- /** What a confined run is given: its id, its credential, the confined listener's socket and the origin to name with it. */
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).length !== 0) throw new AglProxyControlRefusal('Invalid proxy confined request.');
307
- return this.handlers!.confined(connectionArg);
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 with it; the credential lives as
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(signalArg: AbortSignal): Promise<IReq_AglProxyConfined['response'] & IAglProxyLeaseConnection> {
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 response = await exchange<IReq_AglProxyConfined['response']>(this.socketPath, 'agl.proxy.confined', {},
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
- /** The policy of every confined run's credential: the Claude pool, unrestricted, as Claude Code's own lease. */
55
- const confinedPolicy: IInferenceRoutingPolicy = Object.freeze({ provider: 'anthropic' });
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), createdAt: heldArg.createdAt.toISOString(),
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(), holder: connectionArg };
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 Claude pool that continues the confined
323
- * client's sessions and is accepted on the confined listener alone. It is kept, as its digest, before it is handed out,
324
- * and ends when its holder's connection closes or the operator ends it (`confinedEnd`). No model list is read for it:
325
- * Claude Code reads the pool's models through the confined listener, and the pool is judged at each request.
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(connectionArg: IAglProxyConnection): Promise<IAglProxyConfinedGrant> {
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 this.service!.open(confinedPolicy, { sessionScope: aglProxyConfinedClient, route: 'confined' }); }
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: confinedPolicy, createdAt: held.createdAt, route: 'confined' }));
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
- return { id: confinedRunId(lease.digest), token: lease.token, socketPath, baseUrl: aglProxyConfinedOrigin };
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: IAglProxyHolderOptions, signalArg: AbortSignal): Promise<AglProxyConfinedHolder> {
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
- grantArg: { id: string; baseUrl: string; token: string; socketPath: string } & IAglProxyLeaseConnection,
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
 
@@ -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 Claude pool's policy
155
- * and the confined client's sessions; absent for a lease of the loopback listener.
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) !== JSON.stringify({ provider: 'anthropic' })))) {
190
+ || !confinedLeasePolicies.has(JSON.stringify(value.policy))))) {
186
191
  throw new Error('Invalid proxy lease.');
187
192
  }
188
193
  assertLeasePolicy(value.policy);