agl 40.4.0 → 40.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/changelog.md +27 -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 +142 -28
- package/dist_ts_authswitch/authority-contract.js +48 -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 +94 -29
- package/dist_ts_authswitch/classes.authorityclient.d.ts +24 -7
- package/dist_ts_authswitch/classes.authorityclient.js +31 -11
- 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 +16 -7
- package/dist_ts_authswitch/classes.claudenative.js +29 -16
- package/dist_ts_authswitch/classes.claudesignin.js +5 -2
- 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 +198 -21
- 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 +167 -29
- 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 +108 -32
- package/ts_authswitch/classes.authorityclient.ts +41 -10
- 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 +28 -15
- package/ts_authswitch/classes.claudesignin.ts +4 -1
- package/ts_authswitch/native-contracts.md +81 -20
- 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 +3 -1
- package/ts_web/accounts.templates.ts +20 -11
- package/ts_web/classes.accountsreadmodel.ts +322 -70
- 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'
|
|
@@ -1148,10 +1234,13 @@ interface IPillParts {
|
|
|
1148
1234
|
service: IAccountsServiceStatus;
|
|
1149
1235
|
status: IAccountStatus;
|
|
1150
1236
|
worst: TAccountLoginState | null;
|
|
1151
|
-
payment:
|
|
1237
|
+
/** The payment problem's badge while one is held: `Payment problem`, or a key's `Spend cap reached`. */
|
|
1238
|
+
payment: IAccountStatus | null;
|
|
1152
1239
|
/** A switch of a tool on this host that runs on the account could not be settled. */
|
|
1153
1240
|
held: boolean;
|
|
1154
1241
|
limit: plugins.authswitchUsageLimit.IAuthSwitchUsageLimit | null;
|
|
1242
|
+
/** What the pill calls the limit while the account is at it: `At limit`, a key's `At spend limit` or `Unpriced answer`. */
|
|
1243
|
+
limitText: string;
|
|
1155
1244
|
/** Until when the reserve is lifted, while it is. */
|
|
1156
1245
|
liftedUntil: string | null;
|
|
1157
1246
|
}
|
|
@@ -1169,14 +1258,14 @@ const accountPill = (partsArg: IPillParts): IAccountPill => {
|
|
|
1169
1258
|
: textArg;
|
|
1170
1259
|
if (partsArg.removed) return pill('removed', 'Removed', 'neutral');
|
|
1171
1260
|
if (partsArg.service.state !== 'current') return pill('service', partsArg.status.text, partsArg.status.tone);
|
|
1172
|
-
if (partsArg.payment) return pill('payment',
|
|
1261
|
+
if (partsArg.payment) return pill('payment', partsArg.payment.text, 'critical');
|
|
1173
1262
|
if (partsArg.worst === 'quarantined' || partsArg.worst === 'needsReauth') {
|
|
1174
1263
|
return pill('signIn', partsArg.status.text, partsArg.status.tone);
|
|
1175
1264
|
}
|
|
1176
1265
|
if (partsArg.held) return pill('held', 'Switch held', 'critical');
|
|
1177
1266
|
if (partsArg.worst === 'pendingHandoff') return pill('handover', partsArg.status.text, partsArg.status.tone);
|
|
1178
1267
|
if (partsArg.worst === 'retryWait') return pill('renewing', partsArg.status.text, partsArg.status.tone);
|
|
1179
|
-
if (partsArg.limit?.state === 'exhausted') return pill('atLimit', back(
|
|
1268
|
+
if (partsArg.limit?.state === 'exhausted') return pill('atLimit', back(partsArg.limitText), 'neutral');
|
|
1180
1269
|
if (partsArg.limit?.state === 'reserved') return pill('reserved', back('Reserved'), 'neutral');
|
|
1181
1270
|
if (partsArg.liftedUntil) {
|
|
1182
1271
|
return pill('lifted', `Reserve lifted · until ${plugins.authswitchPreuseText.localDateTime(partsArg.liftedUntil)}`, 'info');
|
|
@@ -1202,10 +1291,41 @@ const primaryMeter = (usageArg: IAccountUsage | null, thresholdArg: number | nul
|
|
|
1202
1291
|
};
|
|
1203
1292
|
};
|
|
1204
1293
|
|
|
1205
|
-
/**
|
|
1294
|
+
/**
|
|
1295
|
+
* The share of a key's spend limit from which its spend wants attention: a display tone only, judged on the last reading
|
|
1296
|
+
* the screen holds, never a limit the accounts service stores.
|
|
1297
|
+
*/
|
|
1298
|
+
export const spendAttentionPercent = 80;
|
|
1299
|
+
|
|
1300
|
+
/**
|
|
1301
|
+
* 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
|
|
1302
|
+
* window and wants attention from `spendAttentionPercent`; without a limit its spend alone.
|
|
1303
|
+
*/
|
|
1304
|
+
const spendMeter = (spendArg: interfaces.TControllerAccountSpendReading): IAccountPrimaryMeter => {
|
|
1305
|
+
const spent = plugins.authswitchSpend.authSwitchUsdText(spendArg.spentMicroUsd);
|
|
1306
|
+
const percent = plugins.authswitchSpend.authSwitchSpendPercent(spendArg);
|
|
1307
|
+
if (percent === null) return { label: 'This month', percent: null, valueText: spent, tone: 'neutral', threshold: null };
|
|
1308
|
+
return {
|
|
1309
|
+
label: plugins.authswitchUsageWindows.authSwitchSpendWindowLabel,
|
|
1310
|
+
percent: clampPercent(percent),
|
|
1311
|
+
valueText: spent,
|
|
1312
|
+
tone: percent >= 100 ? 'neutral' : percent >= spendAttentionPercent ? 'attention' : 'positive',
|
|
1313
|
+
threshold: null,
|
|
1314
|
+
};
|
|
1315
|
+
};
|
|
1316
|
+
|
|
1317
|
+
/** An API key as it is named beside its name: its provider and how the key ends, when the service knows that. */
|
|
1318
|
+
const keyIdentityText = (accountArg: TAccount): string => {
|
|
1319
|
+
const provider = keyProvider(accountArg.providerId).name;
|
|
1320
|
+
return accountArg.keyHint === null ? `${provider} API key` : `${provider} API key ending ${accountArg.keyHint}`;
|
|
1321
|
+
};
|
|
1322
|
+
|
|
1323
|
+
/** The list row's second line: who the account is -- email and plan, or a key's identity -- and who uses it. */
|
|
1206
1324
|
const listMeta = (accountArg: TAccount, chipsArg: readonly IAccountHarnessChip[]): string => [
|
|
1207
|
-
accountArg
|
|
1208
|
-
|
|
1325
|
+
...isApiKeyAccount(accountArg) ? [keyIdentityText(accountArg)] : [
|
|
1326
|
+
accountArg.email && accountArg.email !== accountArg.label ? accountArg.email : '',
|
|
1327
|
+
accountArg.plan ? `${accountArg.plan} plan` : '',
|
|
1328
|
+
],
|
|
1209
1329
|
chipsArg.filter((chipArg) => chipArg.unavailableReason === undefined)
|
|
1210
1330
|
.map((chipArg) => harnessLabel(chipArg.harnessId)).join(', '),
|
|
1211
1331
|
].filter(Boolean).join(' · ');
|
|
@@ -1246,21 +1366,96 @@ const usageLimit = (
|
|
|
1246
1366
|
return limit.state === 'available' ? null : limit;
|
|
1247
1367
|
};
|
|
1248
1368
|
|
|
1249
|
-
|
|
1369
|
+
/** Whether a key's month holds it for an answer that could not be priced rather than for what it spent. */
|
|
1370
|
+
const heldUnpriced = (spendArg: interfaces.TControllerAccountSpendReading): boolean => (
|
|
1371
|
+
spendArg.limitMicroUsd !== null && spendArg.spentMicroUsd < spendArg.limitMicroUsd
|
|
1372
|
+
);
|
|
1373
|
+
|
|
1374
|
+
/**
|
|
1375
|
+
* The limit as the page's badge; a key's (`spendArg`, its month) is its spend limit, or an answer that could not be
|
|
1376
|
+
* priced.
|
|
1377
|
+
*/
|
|
1378
|
+
const limitStatus = (
|
|
1379
|
+
limit: plugins.authswitchUsageLimit.IAuthSwitchUsageLimit | null,
|
|
1380
|
+
spendArg: interfaces.TControllerAccountSpendReading | null,
|
|
1381
|
+
): IAccountStatus | null => {
|
|
1250
1382
|
if (!limit) return null;
|
|
1251
1383
|
const reset = limit.resetAt ? `, resets ${plugins.authswitchPreuseText.localDateTime(limit.resetAt)}` : '';
|
|
1384
|
+
if (spendArg) {
|
|
1385
|
+
return { text: heldUnpriced(spendArg) ? `An answer could not be priced, so the key is held${reset}`
|
|
1386
|
+
: `At its spend limit${reset}`, tone: 'critical' };
|
|
1387
|
+
}
|
|
1252
1388
|
return { text: `${sentence(plugins.authswitchUsageLimit.authSwitchUsageLimitText(limit))}${reset}`,
|
|
1253
1389
|
tone: limit.state === 'reserved' ? 'attention' : 'critical' };
|
|
1254
1390
|
};
|
|
1255
1391
|
|
|
1256
|
-
/**
|
|
1257
|
-
|
|
1392
|
+
/**
|
|
1393
|
+
* The payment problem a usage reading holds the account with, as a badge and what it means; `null` without one, and
|
|
1394
|
+
* once a key's spend cap reset (`authSwitchPaymentProblemHeld`). A key's says its cause.
|
|
1395
|
+
*/
|
|
1396
|
+
const paymentView = (accountArg: TAccount, usageArg: TUsage | null, nowArg: number): {
|
|
1397
|
+
status: IAccountStatus; detail: string;
|
|
1398
|
+
} | null => {
|
|
1258
1399
|
const problem = usageArg?.paymentProblem;
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1400
|
+
if (!problem || !plugins.authswitchSubscription.authSwitchPaymentProblemHeld(problem, nowArg)) return null;
|
|
1401
|
+
const seen = `Seen ${plugins.authswitchPreuseText.localDateTime(problem.at)}.`;
|
|
1402
|
+
if (!isApiKeyAccount(accountArg)) {
|
|
1403
|
+
return { status: { text: 'Payment problem', tone: 'critical' },
|
|
1404
|
+
detail: `${plugins.authswitchSubscription.authSwitchPaymentProblemText} ${seen}` };
|
|
1405
|
+
}
|
|
1406
|
+
const resets = problem.until ? ` The cap resets ${plugins.authswitchPreuseText.localDateTime(problem.until)}.` : '';
|
|
1407
|
+
return { status: { text: problem.cause === 'spend_cap' ? 'Spend cap reached' : 'Payment problem', tone: 'critical' },
|
|
1408
|
+
detail: `${plugins.authswitchSubscription.authSwitchApiKeyPaymentProblemText(problem, accountArg.providerId)} ${seen}`
|
|
1409
|
+
+ resets };
|
|
1410
|
+
};
|
|
1411
|
+
|
|
1412
|
+
/** A key's spend limit as its field shows it, in dollars and cents: `50.00`. */
|
|
1413
|
+
export const spendLimitDraft = (spendLimitMicroUsdArg: number): string => (
|
|
1414
|
+
(spendLimitMicroUsdArg / plugins.authswitchSpend.authSwitchMicroUsdPerUsd).toFixed(2)
|
|
1415
|
+
);
|
|
1416
|
+
|
|
1417
|
+
/**
|
|
1418
|
+
* Why a spend limit draft, in dollars, cannot be saved yet, or `''` when it can: an amount to the cent the service's own
|
|
1419
|
+
* rule allows, other than the limit the key has.
|
|
1420
|
+
*/
|
|
1421
|
+
export const spendLimitReason = (draftArg: string, currentArg: number | null): string => {
|
|
1422
|
+
if (draftArg.trim().length === 0) return 'Enter an amount in dollars.';
|
|
1423
|
+
const value = plugins.authswitchSpend.authSwitchParseUsd(draftArg);
|
|
1424
|
+
if (value === null) return 'Enter dollars and cents, such as 50 or 12.50.';
|
|
1425
|
+
if (!interfaces.isControllerSpendLimitMicroUsd(value)) {
|
|
1426
|
+
return `Enter an amount from ${plugins.authswitchSpend.authSwitchUsdText(interfaces.controllerSpendLimitRange.min)} to`
|
|
1427
|
+
+ ` ${plugins.authswitchSpend.authSwitchUsdText(interfaces.controllerSpendLimitRange.max)}.`;
|
|
1428
|
+
}
|
|
1429
|
+
return value === currentArg ? 'The spend limit is unchanged.' : '';
|
|
1430
|
+
};
|
|
1431
|
+
|
|
1432
|
+
/** A key's spend limit in words, as its page and the screen after an added key state it. */
|
|
1433
|
+
export const spendLimitText = (spendLimitMicroUsdArg: number | null): string => spendLimitMicroUsdArg === null
|
|
1434
|
+
? 'No monthly limit'
|
|
1435
|
+
: `${plugins.authswitchSpend.authSwitchUsdText(spendLimitMicroUsdArg)} per calendar month (UTC)`;
|
|
1436
|
+
|
|
1437
|
+
/**
|
|
1438
|
+
* Why the Add API key dialog cannot add yet, or `''` when it can: a key typed, an optional name the service's label rule
|
|
1439
|
+
* accepts, and an optional spend limit in dollars its spend limit rule accepts.
|
|
1440
|
+
*/
|
|
1441
|
+
export const apiKeyReason = (keyTypedArg: boolean, labelArg: string, limitArg: string): string => {
|
|
1442
|
+
if (!keyTypedArg) return 'Paste the key.';
|
|
1443
|
+
if (labelArg.trim().length > 0) {
|
|
1444
|
+
const name = renameReason(labelArg, '');
|
|
1445
|
+
if (name) return name;
|
|
1446
|
+
}
|
|
1447
|
+
return limitArg.trim().length > 0 ? spendLimitReason(limitArg, null) : '';
|
|
1262
1448
|
};
|
|
1263
1449
|
|
|
1450
|
+
/** What the screen says once a key is added: the account, how its key ends, and the spend limit it actually has. */
|
|
1451
|
+
export const apiKeyAddedNotice = (accountArg: TAccount): IAccountsNotice => ({
|
|
1452
|
+
tone: 'positive',
|
|
1453
|
+
// 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.
|
|
1454
|
+
text: `Added ${accountArg.label}${accountArg.keyHint ? `, the key ending ${accountArg.keyHint}` : ''}. `
|
|
1455
|
+
+ (accountArg.spendLimitMicroUsd === null ? 'It has no monthly spend limit.'
|
|
1456
|
+
: `Its monthly spend limit is ${spendLimitText(accountArg.spendLimitMicroUsd)}.`),
|
|
1457
|
+
});
|
|
1458
|
+
|
|
1264
1459
|
/**
|
|
1265
1460
|
* Why a usage threshold draft cannot be saved yet, or `''` when it can: a whole percent the service's own rule
|
|
1266
1461
|
* allows, other than the threshold the account has.
|
|
@@ -1315,11 +1510,17 @@ const serviceRow = (
|
|
|
1315
1510
|
));
|
|
1316
1511
|
const usageLogin = accountArg.removed ? undefined : logins.find((loginArg) => loginArg.health !== 'removed');
|
|
1317
1512
|
const usageRead = contextArg.usage.get(accountArg.id);
|
|
1318
|
-
const usage = usageRead?.state === 'read' ? usageView(usageRead.usage, usageLogin?.owner ?? null) : null;
|
|
1513
|
+
const usage = usageRead?.state === 'read' ? usageView(usageRead.usage, usageLogin?.owner ?? null, contextArg.now) : null;
|
|
1319
1514
|
const rawLimit = accountArg.removed || usageRead?.state !== 'read' ? null
|
|
1320
1515
|
: usageLimit(accountArg, usageRead.usage, contextArg.now);
|
|
1321
|
-
|
|
1322
|
-
const
|
|
1516
|
+
// A key's month, while the read holds one: its limit, its meter and how the pill names the limit are its spend's.
|
|
1517
|
+
const spend = usageRead?.state === 'read' && usageRead.usage.reading?.kind === 'spend' ? usageRead.usage.reading.spend : null;
|
|
1518
|
+
const limit = limitStatus(rawLimit, spend);
|
|
1519
|
+
const payment = accountArg.removed || usageRead?.state !== 'read' ? null
|
|
1520
|
+
: paymentView(accountArg, usageRead.usage, contextArg.now);
|
|
1521
|
+
const apiKey = isApiKeyAccount(accountArg);
|
|
1522
|
+
const provider = keyProvider(accountArg.providerId);
|
|
1523
|
+
const identity = apiKey ? keyIdentityText(accountArg) : identityText(accountArg.email, accountArg.plan);
|
|
1323
1524
|
const liftedUntil = !accountArg.removed && accountArg.maxUsagePercent !== null
|
|
1324
1525
|
&& plugins.authswitchUsageLimit.authSwitchReserveLiftActive(accountArg.reserveLiftUntil, contextArg.now)
|
|
1325
1526
|
? accountArg.reserveLiftUntil! : null;
|
|
@@ -1329,8 +1530,14 @@ const serviceRow = (
|
|
|
1329
1530
|
return {
|
|
1330
1531
|
key: `service:${accountArg.id}`,
|
|
1331
1532
|
providerId: accountArg.providerId,
|
|
1533
|
+
groupId: apiKey ? `${accountArg.providerId}-api-key` : accountArg.providerId,
|
|
1534
|
+
apiKey,
|
|
1535
|
+
spendLimitMicroUsd: apiKey ? accountArg.spendLimitMicroUsd : null,
|
|
1536
|
+
removeNote: apiKey
|
|
1537
|
+
? `AGL stops using the key. It stays valid at ${provider.name} until you revoke it in ${provider.console}.`
|
|
1538
|
+
: '',
|
|
1332
1539
|
label: accountArg.label,
|
|
1333
|
-
identityText:
|
|
1540
|
+
identityText: identity,
|
|
1334
1541
|
email: accountArg.email ?? '',
|
|
1335
1542
|
removed: accountArg.removed,
|
|
1336
1543
|
status,
|
|
@@ -1340,13 +1547,14 @@ const serviceRow = (
|
|
|
1340
1547
|
service: contextArg.service,
|
|
1341
1548
|
status,
|
|
1342
1549
|
worst: worstLoginState(liveLogins),
|
|
1343
|
-
payment: payment
|
|
1550
|
+
payment: payment?.status ?? null,
|
|
1344
1551
|
held: [...uses.values()].some((useArg) => useArg.host === 'quarantined'),
|
|
1345
1552
|
limit: rawLimit,
|
|
1553
|
+
limitText: !spend ? 'At limit' : heldUnpriced(spend) ? 'Unpriced answer' : 'At spend limit',
|
|
1346
1554
|
liftedUntil,
|
|
1347
1555
|
}),
|
|
1348
|
-
listMeta: accountArg.removed ?
|
|
1349
|
-
primaryMeter: primaryMeter(usage, accountArg.maxUsagePercent),
|
|
1556
|
+
listMeta: accountArg.removed ? identity : listMeta(accountArg, harnesses),
|
|
1557
|
+
primaryMeter: spend ? spendMeter(spend) : primaryMeter(usage, accountArg.maxUsagePercent),
|
|
1350
1558
|
backAt: rawLimit?.resetAt ?? null,
|
|
1351
1559
|
harnesses,
|
|
1352
1560
|
usageLoginId: usageLogin?.id ?? null,
|
|
@@ -1373,8 +1581,10 @@ const serviceRow = (
|
|
|
1373
1581
|
logins: logins.map((loginArg) => loginView(loginArg, contextArg.service)),
|
|
1374
1582
|
manage: [
|
|
1375
1583
|
serviceSlot('rename', 'Rename', 'lucide:PenLine', contextArg.service, removed),
|
|
1376
|
-
|
|
1377
|
-
...(
|
|
1584
|
+
// A key's limit is its spend; a usage threshold and its reserve are a sign-in's.
|
|
1585
|
+
...(apiKey ? [serviceSlot('spendLimit', 'Spend limit', 'lucide:Wallet', contextArg.service, removed)]
|
|
1586
|
+
: [serviceSlot('maxUsage', 'Max usage', 'lucide:SlidersHorizontal', contextArg.service, removed)]),
|
|
1587
|
+
...(!apiKey && accountArg.maxUsagePercent !== null && accountArg.maxUsagePercent < 100
|
|
1378
1588
|
? [serviceSlot('liftReserve', 'Lift reserve', 'lucide:Clock', contextArg.service, removed)] : []),
|
|
1379
1589
|
serviceSlot('remove', 'Remove', 'lucide:Trash2', contextArg.service, removed),
|
|
1380
1590
|
...(usageLogin ? [serviceSlot('usage', 'Show latest usage', 'lucide:Gauge', contextArg.service)] : []),
|
|
@@ -1386,11 +1596,12 @@ const serviceRow = (
|
|
|
1386
1596
|
: [],
|
|
1387
1597
|
flexStatusText: flexConnectionId ? contextArg.flex.refreshStatuses.get(flexConnectionId) ?? '' : '',
|
|
1388
1598
|
facts: [
|
|
1389
|
-
{ label: 'Provider', value: providerLabel(accountArg.providerId) },
|
|
1599
|
+
{ label: 'Provider', value: apiKey ? `${provider.name} (API key)` : providerLabel(accountArg.providerId) },
|
|
1390
1600
|
...(usageLogin ? [{ label: 'Usage', value: usageFact(usageRead, usage) }] : []),
|
|
1391
1601
|
...(usageRead?.state === 'read'
|
|
1392
1602
|
? plugins.authswitchSubscription.authSwitchSubscriptionFacts(usageRead.usage.reading) : []),
|
|
1393
|
-
{ label: '
|
|
1603
|
+
apiKey ? { label: 'Monthly spend limit', value: spendLimitText(accountArg.spendLimitMicroUsd) }
|
|
1604
|
+
: { label: 'Max usage', value: maxUsageFact(accountArg.maxUsagePercent) },
|
|
1394
1605
|
{ label: 'Account reference', value: accountArg.id },
|
|
1395
1606
|
],
|
|
1396
1607
|
accountId: accountArg.id,
|
|
@@ -1402,23 +1613,22 @@ const serviceRow = (
|
|
|
1402
1613
|
};
|
|
1403
1614
|
};
|
|
1404
1615
|
|
|
1405
|
-
/** The listed rows, one group per
|
|
1616
|
+
/** The listed rows, one group per kind: ChatGPT, Claude, OpenAI API keys, Anthropic API keys. */
|
|
1406
1617
|
const accountGroups = (rowsArg: readonly IAccountRow[]): IAccountGroup[] => {
|
|
1407
|
-
const
|
|
1408
|
-
const left =
|
|
1409
|
-
const right =
|
|
1410
|
-
if (left !== right) return (left < 0 ?
|
|
1618
|
+
const groups = [...new Set(rowsArg.map((rowArg) => rowArg.groupId))].sort((leftArg, rightArg) => {
|
|
1619
|
+
const left = groupOrder.indexOf(leftArg);
|
|
1620
|
+
const right = groupOrder.indexOf(rightArg);
|
|
1621
|
+
if (left !== right) return (left < 0 ? groupOrder.length : left) - (right < 0 ? groupOrder.length : right);
|
|
1411
1622
|
return leftArg.localeCompare(rightArg);
|
|
1412
1623
|
});
|
|
1413
|
-
return
|
|
1414
|
-
|
|
1415
|
-
label:
|
|
1416
|
-
|
|
1417
|
-
}));
|
|
1624
|
+
return groups.map((groupIdArg) => {
|
|
1625
|
+
const rows = rowsArg.filter((rowArg) => rowArg.groupId === groupIdArg);
|
|
1626
|
+
return { id: groupIdArg, providerId: rows[0]!.providerId, label: groupLabel(groupIdArg), rows };
|
|
1627
|
+
});
|
|
1418
1628
|
};
|
|
1419
1629
|
|
|
1420
1630
|
/**
|
|
1421
|
-
* The account list: the service's accounts, under their provider.
|
|
1631
|
+
* The account list: the service's accounts, under their provider, the API keys apart from the sign-ins.
|
|
1422
1632
|
*
|
|
1423
1633
|
* The rows and the sentence under them come from the same read. The controller reads every page of
|
|
1424
1634
|
* the service's snapshot, so a list that still does not end at its last row says so explicitly
|
|
@@ -1459,12 +1669,17 @@ const accountsSummary = (serviceArg: IAccountsServiceStatus, listArg: IAccountsL
|
|
|
1459
1669
|
const needsYou = rows.filter((rowArg) => needsYouPills.includes(rowArg.pill.kind));
|
|
1460
1670
|
const firstBack = limited.map((rowArg) => rowArg.backAt).filter((atArg): atArg is string => atArg !== null)
|
|
1461
1671
|
.sort((leftArg, rightArg) => Date.parse(leftArg) - Date.parse(rightArg))[0];
|
|
1672
|
+
const keys = rows.filter((rowArg) => rowArg.apiKey);
|
|
1673
|
+
const readyKeys = keys.filter((rowArg) => readyPills.includes(rowArg.pill.kind)).length;
|
|
1462
1674
|
return {
|
|
1463
1675
|
ready: ready.length,
|
|
1464
1676
|
total: rows.length,
|
|
1465
|
-
readyText:
|
|
1466
|
-
|
|
1467
|
-
|
|
1677
|
+
readyText: [
|
|
1678
|
+
...listArg.groups.filter((groupArg) => !groupArg.rows[0]!.apiKey).map((groupArg) => (
|
|
1679
|
+
`${groupArg.rows.filter((rowArg) => readyPills.includes(rowArg.pill.kind)).length} ${groupArg.label}`
|
|
1680
|
+
)),
|
|
1681
|
+
...keys.length ? [`${readyKeys} API ${readyKeys === 1 ? 'key' : 'keys'}`] : [],
|
|
1682
|
+
].join(' · '),
|
|
1468
1683
|
limited: limited.length,
|
|
1469
1684
|
limitedText: firstBack ? `First back ${plugins.authswitchPreuseText.localDateTime(firstBack)}` : '',
|
|
1470
1685
|
needsYou: needsYou.length,
|
|
@@ -1472,10 +1687,13 @@ const accountsSummary = (serviceArg: IAccountsServiceStatus, listArg: IAccountsL
|
|
|
1472
1687
|
};
|
|
1473
1688
|
};
|
|
1474
1689
|
|
|
1475
|
-
/**
|
|
1476
|
-
|
|
1690
|
+
/**
|
|
1691
|
+
* The accounts whose usage a screen reads: each listed account's usage login, by account id. `apiKey` marks a key, whose
|
|
1692
|
+
* spend changes with every answer AGL relays on it, so the screen reads it again on every change it is told of.
|
|
1693
|
+
*/
|
|
1694
|
+
export const usageTargets = (viewArg: IAccountsView): { accountId: string; loginId: string; apiKey: boolean }[] => (
|
|
1477
1695
|
viewArg.list.groups.flatMap((groupArg) => groupArg.rows).flatMap((rowArg) => (
|
|
1478
|
-
rowArg.usageLoginId ? [{ accountId: rowArg.accountId, loginId: rowArg.usageLoginId }] : []
|
|
1696
|
+
rowArg.usageLoginId ? [{ accountId: rowArg.accountId, loginId: rowArg.usageLoginId, apiKey: rowArg.apiKey }] : []
|
|
1479
1697
|
))
|
|
1480
1698
|
);
|
|
1481
1699
|
|
|
@@ -1512,6 +1730,7 @@ export const accountsView = (
|
|
|
1512
1730
|
'addClaude', 'Add Claude account', 'lucide:UserPlus', service,
|
|
1513
1731
|
claudeSignIns.length ? 'A Claude sign-in is already waiting below.' : '',
|
|
1514
1732
|
),
|
|
1733
|
+
addApiKey: serviceSlot('addApiKey', 'Add API key', 'lucide:KeyRound', service),
|
|
1515
1734
|
adoptCodex: projection && interfaces.findHostCodexAssignment(projection)
|
|
1516
1735
|
? null
|
|
1517
1736
|
: serviceSlot('adoptCodex', 'Add Codex\'s current sign-in', 'lucide:LogIn', service),
|
|
@@ -1752,13 +1971,14 @@ const asOfTime = (isoArg: string | null): string => {
|
|
|
1752
1971
|
* `ownerArg` is who renews the login it was read for, which names why the usage of Claude Code's own sign-in is
|
|
1753
1972
|
* missing.
|
|
1754
1973
|
*/
|
|
1755
|
-
export const usageView = (usageArg: TUsage, ownerArg: TLogin['owner'] | null = null
|
|
1974
|
+
export const usageView = (usageArg: TUsage, ownerArg: TLogin['owner'] | null = null,
|
|
1975
|
+
nowArg: number = Date.now()): IAccountUsage => {
|
|
1756
1976
|
const observed = asOfTime(usageArg.observedAt);
|
|
1757
1977
|
const reading = usageArg.reading;
|
|
1758
1978
|
const current = usageArg.state === 'current';
|
|
1759
1979
|
if (ownerArg === 'claude_native' && usageArg.problem === 'access_unavailable') {
|
|
1760
1980
|
return { freshness: usageArg.state === 'stale' && observed ? `Last known, as of ${observed}` : 'Unavailable',
|
|
1761
|
-
current, problemText: claudeHomeUsageText, meters: usageMeters(reading) };
|
|
1981
|
+
current, problemText: claudeHomeUsageText, meters: usageMeters(reading), spend: null };
|
|
1762
1982
|
}
|
|
1763
1983
|
const next = asOfTime(usageArg.nextAttemptAt);
|
|
1764
1984
|
return {
|
|
@@ -1770,6 +1990,38 @@ export const usageView = (usageArg: TUsage, ownerArg: TLogin['owner'] | null = n
|
|
|
1770
1990
|
? `The provider limited usage reads, so the last one was refused${next ? `; the next read is at ${next}` : ''}.`
|
|
1771
1991
|
: plugins.authswitchUsageWindows.authSwitchUsageProblemText(usageArg.problem),
|
|
1772
1992
|
meters: usageMeters(reading),
|
|
1993
|
+
spend: reading?.kind === 'spend' ? spendView(reading.spend, nowArg, observed) : null,
|
|
1994
|
+
};
|
|
1995
|
+
};
|
|
1996
|
+
|
|
1997
|
+
/**
|
|
1998
|
+
* An API key's month in words: what AGL's traffic on it cost against its limit, or without one, how many answers could
|
|
1999
|
+
* not be priced, and when the accounts service read the key's ledger for it (`asOfArg`, the reading's `observedAt`): the
|
|
2000
|
+
* screen reads the month again on Refresh, Show latest usage and each change the controller announces, never on a
|
|
2001
|
+
* timer, so the figure says its time. Attention from `spendAttentionPercent` of the limit, critical once the key is
|
|
2002
|
+
* held, as of that reading. A reading whose month has ended says nothing of this one (`authSwitchSpendMonthEnded`).
|
|
2003
|
+
*/
|
|
2004
|
+
const spendView = (spendArg: interfaces.TControllerAccountSpendReading, nowArg: number, asOfArg: string): IAccountSpend => {
|
|
2005
|
+
const usd = plugins.authswitchSpend.authSwitchUsdText;
|
|
2006
|
+
const unpriced = spendArg.unpricedAnswers === 0 ? ''
|
|
2007
|
+
: ` · ${spendArg.unpricedAnswers} ${spendArg.unpricedAnswers === 1 ? 'answer' : 'answers'} could not be priced`;
|
|
2008
|
+
const asOf = asOfArg ? ` · as of ${asOfArg}` : '';
|
|
2009
|
+
const unmeteredText = spendArg.limitMicroUsd === null
|
|
2010
|
+
? 'No monthly spend limit, so there is nothing to meter: AGL counts what the key costs.' : '';
|
|
2011
|
+
if (plugins.authswitchSpend.authSwitchSpendMonthEnded(spendArg, nowArg)) {
|
|
2012
|
+
return { text: `${usd(spendArg.spentMicroUsd)} in a month that has ended · Refresh reads this one${asOf}`,
|
|
2013
|
+
tone: 'neutral', unmeteredText };
|
|
2014
|
+
}
|
|
2015
|
+
if (spendArg.limitMicroUsd === null) {
|
|
2016
|
+
return { text: `Spent ${usd(spendArg.spentMicroUsd)} this month · no limit${unpriced}${asOf}`, tone: 'neutral',
|
|
2017
|
+
unmeteredText };
|
|
2018
|
+
}
|
|
2019
|
+
const percent = plugins.authswitchSpend.authSwitchSpendPercent(spendArg) ?? 0;
|
|
2020
|
+
return {
|
|
2021
|
+
text: `${usd(spendArg.spentMicroUsd)} of ${usd(spendArg.limitMicroUsd)} this month · AGL traffic only${unpriced}${asOf}`,
|
|
2022
|
+
tone: plugins.authswitchSpend.authSwitchSpendExhausted(spendArg, nowArg) ? 'critical'
|
|
2023
|
+
: percent >= spendAttentionPercent ? 'attention' : 'neutral',
|
|
2024
|
+
unmeteredText,
|
|
1773
2025
|
};
|
|
1774
2026
|
};
|
|
1775
2027
|
|