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
|
@@ -5,9 +5,10 @@ import * as plugins from './accounts.plugins.js';
|
|
|
5
5
|
* The Accounts screen as data.
|
|
6
6
|
*
|
|
7
7
|
* The accounts service is the one place accounts and their logins live; every harness only uses
|
|
8
|
-
* them. So the screen is one list of the service's accounts, each ChatGPT or Claude account
|
|
9
|
-
* row: who it is, one status in plain words, a chip for each harness
|
|
10
|
-
*
|
|
8
|
+
* them. So the screen is one list of the service's accounts, each ChatGPT or Claude account and each
|
|
9
|
+
* OpenAI or Anthropic API key one row: who it is, one status in plain words, a chip for each harness
|
|
10
|
+
* that uses it, and its usage as meters -- a key's usage is its month's spend in dollars.
|
|
11
|
+
* Identifiers, logins and technical facts appear only on its page.
|
|
11
12
|
*
|
|
12
13
|
* Everything the screen says is decided here. The browser holds no account state of its own, keeps
|
|
13
14
|
* no uncertainty flag and runs no timer, so a failed read can never leave a view locked -- the next
|
|
@@ -114,8 +115,12 @@ export interface IAccountsServiceStatus {
|
|
|
114
115
|
|
|
115
116
|
export type TAccountsListState = 'unavailable' | 'empty' | 'complete' | 'partial';
|
|
116
117
|
|
|
117
|
-
/**
|
|
118
|
+
/**
|
|
119
|
+
* The accounts of one kind under its name: ChatGPT and Claude sign-ins, then OpenAI and Anthropic API keys. `id` is the
|
|
120
|
+
* provider for a sign-in group and `<provider>-api-key` for a key group.
|
|
121
|
+
*/
|
|
118
122
|
export interface IAccountGroup {
|
|
123
|
+
id: string;
|
|
119
124
|
providerId: string;
|
|
120
125
|
label: string;
|
|
121
126
|
rows: IAccountRow[];
|
|
@@ -123,7 +128,7 @@ export interface IAccountGroup {
|
|
|
123
128
|
|
|
124
129
|
export interface IAccountsList {
|
|
125
130
|
state: TAccountsListState;
|
|
126
|
-
/** The listed accounts, one group per
|
|
131
|
+
/** The listed accounts, one group per kind, in a fixed order: sign-ins by provider, then API keys by provider. */
|
|
127
132
|
groups: IAccountGroup[];
|
|
128
133
|
/** Removed accounts, kept behind a disclosure: a removal is often why a project lost access. */
|
|
129
134
|
removedRows: IAccountRow[];
|
|
@@ -139,6 +144,9 @@ export type TAccountActionId =
|
|
|
139
144
|
| 'liftReserve'
|
|
140
145
|
| 'maxUsage'
|
|
141
146
|
| 'clearMaxUsage'
|
|
147
|
+
| 'spendLimit'
|
|
148
|
+
| 'clearSpendLimit'
|
|
149
|
+
| 'addApiKey'
|
|
142
150
|
| 'remove'
|
|
143
151
|
| 'usage'
|
|
144
152
|
| 'recheck'
|
|
@@ -188,6 +196,18 @@ export interface IAccountMeter {
|
|
|
188
196
|
resetText: string;
|
|
189
197
|
}
|
|
190
198
|
|
|
199
|
+
/**
|
|
200
|
+
* An API key's month as its page says it: `$12.40 of $50.00 this month · AGL traffic only · as of 14:02`, or `Spent
|
|
201
|
+
* $12.40 this month · no limit · as of 14:02`, and how loud: attention from `spendAttentionPercent` of the limit,
|
|
202
|
+
* critical once the key is held at it.
|
|
203
|
+
*/
|
|
204
|
+
export interface IAccountSpend {
|
|
205
|
+
text: string;
|
|
206
|
+
tone: TAccountsTone;
|
|
207
|
+
/** What stands in place of the meter of a key without a limit, which has nothing to meter; empty with one. */
|
|
208
|
+
unmeteredText: string;
|
|
209
|
+
}
|
|
210
|
+
|
|
191
211
|
/** An account's usage as its row shows it. */
|
|
192
212
|
export interface IAccountUsage {
|
|
193
213
|
/**
|
|
@@ -200,6 +220,8 @@ export interface IAccountUsage {
|
|
|
200
220
|
/** Why there is no current reading, when the service says. Empty otherwise. */
|
|
201
221
|
problemText: string;
|
|
202
222
|
meters: IAccountMeter[];
|
|
223
|
+
/** An API key's spend this month, under its meter; `null` for a sign-in. */
|
|
224
|
+
spend: IAccountSpend | null;
|
|
203
225
|
}
|
|
204
226
|
|
|
205
227
|
/** A device sign-in that has not finished, as the screen shows it. */
|
|
@@ -234,7 +256,9 @@ export interface IAccountStatus {
|
|
|
234
256
|
|
|
235
257
|
/**
|
|
236
258
|
* What a row's one status stands for, worst first: the service, a payment problem, a sign-in, a switch held on this
|
|
237
|
-
* host, a hand-over, a renewal retrying, the usage limit, the reserve, a lifted reserve, no sign-in, and ready.
|
|
259
|
+
* host, a hand-over, a renewal retrying, the usage limit, the reserve, a lifted reserve, no sign-in, and ready. For an
|
|
260
|
+
* API key, a payment problem includes its provider's spend cap, a sign-in is a key its provider rejected, and the usage
|
|
261
|
+
* limit is its monthly spend limit (or an answer that could not be priced); a key has no reserve.
|
|
238
262
|
*/
|
|
239
263
|
export type TAccountPillKind =
|
|
240
264
|
| 'service'
|
|
@@ -254,13 +278,19 @@ export interface IAccountPill extends IAccountStatus {
|
|
|
254
278
|
kind: TAccountPillKind;
|
|
255
279
|
}
|
|
256
280
|
|
|
257
|
-
/**
|
|
281
|
+
/**
|
|
282
|
+
* The tightest usage window of an account, as its list row's one meter. An API key's is its month: what it spent, in
|
|
283
|
+
* percent of its limit, or with no limit its spend alone, with nothing to meter (`percent` null).
|
|
284
|
+
*/
|
|
258
285
|
export interface IAccountPrimaryMeter {
|
|
259
286
|
label: string;
|
|
260
|
-
percent: number;
|
|
261
|
-
/** `96
|
|
287
|
+
percent: number | null;
|
|
288
|
+
/** `96%`; a key's spend, `$12.40`. */
|
|
262
289
|
valueText: string;
|
|
263
|
-
/**
|
|
290
|
+
/**
|
|
291
|
+
* Full is neutral; at the account's maximum or past 90% wants attention, a key's spend from `spendAttentionPercent` of
|
|
292
|
+
* its limit; anything below is fine.
|
|
293
|
+
*/
|
|
264
294
|
tone: TAccountsTone;
|
|
265
295
|
/** The account's maximum usage, drawn as a tick; `null` without one. */
|
|
266
296
|
threshold: number | null;
|
|
@@ -290,11 +320,11 @@ export interface IAccountLoginView {
|
|
|
290
320
|
causeText: string;
|
|
291
321
|
/** A new device sign-in for a login the service renews; absent for any other login. */
|
|
292
322
|
reauth: IAccountActionSlot | null;
|
|
293
|
-
/** Who renews the login: the accounts service, the tool that holds it, or nobody. */
|
|
323
|
+
/** Who renews the login: the accounts service, the tool that holds it, or nobody; empty for an API key, never renewed. */
|
|
294
324
|
renewedBy: string;
|
|
295
325
|
/**
|
|
296
326
|
* When the accounts service renews this login next. It is labelled as a renewal: read as "access
|
|
297
|
-
* expires" it sounds like the user's own sign-in is about to end, which it is not.
|
|
327
|
+
* expires" it sounds like the user's own sign-in is about to end, which it is not. Empty for an API key.
|
|
298
328
|
*/
|
|
299
329
|
renewalText: string;
|
|
300
330
|
/** When the service last looked at this login. */
|
|
@@ -317,8 +347,19 @@ export interface IAccountFact {
|
|
|
317
347
|
export interface IAccountRow {
|
|
318
348
|
key: string;
|
|
319
349
|
providerId: string;
|
|
350
|
+
/** The group the row is listed in (`IAccountGroup.id`). */
|
|
351
|
+
groupId: string;
|
|
352
|
+
/** An API key's account: a spend limit instead of a maximum usage, and nothing to sign in again. */
|
|
353
|
+
apiKey: boolean;
|
|
354
|
+
/** An API key's monthly spend limit in micro-dollars, or `null` for none and for every sign-in. */
|
|
355
|
+
spendLimitMicroUsd: number | null;
|
|
356
|
+
/** What a removal does that the owner must know, said with the removal; empty for a sign-in. */
|
|
357
|
+
removeNote: string;
|
|
320
358
|
label: string;
|
|
321
|
-
/**
|
|
359
|
+
/**
|
|
360
|
+
* Email and plan as one line, each named when the provider did not report it; for an API key its provider and how the
|
|
361
|
+
* key ends.
|
|
362
|
+
*/
|
|
322
363
|
identityText: string;
|
|
323
364
|
/** The account's email as the provider reported it; empty when it reported none. */
|
|
324
365
|
email: string;
|
|
@@ -347,8 +388,9 @@ export interface IAccountRow {
|
|
|
347
388
|
*/
|
|
348
389
|
limitStatus: IAccountStatus | null;
|
|
349
390
|
/**
|
|
350
|
-
* A payment problem the account is held with -- Anthropic refused a request on it with a billing error (HTTP 402)
|
|
351
|
-
*
|
|
391
|
+
* A payment problem the account is held with -- Anthropic refused a request on it with a billing error (HTTP 402), or
|
|
392
|
+
* a key's provider refused it for billing, its spend cap or a used-up credit -- as a badge, and what it means with when
|
|
393
|
+
* it was seen; `null` and `''` while none is held or its usage was not read.
|
|
352
394
|
*/
|
|
353
395
|
paymentStatus: IAccountStatus | null;
|
|
354
396
|
paymentDetail: string;
|
|
@@ -364,7 +406,10 @@ export interface IAccountRow {
|
|
|
364
406
|
operations: IAccountOperationView[];
|
|
365
407
|
/** Details: the service account's logins. */
|
|
366
408
|
logins: IAccountLoginView[];
|
|
367
|
-
/**
|
|
409
|
+
/**
|
|
410
|
+
* The page's own actions: renaming, the usage threshold (a key's spend limit), removing, reading usage again and
|
|
411
|
+
* re-checking a payment problem.
|
|
412
|
+
*/
|
|
368
413
|
manage: IAccountActionSlot[];
|
|
369
414
|
/** Details: reading Flex's models of the account again. */
|
|
370
415
|
flexActions: IAccountActionSlot[];
|
|
@@ -392,7 +437,7 @@ export interface IAccountsSummary {
|
|
|
392
437
|
/** Listed accounts that take work now, of all listed. */
|
|
393
438
|
ready: number;
|
|
394
439
|
total: number;
|
|
395
|
-
/** How the ready ones split by provider: `2 ChatGPT · 3 Claude`. */
|
|
440
|
+
/** How the ready ones split: by provider for sign-ins, the keys together: `2 ChatGPT · 3 Claude · 1 API key`. */
|
|
396
441
|
readyText: string;
|
|
397
442
|
/** Accounts their usage holds back: at a limit, or reserved at their maximum. */
|
|
398
443
|
limited: number;
|
|
@@ -412,6 +457,8 @@ export interface IAccountsView {
|
|
|
412
457
|
add: IAccountActionSlot;
|
|
413
458
|
/** Adding a Claude account: Claude Code signs in, in a terminal, to a home the accounts service made for it. */
|
|
414
459
|
addClaude: IAccountActionSlot;
|
|
460
|
+
/** Adding an OpenAI or Anthropic API key, which the accounts service checks with the provider first. */
|
|
461
|
+
addApiKey: IAccountActionSlot;
|
|
415
462
|
/**
|
|
416
463
|
* Adding the account Codex on this host is signed in to, offered until the accounts service manages Codex's
|
|
417
464
|
* sign-in on this host; `null` from then on.
|
|
@@ -624,10 +671,21 @@ const purposeLabel = (loginArg: TLogin): string => {
|
|
|
624
671
|
case 'claude_host_native': return 'Claude Code on this host';
|
|
625
672
|
case 'claude_container_setup': return 'Claude Code in containers';
|
|
626
673
|
case 'opencode_native': return 'OpenCode sign-in';
|
|
627
|
-
case 'anthropic_api_key': return 'Anthropic API key
|
|
674
|
+
case 'anthropic_api_key': return 'Anthropic API key';
|
|
675
|
+
case 'openai_api_key': return 'OpenAI API key';
|
|
628
676
|
}
|
|
629
677
|
};
|
|
630
678
|
|
|
679
|
+
/** Whether a login is an API key's: a key is never renewed and never signed in again, only replaced by another key. */
|
|
680
|
+
const isApiKeyLogin = (loginArg: TLogin): boolean => (
|
|
681
|
+
loginArg.purpose === 'anthropic_api_key' || loginArg.purpose === 'openai_api_key'
|
|
682
|
+
);
|
|
683
|
+
|
|
684
|
+
/** A key's provider by name, and where its keys are made and revoked. */
|
|
685
|
+
const keyProvider = (providerIdArg: string): { name: string; console: string } => providerIdArg === 'openai'
|
|
686
|
+
? { name: 'OpenAI', console: 'the OpenAI platform' }
|
|
687
|
+
: { name: 'Anthropic', console: 'the Claude Console' };
|
|
688
|
+
|
|
631
689
|
type TNativeTool = NonNullable<TLogin['ownerTool']>;
|
|
632
690
|
|
|
633
691
|
/**
|
|
@@ -661,7 +719,8 @@ const loginCauseText = (loginArg: TLogin): string => {
|
|
|
661
719
|
case 'none': return '';
|
|
662
720
|
case 'provider_unavailable': return 'The provider could not be reached.';
|
|
663
721
|
case 'exchange_uncertain': return 'A sign-in was interrupted and its outcome is unknown.';
|
|
664
|
-
case 'provider_rejected': return 'The provider rejected the
|
|
722
|
+
case 'provider_rejected': return isApiKeyLogin(loginArg) ? 'The provider rejected the key.'
|
|
723
|
+
: 'The provider rejected the saved sign-in.';
|
|
665
724
|
case 'native_owner': return 'Another tool on this host holds this login.';
|
|
666
725
|
}
|
|
667
726
|
};
|
|
@@ -685,6 +744,13 @@ const needsReauthCopy = (): ILoginStateCopy => ({
|
|
|
685
744
|
statusText: 'This login needs a new sign-in.',
|
|
686
745
|
});
|
|
687
746
|
|
|
747
|
+
/** A key its provider no longer accepts: a key is never renewed, so it needs as much as a sign-in, said as a key. */
|
|
748
|
+
const keyRejectedCopy = (): ILoginStateCopy => ({
|
|
749
|
+
state: 'needsReauth',
|
|
750
|
+
status: { text: 'Key rejected', tone: 'attention' },
|
|
751
|
+
statusText: 'The provider no longer accepts this key.',
|
|
752
|
+
});
|
|
753
|
+
|
|
688
754
|
/** A login a native tool renews: ownership, not health, and the screen says so. */
|
|
689
755
|
const nativeOwnedCopy = (toolArg: TNativeTool): ILoginStateCopy => ({
|
|
690
756
|
state: 'nativeOwned',
|
|
@@ -735,7 +801,7 @@ const daemonOwnedCopy = (loginArg: TLogin): ILoginStateCopy => {
|
|
|
735
801
|
*/
|
|
736
802
|
const loginStateCopy = (loginArg: TLogin): ILoginStateCopy => {
|
|
737
803
|
if (loginArg.health === 'handoff_quarantined') return quarantinedCopy();
|
|
738
|
-
if (loginArg.health === 'needs_reauth') return needsReauthCopy();
|
|
804
|
+
if (loginArg.health === 'needs_reauth') return isApiKeyLogin(loginArg) ? keyRejectedCopy() : needsReauthCopy();
|
|
739
805
|
if (loginArg.ownerTool !== null) return nativeOwnedCopy(loginArg.ownerTool);
|
|
740
806
|
return daemonOwnedCopy(loginArg);
|
|
741
807
|
};
|
|
@@ -775,6 +841,10 @@ const preuseReason = (loginArg: TLogin): string => {
|
|
|
775
841
|
*/
|
|
776
842
|
const signInWhere = (loginArg: TLogin): string => {
|
|
777
843
|
if (loginArg.ownerTool !== null) return `Sign in again in ${nativeToolName(loginArg.ownerTool)}.`;
|
|
844
|
+
if (isApiKeyLogin(loginArg)) {
|
|
845
|
+
return `${keyProvider(loginArg.providerId).name} no longer accepts this key. Add a key it accepts with Add account,`
|
|
846
|
+
+ ' then remove this one.';
|
|
847
|
+
}
|
|
778
848
|
if (loginArg.capabilities.canReauthenticate || claudeSignsInAgain(loginArg)) return '';
|
|
779
849
|
if (loginArg.providerId === 'anthropic') {
|
|
780
850
|
return 'A hand-over of this sign-in could not be settled; This host shows what holds it.';
|
|
@@ -785,11 +855,11 @@ const signInWhere = (loginArg: TLogin): string => {
|
|
|
785
855
|
|
|
786
856
|
/**
|
|
787
857
|
* Whether a Claude sign-in of the account replaces this login: the service takes up a login it renews once that login
|
|
788
|
-
* has ended, and a Claude sign-in of the same account lands on the same login.
|
|
858
|
+
* has ended, and a Claude sign-in of the same account lands on the same login. An Anthropic API key is no sign-in.
|
|
789
859
|
*/
|
|
790
860
|
const claudeSignsInAgain = (loginArg: TLogin): boolean => (
|
|
791
|
-
loginArg.providerId === 'anthropic' && (loginArg
|
|
792
|
-
&& loginArg.health === 'needs_reauth'
|
|
861
|
+
loginArg.providerId === 'anthropic' && !isApiKeyLogin(loginArg)
|
|
862
|
+
&& (loginArg.owner === 'daemon' || loginArg.owner === 'none') && loginArg.health === 'needs_reauth'
|
|
793
863
|
);
|
|
794
864
|
|
|
795
865
|
/** A new device sign-in for a login the service renews, or `null` where the service offers none. */
|
|
@@ -832,8 +902,8 @@ const loginView = (
|
|
|
832
902
|
: copy.statusText,
|
|
833
903
|
causeText: loginCauseText(loginArg),
|
|
834
904
|
reauth: reauthSlot(loginArg, serviceArg),
|
|
835
|
-
renewedBy: renewedByLabel(loginArg),
|
|
836
|
-
renewalText: clockTime(loginArg.accessExpiresAt) || 'Not reported',
|
|
905
|
+
renewedBy: isApiKeyLogin(loginArg) ? '' : renewedByLabel(loginArg),
|
|
906
|
+
renewalText: isApiKeyLogin(loginArg) ? '' : clockTime(loginArg.accessExpiresAt) || 'Not reported',
|
|
837
907
|
observedText: clockTime(loginArg.statusObservedAt) || 'Not reported',
|
|
838
908
|
preuse: !loginArg.capabilities.canPreuse && loginArg.purpose !== 'openai_managed'
|
|
839
909
|
? null
|
|
@@ -850,8 +920,23 @@ const providerLabel = (providerIdArg: string): string => {
|
|
|
850
920
|
}
|
|
851
921
|
};
|
|
852
922
|
|
|
853
|
-
/** The fixed order of the
|
|
854
|
-
const
|
|
923
|
+
/** The fixed order of the groups: sign-ins, then API keys; any other provider follows under its own id. */
|
|
924
|
+
const groupOrder = ['openai', 'anthropic', 'openai-api-key', 'anthropic-api-key'];
|
|
925
|
+
|
|
926
|
+
/** A group's heading. */
|
|
927
|
+
const groupLabel = (groupIdArg: string): string => {
|
|
928
|
+
switch (groupIdArg) {
|
|
929
|
+
case 'openai-api-key': return 'OpenAI API keys';
|
|
930
|
+
case 'anthropic-api-key': return 'Anthropic API keys';
|
|
931
|
+
default: return providerLabel(groupIdArg);
|
|
932
|
+
}
|
|
933
|
+
};
|
|
934
|
+
|
|
935
|
+
/**
|
|
936
|
+
* Whether an account is an API key's: the accounts service names `api-key` as the tenancy of every key account, whose
|
|
937
|
+
* key names no organization (`IAuthSwitchAccount.organizationId`).
|
|
938
|
+
*/
|
|
939
|
+
const isApiKeyAccount = (accountArg: TAccount): boolean => accountArg.organizationId === 'api-key';
|
|
855
940
|
|
|
856
941
|
const harnessLabel = (harnessArg: TAccountHarnessId): string => {
|
|
857
942
|
switch (harnessArg) {
|
|
@@ -1036,7 +1121,8 @@ const codexSlot = (
|
|
|
1036
1121
|
projectionArg: interfaces.IControllerAuthorityProjection,
|
|
1037
1122
|
serviceArg: IAccountsServiceStatus,
|
|
1038
1123
|
): IAccountActionSlot | null => {
|
|
1039
|
-
|
|
1124
|
+
// An OpenAI API key is no ChatGPT sign-in: Codex on this host never switches to one.
|
|
1125
|
+
if (accountArg.providerId !== 'openai' || accountArg.removed || isApiKeyAccount(accountArg)) return null;
|
|
1040
1126
|
const host = interfaces.findHostCodexAssignment(projectionArg);
|
|
1041
1127
|
if (host?.accountId === accountArg.id) return null;
|
|
1042
1128
|
const reason = interfaces.findAuthorityDaemonLogin(projectionArg, accountArg.id) === 'no_login'
|
|
@@ -1077,7 +1163,10 @@ const codexReleaseSlot = (
|
|
|
1077
1163
|
const claudeSignInStatusText = (signInArg: interfaces.TControllerClaudeSignIn): string => (
|
|
1078
1164
|
signInArg.state === 'committing'
|
|
1079
1165
|
? 'Adding the account…'
|
|
1080
|
-
:
|
|
1166
|
+
: signInArg.heldBy
|
|
1167
|
+
? 'Claude Code signed in. The account is added once these Claude Code processes have ended: '
|
|
1168
|
+
+ `${plugins.authswitchAuthority.authSwitchClaudeSignInHoldersText(signInArg.heldBy)}.`
|
|
1169
|
+
: 'Waiting for Claude Code to sign in. Run the command in a terminal and follow Claude Code there.'
|
|
1081
1170
|
);
|
|
1082
1171
|
|
|
1083
1172
|
/** One Claude sign-in in progress, with the command that completes it and a terminal to run it in. */
|
|
@@ -1148,10 +1237,13 @@ interface IPillParts {
|
|
|
1148
1237
|
service: IAccountsServiceStatus;
|
|
1149
1238
|
status: IAccountStatus;
|
|
1150
1239
|
worst: TAccountLoginState | null;
|
|
1151
|
-
payment:
|
|
1240
|
+
/** The payment problem's badge while one is held: `Payment problem`, or a key's `Spend cap reached`. */
|
|
1241
|
+
payment: IAccountStatus | null;
|
|
1152
1242
|
/** A switch of a tool on this host that runs on the account could not be settled. */
|
|
1153
1243
|
held: boolean;
|
|
1154
1244
|
limit: plugins.authswitchUsageLimit.IAuthSwitchUsageLimit | null;
|
|
1245
|
+
/** What the pill calls the limit while the account is at it: `At limit`, a key's `At spend limit` or `Unpriced answer`. */
|
|
1246
|
+
limitText: string;
|
|
1155
1247
|
/** Until when the reserve is lifted, while it is. */
|
|
1156
1248
|
liftedUntil: string | null;
|
|
1157
1249
|
}
|
|
@@ -1169,14 +1261,14 @@ const accountPill = (partsArg: IPillParts): IAccountPill => {
|
|
|
1169
1261
|
: textArg;
|
|
1170
1262
|
if (partsArg.removed) return pill('removed', 'Removed', 'neutral');
|
|
1171
1263
|
if (partsArg.service.state !== 'current') return pill('service', partsArg.status.text, partsArg.status.tone);
|
|
1172
|
-
if (partsArg.payment) return pill('payment',
|
|
1264
|
+
if (partsArg.payment) return pill('payment', partsArg.payment.text, 'critical');
|
|
1173
1265
|
if (partsArg.worst === 'quarantined' || partsArg.worst === 'needsReauth') {
|
|
1174
1266
|
return pill('signIn', partsArg.status.text, partsArg.status.tone);
|
|
1175
1267
|
}
|
|
1176
1268
|
if (partsArg.held) return pill('held', 'Switch held', 'critical');
|
|
1177
1269
|
if (partsArg.worst === 'pendingHandoff') return pill('handover', partsArg.status.text, partsArg.status.tone);
|
|
1178
1270
|
if (partsArg.worst === 'retryWait') return pill('renewing', partsArg.status.text, partsArg.status.tone);
|
|
1179
|
-
if (partsArg.limit?.state === 'exhausted') return pill('atLimit', back(
|
|
1271
|
+
if (partsArg.limit?.state === 'exhausted') return pill('atLimit', back(partsArg.limitText), 'neutral');
|
|
1180
1272
|
if (partsArg.limit?.state === 'reserved') return pill('reserved', back('Reserved'), 'neutral');
|
|
1181
1273
|
if (partsArg.liftedUntil) {
|
|
1182
1274
|
return pill('lifted', `Reserve lifted · until ${plugins.authswitchPreuseText.localDateTime(partsArg.liftedUntil)}`, 'info');
|
|
@@ -1202,10 +1294,41 @@ const primaryMeter = (usageArg: IAccountUsage | null, thresholdArg: number | nul
|
|
|
1202
1294
|
};
|
|
1203
1295
|
};
|
|
1204
1296
|
|
|
1205
|
-
/**
|
|
1297
|
+
/**
|
|
1298
|
+
* The share of a key's spend limit from which its spend wants attention: a display tone only, judged on the last reading
|
|
1299
|
+
* the screen holds, never a limit the accounts service stores.
|
|
1300
|
+
*/
|
|
1301
|
+
export const spendAttentionPercent = 80;
|
|
1302
|
+
|
|
1303
|
+
/**
|
|
1304
|
+
* A key's month as the list row's meter: what it spent, in percent of its limit, which is neutral once full like a used
|
|
1305
|
+
* window and wants attention from `spendAttentionPercent`; without a limit its spend alone.
|
|
1306
|
+
*/
|
|
1307
|
+
const spendMeter = (spendArg: interfaces.TControllerAccountSpendReading): IAccountPrimaryMeter => {
|
|
1308
|
+
const spent = plugins.authswitchSpend.authSwitchUsdText(spendArg.spentMicroUsd);
|
|
1309
|
+
const percent = plugins.authswitchSpend.authSwitchSpendPercent(spendArg);
|
|
1310
|
+
if (percent === null) return { label: 'This month', percent: null, valueText: spent, tone: 'neutral', threshold: null };
|
|
1311
|
+
return {
|
|
1312
|
+
label: plugins.authswitchUsageWindows.authSwitchSpendWindowLabel,
|
|
1313
|
+
percent: clampPercent(percent),
|
|
1314
|
+
valueText: spent,
|
|
1315
|
+
tone: percent >= 100 ? 'neutral' : percent >= spendAttentionPercent ? 'attention' : 'positive',
|
|
1316
|
+
threshold: null,
|
|
1317
|
+
};
|
|
1318
|
+
};
|
|
1319
|
+
|
|
1320
|
+
/** An API key as it is named beside its name: its provider and how the key ends, when the service knows that. */
|
|
1321
|
+
const keyIdentityText = (accountArg: TAccount): string => {
|
|
1322
|
+
const provider = keyProvider(accountArg.providerId).name;
|
|
1323
|
+
return accountArg.keyHint === null ? `${provider} API key` : `${provider} API key ending ${accountArg.keyHint}`;
|
|
1324
|
+
};
|
|
1325
|
+
|
|
1326
|
+
/** The list row's second line: who the account is -- email and plan, or a key's identity -- and who uses it. */
|
|
1206
1327
|
const listMeta = (accountArg: TAccount, chipsArg: readonly IAccountHarnessChip[]): string => [
|
|
1207
|
-
accountArg
|
|
1208
|
-
|
|
1328
|
+
...isApiKeyAccount(accountArg) ? [keyIdentityText(accountArg)] : [
|
|
1329
|
+
accountArg.email && accountArg.email !== accountArg.label ? accountArg.email : '',
|
|
1330
|
+
accountArg.plan ? `${accountArg.plan} plan` : '',
|
|
1331
|
+
],
|
|
1209
1332
|
chipsArg.filter((chipArg) => chipArg.unavailableReason === undefined)
|
|
1210
1333
|
.map((chipArg) => harnessLabel(chipArg.harnessId)).join(', '),
|
|
1211
1334
|
].filter(Boolean).join(' · ');
|
|
@@ -1246,21 +1369,96 @@ const usageLimit = (
|
|
|
1246
1369
|
return limit.state === 'available' ? null : limit;
|
|
1247
1370
|
};
|
|
1248
1371
|
|
|
1249
|
-
|
|
1372
|
+
/** Whether a key's month holds it for an answer that could not be priced rather than for what it spent. */
|
|
1373
|
+
const heldUnpriced = (spendArg: interfaces.TControllerAccountSpendReading): boolean => (
|
|
1374
|
+
spendArg.limitMicroUsd !== null && spendArg.spentMicroUsd < spendArg.limitMicroUsd
|
|
1375
|
+
);
|
|
1376
|
+
|
|
1377
|
+
/**
|
|
1378
|
+
* The limit as the page's badge; a key's (`spendArg`, its month) is its spend limit, or an answer that could not be
|
|
1379
|
+
* priced.
|
|
1380
|
+
*/
|
|
1381
|
+
const limitStatus = (
|
|
1382
|
+
limit: plugins.authswitchUsageLimit.IAuthSwitchUsageLimit | null,
|
|
1383
|
+
spendArg: interfaces.TControllerAccountSpendReading | null,
|
|
1384
|
+
): IAccountStatus | null => {
|
|
1250
1385
|
if (!limit) return null;
|
|
1251
1386
|
const reset = limit.resetAt ? `, resets ${plugins.authswitchPreuseText.localDateTime(limit.resetAt)}` : '';
|
|
1387
|
+
if (spendArg) {
|
|
1388
|
+
return { text: heldUnpriced(spendArg) ? `An answer could not be priced, so the key is held${reset}`
|
|
1389
|
+
: `At its spend limit${reset}`, tone: 'critical' };
|
|
1390
|
+
}
|
|
1252
1391
|
return { text: `${sentence(plugins.authswitchUsageLimit.authSwitchUsageLimitText(limit))}${reset}`,
|
|
1253
1392
|
tone: limit.state === 'reserved' ? 'attention' : 'critical' };
|
|
1254
1393
|
};
|
|
1255
1394
|
|
|
1256
|
-
/**
|
|
1257
|
-
|
|
1395
|
+
/**
|
|
1396
|
+
* The payment problem a usage reading holds the account with, as a badge and what it means; `null` without one, and
|
|
1397
|
+
* once a key's spend cap reset (`authSwitchPaymentProblemHeld`). A key's says its cause.
|
|
1398
|
+
*/
|
|
1399
|
+
const paymentView = (accountArg: TAccount, usageArg: TUsage | null, nowArg: number): {
|
|
1400
|
+
status: IAccountStatus; detail: string;
|
|
1401
|
+
} | null => {
|
|
1258
1402
|
const problem = usageArg?.paymentProblem;
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1403
|
+
if (!problem || !plugins.authswitchSubscription.authSwitchPaymentProblemHeld(problem, nowArg)) return null;
|
|
1404
|
+
const seen = `Seen ${plugins.authswitchPreuseText.localDateTime(problem.at)}.`;
|
|
1405
|
+
if (!isApiKeyAccount(accountArg)) {
|
|
1406
|
+
return { status: { text: 'Payment problem', tone: 'critical' },
|
|
1407
|
+
detail: `${plugins.authswitchSubscription.authSwitchPaymentProblemText} ${seen}` };
|
|
1408
|
+
}
|
|
1409
|
+
const resets = problem.until ? ` The cap resets ${plugins.authswitchPreuseText.localDateTime(problem.until)}.` : '';
|
|
1410
|
+
return { status: { text: problem.cause === 'spend_cap' ? 'Spend cap reached' : 'Payment problem', tone: 'critical' },
|
|
1411
|
+
detail: `${plugins.authswitchSubscription.authSwitchApiKeyPaymentProblemText(problem, accountArg.providerId)} ${seen}`
|
|
1412
|
+
+ resets };
|
|
1413
|
+
};
|
|
1414
|
+
|
|
1415
|
+
/** A key's spend limit as its field shows it, in dollars and cents: `50.00`. */
|
|
1416
|
+
export const spendLimitDraft = (spendLimitMicroUsdArg: number): string => (
|
|
1417
|
+
(spendLimitMicroUsdArg / plugins.authswitchSpend.authSwitchMicroUsdPerUsd).toFixed(2)
|
|
1418
|
+
);
|
|
1419
|
+
|
|
1420
|
+
/**
|
|
1421
|
+
* Why a spend limit draft, in dollars, cannot be saved yet, or `''` when it can: an amount to the cent the service's own
|
|
1422
|
+
* rule allows, other than the limit the key has.
|
|
1423
|
+
*/
|
|
1424
|
+
export const spendLimitReason = (draftArg: string, currentArg: number | null): string => {
|
|
1425
|
+
if (draftArg.trim().length === 0) return 'Enter an amount in dollars.';
|
|
1426
|
+
const value = plugins.authswitchSpend.authSwitchParseUsd(draftArg);
|
|
1427
|
+
if (value === null) return 'Enter dollars and cents, such as 50 or 12.50.';
|
|
1428
|
+
if (!interfaces.isControllerSpendLimitMicroUsd(value)) {
|
|
1429
|
+
return `Enter an amount from ${plugins.authswitchSpend.authSwitchUsdText(interfaces.controllerSpendLimitRange.min)} to`
|
|
1430
|
+
+ ` ${plugins.authswitchSpend.authSwitchUsdText(interfaces.controllerSpendLimitRange.max)}.`;
|
|
1431
|
+
}
|
|
1432
|
+
return value === currentArg ? 'The spend limit is unchanged.' : '';
|
|
1433
|
+
};
|
|
1434
|
+
|
|
1435
|
+
/** A key's spend limit in words, as its page and the screen after an added key state it. */
|
|
1436
|
+
export const spendLimitText = (spendLimitMicroUsdArg: number | null): string => spendLimitMicroUsdArg === null
|
|
1437
|
+
? 'No monthly limit'
|
|
1438
|
+
: `${plugins.authswitchSpend.authSwitchUsdText(spendLimitMicroUsdArg)} per calendar month (UTC)`;
|
|
1439
|
+
|
|
1440
|
+
/**
|
|
1441
|
+
* Why the Add API key dialog cannot add yet, or `''` when it can: a key typed, an optional name the service's label rule
|
|
1442
|
+
* accepts, and an optional spend limit in dollars its spend limit rule accepts.
|
|
1443
|
+
*/
|
|
1444
|
+
export const apiKeyReason = (keyTypedArg: boolean, labelArg: string, limitArg: string): string => {
|
|
1445
|
+
if (!keyTypedArg) return 'Paste the key.';
|
|
1446
|
+
if (labelArg.trim().length > 0) {
|
|
1447
|
+
const name = renameReason(labelArg, '');
|
|
1448
|
+
if (name) return name;
|
|
1449
|
+
}
|
|
1450
|
+
return limitArg.trim().length > 0 ? spendLimitReason(limitArg, null) : '';
|
|
1262
1451
|
};
|
|
1263
1452
|
|
|
1453
|
+
/** What the screen says once a key is added: the account, how its key ends, and the spend limit it actually has. */
|
|
1454
|
+
export const apiKeyAddedNotice = (accountArg: TAccount): IAccountsNotice => ({
|
|
1455
|
+
tone: 'positive',
|
|
1456
|
+
// A key added again keeps the limit it had unless the add named one, so this is the account's limit, not the request's.
|
|
1457
|
+
text: `Added ${accountArg.label}${accountArg.keyHint ? `, the key ending ${accountArg.keyHint}` : ''}. `
|
|
1458
|
+
+ (accountArg.spendLimitMicroUsd === null ? 'It has no monthly spend limit.'
|
|
1459
|
+
: `Its monthly spend limit is ${spendLimitText(accountArg.spendLimitMicroUsd)}.`),
|
|
1460
|
+
});
|
|
1461
|
+
|
|
1264
1462
|
/**
|
|
1265
1463
|
* Why a usage threshold draft cannot be saved yet, or `''` when it can: a whole percent the service's own rule
|
|
1266
1464
|
* allows, other than the threshold the account has.
|
|
@@ -1315,11 +1513,17 @@ const serviceRow = (
|
|
|
1315
1513
|
));
|
|
1316
1514
|
const usageLogin = accountArg.removed ? undefined : logins.find((loginArg) => loginArg.health !== 'removed');
|
|
1317
1515
|
const usageRead = contextArg.usage.get(accountArg.id);
|
|
1318
|
-
const usage = usageRead?.state === 'read' ? usageView(usageRead.usage, usageLogin?.owner ?? null) : null;
|
|
1516
|
+
const usage = usageRead?.state === 'read' ? usageView(usageRead.usage, usageLogin?.owner ?? null, contextArg.now) : null;
|
|
1319
1517
|
const rawLimit = accountArg.removed || usageRead?.state !== 'read' ? null
|
|
1320
1518
|
: usageLimit(accountArg, usageRead.usage, contextArg.now);
|
|
1321
|
-
|
|
1322
|
-
const
|
|
1519
|
+
// A key's month, while the read holds one: its limit, its meter and how the pill names the limit are its spend's.
|
|
1520
|
+
const spend = usageRead?.state === 'read' && usageRead.usage.reading?.kind === 'spend' ? usageRead.usage.reading.spend : null;
|
|
1521
|
+
const limit = limitStatus(rawLimit, spend);
|
|
1522
|
+
const payment = accountArg.removed || usageRead?.state !== 'read' ? null
|
|
1523
|
+
: paymentView(accountArg, usageRead.usage, contextArg.now);
|
|
1524
|
+
const apiKey = isApiKeyAccount(accountArg);
|
|
1525
|
+
const provider = keyProvider(accountArg.providerId);
|
|
1526
|
+
const identity = apiKey ? keyIdentityText(accountArg) : identityText(accountArg.email, accountArg.plan);
|
|
1323
1527
|
const liftedUntil = !accountArg.removed && accountArg.maxUsagePercent !== null
|
|
1324
1528
|
&& plugins.authswitchUsageLimit.authSwitchReserveLiftActive(accountArg.reserveLiftUntil, contextArg.now)
|
|
1325
1529
|
? accountArg.reserveLiftUntil! : null;
|
|
@@ -1329,8 +1533,14 @@ const serviceRow = (
|
|
|
1329
1533
|
return {
|
|
1330
1534
|
key: `service:${accountArg.id}`,
|
|
1331
1535
|
providerId: accountArg.providerId,
|
|
1536
|
+
groupId: apiKey ? `${accountArg.providerId}-api-key` : accountArg.providerId,
|
|
1537
|
+
apiKey,
|
|
1538
|
+
spendLimitMicroUsd: apiKey ? accountArg.spendLimitMicroUsd : null,
|
|
1539
|
+
removeNote: apiKey
|
|
1540
|
+
? `AGL stops using the key. It stays valid at ${provider.name} until you revoke it in ${provider.console}.`
|
|
1541
|
+
: '',
|
|
1332
1542
|
label: accountArg.label,
|
|
1333
|
-
identityText:
|
|
1543
|
+
identityText: identity,
|
|
1334
1544
|
email: accountArg.email ?? '',
|
|
1335
1545
|
removed: accountArg.removed,
|
|
1336
1546
|
status,
|
|
@@ -1340,13 +1550,14 @@ const serviceRow = (
|
|
|
1340
1550
|
service: contextArg.service,
|
|
1341
1551
|
status,
|
|
1342
1552
|
worst: worstLoginState(liveLogins),
|
|
1343
|
-
payment: payment
|
|
1553
|
+
payment: payment?.status ?? null,
|
|
1344
1554
|
held: [...uses.values()].some((useArg) => useArg.host === 'quarantined'),
|
|
1345
1555
|
limit: rawLimit,
|
|
1556
|
+
limitText: !spend ? 'At limit' : heldUnpriced(spend) ? 'Unpriced answer' : 'At spend limit',
|
|
1346
1557
|
liftedUntil,
|
|
1347
1558
|
}),
|
|
1348
|
-
listMeta: accountArg.removed ?
|
|
1349
|
-
primaryMeter: primaryMeter(usage, accountArg.maxUsagePercent),
|
|
1559
|
+
listMeta: accountArg.removed ? identity : listMeta(accountArg, harnesses),
|
|
1560
|
+
primaryMeter: spend ? spendMeter(spend) : primaryMeter(usage, accountArg.maxUsagePercent),
|
|
1350
1561
|
backAt: rawLimit?.resetAt ?? null,
|
|
1351
1562
|
harnesses,
|
|
1352
1563
|
usageLoginId: usageLogin?.id ?? null,
|
|
@@ -1373,8 +1584,10 @@ const serviceRow = (
|
|
|
1373
1584
|
logins: logins.map((loginArg) => loginView(loginArg, contextArg.service)),
|
|
1374
1585
|
manage: [
|
|
1375
1586
|
serviceSlot('rename', 'Rename', 'lucide:PenLine', contextArg.service, removed),
|
|
1376
|
-
|
|
1377
|
-
...(
|
|
1587
|
+
// A key's limit is its spend; a usage threshold and its reserve are a sign-in's.
|
|
1588
|
+
...(apiKey ? [serviceSlot('spendLimit', 'Spend limit', 'lucide:Wallet', contextArg.service, removed)]
|
|
1589
|
+
: [serviceSlot('maxUsage', 'Max usage', 'lucide:SlidersHorizontal', contextArg.service, removed)]),
|
|
1590
|
+
...(!apiKey && accountArg.maxUsagePercent !== null && accountArg.maxUsagePercent < 100
|
|
1378
1591
|
? [serviceSlot('liftReserve', 'Lift reserve', 'lucide:Clock', contextArg.service, removed)] : []),
|
|
1379
1592
|
serviceSlot('remove', 'Remove', 'lucide:Trash2', contextArg.service, removed),
|
|
1380
1593
|
...(usageLogin ? [serviceSlot('usage', 'Show latest usage', 'lucide:Gauge', contextArg.service)] : []),
|
|
@@ -1386,11 +1599,12 @@ const serviceRow = (
|
|
|
1386
1599
|
: [],
|
|
1387
1600
|
flexStatusText: flexConnectionId ? contextArg.flex.refreshStatuses.get(flexConnectionId) ?? '' : '',
|
|
1388
1601
|
facts: [
|
|
1389
|
-
{ label: 'Provider', value: providerLabel(accountArg.providerId) },
|
|
1602
|
+
{ label: 'Provider', value: apiKey ? `${provider.name} (API key)` : providerLabel(accountArg.providerId) },
|
|
1390
1603
|
...(usageLogin ? [{ label: 'Usage', value: usageFact(usageRead, usage) }] : []),
|
|
1391
1604
|
...(usageRead?.state === 'read'
|
|
1392
1605
|
? plugins.authswitchSubscription.authSwitchSubscriptionFacts(usageRead.usage.reading) : []),
|
|
1393
|
-
{ label: '
|
|
1606
|
+
apiKey ? { label: 'Monthly spend limit', value: spendLimitText(accountArg.spendLimitMicroUsd) }
|
|
1607
|
+
: { label: 'Max usage', value: maxUsageFact(accountArg.maxUsagePercent) },
|
|
1394
1608
|
{ label: 'Account reference', value: accountArg.id },
|
|
1395
1609
|
],
|
|
1396
1610
|
accountId: accountArg.id,
|
|
@@ -1402,23 +1616,22 @@ const serviceRow = (
|
|
|
1402
1616
|
};
|
|
1403
1617
|
};
|
|
1404
1618
|
|
|
1405
|
-
/** The listed rows, one group per
|
|
1619
|
+
/** The listed rows, one group per kind: ChatGPT, Claude, OpenAI API keys, Anthropic API keys. */
|
|
1406
1620
|
const accountGroups = (rowsArg: readonly IAccountRow[]): IAccountGroup[] => {
|
|
1407
|
-
const
|
|
1408
|
-
const left =
|
|
1409
|
-
const right =
|
|
1410
|
-
if (left !== right) return (left < 0 ?
|
|
1621
|
+
const groups = [...new Set(rowsArg.map((rowArg) => rowArg.groupId))].sort((leftArg, rightArg) => {
|
|
1622
|
+
const left = groupOrder.indexOf(leftArg);
|
|
1623
|
+
const right = groupOrder.indexOf(rightArg);
|
|
1624
|
+
if (left !== right) return (left < 0 ? groupOrder.length : left) - (right < 0 ? groupOrder.length : right);
|
|
1411
1625
|
return leftArg.localeCompare(rightArg);
|
|
1412
1626
|
});
|
|
1413
|
-
return
|
|
1414
|
-
|
|
1415
|
-
label:
|
|
1416
|
-
|
|
1417
|
-
}));
|
|
1627
|
+
return groups.map((groupIdArg) => {
|
|
1628
|
+
const rows = rowsArg.filter((rowArg) => rowArg.groupId === groupIdArg);
|
|
1629
|
+
return { id: groupIdArg, providerId: rows[0]!.providerId, label: groupLabel(groupIdArg), rows };
|
|
1630
|
+
});
|
|
1418
1631
|
};
|
|
1419
1632
|
|
|
1420
1633
|
/**
|
|
1421
|
-
* The account list: the service's accounts, under their provider.
|
|
1634
|
+
* The account list: the service's accounts, under their provider, the API keys apart from the sign-ins.
|
|
1422
1635
|
*
|
|
1423
1636
|
* The rows and the sentence under them come from the same read. The controller reads every page of
|
|
1424
1637
|
* the service's snapshot, so a list that still does not end at its last row says so explicitly
|
|
@@ -1459,12 +1672,17 @@ const accountsSummary = (serviceArg: IAccountsServiceStatus, listArg: IAccountsL
|
|
|
1459
1672
|
const needsYou = rows.filter((rowArg) => needsYouPills.includes(rowArg.pill.kind));
|
|
1460
1673
|
const firstBack = limited.map((rowArg) => rowArg.backAt).filter((atArg): atArg is string => atArg !== null)
|
|
1461
1674
|
.sort((leftArg, rightArg) => Date.parse(leftArg) - Date.parse(rightArg))[0];
|
|
1675
|
+
const keys = rows.filter((rowArg) => rowArg.apiKey);
|
|
1676
|
+
const readyKeys = keys.filter((rowArg) => readyPills.includes(rowArg.pill.kind)).length;
|
|
1462
1677
|
return {
|
|
1463
1678
|
ready: ready.length,
|
|
1464
1679
|
total: rows.length,
|
|
1465
|
-
readyText:
|
|
1466
|
-
|
|
1467
|
-
|
|
1680
|
+
readyText: [
|
|
1681
|
+
...listArg.groups.filter((groupArg) => !groupArg.rows[0]!.apiKey).map((groupArg) => (
|
|
1682
|
+
`${groupArg.rows.filter((rowArg) => readyPills.includes(rowArg.pill.kind)).length} ${groupArg.label}`
|
|
1683
|
+
)),
|
|
1684
|
+
...keys.length ? [`${readyKeys} API ${readyKeys === 1 ? 'key' : 'keys'}`] : [],
|
|
1685
|
+
].join(' · '),
|
|
1468
1686
|
limited: limited.length,
|
|
1469
1687
|
limitedText: firstBack ? `First back ${plugins.authswitchPreuseText.localDateTime(firstBack)}` : '',
|
|
1470
1688
|
needsYou: needsYou.length,
|
|
@@ -1472,10 +1690,13 @@ const accountsSummary = (serviceArg: IAccountsServiceStatus, listArg: IAccountsL
|
|
|
1472
1690
|
};
|
|
1473
1691
|
};
|
|
1474
1692
|
|
|
1475
|
-
/**
|
|
1476
|
-
|
|
1693
|
+
/**
|
|
1694
|
+
* The accounts whose usage a screen reads: each listed account's usage login, by account id. `apiKey` marks a key, whose
|
|
1695
|
+
* spend changes with every answer AGL relays on it, so the screen reads it again on every change it is told of.
|
|
1696
|
+
*/
|
|
1697
|
+
export const usageTargets = (viewArg: IAccountsView): { accountId: string; loginId: string; apiKey: boolean }[] => (
|
|
1477
1698
|
viewArg.list.groups.flatMap((groupArg) => groupArg.rows).flatMap((rowArg) => (
|
|
1478
|
-
rowArg.usageLoginId ? [{ accountId: rowArg.accountId, loginId: rowArg.usageLoginId }] : []
|
|
1699
|
+
rowArg.usageLoginId ? [{ accountId: rowArg.accountId, loginId: rowArg.usageLoginId, apiKey: rowArg.apiKey }] : []
|
|
1479
1700
|
))
|
|
1480
1701
|
);
|
|
1481
1702
|
|
|
@@ -1512,6 +1733,7 @@ export const accountsView = (
|
|
|
1512
1733
|
'addClaude', 'Add Claude account', 'lucide:UserPlus', service,
|
|
1513
1734
|
claudeSignIns.length ? 'A Claude sign-in is already waiting below.' : '',
|
|
1514
1735
|
),
|
|
1736
|
+
addApiKey: serviceSlot('addApiKey', 'Add API key', 'lucide:KeyRound', service),
|
|
1515
1737
|
adoptCodex: projection && interfaces.findHostCodexAssignment(projection)
|
|
1516
1738
|
? null
|
|
1517
1739
|
: serviceSlot('adoptCodex', 'Add Codex\'s current sign-in', 'lucide:LogIn', service),
|
|
@@ -1752,13 +1974,14 @@ const asOfTime = (isoArg: string | null): string => {
|
|
|
1752
1974
|
* `ownerArg` is who renews the login it was read for, which names why the usage of Claude Code's own sign-in is
|
|
1753
1975
|
* missing.
|
|
1754
1976
|
*/
|
|
1755
|
-
export const usageView = (usageArg: TUsage, ownerArg: TLogin['owner'] | null = null
|
|
1977
|
+
export const usageView = (usageArg: TUsage, ownerArg: TLogin['owner'] | null = null,
|
|
1978
|
+
nowArg: number = Date.now()): IAccountUsage => {
|
|
1756
1979
|
const observed = asOfTime(usageArg.observedAt);
|
|
1757
1980
|
const reading = usageArg.reading;
|
|
1758
1981
|
const current = usageArg.state === 'current';
|
|
1759
1982
|
if (ownerArg === 'claude_native' && usageArg.problem === 'access_unavailable') {
|
|
1760
1983
|
return { freshness: usageArg.state === 'stale' && observed ? `Last known, as of ${observed}` : 'Unavailable',
|
|
1761
|
-
current, problemText: claudeHomeUsageText, meters: usageMeters(reading) };
|
|
1984
|
+
current, problemText: claudeHomeUsageText, meters: usageMeters(reading), spend: null };
|
|
1762
1985
|
}
|
|
1763
1986
|
const next = asOfTime(usageArg.nextAttemptAt);
|
|
1764
1987
|
return {
|
|
@@ -1770,6 +1993,38 @@ export const usageView = (usageArg: TUsage, ownerArg: TLogin['owner'] | null = n
|
|
|
1770
1993
|
? `The provider limited usage reads, so the last one was refused${next ? `; the next read is at ${next}` : ''}.`
|
|
1771
1994
|
: plugins.authswitchUsageWindows.authSwitchUsageProblemText(usageArg.problem),
|
|
1772
1995
|
meters: usageMeters(reading),
|
|
1996
|
+
spend: reading?.kind === 'spend' ? spendView(reading.spend, nowArg, observed) : null,
|
|
1997
|
+
};
|
|
1998
|
+
};
|
|
1999
|
+
|
|
2000
|
+
/**
|
|
2001
|
+
* An API key's month in words: what AGL's traffic on it cost against its limit, or without one, how many answers could
|
|
2002
|
+
* not be priced, and when the accounts service read the key's ledger for it (`asOfArg`, the reading's `observedAt`): the
|
|
2003
|
+
* screen reads the month again on Refresh, Show latest usage and each change the controller announces, never on a
|
|
2004
|
+
* timer, so the figure says its time. Attention from `spendAttentionPercent` of the limit, critical once the key is
|
|
2005
|
+
* held, as of that reading. A reading whose month has ended says nothing of this one (`authSwitchSpendMonthEnded`).
|
|
2006
|
+
*/
|
|
2007
|
+
const spendView = (spendArg: interfaces.TControllerAccountSpendReading, nowArg: number, asOfArg: string): IAccountSpend => {
|
|
2008
|
+
const usd = plugins.authswitchSpend.authSwitchUsdText;
|
|
2009
|
+
const unpriced = spendArg.unpricedAnswers === 0 ? ''
|
|
2010
|
+
: ` · ${spendArg.unpricedAnswers} ${spendArg.unpricedAnswers === 1 ? 'answer' : 'answers'} could not be priced`;
|
|
2011
|
+
const asOf = asOfArg ? ` · as of ${asOfArg}` : '';
|
|
2012
|
+
const unmeteredText = spendArg.limitMicroUsd === null
|
|
2013
|
+
? 'No monthly spend limit, so there is nothing to meter: AGL counts what the key costs.' : '';
|
|
2014
|
+
if (plugins.authswitchSpend.authSwitchSpendMonthEnded(spendArg, nowArg)) {
|
|
2015
|
+
return { text: `${usd(spendArg.spentMicroUsd)} in a month that has ended · Refresh reads this one${asOf}`,
|
|
2016
|
+
tone: 'neutral', unmeteredText };
|
|
2017
|
+
}
|
|
2018
|
+
if (spendArg.limitMicroUsd === null) {
|
|
2019
|
+
return { text: `Spent ${usd(spendArg.spentMicroUsd)} this month · no limit${unpriced}${asOf}`, tone: 'neutral',
|
|
2020
|
+
unmeteredText };
|
|
2021
|
+
}
|
|
2022
|
+
const percent = plugins.authswitchSpend.authSwitchSpendPercent(spendArg) ?? 0;
|
|
2023
|
+
return {
|
|
2024
|
+
text: `${usd(spendArg.spentMicroUsd)} of ${usd(spendArg.limitMicroUsd)} this month · AGL traffic only${unpriced}${asOf}`,
|
|
2025
|
+
tone: plugins.authswitchSpend.authSwitchSpendExhausted(spendArg, nowArg) ? 'critical'
|
|
2026
|
+
: percent >= spendAttentionPercent ? 'attention' : 'neutral',
|
|
2027
|
+
unmeteredText,
|
|
1773
2028
|
};
|
|
1774
2029
|
};
|
|
1775
2030
|
|