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.
Files changed (149) hide show
  1. package/changelog.md +27 -0
  2. package/dist_serve/bundle.js +2228 -2177
  3. package/dist_serve/bundle.js.map +2 -2
  4. package/dist_ts/00_commitinfo_data.js +1 -1
  5. package/dist_ts/aglapi/aglapisurface.document.js +2 -2
  6. package/dist_ts/classes.accountsactions.d.ts +11 -1
  7. package/dist_ts/classes.accountsactions.js +59 -2
  8. package/dist_ts/classes.controller.js +5 -5
  9. package/dist_ts/classes.flexaccountaccess.js +3 -2
  10. package/dist_ts/classes.harnessreleases.d.ts +14 -4
  11. package/dist_ts/classes.harnessreleases.js +28 -4
  12. package/dist_ts/classes.inferencegateway.d.ts +28 -3
  13. package/dist_ts/classes.inferencegateway.js +177 -61
  14. package/dist_ts/classes.inferenceplacement.d.ts +5 -3
  15. package/dist_ts/classes.inferenceplacement.js +2 -2
  16. package/dist_ts/classes.inferencerouter.d.ts +349 -91
  17. package/dist_ts/classes.inferencerouter.js +804 -297
  18. package/dist_ts/classes.inferenceservice.d.ts +14 -2
  19. package/dist_ts/classes.inferenceservice.js +53 -14
  20. package/dist_ts/classes.inferencespend.d.ts +339 -0
  21. package/dist_ts/classes.inferencespend.js +815 -0
  22. package/dist_ts/classes.inferenceusagecache.d.ts +8 -0
  23. package/dist_ts/classes.inferenceusagecache.js +9 -5
  24. package/dist_ts/classes.inferenceusagetap.d.ts +50 -9
  25. package/dist_ts/classes.inferenceusagetap.js +185 -31
  26. package/dist_ts/classes.proxycontrol.d.ts +3 -2
  27. package/dist_ts/classes.proxycontrol.js +1 -2
  28. package/dist_ts/classes.proxydaemon.js +3 -2
  29. package/dist_ts/classes.proxystore.d.ts +6 -0
  30. package/dist_ts/classes.proxystore.js +11 -2
  31. package/dist_ts/fleet/classes.claudeworkers.d.ts +5 -4
  32. package/dist_ts/fleet/classes.claudeworkers.js +10 -9
  33. package/dist_ts/functions.claudecoderelease.d.ts +12 -6
  34. package/dist_ts/functions.claudecoderelease.js +13 -7
  35. package/dist_ts/functions.inferencecli.js +12 -8
  36. package/dist_ts/interfaces.auth.d.ts +1 -1
  37. package/dist_ts/interfaces.auth.js +3 -1
  38. package/dist_ts/plugins.d.ts +3 -1
  39. package/dist_ts/plugins.js +7 -2
  40. package/dist_ts_authswitch/authority-apikeys.d.ts +32 -0
  41. package/dist_ts_authswitch/authority-apikeys.js +65 -0
  42. package/dist_ts_authswitch/authority-contract.d.ts +142 -28
  43. package/dist_ts_authswitch/authority-contract.js +48 -1
  44. package/dist_ts_authswitch/authority-overview.js +8 -1
  45. package/dist_ts_authswitch/authority-placement.d.ts +7 -2
  46. package/dist_ts_authswitch/authority-placement.js +10 -3
  47. package/dist_ts_authswitch/authority-runtime-contract.d.ts +2 -2
  48. package/dist_ts_authswitch/authority-spend.d.ts +103 -0
  49. package/dist_ts_authswitch/authority-spend.js +110 -0
  50. package/dist_ts_authswitch/authority-subscription.d.ts +12 -1
  51. package/dist_ts_authswitch/authority-subscription.js +24 -1
  52. package/dist_ts_authswitch/authority-usagelimit.d.ts +7 -4
  53. package/dist_ts_authswitch/authority-usagelimit.js +89 -39
  54. package/dist_ts_authswitch/authority-usagewindows.d.ts +4 -1
  55. package/dist_ts_authswitch/authority-usagewindows.js +41 -16
  56. package/dist_ts_authswitch/classes.authoritybroker.d.ts +30 -19
  57. package/dist_ts_authswitch/classes.authoritybroker.js +152 -61
  58. package/dist_ts_authswitch/classes.authoritycli.d.ts +9 -4
  59. package/dist_ts_authswitch/classes.authoritycli.js +94 -29
  60. package/dist_ts_authswitch/classes.authorityclient.d.ts +24 -7
  61. package/dist_ts_authswitch/classes.authorityclient.js +31 -11
  62. package/dist_ts_authswitch/classes.authoritydaemon.js +39 -12
  63. package/dist_ts_authswitch/classes.authoritydatabase.d.ts +28 -5
  64. package/dist_ts_authswitch/classes.authoritydatabase.js +171 -9
  65. package/dist_ts_authswitch/classes.authorityimport.js +2 -2
  66. package/dist_ts_authswitch/classes.authoritymodels.d.ts +71 -3
  67. package/dist_ts_authswitch/classes.authoritymodels.js +249 -20
  68. package/dist_ts_authswitch/classes.authorityregistry.js +4 -2
  69. package/dist_ts_authswitch/classes.authorityusage.d.ts +69 -7
  70. package/dist_ts_authswitch/classes.authorityusage.js +118 -4
  71. package/dist_ts_authswitch/classes.claudenative.d.ts +16 -7
  72. package/dist_ts_authswitch/classes.claudenative.js +29 -16
  73. package/dist_ts_authswitch/classes.claudesignin.js +5 -2
  74. package/dist_ts_authswitch/testsupport.d.ts +24 -5
  75. package/dist_ts_authswitch/testsupport.js +56 -12
  76. package/dist_ts_authswitch/ts_migration/0011_account_api_key_fields.d.ts +12 -0
  77. package/dist_ts_authswitch/ts_migration/0011_account_api_key_fields.js +21 -0
  78. package/dist_ts_authswitch/ts_migration/index.js +3 -1
  79. package/dist_ts_interfaces/functions.authorityscope.d.ts +18 -7
  80. package/dist_ts_interfaces/functions.authorityscope.js +35 -8
  81. package/dist_ts_interfaces/interfaces.d.ts +1 -1
  82. package/dist_ts_interfaces/interfaces.js +1 -1
  83. package/dist_ts_interfaces/plugins.d.ts +2 -1
  84. package/dist_ts_interfaces/plugins.js +7 -4
  85. package/dist_ts_interfaces/requests.accounts.d.ts +56 -0
  86. package/dist_ts_interfaces/requests.accounts.js +11 -1
  87. package/dist_ts_interfaces/requests.d.ts +2 -2
  88. package/dist_ts_interfaces/requests.js +1 -1
  89. package/docs/authswitch.md +75 -12
  90. package/package.json +5 -5
  91. package/readme.md +198 -21
  92. package/ts/00_commitinfo_data.ts +1 -1
  93. package/ts/aglapi/aglapisurface.document.ts +2 -2
  94. package/ts/classes.accountsactions.ts +75 -1
  95. package/ts/classes.controller.ts +4 -4
  96. package/ts/classes.flexaccountaccess.ts +2 -1
  97. package/ts/classes.harnessreleases.ts +25 -4
  98. package/ts/classes.inferencegateway.ts +184 -64
  99. package/ts/classes.inferenceplacement.ts +6 -4
  100. package/ts/classes.inferencerouter.ts +1026 -314
  101. package/ts/classes.inferenceservice.ts +56 -13
  102. package/ts/classes.inferencespend.ts +870 -0
  103. package/ts/classes.inferenceusagecache.ts +16 -4
  104. package/ts/classes.inferenceusagetap.ts +159 -34
  105. package/ts/classes.proxycontrol.ts +3 -3
  106. package/ts/classes.proxydaemon.ts +2 -1
  107. package/ts/classes.proxystore.ts +11 -1
  108. package/ts/fleet/classes.claudeworkers.ts +9 -8
  109. package/ts/functions.claudecoderelease.ts +13 -6
  110. package/ts/functions.inferencecli.ts +11 -7
  111. package/ts/interfaces.auth.ts +2 -0
  112. package/ts/plugins.ts +7 -0
  113. package/ts_authswitch/authority-apikeys.ts +85 -0
  114. package/ts_authswitch/authority-contract.ts +167 -29
  115. package/ts_authswitch/authority-overview.ts +7 -0
  116. package/ts_authswitch/authority-placement.ts +12 -4
  117. package/ts_authswitch/authority-runtime-contract.ts +2 -2
  118. package/ts_authswitch/authority-spend.ts +168 -0
  119. package/ts_authswitch/authority-subscription.ts +28 -1
  120. package/ts_authswitch/authority-usagelimit.ts +95 -40
  121. package/ts_authswitch/authority-usagewindows.ts +42 -17
  122. package/ts_authswitch/classes.authoritybroker.ts +161 -68
  123. package/ts_authswitch/classes.authoritycli.ts +108 -32
  124. package/ts_authswitch/classes.authorityclient.ts +41 -10
  125. package/ts_authswitch/classes.authoritydaemon.ts +43 -13
  126. package/ts_authswitch/classes.authoritydatabase.ts +178 -8
  127. package/ts_authswitch/classes.authorityimport.ts +1 -1
  128. package/ts_authswitch/classes.authoritymodels.ts +165 -19
  129. package/ts_authswitch/classes.authorityregistry.ts +3 -0
  130. package/ts_authswitch/classes.authorityusage.ts +171 -10
  131. package/ts_authswitch/classes.claudenative.ts +28 -15
  132. package/ts_authswitch/classes.claudesignin.ts +4 -1
  133. package/ts_authswitch/native-contracts.md +81 -20
  134. package/ts_authswitch/readme.md +1 -1
  135. package/ts_authswitch/testsupport.ts +58 -11
  136. package/ts_authswitch/ts_migration/0011_account_api_key_fields.ts +20 -0
  137. package/ts_authswitch/ts_migration/index.ts +2 -0
  138. package/ts_interfaces/functions.authorityscope.ts +43 -8
  139. package/ts_interfaces/interfaces.ts +1 -1
  140. package/ts_interfaces/plugins.ts +6 -3
  141. package/ts_interfaces/requests.accounts.ts +48 -0
  142. package/ts_interfaces/requests.ts +4 -0
  143. package/ts_web/00_commitinfo_data.ts +1 -1
  144. package/ts_web/accounts.plugins.ts +3 -1
  145. package/ts_web/accounts.templates.ts +20 -11
  146. package/ts_web/classes.accountsreadmodel.ts +322 -70
  147. package/ts_web/elements.accounts.ts +219 -20
  148. package/ts_web/elements.codexconnections.ts +3 -1
  149. package/ts_web/plugins.ts +4 -2
@@ -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, TAuthSwitchUsageReading } from './classes.authorityusage.js';
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`, whose own imports carry
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, or the provider refuses
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
- const threshold = authSwitchEffectiveMaxUsage(maxUsagePercentArg, reserveLiftUntilArg, nowArg) ?? 100;
78
- const windows = authSwitchCountedWindows(readingArg, maxUsagePercentArg);
79
- const crossing = windows.filter((windowArg) => authSwitchUsageWindowPercent(windowArg) >= threshold);
80
- const resets = crossing.map((windowArg) => windowArg.resetAt);
81
- let refused: boolean;
82
- if (readingArg.kind === 'openai') {
83
- const details = readingArg.rateLimits.rateLimit;
84
- const mainWindows = [details?.primaryWindow, details?.secondaryWindow]
85
- .filter((windowArg) => windowArg !== undefined);
86
- // The refusal is the used-up windows' where the reading states any, else the whole limit's; once each of those
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. A missing reading has none.
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
- if (readingArg.kind === 'openai') {
64
- return [
65
- ...openAiWindows(null, readingArg.rateLimits.rateLimit),
66
- ...readingArg.rateLimits.additionalRateLimits.flatMap((limitArg) => (
67
- openAiWindows(limitArg.limitName, limitArg.rateLimit)
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
  };