agl 40.4.0 → 40.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/changelog.md +33 -0
- package/dist_serve/bundle.js +2228 -2177
- package/dist_serve/bundle.js.map +2 -2
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/aglapi/aglapisurface.document.js +2 -2
- package/dist_ts/classes.accountsactions.d.ts +11 -1
- package/dist_ts/classes.accountsactions.js +59 -2
- package/dist_ts/classes.controller.js +5 -5
- package/dist_ts/classes.flexaccountaccess.js +3 -2
- package/dist_ts/classes.harnessreleases.d.ts +14 -4
- package/dist_ts/classes.harnessreleases.js +28 -4
- package/dist_ts/classes.inferencegateway.d.ts +28 -3
- package/dist_ts/classes.inferencegateway.js +177 -61
- package/dist_ts/classes.inferenceplacement.d.ts +5 -3
- package/dist_ts/classes.inferenceplacement.js +2 -2
- package/dist_ts/classes.inferencerouter.d.ts +349 -91
- package/dist_ts/classes.inferencerouter.js +804 -297
- package/dist_ts/classes.inferenceservice.d.ts +14 -2
- package/dist_ts/classes.inferenceservice.js +53 -14
- package/dist_ts/classes.inferencespend.d.ts +339 -0
- package/dist_ts/classes.inferencespend.js +815 -0
- package/dist_ts/classes.inferenceusagecache.d.ts +8 -0
- package/dist_ts/classes.inferenceusagecache.js +9 -5
- package/dist_ts/classes.inferenceusagetap.d.ts +50 -9
- package/dist_ts/classes.inferenceusagetap.js +185 -31
- package/dist_ts/classes.proxycontrol.d.ts +3 -2
- package/dist_ts/classes.proxycontrol.js +1 -2
- package/dist_ts/classes.proxydaemon.js +3 -2
- package/dist_ts/classes.proxystore.d.ts +6 -0
- package/dist_ts/classes.proxystore.js +11 -2
- package/dist_ts/fleet/classes.claudeworkers.d.ts +5 -4
- package/dist_ts/fleet/classes.claudeworkers.js +10 -9
- package/dist_ts/functions.claudecoderelease.d.ts +12 -6
- package/dist_ts/functions.claudecoderelease.js +13 -7
- package/dist_ts/functions.inferencecli.js +12 -8
- package/dist_ts/interfaces.auth.d.ts +1 -1
- package/dist_ts/interfaces.auth.js +3 -1
- package/dist_ts/plugins.d.ts +3 -1
- package/dist_ts/plugins.js +7 -2
- package/dist_ts_authswitch/authority-apikeys.d.ts +32 -0
- package/dist_ts_authswitch/authority-apikeys.js +65 -0
- package/dist_ts_authswitch/authority-contract.d.ts +172 -33
- package/dist_ts_authswitch/authority-contract.js +54 -1
- package/dist_ts_authswitch/authority-overview.js +8 -1
- package/dist_ts_authswitch/authority-placement.d.ts +7 -2
- package/dist_ts_authswitch/authority-placement.js +10 -3
- package/dist_ts_authswitch/authority-runtime-contract.d.ts +2 -2
- package/dist_ts_authswitch/authority-spend.d.ts +103 -0
- package/dist_ts_authswitch/authority-spend.js +110 -0
- package/dist_ts_authswitch/authority-subscription.d.ts +12 -1
- package/dist_ts_authswitch/authority-subscription.js +24 -1
- package/dist_ts_authswitch/authority-usagelimit.d.ts +7 -4
- package/dist_ts_authswitch/authority-usagelimit.js +89 -39
- package/dist_ts_authswitch/authority-usagewindows.d.ts +4 -1
- package/dist_ts_authswitch/authority-usagewindows.js +41 -16
- package/dist_ts_authswitch/classes.authoritybroker.d.ts +30 -19
- package/dist_ts_authswitch/classes.authoritybroker.js +152 -61
- package/dist_ts_authswitch/classes.authoritycli.d.ts +9 -4
- package/dist_ts_authswitch/classes.authoritycli.js +99 -30
- package/dist_ts_authswitch/classes.authorityclient.d.ts +24 -7
- package/dist_ts_authswitch/classes.authorityclient.js +45 -15
- package/dist_ts_authswitch/classes.authoritydaemon.js +39 -12
- package/dist_ts_authswitch/classes.authoritydatabase.d.ts +28 -5
- package/dist_ts_authswitch/classes.authoritydatabase.js +171 -9
- package/dist_ts_authswitch/classes.authorityimport.js +2 -2
- package/dist_ts_authswitch/classes.authoritymodels.d.ts +71 -3
- package/dist_ts_authswitch/classes.authoritymodels.js +249 -20
- package/dist_ts_authswitch/classes.authorityregistry.js +4 -2
- package/dist_ts_authswitch/classes.authorityusage.d.ts +69 -7
- package/dist_ts_authswitch/classes.authorityusage.js +118 -4
- package/dist_ts_authswitch/classes.claudenative.d.ts +23 -8
- package/dist_ts_authswitch/classes.claudenative.js +49 -21
- package/dist_ts_authswitch/classes.claudesignin.d.ts +14 -2
- package/dist_ts_authswitch/classes.claudesignin.js +72 -23
- package/dist_ts_authswitch/classes.codexmanaged.d.ts +8 -0
- package/dist_ts_authswitch/classes.codexmanaged.js +33 -18
- package/dist_ts_authswitch/classes.harnessprocesses.d.ts +8 -0
- package/dist_ts_authswitch/classes.harnessprocesses.js +41 -9
- package/dist_ts_authswitch/testsupport.d.ts +24 -5
- package/dist_ts_authswitch/testsupport.js +56 -12
- package/dist_ts_authswitch/ts_migration/0011_account_api_key_fields.d.ts +12 -0
- package/dist_ts_authswitch/ts_migration/0011_account_api_key_fields.js +21 -0
- package/dist_ts_authswitch/ts_migration/index.js +3 -1
- package/dist_ts_interfaces/functions.authorityscope.d.ts +18 -7
- package/dist_ts_interfaces/functions.authorityscope.js +35 -8
- package/dist_ts_interfaces/interfaces.d.ts +1 -1
- package/dist_ts_interfaces/interfaces.js +1 -1
- package/dist_ts_interfaces/plugins.d.ts +2 -1
- package/dist_ts_interfaces/plugins.js +7 -4
- package/dist_ts_interfaces/requests.accounts.d.ts +56 -0
- package/dist_ts_interfaces/requests.accounts.js +11 -1
- package/dist_ts_interfaces/requests.d.ts +2 -2
- package/dist_ts_interfaces/requests.js +1 -1
- package/docs/authswitch.md +75 -12
- package/package.json +5 -5
- package/readme.md +199 -22
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/aglapi/aglapisurface.document.ts +2 -2
- package/ts/classes.accountsactions.ts +75 -1
- package/ts/classes.controller.ts +4 -4
- package/ts/classes.flexaccountaccess.ts +2 -1
- package/ts/classes.harnessreleases.ts +25 -4
- package/ts/classes.inferencegateway.ts +184 -64
- package/ts/classes.inferenceplacement.ts +6 -4
- package/ts/classes.inferencerouter.ts +1026 -314
- package/ts/classes.inferenceservice.ts +56 -13
- package/ts/classes.inferencespend.ts +870 -0
- package/ts/classes.inferenceusagecache.ts +16 -4
- package/ts/classes.inferenceusagetap.ts +159 -34
- package/ts/classes.proxycontrol.ts +3 -3
- package/ts/classes.proxydaemon.ts +2 -1
- package/ts/classes.proxystore.ts +11 -1
- package/ts/fleet/classes.claudeworkers.ts +9 -8
- package/ts/functions.claudecoderelease.ts +13 -6
- package/ts/functions.inferencecli.ts +11 -7
- package/ts/interfaces.auth.ts +2 -0
- package/ts/plugins.ts +7 -0
- package/ts_authswitch/authority-apikeys.ts +85 -0
- package/ts_authswitch/authority-contract.ts +201 -34
- package/ts_authswitch/authority-overview.ts +7 -0
- package/ts_authswitch/authority-placement.ts +12 -4
- package/ts_authswitch/authority-runtime-contract.ts +2 -2
- package/ts_authswitch/authority-spend.ts +168 -0
- package/ts_authswitch/authority-subscription.ts +28 -1
- package/ts_authswitch/authority-usagelimit.ts +95 -40
- package/ts_authswitch/authority-usagewindows.ts +42 -17
- package/ts_authswitch/classes.authoritybroker.ts +161 -68
- package/ts_authswitch/classes.authoritycli.ts +112 -32
- package/ts_authswitch/classes.authorityclient.ts +66 -19
- package/ts_authswitch/classes.authoritydaemon.ts +43 -13
- package/ts_authswitch/classes.authoritydatabase.ts +178 -8
- package/ts_authswitch/classes.authorityimport.ts +1 -1
- package/ts_authswitch/classes.authoritymodels.ts +165 -19
- package/ts_authswitch/classes.authorityregistry.ts +3 -0
- package/ts_authswitch/classes.authorityusage.ts +171 -10
- package/ts_authswitch/classes.claudenative.ts +51 -22
- package/ts_authswitch/classes.claudesignin.ts +74 -24
- package/ts_authswitch/classes.codexmanaged.ts +30 -16
- package/ts_authswitch/classes.harnessprocesses.ts +39 -8
- package/ts_authswitch/native-contracts.md +82 -21
- package/ts_authswitch/readme.md +1 -1
- package/ts_authswitch/testsupport.ts +58 -11
- package/ts_authswitch/ts_migration/0011_account_api_key_fields.ts +20 -0
- package/ts_authswitch/ts_migration/index.ts +2 -0
- package/ts_interfaces/functions.authorityscope.ts +43 -8
- package/ts_interfaces/interfaces.ts +1 -1
- package/ts_interfaces/plugins.ts +6 -3
- package/ts_interfaces/requests.accounts.ts +48 -0
- package/ts_interfaces/requests.ts +4 -0
- package/ts_web/00_commitinfo_data.ts +1 -1
- package/ts_web/accounts.plugins.ts +7 -1
- package/ts_web/accounts.templates.ts +20 -11
- package/ts_web/classes.accountsreadmodel.ts +326 -71
- package/ts_web/elements.accounts.ts +219 -20
- package/ts_web/elements.codexconnections.ts +3 -1
- package/ts_web/plugins.ts +4 -2
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What an API key spent: the calendar month in UTC a key's spend counts in, the reading the accounts service answers
|
|
3
|
+
* for a key's login, the spend limit rule, and how an amount reads in US dollars.
|
|
4
|
+
*
|
|
5
|
+
* Every amount is a whole number of micro-dollars (a millionth of a US dollar), so sums stay exact. The module imports
|
|
6
|
+
* nothing, so it bundles for a browser with nothing behind it.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/** Micro-dollars in one US dollar. */
|
|
10
|
+
export const authSwitchMicroUsdPerUsd = 1_000_000;
|
|
11
|
+
|
|
12
|
+
/** Micro-dollars in one US cent: a spend limit is a whole number of cents. */
|
|
13
|
+
export const authSwitchMicroUsdPerCent = 10_000;
|
|
14
|
+
|
|
15
|
+
/** One calendar month in UTC: the period a key's spend counts in and its limit applies to. */
|
|
16
|
+
export interface IAuthSwitchSpendPeriod {
|
|
17
|
+
/** `YYYY-MM`. */
|
|
18
|
+
key: string;
|
|
19
|
+
/** 00:00 UTC on the month's first day, ISO 8601. */
|
|
20
|
+
start: string;
|
|
21
|
+
/** 00:00 UTC on the next month's first day, ISO 8601: when the month's spend resets. */
|
|
22
|
+
end: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const twoDigits = (valueArg: number): string => String(valueArg).padStart(2, '0');
|
|
26
|
+
|
|
27
|
+
/** The calendar month in UTC that `nowArg` (epoch milliseconds) falls in. */
|
|
28
|
+
export const authSwitchSpendPeriod = (nowArg: number): IAuthSwitchSpendPeriod => {
|
|
29
|
+
const at = new Date(nowArg);
|
|
30
|
+
const year = at.getUTCFullYear();
|
|
31
|
+
if (!Number.isFinite(nowArg) || year < 2000 || year > 9999) throw new RangeError('A spend period needs a time from 2000 to 9999.');
|
|
32
|
+
const month = at.getUTCMonth();
|
|
33
|
+
return {
|
|
34
|
+
key: `${year}-${twoDigits(month + 1)}`,
|
|
35
|
+
start: new Date(Date.UTC(year, month, 1)).toISOString(),
|
|
36
|
+
end: new Date(Date.UTC(year, month + 1, 1)).toISOString(),
|
|
37
|
+
};
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
/** Whether a value is a spend period's key, `YYYY-MM` from 2000-01 to 9999-12. */
|
|
41
|
+
export const isAuthSwitchSpendPeriodKey = (valueArg: unknown): valueArg is string =>
|
|
42
|
+
typeof valueArg === 'string' && /^(?:[2-9]\d{3})-(?:0[1-9]|1[0-2])$/.test(valueArg);
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* An API key's spend in one calendar month, as the accounts service holds it from what AGL's inference proxy reported:
|
|
46
|
+
* the answers the proxy relayed on the key in the month and what each cost by the proxy's price table. It counts AGL's
|
|
47
|
+
* traffic alone; whatever else used the key is not in it.
|
|
48
|
+
*/
|
|
49
|
+
export interface IAuthSwitchSpendReading {
|
|
50
|
+
/** The month, `YYYY-MM` (`IAuthSwitchSpendPeriod.key`). */
|
|
51
|
+
period: string;
|
|
52
|
+
/** 00:00 UTC on the month's first day, ISO 8601. */
|
|
53
|
+
periodStart: string;
|
|
54
|
+
/** 00:00 UTC on the next month's first day, ISO 8601: when the month's spend resets. */
|
|
55
|
+
resetAt: string;
|
|
56
|
+
/** What the month's priced answers cost, in micro-dollars. */
|
|
57
|
+
spentMicroUsd: number;
|
|
58
|
+
/** The key's monthly spend limit when the reading was answered, in micro-dollars; null for none. */
|
|
59
|
+
limitMicroUsd: number | null;
|
|
60
|
+
/** The answers counted in the month, priced or not. */
|
|
61
|
+
answers: number;
|
|
62
|
+
/**
|
|
63
|
+
* The answers of the month the proxy could not price: their cost is unknown, so a key with a limit takes no work
|
|
64
|
+
* until the month ends or its limit is removed.
|
|
65
|
+
*/
|
|
66
|
+
unpricedAnswers: number;
|
|
67
|
+
/** When the month's newest answer ended, ISO 8601; null before the month's first answer. */
|
|
68
|
+
lastAnswerAt: string | null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** A spend limit's range in micro-dollars: one cent to one million dollars. */
|
|
72
|
+
export const authSwitchSpendLimitRange: Readonly<{ min: number; max: number }> = Object.freeze({
|
|
73
|
+
min: authSwitchMicroUsdPerCent,
|
|
74
|
+
max: 1_000_000 * authSwitchMicroUsdPerUsd,
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* The spend limit rule, the one the authority applies to a key's monthly limit: a whole number of cents from
|
|
79
|
+
* `authSwitchSpendLimitRange.min` to `.max`, in micro-dollars. `null` clears a limit. A consumer validates with this
|
|
80
|
+
* rather than restating it; the authority answers any other value with the `invalid_input` refusal.
|
|
81
|
+
*/
|
|
82
|
+
export const isAuthSwitchSpendLimitMicroUsd = (valueArg: unknown): valueArg is number => Number.isSafeInteger(valueArg)
|
|
83
|
+
&& (valueArg as number) >= authSwitchSpendLimitRange.min && (valueArg as number) <= authSwitchSpendLimitRange.max
|
|
84
|
+
&& (valueArg as number) % authSwitchMicroUsdPerCent === 0;
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* A dollar amount as a person types it -- `50`, `12.5`, `$1000.00`, no thousands separator -- in micro-dollars, or
|
|
88
|
+
* `null` when the text is no such amount. It reads at most two decimals: an amount finer than a cent is no amount.
|
|
89
|
+
*/
|
|
90
|
+
export const authSwitchParseUsd = (textArg: string): number | null => {
|
|
91
|
+
const match = /^\$?(\d{1,13})(?:\.(\d{1,2}))?$/.exec(textArg.trim());
|
|
92
|
+
if (!match) return null;
|
|
93
|
+
const microUsd = Number(match[1]) * authSwitchMicroUsdPerUsd
|
|
94
|
+
+ Number((match[2] ?? '').padEnd(2, '0')) * authSwitchMicroUsdPerCent;
|
|
95
|
+
return Number.isSafeInteger(microUsd) ? microUsd : null;
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* An amount in US dollars to the cent, with thousands separators: `$12.40`, `$1,000,000.00`. A part of a cent counts
|
|
100
|
+
* as a whole one, so a spend never reads lower than it is.
|
|
101
|
+
*/
|
|
102
|
+
export const authSwitchUsdText = (microUsdArg: number): string => {
|
|
103
|
+
if (!Number.isSafeInteger(microUsdArg) || microUsdArg < 0) throw new RangeError('An amount is a whole number of micro-dollars.');
|
|
104
|
+
const cents = Math.ceil(microUsdArg / authSwitchMicroUsdPerCent);
|
|
105
|
+
const dollars = String(Math.floor(cents / 100)).replace(/\B(?=(\d{3})+(?!\d))/g, ',');
|
|
106
|
+
return `$${dollars}.${twoDigits(cents % 100)}`;
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
/** The share of its limit a reading's month spent, in percent (above 100 once over it); null without a limit. */
|
|
110
|
+
export const authSwitchSpendPercent = (readingArg: IAuthSwitchSpendReading): number | null =>
|
|
111
|
+
readingArg.limitMicroUsd === null ? null : readingArg.spentMicroUsd * 100 / readingArg.limitMicroUsd;
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Whether a reading's month ended by `nowArg` (epoch milliseconds): its `resetAt` passed. Such a reading says nothing
|
|
115
|
+
* about the next month, which starts with nothing spent; every rule that judges a reading decides this by it.
|
|
116
|
+
*/
|
|
117
|
+
export const authSwitchSpendMonthEnded = (readingArg: IAuthSwitchSpendReading, nowArg: number): boolean =>
|
|
118
|
+
Date.parse(readingArg.resetAt) <= nowArg;
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* An answer AGL's inference proxy relayed on a key whose report the accounts service has not acknowledged yet: when it
|
|
122
|
+
* ended and what it cost.
|
|
123
|
+
*/
|
|
124
|
+
export interface IAuthSwitchSpendPending {
|
|
125
|
+
/** When the answer ended, ISO 8601 UTC: its calendar month in UTC is the one it counts in. */
|
|
126
|
+
endedAt: string;
|
|
127
|
+
/** What the answer cost, in micro-dollars; null when it could not be priced. */
|
|
128
|
+
microUsd: number | null;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* A key's month as the proxy judges it at `nowArg` (epoch milliseconds): the held reading with the answers added whose
|
|
133
|
+
* report the accounts service has not acknowledged yet. Only the month `nowArg` falls in counts. When the held
|
|
134
|
+
* reading's month has ended, that month starts at nothing spent and no answer unpriced, under the reading's limit; of
|
|
135
|
+
* the pending answers only those that ended in it are added, priced and unpriced alike, so an answer from before
|
|
136
|
+
* midnight on the 1st counts in the month that ended, never in the new one. The reading's limit is kept: it is the limit
|
|
137
|
+
* the accounts service answered last.
|
|
138
|
+
*/
|
|
139
|
+
export const authSwitchSpendWithPending = (readingArg: IAuthSwitchSpendReading,
|
|
140
|
+
pendingArg: readonly IAuthSwitchSpendPending[], nowArg: number): IAuthSwitchSpendReading => {
|
|
141
|
+
const period = authSwitchSpendPeriod(nowArg);
|
|
142
|
+
const start = Date.parse(period.start);
|
|
143
|
+
const end = Date.parse(period.end);
|
|
144
|
+
const held = readingArg.period === period.key ? readingArg : null;
|
|
145
|
+
let spentMicroUsd = held?.spentMicroUsd ?? 0;
|
|
146
|
+
let answers = held?.answers ?? 0;
|
|
147
|
+
let unpricedAnswers = held?.unpricedAnswers ?? 0;
|
|
148
|
+
let lastAnswerAt = held?.lastAnswerAt ?? null;
|
|
149
|
+
for (const answer of pendingArg) {
|
|
150
|
+
const endedAt = Date.parse(answer.endedAt);
|
|
151
|
+
if (!(endedAt >= start && endedAt < end)) continue;
|
|
152
|
+
answers += 1;
|
|
153
|
+
if (answer.microUsd === null) unpricedAnswers += 1;
|
|
154
|
+
else spentMicroUsd += answer.microUsd;
|
|
155
|
+
if (lastAnswerAt === null || endedAt > Date.parse(lastAnswerAt)) lastAnswerAt = new Date(endedAt).toISOString();
|
|
156
|
+
}
|
|
157
|
+
return { period: period.key, periodStart: period.start, resetAt: period.end, spentMicroUsd,
|
|
158
|
+
limitMicroUsd: readingArg.limitMicroUsd, answers, unpricedAnswers, lastAnswerAt };
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Whether a key's reading keeps it from taking work at `nowArg` (epoch milliseconds): it has a limit, its month has
|
|
163
|
+
* not ended, and the month spent the limit or holds an answer that could not be priced. A key without a limit is never
|
|
164
|
+
* stopped by its spend; once the month ended the reading says nothing about the next one.
|
|
165
|
+
*/
|
|
166
|
+
export const authSwitchSpendExhausted = (readingArg: IAuthSwitchSpendReading, nowArg: number): boolean =>
|
|
167
|
+
readingArg.limitMicroUsd !== null && !authSwitchSpendMonthEnded(readingArg, nowArg)
|
|
168
|
+
&& (readingArg.spentMicroUsd >= readingArg.limitMicroUsd || readingArg.unpricedAnswers > 0);
|
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
* The authswitch command line and AGL's Accounts screen both show these, so their words are written here, once. Besides
|
|
5
5
|
* types, the module imports nothing, and therefore bundles for a browser with nothing behind it.
|
|
6
6
|
*/
|
|
7
|
-
import type { TAuthSwitchRecheckOutcome,
|
|
7
|
+
import type { IAuthSwitchPaymentProblem, TAuthSwitchRecheckOutcome,
|
|
8
|
+
TAuthSwitchUsageReading } from './classes.authorityusage.js';
|
|
8
9
|
|
|
9
10
|
/**
|
|
10
11
|
* What the `subscription_status` values Claude's profile states mean, in plain words, by value. It is empty: no value's
|
|
@@ -54,6 +55,14 @@ export const authSwitchSubscriptionFacts = (readingArg: TAuthSwitchUsageReading
|
|
|
54
55
|
.map((factArg) => ({ label: factArg.label, value: factArg.value }))
|
|
55
56
|
: [];
|
|
56
57
|
|
|
58
|
+
/**
|
|
59
|
+
* Whether a payment problem still holds the account at `nowArg` (epoch milliseconds): every one does until it is
|
|
60
|
+
* cleared, except a `spend_cap`, which holds nothing once its `until` passed. Every reader of a held problem decides by
|
|
61
|
+
* this, the authority included.
|
|
62
|
+
*/
|
|
63
|
+
export const authSwitchPaymentProblemHeld = (problemArg: IAuthSwitchPaymentProblem, nowArg: number): boolean =>
|
|
64
|
+
problemArg.cause !== 'spend_cap' || (problemArg.until !== undefined && Date.parse(problemArg.until) > nowArg);
|
|
65
|
+
|
|
57
66
|
/**
|
|
58
67
|
* What an account's payment problem means, as one sentence (`IAuthSwitchPaymentProblem`); the time it was seen is the
|
|
59
68
|
* caller's to show.
|
|
@@ -62,6 +71,22 @@ export const authSwitchPaymentProblemText = 'Anthropic refused a request on this
|
|
|
62
71
|
+ ' (HTTP 402), so AGL routes nothing to it until a usage read shows its subscription changed or a re-check reads it'
|
|
63
72
|
+ ' healthy.';
|
|
64
73
|
|
|
74
|
+
/**
|
|
75
|
+
* What an API key's payment problem means, as one sentence (`IAuthSwitchPaymentProblem`), by its cause and the key's
|
|
76
|
+
* provider; when it was seen, and when a spend cap resets, are the caller's to show.
|
|
77
|
+
*/
|
|
78
|
+
export const authSwitchApiKeyPaymentProblemText = (problemArg: IAuthSwitchPaymentProblem, providerIdArg: string): string => {
|
|
79
|
+
const provider = providerIdArg === 'openai' ? 'OpenAI' : 'Anthropic';
|
|
80
|
+
switch (problemArg.cause) {
|
|
81
|
+
case undefined: return `${provider} refused a request on this key with a billing error, so AGL routes nothing to it`
|
|
82
|
+
+ ' until it is re-checked.';
|
|
83
|
+
case 'spend_cap': return `${provider} stopped this key at its monthly spend cap, so AGL routes nothing to it until the`
|
|
84
|
+
+ ' cap resets, or until it is re-checked.';
|
|
85
|
+
case 'quota': return `${provider} reports this key's credit or spend limit used up, so AGL routes nothing to it until`
|
|
86
|
+
+ ' it is re-checked.';
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
|
|
65
90
|
/** How a re-check went, as one sentence. */
|
|
66
91
|
export const authSwitchRecheckOutcomeText = (outcomeArg: TAuthSwitchRecheckOutcome): string => {
|
|
67
92
|
switch (outcomeArg) {
|
|
@@ -71,5 +96,7 @@ export const authSwitchRecheckOutcomeText = (outcomeArg: TAuthSwitchRecheckOutco
|
|
|
71
96
|
case 'profile_missing': return 'Claude did not answer the account\'s profile.';
|
|
72
97
|
case 'mismatch': return 'Claude answered with a different account or organization.';
|
|
73
98
|
case 'read_failed': return 'The account could not be read now; the next read is shown with its usage.';
|
|
99
|
+
case 'cleared': return 'The key\'s hold is lifted: AGL places requests on it again, and the provider\'s next answer '
|
|
100
|
+
+ 'shows whether it takes them.';
|
|
74
101
|
}
|
|
75
102
|
};
|
|
@@ -2,17 +2,18 @@
|
|
|
2
2
|
* Whether an account's usage reading still lets it take work: the one rule the inference proxy's placement, Flex's
|
|
3
3
|
* dispatch and resume, the authswitch command line and AGL's Accounts screen all decide by.
|
|
4
4
|
*
|
|
5
|
-
* Besides types, the module imports only the window reader of `authority-usagewindows.ts
|
|
6
|
-
* nothing to a page, so it bundles for a browser with nothing behind it.
|
|
5
|
+
* Besides types, the module imports only the window reader of `authority-usagewindows.ts` and the spend rule of
|
|
6
|
+
* `authority-spend.ts`, whose own imports carry nothing to a page, so it bundles for a browser with nothing behind it.
|
|
7
7
|
*/
|
|
8
|
-
import type { TAuthSwitchUsageReading } from './classes.authorityusage.js';
|
|
8
|
+
import type { TAuthSwitchProviderUsageReading, TAuthSwitchUsageReading } from './classes.authorityusage.js';
|
|
9
9
|
import { authSwitchUsageWindowName, authSwitchUsageWindowPercent, authSwitchUsageWindows,
|
|
10
10
|
type IAuthSwitchUsageWindow } from './authority-usagewindows.js';
|
|
11
|
+
import { authSwitchSpendExhausted, authSwitchSpendMonthEnded } from './authority-spend.js';
|
|
11
12
|
|
|
12
13
|
/**
|
|
13
14
|
* `available`: the account takes work. `reserved`: a counted window reached the account's `maxUsagePercent`, so
|
|
14
|
-
* the rest of it is kept for use outside AGL. `exhausted`: a counted window is used up,
|
|
15
|
-
* requests until a time it names.
|
|
15
|
+
* the rest of it is kept for use outside AGL. `exhausted`: a counted window is used up, the provider refuses
|
|
16
|
+
* requests until a time it names, or an API key reached its monthly spend limit.
|
|
16
17
|
*/
|
|
17
18
|
export type TAuthSwitchLimitState = 'available' | 'reserved' | 'exhausted';
|
|
18
19
|
|
|
@@ -51,6 +52,82 @@ export const authSwitchReserveLiftActive = (untilArg: string | null | undefined,
|
|
|
51
52
|
export const authSwitchEffectiveMaxUsage = (maxArg: number | null, untilArg: string | null | undefined,
|
|
52
53
|
nowArg: number): number | null => maxArg !== null && authSwitchReserveLiftActive(untilArg, nowArg) ? 100 : maxArg;
|
|
53
54
|
|
|
55
|
+
/** The windows' latest reset among those it names, as ISO 8601; null when none names one. */
|
|
56
|
+
const latestReset = (resetsArg: readonly (string | null)[]): string | null => {
|
|
57
|
+
const resetTimes = resetsArg.flatMap((resetArg) => {
|
|
58
|
+
const at = resetArg === null ? NaN : Date.parse(resetArg);
|
|
59
|
+
return Number.isFinite(at) ? [at] : [];
|
|
60
|
+
});
|
|
61
|
+
return resetTimes.length ? new Date(Math.max(...resetTimes)).toISOString() : null;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
/** A subscription sign-in's limit: see `authSwitchUsageLimit`. */
|
|
65
|
+
const subscriptionLimit = (readingArg: TAuthSwitchProviderUsageReading, maxUsagePercentArg: number | null,
|
|
66
|
+
nowArg: number, reserveLiftUntilArg: string | null): IAuthSwitchUsageLimit => {
|
|
67
|
+
const threshold = authSwitchEffectiveMaxUsage(maxUsagePercentArg, reserveLiftUntilArg, nowArg) ?? 100;
|
|
68
|
+
const windows = authSwitchCountedWindows(readingArg, maxUsagePercentArg);
|
|
69
|
+
const crossing = windows.filter((windowArg) => authSwitchUsageWindowPercent(windowArg) >= threshold);
|
|
70
|
+
const resets = crossing.map((windowArg) => windowArg.resetAt);
|
|
71
|
+
let refused: boolean;
|
|
72
|
+
switch (readingArg.kind) {
|
|
73
|
+
case 'openai': {
|
|
74
|
+
const details = readingArg.rateLimits.rateLimit;
|
|
75
|
+
const mainWindows = [details?.primaryWindow, details?.secondaryWindow]
|
|
76
|
+
.filter((windowArg) => windowArg !== undefined);
|
|
77
|
+
// The refusal is the used-up windows' where the reading states any, else the whole limit's; once each of those
|
|
78
|
+
// started a new period past the reset the reading stated, so did the refusal.
|
|
79
|
+
const used = mainWindows.filter((windowArg) => windowArg.usedPercent >= 100);
|
|
80
|
+
const causes = used.length ? used : mainWindows;
|
|
81
|
+
const rolled = causes.length > 0 && causes.every((windowArg) => windowArg.estimated === true);
|
|
82
|
+
refused = details !== undefined && !rolled && (!details.allowed || details.limitReached);
|
|
83
|
+
// A refusal that names no window is lifted by the latest reset of the windows the limit counts.
|
|
84
|
+
if (refused && crossing.length === 0) resets.push(...windows.map((windowArg) => windowArg.resetAt));
|
|
85
|
+
break;
|
|
86
|
+
}
|
|
87
|
+
case 'anthropic': {
|
|
88
|
+
const retryAt = readingArg.status.rateLimit?.retryAt ?? null;
|
|
89
|
+
refused = retryAt !== null && Date.parse(retryAt) > nowArg;
|
|
90
|
+
if (refused) resets.push(retryAt);
|
|
91
|
+
break;
|
|
92
|
+
}
|
|
93
|
+
default: {
|
|
94
|
+
const unhandledReading: never = readingArg;
|
|
95
|
+
return unhandledReading;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
const state: TAuthSwitchLimitState = refused || crossing.some((windowArg) => authSwitchUsageWindowPercent(windowArg) >= 100)
|
|
99
|
+
? 'exhausted' : crossing.length ? 'reserved' : 'available';
|
|
100
|
+
return {
|
|
101
|
+
state,
|
|
102
|
+
threshold,
|
|
103
|
+
headroom: windows.length
|
|
104
|
+
? Math.min(...windows.map((windowArg) => Math.max(0, threshold - authSwitchUsageWindowPercent(windowArg)))) : null,
|
|
105
|
+
crossing,
|
|
106
|
+
resetAt: state !== 'available' ? latestReset(resets) : null,
|
|
107
|
+
};
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* An API key's limit, from its monthly spend (`authSwitchSpendExhausted`): with a spend limit the key is `exhausted`
|
|
112
|
+
* once its month spent the limit or holds an answer that could not be priced, until the month ends; without one it is
|
|
113
|
+
* always `available`. The threshold is always 100, and neither a usage threshold nor a reserve lift applies to a key.
|
|
114
|
+
*/
|
|
115
|
+
const spendLimit = (readingArg: Extract<TAuthSwitchUsageReading, { kind: 'spend' }>, nowArg: number): IAuthSwitchUsageLimit => {
|
|
116
|
+
const windows = authSwitchUsageWindows(readingArg);
|
|
117
|
+
const exhausted = authSwitchSpendExhausted(readingArg.spend, nowArg);
|
|
118
|
+
// A month that ended says nothing about the next one, which starts with nothing spent.
|
|
119
|
+
const ended = authSwitchSpendMonthEnded(readingArg.spend, nowArg);
|
|
120
|
+
return {
|
|
121
|
+
state: exhausted ? 'exhausted' : 'available',
|
|
122
|
+
threshold: 100,
|
|
123
|
+
headroom: windows.length
|
|
124
|
+
? Math.min(...windows.map((windowArg) => ended ? 100 : Math.max(0, 100 - authSwitchUsageWindowPercent(windowArg))))
|
|
125
|
+
: null,
|
|
126
|
+
crossing: exhausted ? windows : [],
|
|
127
|
+
resetAt: exhausted ? readingArg.spend.resetAt : null,
|
|
128
|
+
};
|
|
129
|
+
};
|
|
130
|
+
|
|
54
131
|
/**
|
|
55
132
|
* The limit an account's usage reading states, judged against its `maxUsagePercent` at `nowArg` (epoch
|
|
56
133
|
* milliseconds). An active reserve lift raises only the threshold to 100 until its absolute expiry.
|
|
@@ -69,47 +146,25 @@ export const authSwitchEffectiveMaxUsage = (maxArg: number | null, untilArg: str
|
|
|
69
146
|
* of a Claude reading is the provider refusing inference, as an answer's headers stated it; a refused usage read never
|
|
70
147
|
* sets it.
|
|
71
148
|
*
|
|
149
|
+
* An API key's spend reading is judged by its spend limit alone (`spendLimit`); `maxUsagePercentArg` and
|
|
150
|
+
* `reserveLiftUntilArg` do not apply to it.
|
|
151
|
+
*
|
|
72
152
|
* Whether a reading is recent enough to decide on, and a cooldown after a provider 429, are the caller's: they belong
|
|
73
153
|
* to how it reads usage, not to what a reading says.
|
|
74
154
|
*/
|
|
75
155
|
export const authSwitchUsageLimit = (readingArg: TAuthSwitchUsageReading, maxUsagePercentArg: number | null,
|
|
76
156
|
nowArg: number, reserveLiftUntilArg: string | null = null): IAuthSwitchUsageLimit => {
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
// started a new period past the reset the reading stated, so did the refusal.
|
|
88
|
-
const used = mainWindows.filter((windowArg) => windowArg.usedPercent >= 100);
|
|
89
|
-
const causes = used.length ? used : mainWindows;
|
|
90
|
-
const rolled = causes.length > 0 && causes.every((windowArg) => windowArg.estimated === true);
|
|
91
|
-
refused = details !== undefined && !rolled && (!details.allowed || details.limitReached);
|
|
92
|
-
// A refusal that names no window is lifted by the latest reset of the windows the limit counts.
|
|
93
|
-
if (refused && crossing.length === 0) resets.push(...windows.map((windowArg) => windowArg.resetAt));
|
|
94
|
-
} else {
|
|
95
|
-
const retryAt = readingArg.status.rateLimit?.retryAt ?? null;
|
|
96
|
-
refused = retryAt !== null && Date.parse(retryAt) > nowArg;
|
|
97
|
-
if (refused) resets.push(retryAt);
|
|
157
|
+
switch (readingArg.kind) {
|
|
158
|
+
case 'openai':
|
|
159
|
+
case 'anthropic':
|
|
160
|
+
return subscriptionLimit(readingArg, maxUsagePercentArg, nowArg, reserveLiftUntilArg);
|
|
161
|
+
case 'spend':
|
|
162
|
+
return spendLimit(readingArg, nowArg);
|
|
163
|
+
default: {
|
|
164
|
+
const unhandledReading: never = readingArg;
|
|
165
|
+
return unhandledReading;
|
|
166
|
+
}
|
|
98
167
|
}
|
|
99
|
-
const state: TAuthSwitchLimitState = refused || crossing.some((windowArg) => authSwitchUsageWindowPercent(windowArg) >= 100)
|
|
100
|
-
? 'exhausted' : crossing.length ? 'reserved' : 'available';
|
|
101
|
-
const resetTimes = resets.flatMap((resetArg) => {
|
|
102
|
-
const at = resetArg === null ? NaN : Date.parse(resetArg);
|
|
103
|
-
return Number.isFinite(at) ? [at] : [];
|
|
104
|
-
});
|
|
105
|
-
return {
|
|
106
|
-
state,
|
|
107
|
-
threshold,
|
|
108
|
-
headroom: windows.length
|
|
109
|
-
? Math.min(...windows.map((windowArg) => Math.max(0, threshold - authSwitchUsageWindowPercent(windowArg)))) : null,
|
|
110
|
-
crossing,
|
|
111
|
-
resetAt: state !== 'available' && resetTimes.length ? new Date(Math.max(...resetTimes)).toISOString() : null,
|
|
112
|
-
};
|
|
113
168
|
};
|
|
114
169
|
|
|
115
170
|
const windowNames = (windowsArg: readonly IAuthSwitchUsageWindow[]): string => {
|
|
@@ -54,29 +54,52 @@ const openAiWindows = (limitNameArg: string | null,
|
|
|
54
54
|
}))
|
|
55
55
|
);
|
|
56
56
|
|
|
57
|
+
/** The name of an API key's one window, its spend in the calendar month against its limit. */
|
|
58
|
+
export const authSwitchSpendWindowLabel = 'Monthly spend';
|
|
59
|
+
|
|
57
60
|
/**
|
|
58
61
|
* The windows of a reading: for ChatGPT the main limit's two windows, then each named extra limit's; for Claude the
|
|
59
|
-
* windows as the service reported them
|
|
62
|
+
* windows as the service reported them; for an API key with a spend limit one account window, the month's spend in
|
|
63
|
+
* percent of the limit, which resets when the month ends -- a key without a limit has none. A missing reading has none.
|
|
60
64
|
*/
|
|
61
65
|
export const authSwitchUsageWindows = (readingArg: TAuthSwitchUsageReading | null): IAuthSwitchUsageWindow[] => {
|
|
62
66
|
if (readingArg === null) return [];
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
67
|
+
switch (readingArg.kind) {
|
|
68
|
+
case 'openai':
|
|
69
|
+
return [
|
|
70
|
+
...openAiWindows(null, readingArg.rateLimits.rateLimit),
|
|
71
|
+
...readingArg.rateLimits.additionalRateLimits.flatMap((limitArg) => (
|
|
72
|
+
openAiWindows(limitArg.limitName, limitArg.rateLimit)
|
|
73
|
+
)),
|
|
74
|
+
];
|
|
75
|
+
case 'anthropic':
|
|
76
|
+
return (readingArg.status.windows ?? []).map((windowArg) => ({
|
|
77
|
+
label: windowArg.label,
|
|
78
|
+
limitName: null,
|
|
79
|
+
scope: windowArg.scope,
|
|
80
|
+
durationSeconds: windowArg.durationSeconds,
|
|
81
|
+
usedPercent: windowArg.usedPercent,
|
|
82
|
+
estimatedUsedPercent: windowArg.estimated ? windowArg.estimatedUsedPercent ?? null : null,
|
|
83
|
+
resetAt: windowArg.resetAt,
|
|
84
|
+
}));
|
|
85
|
+
case 'spend': {
|
|
86
|
+
const spend = readingArg.spend;
|
|
87
|
+
if (spend.limitMicroUsd === null) return [];
|
|
88
|
+
return [{
|
|
89
|
+
label: authSwitchSpendWindowLabel,
|
|
90
|
+
limitName: null,
|
|
91
|
+
scope: 'account',
|
|
92
|
+
durationSeconds: (Date.parse(spend.resetAt) - Date.parse(spend.periodStart)) / 1000,
|
|
93
|
+
usedPercent: spend.spentMicroUsd * 100 / spend.limitMicroUsd,
|
|
94
|
+
estimatedUsedPercent: null,
|
|
95
|
+
resetAt: spend.resetAt,
|
|
96
|
+
}];
|
|
97
|
+
}
|
|
98
|
+
default: {
|
|
99
|
+
const unhandledReading: never = readingArg;
|
|
100
|
+
return unhandledReading;
|
|
101
|
+
}
|
|
70
102
|
}
|
|
71
|
-
return (readingArg.status.windows ?? []).map((windowArg) => ({
|
|
72
|
-
label: windowArg.label,
|
|
73
|
-
limitName: null,
|
|
74
|
-
scope: windowArg.scope,
|
|
75
|
-
durationSeconds: windowArg.durationSeconds,
|
|
76
|
-
usedPercent: windowArg.usedPercent,
|
|
77
|
-
estimatedUsedPercent: windowArg.estimated ? windowArg.estimatedUsedPercent ?? null : null,
|
|
78
|
-
resetAt: windowArg.resetAt,
|
|
79
|
-
}));
|
|
80
103
|
};
|
|
81
104
|
|
|
82
105
|
/** The percent a window is used to as far as AGL knows: the estimate between reads, else the reported value. */
|
|
@@ -101,5 +124,7 @@ export const authSwitchUsageProblemText = (problemArg: TAuthSwitchUsageProblem):
|
|
|
101
124
|
case 'expired': return 'The last reading is too old to show.';
|
|
102
125
|
case 'held_in_container': return 'This login is in use in a container, so its usage is not read here.';
|
|
103
126
|
case 'pending': return 'The account\'s usage has not been read yet.';
|
|
127
|
+
case 'spend_unavailable': return 'The key\'s spend could not be read, so AGL places nothing on it while it has a '
|
|
128
|
+
+ 'spend limit.';
|
|
104
129
|
}
|
|
105
130
|
};
|