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,4 +1,5 @@
|
|
|
1
1
|
import * as plugins from './plugins.js';
|
|
2
|
+
import { authSwitchApiKeyVerifier } from './authority-apikeys.js';
|
|
2
3
|
import { AuthSwitchAuthorityDaemon, type IAuthSwitchAuthorityDaemonOptions } from './classes.authoritydaemon.js';
|
|
3
4
|
import { AuthSwitchOpenAiProvider } from './classes.openaiprovider.js';
|
|
4
5
|
import { ClaudeAccountStatus } from './classes.claudestatus.js';
|
|
@@ -107,9 +108,9 @@ export interface IAuthSwitchFixtureUsage {
|
|
|
107
108
|
}
|
|
108
109
|
|
|
109
110
|
/**
|
|
110
|
-
* Anthropic's profile, usage and token endpoints for exactly the fixture tokens it knows,
|
|
111
|
-
* fixture API keys it issued, and ChatGPT's `wham/usage` for the fixture ChatGPT logins; every
|
|
112
|
-
* an error, so nothing a test starts reaches a real provider.
|
|
111
|
+
* Anthropic's profile, usage and token endpoints for exactly the fixture tokens it knows, Anthropic's and OpenAI's
|
|
112
|
+
* model lists for the fixture API keys it issued, and ChatGPT's `wham/usage` for the fixture ChatGPT logins; every
|
|
113
|
+
* other request fails with an error, so nothing a test starts reaches a real provider.
|
|
113
114
|
*/
|
|
114
115
|
export class AuthSwitchFakeClaudeProvider {
|
|
115
116
|
/** Access token -> the account name it belongs to. */
|
|
@@ -138,6 +139,27 @@ export class AuthSwitchFakeClaudeProvider {
|
|
|
138
139
|
return apiKey;
|
|
139
140
|
}
|
|
140
141
|
|
|
142
|
+
/**
|
|
143
|
+
* The OpenAI API keys OpenAI accepts -> the account name each was issued for, and whether it may read the model list
|
|
144
|
+
* (a restricted key without that permission may not); delete one to revoke it.
|
|
145
|
+
*/
|
|
146
|
+
public readonly openAiApiKeys = new Map<string, { name: string; modelRead: boolean }>();
|
|
147
|
+
/** While true, OpenAI's model list answers 503. */
|
|
148
|
+
public openAiModelsDown = false;
|
|
149
|
+
public openAiModelListCalls = 0;
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Issues `name`'s OpenAI API key, which OpenAI accepts from then on, and answers it: `sk-proj-fixture-<name>`, a
|
|
153
|
+
* project key's prefix, so the authority tells it from an Anthropic key as it would a real one. `modelRead: false`
|
|
154
|
+
* issues a restricted key that may not read the model list.
|
|
155
|
+
*/
|
|
156
|
+
public issueOpenAiApiKey(name: string, options: { modelRead?: boolean } = {}): string {
|
|
157
|
+
assertFixtureName(name);
|
|
158
|
+
const apiKey = `sk-proj-fixture-${name}`;
|
|
159
|
+
this.openAiApiKeys.set(apiKey, { name, modelRead: options.modelRead ?? true });
|
|
160
|
+
return apiKey;
|
|
161
|
+
}
|
|
162
|
+
|
|
141
163
|
/** Accepts the access token of `name`'s login at `generation`, and its refresh token for the next generation. */
|
|
142
164
|
public know(name: string, generation: number): void {
|
|
143
165
|
assertFixtureName(name);
|
|
@@ -168,6 +190,33 @@ export class AuthSwitchFakeClaudeProvider {
|
|
|
168
190
|
}
|
|
169
191
|
return Response.json({ data: [], has_more: false });
|
|
170
192
|
}
|
|
193
|
+
if (url === 'https://api.openai.com/v1/models') {
|
|
194
|
+
// The model list of an OpenAI API key, which the authority reads to learn whether OpenAI accepts a key.
|
|
195
|
+
this.openAiModelListCalls++;
|
|
196
|
+
if (this.openAiModelsDown) {
|
|
197
|
+
return Response.json({ error: { message: 'overloaded', type: 'server_error' } }, { status: 503 });
|
|
198
|
+
}
|
|
199
|
+
const headers = new Headers(init?.headers);
|
|
200
|
+
// Only the request the authority's verifier sends is a fixture request: anything else is answered as malformed.
|
|
201
|
+
if ((init?.method ?? 'GET') !== 'GET' || init?.redirect !== 'error' || headers.has('x-api-key')
|
|
202
|
+
|| headers.has('openai-organization') || headers.has('openai-project')) {
|
|
203
|
+
return Response.json({ error: { message: 'not a fixture request', type: 'invalid_request_error' } },
|
|
204
|
+
{ status: 400 });
|
|
205
|
+
}
|
|
206
|
+
const presented = (headers.get('authorization') ?? '').replace(/^Bearer /, '');
|
|
207
|
+
const issued = this.openAiApiKeys.get(presented);
|
|
208
|
+
if (!issued) {
|
|
209
|
+
// OpenAI quotes the key it refused (masked); the fake quotes it whole, so a test proves nothing repeats it.
|
|
210
|
+
return Response.json({ error: { message: `Incorrect API key provided: ${presented}`,
|
|
211
|
+
type: 'invalid_request_error', param: null, code: 'invalid_api_key' } }, { status: 401 });
|
|
212
|
+
}
|
|
213
|
+
if (!issued.modelRead) {
|
|
214
|
+
return Response.json({ error: { message: 'You have insufficient permissions for this operation. Missing scopes:'
|
|
215
|
+
+ ' api.model.read.', type: 'invalid_request_error', param: null, code: null } }, { status: 403 });
|
|
216
|
+
}
|
|
217
|
+
return Response.json({ object: 'list', data: [{ id: 'gpt-fixture', object: 'model', created: 1_700_000_000,
|
|
218
|
+
owned_by: 'openai' }] });
|
|
219
|
+
}
|
|
171
220
|
if (url === 'https://api.anthropic.com/api/oauth/usage') {
|
|
172
221
|
const token = (new Headers(init?.headers).get('Authorization') ?? '').replace(/^Bearer /, '');
|
|
173
222
|
const name = this.accessTokens.get(token);
|
|
@@ -395,7 +444,7 @@ export const authSwitchSeedClaudeAccount = async (daemon: AuthSwitchAuthorityDae
|
|
|
395
444
|
} else {
|
|
396
445
|
const account: IStoredAuthorityAccount = { id: accountId, providerId: 'anthropic', issuer: 'anthropic',
|
|
397
446
|
subject: identity.accountUuid, workspaceId: identity.organizationUuid, label: name,
|
|
398
|
-
email: identity.emailAddress, plan: 'max', primaryGrantId: loginId, maxUsagePercent: null, reserveLiftUntil: null, lapsesAt: null, removed: false, revision: 1,
|
|
447
|
+
email: identity.emailAddress, plan: 'max', primaryGrantId: loginId, maxUsagePercent: null, reserveLiftUntil: null, lapsesAt: null, spendLimitMicroUsd: null, keyHint: null, removed: false, revision: 1,
|
|
399
448
|
statusObservedAt: now, updateId };
|
|
400
449
|
await database.changeAccountAndGrant(updateId, accountId, loginId, () => ({ account, grant }));
|
|
401
450
|
}
|
|
@@ -418,8 +467,8 @@ export const authSwitchSeedClaudeAccount = async (daemon: AuthSwitchAuthorityDae
|
|
|
418
467
|
* It serves the management and runtime sockets exactly as the installed authority does, with the Claude native
|
|
419
468
|
* authority on a fixture host home, container homes confined to `homesDirectory` and Claude sign-ins made in
|
|
420
469
|
* `claudeSignInDirectory`. Its OpenAI provider, its
|
|
421
|
-
* Claude usage reads and its checks of an Anthropic API key go to the fake provider, which refuses everything
|
|
422
|
-
* not a fixture request, and no Claude Code process is ever seen running on a home.
|
|
470
|
+
* Claude usage reads and its checks of an Anthropic or OpenAI API key go to the fake provider, which refuses everything
|
|
471
|
+
* that is not a fixture request, and no Claude Code process is ever seen running on a home.
|
|
423
472
|
*/
|
|
424
473
|
export class AuthSwitchTestAuthority {
|
|
425
474
|
private stopping: Promise<void> | null = null;
|
|
@@ -468,9 +517,7 @@ export class AuthSwitchTestAuthority {
|
|
|
468
517
|
dataDirectory: plugins.path.join(directory, 'db'), socketPath: databaseSocketPath,
|
|
469
518
|
authoritySocketPath, runtimeSocketPath,
|
|
470
519
|
broker: { provider: new AuthSwitchOpenAiProvider({ fetch: provider.fetch }), secrets: plainCodec, proactive: false,
|
|
471
|
-
|
|
472
|
-
await plugins.flexAnthropic.listAnthropicApiKeyModels({ apiKey }, { signal, fetch: provider.fetch });
|
|
473
|
-
} },
|
|
520
|
+
verifyApiKey: authSwitchApiKeyVerifier(provider.fetch) },
|
|
474
521
|
...claude.options,
|
|
475
522
|
codexNative: { home: new CodexNativeHome({ codexHome: codexHost.codexHome, executable: codexHost.binary,
|
|
476
523
|
scope: codexHost.scope }), secrets: plainCodec },
|
|
@@ -663,7 +710,7 @@ export class AuthSwitchTestAuthority {
|
|
|
663
710
|
await database.changeAccountAndGrant(updateId, account.accountId, account.loginId, () => ({
|
|
664
711
|
account: { id: account.accountId, providerId: 'anthropic', issuer: 'anthropic', subject: identity.accountUuid,
|
|
665
712
|
workspaceId: identity.organizationUuid, label: name, email: identity.emailAddress, plan: 'max',
|
|
666
|
-
primaryGrantId: account.loginId, maxUsagePercent: null, reserveLiftUntil: null, lapsesAt: null, removed: false, revision: 1, statusObservedAt: now, updateId },
|
|
713
|
+
primaryGrantId: account.loginId, maxUsagePercent: null, reserveLiftUntil: null, lapsesAt: null, spendLimitMicroUsd: null, keyHint: null, removed: false, revision: 1, statusObservedAt: now, updateId },
|
|
667
714
|
grant: { id: account.loginId, accountId: account.accountId, providerId: 'anthropic', purpose: 'claude_host_native',
|
|
668
715
|
audience: 'claude-code-host', state: 'native', owner: 'claude_native', grantGeneration: 1, authorizationGeneration: 1,
|
|
669
716
|
revision: 1, ciphertext: null, accessExpiresAt: null, attemptId: null, retryAt: null, retryCount: 0,
|
|
@@ -701,7 +748,7 @@ export class AuthSwitchTestAuthority {
|
|
|
701
748
|
const identity = authSwitchFixtureOpenAiIdentity(name);
|
|
702
749
|
return { id: accountId, providerId: 'openai', issuer: 'openai', subject: identity.userId,
|
|
703
750
|
workspaceId: identity.chatgptAccountId, label: name, email: identity.email, plan: 'pro', primaryGrantId: loginId,
|
|
704
|
-
maxUsagePercent: null, reserveLiftUntil: null, lapsesAt: null, removed: false, revision: 1, statusObservedAt: now, updateId };
|
|
751
|
+
maxUsagePercent: null, reserveLiftUntil: null, lapsesAt: null, spendLimitMicroUsd: null, keyHint: null, removed: false, revision: 1, statusObservedAt: now, updateId };
|
|
705
752
|
}
|
|
706
753
|
|
|
707
754
|
/**
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import * as plugins from '../plugins.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Gives every account recorded before API keys carried a spend limit the two members the account record now states.
|
|
5
|
+
*
|
|
6
|
+
* An API key account may carry a monthly spend limit (`spendLimitMicroUsd`) and names how its key ends (`keyHint`);
|
|
7
|
+
* the record states each, or `null` for none. Every account recorded earlier has neither: it gains
|
|
8
|
+
* `spendLimitMicroUsd: null` and `keyHint: null` -- an Anthropic key held from before keeps no hint, since its key is
|
|
9
|
+
* sealed -- and nothing else about it changes, neither its revision nor its update id, since no one changed the
|
|
10
|
+
* account. A record that already carries a member keeps it exactly as it is, which is what makes an interrupted run
|
|
11
|
+
* safe to repeat.
|
|
12
|
+
*/
|
|
13
|
+
export const addAccountApiKeyFields = async (db: plugins.nosqldb.SmartdataDb): Promise<void> => {
|
|
14
|
+
const accounts = db.mongoDb.collection('authswitch_authority_accounts');
|
|
15
|
+
for (const member of ['spendLimitMicroUsd', 'keyHint'] as const) {
|
|
16
|
+
const missing = { [member]: { $exists: false } };
|
|
17
|
+
if (!await accounts.findOne(missing)) continue;
|
|
18
|
+
await accounts.updateMany(missing, { $set: { [member]: null } });
|
|
19
|
+
}
|
|
20
|
+
};
|
|
@@ -9,6 +9,7 @@ import { addAccountMaxUsage } from './0007_account_max_usage.js';
|
|
|
9
9
|
import { addAccountLapse } from './0008_account_lapse.js';
|
|
10
10
|
import { addAccountReserveLift } from './0009_account_reserve_lift.js';
|
|
11
11
|
import { prepareCodexHostRelease } from './0010_codex_host_release.js';
|
|
12
|
+
import { addAccountApiKeyFields } from './0011_account_api_key_fields.js';
|
|
12
13
|
|
|
13
14
|
/**
|
|
14
15
|
* Runs on every start of an existing store, in this fixed order. Each module recognises the published shapes it
|
|
@@ -25,4 +26,5 @@ export const runAuthorityMigrations = async (db: plugins.nosqldb.SmartdataDb): P
|
|
|
25
26
|
await addAccountLapse(db);
|
|
26
27
|
await addAccountReserveLift(db);
|
|
27
28
|
await prepareCodexHostRelease(db);
|
|
29
|
+
await addAccountApiKeyFields(db);
|
|
28
30
|
};
|
|
@@ -147,10 +147,10 @@ export const findAuthorityDaemonLogin = (
|
|
|
147
147
|
};
|
|
148
148
|
|
|
149
149
|
/**
|
|
150
|
-
* The login AGL's inference proxy binds for an account, and its purpose, like `findAuthorityDaemonLogin`:
|
|
151
|
-
*
|
|
152
|
-
* sign-in (`claude_host_native`), the one that can back a binding
|
|
153
|
-
* (`anthropic_api_key`).
|
|
150
|
+
* The login AGL's inference proxy binds for an account, and its purpose, like `findAuthorityDaemonLogin`: an OpenAI
|
|
151
|
+
* account's daemon-held login -- a ChatGPT sign-in (`openai_managed`) or an API key (`openai_api_key`) -- or an
|
|
152
|
+
* Anthropic account's daemon-held login -- a Claude sign-in (`claude_host_native`), the one that can back a binding
|
|
153
|
+
* when the account holds several, or an API key (`anthropic_api_key`).
|
|
154
154
|
*/
|
|
155
155
|
export const findAuthorityGatewayLogin = (
|
|
156
156
|
projectionArg: IControllerAuthorityProjection,
|
|
@@ -159,17 +159,37 @@ export const findAuthorityGatewayLogin = (
|
|
|
159
159
|
const snapshot = projectionArg.snapshot;
|
|
160
160
|
const account = snapshot?.accounts.find((accountArg) => accountArg.id === accountIdArg);
|
|
161
161
|
if (!snapshot || !account || account.removed) return 'no_account';
|
|
162
|
-
if (account.providerId !== 'anthropic')
|
|
162
|
+
if (account.providerId !== 'anthropic') {
|
|
163
|
+
const managed = findAuthorityDaemonLogin(projectionArg, accountIdArg);
|
|
164
|
+
return managed !== 'no_login' ? managed : snapshot.logins.find((loginArg) => loginArg.accountId === accountIdArg
|
|
165
|
+
&& loginArg.purpose === 'openai_api_key' && loginArg.owner === 'daemon') ?? 'no_login';
|
|
166
|
+
}
|
|
163
167
|
const logins = snapshot.logins.filter((loginArg) => loginArg.accountId === accountIdArg
|
|
164
168
|
&& (loginArg.purpose === 'claude_host_native' || loginArg.purpose === 'anthropic_api_key')
|
|
165
169
|
&& loginArg.owner === 'daemon');
|
|
166
170
|
return logins.find((loginArg) => loginArg.capabilities.canBind) ?? logins[0] ?? 'no_login';
|
|
167
171
|
};
|
|
168
172
|
|
|
173
|
+
/**
|
|
174
|
+
* The login an API key's answers are reported on (`authswitch.authority.usage.spend`): the key's live gateway login, or
|
|
175
|
+
* once the key was removed the login it had, as the projection publishes removed accounts and their logins too; the
|
|
176
|
+
* accounts service records a removed key's spend. `undefined` when the projection names no API-key login of the account.
|
|
177
|
+
*/
|
|
178
|
+
export const findAuthorityApiKeyLogin = (
|
|
179
|
+
projectionArg: IControllerAuthorityProjection,
|
|
180
|
+
accountIdArg: string,
|
|
181
|
+
): TControllerAccountLogin | undefined => {
|
|
182
|
+
const isKeyLogin = (loginArg: TControllerAccountLogin) => loginArg.accountId === accountIdArg && loginArg.owner === 'daemon'
|
|
183
|
+
&& (loginArg.purpose === 'anthropic_api_key' || loginArg.purpose === 'openai_api_key');
|
|
184
|
+
const live = findAuthorityGatewayLogin(projectionArg, accountIdArg);
|
|
185
|
+
if (typeof live !== 'string') return isKeyLogin(live) ? live : undefined;
|
|
186
|
+
return projectionArg.snapshot?.logins.filter(isKeyLogin).at(-1);
|
|
187
|
+
};
|
|
188
|
+
|
|
169
189
|
/**
|
|
170
190
|
* A pool of AGL's inference proxy: its provider, and what its accounts authenticate with -- a subscription sign-in
|
|
171
|
-
* (a ChatGPT or a Claude account), or an
|
|
172
|
-
* Claude models only on the API-key pool.
|
|
191
|
+
* (a ChatGPT or a Claude account), or an API key of the provider. Claude Code runs on the Claude subscription pool,
|
|
192
|
+
* Flex's Claude models only on the Anthropic API-key pool.
|
|
173
193
|
*/
|
|
174
194
|
export interface IInferencePool {
|
|
175
195
|
provider: 'openai' | 'anthropic';
|
|
@@ -179,7 +199,7 @@ export interface IInferencePool {
|
|
|
179
199
|
/**
|
|
180
200
|
* The pool of AGL's inference proxy an account serves in, or `undefined` for none: an account whose gateway login
|
|
181
201
|
* (`findAuthorityGatewayLogin`) the authority holds ready, can bind and keeps in no tool's home -- a ChatGPT account
|
|
182
|
-
* this host's Codex does not run on, a Claude account, or an Anthropic API key.
|
|
202
|
+
* this host's Codex does not run on, a Claude account, or an Anthropic or OpenAI API key.
|
|
183
203
|
*/
|
|
184
204
|
export const findInferencePool = (
|
|
185
205
|
projectionArg: IControllerAuthorityProjection,
|
|
@@ -197,6 +217,9 @@ export const findInferencePool = (
|
|
|
197
217
|
if (account.providerId === 'anthropic' && login.purpose === 'anthropic_api_key') {
|
|
198
218
|
return { provider: 'anthropic', credential: 'api_key' };
|
|
199
219
|
}
|
|
220
|
+
if (account.providerId === 'openai' && login.purpose === 'openai_api_key') {
|
|
221
|
+
return { provider: 'openai', credential: 'api_key' };
|
|
222
|
+
}
|
|
200
223
|
return undefined;
|
|
201
224
|
};
|
|
202
225
|
|
|
@@ -230,6 +253,18 @@ export const isFlexAuthorityConnectionId = (valueArg: unknown): valueArg is stri
|
|
|
230
253
|
*/
|
|
231
254
|
export const flexClaudePoolConnectionId = 'flex:proxy:anthropic';
|
|
232
255
|
|
|
256
|
+
/**
|
|
257
|
+
* Whether an account serves in the Claude pool Flex's Claude models run on: an Anthropic API key in the proxy's
|
|
258
|
+
* Anthropic API-key pool (`findInferencePool`). An OpenAI key serves the OpenAI API-key pool, which Flex does not use.
|
|
259
|
+
*/
|
|
260
|
+
export const isFlexClaudePoolMember = (
|
|
261
|
+
projectionArg: IControllerAuthorityProjection,
|
|
262
|
+
accountIdArg: string,
|
|
263
|
+
): boolean => {
|
|
264
|
+
const pool = findInferencePool(projectionArg, accountIdArg);
|
|
265
|
+
return pool?.provider === 'anthropic' && pool.credential === 'api_key';
|
|
266
|
+
};
|
|
267
|
+
|
|
233
268
|
/** Whether a value is a Flex connection id: an authority account's, or the Claude pool's. */
|
|
234
269
|
export const isFlexConnectionId = (valueArg: unknown): valueArg is string =>
|
|
235
270
|
valueArg === flexClaudePoolConnectionId || isFlexAuthorityConnectionId(valueArg);
|
|
@@ -5,7 +5,7 @@ import type {
|
|
|
5
5
|
TControllerBrowserPaintAvailability,
|
|
6
6
|
TControllerBrowserPaintTermination,
|
|
7
7
|
} from './browserpaint.js';
|
|
8
|
-
export const controllerProtocolVersion =
|
|
8
|
+
export const controllerProtocolVersion = 44 as const;
|
|
9
9
|
export const controllerUpgradeManagementVersion = 2 as const;
|
|
10
10
|
export const controllerPackageName = 'agl' as const;
|
|
11
11
|
/** The systemd user unit `agl service install` writes; a controller it runs reports it in its status. */
|
package/ts_interfaces/plugins.ts
CHANGED
|
@@ -2,9 +2,12 @@ import * as typedrequestInterfaces from '@api.global/typedrequest-interfaces';
|
|
|
2
2
|
import type * as browserRuntime from '@push.rocks/smartbrowser/runtime';
|
|
3
3
|
// The account-management contract on its own. Its own doc comment declares it credential-free and
|
|
4
4
|
// safe for browser code, while the package root is Node-only, so the browser-facing interfaces take
|
|
5
|
-
// the subpath rather than the root export.
|
|
6
|
-
// publishes -- the account label rule -- are taken from it as values rather than
|
|
5
|
+
// the subpath rather than the root export. Besides types it imports only the model id rule, itself a
|
|
6
|
+
// leaf, so the rules it publishes -- the account label rule -- are taken from it as values rather than
|
|
7
|
+
// restated.
|
|
7
8
|
import * as authswitchAuthority from '../ts_authswitch/authority-contract.js';
|
|
9
|
+
// An API key's monthly spend: the spend limit rule and dollar amounts, a leaf that imports nothing.
|
|
10
|
+
import * as authswitchSpend from '../ts_authswitch/authority-spend.js';
|
|
8
11
|
import type {
|
|
9
12
|
AuthenticationResponseJSON,
|
|
10
13
|
PublicKeyCredentialCreationOptionsJSON,
|
|
@@ -14,7 +17,7 @@ import type {
|
|
|
14
17
|
|
|
15
18
|
export { typedrequestInterfaces };
|
|
16
19
|
export type { browserRuntime };
|
|
17
|
-
export { authswitchAuthority };
|
|
20
|
+
export { authswitchAuthority, authswitchSpend };
|
|
18
21
|
|
|
19
22
|
export type {
|
|
20
23
|
AuthenticationResponseJSON,
|
|
@@ -97,6 +97,8 @@ export type TControllerAccountNativeAssignment = plugins.authswitchAuthority.IAu
|
|
|
97
97
|
export type TControllerAccountOperation = plugins.authswitchAuthority.IAuthSwitchOperation;
|
|
98
98
|
export type TControllerAccountPreuse = plugins.authswitchAuthority.IAuthSwitchPreuseOperation;
|
|
99
99
|
export type TControllerAccountUsage = plugins.authswitchAuthority.IAuthSwitchUsageSnapshot;
|
|
100
|
+
/** An API key's month, the reading of an API key account's usage (`kind: 'spend'`). */
|
|
101
|
+
export type TControllerAccountSpendReading = plugins.authswitchAuthority.IAuthSwitchSpendReading;
|
|
100
102
|
/** A re-check of a Claude login: the usage held once its read finished, and how that read went. */
|
|
101
103
|
export type TControllerAccountUsageRecheck = plugins.authswitchAuthority.IAuthSwitchUsageRecheck;
|
|
102
104
|
export type TControllerAccountsDoctorPage = plugins.authswitchAuthority.IAuthSwitchDoctorPage;
|
|
@@ -187,6 +189,52 @@ export interface IReq_ControllerAccountsMaxUsage extends plugins.typedrequestInt
|
|
|
187
189
|
response: { answer: TControllerAccountsAnswer<TControllerAccount> };
|
|
188
190
|
}
|
|
189
191
|
|
|
192
|
+
/** The providers whose API keys the Accounts screen adds as accounts. */
|
|
193
|
+
export type TControllerApiKeyProvider = plugins.authswitchAuthority.TAuthSwitchApiKeyProvider;
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* The spend limit rule, as the accounts service publishes and applies it: a whole number of cents from
|
|
197
|
+
* `controllerSpendLimitRange.min` to `.max`, in micro-dollars. A view checks a value with it before it offers the
|
|
198
|
+
* change, and the service refuses any other value with `invalid_input`.
|
|
199
|
+
*/
|
|
200
|
+
export const isControllerSpendLimitMicroUsd = plugins.authswitchSpend.isAuthSwitchSpendLimitMicroUsd;
|
|
201
|
+
/** The range of a spend limit, in micro-dollars. */
|
|
202
|
+
export const controllerSpendLimitRange = plugins.authswitchSpend.authSwitchSpendLimitRange;
|
|
203
|
+
|
|
204
|
+
/** The longest API key the accounts service holds, in characters; a longer value is refused before it is sent. */
|
|
205
|
+
export const controllerApiKeyMaxLength = 512;
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Adds an Anthropic or OpenAI API key as an account, with an optional label and monthly spend limit
|
|
209
|
+
* (`isControllerSpendLimitMicroUsd`). The accounts service asks the provider whether it accepts the key before it holds
|
|
210
|
+
* it, and answers why when it does not (`api_key_unverified`, `invalid_input`). The key is never answered, logged or
|
|
211
|
+
* audited; the account names it only by how it ends (`keyHint`). The answer arrives only once the controller's own
|
|
212
|
+
* projection shows the account; it can take up to 35 seconds, the provider's answer included.
|
|
213
|
+
*/
|
|
214
|
+
export interface IReq_ControllerAccountsApiKeyAdd extends plugins.typedrequestInterfaces.implementsTR<
|
|
215
|
+
plugins.typedrequestInterfaces.ITypedRequest,
|
|
216
|
+
IReq_ControllerAccountsApiKeyAdd
|
|
217
|
+
> {
|
|
218
|
+
method: 'controller.accounts.apikey.add';
|
|
219
|
+
request: { providerId: TControllerApiKeyProvider; apiKey: string; label?: string; spendLimitMicroUsd?: number };
|
|
220
|
+
response: { answer: TControllerAccountsAnswer<{ account: TControllerAccount; loginId: string }> };
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Sets an API key account's monthly spend limit, or clears it with `null`: at the limit AGL's inference proxy places
|
|
225
|
+
* nothing more on the key until the month ends (UTC). `expectedRevision` is the revision the view showed, so a change of
|
|
226
|
+
* an account that changed meanwhile is refused as `account_changed`, and a value that breaks the rule, or an account on
|
|
227
|
+
* a sign-in, as `invalid_input`, both by the service. The answer arrives only once the projection shows the result.
|
|
228
|
+
*/
|
|
229
|
+
export interface IReq_ControllerAccountsSpendLimit extends plugins.typedrequestInterfaces.implementsTR<
|
|
230
|
+
plugins.typedrequestInterfaces.ITypedRequest,
|
|
231
|
+
IReq_ControllerAccountsSpendLimit
|
|
232
|
+
> {
|
|
233
|
+
method: 'controller.accounts.spendlimit';
|
|
234
|
+
request: { accountId: string; expectedRevision: number; spendLimitMicroUsd: number | null };
|
|
235
|
+
response: { answer: TControllerAccountsAnswer<TControllerAccount> };
|
|
236
|
+
}
|
|
237
|
+
|
|
190
238
|
/** Duration validation shared with the authority; no implicit duration is supplied. */
|
|
191
239
|
export const isControllerReserveLiftHours = plugins.authswitchAuthority.isAuthSwitchReserveLiftHours;
|
|
192
240
|
|
|
@@ -29,6 +29,8 @@ import type {
|
|
|
29
29
|
IReq_ControllerAccountsRename,
|
|
30
30
|
IReq_ControllerAccountsMaxUsage,
|
|
31
31
|
IReq_ControllerAccountsReserveLift,
|
|
32
|
+
IReq_ControllerAccountsApiKeyAdd,
|
|
33
|
+
IReq_ControllerAccountsSpendLimit,
|
|
32
34
|
IReq_ControllerAccountsSnapshot,
|
|
33
35
|
IReq_ControllerAccountsUsageGet,
|
|
34
36
|
IReq_ControllerAccountsUsageRecheck,
|
|
@@ -399,6 +401,8 @@ export type TControllerRequest =
|
|
|
399
401
|
| IReq_ControllerAccountsRename
|
|
400
402
|
| IReq_ControllerAccountsMaxUsage
|
|
401
403
|
| IReq_ControllerAccountsReserveLift
|
|
404
|
+
| IReq_ControllerAccountsApiKeyAdd
|
|
405
|
+
| IReq_ControllerAccountsSpendLimit
|
|
402
406
|
| IReq_ControllerAccountsRemove
|
|
403
407
|
| IReq_ControllerAccountsAdd
|
|
404
408
|
| IReq_ControllerAccountsReauth
|
|
@@ -11,5 +11,7 @@ import * as authswitchUsageWindows from '../ts_authswitch/authority-usagewindows
|
|
|
11
11
|
import * as authswitchUsageLimit from '../ts_authswitch/authority-usagelimit.js';
|
|
12
12
|
// A Claude account's subscription facts and payment problem, in the words the authswitch command line uses; types only behind it.
|
|
13
13
|
import * as authswitchSubscription from '../ts_authswitch/authority-subscription.js';
|
|
14
|
+
// An API key's month: the spend limit rule and dollar amounts, as the command line states them; it imports nothing.
|
|
15
|
+
import * as authswitchSpend from '../ts_authswitch/authority-spend.js';
|
|
14
16
|
|
|
15
|
-
export { authswitchPreuseText, authswitchSubscription, authswitchUsageLimit, authswitchUsageWindows };
|
|
17
|
+
export { authswitchPreuseText, authswitchSpend, authswitchSubscription, authswitchUsageLimit, authswitchUsageWindows };
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import * as plugins from './plugins.js';
|
|
2
|
-
import
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
2
|
+
import {
|
|
3
|
+
spendAttentionPercent,
|
|
4
|
+
type IAccountActionSlot,
|
|
5
|
+
type IAccountFact,
|
|
6
|
+
type IAccountRow,
|
|
7
|
+
type IAccountsSummary,
|
|
8
|
+
type TAccountsTone,
|
|
8
9
|
} from './classes.accountsreadmodel.js';
|
|
9
10
|
|
|
10
11
|
/**
|
|
@@ -95,7 +96,10 @@ export const renderSlots = (
|
|
|
95
96
|
`;
|
|
96
97
|
};
|
|
97
98
|
|
|
98
|
-
/**
|
|
99
|
+
/**
|
|
100
|
+
* An account as one line of the list: mark, name and who uses it, its tightest window -- a key's month, which without a
|
|
101
|
+
* limit has its spend and no bar -- and its one status.
|
|
102
|
+
*/
|
|
99
103
|
export const renderListRow = (
|
|
100
104
|
rowArg: IAccountRow,
|
|
101
105
|
openArg: (rowArg: IAccountRow) => void,
|
|
@@ -113,8 +117,9 @@ export const renderListRow = (
|
|
|
113
117
|
${meter
|
|
114
118
|
? plugins.deesElement.html`
|
|
115
119
|
<span class="meterText"><span>${meter.label}</span><span class="meterValue">${meter.valueText}</span></span>
|
|
116
|
-
|
|
117
|
-
.
|
|
120
|
+
${meter.percent === null ? '' : plugins.deesElement.html`<dees-meter .size=${'sm'} .value=${meter.percent}
|
|
121
|
+
.threshold=${meter.threshold} .tone=${meterTone(meter.tone)} .label=${meter.label}
|
|
122
|
+
.valueText=${meter.valueText}></dees-meter>`}`
|
|
118
123
|
: plugins.deesElement.html`<span class="meterText"><span>No reading</span><span class="meterValue">—</span></span>`}
|
|
119
124
|
</span>
|
|
120
125
|
<dees-badge data-status .type=${badgeType(rowArg.pill.tone)} .text=${rowArg.pill.text}></dees-badge>
|
|
@@ -144,17 +149,21 @@ export const renderSummary = (summaryArg: IAccountsSummary): plugins.deesElement
|
|
|
144
149
|
</div>
|
|
145
150
|
`;
|
|
146
151
|
|
|
147
|
-
/**
|
|
152
|
+
/**
|
|
153
|
+
* An account's usage windows, each with its reset, ticked at its maximum; or why there is no reading. A key's month
|
|
154
|
+
* wants attention from `spendAttentionPercent` of its limit, with no tick: a key has no maximum usage.
|
|
155
|
+
*/
|
|
148
156
|
export const renderUsageMeters = (rowArg: IAccountRow, emptyArg: string): plugins.deesElement.TemplateResult => {
|
|
149
157
|
const meters = rowArg.usage?.meters ?? [];
|
|
150
158
|
const threshold = rowArg.maxUsagePercent !== null && rowArg.maxUsagePercent < 100 ? rowArg.maxUsagePercent : null;
|
|
159
|
+
const attentionAt = rowArg.apiKey ? spendAttentionPercent : threshold;
|
|
151
160
|
return plugins.deesElement.html`
|
|
152
161
|
<mp-usage-meters class=${rowArg.usage?.current === false ? 'stale' : ''} data-usage-meters
|
|
153
162
|
.windows=${meters.map((meterArg) => ({
|
|
154
163
|
label: meterArg.label,
|
|
155
164
|
percent: meterArg.percent,
|
|
156
165
|
resetText: meterArg.resetText,
|
|
157
|
-
tone: meterTone(meterArg.percent >= 100 ? 'neutral' :
|
|
166
|
+
tone: meterTone(meterArg.percent >= 100 ? 'neutral' : attentionAt !== null && meterArg.percent >= attentionAt
|
|
158
167
|
? 'attention' : 'positive'),
|
|
159
168
|
}))}
|
|
160
169
|
.threshold=${threshold}
|