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.
- package/changelog.md +33 -0
- package/dist_serve/bundle.js +2228 -2177
- package/dist_serve/bundle.js.map +2 -2
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/aglapi/aglapisurface.document.js +2 -2
- package/dist_ts/classes.accountsactions.d.ts +11 -1
- package/dist_ts/classes.accountsactions.js +59 -2
- package/dist_ts/classes.controller.js +5 -5
- package/dist_ts/classes.flexaccountaccess.js +3 -2
- package/dist_ts/classes.harnessreleases.d.ts +14 -4
- package/dist_ts/classes.harnessreleases.js +28 -4
- package/dist_ts/classes.inferencegateway.d.ts +28 -3
- package/dist_ts/classes.inferencegateway.js +177 -61
- package/dist_ts/classes.inferenceplacement.d.ts +5 -3
- package/dist_ts/classes.inferenceplacement.js +2 -2
- package/dist_ts/classes.inferencerouter.d.ts +349 -91
- package/dist_ts/classes.inferencerouter.js +804 -297
- package/dist_ts/classes.inferenceservice.d.ts +14 -2
- package/dist_ts/classes.inferenceservice.js +53 -14
- package/dist_ts/classes.inferencespend.d.ts +339 -0
- package/dist_ts/classes.inferencespend.js +815 -0
- package/dist_ts/classes.inferenceusagecache.d.ts +8 -0
- package/dist_ts/classes.inferenceusagecache.js +9 -5
- package/dist_ts/classes.inferenceusagetap.d.ts +50 -9
- package/dist_ts/classes.inferenceusagetap.js +185 -31
- package/dist_ts/classes.proxycontrol.d.ts +3 -2
- package/dist_ts/classes.proxycontrol.js +1 -2
- package/dist_ts/classes.proxydaemon.js +3 -2
- package/dist_ts/classes.proxystore.d.ts +6 -0
- package/dist_ts/classes.proxystore.js +11 -2
- package/dist_ts/fleet/classes.claudeworkers.d.ts +5 -4
- package/dist_ts/fleet/classes.claudeworkers.js +10 -9
- package/dist_ts/functions.claudecoderelease.d.ts +12 -6
- package/dist_ts/functions.claudecoderelease.js +13 -7
- package/dist_ts/functions.inferencecli.js +12 -8
- package/dist_ts/interfaces.auth.d.ts +1 -1
- package/dist_ts/interfaces.auth.js +3 -1
- package/dist_ts/plugins.d.ts +3 -1
- package/dist_ts/plugins.js +7 -2
- package/dist_ts_authswitch/authority-apikeys.d.ts +32 -0
- package/dist_ts_authswitch/authority-apikeys.js +65 -0
- package/dist_ts_authswitch/authority-contract.d.ts +172 -33
- package/dist_ts_authswitch/authority-contract.js +54 -1
- package/dist_ts_authswitch/authority-overview.js +8 -1
- package/dist_ts_authswitch/authority-placement.d.ts +7 -2
- package/dist_ts_authswitch/authority-placement.js +10 -3
- package/dist_ts_authswitch/authority-runtime-contract.d.ts +2 -2
- package/dist_ts_authswitch/authority-spend.d.ts +103 -0
- package/dist_ts_authswitch/authority-spend.js +110 -0
- package/dist_ts_authswitch/authority-subscription.d.ts +12 -1
- package/dist_ts_authswitch/authority-subscription.js +24 -1
- package/dist_ts_authswitch/authority-usagelimit.d.ts +7 -4
- package/dist_ts_authswitch/authority-usagelimit.js +89 -39
- package/dist_ts_authswitch/authority-usagewindows.d.ts +4 -1
- package/dist_ts_authswitch/authority-usagewindows.js +41 -16
- package/dist_ts_authswitch/classes.authoritybroker.d.ts +30 -19
- package/dist_ts_authswitch/classes.authoritybroker.js +152 -61
- package/dist_ts_authswitch/classes.authoritycli.d.ts +9 -4
- package/dist_ts_authswitch/classes.authoritycli.js +99 -30
- package/dist_ts_authswitch/classes.authorityclient.d.ts +24 -7
- package/dist_ts_authswitch/classes.authorityclient.js +45 -15
- package/dist_ts_authswitch/classes.authoritydaemon.js +39 -12
- package/dist_ts_authswitch/classes.authoritydatabase.d.ts +28 -5
- package/dist_ts_authswitch/classes.authoritydatabase.js +171 -9
- package/dist_ts_authswitch/classes.authorityimport.js +2 -2
- package/dist_ts_authswitch/classes.authoritymodels.d.ts +71 -3
- package/dist_ts_authswitch/classes.authoritymodels.js +249 -20
- package/dist_ts_authswitch/classes.authorityregistry.js +4 -2
- package/dist_ts_authswitch/classes.authorityusage.d.ts +69 -7
- package/dist_ts_authswitch/classes.authorityusage.js +118 -4
- package/dist_ts_authswitch/classes.claudenative.d.ts +23 -8
- package/dist_ts_authswitch/classes.claudenative.js +49 -21
- package/dist_ts_authswitch/classes.claudesignin.d.ts +14 -2
- package/dist_ts_authswitch/classes.claudesignin.js +72 -23
- package/dist_ts_authswitch/classes.codexmanaged.d.ts +8 -0
- package/dist_ts_authswitch/classes.codexmanaged.js +33 -18
- package/dist_ts_authswitch/classes.harnessprocesses.d.ts +8 -0
- package/dist_ts_authswitch/classes.harnessprocesses.js +41 -9
- package/dist_ts_authswitch/testsupport.d.ts +24 -5
- package/dist_ts_authswitch/testsupport.js +56 -12
- package/dist_ts_authswitch/ts_migration/0011_account_api_key_fields.d.ts +12 -0
- package/dist_ts_authswitch/ts_migration/0011_account_api_key_fields.js +21 -0
- package/dist_ts_authswitch/ts_migration/index.js +3 -1
- package/dist_ts_interfaces/functions.authorityscope.d.ts +18 -7
- package/dist_ts_interfaces/functions.authorityscope.js +35 -8
- package/dist_ts_interfaces/interfaces.d.ts +1 -1
- package/dist_ts_interfaces/interfaces.js +1 -1
- package/dist_ts_interfaces/plugins.d.ts +2 -1
- package/dist_ts_interfaces/plugins.js +7 -4
- package/dist_ts_interfaces/requests.accounts.d.ts +56 -0
- package/dist_ts_interfaces/requests.accounts.js +11 -1
- package/dist_ts_interfaces/requests.d.ts +2 -2
- package/dist_ts_interfaces/requests.js +1 -1
- package/docs/authswitch.md +75 -12
- package/package.json +5 -5
- package/readme.md +199 -22
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/aglapi/aglapisurface.document.ts +2 -2
- package/ts/classes.accountsactions.ts +75 -1
- package/ts/classes.controller.ts +4 -4
- package/ts/classes.flexaccountaccess.ts +2 -1
- package/ts/classes.harnessreleases.ts +25 -4
- package/ts/classes.inferencegateway.ts +184 -64
- package/ts/classes.inferenceplacement.ts +6 -4
- package/ts/classes.inferencerouter.ts +1026 -314
- package/ts/classes.inferenceservice.ts +56 -13
- package/ts/classes.inferencespend.ts +870 -0
- package/ts/classes.inferenceusagecache.ts +16 -4
- package/ts/classes.inferenceusagetap.ts +159 -34
- package/ts/classes.proxycontrol.ts +3 -3
- package/ts/classes.proxydaemon.ts +2 -1
- package/ts/classes.proxystore.ts +11 -1
- package/ts/fleet/classes.claudeworkers.ts +9 -8
- package/ts/functions.claudecoderelease.ts +13 -6
- package/ts/functions.inferencecli.ts +11 -7
- package/ts/interfaces.auth.ts +2 -0
- package/ts/plugins.ts +7 -0
- package/ts_authswitch/authority-apikeys.ts +85 -0
- package/ts_authswitch/authority-contract.ts +201 -34
- package/ts_authswitch/authority-overview.ts +7 -0
- package/ts_authswitch/authority-placement.ts +12 -4
- package/ts_authswitch/authority-runtime-contract.ts +2 -2
- package/ts_authswitch/authority-spend.ts +168 -0
- package/ts_authswitch/authority-subscription.ts +28 -1
- package/ts_authswitch/authority-usagelimit.ts +95 -40
- package/ts_authswitch/authority-usagewindows.ts +42 -17
- package/ts_authswitch/classes.authoritybroker.ts +161 -68
- package/ts_authswitch/classes.authoritycli.ts +112 -32
- package/ts_authswitch/classes.authorityclient.ts +66 -19
- package/ts_authswitch/classes.authoritydaemon.ts +43 -13
- package/ts_authswitch/classes.authoritydatabase.ts +178 -8
- package/ts_authswitch/classes.authorityimport.ts +1 -1
- package/ts_authswitch/classes.authoritymodels.ts +165 -19
- package/ts_authswitch/classes.authorityregistry.ts +3 -0
- package/ts_authswitch/classes.authorityusage.ts +171 -10
- package/ts_authswitch/classes.claudenative.ts +51 -22
- package/ts_authswitch/classes.claudesignin.ts +74 -24
- package/ts_authswitch/classes.codexmanaged.ts +30 -16
- package/ts_authswitch/classes.harnessprocesses.ts +39 -8
- package/ts_authswitch/native-contracts.md +82 -21
- package/ts_authswitch/readme.md +1 -1
- package/ts_authswitch/testsupport.ts +58 -11
- package/ts_authswitch/ts_migration/0011_account_api_key_fields.ts +20 -0
- package/ts_authswitch/ts_migration/index.ts +2 -0
- package/ts_interfaces/functions.authorityscope.ts +43 -8
- package/ts_interfaces/interfaces.ts +1 -1
- package/ts_interfaces/plugins.ts +6 -3
- package/ts_interfaces/requests.accounts.ts +48 -0
- package/ts_interfaces/requests.ts +4 -0
- package/ts_web/00_commitinfo_data.ts +1 -1
- package/ts_web/accounts.plugins.ts +7 -1
- package/ts_web/accounts.templates.ts +20 -11
- package/ts_web/classes.accountsreadmodel.ts +326 -71
- package/ts_web/elements.accounts.ts +219 -20
- package/ts_web/elements.codexconnections.ts +3 -1
- 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
|
-
|
|
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
|
|
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
|
-
|
|
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.
|
|
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:')) {
|
|
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
|
|
168
|
-
*
|
|
169
|
-
*
|
|
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
|
-
/**
|
|
198
|
-
|
|
199
|
-
|
|
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.
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
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
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
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
|
|
223
|
-
|
|
224
|
-
|
|
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
|
-
|
|
245
|
-
:
|
|
246
|
-
const
|
|
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
|
|
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:
|
|
79
|
-
* `accountId` reads the ChatGPT pool of that one
|
|
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);
|
package/ts/classes.proxystore.ts
CHANGED
|
@@ -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 {
|
|
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
|
|
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(
|
|
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 ${
|
|
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.
|
|
74
|
-
*
|
|
75
|
-
* changed. An older or unreadable version is refused
|
|
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
|
|
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
|
|
5
|
-
* (`ts_authswitch/native-contracts.md`).
|
|
6
|
-
*
|
|
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
|
|
11
|
-
export const claudeCodeVerifiedRelease = '2.1.
|
|
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 ?
|
|
50
|
-
:
|
|
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.
|
|
80
|
-
|
|
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}`};
|
|
83
|
-
|
|
84
|
-
+ `${account.
|
|
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'
|
package/ts/interfaces.auth.ts
CHANGED
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,
|