agl 40.4.0 → 40.5.1

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 (156) hide show
  1. package/changelog.md +33 -0
  2. package/dist_serve/bundle.js +2228 -2177
  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 +2 -2
  6. package/dist_ts/classes.accountsactions.d.ts +11 -1
  7. package/dist_ts/classes.accountsactions.js +59 -2
  8. package/dist_ts/classes.controller.js +5 -5
  9. package/dist_ts/classes.flexaccountaccess.js +3 -2
  10. package/dist_ts/classes.harnessreleases.d.ts +14 -4
  11. package/dist_ts/classes.harnessreleases.js +28 -4
  12. package/dist_ts/classes.inferencegateway.d.ts +28 -3
  13. package/dist_ts/classes.inferencegateway.js +177 -61
  14. package/dist_ts/classes.inferenceplacement.d.ts +5 -3
  15. package/dist_ts/classes.inferenceplacement.js +2 -2
  16. package/dist_ts/classes.inferencerouter.d.ts +349 -91
  17. package/dist_ts/classes.inferencerouter.js +804 -297
  18. package/dist_ts/classes.inferenceservice.d.ts +14 -2
  19. package/dist_ts/classes.inferenceservice.js +53 -14
  20. package/dist_ts/classes.inferencespend.d.ts +339 -0
  21. package/dist_ts/classes.inferencespend.js +815 -0
  22. package/dist_ts/classes.inferenceusagecache.d.ts +8 -0
  23. package/dist_ts/classes.inferenceusagecache.js +9 -5
  24. package/dist_ts/classes.inferenceusagetap.d.ts +50 -9
  25. package/dist_ts/classes.inferenceusagetap.js +185 -31
  26. package/dist_ts/classes.proxycontrol.d.ts +3 -2
  27. package/dist_ts/classes.proxycontrol.js +1 -2
  28. package/dist_ts/classes.proxydaemon.js +3 -2
  29. package/dist_ts/classes.proxystore.d.ts +6 -0
  30. package/dist_ts/classes.proxystore.js +11 -2
  31. package/dist_ts/fleet/classes.claudeworkers.d.ts +5 -4
  32. package/dist_ts/fleet/classes.claudeworkers.js +10 -9
  33. package/dist_ts/functions.claudecoderelease.d.ts +12 -6
  34. package/dist_ts/functions.claudecoderelease.js +13 -7
  35. package/dist_ts/functions.inferencecli.js +12 -8
  36. package/dist_ts/interfaces.auth.d.ts +1 -1
  37. package/dist_ts/interfaces.auth.js +3 -1
  38. package/dist_ts/plugins.d.ts +3 -1
  39. package/dist_ts/plugins.js +7 -2
  40. package/dist_ts_authswitch/authority-apikeys.d.ts +32 -0
  41. package/dist_ts_authswitch/authority-apikeys.js +65 -0
  42. package/dist_ts_authswitch/authority-contract.d.ts +172 -33
  43. package/dist_ts_authswitch/authority-contract.js +54 -1
  44. package/dist_ts_authswitch/authority-overview.js +8 -1
  45. package/dist_ts_authswitch/authority-placement.d.ts +7 -2
  46. package/dist_ts_authswitch/authority-placement.js +10 -3
  47. package/dist_ts_authswitch/authority-runtime-contract.d.ts +2 -2
  48. package/dist_ts_authswitch/authority-spend.d.ts +103 -0
  49. package/dist_ts_authswitch/authority-spend.js +110 -0
  50. package/dist_ts_authswitch/authority-subscription.d.ts +12 -1
  51. package/dist_ts_authswitch/authority-subscription.js +24 -1
  52. package/dist_ts_authswitch/authority-usagelimit.d.ts +7 -4
  53. package/dist_ts_authswitch/authority-usagelimit.js +89 -39
  54. package/dist_ts_authswitch/authority-usagewindows.d.ts +4 -1
  55. package/dist_ts_authswitch/authority-usagewindows.js +41 -16
  56. package/dist_ts_authswitch/classes.authoritybroker.d.ts +30 -19
  57. package/dist_ts_authswitch/classes.authoritybroker.js +152 -61
  58. package/dist_ts_authswitch/classes.authoritycli.d.ts +9 -4
  59. package/dist_ts_authswitch/classes.authoritycli.js +99 -30
  60. package/dist_ts_authswitch/classes.authorityclient.d.ts +24 -7
  61. package/dist_ts_authswitch/classes.authorityclient.js +45 -15
  62. package/dist_ts_authswitch/classes.authoritydaemon.js +39 -12
  63. package/dist_ts_authswitch/classes.authoritydatabase.d.ts +28 -5
  64. package/dist_ts_authswitch/classes.authoritydatabase.js +171 -9
  65. package/dist_ts_authswitch/classes.authorityimport.js +2 -2
  66. package/dist_ts_authswitch/classes.authoritymodels.d.ts +71 -3
  67. package/dist_ts_authswitch/classes.authoritymodels.js +249 -20
  68. package/dist_ts_authswitch/classes.authorityregistry.js +4 -2
  69. package/dist_ts_authswitch/classes.authorityusage.d.ts +69 -7
  70. package/dist_ts_authswitch/classes.authorityusage.js +118 -4
  71. package/dist_ts_authswitch/classes.claudenative.d.ts +23 -8
  72. package/dist_ts_authswitch/classes.claudenative.js +49 -21
  73. package/dist_ts_authswitch/classes.claudesignin.d.ts +14 -2
  74. package/dist_ts_authswitch/classes.claudesignin.js +72 -23
  75. package/dist_ts_authswitch/classes.codexmanaged.d.ts +8 -0
  76. package/dist_ts_authswitch/classes.codexmanaged.js +33 -18
  77. package/dist_ts_authswitch/classes.harnessprocesses.d.ts +8 -0
  78. package/dist_ts_authswitch/classes.harnessprocesses.js +41 -9
  79. package/dist_ts_authswitch/testsupport.d.ts +24 -5
  80. package/dist_ts_authswitch/testsupport.js +56 -12
  81. package/dist_ts_authswitch/ts_migration/0011_account_api_key_fields.d.ts +12 -0
  82. package/dist_ts_authswitch/ts_migration/0011_account_api_key_fields.js +21 -0
  83. package/dist_ts_authswitch/ts_migration/index.js +3 -1
  84. package/dist_ts_interfaces/functions.authorityscope.d.ts +18 -7
  85. package/dist_ts_interfaces/functions.authorityscope.js +35 -8
  86. package/dist_ts_interfaces/interfaces.d.ts +1 -1
  87. package/dist_ts_interfaces/interfaces.js +1 -1
  88. package/dist_ts_interfaces/plugins.d.ts +2 -1
  89. package/dist_ts_interfaces/plugins.js +7 -4
  90. package/dist_ts_interfaces/requests.accounts.d.ts +56 -0
  91. package/dist_ts_interfaces/requests.accounts.js +11 -1
  92. package/dist_ts_interfaces/requests.d.ts +2 -2
  93. package/dist_ts_interfaces/requests.js +1 -1
  94. package/docs/authswitch.md +75 -12
  95. package/package.json +5 -5
  96. package/readme.md +199 -22
  97. package/ts/00_commitinfo_data.ts +1 -1
  98. package/ts/aglapi/aglapisurface.document.ts +2 -2
  99. package/ts/classes.accountsactions.ts +75 -1
  100. package/ts/classes.controller.ts +4 -4
  101. package/ts/classes.flexaccountaccess.ts +2 -1
  102. package/ts/classes.harnessreleases.ts +25 -4
  103. package/ts/classes.inferencegateway.ts +184 -64
  104. package/ts/classes.inferenceplacement.ts +6 -4
  105. package/ts/classes.inferencerouter.ts +1026 -314
  106. package/ts/classes.inferenceservice.ts +56 -13
  107. package/ts/classes.inferencespend.ts +870 -0
  108. package/ts/classes.inferenceusagecache.ts +16 -4
  109. package/ts/classes.inferenceusagetap.ts +159 -34
  110. package/ts/classes.proxycontrol.ts +3 -3
  111. package/ts/classes.proxydaemon.ts +2 -1
  112. package/ts/classes.proxystore.ts +11 -1
  113. package/ts/fleet/classes.claudeworkers.ts +9 -8
  114. package/ts/functions.claudecoderelease.ts +13 -6
  115. package/ts/functions.inferencecli.ts +11 -7
  116. package/ts/interfaces.auth.ts +2 -0
  117. package/ts/plugins.ts +7 -0
  118. package/ts_authswitch/authority-apikeys.ts +85 -0
  119. package/ts_authswitch/authority-contract.ts +201 -34
  120. package/ts_authswitch/authority-overview.ts +7 -0
  121. package/ts_authswitch/authority-placement.ts +12 -4
  122. package/ts_authswitch/authority-runtime-contract.ts +2 -2
  123. package/ts_authswitch/authority-spend.ts +168 -0
  124. package/ts_authswitch/authority-subscription.ts +28 -1
  125. package/ts_authswitch/authority-usagelimit.ts +95 -40
  126. package/ts_authswitch/authority-usagewindows.ts +42 -17
  127. package/ts_authswitch/classes.authoritybroker.ts +161 -68
  128. package/ts_authswitch/classes.authoritycli.ts +112 -32
  129. package/ts_authswitch/classes.authorityclient.ts +66 -19
  130. package/ts_authswitch/classes.authoritydaemon.ts +43 -13
  131. package/ts_authswitch/classes.authoritydatabase.ts +178 -8
  132. package/ts_authswitch/classes.authorityimport.ts +1 -1
  133. package/ts_authswitch/classes.authoritymodels.ts +165 -19
  134. package/ts_authswitch/classes.authorityregistry.ts +3 -0
  135. package/ts_authswitch/classes.authorityusage.ts +171 -10
  136. package/ts_authswitch/classes.claudenative.ts +51 -22
  137. package/ts_authswitch/classes.claudesignin.ts +74 -24
  138. package/ts_authswitch/classes.codexmanaged.ts +30 -16
  139. package/ts_authswitch/classes.harnessprocesses.ts +39 -8
  140. package/ts_authswitch/native-contracts.md +82 -21
  141. package/ts_authswitch/readme.md +1 -1
  142. package/ts_authswitch/testsupport.ts +58 -11
  143. package/ts_authswitch/ts_migration/0011_account_api_key_fields.ts +20 -0
  144. package/ts_authswitch/ts_migration/index.ts +2 -0
  145. package/ts_interfaces/functions.authorityscope.ts +43 -8
  146. package/ts_interfaces/interfaces.ts +1 -1
  147. package/ts_interfaces/plugins.ts +6 -3
  148. package/ts_interfaces/requests.accounts.ts +48 -0
  149. package/ts_interfaces/requests.ts +4 -0
  150. package/ts_web/00_commitinfo_data.ts +1 -1
  151. package/ts_web/accounts.plugins.ts +7 -1
  152. package/ts_web/accounts.templates.ts +20 -11
  153. package/ts_web/classes.accountsreadmodel.ts +326 -71
  154. package/ts_web/elements.accounts.ts +219 -20
  155. package/ts_web/elements.codexconnections.ts +3 -1
  156. package/ts_web/plugins.ts +4 -2
@@ -13,6 +13,13 @@ const supersedes = (nextArg: TUsageSnapshot, heldArg: TUsageSnapshot | undefined
13
13
  export interface IInferenceUsageCacheOptions {
14
14
  /** Reads one account's usage from the account service. */
15
15
  read: (accountIdArg: string, signalArg: AbortSignal) => Promise<TUsageSnapshot>;
16
+ /**
17
+ * What a held reading reads as when it is handed out (`get`, `current`, `peek`), worked out in the same synchronous
18
+ * step that reads it: an API key's spend with the answers its accounts service has not acknowledged yet
19
+ * (`InferenceSpendOutbox.judged`). Their acknowledgement replaces the held reading and drops them in one step
20
+ * (`accept`), so a reading handed out never misses an answer. The held reading as it is when unset.
21
+ */
22
+ view?: (accountIdArg: string, snapshotArg: TUsageSnapshot) => TUsageSnapshot;
16
23
  /** How old a held reading may grow before a request reads it again (0: every request starts one shared read). */
17
24
  refreshAfterMs?: number;
18
25
  /** How long a request waits for that read before it routes on the held reading. */
@@ -68,17 +75,18 @@ export class InferenceUsageCache {
68
75
  if (entry.snapshot) {
69
76
  if (now - entry.readAt >= this.refreshAfterMs) this.refresh(accountIdArg, entry);
70
77
  if (entry.inflight && this.freshWaitMs > 0) await this.wait(entry.inflight, this.freshWaitMs, signalArg);
71
- return entry.snapshot;
78
+ return this.viewed(accountIdArg, entry.snapshot);
72
79
  }
73
80
  this.refresh(accountIdArg, entry);
74
81
  if (entry.inflight) await this.wait(entry.inflight, this.firstReadTimeoutMs, signalArg);
75
- if (entry.snapshot) return entry.snapshot;
82
+ if (entry.snapshot) return this.viewed(accountIdArg, entry.snapshot);
76
83
  throw entry.error instanceof Error ? entry.error : new Error('The account usage could not be read.');
77
84
  }
78
85
 
79
86
  /** The reading held for an account, without reading it again; `undefined` while none is held. */
80
87
  public peek(accountIdArg: string): TUsageSnapshot | undefined {
81
- return this.entries.get(accountIdArg)?.snapshot;
88
+ const snapshot = this.entries.get(accountIdArg)?.snapshot;
89
+ return snapshot && this.viewed(accountIdArg, snapshot);
82
90
  }
83
91
 
84
92
  /**
@@ -95,7 +103,7 @@ export class InferenceUsageCache {
95
103
  }
96
104
  if (!entry.snapshot || this.now() - entry.readAt >= this.refreshAfterMs) this.refresh(accountIdArg, entry);
97
105
  if (entry.inflight && this.freshWaitMs > 0) await this.wait(entry.inflight, this.freshWaitMs, signalArg);
98
- return entry.snapshot;
106
+ return entry.snapshot && this.viewed(accountIdArg, entry.snapshot);
99
107
  }
100
108
 
101
109
  /**
@@ -114,6 +122,10 @@ export class InferenceUsageCache {
114
122
  entry.readAt = this.now();
115
123
  }
116
124
 
125
+ private viewed(accountIdArg: string, snapshotArg: TUsageSnapshot): TUsageSnapshot {
126
+ return this.options.view ? this.options.view(accountIdArg, snapshotArg) : snapshotArg;
127
+ }
128
+
117
129
  /** Waits for a read, at most `msArg`; the read itself goes on and lands in the cache. */
118
130
  private async wait(inflightArg: Promise<void>, msArg: number, signalArg: AbortSignal): Promise<void> {
119
131
  await new Promise<void>((resolve) => {
@@ -1,5 +1,6 @@
1
1
  import * as plugins from './plugins.js';
2
2
  import type { IInferenceTokenCounts } from './classes.inferenceusage.js';
3
+ import type { IInferenceAnsweredUsage, IInferenceResponsesAnswered } from './classes.inferencespend.js';
3
4
 
4
5
  type TTokenUsage = plugins.authswitchUsageTokens.IAuthSwitchTokenUsage;
5
6
 
@@ -31,7 +32,8 @@ interface IStatedUsage {
31
32
  /** What the relay hands each chunk of an answer it passed on, and the end of the answer. */
32
33
  export interface IInferenceUsageTap {
33
34
  chunk(bytesArg: Uint8Array): void;
34
- end(): void;
35
+ /** The answer ended; `wholeArg`: the provider's body was read to its end, not cut or cancelled. */
36
+ end(wholeArg: boolean): void;
35
37
  }
36
38
 
37
39
  /**
@@ -42,6 +44,11 @@ export interface IInferenceUsageTap {
42
44
  * cumulative), so only those events' data lines are parsed and every other line is skipped unread; a cut stream reports
43
45
  * what arrived. A JSON answer states `usage` and `model` at its top level and is read at its end. Anything unreadable
44
46
  * reports nothing: the estimate is the authority's, and the answer is never at stake.
47
+ *
48
+ * `answered`, given for an answer an API key served, takes at the end what the answer stated for its price: the model
49
+ * that answered and its usage object as sent, a stream's merged field by field from `message_start` and the
50
+ * `message_delta`s after it (one that omits `service_tier` keeps the one `message_start` stated), and whether the answer
51
+ * arrived whole (`message_stop`, or a JSON body read to its end). It is called also when the answer could not be read.
45
52
  */
46
53
  export class InferenceClaudeUsageTap implements IInferenceUsageTap {
47
54
  private readonly decoder = new TextDecoder();
@@ -50,11 +57,17 @@ export class InferenceClaudeUsageTap implements IInferenceUsageTap {
50
57
  private event = '';
51
58
  private model?: string;
52
59
  private readonly stated: IStatedUsage = {};
60
+ /** The usage objects the answer stated, merged as sent, for its price. */
61
+ private raw?: Record<string, unknown>;
62
+ /** The stream reached `message_stop`, or the JSON body was read to its end. */
63
+ private complete = false;
53
64
  private readonly json: Uint8Array[] = [];
54
65
  private jsonBytes = 0;
55
66
  private stopped = false;
67
+ private ended = false;
56
68
 
57
- constructor(private readonly format: 'sse' | 'json', private readonly report: (usageArg: TTokenUsage) => void) {}
69
+ constructor(private readonly format: 'sse' | 'json', private readonly report: (usageArg: TTokenUsage) => void,
70
+ private readonly answered?: (answeredArg: IInferenceAnsweredUsage) => void) {}
58
71
 
59
72
  /** Reads a chunk the relay passed on; never throws. */
60
73
  public chunk(bytesArg: Uint8Array): void {
@@ -62,7 +75,8 @@ export class InferenceClaudeUsageTap implements IInferenceUsageTap {
62
75
  try {
63
76
  if (this.format === 'json') {
64
77
  this.jsonBytes += bytesArg.byteLength;
65
- if (this.jsonBytes > maximumJsonBytes) { this.stopped = true; this.json.length = 0; return; }
78
+ // A body too large to read is an answer that arrived all the same, whose usage could not be read.
79
+ if (this.jsonBytes > maximumJsonBytes) { this.stopped = true; this.complete = true; this.json.length = 0; return; }
66
80
  this.json.push(bytesArg.slice());
67
81
  return;
68
82
  }
@@ -72,9 +86,12 @@ export class InferenceClaudeUsageTap implements IInferenceUsageTap {
72
86
 
73
87
  /** Reports the usage that arrived, once: at the end of the answer, also when it was cut or cancelled. */
74
88
  public end(): void {
75
- if (this.stopped) return;
89
+ if (this.ended) return;
90
+ this.ended = true;
76
91
  try {
92
+ if (this.stopped) return;
77
93
  if (this.format === 'json') {
94
+ this.complete = true;
78
95
  const body = record(JSON.parse(new TextDecoder().decode(Buffer.concat(this.json))));
79
96
  this.json.length = 0;
80
97
  if (typeof body?.model === 'string') this.model = body.model;
@@ -86,7 +103,12 @@ export class InferenceClaudeUsageTap implements IInferenceUsageTap {
86
103
  this.stopped = true;
87
104
  const usage = this.usage();
88
105
  if (usage) this.report(usage);
89
- } catch { this.stopped = true; }
106
+ } catch { this.stopped = true; } finally {
107
+ try {
108
+ this.answered?.({ ...(this.model === undefined ? {} : { model: this.model }), ...(this.raw ? { usage: this.raw } : {}),
109
+ complete: this.complete });
110
+ } catch { /* the answer is never at stake */ }
111
+ }
90
112
  }
91
113
 
92
114
  private text(textArg: string): void {
@@ -115,7 +137,11 @@ export class InferenceClaudeUsageTap implements IInferenceUsageTap {
115
137
 
116
138
  private handle(lineArg: string): void {
117
139
  if (lineArg === '') { this.event = ''; return; }
118
- if (lineArg.startsWith('event:')) { this.event = lineArg.slice(6).trim(); return; }
140
+ if (lineArg.startsWith('event:')) {
141
+ this.event = lineArg.slice(6).trim();
142
+ if (this.event === 'message_stop') this.complete = true;
143
+ return;
144
+ }
119
145
  if (!lineArg.startsWith('data:') || !usageEvents.has(this.event)) return;
120
146
  const data = record(JSON.parse(lineArg.slice(5)));
121
147
  if (this.event === 'message_start') {
@@ -129,6 +155,14 @@ export class InferenceClaudeUsageTap implements IInferenceUsageTap {
129
155
 
130
156
  private state(usageArg: Record<string, unknown> | undefined): void {
131
157
  if (!usageArg) return;
158
+ // As sent, later fields over earlier ones; an object field (`cache_creation`, `server_tool_use`) merges its own fields.
159
+ const raw = this.raw ?? {};
160
+ for (const [name, value] of Object.entries(usageArg)) {
161
+ const held = record(raw[name]);
162
+ const next = record(value);
163
+ raw[name] = held && next ? { ...held, ...next } : value;
164
+ }
165
+ this.raw = raw;
132
166
  for (const name of ['input_tokens', 'output_tokens', 'cache_read_input_tokens', 'cache_creation_input_tokens'] as const) {
133
167
  const value = count(usageArg[name]);
134
168
  if (value !== undefined) this.stated[name] = value;
@@ -158,15 +192,28 @@ export class InferenceClaudeUsageTap implements IInferenceUsageTap {
158
192
  /** The Responses stream events that end an answer and carry the whole response with its `usage`. */
159
193
  const responsesTerminalEvents: ReadonlySet<string> = new Set(['response.completed', 'response.incomplete', 'response.failed']);
160
194
 
195
+ /** The Responses stream event that carries each finished output item, a web or file search call among them. */
196
+ const responsesItemDoneEvent = 'response.output_item.done';
197
+
198
+ /** The output item types of a search OpenAI runs and bills per call, and the call fee each counts under. */
199
+ const searchCallTypes: ReadonlyMap<string, 'web' | 'file'> = new Map([['web_search_call', 'web'], ['file_search_call', 'file']]);
200
+
161
201
  /**
162
202
  * Reads the tokens one OpenAI Responses answer used as its bytes pass to the client, as `InferenceClaudeUsageTap` does for
163
203
  * Messages: never holding, changing or delaying the answer, and reporting once at its end.
164
204
  *
165
205
  * A stream (`sse`) states usage only in its terminal event (`response.completed`, `response.incomplete`,
166
- * `response.failed`) as `response.usage`. A frame named by an `event:` line is read only when that name is terminal;
167
- * a data-only frame is kept up to the frame limit and parsed only when it names a terminal type. A JSON answer states
168
- * `usage` at its top level. `input_tokens` counts every input token, so the cached reads (`cached_tokens`) and cache
169
- * writes (`cache_write_tokens`) of `input_tokens_details` are taken out of it.
206
+ * `response.failed`) as `response.usage`. A frame named by an `event:` line is read only when that name is terminal or
207
+ * `response.output_item.done`; a data-only frame is kept up to the frame limit and parsed only when it names a terminal
208
+ * type, or a finished output item that is a search call. A JSON answer states `usage` at its top level. `input_tokens`
209
+ * 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.
211
+ *
212
+ * `answered`, given for an answer an OpenAI API key served, takes at the end what the answer stated for its price
213
+ * (`IInferenceResponsesAnswered`): whether its terminal event or its JSON body was read, arrived too large to read, or
214
+ * never arrived (`end`'s `wholeArg` says whether a JSON body was read to its end); the model, service tier, status and
215
+ * usage the response stated; and the ids of its web and file search calls, from the finished output items and the
216
+ * terminal response's `output`, each once.
170
217
  */
171
218
  export class InferenceResponsesUsageTap implements IInferenceUsageTap {
172
219
  private readonly decoder = new TextDecoder();
@@ -174,11 +221,23 @@ export class InferenceResponsesUsageTap implements IInferenceUsageTap {
174
221
  private skipping = false;
175
222
  private event = '';
176
223
  private usage?: Record<string, unknown>;
224
+ /** The response a terminal event or the JSON body stated, for the answer's price. */
225
+ private response?: Record<string, unknown>;
226
+ /** A terminal event was read (`read`), or one arrived too large or malformed to read (`unreadable`). */
227
+ private terminal: 'none' | 'read' | 'unreadable' = 'none';
228
+ private readonly webSearchCalls = new Set<string>();
229
+ private readonly fileSearchCalls = new Set<string>();
230
+ private unidentifiedCalls = false;
177
231
  private readonly json: Uint8Array[] = [];
178
232
  private jsonBytes = 0;
233
+ private jsonOverflow = false;
179
234
  private stopped = false;
235
+ /** Reading failed on something that passed: what the answer stated is not known whole. */
236
+ private failed = false;
237
+ private ended = false;
180
238
 
181
- constructor(private readonly format: 'sse' | 'json', private readonly report: (countsArg: IInferenceTokenCounts) => void) {}
239
+ constructor(private readonly format: 'sse' | 'json', private readonly report: (countsArg: IInferenceTokenCounts) => void,
240
+ private readonly answered?: (answeredArg: IInferenceResponsesAnswered) => void) {}
182
241
 
183
242
  /** Reads a chunk the relay passed on; never throws. */
184
243
  public chunk(bytesArg: Uint8Array): void {
@@ -186,30 +245,53 @@ export class InferenceResponsesUsageTap implements IInferenceUsageTap {
186
245
  try {
187
246
  if (this.format === 'json') {
188
247
  this.jsonBytes += bytesArg.byteLength;
189
- if (this.jsonBytes > maximumJsonBytes) { this.stopped = true; this.json.length = 0; return; }
248
+ if (this.jsonBytes > maximumJsonBytes) { this.stopped = true; this.jsonOverflow = true; this.json.length = 0; return; }
190
249
  this.json.push(bytesArg.slice());
191
250
  return;
192
251
  }
193
252
  this.text(this.decoder.decode(bytesArg, { stream: true }));
194
- } catch { this.stopped = true; }
253
+ } catch { this.stopped = true; this.failed = true; }
195
254
  }
196
255
 
197
- /** Reports the usage the answer stated, once; a stream cut before its terminal event reports nothing. */
198
- public end(): void {
199
- if (this.stopped) return;
256
+ /**
257
+ * Reports the usage the answer stated, once; a stream cut before its terminal event reports nothing. `wholeArg`: the
258
+ * provider's body was read to its end, which only a JSON answer needs to be told.
259
+ */
260
+ public end(wholeArg = true): void {
261
+ if (this.ended) return;
262
+ this.ended = true;
200
263
  try {
201
- if (this.format === 'json') {
202
- const body = record(JSON.parse(new TextDecoder().decode(Buffer.concat(this.json))));
203
- this.json.length = 0;
204
- this.usage = record(body?.usage);
205
- } else {
206
- this.text(this.decoder.decode());
207
- if (this.line && !this.skipping) this.handle(this.line);
264
+ if (!this.stopped) {
265
+ if (this.format === 'json') {
266
+ if (wholeArg) {
267
+ try {
268
+ const body = record(JSON.parse(new TextDecoder().decode(Buffer.concat(this.json))));
269
+ if (body) this.state(body); else this.terminal = 'unreadable';
270
+ } catch { this.terminal = 'unreadable'; }
271
+ }
272
+ this.json.length = 0;
273
+ } else {
274
+ this.text(this.decoder.decode());
275
+ if (this.line && !this.skipping) this.handle(this.line);
276
+ }
277
+ this.stopped = true;
278
+ const counts = this.counts();
279
+ if (counts) this.report(counts);
208
280
  }
209
- this.stopped = true;
210
- const counts = this.counts();
211
- if (counts) this.report(counts);
212
- } catch { this.stopped = true; }
281
+ } catch { this.stopped = true; this.failed = true; } finally {
282
+ try {
283
+ const outcome = this.format === 'json' && this.jsonOverflow ? (wholeArg ? 'unreadable' : 'cut')
284
+ : this.terminal === 'read' && !this.failed ? 'answered'
285
+ : this.terminal === 'unreadable' || this.failed ? 'unreadable' : 'cut';
286
+ const response = this.response;
287
+ this.answered?.({ outcome,
288
+ ...(typeof response?.status === 'string' ? { status: response.status } : {}),
289
+ ...(typeof response?.model === 'string' ? { model: response.model } : {}),
290
+ ...(response && 'service_tier' in response ? { serviceTier: response.service_tier } : {}),
291
+ ...(this.usage ? { usage: this.usage } : {}),
292
+ webSearchCalls: this.webSearchCalls, fileSearchCalls: this.fileSearchCalls, unidentifiedCalls: this.unidentifiedCalls });
293
+ } catch { /* the answer is never at stake */ }
294
+ }
213
295
  }
214
296
 
215
297
  private text(textArg: string): void {
@@ -219,9 +301,14 @@ export class InferenceResponsesUsageTap implements IInferenceUsageTap {
219
301
  if (newline === -1) {
220
302
  if (!this.skipping) {
221
303
  this.line += textArg.slice(start);
222
- // A data line of a named event without usage, or one longer than any frame read, is not kept.
223
- if ((this.line.startsWith('data:') && this.event !== '' && !responsesTerminalEvents.has(this.event))
224
- || this.line.length > maximumResponsesFrameLength) {
304
+ // A data line of a named event without usage or a finished item, or one longer than any frame read, is not
305
+ // kept; a terminal one that long arrived and could not be read.
306
+ if (this.line.startsWith('data:') && this.event !== '' && !responsesTerminalEvents.has(this.event)
307
+ && this.event !== responsesItemDoneEvent) {
308
+ this.skipping = true;
309
+ this.line = '';
310
+ } else if (this.line.length > maximumResponsesFrameLength) {
311
+ this.overflow(this.line);
225
312
  this.skipping = true;
226
313
  this.line = '';
227
314
  }
@@ -233,20 +320,58 @@ export class InferenceResponsesUsageTap implements IInferenceUsageTap {
233
320
  if (this.skipping) { this.skipping = false; this.line = ''; continue; }
234
321
  const line = this.line + piece;
235
322
  this.line = '';
323
+ if (line.length > maximumResponsesFrameLength) { this.overflow(line); continue; }
236
324
  this.handle(line.endsWith('\r') ? line.slice(0, -1) : line);
237
325
  }
238
326
  }
239
327
 
328
+ /** A line longer than any frame read is not read; a terminal event that long arrived and could not be read. */
329
+ private overflow(lineArg: string): void {
330
+ if (lineArg.startsWith('data:') && this.terminal === 'none' && (this.event !== '' ? responsesTerminalEvents.has(this.event)
331
+ : [...responsesTerminalEvents].some((typeArg) => lineArg.includes(`"${typeArg}"`)))) this.terminal = 'unreadable';
332
+ }
333
+
240
334
  private handle(lineArg: string): void {
241
335
  if (lineArg === '') { this.event = ''; return; }
242
336
  if (lineArg.startsWith('event:')) { this.event = lineArg.slice(6).trim(); return; }
243
337
  if (!lineArg.startsWith('data:')) return;
244
- if (this.event !== '' ? !responsesTerminalEvents.has(this.event)
245
- : ![...responsesTerminalEvents].some((typeArg) => lineArg.includes(typeArg))) return;
246
- const data = record(JSON.parse(lineArg.slice(5)));
338
+ const terminal = this.event !== '' ? responsesTerminalEvents.has(this.event)
339
+ : [...responsesTerminalEvents].some((typeArg) => lineArg.includes(typeArg));
340
+ const searchCall = (this.event === '' || this.event === responsesItemDoneEvent) && lineArg.includes(responsesItemDoneEvent)
341
+ && [...searchCallTypes.keys()].some((typeArg) => lineArg.includes(typeArg));
342
+ if (!terminal && !searchCall) return;
343
+ let data: Record<string, unknown> | undefined;
344
+ try {
345
+ data = record(JSON.parse(lineArg.slice(5)));
346
+ } catch {
347
+ // A terminal event or a search call that cannot be read: what the answer cost is not known.
348
+ if (this.event !== '' ? terminal : true) this.terminal = this.terminal === 'read' ? 'read' : 'unreadable';
349
+ if (searchCall) this.unidentifiedCalls = true;
350
+ return;
351
+ }
352
+ if (data?.type === responsesItemDoneEvent) { this.item(data.item); return; }
247
353
  if (typeof data?.type !== 'string' || !responsesTerminalEvents.has(data.type)) return;
248
- const usage = record(record(data.response)?.usage);
354
+ const response = record(data.response);
355
+ if (!response) { if (this.terminal === 'none') this.terminal = 'unreadable'; return; }
356
+ this.state(response);
357
+ }
358
+
359
+ /** Takes what a terminal response (or a JSON answer) states: its usage, model, tier and status, and its search calls. */
360
+ private state(responseArg: Record<string, unknown>): void {
361
+ this.terminal = 'read';
362
+ this.response = responseArg;
363
+ const usage = record(responseArg.usage);
249
364
  if (usage) this.usage = usage;
365
+ if (Array.isArray(responseArg.output)) for (const item of responseArg.output) this.item(item);
366
+ }
367
+
368
+ /** Counts a finished output item that is a web or file search call, once per id. */
369
+ private item(itemArg: unknown): void {
370
+ const item = record(itemArg);
371
+ const kind = typeof item?.type === 'string' ? searchCallTypes.get(item.type) : undefined;
372
+ if (!kind) return;
373
+ if (typeof item!.id !== 'string' || !item!.id) { this.unidentifiedCalls = true; return; }
374
+ (kind === 'web' ? this.webSearchCalls : this.fileSearchCalls).add(item!.id);
250
375
  }
251
376
 
252
377
  private counts(): IInferenceTokenCounts | undefined {
@@ -75,8 +75,9 @@ export interface IReq_AglProxyCredential extends ITypedRequest {
75
75
 
76
76
  /**
77
77
  * The models a pool offers now, without a credential: what a configuration names for the pool. `credential` names the
78
- * pool's kind as a lease policy does: subscription accounts when omitted, Anthropic API keys with `api_key`.
79
- * `accountId` reads the ChatGPT pool of that one account, as a lease restricted to it (`accountIds`) is served.
78
+ * pool's kind as a lease policy does: when omitted, its best credential tier with a readable account (subscription
79
+ * accounts, else API keys); `api_key`, the provider's API keys alone. `accountId` reads the ChatGPT pool of that one
80
+ * account, as a lease restricted to it (`accountIds`) is served.
80
81
  */
81
82
  export interface IReq_AglProxyCatalog extends ITypedRequest {
82
83
  method: 'agl.proxy.catalog';
@@ -224,7 +225,6 @@ export class AglProxyControlServer {
224
225
  || Object.keys(requestArg).some((keyArg) => !['provider', 'credential', 'accountId'].includes(keyArg))
225
226
  || !inferenceProviders.includes(requestArg.provider)
226
227
  || (requestArg.credential !== undefined && !inferenceCredentials.includes(requestArg.credential))
227
- || (requestArg.credential === 'api_key' && requestArg.provider !== 'anthropic')
228
228
  || (requestArg.accountId !== undefined
229
229
  && (requestArg.provider !== 'openai' || requestArg.credential !== undefined || !isAuthorityAccountId(requestArg.accountId)))) {
230
230
  throw new AglProxyControlRefusal('Invalid proxy catalog request.');
@@ -105,7 +105,7 @@ export class AglProxyDaemon {
105
105
  this.usage.start();
106
106
  this.service = new InferenceService({ authorityPaths: this.options.authorityPaths, installationId: identity.installationId,
107
107
  port: this.options.port, signatureKey: identity.signatureKey, affinity: this.store.affinity(),
108
- placements: this.store.placements(), usageLedger: this.usage,
108
+ placements: this.store.placements(), usageLedger: this.usage, spendStore: this.store.spend(),
109
109
  ...(this.options.providerFetch ? { providerFetch: this.options.providerFetch } : {}),
110
110
  ...(this.options.limits ? { limits: this.options.limits } : {}) });
111
111
  const baseUrl = await this.service.start(signal);
@@ -247,6 +247,7 @@ export class AglProxyDaemon {
247
247
  ? { provider: 'anthropic', models: await service.router.claudeModels({ provider: 'anthropic',
248
248
  ...(requestArg.credential ? { credential: requestArg.credential } : {}) }, signal) }
249
249
  : { provider: 'openai', models: await service.router.models({ provider: 'openai',
250
+ ...(requestArg.credential ? { credential: requestArg.credential } : {}),
250
251
  ...(requestArg.accountId === undefined ? {} : { accountIds: [requestArg.accountId] }) }, signal) } };
251
252
  } catch (errorArg) {
252
253
  throw catalogRefusal(errorArg);
@@ -4,6 +4,7 @@ import { InferenceAffinityModel, InferenceAffinityStore } from './classes.infere
4
4
  import { InferenceSessionModel } from './classes.inferencesession.js';
5
5
  import { InferencePlacementModel, InferencePlacementStore } from './classes.inferenceplacement.js';
6
6
  import { InferenceUsageModel, InferenceUsageStore } from './classes.inferenceusage.js';
7
+ import { InferenceSpendModel, InferenceSpendStore } from './classes.inferencespend.js';
7
8
  import { assertInferenceRoutingPolicy } from './classes.inferencegateway.js';
8
9
  import type { IInferenceRoutingPolicy } from './classes.inferencerouter.js';
9
10
  import {
@@ -191,7 +192,7 @@ export class ProxyLeaseModel extends plugins.nosqldb.SmartDataDbDoc<ProxyLeaseMo
191
192
  }
192
193
 
193
194
  const proxyModels = [ProxyIdentityModel, ProxyClientModel, ProxyDeliveryModel, ProxyLeaseModel, InferenceAffinityModel,
194
- InferenceSessionModel, InferencePlacementModel, InferenceUsageModel];
195
+ InferenceSessionModel, InferencePlacementModel, InferenceUsageModel, InferenceSpendModel];
195
196
  const inactiveManager = {
196
197
  get db(): plugins.nosqldb.SmartdataDb { throw new Error('The proxy store is not open.'); },
197
198
  };
@@ -288,6 +289,15 @@ export class ProxyStore {
288
289
  return new InferenceUsageStore();
289
290
  }
290
291
 
292
+ /**
293
+ * Where the proxy keeps the spend of the answers it relayed on API keys until the accounts service acknowledged it
294
+ * (`InferenceSpendOutbox`).
295
+ */
296
+ public spend(): InferenceSpendStore {
297
+ this.identity();
298
+ return new InferenceSpendStore();
299
+ }
300
+
291
301
  /** Every client's credential, creating the named clients that have none. */
292
302
  public async credentials(ensureArg: readonly string[] = defaultProxyClients): Promise<IProxyClientCredential[]> {
293
303
  for (const name of ensureArg) await this.ensureClient(name);
@@ -8,7 +8,7 @@ import { FleetDispatchError, boundedFleetReport, fleetMessageDigest, fleetReport
8
8
  import { runFleetWorkerCommand, type IFleetWorkerCommand, type TFleetWorkerRunner } from './classes.workercommand.js';
9
9
  import { sanitizeFailureText } from '../functions.failuredescription.js';
10
10
  import type { FleetWorkerLaunches } from './classes.workerlaunches.js';
11
- import { claudeCodeVerifiedRelease, claudeCodeVersionOutput, compareClaudeCodeReleases, parseClaudeCodeRelease } from '../functions.claudecoderelease.js';
11
+ import { claudeCodeFleetFloor, claudeCodeVersionOutput, compareClaudeCodeReleases, isNewerClaudeCodeRelease, parseClaudeCodeRelease } from '../functions.claudecoderelease.js';
12
12
 
13
13
  const record = plugins.z.record(plugins.z.string(), plugins.z.unknown());
14
14
  const agentSchema = plugins.z.object({ sessionId: plugins.z.string().uuid(), id: plugins.z.string().min(1).optional(), cwd: plugins.z.string(), name: plugins.z.string().optional(),
@@ -36,19 +36,19 @@ const commandFailure = (subcommand: string, result: IFleetWorkerCommand, diagnos
36
36
  return `Claude ${subcommand} failed (exit ${result.code})${output ? `: ${output}` : '.'}`;
37
37
  };
38
38
  /**
39
- * The release a `claude --version` run printed, which the fleet runs on when it is the verified release or a newer one.
39
+ * The release a `claude --version` run printed, which the fleet runs on when it is the fleet's floor or a newer one.
40
40
  * The output must read exactly `X.Y.Z (Claude Code)`; anything else, an older release and a failed run are refused,
41
41
  * naming what was printed.
42
42
  */
43
43
  export const fleetClaudeVersion = (result: IFleetWorkerCommand): string => {
44
44
  const release = result.code === 0 ? claudeCodeVersionOutput(result.stdout) : null;
45
45
  const parsed = release === null ? null : parseClaudeCodeRelease(release);
46
- if (release !== null && parsed !== null && compareClaudeCodeReleases(parsed, parseClaudeCodeRelease(claudeCodeVerifiedRelease)!) >= 0) return release;
46
+ if (release !== null && parsed !== null && compareClaudeCodeReleases(parsed, parseClaudeCodeRelease(claudeCodeFleetFloor)!) >= 0) return release;
47
47
  const output = commandOutput(result, true);
48
48
  const found = result.code !== 0 ? `exited with code ${result.code}${output ? `: ${output}` : '.'}`
49
49
  : release !== null ? `printed release ${release}, which is older.`
50
50
  : output ? `printed ${JSON.stringify(output)}, not "X.Y.Z (Claude Code)".` : 'printed nothing.';
51
- throw new FleetDispatchError(`Claude fleet workers need Claude Code ${claudeCodeVerifiedRelease} or newer; \`claude --version\` ${found}`, false);
51
+ throw new FleetDispatchError(`Claude fleet workers need Claude Code ${claudeCodeFleetFloor} or newer; \`claude --version\` ${found}`, false);
52
52
  };
53
53
  /** Claude colours its output when FORCE_COLOR is set, through a pipe too; its control sequences are no text. */
54
54
  const plain = (text: string): string => text.replace(/\u001b\[[0-?]*[ -/]*[@-~]/gu, '');
@@ -70,9 +70,10 @@ const address = (inventory: TClaudeAgent[], agent: TClaudeAgent): string => {
70
70
  };
71
71
 
72
72
  /**
73
- * Version-coupled transcript interpretation stays here. Claude Code 2.1.282 is the release it was verified on; a newer
74
- * one is used optimistically and these parsers are its checks at the point of use, failing visibly where its output
75
- * changed. An older or unreadable version is refused (`fleetClaudeVersion`).
73
+ * Version-coupled transcript interpretation stays here. It was verified on Claude Code 2.1.306 and on 2.1.282, the
74
+ * fleet's floor, whose output the tests still pin; a newer release is used optimistically and these parsers are its
75
+ * checks at the point of use, failing visibly where its output changed. An older or unreadable version is refused
76
+ * (`fleetClaudeVersion`).
76
77
  */
77
78
  export const parseFleetClaudeTranscript = (payload: string): Record<string, unknown>[] => payload.split('\n').slice(0, -1).filter(Boolean).map((line) => record.parse(JSON.parse(line)));
78
79
  export const fleetClaudePermissionMode = (records: Record<string, unknown>[]): string => {
@@ -225,7 +226,7 @@ export class FleetClaudeWorkers implements IFleetWorkerBackend {
225
226
  const release = fleetClaudeVersion(await this.runner(executable, ['--version'], cwd, env, this.signal));
226
227
  if (await identity() !== fingerprint) throw new FleetDispatchError('Claude executable changed during version verification.', false);
227
228
  this.versions.set(key, fingerprint);
228
- if (release !== claudeCodeVerifiedRelease) this.observeRelease?.(release);
229
+ if (isNewerClaudeCodeRelease(release)) this.observeRelease?.(release);
229
230
  }
230
231
  return executable;
231
232
  }
@@ -1,14 +1,21 @@
1
1
  /**
2
2
  * Claude Code releases as AGL's controller reads them.
3
3
  *
4
- * AGL verified Claude Code's transcript, inventory and command output on one release, `claudeCodeVerifiedRelease`
5
- * (`ts_authswitch/native-contracts.md`). It uses every later release optimistically and refuses an earlier one; each
6
- * later release it meets is mentioned once to the driver (`HarnessReleaseNotices`), so the driver can file a
7
- * compatibility check for it.
4
+ * AGL verified Claude Code's transcript, inventory and command output on `claudeCodeVerifiedRelease`
5
+ * (`ts_authswitch/native-contracts.md`). Its fleet runs every release from `claudeCodeFleetFloor` on, later ones
6
+ * optimistically, and refuses an earlier one; each release newer than the verified one it meets is mentioned once to
7
+ * the driver (`HarnessReleaseNotices`), so the driver can file a compatibility check for it.
8
8
  */
9
9
 
10
- /** The newest Claude Code release AGL verified; the floor of the fleet and the line above which a release is new. */
11
- export const claudeCodeVerifiedRelease = '2.1.282';
10
+ /** The newest Claude Code release AGL verified: the line above which a release is new and mentioned once. */
11
+ export const claudeCodeVerifiedRelease = '2.1.306';
12
+
13
+ /**
14
+ * The oldest Claude Code release the fleet runs: the oldest whose fleet output the tests still pin. It rises only when
15
+ * the fleet's adapter stops handling an older release's output, never because a newer release was verified. The
16
+ * accounts service's usage read takes every release from the same line on (`CLAUDE_NATIVE_READ_AS_VERIFIED_FROM`).
17
+ */
18
+ export const claudeCodeFleetFloor = '2.1.282';
12
19
 
13
20
  export type TClaudeCodeRelease = readonly [number, number, number];
14
21
 
@@ -46,8 +46,8 @@ const placementText = (accountArg: NonNullable<TReportedProxyStatus['accounts']>
46
46
  * estimate between its reads, and a note when the service's last read was refused for too many requests.
47
47
  */
48
48
  const readingText = (accountArg: NonNullable<TReportedProxyStatus['accounts']>[number]): string =>
49
- accountArg.readingObservedAt === null ? 'no usage reading'
50
- : `usage as of ${accountArg.readingObservedAt} `
49
+ accountArg.readingObservedAt === null ? `no ${accountArg.credential === 'api_key' ? 'spend' : 'usage'} reading`
50
+ : `${accountArg.credential === 'api_key' ? 'spend' : 'usage'} as of ${accountArg.readingObservedAt} `
51
51
  + `(${accountArg.readingSource === 'headers' ? 'from provider answers' : 'read by the account service'}`
52
52
  + `${accountArg.readingReadAt === null || accountArg.readingSource !== 'headers' ? ''
53
53
  : `, last read ${accountArg.readingReadAt}`})`
@@ -76,12 +76,16 @@ export const formatProxyStatus = (unitArg: string, statusArg: TReportedProxyStat
76
76
  return lines.join('\n') + '\n';
77
77
  }
78
78
  for (const account of statusArg.accounts) {
79
- lines.push(`Account ${account.label} (${account.provider === 'openai' ? 'ChatGPT'
80
- : account.credential === 'api_key' ? 'Anthropic API key' : 'Claude'}`
79
+ lines.push(`Account ${account.label} (${account.credential === 'api_key'
80
+ ? `${account.provider === 'openai' ? 'OpenAI' : 'Anthropic'} API key` : account.provider === 'openai' ? 'ChatGPT' : 'Claude'}`
81
81
  + `${account.plan === null ? '' : `, ${account.plan}`}): ${account.stateText}`
82
- + `${account.resetAt === null ? '' : `, until ${account.resetAt}`}; max usage `
83
- + `${account.maxUsagePercent === null ? 'not set' : `${account.maxUsagePercent}%`}`
84
- + `${account.headroom === null ? '' : `, ${Math.round(account.headroom)}% headroom`}, ${account.inFlight} in flight`
82
+ + `${account.resetAt === null ? '' : `, until ${account.resetAt}`}; `
83
+ // A key has no maximum usage: its headroom is what is left of its monthly spend limit.
84
+ + `${account.credential === 'api_key'
85
+ ? `${account.headroom === null ? '' : `${Math.round(account.headroom)}% of its spend limit left, `}`
86
+ : `max usage ${account.maxUsagePercent === null ? 'not set' : `${account.maxUsagePercent}%`}`
87
+ + `${account.headroom === null ? '' : `, ${Math.round(account.headroom)}% headroom`}, `}`
88
+ + `${account.inFlight} in flight`
85
89
  + `${placementText(account)}; ${readingText(account)}`);
86
90
  }
87
91
  const accountLabel = (idArg: string | null): string => idArg === null ? 'none'
@@ -72,6 +72,8 @@ export const controllerAuditEventTypes = [
72
72
  'accounts.rename',
73
73
  'accounts.maxusage',
74
74
  'accounts.reserve.lift',
75
+ 'accounts.apikey.add',
76
+ 'accounts.spendlimit',
75
77
  'accounts.remove',
76
78
  'accounts.add',
77
79
  'accounts.reauth',
package/ts/plugins.ts CHANGED
@@ -61,6 +61,11 @@ import * as authswitchUsageTokens from '../ts_authswitch/authority-usagetokens.j
61
61
  // Claude Code's native contract: which releases' credential custody was read, for the mention of a new release. The
62
62
  // package root already loads this module through the authority service, so the value import adds none.
63
63
  import * as authswitchClaudeNative from '../ts_authswitch/classes.claudenative.js';
64
+ // An API key's monthly spend: its calendar month in UTC, the spend limit rule and dollar amounts, which the proxy, the
65
+ // controller and the screens share.
66
+ import * as authswitchSpend from '../ts_authswitch/authority-spend.js';
67
+ // Whether a held payment problem still holds an account, the one rule the authority and the proxy share.
68
+ import * as authswitchSubscription from '../ts_authswitch/authority-subscription.js';
64
69
  import * as crossharness from '@modelprofile.com/mcp-crossharness';
65
70
  import * as mcpSystem from '@modelprofile.com/mcp-system';
66
71
  import * as flexharnessMigration from '@modelprofile.com/flexharness/migration';
@@ -80,6 +85,8 @@ export {
80
85
  authswitchUsageHeaders,
81
86
  authswitchUsageTokens,
82
87
  authswitchClaudeNative,
88
+ authswitchSpend,
89
+ authswitchSubscription,
83
90
  flexharness,
84
91
  browserRuntime,
85
92
  smartbrowser,