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
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import * as plugins from './plugins.js';
|
|
2
2
|
import type * as interfaces from '../ts_interfaces/index.js';
|
|
3
|
-
import { isControllerReserveLiftHours } from '../ts_interfaces/index.js';
|
|
3
|
+
import { controllerApiKeyMaxLength, isControllerReserveLiftHours } from '../ts_interfaces/index.js';
|
|
4
4
|
import type { ControllerSocketClient } from './classes.controllersocketclient.js';
|
|
5
5
|
import {
|
|
6
6
|
accountsHowTo,
|
|
7
7
|
accountsView,
|
|
8
8
|
answerNotice,
|
|
9
|
+
apiKeyAddedNotice,
|
|
10
|
+
apiKeyReason,
|
|
9
11
|
claudeHostReadPending,
|
|
10
12
|
claudeHostView,
|
|
11
13
|
claudeReadPending,
|
|
@@ -31,6 +33,8 @@ import {
|
|
|
31
33
|
recheckNotice,
|
|
32
34
|
renameReason,
|
|
33
35
|
servicePanel,
|
|
36
|
+
spendLimitDraft,
|
|
37
|
+
spendLimitReason,
|
|
34
38
|
usageTargets,
|
|
35
39
|
type IAccountActionSlot,
|
|
36
40
|
type IAccountLoginView,
|
|
@@ -102,9 +106,27 @@ type TAccountsDialog =
|
|
|
102
106
|
| { kind: 'codexRelease'; accountId: string }
|
|
103
107
|
| { kind: 'preuse'; accountId: string; loginId: string }
|
|
104
108
|
| { kind: 'liftReserve'; accountId: string }
|
|
109
|
+
/** Adding an API key of the provider Add account chose. */
|
|
110
|
+
| { kind: 'apiKey'; providerId: interfaces.TControllerApiKeyProvider }
|
|
105
111
|
| { kind: 'claudeRelease' }
|
|
106
112
|
| { kind: 'claudeRepair' };
|
|
107
113
|
|
|
114
|
+
/** What a spend limit does, under its field. */
|
|
115
|
+
const spendLimitDescription = 'What AGL\'s own traffic on the key may cost in a calendar month (UTC), in dollars. At'
|
|
116
|
+
+ ' the limit AGL places nothing more on the key until the month ends; answers already running finish. Keep the'
|
|
117
|
+
+ ' provider\'s own limit higher, as the backstop for everything else that uses the key.';
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* How long the browser waits for an added key: the accounts service gives the provider 15 seconds to answer whether it
|
|
121
|
+
* accepts the key, and the controller answers once its own projection shows the account.
|
|
122
|
+
*/
|
|
123
|
+
const apiKeyAddTimeoutMs = 60_000;
|
|
124
|
+
|
|
125
|
+
/** A key's provider by name, as the Add API key dialog names it. */
|
|
126
|
+
const apiKeyProviderName = (providerIdArg: interfaces.TControllerApiKeyProvider): string => (
|
|
127
|
+
providerIdArg === 'openai' ? 'OpenAI' : 'Anthropic'
|
|
128
|
+
);
|
|
129
|
+
|
|
108
130
|
/** What a usage threshold does, under its field. */
|
|
109
131
|
const maxUsageDescription = 'At this share of any usage window AGL places no new work on the account and moves its'
|
|
110
132
|
+ ' sessions on, so the rest stays yours outside AGL.';
|
|
@@ -166,8 +188,9 @@ const liftOptions = Array.from({ length: 24 }, (_, indexArg) => ({
|
|
|
166
188
|
}));
|
|
167
189
|
|
|
168
190
|
/**
|
|
169
|
-
* The Accounts screen: the accounts service's ChatGPT and Claude accounts
|
|
170
|
-
* tightest usage window -- that each open the account's own page; the
|
|
191
|
+
* The Accounts screen: the accounts service's ChatGPT and Claude accounts and OpenAI and Anthropic API keys as a list of
|
|
192
|
+
* one-line rows -- one status, the tightest usage window or a key's month -- that each open the account's own page; the
|
|
193
|
+
* tools on this host; and the service itself.
|
|
171
194
|
*
|
|
172
195
|
* It holds no account state of its own. It renders the controller's projection and Flex's connections as the app hands
|
|
173
196
|
* them over, re-reads when the controller announces a change, and runs no timer and no poll -- so a failed read is a
|
|
@@ -236,6 +259,24 @@ export class AglAccounts extends plugins.deesElement.DeesElement {
|
|
|
236
259
|
@plugins.deesElement.state()
|
|
237
260
|
private accessor maxUsageDraft: string | null = null;
|
|
238
261
|
|
|
262
|
+
/** A key's spend limit, in dollars, as the open page's field holds it while edited; `null` while it shows the saved one. */
|
|
263
|
+
@plugins.deesElement.state()
|
|
264
|
+
private accessor spendLimitDraft: string | null = null;
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Whether the Add API key dialog's key field holds a key. The key itself stays in its field only, which leaves the
|
|
268
|
+
* page with the dialog; it is read from there when the owner adds it.
|
|
269
|
+
*/
|
|
270
|
+
@plugins.deesElement.state()
|
|
271
|
+
private accessor apiKeyTyped = false;
|
|
272
|
+
|
|
273
|
+
/** The name and the spend limit, in dollars, the Add API key dialog holds. */
|
|
274
|
+
@plugins.deesElement.state()
|
|
275
|
+
private accessor apiKeyName = '';
|
|
276
|
+
|
|
277
|
+
@plugins.deesElement.state()
|
|
278
|
+
private accessor apiKeyLimit = '';
|
|
279
|
+
|
|
239
280
|
/** What the screen read of Claude Code's own home on this host. */
|
|
240
281
|
@plugins.deesElement.state()
|
|
241
282
|
private accessor claudeHost: TAccountsClaudeHostRead = claudeHostReadPending;
|
|
@@ -386,7 +427,7 @@ export class AglAccounts extends plugins.deesElement.DeesElement {
|
|
|
386
427
|
}
|
|
387
428
|
|
|
388
429
|
.figure { font-size: 20px; font-weight: 600; font-variant-numeric: tabular-nums; }
|
|
389
|
-
.tile.loud .figure { color: var(--dees-color-
|
|
430
|
+
.tile.loud .figure { color: var(--dees-color-text-warning); }
|
|
390
431
|
.of { color: var(--dees-color-text-muted); font-size: var(--dees-font-footnote-size); font-weight: 500; }
|
|
391
432
|
.tileLabel { font-size: var(--dees-font-footnote-size); font-weight: 500; }
|
|
392
433
|
|
|
@@ -490,6 +531,9 @@ export class AglAccounts extends plugins.deesElement.DeesElement {
|
|
|
490
531
|
}
|
|
491
532
|
|
|
492
533
|
.meterValue { color: var(--dees-color-text-secondary); font-variant-numeric: tabular-nums; }
|
|
534
|
+
.spend { margin: 0; color: var(--dees-color-text-secondary); font-size: var(--dees-font-caption1-size); }
|
|
535
|
+
.spend.attention { color: var(--dees-color-text-warning); }
|
|
536
|
+
.spend.critical { color: var(--dees-color-text-error); }
|
|
493
537
|
.row dees-badge { justify-self: end; }
|
|
494
538
|
.chevron { color: var(--dees-color-label-quaternary, var(--dees-color-text-muted)); }
|
|
495
539
|
|
|
@@ -756,7 +800,10 @@ export class AglAccounts extends plugins.deesElement.DeesElement {
|
|
|
756
800
|
this.settleStartedClaudeSignIns(authority);
|
|
757
801
|
const targets = usageTargets(accountsView(this.read, this.projects, this.flex));
|
|
758
802
|
for (const target of targets) {
|
|
759
|
-
|
|
803
|
+
// A key's spend moves with every answer AGL relays on it, and each change of it the controller announces.
|
|
804
|
+
if (rereadUsageArg || target.apiKey || !this.usage.has(target.accountId)) {
|
|
805
|
+
void this.readUsage(target.accountId, target.loginId);
|
|
806
|
+
}
|
|
760
807
|
}
|
|
761
808
|
} catch (errorArg) {
|
|
762
809
|
if (generation !== this.readGeneration) return;
|
|
@@ -870,8 +917,9 @@ export class AglAccounts extends plugins.deesElement.DeesElement {
|
|
|
870
917
|
}
|
|
871
918
|
|
|
872
919
|
/**
|
|
873
|
-
* Re-checks one Claude account: the service reads
|
|
874
|
-
*
|
|
920
|
+
* Re-checks one Claude account or API key: the service reads a Claude account at once and clears its payment problem
|
|
921
|
+
* when it reads healthy, and lifts a key's hold without asking its provider. The usage it answers replaces what the
|
|
922
|
+
* row showed, and the notice says how the read went.
|
|
875
923
|
*/
|
|
876
924
|
private async recheckUsage(accountIdArg: string, loginIdArg: string, labelArg: string): Promise<void> {
|
|
877
925
|
const client = this.client;
|
|
@@ -945,6 +993,7 @@ export class AglAccounts extends plugins.deesElement.DeesElement {
|
|
|
945
993
|
this.dialogAnswer = null;
|
|
946
994
|
this.dialogCheck = false;
|
|
947
995
|
this.liftHours = undefined;
|
|
996
|
+
this.forgetApiKeyDrafts();
|
|
948
997
|
}
|
|
949
998
|
|
|
950
999
|
/** Closes the dialog on screen before it leaves the template, so focus returns to the control that opened it. */
|
|
@@ -959,6 +1008,14 @@ export class AglAccounts extends plugins.deesElement.DeesElement {
|
|
|
959
1008
|
this.dialogAnswer = null;
|
|
960
1009
|
this.dialogCheck = false;
|
|
961
1010
|
this.liftHours = undefined;
|
|
1011
|
+
this.forgetApiKeyDrafts();
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
/** The Add API key dialog's drafts; its key field leaves the page with the dialog. */
|
|
1015
|
+
private forgetApiKeyDrafts(): void {
|
|
1016
|
+
this.apiKeyTyped = false;
|
|
1017
|
+
this.apiKeyName = '';
|
|
1018
|
+
this.apiKeyLimit = '';
|
|
962
1019
|
}
|
|
963
1020
|
|
|
964
1021
|
/** Asks the app to run a Flex action; Flex's state and its requests live there. */
|
|
@@ -999,6 +1056,47 @@ export class AglAccounts extends plugins.deesElement.DeesElement {
|
|
|
999
1056
|
});
|
|
1000
1057
|
}
|
|
1001
1058
|
|
|
1059
|
+
/** Sets an API key's monthly spend limit from the field, in dollars, or clears it with `null`. */
|
|
1060
|
+
private async setSpendLimit(rowArg: IAccountRow, spendLimitMicroUsdArg: number | null): Promise<void> {
|
|
1061
|
+
await this.runAction(`spendlimit:${rowArg.accountId}`, async () => {
|
|
1062
|
+
const { answer } = await this.client!.fire<interfaces.IReq_ControllerAccountsSpendLimit>(
|
|
1063
|
+
'controller.accounts.spendlimit',
|
|
1064
|
+
{ accountId: rowArg.accountId, expectedRevision: rowArg.revision, spendLimitMicroUsd: spendLimitMicroUsdArg },
|
|
1065
|
+
{ maxRetries: 0 },
|
|
1066
|
+
);
|
|
1067
|
+
this.notice = answerNotice(answer);
|
|
1068
|
+
if (answer.state === 'done') this.spendLimitDraft = null;
|
|
1069
|
+
await this.refresh();
|
|
1070
|
+
});
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
/**
|
|
1074
|
+
* Adds the API key the dialog holds. The accounts service asks the provider whether it accepts the key before it holds
|
|
1075
|
+
* anything, so the dialog's fields stay locked while it does; added, the dialog closes and the screen says the spend
|
|
1076
|
+
* limit the account actually has -- a key added again keeps its earlier one unless this add named one. A refusal stays
|
|
1077
|
+
* in the dialog, and nothing is sent again on its own.
|
|
1078
|
+
*/
|
|
1079
|
+
private async addApiKey(providerIdArg: interfaces.TControllerApiKeyProvider): Promise<void> {
|
|
1080
|
+
const field = this.shadowRoot?.querySelector<plugins.deesCatalog.DeesInputText>(
|
|
1081
|
+
'dialog[data-dialog="apiKey"] dees-input-text[data-api-key]');
|
|
1082
|
+
const apiKey = String(field?.value ?? '').trim();
|
|
1083
|
+
const label = this.apiKeyName.trim();
|
|
1084
|
+
const limit = this.apiKeyLimit.trim();
|
|
1085
|
+
if (apiKeyReason(apiKey !== '', label, limit) !== '') return;
|
|
1086
|
+
const spendLimitMicroUsd = limit ? plugins.authswitchSpend.authSwitchParseUsd(limit) : null;
|
|
1087
|
+
await this.runInDialog(`apikey:${providerIdArg}`, async () => {
|
|
1088
|
+
const { answer } = await this.client!.fire<interfaces.IReq_ControllerAccountsApiKeyAdd>(
|
|
1089
|
+
'controller.accounts.apikey.add',
|
|
1090
|
+
{ providerId: providerIdArg, apiKey, ...(label ? { label } : {}),
|
|
1091
|
+
...(spendLimitMicroUsd === null ? {} : { spendLimitMicroUsd }) },
|
|
1092
|
+
{ timeoutMs: apiKeyAddTimeoutMs, maxRetries: 0 },
|
|
1093
|
+
);
|
|
1094
|
+
this.notice = answer.state === 'done' ? apiKeyAddedNotice(answer.value.account) : answerNotice(answer);
|
|
1095
|
+
if (answer.state === 'done') await this.refresh();
|
|
1096
|
+
return answer.state === 'done';
|
|
1097
|
+
});
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1002
1100
|
/** Lifts the account's reserve for the hours the dialog holds. An uncertain send is never retried. */
|
|
1003
1101
|
private async liftReserve(rowArg: IAccountRow): Promise<void> {
|
|
1004
1102
|
const hours = this.liftHours;
|
|
@@ -1306,6 +1404,7 @@ export class AglAccounts extends plugins.deesElement.DeesElement {
|
|
|
1306
1404
|
this.openKey = rowArg.key;
|
|
1307
1405
|
this.renaming = false;
|
|
1308
1406
|
this.maxUsageDraft = null;
|
|
1407
|
+
this.spendLimitDraft = null;
|
|
1309
1408
|
}
|
|
1310
1409
|
|
|
1311
1410
|
private renderNotice(noticeArg: IAccountsNotice): plugins.deesElement.TemplateResult {
|
|
@@ -1363,7 +1462,7 @@ export class AglAccounts extends plugins.deesElement.DeesElement {
|
|
|
1363
1462
|
${viewArg.operations.map((operationArg) => this.renderOperation(operationArg))}
|
|
1364
1463
|
${viewArg.claudeSignIns.map((signInArg) => this.renderClaudeSignIn(signInArg))}
|
|
1365
1464
|
${list.groups.map((groupArg) => plugins.deesElement.html`
|
|
1366
|
-
<section class="group" data-provider-group=${groupArg.
|
|
1465
|
+
<section class="group" data-provider-group=${groupArg.id}>
|
|
1367
1466
|
<div class="groupHead"><h3 class="groupTitle">${groupArg.label}</h3><span>${groupArg.rows.length}</span></div>
|
|
1368
1467
|
<div class="card rows" role="list" aria-label=${`${groupArg.label} accounts`}>
|
|
1369
1468
|
${groupArg.rows.map((rowArg) => plugins.deesElement.html`<div role="listitem">${renderListRow(rowArg, open)}</div>`)}
|
|
@@ -1414,8 +1513,9 @@ export class AglAccounts extends plugins.deesElement.DeesElement {
|
|
|
1414
1513
|
<div class="itemText">${loginArg.statusText}</div>
|
|
1415
1514
|
<dl class="facts">
|
|
1416
1515
|
${loginArg.causeText ? plugins.deesElement.html`<dt>Cause</dt><dd>${loginArg.causeText}</dd>` : ''}
|
|
1417
|
-
|
|
1418
|
-
|
|
1516
|
+
${loginArg.renewedBy ? plugins.deesElement.html`<dt>Renewed by</dt><dd>${loginArg.renewedBy}</dd>` : ''}
|
|
1517
|
+
${loginArg.renewalText
|
|
1518
|
+
? plugins.deesElement.html`<dt>Next automatic renewal</dt><dd>${loginArg.renewalText}</dd>` : ''}
|
|
1419
1519
|
<dt>Last checked</dt><dd>${loginArg.observedText}</dd>
|
|
1420
1520
|
</dl>
|
|
1421
1521
|
${this.slots([...(loginArg.reauth ? [loginArg.reauth] : []), ...(loginArg.preuse ? [loginArg.preuse] : [])],
|
|
@@ -1494,12 +1594,64 @@ export class AglAccounts extends plugins.deesElement.DeesElement {
|
|
|
1494
1594
|
`;
|
|
1495
1595
|
}
|
|
1496
1596
|
|
|
1597
|
+
/**
|
|
1598
|
+
* An API key's monthly spend limit in dollars, saved with its own Save, or removed so the key serves without one. A
|
|
1599
|
+
* limit below what the month spent holds the key at its next request.
|
|
1600
|
+
*/
|
|
1601
|
+
private renderSpendLimit(rowArg: IAccountRow, slotArg: IAccountActionSlot): plugins.deesElement.TemplateResult {
|
|
1602
|
+
const saved = rowArg.spendLimitMicroUsd;
|
|
1603
|
+
if (saved === null && this.spendLimitDraft === null) {
|
|
1604
|
+
return plugins.deesElement.html`
|
|
1605
|
+
<div class="item" data-spend-limit>
|
|
1606
|
+
<div>
|
|
1607
|
+
<div class="itemLabel">Monthly spend limit</div>
|
|
1608
|
+
<div class="itemText">Not set: AGL places requests on the key whatever they cost this month. Set a limit to
|
|
1609
|
+
stop at an amount of your choosing.</div>
|
|
1610
|
+
</div>
|
|
1611
|
+
${this.slots([slotArg], () => { this.spendLimitDraft = ''; }, () => 'Set a limit…')}
|
|
1612
|
+
</div>
|
|
1613
|
+
`;
|
|
1614
|
+
}
|
|
1615
|
+
const draft = this.spendLimitDraft ?? spendLimitDraft(saved!);
|
|
1616
|
+
const reason = slotArg.reason || spendLimitReason(draft, saved);
|
|
1617
|
+
return plugins.deesElement.html`
|
|
1618
|
+
<div class="item stacked" data-spend-limit>
|
|
1619
|
+
<div>
|
|
1620
|
+
<div class="itemLabel">Monthly spend limit</div>
|
|
1621
|
+
<div class="itemText">${spendLimitDescription}</div>
|
|
1622
|
+
</div>
|
|
1623
|
+
<div class="inline">
|
|
1624
|
+
<span class="itemText">$</span>
|
|
1625
|
+
<dees-input-text .label=${'Monthly spend limit ($)'} .labelPosition=${'none'} .value=${draft}
|
|
1626
|
+
@input=${(eventArg: Event) => {
|
|
1627
|
+
const target = eventArg.currentTarget as HTMLElement & { value?: string };
|
|
1628
|
+
this.spendLimitDraft = target.value ?? '';
|
|
1629
|
+
}}></dees-input-text>
|
|
1630
|
+
<div>${this.slots(
|
|
1631
|
+
[
|
|
1632
|
+
{ id: 'spendLimit', label: 'Save', icon: 'lucide:Check', reason },
|
|
1633
|
+
...(saved === null ? []
|
|
1634
|
+
: [{ id: 'clearSpendLimit' as const, label: 'No limit', icon: 'lucide:X', reason: slotArg.reason }]),
|
|
1635
|
+
],
|
|
1636
|
+
(runArg) => void this.setSpendLimit(rowArg, runArg.id === 'clearSpendLimit' ? null
|
|
1637
|
+
: plugins.authswitchSpend.authSwitchParseUsd(draft)),
|
|
1638
|
+
)}</div>
|
|
1639
|
+
${saved === null
|
|
1640
|
+
? plugins.deesElement.html`<dees-button size="sm" type="ghost" text="Keep it unset" data-action="keepSpendLimit"
|
|
1641
|
+
@clicked=${() => { this.spendLimitDraft = null; }}></dees-button>`
|
|
1642
|
+
: ''}
|
|
1643
|
+
</div>
|
|
1644
|
+
</div>
|
|
1645
|
+
`;
|
|
1646
|
+
}
|
|
1647
|
+
|
|
1497
1648
|
/** An account's own page: everything the row stands for, each change with its own control, risky ones confirmed. */
|
|
1498
1649
|
private renderPage(rowArg: IAccountRow): plugins.deesElement.TemplateResult {
|
|
1499
1650
|
const slot = (idArg: TAccountActionId) => rowArg.manage.find((slotArg) => slotArg.id === idArg)
|
|
1500
1651
|
?? rowArg.actions.find((slotArg) => slotArg.id === idArg) ?? null;
|
|
1501
1652
|
const renameSlot = slot('rename');
|
|
1502
1653
|
const maxUsageSlot = slot('maxUsage');
|
|
1654
|
+
const spendLimitSlot = slot('spendLimit');
|
|
1503
1655
|
const liftSlot = slot('liftReserve');
|
|
1504
1656
|
const removeSlot = slot('remove');
|
|
1505
1657
|
const useCodexSlot = slot('useCodex');
|
|
@@ -1535,6 +1687,9 @@ export class AglAccounts extends plugins.deesElement.DeesElement {
|
|
|
1535
1687
|
}
|
|
1536
1688
|
};
|
|
1537
1689
|
const confirmLabel = (slotArg: IAccountActionSlot) => `${slotArg.label}…`;
|
|
1690
|
+
// A key's usage is its month's spend, and its one login is the key itself.
|
|
1691
|
+
const usageHead = rowArg.apiKey ? 'Spend' : 'Usage';
|
|
1692
|
+
const signInHead = rowArg.apiKey ? 'Key' : 'Sign-in';
|
|
1538
1693
|
return plugins.deesElement.html`
|
|
1539
1694
|
<section class="page" data-account-page=${rowArg.key} aria-label=${`Account ${rowArg.label}`}>
|
|
1540
1695
|
<button class="back" type="button" data-action="back" @click=${() => { this.openKey = ''; }}>
|
|
@@ -1573,10 +1728,13 @@ export class AglAccounts extends plugins.deesElement.DeesElement {
|
|
|
1573
1728
|
</div></div>
|
|
1574
1729
|
</section>`
|
|
1575
1730
|
: ''}
|
|
1576
|
-
<section class="section" data-usage aria-label=${
|
|
1577
|
-
<div class="sectionHead"><span
|
|
1578
|
-
${renderUsageMeters(rowArg, rowArg.usage?.problemText
|
|
1731
|
+
<section class="section" data-usage aria-label=${`${usageHead} · ${rowArg.usage?.freshness ?? ''}`}>
|
|
1732
|
+
<div class="sectionHead"><span>${usageHead}</span><span>${rowArg.usage?.freshness ?? ''}</span></div>
|
|
1733
|
+
${renderUsageMeters(rowArg, rowArg.usage?.problemText || rowArg.usage?.spend?.unmeteredText
|
|
1579
1734
|
|| rowArg.facts.find((factArg) => factArg.label === 'Usage')?.value || 'No reading yet.')}
|
|
1735
|
+
${rowArg.usage?.spend
|
|
1736
|
+
? plugins.deesElement.html`<p class=${`spend ${rowArg.usage.spend.tone}`} data-spend>${rowArg.usage.spend.text}</p>`
|
|
1737
|
+
: ''}
|
|
1580
1738
|
${rowArg.usage?.problemText && rowArg.usage.meters.length
|
|
1581
1739
|
? plugins.deesElement.html`<p class="reason">${rowArg.usage.problemText}</p>` : ''}
|
|
1582
1740
|
${rowArg.limitStatus
|
|
@@ -1585,6 +1743,12 @@ export class AglAccounts extends plugins.deesElement.DeesElement {
|
|
|
1585
1743
|
: ''}
|
|
1586
1744
|
${usageSlots.length ? this.slots(usageSlots, run) : ''}
|
|
1587
1745
|
</section>
|
|
1746
|
+
${spendLimitSlot
|
|
1747
|
+
? plugins.deesElement.html`<section class="section" data-limits aria-label="Limits">
|
|
1748
|
+
<div class="sectionHead"><span>Limits</span></div>
|
|
1749
|
+
<div class="card">${this.renderSpendLimit(rowArg, this.effectiveSlot(spendLimitSlot))}</div>
|
|
1750
|
+
</section>`
|
|
1751
|
+
: ''}
|
|
1588
1752
|
${maxUsageSlot
|
|
1589
1753
|
? plugins.deesElement.html`<section class="section" data-limits aria-label="Limits">
|
|
1590
1754
|
<div class="sectionHead"><span>Limits</span></div>
|
|
@@ -1623,8 +1787,8 @@ export class AglAccounts extends plugins.deesElement.DeesElement {
|
|
|
1623
1787
|
</section>`
|
|
1624
1788
|
: ''}
|
|
1625
1789
|
${rowArg.logins.length
|
|
1626
|
-
? plugins.deesElement.html`<section class="section" data-signin aria-label
|
|
1627
|
-
<div class="sectionHead"><span
|
|
1790
|
+
? plugins.deesElement.html`<section class="section" data-signin aria-label=${signInHead}>
|
|
1791
|
+
<div class="sectionHead"><span>${signInHead}</span></div>
|
|
1628
1792
|
<div class="card">
|
|
1629
1793
|
${rowArg.logins.map((loginArg) => this.renderLogin(rowArg, loginArg))}
|
|
1630
1794
|
${claudeSignIn ? plugins.deesElement.html`<div class="item">
|
|
@@ -1645,11 +1809,11 @@ export class AglAccounts extends plugins.deesElement.DeesElement {
|
|
|
1645
1809
|
? plugins.deesElement.html`<section class="section" data-remove aria-label="Remove">
|
|
1646
1810
|
<div class="card"><div class="item">
|
|
1647
1811
|
<div>
|
|
1648
|
-
<div class="itemLabel"
|
|
1812
|
+
<div class="itemLabel">${rowArg.apiKey ? 'Remove key' : 'Remove account'}</div>
|
|
1649
1813
|
<div class="itemText">${rowArg.hostCodex
|
|
1650
1814
|
? 'Codex on this host runs on it, so removing hands Codex\'s sign-in to AGL first. Using the account'
|
|
1651
1815
|
+ ' again takes a new sign-in.'
|
|
1652
|
-
: 'AGL stops placing work on it. Using it again takes a new sign-in.'}</div>
|
|
1816
|
+
: rowArg.removeNote || 'AGL stops placing work on it. Using it again takes a new sign-in.'}</div>
|
|
1653
1817
|
</div>
|
|
1654
1818
|
<div>${this.slots([removeSlot], run, () => rowArg.hostCodex ? 'Hand to AGL and remove…' : 'Remove…')}</div>
|
|
1655
1819
|
</div></div>
|
|
@@ -1857,7 +2021,8 @@ export class AglAccounts extends plugins.deesElement.DeesElement {
|
|
|
1857
2021
|
</button>`;
|
|
1858
2022
|
};
|
|
1859
2023
|
return this.dialogShell('add', 'Add account', plugins.deesElement.html`
|
|
1860
|
-
<p class="itemText">AGL places work on the accounts it holds
|
|
2024
|
+
<p class="itemText">AGL places work on the accounts it holds: a subscription with its own sign-in, and an API key
|
|
2025
|
+
once no subscription can take a request.</p>
|
|
1861
2026
|
<div class="sectionHead"><span>Subscription</span></div>
|
|
1862
2027
|
${choice('chatgpt', viewArg.add, 'ChatGPT', 'Sign in on any device with a one-time code',
|
|
1863
2028
|
() => void this.addAccount(), 'openai')}
|
|
@@ -1867,8 +2032,40 @@ export class AglAccounts extends plugins.deesElement.DeesElement {
|
|
|
1867
2032
|
? plugins.deesElement.html`<div class="inline"><span class="itemText">Codex on this host is signed in already?</span>
|
|
1868
2033
|
${this.slots([viewArg.adoptCodex], () => void this.adoptCodexHost())}</div>`
|
|
1869
2034
|
: ''}
|
|
2035
|
+
<div class="sectionHead"><span>API key</span></div>
|
|
2036
|
+
${choice('openai-api-key', viewArg.addApiKey, 'OpenAI API key', 'A key from the OpenAI platform, paid per request',
|
|
2037
|
+
() => this.openDialog({ kind: 'apiKey', providerId: 'openai' }), 'openai')}
|
|
2038
|
+
${choice('anthropic-api-key', viewArg.addApiKey, 'Anthropic API key', 'A key from the Claude Console, paid per request',
|
|
2039
|
+
() => this.openDialog({ kind: 'apiKey', providerId: 'anthropic' }), 'anthropic')}
|
|
1870
2040
|
`, this.dialogFoot('Cancel', null));
|
|
1871
2041
|
}
|
|
2042
|
+
case 'apiKey': {
|
|
2043
|
+
const provider = apiKeyProviderName(dialog.providerId);
|
|
2044
|
+
const running = this.busyAction === `apikey:${dialog.providerId}`;
|
|
2045
|
+
const value = (eventArg: Event): string => String(
|
|
2046
|
+
(eventArg.currentTarget as HTMLElement & { value?: unknown }).value ?? '');
|
|
2047
|
+
return this.dialogShell('apiKey', `Add ${provider} API key`, plugins.deesElement.html`
|
|
2048
|
+
<p class="itemText">An account of its own, which AGL places requests on at API prices once no subscription can take
|
|
2049
|
+
them. ${provider} is asked whether it accepts the key before anything is kept.</p>
|
|
2050
|
+
<dees-input-text data-api-key .label=${'API key'} .isPasswordBool=${true} .autocomplete=${'off'}
|
|
2051
|
+
.maxLength=${controllerApiKeyMaxLength} .disabled=${running}
|
|
2052
|
+
.description=${'Sent to the accounts service, which keeps it sealed and never shows it again.'}
|
|
2053
|
+
@input=${(eventArg: Event) => { this.apiKeyTyped = value(eventArg).trim() !== ''; }}></dees-input-text>
|
|
2054
|
+
<dees-input-text data-api-key-name .label=${'Name (optional)'} .value=${this.apiKeyName} .disabled=${running}
|
|
2055
|
+
@input=${(eventArg: Event) => { this.apiKeyName = value(eventArg); }}></dees-input-text>
|
|
2056
|
+
<dees-input-text data-api-key-limit .label=${'Monthly spend limit in dollars (optional)'} .value=${this.apiKeyLimit}
|
|
2057
|
+
.description=${spendLimitDescription} .disabled=${running}
|
|
2058
|
+
@input=${(eventArg: Event) => { this.apiKeyLimit = value(eventArg); }}></dees-input-text>
|
|
2059
|
+
${running
|
|
2060
|
+
? plugins.deesElement.html`<dees-hint data-api-key-progress .type=${'info'}
|
|
2061
|
+
.text=${`Asking ${provider} whether it accepts this key…`}></dees-hint>`
|
|
2062
|
+
: ''}
|
|
2063
|
+
`, this.dialogFoot('Cancel', {
|
|
2064
|
+
label: 'Add key', type: 'accent',
|
|
2065
|
+
reason: apiKeyReason(this.apiKeyTyped, this.apiKeyName, this.apiKeyLimit) || busy(viewArg.addApiKey),
|
|
2066
|
+
run: () => void this.addApiKey(dialog.providerId),
|
|
2067
|
+
}));
|
|
2068
|
+
}
|
|
1872
2069
|
case 'signIn': {
|
|
1873
2070
|
const operation = [...viewArg.operations, ...rows.flatMap((rowArg) => rowArg.operations)]
|
|
1874
2071
|
.find((operationArg) => operationArg.id === dialog.operationId);
|
|
@@ -1914,10 +2111,12 @@ export class AglAccounts extends plugins.deesElement.DeesElement {
|
|
|
1914
2111
|
const running = this.busyAction === `codex-release:${row.accountId}`;
|
|
1915
2112
|
const question = asQuestion(handOver
|
|
1916
2113
|
? remove ? codexReleaseRemoveConfirmText(row.label) : codexReleaseConfirmText(row.label)
|
|
1917
|
-
: row.assigned
|
|
2114
|
+
: `${row.assigned
|
|
1918
2115
|
? `Remove ${row.label}? It is in use by ${row.assignedSummary}. AGL cannot stop those from here, so the`
|
|
1919
2116
|
+ ' removal is refused while they use it.'
|
|
1920
|
-
: `Remove ${row.label}? Nothing uses it
|
|
2117
|
+
: row.apiKey ? `Remove ${row.label}? Nothing uses it.`
|
|
2118
|
+
: `Remove ${row.label}? Nothing uses it. Once removed, using it again takes a new sign-in.`}`
|
|
2119
|
+
+ (row.removeNote ? ` ${row.removeNote}` : ''),
|
|
1921
2120
|
remove ? `Remove ${row.label}?` : `Hand Codex's sign-in of ${row.label} to AGL?`);
|
|
1922
2121
|
return this.dialogShell(dialog.kind, question.title,
|
|
1923
2122
|
plugins.deesElement.html`
|
|
@@ -33,8 +33,10 @@ const connectionChoices = (
|
|
|
33
33
|
};
|
|
34
34
|
const profiles = profilesArg.filter(profile => profile.state === 'active');
|
|
35
35
|
const withProfile = new Set(profiles.flatMap(profile => profile.accountId ? [profile.accountId] : []));
|
|
36
|
+
// A ChatGPT account; an OpenAI API key account (tenancy `api-key`) is no sign-in Codex runs on here.
|
|
36
37
|
const accounts = (authorityArg?.state === 'ready' ? authorityArg.snapshot?.accounts ?? [] : [])
|
|
37
|
-
.filter(account => account.providerId === 'openai' &&
|
|
38
|
+
.filter(account => account.providerId === 'openai' && account.organizationId !== 'api-key' && !account.removed
|
|
39
|
+
&& !withProfile.has(account.id));
|
|
38
40
|
return [
|
|
39
41
|
...profiles.map(profile => ({ value: profile.id, name: profile.name, mode: profile.mode,
|
|
40
42
|
reason: profile.accountId ? accountReason(profile.accountId) : '' })),
|
package/ts_web/plugins.ts
CHANGED
|
@@ -9,8 +9,9 @@ import * as mpCatalog from '@modelprofile.com/catalog';
|
|
|
9
9
|
|
|
10
10
|
import * as simpleWebAuthnBrowser from '@simplewebauthn/browser';
|
|
11
11
|
|
|
12
|
-
// authswitch's time and preuse sentences, taken from where the Node-loadable read
|
|
13
|
-
|
|
12
|
+
// authswitch's time and preuse sentences and an API key's dollar amounts, taken from where the Node-loadable read
|
|
13
|
+
// models take them.
|
|
14
|
+
import { authswitchPreuseText, authswitchSpend } from './accounts.plugins.js';
|
|
14
15
|
|
|
15
16
|
// Bundled statically: the CSP allows no CDN scripts, so the catalog's lib
|
|
16
17
|
// loader is fed these modules instead of fetching them at runtime. The
|
|
@@ -59,6 +60,7 @@ import type {
|
|
|
59
60
|
|
|
60
61
|
export {
|
|
61
62
|
authswitchPreuseText,
|
|
63
|
+
authswitchSpend,
|
|
62
64
|
deesCatalog,
|
|
63
65
|
deesComms,
|
|
64
66
|
deesElement,
|