agl 41.0.1 → 41.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/changelog.md +24 -0
- package/dist_serve/bundle.js +1448 -1448
- package/dist_serve/bundle.js.map +2 -2
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/aglapi/aglapisurface.document.js +7 -3
- package/dist_ts/classes.authmodels.d.ts +15 -0
- package/dist_ts/classes.authmodels.js +78 -7
- package/dist_ts/classes.authstore.js +23 -6
- package/dist_ts/classes.cli.js +8 -3
- package/dist_ts/classes.controller.d.ts +13 -0
- package/dist_ts/classes.controller.js +51 -14
- package/dist_ts/classes.controllermanagement.d.ts +53 -6
- package/dist_ts/classes.controllermanagement.js +180 -72
- package/dist_ts/classes.inferencegateway.d.ts +6 -6
- package/dist_ts/classes.inferencegateway.js +28 -15
- package/dist_ts/classes.inferenceusage.d.ts +19 -7
- package/dist_ts/classes.inferenceusage.js +49 -13
- package/dist_ts/classes.inferenceusagetap.d.ts +15 -3
- package/dist_ts/classes.inferenceusagetap.js +16 -9
- package/dist_ts/classes.processinspection.d.ts +3 -1
- package/dist_ts/classes.processinspection.js +4 -2
- package/dist_ts/classes.proxycontrol.d.ts +22 -7
- package/dist_ts/classes.proxycontrol.js +5 -3
- package/dist_ts/classes.proxycontrolclient.d.ts +4 -3
- package/dist_ts/classes.proxycontrolclient.js +7 -5
- package/dist_ts/classes.proxydaemon.d.ts +6 -4
- package/dist_ts/classes.proxydaemon.js +42 -17
- package/dist_ts/classes.proxyleaseholder.d.ts +11 -2
- package/dist_ts/classes.proxyleaseholder.js +25 -2
- package/dist_ts/classes.proxystore.d.ts +3 -2
- package/dist_ts/classes.proxystore.js +5 -2
- package/dist_ts/classes.upgradecoordinator.d.ts +2 -1
- package/dist_ts/classes.upgradecoordinator.js +13 -10
- package/dist_ts/classes.upgradetransaction.d.ts +6 -0
- package/dist_ts/classes.upgradetransaction.js +38 -13
- package/dist_ts/classes.usagefeed.js +3 -2
- package/dist_ts/functions.controllermanagementsurface.d.ts +37 -0
- package/dist_ts/functions.controllermanagementsurface.js +110 -0
- package/dist_ts/functions.inferencecli.d.ts +6 -3
- package/dist_ts/functions.inferencecli.js +9 -5
- package/dist_ts/functions.inferencecliarguments.d.ts +1 -1
- package/dist_ts/functions.inferencecliarguments.js +19 -7
- package/dist_ts/functions.inferencelaunch.d.ts +30 -9
- package/dist_ts/functions.inferencelaunch.js +49 -23
- package/dist_ts/interfaces.auth.d.ts +14 -3
- package/dist_ts/interfaces.auth.js +4 -3
- package/dist_ts_interfaces/index.d.ts +1 -0
- package/dist_ts_interfaces/index.js +2 -1
- package/dist_ts_interfaces/interfaces.d.ts +6 -1
- package/dist_ts_interfaces/interfaces.js +2 -2
- package/dist_ts_interfaces/management.d.ts +33 -0
- package/dist_ts_interfaces/management.js +35 -0
- package/dist_ts_interfaces/metrics.d.ts +29 -7
- package/dist_ts_interfaces/metrics.js +14 -1
- package/package.json +6 -5
- package/readme.md +123 -28
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/aglapi/aglapisurface.document.ts +6 -2
- package/ts/classes.authmodels.ts +74 -5
- package/ts/classes.authstore.ts +24 -4
- package/ts/classes.cli.ts +9 -2
- package/ts/classes.controller.ts +98 -59
- package/ts/classes.controllermanagement.ts +284 -107
- package/ts/classes.inferencegateway.ts +31 -19
- package/ts/classes.inferenceusage.ts +58 -19
- package/ts/classes.inferenceusagetap.ts +27 -8
- package/ts/classes.processinspection.ts +2 -1
- package/ts/classes.proxycontrol.ts +25 -9
- package/ts/classes.proxycontrolclient.ts +7 -4
- package/ts/classes.proxydaemon.ts +43 -17
- package/ts/classes.proxyleaseholder.ts +36 -7
- package/ts/classes.proxystore.ts +8 -3
- package/ts/classes.upgradecoordinator.ts +20 -11
- package/ts/classes.upgradetransaction.ts +53 -2
- package/ts/classes.usagefeed.ts +2 -1
- package/ts/functions.controllermanagementsurface.ts +149 -0
- package/ts/functions.inferencecli.ts +15 -7
- package/ts/functions.inferencecliarguments.ts +16 -6
- package/ts/functions.inferencelaunch.ts +57 -23
- package/ts/interfaces.auth.ts +14 -3
- package/ts_interfaces/index.ts +1 -0
- package/ts_interfaces/interfaces.ts +6 -1
- package/ts_interfaces/management.ts +52 -0
- package/ts_interfaces/metrics.ts +34 -7
- package/ts_web/00_commitinfo_data.ts +1 -1
- package/ts_web/classes.controllersocketclient.ts +5 -2
- package/ts_web/elements.metricsboard.ts +68 -18
|
@@ -37,10 +37,41 @@ const claudeNativeLaunch = (optionsArg: IInferenceLaunchOptions & { catalog: { p
|
|
|
37
37
|
};
|
|
38
38
|
|
|
39
39
|
/**
|
|
40
|
-
* Claude Code on a ChatGPT pool: the proxy's Anthropic Messages endpoint
|
|
41
|
-
* for every model alias, the context window the catalog states for it, and
|
|
42
|
-
*
|
|
43
|
-
*
|
|
40
|
+
* The variables that put Claude Code on a ChatGPT pool: the proxy's Anthropic Messages endpoint and credential, the
|
|
41
|
+
* pool's default model for every model alias, the context window the catalog states for it, and nothing beyond its model
|
|
42
|
+
* requests: no experimental betas, no nonessential traffic, no attribution. `agl proxy run claude --provider openai` and a
|
|
43
|
+
* ChatGPT-pool confined run give Claude Code these alike.
|
|
44
|
+
*/
|
|
45
|
+
export const claudeChatGptEnvironment = (inputArg: {
|
|
46
|
+
baseUrl: string;
|
|
47
|
+
token: string;
|
|
48
|
+
models: readonly TInferenceModel[];
|
|
49
|
+
}): Record<string, string> => {
|
|
50
|
+
const model = inputArg.models.find((entryArg) => entryArg.isDefault) ?? inputArg.models[0];
|
|
51
|
+
if (!model) throw new Error('The ChatGPT pool offers no model Claude Code can run on.');
|
|
52
|
+
return {
|
|
53
|
+
ANTHROPIC_BASE_URL: inputArg.baseUrl,
|
|
54
|
+
ANTHROPIC_AUTH_TOKEN: inputArg.token,
|
|
55
|
+
ANTHROPIC_MODEL: model.modelId,
|
|
56
|
+
ANTHROPIC_DEFAULT_OPUS_MODEL: model.modelId,
|
|
57
|
+
ANTHROPIC_DEFAULT_SONNET_MODEL: model.modelId,
|
|
58
|
+
ANTHROPIC_DEFAULT_HAIKU_MODEL: model.modelId,
|
|
59
|
+
...(model.contextWindow === undefined ? {} : { CLAUDE_CODE_MAX_CONTEXT_TOKENS: String(model.contextWindow) }),
|
|
60
|
+
CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS: '1',
|
|
61
|
+
CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC: '1',
|
|
62
|
+
CLAUDE_CODE_ATTRIBUTION_HEADER: '0',
|
|
63
|
+
};
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
/** Every variable `claudeChatGptEnvironment` may set, the context window's included. */
|
|
67
|
+
const claudeChatGptVariables: readonly string[] = ['ANTHROPIC_BASE_URL', 'ANTHROPIC_AUTH_TOKEN', 'ANTHROPIC_MODEL',
|
|
68
|
+
'ANTHROPIC_DEFAULT_OPUS_MODEL', 'ANTHROPIC_DEFAULT_SONNET_MODEL', 'ANTHROPIC_DEFAULT_HAIKU_MODEL',
|
|
69
|
+
'CLAUDE_CODE_MAX_CONTEXT_TOKENS', 'CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS', 'CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC',
|
|
70
|
+
'CLAUDE_CODE_ATTRIBUTION_HEADER'];
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Claude Code on a ChatGPT pool (`claudeChatGptEnvironment`), with the pool's models as the whole `/model` picker. Its
|
|
74
|
+
* configuration lives in its own directory, never the host's, so the host's `settings.json` cannot route it elsewhere.
|
|
44
75
|
*/
|
|
45
76
|
export const claudeChatGptLaunch = (inputArg: {
|
|
46
77
|
baseUrl: string;
|
|
@@ -48,21 +79,10 @@ export const claudeChatGptLaunch = (inputArg: {
|
|
|
48
79
|
configDirectory: string;
|
|
49
80
|
models: readonly TInferenceModel[];
|
|
50
81
|
}): { environment: Record<string, string>; args: string[] } => {
|
|
51
|
-
const model = inputArg.models.find((entryArg) => entryArg.isDefault) ?? inputArg.models[0];
|
|
52
|
-
if (!model) throw new Error('The ChatGPT pool offers no model Claude Code can run on.');
|
|
53
82
|
return {
|
|
54
83
|
environment: {
|
|
55
84
|
CLAUDE_CONFIG_DIR: inputArg.configDirectory,
|
|
56
|
-
|
|
57
|
-
ANTHROPIC_AUTH_TOKEN: inputArg.token,
|
|
58
|
-
ANTHROPIC_MODEL: model.modelId,
|
|
59
|
-
ANTHROPIC_DEFAULT_OPUS_MODEL: model.modelId,
|
|
60
|
-
ANTHROPIC_DEFAULT_SONNET_MODEL: model.modelId,
|
|
61
|
-
ANTHROPIC_DEFAULT_HAIKU_MODEL: model.modelId,
|
|
62
|
-
...(model.contextWindow === undefined ? {} : { CLAUDE_CODE_MAX_CONTEXT_TOKENS: String(model.contextWindow) }),
|
|
63
|
-
CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS: '1',
|
|
64
|
-
CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC: '1',
|
|
65
|
-
CLAUDE_CODE_ATTRIBUTION_HEADER: '0',
|
|
85
|
+
...claudeChatGptEnvironment(inputArg),
|
|
66
86
|
},
|
|
67
87
|
args: ['--settings', JSON.stringify({
|
|
68
88
|
modelPicker: {
|
|
@@ -140,16 +160,23 @@ export const inferenceHarnessLaunch = (optionsArg: IInferenceLaunchOptions): { a
|
|
|
140
160
|
return { environment, args: args[0] !== undefined && openCodeCommands.has(args[0]) ? args : [...selection, ...args] };
|
|
141
161
|
};
|
|
142
162
|
|
|
163
|
+
/** A confined run's pool, as its launcher configures Claude Code for it: a ChatGPT-pool run with the pool's models. */
|
|
164
|
+
export type TConfinedLaunchPool = { provider: 'anthropic' } | { provider: 'openai'; models: readonly TInferenceModel[] };
|
|
165
|
+
|
|
143
166
|
/**
|
|
144
167
|
* A confined run's `docker run`: the given command, with the confined listener's directory mounted read-only at
|
|
145
168
|
* `aglProxyConfinedMountPoint` and Claude Code's settings for it added right after `run`, before the launcher's own
|
|
146
169
|
* options and image. Claude Code inside reaches the proxy through the socket alone (`ANTHROPIC_UNIX_SOCKET`, which
|
|
147
170
|
* Claude Code sends its API requests through as plain HTTP to `ANTHROPIC_BASE_URL`'s path), on the run's credential
|
|
148
|
-
* (`ANTHROPIC_AUTH_TOKEN`),
|
|
149
|
-
*
|
|
150
|
-
*
|
|
151
|
-
*
|
|
152
|
-
*
|
|
171
|
+
* (`ANTHROPIC_AUTH_TOKEN`), and sends nothing beyond its model requests. On the Claude pool it reads the pool's models
|
|
172
|
+
* from the proxy; on the ChatGPT pool it gets the variables `agl proxy run claude --provider openai` gives it
|
|
173
|
+
* (`claudeChatGptEnvironment`: the pool's default model for every alias, its context window), but neither that command's
|
|
174
|
+
* configuration directory, which lies in the AGL home, nor its `/model` picker, an option of the container's command
|
|
175
|
+
* that the launcher does not touch. The container gets each variable by name from the docker command's environment, so
|
|
176
|
+
* the credential never appears on a command line; that environment drops every variable that would give Claude Code
|
|
177
|
+
* another login or route, and on the ChatGPT pool every model variable it sets, so `-e NAME` of one of them passes
|
|
178
|
+
* nothing. The mount is the socket's own directory, which holds nothing else and survives a proxy restart, unlike a
|
|
179
|
+
* mount of the socket file. Only `docker run` and `docker container run` are taken.
|
|
153
180
|
*/
|
|
154
181
|
export const confinedDockerLaunch = (optionsArg: {
|
|
155
182
|
args: readonly string[];
|
|
@@ -159,6 +186,7 @@ export const confinedDockerLaunch = (optionsArg: {
|
|
|
159
186
|
baseUrl: string;
|
|
160
187
|
token: string;
|
|
161
188
|
environment: NodeJS.ProcessEnv;
|
|
189
|
+
pool: TConfinedLaunchPool;
|
|
162
190
|
}): { command: string; args: string[]; environment: NodeJS.ProcessEnv } => {
|
|
163
191
|
const [command, ...args] = optionsArg.args;
|
|
164
192
|
const runAt = args[0] === 'run' ? 1 : args[0] === 'container' && args[1] === 'run' ? 2 : 0;
|
|
@@ -168,13 +196,19 @@ export const confinedDockerLaunch = (optionsArg: {
|
|
|
168
196
|
const directory = path.dirname(optionsArg.socketPath);
|
|
169
197
|
// `--mount` separates its fields with commas and reads quotes; a directory naming either cannot be passed safely.
|
|
170
198
|
if (/[,"'\n]/u.test(directory)) throw new Error(`The confined listener's directory ${directory} cannot be mounted: it holds a comma, quote or line break.`);
|
|
171
|
-
const
|
|
199
|
+
const socket = `${aglProxyConfinedMountPoint}/${path.basename(optionsArg.socketPath)}`;
|
|
200
|
+
const variables: Record<string, string> = optionsArg.pool.provider === 'anthropic' ? {
|
|
172
201
|
...claudeProxyEnvironment(optionsArg.baseUrl),
|
|
173
|
-
ANTHROPIC_UNIX_SOCKET:
|
|
202
|
+
ANTHROPIC_UNIX_SOCKET: socket,
|
|
174
203
|
ANTHROPIC_AUTH_TOKEN: optionsArg.token,
|
|
204
|
+
} : {
|
|
205
|
+
...claudeChatGptEnvironment({ baseUrl: `${optionsArg.baseUrl}/anthropic`, token: optionsArg.token,
|
|
206
|
+
models: optionsArg.pool.models }),
|
|
207
|
+
ANTHROPIC_UNIX_SOCKET: socket,
|
|
175
208
|
};
|
|
176
209
|
const environment = { ...optionsArg.environment };
|
|
177
210
|
for (const name of claudeProxyReservedVariables) delete environment[name];
|
|
211
|
+
if (optionsArg.pool.provider === 'openai') for (const name of claudeChatGptVariables) delete environment[name];
|
|
178
212
|
return {
|
|
179
213
|
command,
|
|
180
214
|
args: [...args.slice(0, runAt),
|
package/ts/interfaces.auth.ts
CHANGED
|
@@ -7,9 +7,10 @@ export type TControllerAuthLifecycleState = 'setupRequired' | 'ready';
|
|
|
7
7
|
export type TWebAuthnCeremonyKind = 'setup' | 'authentication';
|
|
8
8
|
export type TWebAuthnCeremonyState = 'pending' | 'consumed';
|
|
9
9
|
/**
|
|
10
|
-
* Single source of the audit
|
|
10
|
+
* Single source of the audit types this release writes: the persisted-document
|
|
11
11
|
* assertion consumes this list at runtime, so a type added here is
|
|
12
|
-
* automatically storable — the two can no longer
|
|
12
|
+
* automatically storable with its type-specific checks — the two can no longer
|
|
13
|
+
* drift apart. Other well-formed types are a later release's and still read.
|
|
13
14
|
*/
|
|
14
15
|
export const controllerAuditEventTypes = [
|
|
15
16
|
'controller.started',
|
|
@@ -249,11 +250,16 @@ export type TControllerAuditSessionId =
|
|
|
249
250
|
| IControllerRuntimeId
|
|
250
251
|
| plugins.browserRuntime.IBrowserClaudeSessionId;
|
|
251
252
|
|
|
253
|
+
/**
|
|
254
|
+
* A stored audit event. The audit log is shared with later releases, so a stored `type` is any audit type, also one
|
|
255
|
+
* this release does not know (a later release's, read back after a rollback). This release writes only its own types
|
|
256
|
+
* (`IRecordAuditEventInput`).
|
|
257
|
+
*/
|
|
252
258
|
export interface IControllerAuditEvent {
|
|
253
259
|
id: string;
|
|
254
260
|
controllerId: string;
|
|
255
261
|
timestamp: Date;
|
|
256
|
-
type:
|
|
262
|
+
type: string;
|
|
257
263
|
outcome: TControllerAuditOutcome;
|
|
258
264
|
operationId?: string;
|
|
259
265
|
peerId?: string;
|
|
@@ -263,6 +269,11 @@ export interface IControllerAuditEvent {
|
|
|
263
269
|
/** The sender label an agent message declared (`session.message`). */
|
|
264
270
|
senderLabel?: string;
|
|
265
271
|
localCli?: IConfigurationLocalCliActor;
|
|
272
|
+
/**
|
|
273
|
+
* What a later release's type records beyond the envelope (bounded plain JSON). This release never writes or
|
|
274
|
+
* interprets it; it is declared so a rolled-back controller still reads and prunes such events.
|
|
275
|
+
*/
|
|
276
|
+
details?: Record<string, unknown>;
|
|
266
277
|
}
|
|
267
278
|
|
|
268
279
|
export interface IControllerTempPasswordDocument {
|
package/ts_interfaces/index.ts
CHANGED
|
@@ -9,6 +9,7 @@ export * from './functions.browserpaintcopy.js';
|
|
|
9
9
|
export * from './devtools.js';
|
|
10
10
|
export * from './requests.js';
|
|
11
11
|
export * from './mcprequests.js';
|
|
12
|
+
export * from './management.js';
|
|
12
13
|
export * from './codex.js';
|
|
13
14
|
export * from './mcpworkspacerequests.js';
|
|
14
15
|
export * from './mcpjobrequests.js';
|
|
@@ -5,7 +5,7 @@ import type {
|
|
|
5
5
|
TControllerBrowserPaintAvailability,
|
|
6
6
|
TControllerBrowserPaintTermination,
|
|
7
7
|
} from './browserpaint.js';
|
|
8
|
-
export const controllerProtocolVersion =
|
|
8
|
+
export const controllerProtocolVersion = 47 as const;
|
|
9
9
|
export const controllerUpgradeManagementVersion = 2 as const;
|
|
10
10
|
export const controllerPackageName = 'agl' as const;
|
|
11
11
|
/** The systemd user unit `agl service install` writes; a controller it runs reports it in its status. */
|
|
@@ -325,6 +325,11 @@ export interface IControllerStatus {
|
|
|
325
325
|
packageVersion: string;
|
|
326
326
|
protocolVersion: typeof controllerProtocolVersion;
|
|
327
327
|
upgradeManagementVersion: number;
|
|
328
|
+
/**
|
|
329
|
+
* The protocol of the controller's loopback management endpoint (`controllerManagementProtocolVersion`); absent
|
|
330
|
+
* from a controller older than the endpoint, which is reached over its socket.
|
|
331
|
+
*/
|
|
332
|
+
managementProtocolVersion?: number;
|
|
328
333
|
controllerPid: number;
|
|
329
334
|
processGroupId: number;
|
|
330
335
|
processFingerprint: string;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { IReq_ControllerStatus } from './requests.status.js';
|
|
2
|
+
import type {
|
|
3
|
+
IReq_ControllerUpgradeFinalize,
|
|
4
|
+
IReq_ControllerUpgradeLaunch,
|
|
5
|
+
IReq_ControllerUpgradePrepareBegin,
|
|
6
|
+
} from './requests.upgrade.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* The controller's loopback management endpoint, on the controller's own port: one TypedRequest envelope
|
|
10
|
+
* (`method`, `request`, `response`, `correlation`, `requestInstanceId`) per HTTP POST of `application/json`, with
|
|
11
|
+
* `Host: 127.0.0.1:<port>`. The local CLI and the upgrade worker read the status and drive an upgrade through it,
|
|
12
|
+
* so an upgrade worker of one release can finalize the controller of the next whatever transport the browser
|
|
13
|
+
* speaks. Every later release keeps this path, these methods, their request and response shapes and the envelope
|
|
14
|
+
* (`test/test.controllermanagementwire.node.ts` pins the bytes an AGL 41 client sends and what it accepts).
|
|
15
|
+
*/
|
|
16
|
+
export const controllerManagementTypedRequestPath = '/management/typedrequest' as const;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* The protocol of this loopback management endpoint: its path, methods, envelope and their request and response
|
|
20
|
+
* shapes. 1 is the endpoint as its first release ships it; only an incompatible change to that contract raises it.
|
|
21
|
+
* A release declares it twice: as `controllerManagementProtocolVersion` in its `package.json`, which a client reads
|
|
22
|
+
* before the release's controller runs, and as `managementProtocolVersion` in that controller's status. A client
|
|
23
|
+
* reaches a controller over this endpoint only when it declares 1 or more, and over the controller socket otherwise
|
|
24
|
+
* (an older release has no endpoint).
|
|
25
|
+
*
|
|
26
|
+
* It is not `controllerUpgradeManagementVersion` (2), the upgrade contract: every upgrade worker requires exactly
|
|
27
|
+
* that number in its target's `package.json` and status, and upgrade transactions record it, so it cannot move
|
|
28
|
+
* without breaking the online upgrade of every earlier release. This number says only how the management methods
|
|
29
|
+
* are reached.
|
|
30
|
+
*/
|
|
31
|
+
export const controllerManagementProtocolVersion = 1 as const;
|
|
32
|
+
|
|
33
|
+
/** The methods the endpoint answers; every other method is refused before it is routed. */
|
|
34
|
+
export const controllerManagementMethods = [
|
|
35
|
+
'controller.status',
|
|
36
|
+
'controller.upgrade.launch',
|
|
37
|
+
'controller.upgrade.prepare.begin',
|
|
38
|
+
'controller.upgrade.finalize',
|
|
39
|
+
] as const;
|
|
40
|
+
|
|
41
|
+
export type TControllerManagementMethod = typeof controllerManagementMethods[number];
|
|
42
|
+
|
|
43
|
+
export type TControllerManagementTypedRequest =
|
|
44
|
+
| IReq_ControllerStatus
|
|
45
|
+
| IReq_ControllerUpgradeLaunch
|
|
46
|
+
| IReq_ControllerUpgradePrepareBegin
|
|
47
|
+
| IReq_ControllerUpgradeFinalize;
|
|
48
|
+
|
|
49
|
+
/** A management request carries a token, a version and a mode at most. */
|
|
50
|
+
export const controllerManagementRequestMaximumBodyBytes = 16 * 1024;
|
|
51
|
+
/** The status document with every harness profile and upgrade blocker it may report. */
|
|
52
|
+
export const controllerManagementResponseMaximumBytes = 1024 * 1024;
|
package/ts_interfaces/metrics.ts
CHANGED
|
@@ -10,8 +10,27 @@ export type TControllerUsageWindow = '1h' | '24h' | '7d';
|
|
|
10
10
|
export const controllerUsageWindows: readonly TControllerUsageWindow[] = ['1h', '24h', '7d'];
|
|
11
11
|
export type TControllerUsageProvider = 'anthropic' | 'openai';
|
|
12
12
|
|
|
13
|
+
/**
|
|
14
|
+
* The tokens of the answers one bucket counts. Input counts only the tokens read neither from nor written to the
|
|
15
|
+
* provider's prompt cache. `cacheReadTokens` and `cacheWriteTokens` add up the cache reads and writes the answers
|
|
16
|
+
* stated; `cacheReadUnknownCalls` and `cacheWriteUnknownCalls` (each at most `calls`) count the answers that did not
|
|
17
|
+
* state theirs, which read or wrote an unknown number, not none. With either above 0 the stated sum is not the whole
|
|
18
|
+
* count, and `controllerUsageTotalIsLowerBound` of the bucket's provider says whether its total is then one. A bucket
|
|
19
|
+
* stored before these were counted reads with all its answers unknown.
|
|
20
|
+
*/
|
|
21
|
+
export interface IControllerUsageCounts {
|
|
22
|
+
/** The answers counted. */
|
|
23
|
+
calls: number;
|
|
24
|
+
inputTokens: number;
|
|
25
|
+
outputTokens: number;
|
|
26
|
+
cacheReadTokens: number;
|
|
27
|
+
cacheWriteTokens: number;
|
|
28
|
+
cacheReadUnknownCalls: number;
|
|
29
|
+
cacheWriteUnknownCalls: number;
|
|
30
|
+
}
|
|
31
|
+
|
|
13
32
|
/** One span of one model's tokens on one harness, account and session; never a prompt, a credential or an answer. */
|
|
14
|
-
export interface IControllerUsageBucket {
|
|
33
|
+
export interface IControllerUsageBucket extends IControllerUsageCounts {
|
|
15
34
|
/** The bucket's digest: SHA-256 of its span, start and dimensions. */
|
|
16
35
|
id: string;
|
|
17
36
|
span: TControllerUsageSpan;
|
|
@@ -25,14 +44,22 @@ export interface IControllerUsageBucket {
|
|
|
25
44
|
accountId: string;
|
|
26
45
|
/** The session id the client sent; `null` when it sent none. */
|
|
27
46
|
session: string | null;
|
|
28
|
-
/** The answers counted. */
|
|
29
|
-
calls: number;
|
|
30
|
-
inputTokens: number;
|
|
31
|
-
outputTokens: number;
|
|
32
|
-
cacheReadTokens: number;
|
|
33
|
-
cacheWriteTokens: number;
|
|
34
47
|
}
|
|
35
48
|
|
|
49
|
+
/**
|
|
50
|
+
* Whether a provider's answers state their input without the tokens read from or written to the cache, so that a bucket
|
|
51
|
+
* of that provider with unknown cache counts (`cacheReadUnknownCalls` or `cacheWriteUnknownCalls` above 0) has total
|
|
52
|
+
* tokens (input, output, cache reads and cache writes) that are a lower bound of what its answers used. An Anthropic
|
|
53
|
+
* account's answers leave the cache out of their input, so a cache count such an answer did not state is counted
|
|
54
|
+
* nowhere. An OpenAI account's answers state every input token in their input count, the Messages answers the proxy
|
|
55
|
+
* translates from them included, so their unknown cache counts lie inside the input and their total is exact. Every
|
|
56
|
+
* provider must be named here: a new one fails the type check instead of reading as exact.
|
|
57
|
+
*/
|
|
58
|
+
export const controllerUsageTotalIsLowerBound: Readonly<Record<TControllerUsageProvider, boolean>> = {
|
|
59
|
+
anthropic: true,
|
|
60
|
+
openai: false,
|
|
61
|
+
};
|
|
62
|
+
|
|
36
63
|
const minuteMs = 60_000;
|
|
37
64
|
const hourMs = 60 * minuteMs;
|
|
38
65
|
|
|
@@ -606,7 +606,8 @@ export const isControllerSessionFeedList = (
|
|
|
606
606
|
|
|
607
607
|
const usageBucketKeys = contractKeys<interfaces.IControllerUsageBucket>({
|
|
608
608
|
id: true, span: true, start: true, provider: true, model: true, harness: true, accountId: true, session: true,
|
|
609
|
-
calls: true, inputTokens: true, outputTokens: true, cacheReadTokens: true, cacheWriteTokens: true,
|
|
609
|
+
calls: true, inputTokens: true, outputTokens: true, cacheReadTokens: true, cacheWriteTokens: true, cacheReadUnknownCalls: true,
|
|
610
|
+
cacheWriteUnknownCalls: true,
|
|
610
611
|
});
|
|
611
612
|
const usageHex64 = /^[a-f0-9]{64}$/;
|
|
612
613
|
const usageCount = (valueArg: unknown): boolean => Number.isSafeInteger(valueArg) && (valueArg as number) >= 0;
|
|
@@ -629,7 +630,9 @@ export const isControllerUsageBucket = (valueArg: unknown): valueArg is interfac
|
|
|
629
630
|
&& (candidate.session === null || usageText(candidate.session, 512))
|
|
630
631
|
&& usageCount(candidate.calls) && (candidate.calls as number) >= 1
|
|
631
632
|
&& usageCount(candidate.inputTokens) && usageCount(candidate.outputTokens)
|
|
632
|
-
&& usageCount(candidate.cacheReadTokens) && usageCount(candidate.cacheWriteTokens)
|
|
633
|
+
&& usageCount(candidate.cacheReadTokens) && usageCount(candidate.cacheWriteTokens)
|
|
634
|
+
&& usageCount(candidate.cacheReadUnknownCalls) && (candidate.cacheReadUnknownCalls as number) <= (candidate.calls as number)
|
|
635
|
+
&& usageCount(candidate.cacheWriteUnknownCalls) && (candidate.cacheWriteUnknownCalls as number) <= (candidate.calls as number);
|
|
633
636
|
};
|
|
634
637
|
|
|
635
638
|
export const isControllerUsageDelta = (valueArg: unknown): valueArg is interfaces.TControllerUsageDelta => {
|
|
@@ -13,23 +13,29 @@ export interface IMetricsBoardSender {
|
|
|
13
13
|
): Promise<TRequest['response']>;
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
-
type TUsageCounts =
|
|
17
|
-
|
|
16
|
+
type TUsageCounts = interfaces.IControllerUsageCounts;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* A table row's counts: its buckets' counts, their total tokens and whether that total is a lower bound
|
|
20
|
+
* (some bucket of it has unknown cache counts and a provider `controllerUsageTotalIsLowerBound` marks).
|
|
21
|
+
*/
|
|
22
|
+
interface IMetricsRowCounts extends TUsageCounts {
|
|
23
|
+
totalTokens: number;
|
|
24
|
+
totalLowerBound: boolean;
|
|
25
|
+
}
|
|
18
26
|
|
|
19
27
|
/** A model's usage over the window: one row of the models table. */
|
|
20
|
-
export interface IMetricsModelRow extends
|
|
28
|
+
export interface IMetricsModelRow extends IMetricsRowCounts {
|
|
21
29
|
key: string;
|
|
22
30
|
provider: interfaces.TControllerUsageProvider;
|
|
23
31
|
model: string;
|
|
24
|
-
totalTokens: number;
|
|
25
32
|
}
|
|
26
33
|
|
|
27
34
|
/** One session on one harness of the opened model: one row of the drill-down table. */
|
|
28
|
-
export interface IMetricsSessionRow extends
|
|
35
|
+
export interface IMetricsSessionRow extends IMetricsRowCounts {
|
|
29
36
|
key: string;
|
|
30
37
|
harness: string;
|
|
31
38
|
session: string;
|
|
32
|
-
totalTokens: number;
|
|
33
39
|
}
|
|
34
40
|
|
|
35
41
|
const windowOptions: { key: interfaces.TControllerUsageWindow; label: string; description: string }[] = [
|
|
@@ -42,12 +48,15 @@ const modelKey = (bucketArg: Pick<interfaces.IControllerUsageBucket, 'provider'
|
|
|
42
48
|
`${bucketArg.provider}/${bucketArg.model}`
|
|
43
49
|
);
|
|
44
50
|
|
|
45
|
-
/** Every token a call counted: input, output
|
|
51
|
+
/** Every token a call counted: input, output and the cache reads and writes it stated. */
|
|
46
52
|
const totalTokens = (countsArg: TUsageCounts): number => (
|
|
47
53
|
countsArg.inputTokens + countsArg.outputTokens + countsArg.cacheReadTokens + countsArg.cacheWriteTokens
|
|
48
54
|
);
|
|
49
55
|
|
|
50
|
-
const emptyCounts = (): TUsageCounts => ({ calls: 0, inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0
|
|
56
|
+
const emptyCounts = (): TUsageCounts => ({ calls: 0, inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0,
|
|
57
|
+
cacheReadUnknownCalls: 0, cacheWriteUnknownCalls: 0 });
|
|
58
|
+
|
|
59
|
+
const emptyRowCounts = (): IMetricsRowCounts => ({ ...emptyCounts(), totalTokens: 0, totalLowerBound: false });
|
|
51
60
|
|
|
52
61
|
const addCounts = (targetArg: TUsageCounts, bucketArg: TUsageCounts): void => {
|
|
53
62
|
targetArg.calls += bucketArg.calls;
|
|
@@ -55,6 +64,31 @@ const addCounts = (targetArg: TUsageCounts, bucketArg: TUsageCounts): void => {
|
|
|
55
64
|
targetArg.outputTokens += bucketArg.outputTokens;
|
|
56
65
|
targetArg.cacheReadTokens += bucketArg.cacheReadTokens;
|
|
57
66
|
targetArg.cacheWriteTokens += bucketArg.cacheWriteTokens;
|
|
67
|
+
targetArg.cacheReadUnknownCalls += bucketArg.cacheReadUnknownCalls;
|
|
68
|
+
targetArg.cacheWriteUnknownCalls += bucketArg.cacheWriteUnknownCalls;
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const addBucket = (rowArg: IMetricsRowCounts, bucketArg: interfaces.IControllerUsageBucket): void => {
|
|
72
|
+
addCounts(rowArg, bucketArg);
|
|
73
|
+
rowArg.totalTokens += totalTokens(bucketArg);
|
|
74
|
+
rowArg.totalLowerBound ||= interfaces.controllerUsageTotalIsLowerBound[bucketArg.provider]
|
|
75
|
+
&& (bucketArg.cacheReadUnknownCalls > 0 || bucketArg.cacheWriteUnknownCalls > 0);
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
/** The cache counts the board states: reads and writes, each with the calls that did not state theirs. */
|
|
79
|
+
const cacheCounts = {
|
|
80
|
+
cacheReadTokens: 'cacheReadUnknownCalls',
|
|
81
|
+
cacheWriteTokens: 'cacheWriteUnknownCalls',
|
|
82
|
+
} as const satisfies Record<string, keyof TUsageCounts>;
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* A cache count as the board states it: what the calls stated; "≥" before it when some calls did not state theirs, as
|
|
86
|
+
* those read or wrote an unknown number, not none; and "Unknown" when no call stated any.
|
|
87
|
+
*/
|
|
88
|
+
const cacheText = (countsArg: TUsageCounts, keyArg: keyof typeof cacheCounts, formatArg: Intl.NumberFormat): string => {
|
|
89
|
+
const stated = countsArg[keyArg], unknownCalls = countsArg[cacheCounts[keyArg]];
|
|
90
|
+
return unknownCalls === 0 ? formatArg.format(stated)
|
|
91
|
+
: stated === 0 && unknownCalls === countsArg.calls ? 'Unknown' : `≥ ${formatArg.format(stated)}`;
|
|
58
92
|
};
|
|
59
93
|
|
|
60
94
|
const compactNumber = new Intl.NumberFormat(undefined, { notation: 'compact', maximumFractionDigits: 1 });
|
|
@@ -68,7 +102,8 @@ const pointTimeFormatters: Record<interfaces.TControllerUsageSpan, (epochMsArg:
|
|
|
68
102
|
hour: (epochMsArg) => momentFormat.formatRange(epochMsArg, epochMsArg + interfaces.controllerUsageSpanMs.hour),
|
|
69
103
|
};
|
|
70
104
|
|
|
71
|
-
|
|
105
|
+
/** The count columns; a cache count as `cacheText` states it, and the total with "≥" when it is a lower bound. */
|
|
106
|
+
const countColumns = <TRow extends IMetricsRowCounts>(): plugins.deesCatalog.Column<TRow>[] => (
|
|
72
107
|
([
|
|
73
108
|
['calls', 'Calls'],
|
|
74
109
|
['inputTokens', 'Input'],
|
|
@@ -80,7 +115,9 @@ const countColumns = <TRow extends TUsageCounts & { totalTokens: number }>(): pl
|
|
|
80
115
|
key: keyArg,
|
|
81
116
|
header: headerArg,
|
|
82
117
|
align: 'end' as const,
|
|
83
|
-
renderer: (valueArg: number) =>
|
|
118
|
+
renderer: (valueArg: number, rowArg: TRow) => (keyArg === 'cacheReadTokens' || keyArg === 'cacheWriteTokens'
|
|
119
|
+
? cacheText(rowArg, keyArg, fullNumber)
|
|
120
|
+
: keyArg === 'totalTokens' && rowArg.totalLowerBound ? `≥ ${fullNumber.format(valueArg)}` : fullNumber.format(valueArg)),
|
|
84
121
|
}))
|
|
85
122
|
);
|
|
86
123
|
|
|
@@ -375,11 +412,10 @@ export class AglMetricsBoard extends plugins.deesElement.DeesElement {
|
|
|
375
412
|
const key = modelKey(bucket);
|
|
376
413
|
let row = rows.get(key);
|
|
377
414
|
if (!row) {
|
|
378
|
-
row = { key, provider: bucket.provider, model: bucket.model, ...
|
|
415
|
+
row = { key, provider: bucket.provider, model: bucket.model, ...emptyRowCounts() };
|
|
379
416
|
rows.set(key, row);
|
|
380
417
|
}
|
|
381
|
-
|
|
382
|
-
row.totalTokens += totalTokens(bucket);
|
|
418
|
+
addBucket(row, bucket);
|
|
383
419
|
}
|
|
384
420
|
return [...rows.values()];
|
|
385
421
|
}
|
|
@@ -393,11 +429,10 @@ export class AglMetricsBoard extends plugins.deesElement.DeesElement {
|
|
|
393
429
|
const key = JSON.stringify([bucket.harness, bucket.session]);
|
|
394
430
|
let row = rows.get(key);
|
|
395
431
|
if (!row) {
|
|
396
|
-
row = { key, harness, session, ...
|
|
432
|
+
row = { key, harness, session, ...emptyRowCounts() };
|
|
397
433
|
rows.set(key, row);
|
|
398
434
|
}
|
|
399
|
-
|
|
400
|
-
row.totalTokens += totalTokens(bucket);
|
|
435
|
+
addBucket(row, bucket);
|
|
401
436
|
}
|
|
402
437
|
return [...rows.values()];
|
|
403
438
|
}
|
|
@@ -409,12 +444,27 @@ export class AglMetricsBoard extends plugins.deesElement.DeesElement {
|
|
|
409
444
|
return [
|
|
410
445
|
{ id: 'input', title: 'Input', value: value(counts.inputTokens), unit: 'tokens', type: 'number', icon: 'lucide:ArrowDownToLine' },
|
|
411
446
|
{ id: 'output', title: 'Output', value: value(counts.outputTokens), unit: 'tokens', type: 'number', icon: 'lucide:ArrowUpFromLine' },
|
|
412
|
-
|
|
413
|
-
|
|
447
|
+
this.cacheTile(counts, knownArg, 'cacheReadTokens'),
|
|
448
|
+
this.cacheTile(counts, knownArg, 'cacheWriteTokens'),
|
|
414
449
|
{ id: 'calls', title: 'Calls', value: value(counts.calls), type: 'number', icon: 'lucide:Activity' },
|
|
415
450
|
];
|
|
416
451
|
}
|
|
417
452
|
|
|
453
|
+
/**
|
|
454
|
+
* A cache read or write tile: "Unknown" without a unit when no call stated its count, "≥" the stated count when some
|
|
455
|
+
* did not, and then how many calls left it unknown.
|
|
456
|
+
*/
|
|
457
|
+
private cacheTile(countsArg: TUsageCounts, knownArg: boolean, keyArg: keyof typeof cacheCounts): plugins.deesCatalog.IStatsTile {
|
|
458
|
+
const text = knownArg ? cacheText(countsArg, keyArg, compactNumber) : '–';
|
|
459
|
+
const unknownCalls = countsArg[cacheCounts[keyArg]];
|
|
460
|
+
const tile = keyArg === 'cacheReadTokens' ? { id: 'cacheRead', title: 'Cache read', icon: 'lucide:DatabaseZap' }
|
|
461
|
+
: { id: 'cacheWrite', title: 'Cache write', icon: 'lucide:Database' };
|
|
462
|
+
return { ...tile, value: text, ...(text === 'Unknown' ? {} : { unit: 'tokens' }), type: 'number',
|
|
463
|
+
...(knownArg && unknownCalls > 0
|
|
464
|
+
? { description: `Unknown for ${fullNumber.format(unknownCalls)} of ${fullNumber.format(countsArg.calls)} calls` }
|
|
465
|
+
: {}) };
|
|
466
|
+
}
|
|
467
|
+
|
|
418
468
|
/**
|
|
419
469
|
* One series per model, the largest first, each drawn from zero: its tokens per minute in every span of the window,
|
|
420
470
|
* the empty ones as 0. A minute that is still open is left out, as its count is partial; the totals and tables keep
|