@bitkyc08/opencodex 2.55.0 → 2.56.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 (167) hide show
  1. package/gui/dist/assets/{index-VuoiWj9J.js → index-D4zuyIxQ.js} +1 -1
  2. package/gui/dist/index.html +1 -1
  3. package/package.json +2 -1
  4. package/src/adapters/base.ts +21 -0
  5. package/src/adapters/cursor/transport-retry.ts +46 -1
  6. package/src/adapters/cursor.ts +4 -0
  7. package/src/adapters/kiro/adapter.ts +42 -1
  8. package/src/adapters/kiro-retry.ts +23 -4
  9. package/src/adapters/openai-chat/errors.ts +116 -0
  10. package/src/adapters/openai-chat/messages.ts +346 -0
  11. package/src/adapters/openai-chat/passthrough.ts +146 -0
  12. package/src/adapters/openai-chat/response-events.ts +117 -0
  13. package/src/adapters/openai-chat/tool-call-validation.ts +200 -0
  14. package/src/adapters/openai-chat/tool-schema.ts +477 -0
  15. package/src/adapters/openai-chat/wire.ts +50 -0
  16. package/src/adapters/openai-chat.ts +33 -1445
  17. package/src/adapters/openai-responses/canonical-forward.ts +202 -0
  18. package/src/adapters/openai-responses/image-gen.ts +406 -0
  19. package/src/adapters/openai-responses/internal.ts +3 -0
  20. package/src/adapters/openai-responses/passthrough.ts +611 -0
  21. package/src/adapters/openai-responses/prompt-cache.ts +83 -0
  22. package/src/adapters/openai-responses/reasoning.ts +220 -0
  23. package/src/adapters/openai-responses/request-strips.ts +185 -0
  24. package/src/adapters/openai-responses/tool-output-recovery.ts +509 -0
  25. package/src/adapters/openai-responses/tool-schema.ts +293 -0
  26. package/src/adapters/openai-responses/web-search.ts +156 -0
  27. package/src/adapters/openai-responses.ts +4 -2625
  28. package/src/bridge/errors.ts +34 -0
  29. package/src/bridge/internal.ts +174 -0
  30. package/src/bridge/response-json.ts +624 -0
  31. package/src/bridge/sse.ts +1444 -0
  32. package/src/bridge.ts +5 -2204
  33. package/src/chat/inbound.ts +12 -1
  34. package/src/codex/account-lifecycle.ts +3 -0
  35. package/src/codex/account-store.ts +71 -9
  36. package/src/codex/auth-api/account-list.ts +507 -0
  37. package/src/codex/auth-api/http.ts +32 -0
  38. package/src/codex/auth-api/login-flow.ts +554 -0
  39. package/src/codex/auth-api/login-state.ts +64 -0
  40. package/src/codex/auth-api/main-account-probe.ts +331 -0
  41. package/src/codex/auth-api/pool-mode-gate.ts +274 -0
  42. package/src/codex/auth-api/pool-quota-probe.ts +512 -0
  43. package/src/codex/auth-api/reset-credit-service.ts +422 -0
  44. package/src/codex/auth-api/routes.ts +425 -0
  45. package/src/codex/auth-api/runtime-config.ts +48 -0
  46. package/src/codex/auth-api.ts +27 -3118
  47. package/src/codex/auth-context.ts +95 -28
  48. package/src/codex/catalog/auto-review.ts +507 -0
  49. package/src/codex/catalog/build-entries.ts +981 -0
  50. package/src/codex/catalog/combo-member.ts +375 -0
  51. package/src/codex/catalog/derive-entry.ts +229 -0
  52. package/src/codex/catalog/effort.ts +0 -1
  53. package/src/codex/catalog/gated-native-warn.ts +63 -0
  54. package/src/codex/catalog/gather-capture.ts +533 -0
  55. package/src/codex/catalog/model-hints.ts +691 -0
  56. package/src/codex/catalog/model-visibility.ts +304 -0
  57. package/src/codex/catalog/provider-fetch.ts +52 -2942
  58. package/src/codex/catalog/provider-models.ts +685 -0
  59. package/src/codex/catalog/restore.ts +132 -0
  60. package/src/codex/catalog/retained-sync.ts +706 -0
  61. package/src/codex/catalog/routed-gather.ts +858 -0
  62. package/src/codex/catalog/subagent-roster.ts +176 -0
  63. package/src/codex/catalog/sync.ts +52 -2698
  64. package/src/codex/inject/config-toml.ts +563 -0
  65. package/src/codex/inject/remove.ts +192 -0
  66. package/src/codex/inject/restore.ts +540 -0
  67. package/src/codex/inject/routing-classify.ts +109 -0
  68. package/src/codex/inject/routing-target.ts +125 -0
  69. package/src/codex/inject.ts +81 -1436
  70. package/src/codex/lineage.ts +458 -0
  71. package/src/codex/pool-refresh-backoff.ts +152 -0
  72. package/src/codex/routing/active-account.ts +194 -0
  73. package/src/codex/routing/cooldown-math.ts +275 -0
  74. package/src/codex/routing/health-store.ts +402 -0
  75. package/src/codex/routing/probe-lease.ts +358 -0
  76. package/src/codex/routing/selection.ts +703 -0
  77. package/src/codex/routing/thread-affinity.ts +538 -0
  78. package/src/codex/routing.ts +353 -2234
  79. package/src/codex/shim-fingerprint.ts +223 -0
  80. package/src/codex/shim-inspect.ts +175 -0
  81. package/src/codex/shim-probe.ts +367 -0
  82. package/src/codex/shim-restore-lock.ts +169 -0
  83. package/src/codex/shim-state-file.ts +151 -0
  84. package/src/codex/shim-templates.ts +265 -0
  85. package/src/codex/shim.ts +48 -1268
  86. package/src/config/diagnostics.ts +705 -0
  87. package/src/config/feature-flags.ts +55 -0
  88. package/src/config/live-reconcile.ts +403 -0
  89. package/src/config/load-degrade.ts +880 -0
  90. package/src/config/mutation-lock.ts +244 -0
  91. package/src/config/openai-tier-backup.ts +268 -0
  92. package/src/config/persist-unlocked.ts +92 -0
  93. package/src/config/proxy-env.ts +188 -0
  94. package/src/config/salvage.ts +244 -0
  95. package/src/config/schema/config-schema.ts +640 -0
  96. package/src/config/schema/leaf-validators.ts +855 -0
  97. package/src/config/warn-memo.ts +28 -0
  98. package/src/config.ts +234 -4481
  99. package/src/generated/compatibility-version.json +539 -39
  100. package/src/lib/request-execution-budget.ts +69 -20
  101. package/src/lib/spend-reservation-ledger.ts +940 -0
  102. package/src/lib/upstream-retry.ts +55 -11
  103. package/src/lib/workflow-budget.ts +553 -30
  104. package/src/providers/quota/account-cache.ts +441 -0
  105. package/src/providers/quota/antigravity.ts +295 -0
  106. package/src/providers/quota/report-cache.ts +320 -0
  107. package/src/providers/quota/vendor-probes-key.ts +1243 -0
  108. package/src/providers/quota/vendor-probes-oauth.ts +590 -0
  109. package/src/providers/quota.ts +324 -3079
  110. package/src/providers/registry/entries-core.ts +1221 -0
  111. package/src/providers/registry/entries-extended.ts +1204 -0
  112. package/src/providers/registry/model-seeds.ts +908 -0
  113. package/src/providers/registry/types.ts +352 -0
  114. package/src/providers/registry.ts +24 -3536
  115. package/src/responses/continuation-ownership.ts +29 -0
  116. package/src/responses/state/replay-fingerprint.ts +80 -0
  117. package/src/responses/state/snapshot-codec.ts +104 -0
  118. package/src/responses/state/spill-failure.ts +118 -0
  119. package/src/responses/state/spill-queue.ts +665 -0
  120. package/src/responses/state/temp-recovery.ts +257 -0
  121. package/src/responses/state.ts +82 -1143
  122. package/src/routing/identity-domains.ts +449 -0
  123. package/src/routing/probe-lease.ts +511 -0
  124. package/src/server/index/bounded-request.ts +88 -0
  125. package/src/server/index/live-sideband.ts +565 -0
  126. package/src/server/index/serve-options.ts +1766 -0
  127. package/src/server/index/startup-warnings.ts +213 -0
  128. package/src/server/index/websocket-handler.ts +335 -0
  129. package/src/server/index.ts +40 -2547
  130. package/src/server/management/route-registry.ts +26 -23
  131. package/src/server/management/shared.ts +8 -5
  132. package/src/server/management/workflow-budget-routes.ts +133 -0
  133. package/src/server/management-api.ts +12 -0
  134. package/src/server/request-log-conversation.ts +9 -7
  135. package/src/server/request-log.ts +245 -1
  136. package/src/server/responses/account-change-state.ts +233 -0
  137. package/src/server/responses/adapter-continuation.ts +514 -0
  138. package/src/server/responses/adapter-delivery.ts +214 -0
  139. package/src/server/responses/adapter-dispatch.ts +971 -0
  140. package/src/server/responses/compact.ts +59 -4
  141. package/src/server/responses/completion-policy.ts +33 -0
  142. package/src/server/responses/core-auth.ts +527 -0
  143. package/src/server/responses/core-codex-account.ts +859 -0
  144. package/src/server/responses/core-combo-failure.ts +210 -0
  145. package/src/server/responses/core-combo.ts +707 -0
  146. package/src/server/responses/core-errors.ts +152 -0
  147. package/src/server/responses/core-lifetime.ts +95 -0
  148. package/src/server/responses/core-normalize.ts +350 -0
  149. package/src/server/responses/core-opaque-recovery.ts +380 -0
  150. package/src/server/responses/core-options.ts +159 -0
  151. package/src/server/responses/core-replay.ts +225 -0
  152. package/src/server/responses/core.ts +192 -8893
  153. package/src/server/responses/passthrough-delivery.ts +856 -0
  154. package/src/server/responses/passthrough-dispatch.ts +1476 -0
  155. package/src/server/responses/passthrough-execution.ts +54 -0
  156. package/src/server/responses/request-prepare.ts +970 -0
  157. package/src/server/responses/request-send-budget.ts +164 -0
  158. package/src/server/responses/request-sidecar-auth.ts +149 -0
  159. package/src/server/responses/request-transport.ts +744 -0
  160. package/src/server/responses/response-effects.ts +157 -0
  161. package/src/server/responses/run-turn-execution.ts +448 -0
  162. package/src/server/responses/sidecar-execution.ts +469 -0
  163. package/src/server/responses-image-gen-repair.ts +1 -1
  164. package/src/server/workflow-refusal.ts +84 -0
  165. package/src/types/config.ts +30 -0
  166. package/src/usage/log.ts +146 -0
  167. package/src/usage/summary.ts +171 -21
@@ -1,3078 +1,102 @@
1
- import { createHash } from "node:crypto";
2
- import {
3
- effectiveCodexAuthAccountId,
4
- fetchMainAccountInfoSnapshot,
5
- listCodexAuthAccountsSnapshot,
6
- } from "../codex/auth-api";
7
- import { withoutRetiredCodexQuota, type StoredAccountQuota } from "../codex/quota";
8
- import { isMainAccountIdentityGenerationLive } from "../codex/main-account-cache";
9
- import { MAIN_CODEX_ACCOUNT_ID } from "../codex/main-account";
10
- import { codexPlanKey } from "../codex/plan";
11
- import { resolveEnvValue } from "../config";
12
- import { resolveProviderApiKey } from "./key-store";
13
- import { getValidAccessToken, getValidAccessTokenForAccount } from "../oauth";
14
- import { getAccountCredential, getAccountSet, getCredential } from "../oauth/store";
15
- import { antigravityUserAgent } from "../adapters/client-fingerprint";
16
- import { isCanonicalOllamaCloudUrl } from "../adapters/ollama-native-url";
17
- import { DestinationDnsResolutionError } from "../lib/destination-policy";
18
- import { PinnedHttpError } from "../lib/pinned-http";
19
- import { ProviderOutboundPolicyError, providerOutboundPost, providerRedirectError, type ProviderOutboundDependencies } from "../lib/provider-outbound";
20
- import { apiKeyPoolEntryId } from "./api-keys";
21
- import { fetchMuseKeyQuotaSnapshot } from "./muse-key-quota";
22
- import { XAI_GROK_CLIENT_VERSION, XAI_GROK_COMPATIBILITY } from "./xai-transport";
23
- import { getProviderRegistryEntry, providerCodexAccountMode, registryEntryForProviderDestination } from "./registry";
24
- import type { OcxConfig, OcxProviderConfig } from "../types";
25
- import { isCanonicalOpenAiForwardProvider, OPENAI_CODEX_PROVIDER_ID } from "./openai-tiers";
26
- import {
27
- captureConfigGeneration,
28
- sweepExpiredOnWrite,
29
- type GenerationContext,
30
- } from "../lib/state-store-sweeper";
31
- import {
32
- ACCOUNT_QUOTA_TTL_MS,
33
- asRecord,
34
- CACHE_TTL_MS,
35
- normalizePercent,
36
- normalizeResetAt,
37
- QUOTA_JSON_READ_FAILURE,
38
- readQuotaJson,
39
- REQUEST_TIMEOUT_MS,
40
- toFiniteNumber,
41
- } from "./quota-wire";
42
- import {
43
- clearCachedProviderQuotas,
44
- providerQuotaRoutingBinding,
45
- replaceCachedProviderQuotas,
46
- type ProviderQuotaRoutingEvidence,
47
- } from "./quota-routing-cache";
48
- import {
49
- aggregateCodexPoolCapacity,
50
- CODEX_CAPACITY_MAX_QUOTA_AGE_MS,
51
- type CodexCapacityAggregation,
52
- type CodexCapacityQuota,
53
- } from "./codex-capacity";
54
- import type {
55
- AccountQuotaMode,
56
- QuotaFailureCode,
57
- ProviderQuota,
58
- ProviderQuotaCreditsUsd,
59
- ProviderQuotaWindow,
60
- ProviderRoutingQuota,
61
- } from "./quota-types";
62
- import {
63
- clearKiroAccountUsageState,
64
- commitKiroAccountUsageState,
65
- fetchKiroUsageSnapshot,
66
- type KiroUsageSnapshot,
67
- kiroUsageContextForAccount,
68
- reconcileKiroAccountUsageState,
69
- } from "./kiro-usage";
70
- import {
71
- cancelPendingAccountQuotaPersist,
72
- readPersistedAccountQuotas,
73
- schedulePersistAccountQuotas,
74
- } from "./account-quota-disk";
75
- import { clearProviderApiKeyQuotaCache, mapQuotaRoster, readProviderApiKeyQuotas, type ProviderApiKeyQuota } from "./quota-key-accounts";
76
-
77
- export type { ProviderQuota, ProviderQuotaCreditsUsd, ProviderQuotaWindow } from "./quota-types";
78
-
79
- /** Match oauth/index REFRESH_SKEW_MS — use stored access without refresh when still fresh. */
80
- const ACCOUNT_TOKEN_SKEW_MS = 60_000;
81
- /** Successful provider quota payloads are small; reject oversized or stalled JSON before parsing. */
82
- export { QUOTA_RESPONSE_MAX_BYTES } from "./quota-wire";
83
- const KIMI_CODE_BASE_URL = "https://api.kimi.com/coding/v1";
84
- const KIMI_CODE_USAGE_URL = `${KIMI_CODE_BASE_URL}/usages`;
85
- const COMMAND_CODE_BASE_URL = "https://api.commandcode.ai";
86
- const COMMAND_CODE_WHOAMI_URL = `${COMMAND_CODE_BASE_URL}/alpha/whoami`;
87
- const COMMAND_CODE_CREDITS_URL = `${COMMAND_CODE_BASE_URL}/alpha/billing/credits`;
88
- const COMMAND_CODE_SUBSCRIPTIONS_URL = `${COMMAND_CODE_BASE_URL}/alpha/billing/subscriptions`;
89
- const COMMAND_CODE_USAGE_URL = `${COMMAND_CODE_BASE_URL}/alpha/usage/summary`;
90
- const A6API_BASE_URL = "https://api.a6api.com";
91
- const OPENCODE_GO_BASE_URL = "https://opencode.ai/zen/go/v1";
92
- const OPENCODE_GO_USAGE_URL = `${OPENCODE_GO_BASE_URL}/usage`;
93
- const OPENROUTER_BASE_URL = "https://openrouter.ai/api/v1";
94
- const DEEPSEEK_BASE_URL = "https://api.deepseek.com";
95
- const CLINE_BASE_URL = "https://api.cline.bot";
96
- const OLLAMA_CLOUD_BASE_URL = "https://ollama.com";
97
- const OLLAMA_CLOUD_USAGE_URL = `${OLLAMA_CLOUD_BASE_URL}/api/usage`;
98
- const ZAI_BASE_URL = "https://api.z.ai";
99
- const ZAI_CN_BASE_URL = "https://open.bigmodel.cn";
100
- const MINIMAX_REMAINS_URL = "https://www.minimax.io/v1/token_plan/remains";
101
- const MOONSHOT_BASE_URL = "https://api.moonshot.ai/v1";
102
- const VENICE_BASE_URL = "https://api.venice.ai/api/v1";
103
- const SYNTHETIC_BASE_URL = "https://api.synthetic.new/v2";
104
- const DEEPINFRA_BASE_URL = "https://api.deepinfra.com";
105
- const NEURALWATT_BASE_URL = "https://api.neuralwatt.com/v1";
106
- const XAI_BILLING_URL = "https://cli-chat-proxy.grok.com/v1/billing";
107
- const XAI_CREDITS_URL = `${XAI_BILLING_URL}?format=credits`;
108
- /** Keep a failed probe's previous row at most this long before dropping it. */
109
- const LAST_GOOD_MAX_AGE_MS = CODEX_CAPACITY_MAX_QUOTA_AGE_MS;
110
- const nativeMainReportGenerations = new WeakMap<ProviderQuotaReport, number>();
111
- const accountReportCurrent = new WeakMap<ProviderQuotaReport, () => boolean>();
112
- const routingEvidence = new WeakMap<ProviderQuotaReport, ProviderQuotaRoutingEvidence>();
113
- let providerQuotaBeforePublishForTests: (() => void | Promise<void>) | null = null;
114
-
115
- /** Test-only seam for identity/config invalidation after probes but before publication. */
116
- export function setProviderQuotaBeforePublishForTests(
117
- hook: (() => void | Promise<void>) | null,
118
- ): void {
119
- providerQuotaBeforePublishForTests = hook;
120
- }
121
- const TERMINAL_QUOTA_FAILURE = Symbol("terminal-quota-failure");
122
- /**
123
- * The probe succeeded and the upstream authoritatively reported NO model-quota windows.
124
- *
125
- * Distinct from `null`, which means "this probe told us nothing" and deliberately preserves
126
- * the last-good row for up to 30 minutes. Collapsing the two would let a stale report outlive
127
- * the authoritative answer that replaced it: a GLM plan whose payload carries only MCP
128
- * `TIME_LIMIT` rows has no model windows, and the dashboard and quota-aware routing must stop
129
- * showing the previous token windows rather than keep them for another half hour.
130
- *
131
- * Suppression is shared with `TERMINAL_QUOTA_FAILURE`; only the reason differs.
132
- */
133
- const AUTHORITATIVE_EMPTY_QUOTA = Symbol("authoritative-empty-quota");
134
- type ProviderQuotaProbeResult =
135
- | ProviderQuotaReport
136
- | null
137
- | typeof TERMINAL_QUOTA_FAILURE
138
- | typeof AUTHORITATIVE_EMPTY_QUOTA;
139
-
140
- export interface ProviderQuotaReport {
141
- provider: string;
142
- label: string;
143
- source: string;
144
- quota: ProviderQuota;
145
- updatedAt: number;
146
- /** Added by the management response projection, never stored on a cached report. */
147
- routingQuota?: ProviderRoutingQuota;
148
- reverseEngineered?: boolean;
149
- /**
150
- * The row was OBSERVED in-band on a streaming turn rather than probed.
151
- *
152
- * Age means something different for these. A probed provider re-reads on its own TTL,
153
- * so a row older than the last-good bound means the probe is failing and showing it
154
- * would misrepresent a live number. A passive provider publishes no endpoint at all
155
- * (`hasPassiveAccountQuota`), so its last observation is not a stale reading of
156
- * something fresher — it is the only measurement that exists, and dropping it leaves
157
- * the operator with nothing. Consumers that enforce a freshness bound must exempt
158
- * these and state the observation age instead.
159
- */
160
- observed?: boolean;
161
- aggregation?: CodexCapacityAggregation;
162
- }
163
-
164
- export interface ProviderQuotaResponse {
165
- generatedAt: number;
166
- reports: ProviderQuotaReport[];
167
- }
168
-
169
- let cache: { key: string; ts: number; response: ProviderQuotaResponse } | null = null;
170
- const inflight = new Map<string, { epoch: number; promise: Promise<ProviderQuotaResponse> }>();
171
- /** Bumped on cache clear and on force-refresh start; stale-epoch probes lose commit authority. */
172
- let invalidationEpoch = 0;
173
-
174
- /** Invalidate the report cache (e.g. after switching a provider's active account). */
175
- export function clearProviderQuotaCache(): void {
176
- cache = null;
177
- clearCachedProviderQuotas();
178
- clearProviderApiKeyQuotaCache();
179
- invalidationEpoch += 1;
180
- }
181
-
182
- function cacheKey(config: OcxConfig): string {
183
- const providers = Object.entries(config.providers)
184
- .map(([name, provider]) => {
185
- const resolvedKey = typeof provider.apiKey === "string"
186
- ? resolveProviderApiKey(provider.apiKey)?.trim()
187
- : undefined;
188
- const activeKeyId = resolvedKey ? apiKeyPoolEntryId(resolvedKey) : "none";
189
- return `${name}:${provider.adapter}:${provider.authMode ?? "key"}:${providerCodexAccountMode(name, provider) ?? "none"}:${provider.disabled === true ? "off" : "on"}:${provider.baseUrl}:${activeKeyId}`;
190
- })
191
- .sort()
192
- .join("|");
193
- return `${config.defaultProvider}|${providers}`;
194
- }
195
-
196
- type CodexAuthAccountsSnapshotPromise = ReturnType<typeof listCodexAuthAccountsSnapshot>;
197
-
198
- function hasCodexPoolProvider(config: OcxConfig): boolean {
199
- return Object.entries(config.providers).some(([name, provider]) => (
200
- provider.disabled !== true
201
- && isBuiltInChatGptForwardProvider(name, provider)
202
- && providerCodexAccountMode(name, provider) !== "direct"
203
- ));
204
- }
205
-
206
- function quotaSignatureValue(quota: CodexCapacityQuota | null): unknown {
207
- if (!quota) return null;
208
- return {
209
- fiveHourPercent: quota.fiveHourPercent,
210
- fiveHourResetAt: quota.fiveHourResetAt,
211
- weeklyPercent: quota.weeklyPercent,
212
- weeklyResetAt: quota.weeklyResetAt,
213
- monthlyPercent: quota.monthlyPercent,
214
- monthlyResetAt: quota.monthlyResetAt,
215
- updatedAt: quota.updatedAt,
216
- customWindows: [...(quota.customWindows ?? [])]
217
- .map(window => ({ label: window.label, percent: window.percent, resetAt: window.resetAt }))
218
- .sort((a, b) => a.label.localeCompare(b.label)),
219
- };
220
- }
221
-
222
- function providerQuotaFromCodexQuota(
223
- quota: StoredAccountQuota | Omit<StoredAccountQuota, "updatedAt"> | null | undefined,
224
- ): CodexCapacityQuota | null {
225
- if (!quota) return null;
226
- // Direct snapshots bypass account DTOs; sanitize here as well as at ingestion.
227
- quota = withoutRetiredCodexQuota(quota);
228
- if (!quota) return null;
229
- const projected: CodexCapacityQuota = {
230
- ...(quota.shortPercent !== undefined ? { fiveHourPercent: quota.shortPercent } : {}),
231
- ...(quota.shortResetAt !== undefined ? { fiveHourResetAt: quota.shortResetAt } : {}),
232
- ...(quota.weeklyPercent !== undefined ? { weeklyPercent: quota.weeklyPercent } : {}),
233
- ...(quota.weeklyResetAt !== undefined ? { weeklyResetAt: quota.weeklyResetAt } : {}),
234
- ...(quota.monthlyPercent !== undefined ? { monthlyPercent: quota.monthlyPercent } : {}),
235
- ...(quota.monthlyResetAt !== undefined ? { monthlyResetAt: quota.monthlyResetAt } : {}),
236
- ...(quota.customWindows !== undefined ? { customWindows: quota.customWindows } : {}),
237
- updatedAt: "updatedAt" in quota ? quota.updatedAt : Date.now(),
238
- };
239
- return hasQuotaRows(projected) ? projected : null;
240
- }
241
-
242
- /** Hash only presentation-relevant state; account ids and email addresses never enter the key. */
243
- function cacheKeyWithAggregationState(
244
- config: OcxConfig,
245
- prefetchedSnapshot?: CodexAuthAccountsSnapshotPromise,
246
- ): string | Promise<string> {
247
- const base = cacheKey(config);
248
- if (!hasCodexPoolProvider(config)) return base;
249
- return (async () => {
250
- try {
251
- const activeId = effectiveCodexAuthAccountId(config);
252
- const snapshot = await (prefetchedSnapshot ?? listCodexAuthAccountsSnapshot(config, false));
253
- const rows = snapshot.accounts.map(account => ({
254
- isMain: account.isMain,
255
- active: account.id === activeId,
256
- plan: codexPlanKey(account.plan) ?? null,
257
- paused: account.paused,
258
- needsReauth: account.needsReauth === true,
259
- quota: quotaSignatureValue(providerQuotaFromCodexQuota(account.quota)),
260
- }));
261
- const canonicalRows = rows.map(row => JSON.stringify(row)).sort();
262
- const digest = createHash("sha256").update(JSON.stringify(canonicalRows)).digest("hex").slice(0, 24);
263
- return `${base}|codex-pool:${digest}`;
264
- } catch {
265
- return `${base}|codex-pool:unavailable`;
266
- }
267
- })();
268
- }
269
-
270
- function publicCapacityWindow(window: import("./codex-capacity").CodexCapacityWindowAggregation) {
271
- const { totalWeight: _totalWeight, consumedWeight: _consumedWeight, remainingWeight: _remainingWeight, ...safe } = window;
272
- return safe;
273
- }
274
-
275
- /** Management API metadata intentionally omits configured/weighted unit counts. */
276
- function publicCapacityAggregation(
277
- aggregation: CodexCapacityAggregation,
278
- presentation: NonNullable<CodexCapacityAggregation["presentation"]>,
279
- ): CodexCapacityAggregation {
280
- const safeCurrentAccount = presentation === "coverage-only" && aggregation.currentAccount
281
- ? { ...aggregation.currentAccount, quota: null }
282
- : aggregation.currentAccount;
283
- return {
284
- ...aggregation,
285
- presentation,
286
- ...(safeCurrentAccount ? { currentAccount: safeCurrentAccount } : {}),
287
- ...(aggregation.fiveHour ? { fiveHour: publicCapacityWindow(aggregation.fiveHour) } : {}),
288
- ...(aggregation.weekly ? { weekly: publicCapacityWindow(aggregation.weekly) } : {}),
289
- ...(aggregation.monthly ? { monthly: publicCapacityWindow(aggregation.monthly) } : {}),
290
- ...(aggregation.customWindows ? {
291
- customWindows: aggregation.customWindows.map(window => ({
292
- label: window.label,
293
- ...publicCapacityWindow(window),
294
- })),
295
- } : {}),
296
- };
297
- }
298
-
299
- function hasQuotaRows(quota: ProviderQuota | null | undefined): quota is ProviderQuota {
300
- if (!quota) return false;
301
- return typeof quota.fiveHourPercent === "number"
302
- || typeof quota.weeklyPercent === "number"
303
- || typeof quota.monthlyPercent === "number"
304
- || quota.creditsUsd?.unlimited === true
305
- || typeof quota.creditsUsd?.percent === "number"
306
- || !!quota.customWindows?.some(window => typeof window.percent === "number");
307
- }
308
-
309
- function providerLabel(providerId: string): string {
310
- return getProviderRegistryEntry(providerId)?.label ?? providerId;
311
- }
312
-
313
- /** Test-only access to the quota reader's deadline and cancellation contract. */
314
- export async function readProviderQuotaJsonForTests(response: Response, timeoutMs: number): Promise<unknown> {
315
- const result = await readQuotaJson(response, timeoutMs);
316
- return result === QUOTA_JSON_READ_FAILURE ? null : result;
317
- }
318
-
319
- function isBuiltInChatGptForwardProvider(name: string, provider: OcxProviderConfig): boolean {
320
- return name === OPENAI_CODEX_PROVIDER_ID && isCanonicalOpenAiForwardProvider(provider);
321
- }
322
-
323
- function isCanonicalA6apiBaseUrl(baseUrl: string): boolean {
324
- const normalized = normalizedBaseUrl(baseUrl);
325
- return normalized === A6API_BASE_URL || normalized === `${A6API_BASE_URL}/v1`;
326
- }
327
-
328
- function isCanonicalOpenCodeGoBaseUrl(baseUrl: string): boolean {
329
- return normalizedBaseUrl(baseUrl) === OPENCODE_GO_BASE_URL;
330
- }
331
-
332
- function isCanonicalOpenRouterBaseUrl(baseUrl: string): boolean {
333
- const normalized = normalizedBaseUrl(baseUrl);
334
- return normalized === OPENROUTER_BASE_URL;
335
- }
336
-
337
- function isCanonicalDeepSeekBaseUrl(baseUrl: string): boolean {
338
- const normalized = normalizedBaseUrl(baseUrl);
339
- return normalized === DEEPSEEK_BASE_URL || normalized === `${DEEPSEEK_BASE_URL}/v1`;
340
- }
341
-
342
- function isCanonicalClineBaseUrl(baseUrl: string): boolean {
343
- const normalized = normalizedBaseUrl(baseUrl);
344
- return normalized === CLINE_BASE_URL || normalized === `${CLINE_BASE_URL}/api/v1`;
345
- }
346
-
347
- function isCanonicalOllamaCloudBaseUrl(baseUrl?: string): boolean {
348
- if (!baseUrl) return false;
349
- try {
350
- return isCanonicalOllamaCloudUrl(baseUrl);
351
- } catch {
352
- return false;
353
- }
354
- }
355
-
356
- function zaiQuotaMonitorHost(baseUrl: string): string | null {
357
- // Admission and destination selection must share one mapping: admitting a new
358
- // international wire must never fall through to the CN host/authentication scheme.
359
- switch (normalizedBaseUrl(baseUrl)) {
360
- case ZAI_BASE_URL:
361
- case `${ZAI_BASE_URL}/api/coding/paas/v4`:
362
- case `${ZAI_BASE_URL}/api/anthropic`:
363
- case `${ZAI_BASE_URL}/api/v1`:
364
- return ZAI_BASE_URL;
365
- case ZAI_CN_BASE_URL:
366
- case `${ZAI_CN_BASE_URL}/api/coding/paas/v4`:
367
- case `${ZAI_CN_BASE_URL}/api/v1`:
368
- return ZAI_CN_BASE_URL;
369
- default:
370
- return null;
371
- }
372
- }
373
-
374
- function isCanonicalZaiBaseUrl(baseUrl: string): boolean {
375
- return zaiQuotaMonitorHost(baseUrl) !== null;
376
- }
377
-
378
- function isCanonicalMinimaxBaseUrl(baseUrl: string): boolean {
379
- const normalized = normalizedBaseUrl(baseUrl);
380
- return normalized === "https://api.minimax.io/v1" || normalized === "https://api.minimaxi.com/v1";
381
- }
382
-
383
- function isCanonicalMoonshotBaseUrl(baseUrl: string): boolean {
384
- const normalized = normalizedBaseUrl(baseUrl);
385
- return normalized === MOONSHOT_BASE_URL || normalized === "https://api.moonshot.cn/v1";
386
- }
387
-
388
- function isCanonicalVeniceBaseUrl(baseUrl: string): boolean {
389
- return normalizedBaseUrl(baseUrl) === VENICE_BASE_URL;
390
- }
391
-
392
- function isCanonicalSyntheticBaseUrl(baseUrl: string): boolean {
393
- const normalized = normalizedBaseUrl(baseUrl);
394
- return normalized === SYNTHETIC_BASE_URL || normalized === "https://api.synthetic.new/openai/v1";
395
- }
396
-
397
- function isCanonicalDeepInfraBaseUrl(baseUrl: string): boolean {
398
- const normalized = normalizedBaseUrl(baseUrl);
399
- return normalized === DEEPINFRA_BASE_URL || normalized === `${DEEPINFRA_BASE_URL}/v1/openai`;
400
- }
401
-
402
- function isCanonicalNeuralwattBaseUrl(baseUrl: string): boolean {
403
- return normalizedBaseUrl(baseUrl) === NEURALWATT_BASE_URL;
404
- }
405
-
406
- function a6apiPayload(value: unknown): Record<string, unknown> | null {
407
- const body = asRecord(value);
408
- return asRecord(body?.data) ?? body;
409
- }
410
-
411
- function firstFinite(record: Record<string, unknown> | null, names: string[]): number | undefined {
412
- if (!record) return undefined;
413
- for (const name of names) {
414
- const value = toFiniteNumber(record[name]);
415
- if (value !== undefined) return value;
416
- }
417
- return undefined;
418
- }
419
-
420
- async function fetchA6apiQuota(provider: string, config: OcxProviderConfig): Promise<ProviderQuotaProbeResult> {
421
- // Never send a configured API key to a lookalike host or through a redirect.
422
- if (!isCanonicalA6apiBaseUrl(config.baseUrl)) return null;
423
- const apiKey = resolveProviderApiKey(config.apiKey)?.trim();
424
- if (!apiKey) return null;
425
- const headers = { Accept: "application/json", Authorization: `Bearer ${apiKey}` } as const;
426
- const [subscriptionResponse, tokenResponse] = await Promise.all([
427
- fetch(`${A6API_BASE_URL}/dashboard/billing/subscription`, {
428
- headers, redirect: "error", signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
429
- }),
430
- fetch(`${A6API_BASE_URL}/api/usage/token/`, {
431
- headers, redirect: "error", signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
432
- }),
433
- ]);
434
- if (!subscriptionResponse.ok || !tokenResponse.ok) {
435
- const statuses = [subscriptionResponse.status, tokenResponse.status];
436
- // 408/429 are transient (timeout/throttle), not invalid-account signals: keep the
437
- // last-good row like 5xx/network failures. 401/403 (bad key) and 404 (contract change)
438
- // stay terminal.
439
- return statuses.some(status => status >= 400 && status < 500 && status !== 429 && status !== 408)
440
- ? TERMINAL_QUOTA_FAILURE
441
- : null;
442
- }
443
- const [subscriptionBody, tokenBody] = await Promise.all([
444
- readQuotaJson(subscriptionResponse),
445
- readQuotaJson(tokenResponse),
446
- ]);
447
- if (subscriptionBody === QUOTA_JSON_READ_FAILURE || tokenBody === QUOTA_JSON_READ_FAILURE) return null;
448
- const subscription = a6apiPayload(subscriptionBody);
449
- const token = a6apiPayload(tokenBody);
450
- const unlimited = token?.unlimited_quota === true
451
- || token?.unlimited_quota === 1
452
- || token?.unlimited_quota === "true";
453
- const normalizedExpiry = normalizeResetAt(token?.expires_at);
454
- const expiry = normalizedExpiry && normalizedExpiry > 0
455
- ? { expiresAt: normalizedExpiry }
456
- : {};
457
- if (unlimited) {
458
- // Every row is an API-credit constraint on inference, so the display quota is also
459
- // the routing projection. Passing it explicitly is the opt-in.
460
- const quota: ProviderQuota = {
461
- creditsUsd: {
462
- used: 0,
463
- limit: 0,
464
- remaining: 0,
465
- percent: 0,
466
- unlimited: true,
467
- ...expiry,
468
- },
469
- customWindows: [{ label: "Unlimited API credits", percent: 0 }],
470
- updatedAt: Date.now(),
471
- };
472
- return keyReport(provider, "a6api:billing", quota, config, apiKey, quota);
473
- }
474
- const limitUsd = firstFinite(subscription, ["hard_limit_usd"]);
475
- const grantedUnits = firstFinite(token, ["total_granted"]);
476
- const usedUnits = firstFinite(token, ["total_used"]);
477
- const availableUnits = firstFinite(token, ["total_available"]);
478
- const reconciledUnits = usedUnits !== undefined && availableUnits !== undefined
479
- ? usedUnits + availableUnits
480
- : undefined;
481
- const reconciliationTolerance = grantedUnits !== undefined
482
- ? Math.abs(grantedUnits) * 1e-9
483
- : 0;
484
- if (limitUsd === undefined || grantedUnits === undefined || usedUnits === undefined
485
- || availableUnits === undefined || limitUsd <= 0 || grantedUnits <= 0
486
- || usedUnits < 0 || availableUnits < 0
487
- || reconciledUnits === undefined
488
- || Math.abs(reconciledUnits - grantedUnits) > reconciliationTolerance) return TERMINAL_QUOTA_FAILURE;
489
- const usdPerUnit = limitUsd / grantedUnits;
490
- const usedUsd = usedUnits * usdPerUnit;
491
- const remainingUsd = Math.max(0, availableUnits * usdPerUnit);
492
- const percent = normalizePercent((usedUsd / limitUsd) * 100);
493
- if (percent === undefined) return TERMINAL_QUOTA_FAILURE;
494
- const label = `API credits ($${remainingUsd.toFixed(2)} of $${limitUsd.toFixed(2)} remaining)`;
495
- const quota: ProviderQuota = {
496
- creditsUsd: {
497
- used: usedUsd,
498
- limit: limitUsd,
499
- remaining: remainingUsd,
500
- percent,
501
- ...expiry,
502
- },
503
- customWindows: [{ label, percent }],
504
- updatedAt: Date.now(),
505
- };
506
- // The credit balance funds inference itself, so display and routing scope agree.
507
- return keyReport(provider, "a6api:billing", quota, config, apiKey, quota);
508
- }
509
-
510
- function parseOpenCodeGoUsageWindow(value: unknown): { percent: number; resetAt?: number } | null {
511
- const row = asRecord(value);
512
- if (!row) return null;
513
- const percent = normalizePercent(row.percent);
514
- if (percent === undefined) return null;
515
- const resetAt = normalizeResetAt(row.resetsAt);
516
- return { percent, ...(resetAt !== undefined ? { resetAt } : {}) };
517
- }
518
-
519
- async function fetchOpenCodeGoQuota(provider: string, config: OcxProviderConfig): Promise<ProviderQuotaProbeResult> {
520
- // Never send a configured API key when the provider destination is not the built-in Go endpoint.
521
- if (!isCanonicalOpenCodeGoBaseUrl(config.baseUrl)) return null;
522
- const apiKey = resolveProviderApiKey(config.apiKey)?.trim();
523
- if (!apiKey) return null;
524
- const response = await fetch(OPENCODE_GO_USAGE_URL, {
525
- headers: { Accept: "application/json", Authorization: `Bearer ${apiKey}` },
526
- redirect: "error",
527
- signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
528
- });
529
- if (!response.ok) {
530
- return response.status >= 400 && response.status < 500 && response.status !== 408 && response.status !== 429
531
- ? TERMINAL_QUOTA_FAILURE
532
- : null;
533
- }
534
- const body = asRecord(await readQuotaJson(response));
535
- const usage = asRecord(body?.usage);
536
- if (!usage) return null;
537
- const rolling = parseOpenCodeGoUsageWindow(usage.rolling);
538
- const weekly = parseOpenCodeGoUsageWindow(usage.weekly);
539
- const monthly = parseOpenCodeGoUsageWindow(usage.monthly);
540
- const quota: ProviderQuota = {
541
- ...(rolling ? {
542
- fiveHourPercent: rolling.percent,
543
- ...(rolling.resetAt !== undefined ? { fiveHourResetAt: rolling.resetAt } : {}),
544
- } : {}),
545
- ...(weekly ? {
546
- weeklyPercent: weekly.percent,
547
- ...(weekly.resetAt !== undefined ? { weeklyResetAt: weekly.resetAt } : {}),
548
- } : {}),
549
- ...(monthly ? {
550
- monthlyPercent: monthly.percent,
551
- ...(monthly.resetAt !== undefined ? { monthlyResetAt: monthly.resetAt } : {}),
552
- } : {}),
553
- updatedAt: Date.now(),
554
- };
555
- return keyReport(provider, "opencode-go:usage", quota, config, apiKey, quota);
556
- }
557
-
558
- /**
559
- * OpenRouter `GET /api/v1/key` — the key's own credit balance and optional
560
- * per-key spending cap. `limit` is the configured cap (absent = uncapped);
561
- * `usage` is lifetime spend; `limit_remaining` is what is left of the cap.
562
- * When no cap is set there is no hard limit to meter against, so no bar is
563
- * produced — the provider falls back to its documented reference.
564
- */
565
- async function fetchOpenRouterQuota(provider: string, config: OcxProviderConfig): Promise<ProviderQuotaProbeResult> {
566
- // Never send a configured API key to a lookalike host or through a redirect.
567
- if (!isCanonicalOpenRouterBaseUrl(config.baseUrl)) return null;
568
- const apiKey = resolveProviderApiKey(config.apiKey)?.trim();
569
- if (!apiKey) return null;
570
- const response = await fetch(`${OPENROUTER_BASE_URL}/key`, {
571
- headers: { Accept: "application/json", Authorization: `Bearer ${apiKey}` },
572
- redirect: "error",
573
- signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
574
- });
575
- if (!response.ok) {
576
- return response.status >= 400 && response.status < 500 && response.status !== 408 && response.status !== 429
577
- ? TERMINAL_QUOTA_FAILURE
578
- : null;
579
- }
580
- const body = asRecord(await readQuotaJson(response));
581
- const data = asRecord(body?.data) ?? body;
582
- if (!data) return null;
583
- const limit = toFiniteNumber(data.limit);
584
- const limitRemaining = toFiniteNumber(data.limit_remaining);
585
- const usage = toFiniteNumber(data.usage);
586
- // A successful no-cap response is a DELIBERATE change, not a transient
587
- // failure: the old capped row must be dropped, not preserved as last-good.
588
- if (limit === undefined || limit <= 0) return TERMINAL_QUOTA_FAILURE;
589
- // Prefer the authoritative remaining-cap value when present: `usage` is
590
- // lifetime accumulated spend and overstates a reset or re-capped key.
591
- const used = limitRemaining !== undefined
592
- ? Math.max(0, limit - limitRemaining)
593
- : usage !== undefined && usage >= 0 ? usage : undefined;
594
- if (used === undefined) return null;
595
- const percent = normalizePercent((used / limit) * 100);
596
- if (percent === undefined) return null;
597
- const remaining = Math.max(0, limit - used);
598
- const label = `API credits ($${remaining.toFixed(2)} of $${limit.toFixed(2)} remaining)`;
599
- // The per-key spending cap stops every request this credential can make, so the
600
- // whole report is inference-wide routing evidence.
601
- const quota: ProviderQuota = {
602
- customWindows: [{ label, percent }],
603
- updatedAt: Date.now(),
604
- };
605
- return keyReport(provider, "openrouter:key-info", quota, config, apiKey, quota);
606
- }
607
-
608
- /**
609
- * DeepSeek `GET /user/balance` — the account's granted + topped-up credit
610
- * balance. The payload places `total_balance` / `granted_balance` inside
611
- * entries of `balance_infos` (one row per currency); the row for the account's
612
- * currency is selected by preference. `granted_balance` is a CURRENT balance
613
- * component, not the original grant ceiling, so no consumed percentage is
614
- * fabricated — the balance is reported as a balance-only window.
615
- */
616
- async function fetchDeepSeekQuota(provider: string, config: OcxProviderConfig): Promise<ProviderQuotaProbeResult> {
617
- if (!isCanonicalDeepSeekBaseUrl(config.baseUrl)) return null;
618
- const apiKey = resolveProviderApiKey(config.apiKey)?.trim();
619
- if (!apiKey) return null;
620
- const response = await fetch(`${DEEPSEEK_BASE_URL}/user/balance`, {
621
- headers: { Accept: "application/json", Authorization: `Bearer ${apiKey}` },
622
- redirect: "error",
623
- signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
624
- });
625
- if (!response.ok) {
626
- return response.status >= 400 && response.status < 500 && response.status !== 408 && response.status !== 429
627
- ? TERMINAL_QUOTA_FAILURE
628
- : null;
629
- }
630
- const body = asRecord(await readQuotaJson(response));
631
- // The payload nests balances under `balance_infos` rows keyed by currency;
632
- // prefer a USD row, then CNY, then the first row that parses.
633
- const infos = Array.isArray(body?.balance_infos) ? body.balance_infos as unknown[] : null;
634
- const rows = infos
635
- ? infos.map((raw): Record<string, unknown> | null => asRecord(raw)).filter((r): r is Record<string, unknown> => r !== null)
636
- : [];
637
- const pick = (currency: string): Record<string, unknown> | null =>
638
- rows.find(row => String(row.currency ?? "").toUpperCase() === currency) ?? null;
639
- const preferred = pick("USD") ?? pick("CNY") ?? rows[0] ?? null;
640
- if (!preferred) return null;
641
- const totalBalance = toFiniteNumber(preferred.total_balance);
642
- const grantedBalance = toFiniteNumber(preferred.granted_balance);
643
- const toppedUp = toFiniteNumber(preferred.topped_up_balance);
644
- const balance = totalBalance ?? grantedBalance ?? toppedUp;
645
- if (balance === undefined || balance < 0) return null;
646
- const label = grantedBalance !== undefined && grantedBalance > 0
647
- ? `API balance ($${balance.toFixed(2)} total, $${grantedBalance.toFixed(2)} granted)`
648
- : `API balance ($${balance.toFixed(2)})`;
649
- return report(provider, "deepseek:balance", {
650
- customWindows: [{ label, percent: 0 }],
651
- updatedAt: Date.now(),
652
- });
653
- }
654
-
655
- /**
656
- * ClinePass `GET /api/v1/users/me/plan/usage-limits` — the subscription's
657
- * rolling five-hour, weekly, and monthly utilization, matching the existing
658
- * ProviderQuota windows directly. The endpoint 404s (or returns a null plan)
659
- * for accounts without an active ClinePass, which is a no-report, not an error.
660
- */
661
- async function fetchClineQuota(provider: string, config: OcxProviderConfig): Promise<ProviderQuotaProbeResult> {
662
- if (!isCanonicalClineBaseUrl(config.baseUrl)) return null;
663
- const apiKey = resolveProviderApiKey(config.apiKey)?.trim();
664
- if (!apiKey) return null;
665
- const response = await fetch(`${CLINE_BASE_URL}/api/v1/users/me/plan/usage-limits`, {
666
- headers: { Accept: "application/json", Authorization: `Bearer ${apiKey}` },
667
- redirect: "error",
668
- signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
669
- });
670
- if (!response.ok) {
671
- // 404 = no active plan; a plain "no plan" is a no-report, everything else
672
- // 4xx (except 408/429) is a credential/contract problem.
673
- if (response.status === 404) return null;
674
- return response.status >= 400 && response.status < 500 && response.status !== 408 && response.status !== 429
675
- ? TERMINAL_QUOTA_FAILURE
676
- : null;
677
- }
678
- const body = asRecord(await readQuotaJson(response));
679
- const data = asRecord(body?.data) ?? body;
680
- const limits = Array.isArray(data?.limits) ? data.limits : null;
681
- if (!limits) return null;
682
- const quota: ProviderQuota = { updatedAt: Date.now() };
683
- let windows = 0;
684
- for (const raw of limits) {
685
- const row = asRecord(raw);
686
- if (!row) continue;
687
- const percent = normalizePercent(row.percentUsed);
688
- if (percent === undefined) continue;
689
- const resetAt = normalizeResetAt(row.resetsAt);
690
- if (row.type === "five_hour") {
691
- quota.fiveHourPercent = percent;
692
- if (resetAt !== undefined) quota.fiveHourResetAt = resetAt;
693
- windows += 1;
694
- } else if (row.type === "weekly") {
695
- quota.weeklyPercent = percent;
696
- if (resetAt !== undefined) quota.weeklyResetAt = resetAt;
697
- windows += 1;
698
- } else if (row.type === "monthly") {
699
- quota.monthlyPercent = percent;
700
- if (resetAt !== undefined) quota.monthlyResetAt = resetAt;
701
- windows += 1;
702
- }
703
- }
704
- return windows > 0 ? keyReport(provider, "cline:plan-usage-limits", quota, config, apiKey, quota) : null;
705
- }
706
-
707
- /**
708
- * Ollama Cloud `GET https://ollama.com/api/usage` — returns account usage.
709
- * Legacy plans report rolling 5-hour `limits.session.usage` and 7-day
710
- * `limits.weekly.usage`. Migrated monthly-credit plans report
711
- * `limits.monthly.usage`. `usage` values are normalized fractions (0..1).
712
- */
713
- function parseOllamaPercent(usageValue: unknown): number | undefined {
714
- const usage = toFiniteNumber(usageValue);
715
- if (usage === undefined || usage < 0) return undefined;
716
- const percent = Math.round(usage * 10000) / 100;
717
- return normalizePercent(percent);
718
- }
719
-
720
- export function parseOllamaCloudQuota(body: Record<string, unknown> | null): ProviderQuota | null {
721
- if (!body) return null;
722
- const limits = asRecord(body.limits);
723
- if (!limits) return null;
724
-
725
- const quota: ProviderQuota = { updatedAt: Date.now() };
726
- let windows = 0;
727
-
728
- const session = asRecord(limits.session);
729
- if (session) {
730
- const percent = parseOllamaPercent(session.usage);
731
- if (percent !== undefined) {
732
- quota.fiveHourPercent = percent;
733
- windows += 1;
734
- }
735
- }
736
-
737
- const weekly = asRecord(limits.weekly);
738
- if (weekly) {
739
- const percent = parseOllamaPercent(weekly.usage);
740
- if (percent !== undefined) {
741
- quota.weeklyPercent = percent;
742
- windows += 1;
743
- }
744
- }
745
-
746
- const monthly = asRecord(limits.monthly);
747
- if (monthly) {
748
- const percent = parseOllamaPercent(monthly.usage);
749
- if (percent !== undefined) {
750
- quota.monthlyPercent = percent;
751
- windows += 1;
752
- }
753
- }
754
-
755
- return windows > 0 ? quota : null;
756
- }
757
-
758
- async function fetchOllamaCloudQuota(provider: string, config: OcxProviderConfig): Promise<ProviderQuotaProbeResult> {
759
- const effectiveBaseUrl = config.baseUrl ?? getProviderRegistryEntry(provider)?.baseUrl ?? "";
760
- if (!isCanonicalOllamaCloudBaseUrl(effectiveBaseUrl)) return null;
761
- const apiKey = resolveProviderApiKey(config.apiKey)?.trim();
762
- if (!apiKey) return null;
763
- const response = await fetch(OLLAMA_CLOUD_USAGE_URL, {
764
- headers: { Accept: "application/json", Authorization: `Bearer ${apiKey}` },
765
- redirect: "error",
766
- signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
767
- });
768
- if (!response.ok) {
769
- if (response.status === 404) return null;
770
- return response.status >= 400 && response.status < 500 && response.status !== 408 && response.status !== 429
771
- ? TERMINAL_QUOTA_FAILURE
772
- : null;
773
- }
774
- const body = asRecord(await readQuotaJson(response));
775
- const quota = parseOllamaCloudQuota(body);
776
- return quota ? keyReport(provider, "ollama-cloud:usage", quota, config, apiKey, quota) : null;
777
- }
778
-
779
- /**
780
- * Z.AI GLM Coding Plan `GET /api/monitor/usage/quota/limit` — the coding-plan
781
- * limits arrive as a `limits` array of `TOKENS_LIMIT` (newer plans call the
782
- * same rows `CREDIT_LIMIT`) and `TIME_LIMIT` rows. `TOKENS_LIMIT`/`CREDIT_LIMIT`
783
- * rows carry the window length as `unit`/`number`: unit 3 is hours (number 5 →
784
- * the rolling five-hour window), unit 6 is weeks (number 1 → the weekly
785
- * window). Every row's `percentage` is the consumed share (falling
786
- * back to `currentValue`/`usage` when absent) and `nextResetTime` (unix ms)
787
- * the window reset.
788
- *
789
- * `TIME_LIMIT` rows are deliberately ignored (issue #1168). They are the shared
790
- * monthly MCP *call* allowance for Web Search / Web Reader / Zread — not a
791
- * model-token budget — and `ProviderQuota.monthlyPercent` is consumed as a
792
- * model-capacity signal: `headroomOf()` in `src/oauth/account-quota-rank.ts`
793
- * takes the MAX across every window, so a user who spent their MCP search
794
- * allowance would be ranked as having no model capacity left, and the dashboard
795
- * would draw a full monthly bar for a plan whose model tokens are untouched.
796
- * A payload carrying only `TIME_LIMIT` rows therefore reports no quota at all,
797
- * which is the honest answer rather than a fabricated one.
798
- */
799
- export function parseZaiQuotaLimits(data: Record<string, unknown> | null): ProviderQuota | null {
800
- const limits = Array.isArray(data?.limits) ? data.limits as unknown[] : null;
801
- if (!limits) return null;
802
- const quota: ProviderQuota = { updatedAt: Date.now() };
803
- let windows = 0;
804
- for (const raw of limits) {
805
- const row = asRecord(raw);
806
- if (!row) continue;
807
- // Gate on row type before deriving a percentage: an MCP row must not even
808
- // contribute a parsed value to a model-quota report.
809
- if (row.type !== "TOKENS_LIMIT" && row.type !== "CREDIT_LIMIT") continue;
810
- const resetAt = normalizeResetAt(row.nextResetTime);
811
- let percent = normalizePercent(row.percentage);
812
- if (percent === undefined) {
813
- const used = toFiniteNumber(row.currentValue);
814
- const total = toFiniteNumber(row.usage);
815
- if (used !== undefined && total !== undefined && total > 0) {
816
- percent = normalizePercent((used / total) * 100);
817
- }
818
- }
819
- if (percent === undefined) continue;
820
- const unit = toFiniteNumber(row.unit);
821
- const number = toFiniteNumber(row.number);
822
- if (unit === 3 && number === 5) {
823
- quota.fiveHourPercent = percent;
824
- if (resetAt !== undefined) quota.fiveHourResetAt = resetAt;
825
- windows += 1;
826
- } else if (unit === 6 && number === 1) {
827
- quota.weeklyPercent = percent;
828
- if (resetAt !== undefined) quota.weeklyResetAt = resetAt;
829
- windows += 1;
830
- }
831
- }
832
- return windows > 0 ? quota : null;
833
- }
834
-
835
- /**
836
- * Legacy Z.AI payload shape: percent fields with window identifiers directly on
837
- * the data object (optionally nested under `quota`). Kept as a fallback so
838
- * older responses keep rendering when the `limits` array is absent.
839
- */
840
- function parseZaiQuotaLegacyFields(data: Record<string, unknown> | null): ProviderQuota | null {
841
- if (!data) return null;
842
- const quota: ProviderQuota = { updatedAt: Date.now() };
843
- let windows = 0;
844
- const percentAt = (key: string): number | undefined => {
845
- const value = normalizePercent(data[key]);
846
- if (value !== undefined) return value;
847
- const nested = asRecord(data.quota);
848
- return nested ? normalizePercent(nested[key]) : undefined;
849
- };
850
- const fiveHour = percentAt("fiveHourPercent") ?? percentAt("fiveHourUsage") ?? percentAt("fiveHourUsed");
851
- const weekly = percentAt("weeklyPercent") ?? percentAt("weeklyUsage") ?? percentAt("weeklyUsed");
852
- const monthly = percentAt("monthlyPercent") ?? percentAt("mcpPercent") ?? percentAt("monthlyMCPUsage");
853
- if (fiveHour !== undefined) {
854
- quota.fiveHourPercent = fiveHour;
855
- windows += 1;
856
- }
857
- if (weekly !== undefined) {
858
- quota.weeklyPercent = weekly;
859
- windows += 1;
860
- }
861
- if (monthly !== undefined) {
862
- quota.monthlyPercent = monthly;
863
- windows += 1;
864
- }
865
- return windows > 0 ? quota : null;
866
- }
867
-
868
- /**
869
- * Fetches the Z.AI GLM Coding Plan quota — on whichever region the provider
870
- * points at (api.z.ai or open.bigmodel.cn). The `limits` array shape is
871
- * preferred; older field-name payloads fall back to the legacy parser.
872
- *
873
- * Authentication differs by host (issue #1168). `api.z.ai` takes the API key as
874
- * a Bearer token per Z.AI's API reference; `open.bigmodel.cn` expects the key
875
- * directly in `Authorization` with no scheme prefix and answers a Bearer header
876
- * with an auth error, which is why BigModel Coding Plan quota never rendered.
877
- * The host is already canonicalized by `isCanonicalZaiBaseUrl` above and
878
- * `redirect: "error"` stays set, so the bare key cannot travel to a lookalike
879
- * host or follow a redirect off-origin.
880
- */
881
- async function fetchZaiQuota(provider: string, config: OcxProviderConfig): Promise<ProviderQuotaProbeResult> {
882
- const monitorHost = zaiQuotaMonitorHost(config.baseUrl);
883
- if (!monitorHost) return null;
884
- const apiKey = resolveProviderApiKey(config.apiKey)?.trim();
885
- if (!apiKey) return null;
886
- const authorization = monitorHost === ZAI_CN_BASE_URL ? apiKey : `Bearer ${apiKey}`;
887
- const response = await fetch(`${monitorHost}/api/monitor/usage/quota/limit`, {
888
- headers: { Accept: "application/json", Authorization: authorization },
889
- redirect: "error",
890
- signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
891
- });
892
- if (!response.ok) {
893
- return response.status >= 400 && response.status < 500 && response.status !== 408 && response.status !== 429
894
- ? TERMINAL_QUOTA_FAILURE
895
- : null;
896
- }
897
- const body = asRecord(await readQuotaJson(response));
898
- if (!body || body.success === false) return null;
899
- const data = asRecord(body.data) ?? body;
900
- if (Array.isArray(data?.limits)) {
901
- const quota = parseZaiQuotaLimits(data);
902
- // A well-formed `limits[]` we fully understood is authoritative even when it yields no
903
- // model window — for example a plan reporting only the monthly MCP `TIME_LIMIT` row.
904
- // Returning `null` here would preserve the previous token windows for up to 30 minutes
905
- // and keep quota-aware routing acting on a report the provider has already superseded.
906
- return quota
907
- ? keyReport(provider, "zai:quota-limit", quota, config, apiKey, quota)
908
- : AUTHORITATIVE_EMPTY_QUOTA;
909
- }
910
- const legacy = parseZaiQuotaLegacyFields(data);
911
- if (!legacy) return null;
912
- // The legacy monthly figure also carries MCP usage; it is display evidence, not
913
- // proof that model inference is unavailable. Modern TOKEN_LIMIT rows above are scoped.
914
- const inferenceQuota = { ...legacy };
915
- delete inferenceQuota.monthlyPercent;
916
- delete inferenceQuota.monthlyResetAt;
917
- return keyReport(provider, "zai:quota-limit", legacy, config, apiKey, inferenceQuota);
918
- }
919
-
920
- /**
921
- * MiniMax Token Plan `GET /v1/token_plan/remains` — the subscription's
922
- * remaining quota as a countdown-time value (ms). The endpoint does not expose
923
- * the plan's total duration, so no percentage is fabricated from a presumed
924
- * window: the remaining time is reported as a duration-only window. When the
925
- * API supplies a total (`total_time` / `plan_duration_ms`), a consumed share
926
- * is derived from it. Region selects the host: `minimax` → www.minimax.io,
927
- * `minimax-cn` → api.minimaxi.com.
928
- */
929
- async function fetchMinimaxQuota(provider: string, config: OcxProviderConfig): Promise<ProviderQuotaProbeResult> {
930
- if (!isCanonicalMinimaxBaseUrl(config.baseUrl)) return null;
931
- const apiKey = resolveProviderApiKey(config.apiKey)?.trim();
932
- if (!apiKey) return null;
933
- const cnHost = normalizedBaseUrl(config.baseUrl)?.startsWith("https://api.minimaxi.com");
934
- const remainsUrl = cnHost ? "https://api.minimaxi.com/v1/token_plan/remains" : MINIMAX_REMAINS_URL;
935
- const response = await fetch(remainsUrl, {
936
- headers: { Accept: "application/json", Authorization: `Bearer ${apiKey}` },
937
- redirect: "error",
938
- signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
939
- });
940
- if (!response.ok) {
941
- return response.status >= 400 && response.status < 500 && response.status !== 408 && response.status !== 429
942
- ? TERMINAL_QUOTA_FAILURE
943
- : null;
944
- }
945
- const body = asRecord(await readQuotaJson(response));
946
- if (!body || body.success === false) return null;
947
- const data = asRecord(body.data) ?? body;
948
- const remainsMs = toFiniteNumber(data.remains_time ?? data.remainsTime);
949
- if (remainsMs === undefined || remainsMs < 0) return null;
950
- const hours = Math.floor(remainsMs / 3_600_000);
951
- const label = `Token Plan remaining (${hours}h)`;
952
- // Only derive a consumed share when the API actually reports the plan total;
953
- // a presumed window (e.g. 30 days) would fabricate utilization. A valid
954
- // response that omits the total after a prior refresh had it is a DELIBERATE
955
- // contract change — the old row must be dropped (terminal), not preserved as
956
- // a transient last-good.
957
- const totalMs = toFiniteNumber(data.total_time ?? data.plan_duration_ms ?? data.total_duration_ms);
958
- if (totalMs === undefined || totalMs <= 0) return TERMINAL_QUOTA_FAILURE;
959
- const consumed = Math.max(0, totalMs - remainsMs);
960
- const percent = normalizePercent((consumed / totalMs) * 100);
961
- if (percent === undefined) return null;
962
- return report(provider, "minimax:token-plan-remains", {
963
- customWindows: [{ label, percent }],
964
- updatedAt: Date.now(),
965
- });
966
- }
967
-
968
- /**
969
- * Moonshot/Kimi `GET /v1/users/me/balance` — the account's available balance
970
- * (voucher + cash). Renders a single balance window against the sum of
971
- * voucher + cash when positive (there is no per-window rate limit to meter).
972
- */
973
- async function fetchMoonshotQuota(provider: string, config: OcxProviderConfig): Promise<ProviderQuotaProbeResult> {
974
- if (!isCanonicalMoonshotBaseUrl(config.baseUrl)) return null;
975
- const apiKey = resolveProviderApiKey(config.apiKey)?.trim();
976
- if (!apiKey) return null;
977
- const host = normalizedBaseUrl(config.baseUrl)?.startsWith("https://api.moonshot.cn") ? "https://api.moonshot.cn/v1" : MOONSHOT_BASE_URL;
978
- const response = await fetch(`${host}/users/me/balance`, {
979
- headers: { Accept: "application/json", Authorization: `Bearer ${apiKey}` },
980
- redirect: "error",
981
- signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
982
- });
983
- if (!response.ok) {
984
- return response.status >= 400 && response.status < 500 && response.status !== 408 && response.status !== 429
985
- ? TERMINAL_QUOTA_FAILURE
986
- : null;
987
- }
988
- const body = asRecord(await readQuotaJson(response));
989
- const data = asRecord(body?.data) ?? body;
990
- if (!data) return null;
991
- const available = toFiniteNumber(data.available_balance);
992
- const voucher = toFiniteNumber(data.voucher_balance);
993
- const cash = toFiniteNumber(data.cash_balance);
994
- if (available === undefined || available < 0) return null;
995
- // Moonshot exposes no per-window quota ceiling, only a balance — report it
996
- // as a balance-only window (percent 0) rather than a fabricated utilization.
997
- // Currency is host-scoped: China platform (api.moonshot.cn) bills in CNY;
998
- // the international platform (api.moonshot.ai) bills in USD. Do not force
999
- // either side into the other unit — the number is correct, only the unit
1000
- // must match the host.
1001
- const isChinaHost = host.startsWith("https://api.moonshot.cn");
1002
- const money = (n: number) => isChinaHost ? `¥${n.toFixed(2)}` : `$${n.toFixed(2)}`;
1003
- const unit = isChinaHost ? "CNY" : "USD";
1004
- const label = voucher !== undefined && cash !== undefined
1005
- ? `Balance (${money(available)} ${unit} available, ${money(voucher)} voucher)`
1006
- : `Balance (${money(available)} ${unit} available)`;
1007
- return report(provider, "moonshot:balance", {
1008
- customWindows: [{ label, percent: 0 }],
1009
- updatedAt: Date.now(),
1010
- });
1011
- }
1012
-
1013
- /**
1014
- * Venice `GET /api/v1/billing/balance` — DIEM (native credits) or USD balance.
1015
- * Shows the remaining balance; epoch allocation progress when present.
1016
- */
1017
- async function fetchVeniceQuota(provider: string, config: OcxProviderConfig): Promise<ProviderQuotaProbeResult> {
1018
- if (!isCanonicalVeniceBaseUrl(config.baseUrl)) return null;
1019
- const apiKey = resolveProviderApiKey(config.apiKey)?.trim();
1020
- if (!apiKey) return null;
1021
- const response = await fetch(`${VENICE_BASE_URL}/billing/balance`, {
1022
- headers: { Accept: "application/json", Authorization: `Bearer ${apiKey}` },
1023
- redirect: "error",
1024
- signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
1025
- });
1026
- if (!response.ok) {
1027
- return response.status >= 400 && response.status < 500 && response.status !== 408 && response.status !== 429
1028
- ? TERMINAL_QUOTA_FAILURE
1029
- : null;
1030
- }
1031
- const body = asRecord(await readQuotaJson(response));
1032
- const data = asRecord(body?.data) ?? body;
1033
- if (!data) return null;
1034
- const diemBalance = toFiniteNumber(data.balance);
1035
- const usdBalance = toFiniteNumber(data.balance_usd);
1036
- const epochUsed = toFiniteNumber(data.diem_epoch_used);
1037
- const epochAllocated = toFiniteNumber(data.diem_epoch_allocated);
1038
- if (diemBalance === undefined && usdBalance === undefined) return null;
1039
- const label = diemBalance !== undefined
1040
- ? `DIEM balance (${Math.round(diemBalance)})`
1041
- : `USD balance ($${usdBalance?.toFixed(2) ?? "?"})`;
1042
- if (epochAllocated !== undefined && epochAllocated > 0 && epochUsed !== undefined) {
1043
- const percent = normalizePercent((epochUsed / epochAllocated) * 100);
1044
- if (percent === undefined) return null;
1045
- return report(provider, "venice:billing-balance", {
1046
- customWindows: [{ label, percent }],
1047
- updatedAt: Date.now(),
1048
- });
1049
- }
1050
- return report(provider, "venice:billing-balance", {
1051
- customWindows: [{ label, percent: 0 }],
1052
- updatedAt: Date.now(),
1053
- });
1054
- }
1055
-
1056
- /**
1057
- * Synthetic `GET /v2/quotas` — the known quota lanes (rolling 5-hour,
1058
- * weekly token, search-hourly) mapped onto the quota windows.
1059
- */
1060
- async function fetchSyntheticQuota(provider: string, config: OcxProviderConfig): Promise<ProviderQuotaProbeResult> {
1061
- if (!isCanonicalSyntheticBaseUrl(config.baseUrl)) return null;
1062
- const apiKey = resolveProviderApiKey(config.apiKey)?.trim();
1063
- if (!apiKey) return null;
1064
- const response = await fetch(`${SYNTHETIC_BASE_URL}/quotas`, {
1065
- headers: { Accept: "application/json", Authorization: `Bearer ${apiKey}` },
1066
- redirect: "error",
1067
- signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
1068
- });
1069
- if (!response.ok) {
1070
- return response.status >= 400 && response.status < 500 && response.status !== 408 && response.status !== 429
1071
- ? TERMINAL_QUOTA_FAILURE
1072
- : null;
1073
- }
1074
- const body = asRecord(await readQuotaJson(response));
1075
- const data = asRecord(body?.data) ?? body;
1076
- const quota: ProviderQuota = { updatedAt: Date.now() };
1077
- let windows = 0;
1078
- const percentAt = (key: string): number | undefined => {
1079
- const value = normalizePercent(data?.[key]);
1080
- if (value !== undefined) return value;
1081
- const nested = asRecord(data?.quota) ?? asRecord(data?.quotas);
1082
- return nested ? normalizePercent(nested[key]) : undefined;
1083
- };
1084
- const fiveHour = percentAt("rollingFiveHourLimit");
1085
- const weekly = percentAt("weeklyTokenLimit");
1086
- if (fiveHour !== undefined) {
1087
- quota.fiveHourPercent = fiveHour;
1088
- windows += 1;
1089
- }
1090
- if (weekly !== undefined) {
1091
- quota.weeklyPercent = weekly;
1092
- windows += 1;
1093
- }
1094
- const search = asRecord(data?.search);
1095
- const searchHourly = search ? normalizePercent(search.hourly) : undefined;
1096
- if (searchHourly !== undefined) {
1097
- quota.customWindows = [...(quota.customWindows ?? []), { label: "Search hourly", percent: searchHourly }];
1098
- windows += 1;
1099
- }
1100
- const inferenceQuota = { ...quota };
1101
- delete inferenceQuota.customWindows; // search.hourly does not constrain model inference.
1102
- return windows > 0 ? keyReport(provider, "synthetic:quotas", quota, config, apiKey, inferenceQuota) : null;
1103
- }
1104
-
1105
- /**
1106
- * DeepInfra `GET /payment/checklist?compute_owed=true` — prepaid balance,
1107
- * recent spend, spending limit, and suspension state. Renders a balance
1108
- * window (prepaid funds are a negative `stripe_balance` → positive available).
1109
- */
1110
- async function fetchDeepInfraQuota(provider: string, config: OcxProviderConfig): Promise<ProviderQuotaProbeResult> {
1111
- if (!isCanonicalDeepInfraBaseUrl(config.baseUrl)) return null;
1112
- const apiKey = resolveProviderApiKey(config.apiKey)?.trim();
1113
- if (!apiKey) return null;
1114
- const response = await fetch(`${DEEPINFRA_BASE_URL}/payment/checklist?compute_owed=true`, {
1115
- headers: { Accept: "application/json", Authorization: `Bearer ${apiKey}` },
1116
- redirect: "error",
1117
- signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
1118
- });
1119
- if (!response.ok) {
1120
- return response.status >= 400 && response.status < 500 && response.status !== 408 && response.status !== 429
1121
- ? TERMINAL_QUOTA_FAILURE
1122
- : null;
1123
- }
1124
- const body = asRecord(await readQuotaJson(response));
1125
- const data = asRecord(body?.data) ?? body;
1126
- if (!data) return null;
1127
- const stripeBalance = toFiniteNumber(data.stripe_balance);
1128
- const spendLimit = toFiniteNumber(data.spending_limit);
1129
- const total = toFiniteNumber(data.total_amount_due);
1130
- if (stripeBalance === undefined) return null;
1131
- // Prepaid funds are negative; a positive value is money owed.
1132
- const available = stripeBalance < 0 ? -stripeBalance : 0;
1133
- if (spendLimit !== undefined && spendLimit > 0) {
1134
- const spent = total !== undefined && total > 0 ? total : Math.max(0, spendLimit - available);
1135
- const percent = normalizePercent((spent / spendLimit) * 100);
1136
- if (percent === undefined) return null;
1137
- return report(provider, "deepinfra:billing-checklist", {
1138
- customWindows: [{ label: `Billing cycle spend ($${spent.toFixed(2)} of $${spendLimit.toFixed(2)})`, percent }],
1139
- updatedAt: Date.now(),
1140
- });
1141
- }
1142
- return report(provider, "deepinfra:billing-checklist", {
1143
- customWindows: [{ label: `Prepaid balance ($${available.toFixed(2)})`, percent: 0 }],
1144
- updatedAt: Date.now(),
1145
- });
1146
- }
1147
-
1148
- /**
1149
- * Neuralwatt `GET /v1/quota` — subscription kWh usage (primary window) and
1150
- * prepaid USD credit balance (secondary).
1151
- */
1152
- async function fetchNeuralwattQuota(provider: string, config: OcxProviderConfig): Promise<ProviderQuotaProbeResult> {
1153
- if (!isCanonicalNeuralwattBaseUrl(config.baseUrl)) return null;
1154
- const apiKey = resolveProviderApiKey(config.apiKey)?.trim();
1155
- if (!apiKey) return null;
1156
- const response = await fetch(`${NEURALWATT_BASE_URL}/quota`, {
1157
- headers: { Accept: "application/json", Authorization: `Bearer ${apiKey}` },
1158
- redirect: "error",
1159
- signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
1160
- });
1161
- if (!response.ok) {
1162
- return response.status >= 400 && response.status < 500 && response.status !== 408 && response.status !== 429
1163
- ? TERMINAL_QUOTA_FAILURE
1164
- : null;
1165
- }
1166
- const body = asRecord(await readQuotaJson(response));
1167
- const data = asRecord(body?.data) ?? body;
1168
- const quota: ProviderQuota = { updatedAt: Date.now() };
1169
- let windows = 0;
1170
- const subscription = asRecord(data?.subscription);
1171
- const kwhUsed = subscription ? toFiniteNumber(subscription.kwh_used) : undefined;
1172
- const kwhIncluded = subscription ? toFiniteNumber(subscription.kwh_included) : undefined;
1173
- if (kwhUsed !== undefined && kwhIncluded !== undefined && kwhIncluded > 0) {
1174
- const percent = normalizePercent((kwhUsed / kwhIncluded) * 100);
1175
- if (percent !== undefined) {
1176
- quota.fiveHourPercent = percent;
1177
- const periodEnd = subscription ? normalizeResetAt(subscription.current_period_end) : undefined;
1178
- if (periodEnd !== undefined) quota.fiveHourResetAt = periodEnd;
1179
- windows += 1;
1180
- }
1181
- }
1182
- const balance = asRecord(data?.balance);
1183
- const totalCredits = balance ? toFiniteNumber(balance.total_credits_usd) : undefined;
1184
- const remainingCredits = balance ? toFiniteNumber(balance.credits_remaining_usd) : undefined;
1185
- if (totalCredits !== undefined && totalCredits > 0 && remainingCredits !== undefined) {
1186
- // Utilization is CONSUMED credits, not the remaining share.
1187
- const used = Math.max(0, totalCredits - remainingCredits);
1188
- const percent = normalizePercent((used / totalCredits) * 100);
1189
- if (percent !== undefined) {
1190
- quota.customWindows = [...(quota.customWindows ?? []), { label: "Prepaid credits", percent }];
1191
- windows += 1;
1192
- }
1193
- }
1194
- return windows > 0 ? report(provider, "neuralwatt:quota", quota) : null;
1195
- }
1196
-
1197
- function report(
1198
- provider: string,
1199
- source: string,
1200
- quota: ProviderQuota,
1201
- aggregation?: CodexCapacityAggregation,
1202
- ): ProviderQuotaReport | null {
1203
- if (!hasQuotaRows(quota)) return null;
1204
- return {
1205
- provider,
1206
- label: providerLabel(provider),
1207
- source,
1208
- quota,
1209
- updatedAt: quota.updatedAt,
1210
- ...(aggregation ? { aggregation } : {}),
1211
- };
1212
- }
1213
-
1214
- /**
1215
- * Publish a credential-bound report, and routing evidence only when the producer
1216
- * hands over its inference-only projection.
1217
- *
1218
- * The projection is deliberately not defaulted to the display quota. A producer must
1219
- * decide that its rows really do constrain inference on the probed credential; omitting
1220
- * the argument leaves the report display-only, so a new producer cannot inherit
1221
- * provider-veto authority merely by calling this helper. Ownership alone is not the
1222
- * scope decision: providerQuotaRoutingBinding resolving is necessary, never sufficient.
1223
- */
1224
- function keyReport(
1225
- provider: string,
1226
- source: string,
1227
- quota: ProviderQuota,
1228
- config: OcxProviderConfig,
1229
- probedCredential: string,
1230
- inferenceQuota?: ProviderQuota,
1231
- ): ProviderQuotaReport | null {
1232
- const result = report(provider, source, quota);
1233
- if (!result || !inferenceQuota) return result;
1234
- const binding = providerQuotaRoutingBinding(provider, config, probedCredential);
1235
- if (binding) routingEvidence.set(result, { quota: inferenceQuota, binding });
1236
- return result;
1237
- }
1238
-
1239
- function tagNativeMainReport(
1240
- value: ProviderQuotaReport | null,
1241
- generation: number,
1242
- ): ProviderQuotaReport | null {
1243
- if (value) nativeMainReportGenerations.set(value, generation);
1244
- return value;
1245
- }
1246
-
1247
- /**
1248
- * Test-only seam: publish exactly as a credential-bound producer does, and hand back the
1249
- * routing evidence the publication actually attached.
1250
- *
1251
- * Live producers all pass a projection today, so no probe fixture can prove the OTHER half
1252
- * of the contract: that omitting it stays display-only. Routing an omitted argument through
1253
- * the real helper keeps that provable, and a re-introduced `= quota` default would be
1254
- * observed here (a defaulted parameter also fires for an explicitly undefined argument).
1255
- */
1256
- export function publishKeyReportForTests(
1257
- provider: string,
1258
- source: string,
1259
- quota: ProviderQuota,
1260
- config: OcxProviderConfig,
1261
- probedCredential: string,
1262
- inferenceQuota?: ProviderQuota,
1263
- ): { report: ProviderQuotaReport | null; routing: ProviderQuotaRoutingEvidence | undefined } {
1264
- const result = keyReport(provider, source, quota, config, probedCredential, inferenceQuota);
1265
- return { report: result, routing: result ? routingEvidence.get(result) : undefined };
1266
- }
1267
-
1268
- function isProviderQuotaReportCurrent(value: ProviderQuotaReport): boolean {
1269
- const generation = nativeMainReportGenerations.get(value);
1270
- return (generation === undefined || isMainAccountIdentityGenerationLive(generation))
1271
- && (accountReportCurrent.get(value)?.() ?? true);
1272
- }
1273
-
1274
- async function fetchChatGptForwardQuota(
1275
- config: OcxConfig,
1276
- provider: string,
1277
- providerConfig: OcxProviderConfig,
1278
- forceRefresh: boolean,
1279
- prefetchedSnapshot?: CodexAuthAccountsSnapshotPromise,
1280
- ): Promise<ProviderQuotaReport | null> {
1281
- if (providerCodexAccountMode(provider, providerConfig) === "direct") {
1282
- const snapshot = await fetchMainAccountInfoSnapshot(forceRefresh);
1283
- const quota = providerQuotaFromCodexQuota(snapshot.info.quota);
1284
- if (quota) quota.updatedAt = Date.now();
1285
- return quota
1286
- ? tagNativeMainReport(report(provider, "chatgpt:wham", quota), snapshot.mainIdentityGeneration)
1287
- : null;
1288
- }
1289
- const snapshot = await (prefetchedSnapshot ?? listCodexAuthAccountsSnapshot(config, forceRefresh));
1290
- const accounts = snapshot.accounts;
1291
- const activeId = effectiveCodexAuthAccountId(config);
1292
- const capacityAccounts = accounts.map(account => ({
1293
- ...account,
1294
- active: account.id === activeId,
1295
- quota: providerQuotaFromCodexQuota(account.quota),
1296
- }));
1297
- const active = capacityAccounts.find(account => account.active)
1298
- ?? capacityAccounts.find(account => account.id === MAIN_CODEX_ACCOUNT_ID)
1299
- ?? capacityAccounts[0];
1300
- const now = Date.now();
1301
- const capacity = aggregateCodexPoolCapacity(capacityAccounts, now);
1302
- if (capacity.aggregation && capacity.quota) {
1303
- return tagNativeMainReport(
1304
- report(
1305
- provider,
1306
- "chatgpt:wham",
1307
- capacity.quota as ProviderQuota,
1308
- publicCapacityAggregation(capacity.aggregation, "aggregate"),
1309
- ),
1310
- snapshot.mainIdentityGeneration,
1311
- );
1312
- }
1313
- const activeUsable = !!active && !active.paused && active.needsReauth !== true;
1314
- const quota = activeUsable && active?.quota
1315
- ? { ...active.quota, updatedAt: active.quota.updatedAt ?? Date.now() } as CodexCapacityQuota
1316
- : null;
1317
- const quotaFresh = !!quota
1318
- && Number.isFinite(quota.updatedAt)
1319
- && now - quota.updatedAt < CODEX_CAPACITY_MAX_QUOTA_AGE_MS;
1320
- if (quota && quotaFresh) {
1321
- const fallback = report(
1322
- provider,
1323
- "chatgpt:wham",
1324
- quota as ProviderQuota,
1325
- capacity.aggregation
1326
- ? publicCapacityAggregation(capacity.aggregation, "effective-account-fallback")
1327
- : undefined,
1328
- );
1329
- return tagNativeMainReport(fallback, snapshot.mainIdentityGeneration);
1330
- }
1331
- if (capacity.aggregation) {
1332
- const updatedAt = Date.now();
1333
- return tagNativeMainReport(
1334
- {
1335
- provider,
1336
- label: providerLabel(provider),
1337
- source: "chatgpt:wham",
1338
- quota: { updatedAt },
1339
- updatedAt,
1340
- aggregation: publicCapacityAggregation(capacity.aggregation, "coverage-only"),
1341
- },
1342
- snapshot.mainIdentityGeneration,
1343
- );
1344
- }
1345
- return null;
1346
- }
1347
-
1348
- function centsValue(value: unknown): number | undefined {
1349
- const rec = asRecord(value);
1350
- return rec ? toFiniteNumber(rec.val) : undefined;
1351
- }
1352
-
1353
- /** Decode JWT payload `sub` for xAI weekly credits when the stored credential lacks accountId. */
1354
- function xaiUserIdFromAccessToken(accessToken: string): string | undefined {
1355
- const parts = accessToken.split(".");
1356
- if (parts.length < 2 || !parts[1]) return undefined;
1357
- try {
1358
- const payload = JSON.parse(Buffer.from(parts[1], "base64url").toString("utf8")) as { sub?: unknown };
1359
- return typeof payload.sub === "string" && payload.sub.trim() ? payload.sub.trim() : undefined;
1360
- } catch {
1361
- return undefined;
1362
- }
1363
- }
1364
-
1365
- /**
1366
- * Grok Build weekly credits envelope:
1367
- * `{ config: { creditUsagePercent?, currentPeriod: { type: "USAGE_PERIOD_TYPE_WEEKLY", end } } }`.
1368
- * Omitted percent is treated as 0 (proto3 default).
1369
- */
1370
- export function parseXaiCreditsResponse(value: unknown): { percent: number; resetAt?: number } | null {
1371
- const body = asRecord(value);
1372
- const config = asRecord(body?.config);
1373
- if (!config) return null;
1374
- const period = asRecord(config.currentPeriod);
1375
- if (!period || period.type !== "USAGE_PERIOD_TYPE_WEEKLY") return null;
1376
- let percent = 0;
1377
- if (config.creditUsagePercent !== undefined) {
1378
- const normalized = normalizePercent(config.creditUsagePercent);
1379
- if (normalized === undefined) return null;
1380
- percent = normalized;
1381
- }
1382
- const resetAt = normalizeResetAt(period.end);
1383
- return {
1384
- percent,
1385
- ...(resetAt !== undefined ? { resetAt } : {}),
1386
- };
1387
- }
1388
-
1389
- async function fetchXaiWeeklyCredits(accessToken: string, userId: string): Promise<ProviderQuota | null> {
1390
- try {
1391
- const response = await fetch(XAI_CREDITS_URL, {
1392
- redirect: "error",
1393
- headers: {
1394
- Accept: "application/json",
1395
- Authorization: `Bearer ${accessToken}`,
1396
- [XAI_GROK_COMPATIBILITY.headers.tokenAuth]: "xai-grok-cli",
1397
- [XAI_GROK_COMPATIBILITY.headers.authenticateResponse]: "authenticate-response",
1398
- "x-userid": userId,
1399
- [XAI_GROK_COMPATIBILITY.headers.clientVersion]: XAI_GROK_CLIENT_VERSION,
1400
- },
1401
- signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
1402
- });
1403
- if (!response.ok) return null;
1404
- const parsed = parseXaiCreditsResponse(await readQuotaJson(response));
1405
- if (!parsed) return null;
1406
- return {
1407
- weeklyPercent: parsed.percent,
1408
- ...(parsed.resetAt !== undefined ? { weeklyResetAt: parsed.resetAt } : {}),
1409
- updatedAt: Date.now(),
1410
- };
1411
- } catch {
1412
- return null;
1413
- }
1414
- }
1415
-
1416
- async function fetchXaiQuota(provider: string, context: { accessToken: string; upstreamAccountId?: string }): Promise<ProviderQuotaReport | null> {
1417
- const { accessToken } = context;
1418
-
1419
- // Prefer the SuperGrok weekly credits window that actually gates prompting (#1283).
1420
- const userId = context.upstreamAccountId?.trim() || xaiUserIdFromAccessToken(accessToken);
1421
- if (userId) {
1422
- const weekly = await fetchXaiWeeklyCredits(accessToken, userId);
1423
- if (weekly) return report(provider, "xai:grok-billing-credits", weekly);
1424
- }
1425
-
1426
- // Legacy monthly dollar pool — retained when weekly is unavailable.
1427
- try {
1428
- const response = await fetch(XAI_BILLING_URL, {
1429
- redirect: "error",
1430
- headers: { Accept: "application/json", Authorization: `Bearer ${accessToken}` },
1431
- signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
1432
- });
1433
- if (!response.ok) return null;
1434
- const body = asRecord(await readQuotaJson(response));
1435
- const config = asRecord(body?.config);
1436
- if (!config) return null;
1437
- const limitCents = centsValue(config.monthlyLimit);
1438
- const usedCents = centsValue(config.used);
1439
- if (limitCents === undefined || usedCents === undefined || limitCents <= 0) return null;
1440
- const percent = normalizePercent((usedCents / limitCents) * 100);
1441
- if (percent === undefined) return null;
1442
- return report(provider, "xai:grok-billing", {
1443
- monthlyPercent: percent,
1444
- monthlyResetAt: normalizeResetAt(config.billingPeriodEnd),
1445
- updatedAt: Date.now(),
1446
- });
1447
- } catch {
1448
- return null;
1449
- }
1450
- }
1451
-
1452
- function parseClaudeBucket(value: unknown): { percent?: number; resetAt?: number } | null {
1453
- const rec = asRecord(value);
1454
- if (!rec) return null;
1455
- const percent = normalizePercent(rec.utilization);
1456
- const resetAt = normalizeResetAt(rec.resets_at);
1457
- if (percent === undefined && resetAt === undefined) return null;
1458
- return { percent, resetAt };
1459
- }
1460
-
1461
- function parseClaudeLimit(value: unknown): { label: string; percent: number; resetAt?: number } | null {
1462
- const rec = asRecord(value);
1463
- if (!rec) return null;
1464
- const percent = normalizePercent(rec.percent);
1465
- if (percent === undefined) return null;
1466
- const scope = asRecord(rec.scope);
1467
- const model = asRecord(scope?.model);
1468
- const rawLabel = String(model?.display_name ?? "").trim();
1469
- if (!rawLabel) return null;
1470
- const lowerLabel = rawLabel.toLowerCase();
1471
- const label = lowerLabel.includes("fable") ? "Fable"
1472
- : lowerLabel.includes("opus") ? "Opus"
1473
- : lowerLabel.includes("sonnet") ? "Sonnet"
1474
- : rawLabel;
1475
- const resetAt = normalizeResetAt(rec.resets_at);
1476
- return { label, percent, ...(resetAt !== undefined ? { resetAt } : {}) };
1477
- }
1478
-
1479
- /** Claude's OAuth usage endpoint, probed with ONE account's own bearer token. */
1480
- const anthropicUsageInflight = new Map<string, Promise<ProviderQuota | null>>();
1481
-
1482
- /**
1483
- * Anthropic per-credential usage.
1484
- *
1485
- * This endpoint reports quota only. Its body carries `five_hour`, `seven_day`, the
1486
- * model-scoped weekly buckets (`seven_day_fable`/`_opus`/`_sonnet`) and a `limits` array,
1487
- * and **no subscription or tier field** — nor does the OAuth token response, which yields only
1488
- * `account.uuid` and `account.email_address` (`src/oauth/anthropic.ts`). That is why
1489
- * `OAuthAccountSummary.plan` is `null` for Anthropic rather than populated here (#3777); it is
1490
- * a missing upstream field, not an unfinished mapping.
1491
- *
1492
- * A tier must not be inferred from what is here. Percentages are normalized per account, so a
1493
- * Max x5 seat at 50% is byte-identical to a Max x20 seat at 50%, and the presence of a
1494
- * model-scoped window tracks entitlement rather than seat size. Populate `plan` only when
1495
- * upstream returns the tier itself.
1496
- */
1497
- async function fetchAnthropicUsageQuota(accessToken: string): Promise<ProviderQuota | null> {
1498
- const joinable = anthropicUsageInflight.get(accessToken);
1499
- if (joinable) return joinable;
1500
-
1501
- const probe = (async (): Promise<ProviderQuota | null> => {
1502
- const response = await fetch("https://api.anthropic.com/api/oauth/usage", {
1503
- headers: {
1504
- Accept: "application/json, text/plain, */*",
1505
- "Content-Type": "application/json",
1506
- "User-Agent": "claude-cli/2.1.63 (external, cli)",
1507
- "anthropic-beta": "claude-code-20250219,oauth-2025-04-20,interleaved-thinking-2025-05-14,context-management-2025-06-27,prompt-caching-scope-2026-01-05",
1508
- Authorization: `Bearer ${accessToken}`,
1509
- },
1510
- signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
1511
- });
1512
- if (!response.ok) return null;
1513
- const body = asRecord(await readQuotaJson(response));
1514
- if (!body) return null;
1515
- const fiveHour = parseClaudeBucket(body.five_hour);
1516
- const sevenDay = parseClaudeBucket(body.seven_day);
1517
- const fable = parseClaudeBucket(body.seven_day_fable);
1518
- const opus = parseClaudeBucket(body.seven_day_opus);
1519
- const sonnet = parseClaudeBucket(body.seven_day_sonnet);
1520
- const customWindows: ProviderQuotaWindow[] = [];
1521
- if (fable?.percent !== undefined) customWindows.push({ label: "Fable", percent: fable.percent, ...(fable.resetAt !== undefined ? { resetAt: fable.resetAt } : {}) });
1522
- if (opus?.percent !== undefined) customWindows.push({ label: "Opus", percent: opus.percent, ...(opus.resetAt !== undefined ? { resetAt: opus.resetAt } : {}) });
1523
- if (sonnet?.percent !== undefined) customWindows.push({ label: "Sonnet", percent: sonnet.percent, ...(sonnet.resetAt !== undefined ? { resetAt: sonnet.resetAt } : {}) });
1524
- const knownLabels = new Set(customWindows.map(window => window.label.toLowerCase()));
1525
- const limits = Array.isArray(body.limits) ? body.limits : [];
1526
- for (const rawLimit of limits) {
1527
- const limitRecord = asRecord(rawLimit);
1528
- // `session` and `weekly_all` mirror the canonical five-hour and weekly
1529
- // buckets above; only model-scoped weekly limits add a third window.
1530
- if (String(limitRecord?.kind ?? "").trim().toLowerCase() !== "weekly_scoped") continue;
1531
- const limit = parseClaudeLimit(rawLimit);
1532
- if (!limit || knownLabels.has(limit.label.toLowerCase())) continue;
1533
- knownLabels.add(limit.label.toLowerCase());
1534
- customWindows.push(limit);
1535
- }
1536
- const quota: ProviderQuota = {
1537
- // Claude's 5-hour window is a first-class rate limit, same as the Codex login 5h/weekly
1538
- // rows: report it in the canonical fields so the dashboard renders it with the standard
1539
- // "5-hour limit" label and ordering instead of as a generic extra window.
1540
- ...(fiveHour?.percent !== undefined ? { fiveHourPercent: fiveHour.percent } : {}),
1541
- ...(fiveHour?.resetAt !== undefined ? { fiveHourResetAt: fiveHour.resetAt } : {}),
1542
- ...(sevenDay?.percent !== undefined ? { weeklyPercent: sevenDay.percent } : {}),
1543
- ...(sevenDay?.resetAt !== undefined ? { weeklyResetAt: sevenDay.resetAt } : {}),
1544
- ...(customWindows.length > 0 ? { customWindows } : {}),
1545
- updatedAt: Date.now(),
1546
- };
1547
- // Empty / schema-changed payloads must not cache as "success with no bars".
1548
- return hasQuotaRows(quota) ? quota : null;
1549
- })().finally(() => {
1550
- if (anthropicUsageInflight.get(accessToken) === probe) anthropicUsageInflight.delete(accessToken);
1551
- });
1552
- anthropicUsageInflight.set(accessToken, probe);
1553
- return probe;
1554
- }
1555
-
1556
- async function fetchAnthropicQuota(provider: string): Promise<ProviderQuotaReport | null> {
1557
- // Capture the account we intend to probe before awaiting — a mid-flight active
1558
- // switch must not seed the wrong account's cache with this response.
1559
- const probedAccountId = getAccountSet("anthropic")?.activeAccountId;
1560
- const probedAccountKey = probedAccountId ? accountCacheKey("anthropic", probedAccountId) : null;
1561
- const writerGeneration = captureConfigGeneration();
1562
- let accessToken: string;
1563
- try {
1564
- accessToken = await getValidAccessToken("anthropic");
1565
- } catch {
1566
- return null;
1567
- }
1568
- const quota = await fetchAnthropicUsageQuota(accessToken);
1569
- if (!quota) return null;
1570
- // Share the active-account probe with the per-account cache so Providers-page
1571
- // loads do not double-hit Anthropic's rate-limited usage endpoint.
1572
- if (probedAccountId && probedAccountKey) {
1573
- const stillOwnsToken = getAccountCredential("anthropic", probedAccountId)?.access === accessToken;
1574
- if (stillOwnsToken && mayCommitAccountQuotaKey(probedAccountKey, writerGeneration)) {
1575
- accountQuotaCache.set(probedAccountKey, { ts: Date.now(), quota });
1576
- }
1577
- }
1578
- return report(provider, "anthropic:oauth-usage", quota);
1579
- }
1580
-
1581
- /**
1582
- * Provider-level Kiro row: the active account's usage, shown on the Providers page.
1583
- *
1584
- * The per-account cache is seeded from the same probe so opening that page does not read
1585
- * the active account twice, and the account id is captured before the await so a
1586
- * concurrent account switch cannot file this answer under the wrong account.
1587
- */
1588
- async function fetchKiroQuota(provider: string): Promise<ProviderQuotaReport | null> {
1589
- const probedAccountId = getAccountSet("kiro")?.activeAccountId;
1590
- if (!probedAccountId) return null;
1591
- const probedAccountKey = accountCacheKey("kiro", probedAccountId);
1592
- const writerGeneration = captureConfigGeneration();
1593
- let snapshot: KiroUsageSnapshot | null;
1594
- try {
1595
- snapshot = await fetchKiroUsageSnapshot(await kiroUsageContextForAccount(probedAccountId));
1596
- } catch {
1597
- return null;
1598
- }
1599
- if (!snapshot) return null;
1600
- if (mayCommitAccountQuotaKey(probedAccountKey, writerGeneration)) {
1601
- accountQuotaCache.set(probedAccountKey, { ts: Date.now(), quota: snapshot.quota });
1602
- commitKiroAccountUsageState(probedAccountKey, snapshot);
1603
- }
1604
- return report(provider, "kiro:usage-limits", snapshot.quota);
1605
- }
1606
-
1607
- /**
1608
- * Provider-level row probed from the key endpoint, for an account that CAN be probed.
1609
- *
1610
- * Written through the same account cache the passive path reads, so the measurement
1611
- * survives a restart and the per-account rows at oauth-account-routes.ts:313 pick it up
1612
- * with no mode change. Deliberately does not flip providerOAuthAccountQuotaMode: that
1613
- * mode selects readPassiveProviderAccountQuotas, and the probed per-account path it would
1614
- * switch to is gated on supportsPerAccountQuota, which has no meta-muse reader, so the
1615
- * GUI account list would go from showing observations to showing nothing.
1616
- */
1617
- async function fetchMuseKeyQuota(provider: string): Promise<ProviderQuotaReport | null> {
1618
- const probedAccountId = getAccountSet(provider)?.activeAccountId;
1619
- if (!probedAccountId) return null;
1620
- const oauthAccessToken = getAccountCredential(provider, probedAccountId)?.muse?.oauthAccessToken;
1621
- // An imported or pasted credential has no account token and never will: it is
1622
- // capability, not provider id, that decides whether a probe is possible.
1623
- if (!oauthAccessToken) return null;
1624
- const probedAccountKey = accountCacheKey(provider, probedAccountId);
1625
- const writerGeneration = captureConfigGeneration();
1626
- const quota = await fetchMuseKeyQuotaSnapshot(probedAccountId, oauthAccessToken);
1627
- if (!quota) return null;
1628
- if (mayCommitAccountQuotaKey(probedAccountKey, writerGeneration)) {
1629
- // Hydrate before writing, for the same reason recordPassiveAccountQuota does:
1630
- // persistAccountQuotaCache serializes the whole in-memory map.
1631
- hydrateAccountQuotaCache();
1632
- accountQuotaCache.set(probedAccountKey, { ts: Date.now(), quota });
1633
- persistAccountQuotaCache();
1634
- }
1635
- return report(provider, `${provider}:key-endpoint`, quota);
1636
- }
1637
- /**
1638
- * Provider-level row for a passive provider: the ACTIVE account's last observed
1639
- * subscription windows, the same shape `fetchAnthropicQuota` and `fetchKiroQuota`
1640
- * return.
1641
- *
1642
- * Cache-only. A dashboard load or `ocx account refresh` must never spend an inference
1643
- * turn, so `forceRefresh` does not exist on this path — there is nothing to refresh.
1644
- * `report.updatedAt` is the observation time, which is what both GUI surfaces render
1645
- * as the relative age of the row.
1646
- */
1647
- async function fetchPassiveProviderQuota(provider: string): Promise<ProviderQuotaReport | null> {
1648
- const activeId = getAccountSet(provider)?.activeAccountId;
1649
- if (!activeId) return null;
1650
- // Idempotent; without it a proxy restart shows nothing until the next streaming turn
1651
- // even though the last observation is on disk.
1652
- hydrateAccountQuotaCache();
1653
- const entry = accountQuotaCache.get(accountCacheKey(provider, activeId));
1654
- if (!entry?.quota) return null;
1655
- const built = report(provider, `${provider}:subscription-observation`, entry.quota);
1656
- // Tagged here rather than inside report(), which every probed path shares.
1657
- return built ? { ...built, observed: true } : null;
1658
- }
1659
-
1660
- // ---------------------------------------------------------------------------
1661
- // Per-account quota (multiauth)
1662
- // ---------------------------------------------------------------------------
1663
-
1664
- /**
1665
- * Anthropic and Kiro both report usage per CREDENTIAL, so every logged-in account can be
1666
- * probed with its own bearer token — the active-account selection and the local usage log
1667
- * are irrelevant here. Mirrors the Codex pool behaviour
1668
- * (codex/auth-api.ts:fetchPoolAccountQuota), including a per-account TTL so N accounts cost
1669
- * at most N upstream calls per window. `ACCOUNT_QUOTA_TTL_MS` lives in `quota-wire.ts`
1670
- * because the Kiro exhaustion reader applies the same staleness bound.
1671
- */
1672
- type AccountQuotaCacheEntry = {
1673
- ts: number;
1674
- quota: ProviderQuota | null;
1675
- /** Last probe failed (429 / network / expired login); still may hold last-good quota. */
1676
- unavailable?: true;
1677
- quotaFailure?: QuotaFailureCode;
1678
- quotaFailureIsCurrent?: () => boolean;
1679
- /** Private new-reader identity; never persisted or serialized. */
1680
- identity?: string;
1681
- isCurrent?: () => boolean;
1682
- };
1683
- /** Expired measurements become unknown; missing reset evidence never implies a fresh allowance. */
1684
- function normalizeAnthropicQuota(quota: ProviderQuota | null | undefined, now: number): ProviderQuota | null {
1685
- if (!quota) return null;
1686
- const validReset = (resetAt: unknown): resetAt is number => typeof resetAt === "number"
1687
- && Number.isFinite(resetAt) && resetAt > 0 && Number.isFinite(new Date(resetAt).getTime());
1688
- let result = quota;
1689
- for (const [percent, reset] of [
1690
- ["fiveHourPercent", "fiveHourResetAt"],
1691
- ["weeklyPercent", "weeklyResetAt"],
1692
- ["monthlyPercent", "monthlyResetAt"],
1693
- ] as const) {
1694
- const resetAt = quota[reset];
1695
- if (resetAt === undefined) continue;
1696
- const valid = validReset(resetAt);
1697
- if (valid && resetAt > now) continue;
1698
- if (result === quota) result = { ...quota };
1699
- if (valid) delete result[percent];
1700
- delete result[reset];
1701
- }
1702
- // Persisted rows validate only the outer quota object, so custom data may be malformed.
1703
- if (quota.customWindows !== undefined) {
1704
- const windows = Array.isArray(quota.customWindows) ? quota.customWindows : [];
1705
- const retained: ProviderQuotaWindow[] = [];
1706
- let changed = !Array.isArray(quota.customWindows);
1707
- for (const window of windows) {
1708
- if (!window || typeof window !== "object" || typeof window.label !== "string" || !window.label.trim()
1709
- || typeof window.percent !== "number" || !Number.isFinite(window.percent)
1710
- || window.percent < 0 || window.percent > 100) {
1711
- changed = true;
1712
- continue;
1713
- }
1714
- if (validReset(window.resetAt) && window.resetAt <= now) {
1715
- changed = true;
1716
- continue;
1717
- }
1718
- if (window.resetAt !== undefined && !validReset(window.resetAt)) {
1719
- const normalized = { ...window };
1720
- delete normalized.resetAt;
1721
- retained.push(normalized);
1722
- changed = true;
1723
- } else {
1724
- retained.push(window);
1725
- }
1726
- }
1727
- if (changed) {
1728
- if (result === quota) result = { ...quota };
1729
- if (retained.length) result.customWindows = retained;
1730
- else delete result.customWindows;
1731
- }
1732
- }
1733
- return hasQuotaRows(result) ? result : null;
1734
- }
1735
-
1736
- const accountQuotaCache = new Map<string, AccountQuotaCacheEntry>();
1737
- let explicitAccountEpoch = 0;
1738
-
1739
- /**
1740
- * Seed the cache from the last run, once.
1741
- *
1742
- * Without this a restart forgets every measurement, so the pool opens its next turn with
1743
- * no idea which account has room — the exact blindness pre-dispatch selection exists to
1744
- * remove. A hydrated row is still subject to the ordinary TTL, so it orders the first
1745
- * request and is replaced by a live probe immediately after.
1746
- */
1747
- let diskHydrated = false;
1748
- function hydrateAccountQuotaCache(): void {
1749
- if (diskHydrated) return;
1750
- diskHydrated = true;
1751
- for (const [key, quota] of readPersistedAccountQuotas()) {
1752
- // Disk stores observation time, not the Anthropic usage probe's clock.
1753
- if (!accountQuotaCache.has(key)) {
1754
- const anthropic = key.startsWith("anthropic\u0000");
1755
- accountQuotaCache.set(key, {
1756
- ts: anthropic ? 0 : quota.updatedAt,
1757
- quota: anthropic ? normalizeAnthropicQuota(quota, Date.now()) : quota,
1758
- });
1759
- }
1760
- }
1761
- }
1762
-
1763
- function persistAccountQuotaCache(): void {
1764
- schedulePersistAccountQuotas(function* () {
1765
- const now = Date.now();
1766
- for (const [key, entry] of accountQuotaCache) {
1767
- const quota = key.startsWith("anthropic\u0000") ? normalizeAnthropicQuota(entry.quota, now) : entry.quota;
1768
- if (quota) yield [key, quota] as [string, ProviderQuota];
1769
- }
1770
- });
1771
- }
1772
- const accountQuotaInflight = new Map<string, Promise<AccountQuotaCacheEntry>>();
1773
- let lastReconciledGeneration = 0;
1774
- let liveAccountQuotaKeys = new Set<string>();
1775
- let liveProviderQuotaKeys = new Set<string>();
1776
-
1777
- function mayCommitAccountQuotaKey(key: string, writerGeneration: number): boolean {
1778
- return writerGeneration >= lastReconciledGeneration || liveAccountQuotaKeys.has(key);
1779
- }
1780
-
1781
- function mayCommitProviderQuotaKey(key: string, writerGeneration: number): boolean {
1782
- return writerGeneration >= lastReconciledGeneration || liveProviderQuotaKeys.has(key);
1783
- }
1784
-
1785
- export interface ProviderAccountQuota {
1786
- accountId: string;
1787
- quota: ProviderQuota | null;
1788
- /** Set when the probe could not reach upstream (expired login, 429, network). */
1789
- unavailable?: true;
1790
- quotaFailure?: QuotaFailureCode;
1791
- quotaFailureIsCurrent?: () => boolean;
1792
- isCurrent?: () => boolean;
1793
- }
1794
-
1795
- /** Providers whose per-account quota can be probed. Extend as other OAuth APIs are covered. */
1796
- export function supportsPerAccountQuota(provider: string): boolean {
1797
- return provider === "anthropic" || provider === "kiro" || provider === "google-antigravity"
1798
- || explicitAccountReader(provider);
1799
- }
1800
-
1801
- function explicitAccountReader(provider: string): boolean {
1802
- return provider === "xai" || provider === "cursor" || provider === "kimi" || provider === "command-code";
1803
- }
1804
-
1805
- export function providerOAuthAccountQuotaMode(provider: string): AccountQuotaMode {
1806
- return hasPassiveAccountQuota(provider) ? "passive" : supportsPerAccountQuota(provider) ? "probe" : "unsupported";
1807
- }
1808
-
1809
- function accountCacheKey(provider: string, accountId: string): string {
1810
- return `${provider}\u0000${accountId}`;
1811
- }
1812
-
1813
- /**
1814
- * Synchronous last-good per-account quota read for routing. Never probes the network.
1815
- * Returns null when nothing is cached (or the cached row has no bars).
1816
- */
1817
- export function getCachedProviderAccountQuota(provider: string, accountId: string): ProviderQuota | null {
1818
- const entry = accountQuotaCache.get(accountCacheKey(provider, accountId));
1819
- if (entry?.isCurrent && !entry.isCurrent()) return null;
1820
- return provider === "anthropic" ? normalizeAnthropicQuota(entry?.quota, Date.now()) : entry?.quota ?? null;
1821
- }
1822
-
1823
- /** Test-only: seed or clear the per-account quota cache without probing upstream. */
1824
- export function setCachedProviderAccountQuotaForTests(
1825
- provider: string,
1826
- accountId: string,
1827
- quota: ProviderQuota | null,
1828
- ): void {
1829
- const key = accountCacheKey(provider, accountId);
1830
- if (quota === null) {
1831
- accountQuotaCache.delete(key);
1832
- return;
1833
- }
1834
- accountQuotaCache.set(key, { ts: Date.now(), quota });
1835
- }
1836
-
1837
- /** Unified headers report utilization fractions and epoch-second reset times. */
1838
- function anthropicHeaderResetAt(value: string | null): number | undefined {
1839
- const seconds = toFiniteNumber(value);
1840
- if (seconds === undefined || seconds <= 0) return undefined;
1841
- const timestamp = seconds * 1000;
1842
- return Number.isFinite(new Date(timestamp).getTime()) ? timestamp : undefined;
1843
- }
1844
-
1845
- export function parseAnthropicRateLimitHeaders(headers: Headers): ProviderQuota | null {
1846
- const fiveHourPercent = normalizeUtilizationFraction(headers.get("anthropic-ratelimit-unified-5h-utilization"));
1847
- const weeklyPercent = normalizeUtilizationFraction(headers.get("anthropic-ratelimit-unified-7d-utilization"));
1848
- if (fiveHourPercent === undefined && weeklyPercent === undefined) return null;
1849
- const fiveHourResetAt = anthropicHeaderResetAt(headers.get("anthropic-ratelimit-unified-5h-reset"));
1850
- const weeklyResetAt = anthropicHeaderResetAt(headers.get("anthropic-ratelimit-unified-7d-reset"));
1851
- return {
1852
- ...(fiveHourPercent !== undefined ? { fiveHourPercent } : {}),
1853
- ...(fiveHourPercent !== undefined && fiveHourResetAt !== undefined ? { fiveHourResetAt } : {}),
1854
- ...(weeklyPercent !== undefined ? { weeklyPercent } : {}),
1855
- ...(weeklyPercent !== undefined && weeklyResetAt !== undefined ? { weeklyResetAt } : {}),
1856
- updatedAt: Date.now(),
1857
- };
1858
- }
1859
-
1860
- /** Reject unknown scales; round fraction conversion for persisted/displayed percentages. */
1861
- function normalizeUtilizationFraction(value: string | null): number | undefined {
1862
- const numeric = toFiniteNumber(value);
1863
- if (numeric === undefined || numeric < 0 || numeric > 1) return undefined;
1864
- return Math.round(numeric * 10_000) / 100;
1865
- }
1866
-
1867
- /**
1868
- * Merge serving-account observations without advancing the usage probe's clock or
1869
- * erasing model-specific windows. The caller owns credential attribution; this guard
1870
- * prevents a retired account key from being revived by an older config generation.
1871
- */
1872
- export function recordAnthropicAccountQuotaFromHeaders(
1873
- accountId: string,
1874
- headers: Headers,
1875
- writerGeneration: number,
1876
- ): void {
1877
- if (!accountId) return;
1878
- const observed = parseAnthropicRateLimitHeaders(headers);
1879
- if (!observed) return;
1880
- const key = accountCacheKey("anthropic", accountId);
1881
- if (!mayCommitAccountQuotaKey(key, writerGeneration)) return;
1882
- // Hydrate before writing, for the same reason `recordPassiveAccountQuota` does: this write
1883
- // arrives unprompted from the request path, and `persistAccountQuotaCache` serializes the
1884
- // whole map. Landing before any reader has hydrated would persist this single row and erase
1885
- // every other provider's saved row.
1886
- hydrateAccountQuotaCache();
1887
- const previous = accountQuotaCache.get(key);
1888
- accountQuotaCache.set(key, {
1889
- ...previous,
1890
- // Headers do not prove that the last usage probe succeeded.
1891
- ts: previous?.ts ?? 0,
1892
- quota: normalizeAnthropicQuota({
1893
- ...normalizeAnthropicQuota(previous?.quota, observed.updatedAt), ...observed,
1894
- }, observed.updatedAt),
1895
- });
1896
- persistAccountQuotaCache();
1897
- }
1898
-
1899
- /**
1900
- * Providers whose per-account quota is OBSERVED in-band, never probed.
1901
- *
1902
- * Deliberately separate from `supportsPerAccountQuota` rather than folded into it. That
1903
- * predicate gates explicit upstream readers. Meta publishes no quota endpoint, so it
1904
- * remains a cache-only observation even when every probe reader is account-scoped.
1905
- */
1906
- export function hasPassiveAccountQuota(provider: string): boolean {
1907
- return provider === "meta-muse";
1908
- }
1909
-
1910
- /**
1911
- * Record a quota observed in-band on a streaming turn.
1912
- *
1913
- * The CALLER captures `writerGeneration` when it resolves the serving credential, not
1914
- * this function at write time. A streaming turn is a long await, and a generation
1915
- * captured immediately before the write cannot see a config or account change that
1916
- * happened EARLIER in the same turn — which is exactly the case the fence exists for.
1917
- */
1918
- export function recordPassiveAccountQuota(
1919
- provider: string,
1920
- accountId: string,
1921
- quota: ProviderQuota,
1922
- writerGeneration: number,
1923
- ): void {
1924
- if (!hasPassiveAccountQuota(provider) || !accountId) return;
1925
- const key = accountCacheKey(provider, accountId);
1926
- if (!mayCommitAccountQuotaKey(key, writerGeneration)) return;
1927
- // Hydrate BEFORE writing, not only on the read path. `persistAccountQuotaCache`
1928
- // serializes the whole in-memory map, so a passive write that lands before anything
1929
- // has read the cache would persist this one row and erase every other provider's
1930
- // saved row -- and `diskHydrated` would then stop any later reader from recovering
1931
- // them. A probe writer cannot hit this because its own read hydrates first; an
1932
- // observation arrives unprompted, so it must hydrate itself.
1933
- hydrateAccountQuotaCache();
1934
- accountQuotaCache.set(key, { ts: Date.now(), quota });
1935
- // Persisted so a restart keeps the last observation: with no probe to re-establish it,
1936
- // a forgotten row stays forgotten until the user happens to run another streaming turn.
1937
- persistAccountQuotaCache();
1938
- // sweepExpiredOnWrite is deliberately NOT called. Existing probe writers call it
1939
- // because they run on a poll; this runs on the request path, where a state sweep does
1940
- // not belong. Passive rows are still reclaimed by generation reconciliation
1941
- // (reconcileProviderAccountQuotaRows) and by the disk reader's age bound.
1942
- }
1943
-
1944
- /**
1945
- * Cache-only per-account rows for a passive provider. Never probes, never refreshes.
1946
- *
1947
- * An account with no observation is OMITTED rather than returned with `quota: null` and
1948
- * `unavailable`: that pair means "a probe was attempted and failed", and no probe was
1949
- * ever attempted here. A user who has not yet run a streaming turn simply has no
1950
- * measurement, which is not an error state.
1951
- */
1952
- export function readPassiveProviderAccountQuotas(provider: string): ProviderAccountQuota[] {
1953
- if (!hasPassiveAccountQuota(provider)) return [];
1954
- // Idempotent, and otherwise only reached from probe paths a passive provider never
1955
- // enters — without it a restart shows nothing until the next streaming turn, even
1956
- // though the row is sitting on disk.
1957
- hydrateAccountQuotaCache();
1958
- const set = getAccountSet(provider);
1959
- if (!set) return [];
1960
- const rows: ProviderAccountQuota[] = [];
1961
- for (const account of set.accounts) {
1962
- const entry = accountQuotaCache.get(accountCacheKey(provider, account.id));
1963
- if (entry?.quota) rows.push({ accountId: account.id, quota: entry.quota });
1964
- }
1965
- return rows;
1966
- }
1967
-
1968
- export function sweepExpiredProviderAccountQuotaRows(now = Date.now()): number {
1969
- let removed = 0;
1970
- for (const [key, entry] of accountQuotaCache) {
1971
- // Anthropic observations extend retention, never the usage probe's eligibility clock.
1972
- const retainedAt = key.startsWith("anthropic\u0000")
1973
- ? Math.max(entry.ts, entry.quota?.updatedAt ?? 0)
1974
- : entry.ts;
1975
- if (retainedAt + ACCOUNT_QUOTA_TTL_MS > now) continue;
1976
- accountQuotaCache.delete(key);
1977
- removed += 1;
1978
- }
1979
- return removed;
1980
- }
1981
-
1982
- export function reconcileProviderAccountQuotaRows(context: GenerationContext): number {
1983
- if (context.generation <= lastReconciledGeneration) return 0;
1984
- let removed = 0;
1985
- for (const key of accountQuotaCache.keys()) {
1986
- if (context.oauthAccountKeys.has(key)) continue;
1987
- accountQuotaCache.delete(key);
1988
- removed += 1;
1989
- }
1990
- // Kiro exhaustion rows are keyed identically, so they retire with their quota row; a
1991
- // verdict outliving its account would hand the replacement a cooldown it never earned.
1992
- removed += reconcileKiroAccountUsageState(context.oauthAccountKeys);
1993
- if (cache) {
1994
- const reports = cache.response.reports.filter(report => context.providerNames.has(report.provider));
1995
- removed += cache.response.reports.length - reports.length;
1996
- cache = { ...cache, response: { ...cache.response, reports } };
1997
- replaceCachedProviderQuotas(reports, routingEvidence);
1998
- }
1999
- liveAccountQuotaKeys = new Set(context.oauthAccountKeys);
2000
- liveProviderQuotaKeys = new Set(context.providerNames);
2001
- lastReconciledGeneration = context.generation;
2002
- return removed;
2003
- }
2004
-
2005
- /** Test-only reset so a direct reconcile call in one file cannot leak across files. */
2006
- export function resetProviderQuotaReconcileStateForTests(): void {
2007
- lastReconciledGeneration = 0;
2008
- liveAccountQuotaKeys = new Set();
2009
- liveProviderQuotaKeys = new Set();
2010
- }
2011
-
2012
- /** Drop cached per-account rows (all, or just one provider's). */
2013
- export function clearAccountQuotaCache(provider?: string): void {
2014
- explicitAccountEpoch += 1;
2015
- if (!provider) {
2016
- accountQuotaCache.clear();
2017
- accountQuotaInflight.clear();
2018
- clearKiroAccountUsageState();
2019
- // A cleared cache must not be re-seeded from the file it was just cleared of, and any
2020
- // pending write of the old rows is abandoned.
2021
- diskHydrated = false;
2022
- cancelPendingAccountQuotaPersist();
2023
- return;
2024
- }
2025
- const prefix = `${provider}\u0000`;
2026
- for (const key of [...accountQuotaCache.keys()]) {
2027
- if (key.startsWith(prefix)) accountQuotaCache.delete(key);
2028
- }
2029
- clearKiroAccountUsageState(prefix);
2030
- // Drop in-flight probes too so a late resolve cannot repopulate after logout/remove.
2031
- for (const key of [...accountQuotaInflight.keys()]) {
2032
- if (key.startsWith(prefix)) accountQuotaInflight.delete(key);
2033
- }
2034
- persistAccountQuotaCache();
2035
- }
2036
-
2037
- /**
2038
- * Resolve a bearer for quota probing without silently adopting a newer global
2039
- * Claude CLI credential into a background multiauth slot.
2040
- *
2041
- * - Fresh stored access → use as-is (no refresh).
2042
- * - Active account with expired access → normal refresh path.
2043
- * - Background `local-cli` with expired access → fail closed (unavailable):
2044
- * `getValidAccessTokenForAccount` can persist a mismatched Claude CLI identity.
2045
- * - Background ordinary OAuth (`source !== "local-cli"`) → safe to refresh;
2046
- * Anthropic's lock only adopts disk credentials for `local-cli` rows.
2047
- */
2048
- async function getTokenForAccountQuotaProbe(provider: string, accountId: string): Promise<string> {
2049
- const stored = getAccountCredential(provider, accountId);
2050
- if (!stored) throw new Error("account credential missing");
2051
- if (stored.expires > Date.now() + ACCOUNT_TOKEN_SKEW_MS) return stored.access;
2052
- const activeId = getAccountSet(provider)?.activeAccountId;
2053
- if (activeId !== accountId && stored.source === "local-cli") {
2054
- throw new Error("background local-cli token expired; skip CLI-adopting refresh for quota probe");
2055
- }
2056
- return getValidAccessTokenForAccount(provider, accountId);
2057
- }
2058
-
2059
- function explicitQuotaConfig(provider: string, configured?: OcxProviderConfig): OcxProviderConfig | undefined {
2060
- if (configured) return configured;
2061
- const entry = getProviderRegistryEntry(provider);
2062
- return entry ? { adapter: entry.adapter, baseUrl: entry.baseUrl, authMode: "oauth" } : undefined;
2063
- }
2064
-
2065
- function explicitQuotaIdentity(provider: string, accountId: string, configured?: OcxProviderConfig): string | undefined {
2066
- const credential = getAccountCredential(provider, accountId);
2067
- const target = explicitQuotaConfig(provider, configured);
2068
- if (!credential || !target) return undefined;
2069
- return quotaCredentialIdentity(provider, accountId, credential, target);
2070
- }
2071
-
2072
- function quotaCredentialIdentity(provider: string, accountId: string, credential: NonNullable<ReturnType<typeof getAccountCredential>>, target: OcxProviderConfig): string {
2073
- return createHash("sha256").update(JSON.stringify([
2074
- provider, accountId, credential.access, credential.refresh, credential.expires,
2075
- credential.accountId, credential.projectId, credential.source,
2076
- target.adapter, target.baseUrl, target.authMode, target.disabled === true,
2077
- ])).digest("hex");
2078
- }
2079
-
2080
- function explicitQuotaDestination(provider: string, config: OcxProviderConfig): boolean {
2081
- if (config.disabled === true || config.authMode !== "oauth") return false;
2082
- if (provider === "kimi") return isCanonicalKimiCodeBaseUrl(config.baseUrl);
2083
- if (provider === "command-code") return isCanonicalCommandCodeBaseUrl(config.baseUrl);
2084
- // These readers use fixed canonical billing origins, never config.baseUrl.
2085
- return provider === "xai" || provider === "cursor";
2086
- }
2087
-
2088
- async function readExplicitAccountQuota(provider: string, accountId: string, configured?: OcxProviderConfig): Promise<{
2089
- result: ProviderQuotaProbeResult;
2090
- identity: string | undefined;
2091
- isCurrent: () => boolean;
2092
- } | null> {
2093
- const target = explicitQuotaConfig(provider, configured);
2094
- if (!target || !explicitQuotaDestination(provider, target)) return null;
2095
- const config = { ...target };
2096
- const epoch = explicitAccountEpoch;
2097
- const accessToken = await getTokenForAccountQuotaProbe(provider, accountId);
2098
- const credential = getAccountCredential(provider, accountId);
2099
- if (!credential || credential.access !== accessToken) return null;
2100
- // Pair the post-renewal credential with the destination captured before renewal.
2101
- const identity = explicitQuotaIdentity(provider, accountId, config);
2102
- const isCurrent = () => epoch === explicitAccountEpoch
2103
- && identity === explicitQuotaIdentity(provider, accountId, configured);
2104
- if (!isCurrent()) return null;
2105
- let result: ProviderQuotaProbeResult;
2106
- switch (provider) {
2107
- case "xai": result = await fetchXaiQuota(provider, { accessToken, upstreamAccountId: credential.accountId }); break;
2108
- case "cursor": result = await fetchCursorQuota(provider, accessToken); break;
2109
- case "kimi": result = await fetchKimiQuota(provider, config, accessToken); break;
2110
- case "command-code": result = await fetchCommandCodeQuota(provider, config, accessToken); break;
2111
- default: return null;
2112
- }
2113
- return { result, identity, isCurrent };
2114
- }
2115
-
2116
- async function fetchExplicitAccountQuota(provider: string, accountId: string, force: boolean, configured?: OcxProviderConfig): Promise<AccountQuotaCacheEntry> {
2117
- const key = accountCacheKey(provider, accountId);
2118
- const identity = explicitQuotaIdentity(provider, accountId, configured);
2119
- const previous = accountQuotaCache.get(key);
2120
- const cached = identity && previous?.identity === identity && previous.isCurrent?.() ? previous : undefined;
2121
- if (!force && cached && Date.now() - cached.ts < ACCOUNT_QUOTA_TTL_MS
2122
- && (!cached.quota || Date.now() - cached.quota.updatedAt < LAST_GOOD_MAX_AGE_MS)) return cached;
2123
- const flightKey = `${key}\u0000${identity ?? "missing"}`;
2124
- const running = accountQuotaInflight.get(flightKey);
2125
- if (running) return running;
2126
- const epoch = explicitAccountEpoch;
2127
- const lastGood = cached?.quota && Date.now() - cached.quota.updatedAt < LAST_GOOD_MAX_AGE_MS ? cached.quota : null;
2128
- const flight = (async (): Promise<AccountQuotaCacheEntry> => {
2129
- let read: Awaited<ReturnType<typeof readExplicitAccountQuota>> = null;
2130
- try { read = await readExplicitAccountQuota(provider, accountId, configured); } catch { /* unavailable */ }
2131
- const isCurrent = read?.isCurrent ?? (() => epoch === explicitAccountEpoch && !!identity
2132
- && identity === explicitQuotaIdentity(provider, accountId, configured));
2133
- const result = read?.result;
2134
- const current = epoch === explicitAccountEpoch && isCurrent();
2135
- const quota = current && result && typeof result !== "symbol" ? result.quota : null;
2136
- const empty = result === AUTHORITATIVE_EMPTY_QUOTA;
2137
- const entry: AccountQuotaCacheEntry = {
2138
- ts: Date.now(),
2139
- quota: quota ?? (current && result !== TERMINAL_QUOTA_FAILURE && !empty
2140
- && lastGood && Date.now() - lastGood.updatedAt < LAST_GOOD_MAX_AGE_MS ? lastGood : null),
2141
- ...(!current || (!quota && !empty) ? { unavailable: true as const } : {}),
2142
- identity: read?.identity ?? identity,
2143
- isCurrent: () => epoch === explicitAccountEpoch && isCurrent(),
2144
- };
2145
- if (entry.isCurrent?.()) accountQuotaCache.set(key, entry);
2146
- return entry;
2147
- })().finally(() => { if (accountQuotaInflight.get(flightKey) === flight) accountQuotaInflight.delete(flightKey); });
2148
- accountQuotaInflight.set(flightKey, flight);
2149
- return flight;
2150
- }
2151
-
2152
- async function fetchExplicitCurrentQuota(provider: string, config: OcxProviderConfig, liveConfig: OcxConfig): Promise<ProviderQuotaProbeResult> {
2153
- const id = getAccountSet(provider)?.activeAccountId;
2154
- if (!id) return null;
2155
- const read = await readExplicitAccountQuota(provider, id, config);
2156
- if (!read) return null;
2157
- const isCurrent = () => liveConfig.providers[provider] === config
2158
- && read.isCurrent() && getAccountSet(provider)?.activeAccountId === id;
2159
- if (!isCurrent()) return TERMINAL_QUOTA_FAILURE;
2160
- if (read.result && typeof read.result !== "symbol") accountReportCurrent.set(read.result, isCurrent);
2161
- return read.result;
2162
- }
2163
-
2164
- function antigravityQuotaDiagnosticIdentity(accountId: string, credential = getAccountCredential("google-antigravity", accountId)): string | undefined {
2165
- return credential ? quotaCredentialIdentity("google-antigravity", accountId, credential, {
2166
- adapter: "google", baseUrl: ANTIGRAVITY_ACCOUNT_QUOTA_BASE, authMode: "oauth",
2167
- }) : undefined;
2168
- }
2169
-
2170
- async function fetchAccountQuota(
2171
- provider: string,
2172
- accountId: string,
2173
- forceRefresh: boolean,
2174
- providerConfig?: OcxProviderConfig,
2175
- ): Promise<AccountQuotaCacheEntry> {
2176
- if (!supportsPerAccountQuota(provider)) return { ts: Date.now(), quota: null, unavailable: true };
2177
- if (explicitAccountReader(provider)) return fetchExplicitAccountQuota(provider, accountId, forceRefresh, providerConfig);
2178
- if (provider === "anthropic") hydrateAccountQuotaCache();
2179
- const key = accountCacheKey(provider, accountId);
2180
- const writerGeneration = captureConfigGeneration();
2181
- const cached = accountQuotaCache.get(key);
2182
- if (!forceRefresh && cached && Date.now() - cached.ts < ACCOUNT_QUOTA_TTL_MS) {
2183
- if (provider === "google-antigravity" && cached.quotaFailure && cached.quotaFailureIsCurrent?.() !== true) return { ...cached, quotaFailure: undefined };
2184
- return provider === "anthropic" ? { ...cached, quota: normalizeAnthropicQuota(cached.quota, Date.now()) } : cached;
2185
- }
2186
- const joinable = accountQuotaInflight.get(key);
2187
- if (joinable) return joinable;
2188
-
2189
- const epoch = explicitAccountEpoch;
2190
- const probe = (async (): Promise<AccountQuotaCacheEntry> => {
2191
- let diagnosticIdentity: string | undefined;
2192
- let quotaFailure: QuotaFailureCode | undefined;
2193
- const quotaFailureIsCurrent = () => {
2194
- try { return epoch === explicitAccountEpoch && diagnosticIdentity !== undefined && diagnosticIdentity === antigravityQuotaDiagnosticIdentity(accountId); }
2195
- catch { return false; }
2196
- };
2197
- const diagnosticFields = () => quotaFailure && quotaFailureIsCurrent() ? { quotaFailure, quotaFailureIsCurrent } : {};
2198
- try {
2199
- if (provider === "google-antigravity") diagnosticIdentity = antigravityQuotaDiagnosticIdentity(accountId);
2200
- let quota: ProviderQuota | null;
2201
- let kiroSnapshot: KiroUsageSnapshot | null = null;
2202
- if (provider === "kiro") {
2203
- // Kiro resolves the bearer and its routing metadata from ONE account-scoped
2204
- // snapshot. It deliberately does not use getTokenForAccountQuotaProbe: that
2205
- // helper refuses to refresh a background `local-cli` slot because Anthropic's
2206
- // lock can adopt a mismatched Claude CLI identity, but Kiro marks every
2207
- // CLI-imported credential `local-cli`, so the same rule would blank the quota of
2208
- // every inactive pool account the moment its token expired.
2209
- kiroSnapshot = await fetchKiroUsageSnapshot(await kiroUsageContextForAccount(accountId));
2210
- quota = kiroSnapshot?.quota ?? null;
2211
- } else {
2212
- const token = await getTokenForAccountQuotaProbe(provider, accountId);
2213
- if (provider === "google-antigravity") {
2214
- // Per-account Gem/Cla windows (#1082). The project id is part of the stored
2215
- // credential; without it the probe cannot be made, and that is "unavailable",
2216
- // never 0%.
2217
- const credential = getAccountCredential(provider, accountId);
2218
- diagnosticIdentity = credential?.access === token ? antigravityQuotaDiagnosticIdentity(accountId, credential) : undefined;
2219
- if (!diagnosticIdentity || !credential?.projectId) throw new Error("antigravity account unavailable");
2220
- const result = await probeAntigravityUsageQuota(token, credential.projectId);
2221
- quota = result.kind === "available" ? result.quota : null;
2222
- if (result.kind === "unavailable") quotaFailure = result.failure;
2223
- } else if (provider === "anthropic") {
2224
- quota = await fetchAnthropicUsageQuota(token);
2225
- } else {
2226
- return { ts: Date.now(), quota: null, unavailable: true };
2227
- }
2228
- }
2229
- if (!quota) {
2230
- // Preserve last-good bars and mark unavailable; advance TTL so failures
2231
- // negative-cache instead of re-probing on every GUI poll.
2232
- const entry: AccountQuotaCacheEntry = {
2233
- ts: Date.now(),
2234
- // Settle once for all joiners against observations committed during the probe.
2235
- quota: provider === "anthropic"
2236
- ? normalizeAnthropicQuota(accountQuotaCache.get(key)?.quota, Date.now()) : cached?.quota ?? null,
2237
- unavailable: true,
2238
- ...diagnosticFields(),
2239
- };
2240
- if (mayCommitAccountQuotaKey(key, writerGeneration)) {
2241
- accountQuotaCache.set(key, entry);
2242
- if (provider === "kiro") commitKiroAccountUsageState(key, null);
2243
- sweepExpiredOnWrite(entry.ts);
2244
- }
2245
- return entry;
2246
- }
2247
- const entry: AccountQuotaCacheEntry = {
2248
- ts: Date.now(), quota: provider === "anthropic" ? normalizeAnthropicQuota(quota, Date.now()) : quota,
2249
- };
2250
- if (mayCommitAccountQuotaKey(key, writerGeneration)) {
2251
- accountQuotaCache.set(key, entry);
2252
- // Exhaustion state rides the SAME commit guard as the quota row: a probe from a
2253
- // superseded config generation must not publish either half.
2254
- if (provider === "kiro") commitKiroAccountUsageState(key, kiroSnapshot);
2255
- sweepExpiredOnWrite(entry.ts);
2256
- }
2257
- return entry;
2258
- } catch {
2259
- if (provider === "google-antigravity") quotaFailure = "account_unavailable";
2260
- const entry: AccountQuotaCacheEntry = {
2261
- ts: Date.now(),
2262
- quota: provider === "anthropic"
2263
- ? normalizeAnthropicQuota(accountQuotaCache.get(key)?.quota, Date.now()) : cached?.quota ?? null,
2264
- unavailable: true,
2265
- ...diagnosticFields(),
2266
- };
2267
- if (mayCommitAccountQuotaKey(key, writerGeneration)) {
2268
- accountQuotaCache.set(key, entry);
2269
- sweepExpiredOnWrite(entry.ts);
2270
- }
2271
- return entry;
2272
- }
2273
- })().finally(() => {
2274
- if (accountQuotaInflight.get(key) === probe) accountQuotaInflight.delete(key);
2275
- });
2276
- accountQuotaInflight.set(key, probe);
2277
- return probe;
2278
- }
2279
-
2280
- /**
2281
- * Per-account quota rows for a provider's logged-in accounts. Probes run in parallel; a
2282
- * single failing account never blocks the others.
2283
- */
2284
- export async function fetchProviderAccountQuotas(
2285
- provider: string,
2286
- forceRefresh = false,
2287
- providerConfig?: OcxProviderConfig,
2288
- ): Promise<ProviderAccountQuota[]> {
2289
- if (!supportsPerAccountQuota(provider)) return [];
2290
- const set = getAccountSet(provider);
2291
- if (!set) return [];
2292
- return mapQuotaRoster(set.accounts, async account => {
2293
- const entry = await fetchAccountQuota(provider, account.id, forceRefresh, providerConfig);
2294
- const result: ProviderAccountQuota = {
2295
- accountId: account.id,
2296
- quota: provider === "anthropic" ? normalizeAnthropicQuota(entry.quota, Date.now()) : entry.quota,
2297
- ...(entry.unavailable ? { unavailable: true as const } : {}),
2298
- ...(entry.unavailable && entry.quotaFailure && entry.quotaFailureIsCurrent?.() === true ? { quotaFailure: entry.quotaFailure } : {}),
2299
- };
2300
- if (entry.quotaFailureIsCurrent) Object.defineProperty(result, "quotaFailureIsCurrent", { value: entry.quotaFailureIsCurrent });
2301
- if (!explicitAccountReader(provider)) return result;
2302
- const identity = entry.identity;
2303
- Object.defineProperty(result, "isCurrent", { value: () => {
2304
- if (entry.isCurrent) return entry.isCurrent();
2305
- const credential = getAccountCredential(provider, account.id);
2306
- return !!credential && (!identity || explicitQuotaIdentity(provider, account.id, providerConfig) === identity);
2307
- } });
2308
- return result;
2309
- });
2310
- }
2311
-
2312
- function normalizedBaseUrl(value: string): string | null {
2313
- try {
2314
- const url = new URL(value);
2315
- if (url.username || url.password || url.search || url.hash) return null;
2316
- return `${url.origin.toLowerCase()}${url.pathname.replace(/\/+$/, "")}`;
2317
- } catch {
2318
- return null;
2319
- }
2320
- }
2321
-
2322
- function quotaResetAt(row: Record<string, unknown>): number | undefined {
2323
- return normalizeResetAt(row.resetTime ?? row.resetAt ?? row.reset_time ?? row.reset_at);
2324
- }
2325
-
2326
- function isCanonicalKimiCodeBaseUrl(baseUrl: string): boolean {
2327
- return normalizedBaseUrl(baseUrl) === KIMI_CODE_BASE_URL;
2328
- }
2329
-
2330
- function isCanonicalCommandCodeBaseUrl(baseUrl: string): boolean {
2331
- const normalized = normalizedBaseUrl(baseUrl);
2332
- // OAuth preset points at the API root; the Provider-API preset at /provider/v1.
2333
- return normalized === COMMAND_CODE_BASE_URL || normalized === `${COMMAND_CODE_BASE_URL}/provider/v1`;
2334
- }
2335
-
2336
- /** Prefer the nested `data` shell when the outer object is only an envelope. */
2337
- function unwrapKimiQuotaPayload(value: unknown): Record<string, unknown> | null {
2338
- const body = asRecord(value);
2339
- if (!body) return null;
2340
- const nested = asRecord(body.data);
2341
- if (!nested) return body;
2342
- // A null/non-usable outer field is a placeholder, not data — an envelope like
2343
- // { usage: null, data: { usage: {...} } } must still unwrap to the nested payload.
2344
- const usable = (field: unknown): boolean => field !== undefined && field !== null;
2345
- const outerHasUsage = usable(body.usage) || usable(body.limits) || usable(body.totalQuota);
2346
- const nestedHasUsage = usable(nested.usage) || usable(nested.limits) || usable(nested.totalQuota);
2347
- return !outerHasUsage && nestedHasUsage ? nested : body;
2348
- }
2349
-
2350
- function kimiLimitLabel(item: Record<string, unknown>, detail: Record<string, unknown>): string {
2351
- return [item.name, item.title, item.scope, detail.name, detail.title]
2352
- .filter((value): value is string => typeof value === "string")
2353
- .join(" ")
2354
- .toLowerCase();
2355
- }
2356
-
2357
- function parseKimiQuotaRow(value: unknown, resetFallback?: Record<string, unknown>): { percent: number; resetAt?: number } | null {
2358
- const row = asRecord(value);
2359
- if (!row) return null;
2360
- const resetAt = quotaResetAt(row) ?? (resetFallback ? quotaResetAt(resetFallback) : undefined);
2361
- const limit = toFiniteNumber(row.limit);
2362
- if (limit !== undefined && limit > 0) {
2363
- let used = toFiniteNumber(row.used);
2364
- if (used === undefined) {
2365
- const remaining = toFiniteNumber(row.remaining);
2366
- if (remaining !== undefined) used = limit - remaining;
2367
- }
2368
- if (used !== undefined) {
2369
- const percent = normalizePercent((used / limit) * 100);
2370
- if (percent !== undefined) return { percent, ...(resetAt !== undefined ? { resetAt } : {}) };
2371
- }
2372
- }
2373
- // Some payloads expose utilisation directly when limit/used arithmetic is absent.
2374
- const direct = normalizePercent(row.utilization ?? row.percent ?? row.usedPercent ?? row.used_percent);
2375
- return direct === undefined ? null : { percent: direct, ...(resetAt !== undefined ? { resetAt } : {}) };
2376
- }
2377
-
2378
- function isKimiFiveHourLimit(item: Record<string, unknown>, detail: Record<string, unknown>, window: Record<string, unknown>): boolean {
2379
- const duration = toFiniteNumber(window.duration ?? item.duration ?? detail.duration);
2380
- const unit = String(window.timeUnit ?? item.timeUnit ?? detail.timeUnit ?? "").toUpperCase();
2381
- if ((unit.includes("MINUTE") && duration === 300) || (unit.includes("HOUR") && duration === 5)) return true;
2382
- return /(^|\b)5\s*(?:h|hour)/.test(kimiLimitLabel(item, detail));
2383
- }
2384
-
2385
- function isKimiWeeklyLimit(item: Record<string, unknown>, detail: Record<string, unknown>, window: Record<string, unknown>): boolean {
2386
- const duration = toFiniteNumber(window.duration ?? item.duration ?? detail.duration);
2387
- const unit = String(window.timeUnit ?? item.timeUnit ?? detail.timeUnit ?? "").toUpperCase();
2388
- if ((unit.includes("DAY") && duration === 7) || (unit.includes("HOUR") && duration === 168)) return true;
2389
- return /weekly|7\s*(?:d|day)/.test(kimiLimitLabel(item, detail));
2390
- }
2391
-
2392
- function parseKimiQuotaPayload(value: unknown): ProviderQuota | null {
2393
- const body = unwrapKimiQuotaPayload(value);
2394
- if (!body) return null;
2395
- let weekly = parseKimiQuotaRow(body.usage);
2396
- const total = parseKimiQuotaRow(body.totalQuota);
2397
- let fiveHour: { percent: number; resetAt?: number } | null = null;
2398
- if (Array.isArray(body.limits)) {
2399
- for (const rawItem of body.limits) {
2400
- const item = asRecord(rawItem);
2401
- if (!item) continue;
2402
- const detail = asRecord(item.detail) ?? item;
2403
- const window = asRecord(item.window) ?? {};
2404
- if (!fiveHour && isKimiFiveHourLimit(item, detail, window)) {
2405
- fiveHour = parseKimiQuotaRow(detail, window);
2406
- }
2407
- if (!weekly && isKimiWeeklyLimit(item, detail, window)) {
2408
- weekly = parseKimiQuotaRow(detail, window);
2409
- }
2410
- if (fiveHour && weekly) break;
2411
- }
2412
- }
2413
- const quota: ProviderQuota = {
2414
- ...(fiveHour ? {
2415
- fiveHourPercent: fiveHour.percent,
2416
- ...(fiveHour.resetAt !== undefined ? { fiveHourResetAt: fiveHour.resetAt } : {}),
2417
- } : {}),
2418
- ...(weekly ? {
2419
- weeklyPercent: weekly.percent,
2420
- ...(weekly.resetAt !== undefined ? { weeklyResetAt: weekly.resetAt } : {}),
2421
- } : {}),
2422
- ...(total ? { customWindows: [{ label: "Total subscription credits", percent: total.percent, ...(total.resetAt !== undefined ? { resetAt: total.resetAt } : {}) }] } : {}),
2423
- updatedAt: Date.now(),
2424
- };
2425
- return hasQuotaRows(quota) ? quota : null;
2426
- }
2427
-
2428
- async function resolveKimiQuotaBearer(config: OcxProviderConfig, accountId?: string): Promise<string | null> {
2429
- if (config.authMode === "oauth") {
2430
- try {
2431
- return accountId ? await getTokenForAccountQuotaProbe("kimi", accountId) : null;
2432
- } catch {
2433
- return null;
2434
- }
2435
- }
2436
- // ACTIVE key only: silently walking apiKeyPool when the primary env reference is
2437
- // unresolved would render a quota bar for a DIFFERENT account than the one routing
2438
- // requests — a wrong meter is worse than no meter.
2439
- const primary = resolveProviderApiKey(config.apiKey)?.trim();
2440
- return primary || null;
2441
- }
2442
-
2443
- async function fetchKimiQuota(provider: string, config: OcxProviderConfig, accessToken: string): Promise<ProviderQuotaReport | null> {
2444
- // Never release credentials to a user-edited or lookalike provider host.
2445
- if (!isCanonicalKimiCodeBaseUrl(config.baseUrl)) return null;
2446
- if (!accessToken) return null;
2447
- const response = await fetch(KIMI_CODE_USAGE_URL, {
2448
- headers: { Accept: "application/json", Authorization: `Bearer ${accessToken}` },
2449
- redirect: "error",
2450
- signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
2451
- });
2452
- if (!response.ok) return null;
2453
- const quota = parseKimiQuotaPayload(await readQuotaJson(response));
2454
- return quota ? keyReport(provider, "kimi:usages", quota, config, accessToken, quota) : null;
2455
- }
2456
-
2457
- /**
2458
- * Command Code rolling window: `{ cap, used, resetAt }` off /alpha/billing/credits,
2459
- * normalized to a percent with an optional reset timestamp.
2460
- */
2461
- function parseCommandCodeWindow(value: unknown): { percent: number; resetAt?: number } | null {
2462
- const row = asRecord(value);
2463
- if (!row) return null;
2464
- const cap = toFiniteNumber(row.cap);
2465
- const used = toFiniteNumber(row.used);
2466
- if (cap === undefined || used === undefined || cap <= 0 || used < 0) return null;
2467
- const percent = normalizePercent((used / cap) * 100);
2468
- if (percent === undefined) return null;
2469
- const resetAt = quotaResetAt(row);
2470
- return { percent, ...(resetAt !== undefined ? { resetAt } : {}) };
2471
- }
2472
-
2473
- /** Soft-fail GET returning a parsed record, or null when unavailable. */
2474
- async function fetchCommandCodeJson(url: string, bearer: string): Promise<Record<string, unknown> | null> {
2475
- try {
2476
- const response = await fetch(url, {
2477
- headers: { Accept: "application/json", Authorization: `Bearer ${bearer}` },
2478
- redirect: "error",
2479
- signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
2480
- });
2481
- if (!response.ok) return null;
2482
- return asRecord(await readQuotaJson(response));
2483
- } catch {
2484
- return null;
2485
- }
2486
- }
2487
-
2488
- /**
2489
- * Soft-fail period spend (used) against the remaining credit pools → creditsUsd.
2490
- * Period scoping: `since=<currentPeriodStart>` keeps spend aligned with the
2491
- * pools' billing cycle, and `currentPeriodEnd` becomes expiresAt.
2492
- */
2493
- async function fetchCommandCodeSpend(
2494
- bearer: string,
2495
- credits: Record<string, unknown> | null,
2496
- orgQuery: string,
2497
- ): Promise<ProviderQuotaCreditsUsd | undefined> {
2498
- if (!credits) return undefined;
2499
- const subscriptionBody = await fetchCommandCodeJson(`${COMMAND_CODE_SUBSCRIPTIONS_URL}${orgQuery}`, bearer);
2500
- const subscription = asRecord(subscriptionBody?.data) ?? subscriptionBody;
2501
- const periodStart = typeof subscription?.currentPeriodStart === "string" ? subscription.currentPeriodStart.trim() : "";
2502
- // Unscoped /usage/summary is lifetime spend; mixing it with current-cycle
2503
- // remaining pools produces a wrong percent. Omit creditsUsd until a period exists.
2504
- if (!periodStart) return undefined;
2505
- const sinceQuery = `${orgQuery ? "&" : "?"}since=${encodeURIComponent(periodStart)}`;
2506
- const expiresAt = normalizeResetAt(subscription?.currentPeriodEnd);
2507
- const summaryBody = await fetchCommandCodeJson(`${COMMAND_CODE_USAGE_URL}${orgQuery}${sinceQuery}`, bearer);
2508
- const summary = asRecord(summaryBody?.data) ?? summaryBody;
2509
- const used = toFiniteNumber(summary?.totalCost) ?? toFiniteNumber(summary?.totalMonthlyCredits);
2510
- if (used === undefined || used < 0) return undefined;
2511
- const pools = [credits.monthlyCredits, credits.purchasedCredits, credits.freeCredits]
2512
- .map(value => toFiniteNumber(value))
2513
- .filter((value): value is number => value !== undefined);
2514
- // Field presence is what separates a real balance from absent data: an exhausted
2515
- // all-zero account still reports remaining=0, while no remaining-credit field at
2516
- // all means there is nothing to meter.
2517
- if (pools.length === 0) return undefined;
2518
- const remaining = pools.reduce((sum, value) => sum + Math.max(0, value ?? 0), 0);
2519
- const limit = used + remaining;
2520
- const percent = normalizePercent(limit > 0 ? (used / limit) * 100 : 0);
2521
- // Purchased credits roll over past the subscription period end, so an expiry is
2522
- // only truthful when the aggregate contains no non-expiring purchased pool.
2523
- const purchased = toFiniteNumber(credits.purchasedCredits) ?? 0;
2524
- return percent === undefined
2525
- ? undefined
2526
- : {
2527
- used,
2528
- limit,
2529
- remaining,
2530
- percent,
2531
- ...(expiresAt !== undefined && purchased <= 0 ? { expiresAt } : {}),
2532
- };
2533
- }
2534
-
2535
- /** OAuth access token or ACTIVE Provider-API key for the Command Code quota probe. */
2536
- async function resolveCommandCodeQuotaBearer(config: OcxProviderConfig, accountId?: string): Promise<string | null> {
2537
- if (config.authMode === "oauth") {
2538
- try {
2539
- return accountId ? await getTokenForAccountQuotaProbe("command-code", accountId) : null;
2540
- } catch {
2541
- return null;
2542
- }
2543
- }
2544
- // ACTIVE key only: a quota bar for a different account than the one routing
2545
- // requests is a wrong meter, not a helpful one.
2546
- return resolveProviderApiKey(config.apiKey)?.trim() || null;
2547
- }
2548
-
2549
- /**
2550
- * Command Code `GET /alpha/billing/credits` — the same Bearer surface the CLI's
2551
- * usage view uses (windowLimits.fiveHour / windowLimits.weekly), plus soft
2552
- * whoami (team orgId scoping) and subscription-scoped spend for creditsUsd.
2553
- */
2554
- async function fetchCommandCodeQuota(provider: string, config: OcxProviderConfig, bearer: string): Promise<ProviderQuotaProbeResult> {
2555
- // Never release credentials to a user-edited or lookalike provider host.
2556
- if (!isCanonicalCommandCodeBaseUrl(config.baseUrl)) return null;
2557
- if (!bearer) return null;
2558
- const whoamiBody = await fetchCommandCodeJson(COMMAND_CODE_WHOAMI_URL, bearer);
2559
- const whoami = asRecord(whoamiBody?.data) ?? whoamiBody;
2560
- const org = asRecord(whoami?.org);
2561
- const orgId = typeof org?.id === "string" && org.id.trim() ? org.id.trim() : null;
2562
- const orgQuery = orgId ? `?orgId=${encodeURIComponent(orgId)}` : "";
2563
- const response = await fetch(`${COMMAND_CODE_CREDITS_URL}${orgQuery}`, {
2564
- headers: { Accept: "application/json", Authorization: `Bearer ${bearer}` },
2565
- redirect: "error",
2566
- signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
2567
- });
2568
- if (!response.ok) {
2569
- return response.status >= 400 && response.status < 500 && response.status !== 408 && response.status !== 429
2570
- ? TERMINAL_QUOTA_FAILURE
2571
- : null;
2572
- }
2573
- const raw = asRecord(await readQuotaJson(response));
2574
- const body = asRecord(raw?.data) ?? raw;
2575
- const credits = asRecord(body?.credits);
2576
- const limits = asRecord(body?.windowLimits);
2577
- if (!credits && !limits) return null;
2578
- const fiveHour = parseCommandCodeWindow(limits?.fiveHour);
2579
- const weekly = parseCommandCodeWindow(limits?.weekly);
2580
- const creditsUsd = await fetchCommandCodeSpend(bearer, credits, orgQuery);
2581
- const quota: ProviderQuota = {
2582
- ...(fiveHour ? {
2583
- fiveHourPercent: fiveHour.percent,
2584
- ...(fiveHour.resetAt !== undefined ? { fiveHourResetAt: fiveHour.resetAt } : {}),
2585
- } : {}),
2586
- ...(weekly ? {
2587
- weeklyPercent: weekly.percent,
2588
- ...(weekly.resetAt !== undefined ? { weeklyResetAt: weekly.resetAt } : {}),
2589
- } : {}),
2590
- ...(creditsUsd ? { creditsUsd } : {}),
2591
- updatedAt: Date.now(),
2592
- };
2593
- // Rolling windows and the credit balance both gate inference on this bearer.
2594
- return keyReport(provider, "command-code:credits", quota, config, bearer, quota);
2595
- }
2596
-
2597
- /** Cursor included usage via api2.cursor.sh (Bearer from OAuth) — unofficial, may change. */
2598
- async function fetchCursorQuota(provider: string, accessToken: string): Promise<ProviderQuotaReport | null> {
2599
-
2600
- const authHeaders = {
2601
- Accept: "application/json",
2602
- Authorization: `Bearer ${accessToken}`,
2603
- "User-Agent": "opencodex-quota",
2604
- } as const;
2605
-
2606
- // Prefer dashboard period usage (Pro/Team/Ultra spend allowance in USD cents).
2607
- // Field names follow Cursor's Connect RPC shape (limit/remaining/includedSpend), not usedCents.
2608
- try {
2609
- const periodRes = await fetch("https://api2.cursor.sh/aiserver.v1.DashboardService/GetCurrentPeriodUsage", {
2610
- method: "POST",
2611
- redirect: "error",
2612
- headers: {
2613
- ...authHeaders,
2614
- "Content-Type": "application/json",
2615
- "Connect-Protocol-Version": "1",
2616
- },
2617
- body: "{}",
2618
- signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
2619
- });
2620
- if (periodRes.ok) {
2621
- const body = asRecord(await readQuotaJson(periodRes));
2622
- const planUsage = asRecord(body?.planUsage);
2623
- if (planUsage) {
2624
- const resetAt = normalizeResetAt(body?.billingCycleEnd ?? planUsage.billingCycleEnd ?? body?.periodEnd);
2625
-
2626
- // Primary meter: overall included allowance (Cursor Settings → Usage total %).
2627
- // autoPercentUsed / apiPercentUsed are secondary pools and must not replace the total.
2628
- const limit = toFiniteNumber(planUsage.limit ?? planUsage.limitCents ?? planUsage.totalLimitCents);
2629
- const remaining = toFiniteNumber(planUsage.remaining ?? planUsage.remainingCents);
2630
- const includedSpend = toFiniteNumber(planUsage.includedSpend ?? planUsage.usedCents ?? planUsage.used);
2631
- const totalSpend = toFiniteNumber(planUsage.totalSpend);
2632
- let used: number | undefined;
2633
- if (includedSpend !== undefined) used = includedSpend;
2634
- else if (limit !== undefined && remaining !== undefined) used = Math.max(0, limit - remaining);
2635
- else if (totalSpend !== undefined) used = totalSpend;
2636
- const totalPercent = normalizePercent(planUsage.totalPercentUsed ?? planUsage.percentUsed)
2637
- ?? (limit !== undefined && limit > 0 && used !== undefined
2638
- ? normalizePercent((used / limit) * 100)
2639
- : undefined);
2640
-
2641
- const autoPercent = normalizePercent(planUsage.autoPercentUsed);
2642
- const apiPercent = normalizePercent(planUsage.apiPercentUsed);
2643
- const customWindows: ProviderQuotaWindow[] = [];
2644
- if (autoPercent !== undefined) {
2645
- customWindows.push({
2646
- label: "First-party models",
2647
- percent: autoPercent,
2648
- ...(resetAt !== undefined ? { resetAt } : {}),
2649
- });
2650
- }
2651
- if (apiPercent !== undefined) {
2652
- customWindows.push({
2653
- label: "API usage",
2654
- percent: apiPercent,
2655
- ...(resetAt !== undefined ? { resetAt } : {}),
2656
- });
2657
- }
2658
-
2659
- if (totalPercent !== undefined || customWindows.length > 0) {
2660
- const built = report(provider, "cursor:period-usage", {
2661
- ...(totalPercent !== undefined ? {
2662
- monthlyPercent: totalPercent,
2663
- ...(resetAt !== undefined ? { monthlyResetAt: resetAt } : {}),
2664
- } : {}),
2665
- ...(customWindows.length > 0 ? { customWindows } : {}),
2666
- updatedAt: Date.now(),
2667
- });
2668
- if (built) return { ...built, reverseEngineered: true };
2669
- }
2670
- }
2671
- }
2672
- } catch {
2673
- /* fall through */
2674
- }
2675
-
2676
- // /api/usage/summary — same host, sometimes richer than /auth/usage for Team plans.
2677
- try {
2678
- const summaryRes = await fetch("https://api2.cursor.sh/api/usage/summary", {
2679
- headers: authHeaders,
2680
- redirect: "error",
2681
- signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
2682
- });
2683
- if (summaryRes.ok) {
2684
- const body = asRecord(await readQuotaJson(summaryRes));
2685
- const individual = asRecord(body?.individualUsage);
2686
- const plan = asRecord(individual?.plan);
2687
- if (plan) {
2688
- const used = toFiniteNumber(plan.used);
2689
- const limit = toFiniteNumber(plan.limit);
2690
- const percent = normalizePercent(plan.totalPercentUsed)
2691
- ?? (used !== undefined && limit !== undefined && limit > 0
2692
- ? normalizePercent((used / limit) * 100)
2693
- : undefined);
2694
- if (percent !== undefined) {
2695
- const built = report(provider, "cursor:usage-summary", {
2696
- monthlyPercent: percent,
2697
- monthlyResetAt: normalizeResetAt(body?.billingCycleEnd),
2698
- updatedAt: Date.now(),
2699
- });
2700
- if (built) return { ...built, reverseEngineered: true };
2701
- }
2702
- }
2703
- }
2704
- } catch {
2705
- /* fall through to /auth/usage */
2706
- }
2707
-
2708
- const response = await fetch("https://api2.cursor.sh/auth/usage", {
2709
- headers: authHeaders,
2710
- redirect: "error",
2711
- signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
2712
- });
2713
- if (!response.ok) return null;
2714
- const body = asRecord(await readQuotaJson(response));
2715
- if (!body) return null;
2716
-
2717
- // Prefer the gpt-4 bucket (historical "fast requests"); else first model with used+limit.
2718
- let used: number | undefined;
2719
- let limit: number | undefined;
2720
- const gpt4 = asRecord(body["gpt-4"]);
2721
- if (gpt4) {
2722
- used = toFiniteNumber(gpt4.numRequests ?? gpt4.used);
2723
- limit = toFiniteNumber(gpt4.maxRequestUsage ?? gpt4.limit ?? gpt4.maxRequests);
2724
- }
2725
- if (used === undefined || limit === undefined || limit <= 0) {
2726
- for (const [key, value] of Object.entries(body)) {
2727
- if (key === "startOfMonth" || key === "billingCycleStart") continue;
2728
- const bucket = asRecord(value);
2729
- if (!bucket) continue;
2730
- const bucketUsed = toFiniteNumber(bucket.numRequests ?? bucket.used);
2731
- const bucketLimit = toFiniteNumber(bucket.maxRequestUsage ?? bucket.limit ?? bucket.maxRequests);
2732
- if (bucketUsed !== undefined && bucketLimit !== undefined && bucketLimit > 0) {
2733
- used = bucketUsed;
2734
- limit = bucketLimit;
2735
- break;
2736
- }
2737
- }
2738
- }
2739
- if (used === undefined || limit === undefined || limit <= 0) return null;
2740
- const percent = normalizePercent((used / limit) * 100);
2741
- if (percent === undefined) return null;
2742
- const startOfMonth = normalizeResetAt(body.startOfMonth ?? body.billingCycleStart);
2743
- // Next reset = same day next month, computed in UTC to avoid timezone-shifted rollover.
2744
- const monthlyResetAt = startOfMonth !== undefined
2745
- ? (() => {
2746
- const start = new Date(startOfMonth);
2747
- return Date.UTC(start.getUTCFullYear(), start.getUTCMonth() + 1, start.getUTCDate());
2748
- })()
2749
- : undefined;
2750
- const built = report(provider, "cursor:auth-usage", {
2751
- monthlyPercent: percent,
2752
- ...(monthlyResetAt !== undefined ? { monthlyResetAt } : {}),
2753
- updatedAt: Date.now(),
2754
- });
2755
- return built ? { ...built, reverseEngineered: true } : null;
2756
- }
2757
-
2758
- function quotaInfoEntries(modelInfo: Record<string, unknown>): Record<string, unknown>[] {
2759
- const entries: Record<string, unknown>[] = [];
2760
- const add = (value: unknown, tier?: string) => {
2761
- const rec = asRecord(value);
2762
- if (!rec) return;
2763
- entries.push(tier ? { ...rec, tier } : rec);
2764
- };
2765
- const addArray = (value: unknown) => {
2766
- if (!Array.isArray(value)) return;
2767
- for (const entry of value) add(entry);
2768
- };
2769
-
2770
- if (Array.isArray(modelInfo.quotaInfo)) addArray(modelInfo.quotaInfo);
2771
- else add(modelInfo.quotaInfo);
2772
- addArray(modelInfo.quotaInfos);
2773
-
2774
- const byTier = asRecord(modelInfo.quotaInfoByTier);
2775
- if (byTier) {
2776
- for (const [tier, value] of Object.entries(byTier)) {
2777
- if (Array.isArray(value)) {
2778
- for (const entry of value) add(entry, tier);
2779
- } else {
2780
- add(value, tier);
2781
- }
2782
- }
2783
- }
2784
- return entries;
2785
- }
2786
-
2787
- function classifyAntigravityFamily(modelId: string, modelInfo: Record<string, unknown>, quotaInfo: Record<string, unknown>): "Gem" | "Cla" | null {
2788
- const displayName = typeof modelInfo.displayName === "string" ? modelInfo.displayName : "";
2789
- const tier = typeof quotaInfo.tier === "string" ? quotaInfo.tier : "";
2790
- const haystack = `${modelId} ${displayName} ${tier}`.toLowerCase();
2791
- if (haystack.includes("gemini")) return "Gem";
2792
- if (haystack.includes("claude") || haystack.includes("opus") || haystack.includes("sonnet") || haystack.includes("gpt-oss") || haystack.includes("gpt_oss")) return "Cla";
2793
- return null;
2794
- }
2795
-
2796
- function antigravityUsedPercent(quotaInfo: Record<string, unknown>): number | undefined {
2797
- const target = asRecord(quotaInfo.remaining) ?? quotaInfo;
2798
- const remaining = normalizePercent(toFiniteNumber(target.remainingFraction) !== undefined
2799
- ? toFiniteNumber(target.remainingFraction)! * 100
2800
- : toFiniteNumber(target.remainingPercentage) !== undefined
2801
- ? toFiniteNumber(target.remainingPercentage)! * 100
2802
- : undefined);
2803
- if (remaining === undefined) return undefined;
2804
- return normalizePercent(100 - remaining);
2805
- }
2806
-
2807
- /** Gem/Cla windows from a `fetchAvailableModels` body; shared by the provider and account probes. */
2808
- function antigravityWindowsFromModels(body: Record<string, unknown> | null): ProviderQuotaWindow[] {
2809
- const models = asRecord(body?.models);
2810
- if (!models) return [];
2811
-
2812
- const windows = new Map<string, ProviderQuotaWindow>();
2813
- for (const [modelId, rawModelInfo] of Object.entries(models)) {
2814
- const modelInfo = asRecord(rawModelInfo);
2815
- if (!modelInfo) continue;
2816
- for (const quotaInfo of quotaInfoEntries(modelInfo)) {
2817
- const label = classifyAntigravityFamily(modelId, modelInfo, quotaInfo);
2818
- if (!label || windows.has(label)) continue;
2819
- const percent = antigravityUsedPercent(quotaInfo);
2820
- if (percent === undefined) continue;
2821
- windows.set(label, {
2822
- label,
2823
- percent,
2824
- ...(normalizeResetAt(quotaInfo.resetTime) !== undefined ? { resetAt: normalizeResetAt(quotaInfo.resetTime) } : {}),
2825
- });
2826
- }
2827
- }
2828
-
2829
- const customWindows = ["Gem", "Cla"].flatMap(label => {
2830
- const window = windows.get(label);
2831
- return window ? [window] : [];
2832
- });
2833
- return customWindows;
2834
- }
2835
-
2836
- /**
2837
- * Parse Google Antigravity quota from `v1internal:retrieveUserQuotaSummary`.
2838
- * Groups contain Gemini models and Claude/3P models, each with 5h and weekly limit buckets.
2839
- */
2840
- function parseAntigravityQuotaSummary(body: Record<string, unknown> | null): ProviderQuota | null {
2841
- const groups = Array.isArray(body?.groups) ? (body.groups as unknown[]) : [];
2842
- if (groups.length === 0) return null;
2843
-
2844
- const customWindowsMap = new Map<string, ProviderQuotaWindow>();
2845
-
2846
- for (const rawGroup of groups) {
2847
- const group = asRecord(rawGroup);
2848
- if (!group) continue;
2849
- const groupName = `${typeof group.displayName === "string" ? group.displayName : ""} ${typeof group.description === "string" ? group.description : ""}`.toLowerCase();
2850
- const isGemini = groupName.includes("gemini");
2851
- const isClaude = groupName.includes("claude") || groupName.includes("3p") || groupName.includes("gpt");
2852
-
2853
- const buckets = Array.isArray(group.buckets) ? (group.buckets as unknown[]) : [];
2854
- for (const rawBucket of buckets) {
2855
- const bucket = asRecord(rawBucket);
2856
- if (!bucket) continue;
2857
- const windowStr = `${typeof bucket.window === "string" ? bucket.window : ""} ${typeof bucket.bucketId === "string" ? bucket.bucketId : ""} ${typeof bucket.displayName === "string" ? bucket.displayName : ""}`.toLowerCase();
2858
- const percent = antigravityUsedPercent(bucket);
2859
- if (percent === undefined) continue;
2860
- const resetAt = normalizeResetAt(bucket.resetTime);
2861
-
2862
- const isWeekly = windowStr.includes("week");
2863
- const is5h = windowStr.includes("5h") || windowStr.includes("five");
2864
-
2865
- if (isGemini) {
2866
- const label = is5h ? "Gem" : isWeekly ? "Gem (Weekly)" : "";
2867
- if (label && !customWindowsMap.has(label)) {
2868
- customWindowsMap.set(label, { label, percent, ...(resetAt !== undefined ? { resetAt } : {}) });
2869
- }
2870
- } else if (isClaude) {
2871
- const label = is5h ? "Cla" : isWeekly ? "Cla (Weekly)" : "";
2872
- if (label && !customWindowsMap.has(label)) {
2873
- customWindowsMap.set(label, { label, percent, ...(resetAt !== undefined ? { resetAt } : {}) });
2874
- }
2875
- } else {
2876
- const baseLabel = typeof group.displayName === "string" ? group.displayName : "Other";
2877
- const label = isWeekly ? `${baseLabel} (Weekly)` : baseLabel;
2878
- if (!customWindowsMap.has(label)) {
2879
- customWindowsMap.set(label, { label, percent, ...(resetAt !== undefined ? { resetAt } : {}) });
2880
- }
2881
- }
2882
- }
2883
- }
2884
-
2885
- const PREFERRED_ORDER = ["Gem", "Gem (Weekly)", "Cla", "Cla (Weekly)"];
2886
- const customWindows = Array.from(customWindowsMap.values()).sort((a, b) => {
2887
- const ia = PREFERRED_ORDER.indexOf(a.label);
2888
- const ib = PREFERRED_ORDER.indexOf(b.label);
2889
- if (ia !== -1 && ib !== -1) return ia - ib;
2890
- if (ia !== -1) return -1;
2891
- if (ib !== -1) return 1;
2892
- return a.label.localeCompare(b.label);
2893
- });
2894
-
2895
- if (customWindows.length === 0) {
2896
- return null;
2897
- }
2898
-
2899
- return {
2900
- customWindows,
2901
- updatedAt: Date.now(),
2902
- };
2903
- }
2904
-
2905
- const ANTIGRAVITY_ACCOUNT_QUOTA_BASE = "https://daily-cloudcode-pa.googleapis.com";
2906
- const ANTIGRAVITY_QUOTA_SUMMARY_URL = `${ANTIGRAVITY_ACCOUNT_QUOTA_BASE}/v1internal:retrieveUserQuotaSummary`;
2907
- const ANTIGRAVITY_QUOTA_MODELS_URL = `${ANTIGRAVITY_ACCOUNT_QUOTA_BASE}/v1internal:fetchAvailableModels`;
2908
-
2909
- /** Only these fixed accounting destinations may use transparent Fake-IP DNS. */
2910
- export function isCanonicalAntigravityQuotaUrl(name: string, url: string): boolean {
2911
- return name === "google-antigravity"
2912
- && (url === ANTIGRAVITY_QUOTA_SUMMARY_URL || url === ANTIGRAVITY_QUOTA_MODELS_URL);
2913
- }
2914
-
2915
- let antigravityOutboundDependencies: ProviderOutboundDependencies = {
2916
- isCanonicalUrl: isCanonicalAntigravityQuotaUrl,
2917
- };
2918
-
2919
- /** Test seam: inject resolver/pinned transport for provider and per-account probes. */
2920
- export function setAntigravityAccountQuotaTransportForTests(dependencies: ProviderOutboundDependencies | null): void {
2921
- antigravityOutboundDependencies = { ...dependencies, isCanonicalUrl: isCanonicalAntigravityQuotaUrl };
2922
- }
2923
-
2924
- /**
2925
- * Per-account Antigravity quota (#1082). Always probes Google's own Cloud Code Assist host
2926
- * through the pinned provider-outbound transport: a configured `baseUrl` is a routing choice
2927
- * for requests, not a second source of Google's accounting for a stored credential, and fixing
2928
- * the destination keeps the `provider\0accountId` cache identity exact across config changes.
2929
- * A redirect or non-2xx yields null (unavailable), never a partial row.
2930
- */
2931
- type AntigravityQuotaProbeResult =
2932
- | { kind: "available"; quota: ProviderQuota; source: "google-antigravity:retrieveUserQuotaSummary" | "google-antigravity:fetchAvailableModels" }
2933
- | { kind: "unavailable"; failure: QuotaFailureCode; legacy: { kind: "null" } | { kind: "throw"; error: unknown } };
2934
-
2935
- function quotaTransportFailure(error: unknown): QuotaFailureCode {
2936
- if (error instanceof ProviderOutboundPolicyError) return "destination_blocked";
2937
- if (error instanceof DestinationDnsResolutionError) return "dns_failed";
2938
- if (error instanceof PinnedHttpError) return error.code === "output_byte_limit" ? "response_unusable" : "timeout";
2939
- if (error instanceof DOMException && error.name === "TimeoutError") return "timeout";
2940
- return "transport_error";
2941
- }
2942
-
2943
- function quotaHttpFailure(status: number): QuotaFailureCode {
2944
- if (status >= 300 && status < 400) return "redirect_blocked";
2945
- if (status === 401 || status === 403) return "access_denied";
2946
- if (status === 429) return "rate_limited";
2947
- return "upstream_error";
2948
- }
2949
-
2950
- function unavailableAntigravityQuota(failure: QuotaFailureCode): AntigravityQuotaProbeResult {
2951
- return { kind: "unavailable", failure, legacy: { kind: "null" } };
2952
- }
2953
-
2954
- /**
2955
- * Prefer a summary network-policy diagnosis over a vaguer fallback. A blocked
2956
- * destination is an actionable local-network fact, while "upstream_error" tells
2957
- * the operator to go look at Google. A successful models probe still clears
2958
- * the first failure completely.
2959
- */
2960
- function antigravityUnavailableFailure(
2961
- summaryFailure: QuotaFailureCode | undefined,
2962
- fallbackFailure: QuotaFailureCode,
2963
- ): QuotaFailureCode {
2964
- if (
2965
- (summaryFailure === "destination_blocked" || summaryFailure === "dns_failed")
2966
- && fallbackFailure !== "destination_blocked"
2967
- && fallbackFailure !== "dns_failed"
2968
- ) {
2969
- return summaryFailure;
2970
- }
2971
- return fallbackFailure;
2972
- }
2973
-
2974
- async function probeAntigravityUsageQuota(accessToken: string, projectId: string): Promise<AntigravityQuotaProbeResult> {
2975
- const fetchQuota = (url: string) => providerOutboundPost("google-antigravity", { baseUrl: ANTIGRAVITY_ACCOUNT_QUOTA_BASE }, url, {
2976
- headers: {
2977
- Accept: "application/json", "Content-Type": "application/json",
2978
- "User-Agent": antigravityUserAgent(), Authorization: `Bearer ${accessToken}`,
2979
- },
2980
- body: JSON.stringify({ project: projectId }), signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
2981
- }, antigravityOutboundDependencies);
2982
- let summaryFailure: QuotaFailureCode | undefined;
2983
- try {
2984
- const response = await fetchQuota(ANTIGRAVITY_QUOTA_SUMMARY_URL);
2985
- if (await providerRedirectError(response, ANTIGRAVITY_QUOTA_SUMMARY_URL)) return unavailableAntigravityQuota("redirect_blocked");
2986
- if (response.status === 401 || response.status === 403) return unavailableAntigravityQuota("access_denied");
2987
- if (response.ok) {
2988
- const quota = parseAntigravityQuotaSummary(asRecord(await readQuotaJson(response)));
2989
- if (quota) return { kind: "available", quota, source: "google-antigravity:retrieveUserQuotaSummary" };
2990
- }
2991
- } catch (error) {
2992
- // Existing behavior: summary transport/parse failure may recover through the models probe.
2993
- summaryFailure = quotaTransportFailure(error);
2994
- }
2995
- try {
2996
- const response = await fetchQuota(ANTIGRAVITY_QUOTA_MODELS_URL);
2997
- if (await providerRedirectError(response, ANTIGRAVITY_QUOTA_MODELS_URL)) {
2998
- return unavailableAntigravityQuota(antigravityUnavailableFailure(summaryFailure, "redirect_blocked"));
2999
- }
3000
- if (!response.ok) {
3001
- return unavailableAntigravityQuota(antigravityUnavailableFailure(summaryFailure, quotaHttpFailure(response.status)));
3002
- }
3003
- const customWindows = antigravityWindowsFromModels(asRecord(await readQuotaJson(response)));
3004
- if (!customWindows.length) {
3005
- return unavailableAntigravityQuota(antigravityUnavailableFailure(summaryFailure, "response_unusable"));
3006
- }
3007
- return { kind: "available", quota: { customWindows, updatedAt: Date.now() }, source: "google-antigravity:fetchAvailableModels" };
3008
- } catch (error) {
3009
- // The public compatibility wrapper still rejects this exact fallback error; it never enters a DTO.
3010
- return {
3011
- kind: "unavailable",
3012
- failure: antigravityUnavailableFailure(summaryFailure, quotaTransportFailure(error)),
3013
- legacy: { kind: "throw", error },
3014
- };
3015
- }
3016
- }
3017
-
3018
- export async function fetchAntigravityUsageQuota(accessToken: string, projectId: string): Promise<ProviderQuota | null> {
3019
- const result = await probeAntigravityUsageQuota(accessToken, projectId);
3020
- if (result.kind === "available") return result.quota;
3021
- if (result.legacy.kind === "throw") throw result.legacy.error;
3022
- return null;
3023
- }
3024
-
3025
- async function fetchAntigravityQuota(provider: string): Promise<ProviderQuotaReport | null> {
3026
- const credential = getCredential("google-antigravity");
3027
- if (!credential?.projectId) return null;
3028
- let accessToken: string;
3029
- try { accessToken = await getValidAccessToken("google-antigravity"); } catch { return null; }
3030
- const result = await probeAntigravityUsageQuota(accessToken, credential.projectId);
3031
- if (result.kind === "available") return report(provider, result.source, result.quota);
3032
- if (result.legacy.kind === "throw") throw result.legacy.error;
3033
- return null;
3034
- }
3035
1
 
3036
- type KeyQuotaReader = (name: string, provider: OcxProviderConfig) => Promise<ProviderQuotaProbeResult>;
3037
-
3038
- /** Same selector drives cheap capabilities and uncached reads; never resolves credentials. */
3039
- function keyQuotaReaderForProvider(name: string, provider: OcxProviderConfig): KeyQuotaReader | null {
3040
- if (provider.disabled === true || (provider.authMode ?? "key") !== "key") return null;
3041
- if (isCanonicalKimiCodeBaseUrl(provider.baseUrl)) {
3042
- return async (id, config) => {
3043
- const bearer = await resolveKimiQuotaBearer(config);
3044
- return bearer ? fetchKimiQuota(id, config, bearer) : null;
3045
- };
3046
- }
3047
- if (name === "commandcode" && isCanonicalCommandCodeBaseUrl(provider.baseUrl)) {
3048
- return async (id, config) => {
3049
- const bearer = await resolveCommandCodeQuotaBearer(config);
3050
- return bearer ? fetchCommandCodeQuota(id, config, bearer) : null;
3051
- };
3052
- }
3053
- if (registryEntryForProviderDestination(provider)?.id === "opencode-go") return fetchOpenCodeGoQuota;
3054
- if (isCanonicalA6apiBaseUrl(provider.baseUrl)) return fetchA6apiQuota;
3055
- if (name === "openrouter" && isCanonicalOpenRouterBaseUrl(provider.baseUrl)) return fetchOpenRouterQuota;
3056
- if (name === "deepseek" && isCanonicalDeepSeekBaseUrl(provider.baseUrl)) return fetchDeepSeekQuota;
3057
- if (name === "cline-pass" && isCanonicalClineBaseUrl(provider.baseUrl)) return fetchClineQuota;
3058
- if (isCanonicalOllamaCloudBaseUrl(provider.baseUrl ?? getProviderRegistryEntry(name)?.baseUrl)) return fetchOllamaCloudQuota;
3059
- // #4201: the Responses preset is the same domestic GLM Coding Plan subscription on the OpenAI
3060
- // Responses wire, so it reads the same monitor endpoint. Eligibility stays a name list AND the
3061
- // canonical-URL guard: the guard is what keeps BigModel's bare-key Authorization from reaching a
3062
- // lookalike host, so a same-named custom destination still dispatches nothing.
3063
- if (["zai", "glm", "glm-cn", "zhipu-bigmodel-coding", "zhipu-bigmodel-responses"].includes(name) && isCanonicalZaiBaseUrl(provider.baseUrl)) return fetchZaiQuota;
3064
- if (["minimax", "minimax-cn"].includes(name) && isCanonicalMinimaxBaseUrl(provider.baseUrl)) return fetchMinimaxQuota;
3065
- if (name === "moonshot" && isCanonicalMoonshotBaseUrl(provider.baseUrl)) return fetchMoonshotQuota;
3066
- if (name === "venice" && isCanonicalVeniceBaseUrl(provider.baseUrl)) return fetchVeniceQuota;
3067
- if (name === "synthetic" && isCanonicalSyntheticBaseUrl(provider.baseUrl)) return fetchSyntheticQuota;
3068
- if (name === "deepinfra" && isCanonicalDeepInfraBaseUrl(provider.baseUrl)) return fetchDeepInfraQuota;
3069
- if (name === "neuralwatt" && isCanonicalNeuralwattBaseUrl(provider.baseUrl)) return fetchNeuralwattQuota;
3070
- return null;
3071
- }
2
+ import { listCodexAuthAccountsSnapshot } from "../codex/auth-api";
3
+ import { resolveEnvValue } from "../config";
4
+ import { getAccountCredential, getAccountSet } from "../oauth/store";
5
+ import { apiKeyPoolEntryId } from "./api-keys";
6
+ import { captureConfigGeneration, sweepExpiredOnWrite } from "../lib/state-store-sweeper";
7
+ import { ACCOUNT_QUOTA_TTL_MS, CACHE_TTL_MS } from "./quota-wire";
8
+ import { replaceCachedProviderQuotas } from "./quota-routing-cache";
9
+ import {
10
+ commitKiroAccountUsageState,
11
+ fetchKiroUsageSnapshot,
12
+ type KiroUsageSnapshot,
13
+ kiroUsageContextForAccount,
14
+ } from "./kiro-usage";
15
+ import { mapQuotaRoster, readProviderApiKeyQuotas, type ProviderApiKeyQuota } from "./quota-key-accounts";
16
+ import type { OcxConfig, OcxProviderConfig } from "../types";
17
+ import type { ProviderQuota, QuotaFailureCode } from "./quota-types";
18
+ import {
19
+ accountReportCurrent,
20
+ AUTHORITATIVE_EMPTY_QUOTA,
21
+ bumpProviderQuotaInvalidationEpoch,
22
+ cacheKeyWithAggregationState,
23
+ getProviderQuotaReportCache,
24
+ hasCodexPoolProvider,
25
+ inflight,
26
+ invalidationEpoch,
27
+ isBuiltInChatGptForwardProvider,
28
+ isProviderQuotaReportCurrent,
29
+ LAST_GOOD_MAX_AGE_MS,
30
+ providerQuotaBeforePublishForTests,
31
+ routingEvidence,
32
+ setProviderQuotaReportCache,
33
+ TERMINAL_QUOTA_FAILURE,
34
+ type CodexAuthAccountsSnapshotPromise,
35
+ type ProviderQuotaProbeResult,
36
+ type ProviderQuotaReport,
37
+ type ProviderQuotaResponse,
38
+ } from "./quota/report-cache";
39
+ import {
40
+ accountCacheKey,
41
+ accountQuotaCache,
42
+ accountQuotaInflight,
43
+ explicitAccountEpoch,
44
+ explicitAccountReader,
45
+ explicitQuotaConfig,
46
+ explicitQuotaDestination,
47
+ explicitQuotaIdentity,
48
+ getTokenForAccountQuotaProbe,
49
+ hasPassiveAccountQuota,
50
+ hydrateAccountQuotaCache,
51
+ mayCommitAccountQuotaKey,
52
+ mayCommitProviderQuotaKey,
53
+ normalizeAnthropicQuota,
54
+ supportsPerAccountQuota,
55
+ type AccountQuotaCacheEntry,
56
+ type ProviderAccountQuota,
57
+ } from "./quota/account-cache";
58
+ import {
59
+ fetchAnthropicQuota,
60
+ fetchAnthropicUsageQuota,
61
+ fetchChatGptForwardQuota,
62
+ fetchCursorQuota,
63
+ fetchKiroQuota,
64
+ fetchMuseKeyQuota,
65
+ fetchPassiveProviderQuota,
66
+ fetchXaiQuota,
67
+ } from "./quota/vendor-probes-oauth";
68
+ import { fetchCommandCodeQuota, fetchKimiQuota, keyQuotaReaderForProvider } from "./quota/vendor-probes-key";
69
+ import { antigravityQuotaDiagnosticIdentity, fetchAntigravityQuota, probeAntigravityUsageQuota } from "./quota/antigravity";
3072
70
 
3073
- export function providerApiKeyQuotaMode(name: string, provider: OcxProviderConfig): AccountQuotaMode {
3074
- return keyQuotaReaderForProvider(name, provider) ? "probe" : "unsupported";
3075
- }
71
+ export type { ProviderQuota, ProviderQuotaCreditsUsd, ProviderQuotaWindow } from "./quota-types";
72
+ export { QUOTA_RESPONSE_MAX_BYTES } from "./quota-wire";
73
+ export {
74
+ clearProviderQuotaCache,
75
+ publishKeyReportForTests,
76
+ readProviderQuotaJsonForTests,
77
+ setProviderQuotaBeforePublishForTests,
78
+ type ProviderQuotaReport,
79
+ type ProviderQuotaResponse,
80
+ } from "./quota/report-cache";
81
+ export {
82
+ clearAccountQuotaCache,
83
+ getCachedProviderAccountQuota,
84
+ hasPassiveAccountQuota,
85
+ parseAnthropicRateLimitHeaders,
86
+ providerOAuthAccountQuotaMode,
87
+ readPassiveProviderAccountQuotas,
88
+ recordAnthropicAccountQuotaFromHeaders,
89
+ recordPassiveAccountQuota,
90
+ reconcileProviderAccountQuotaRows,
91
+ resetProviderQuotaReconcileStateForTests,
92
+ setCachedProviderAccountQuotaForTests,
93
+ supportsPerAccountQuota,
94
+ sweepExpiredProviderAccountQuotaRows,
95
+ type ProviderAccountQuota,
96
+ } from "./quota/account-cache";
97
+ export { fetchAntigravityUsageQuota, isCanonicalAntigravityQuotaUrl, setAntigravityAccountQuotaTransportForTests } from "./quota/antigravity";
98
+ export { parseOllamaCloudQuota, parseZaiQuotaLimits, providerApiKeyQuotaMode } from "./quota/vendor-probes-key";
99
+ export { parseXaiCreditsResponse } from "./quota/vendor-probes-oauth";
3076
100
 
3077
101
  export async function fetchProviderApiKeyQuotas(config: OcxConfig, name: string, forceRefresh = false): Promise<ProviderApiKeyQuota[]> {
3078
102
  const provider = config.providers[name];
@@ -3220,19 +244,21 @@ export async function fetchProviderQuotaReports(config: OcxConfig, forceRefresh
3220
244
  // by construction and never becomes fresher on its own. Without the exemption a single
3221
245
  // configured passive provider makes this predicate permanently false, so every dashboard
3222
246
  // poll re-probes every OTHER provider upstream instead of serving the 5-minute cache.
3223
- const cacheFresh = cache && cache.key === key && now - cache.ts < CACHE_TTL_MS
3224
- && cache.response.reports.every(item =>
247
+ const currentCache = getProviderQuotaReportCache();
248
+ const cacheFresh = currentCache && currentCache.key === key && now - currentCache.ts < CACHE_TTL_MS
249
+ && currentCache.response.reports.every(item =>
3225
250
  (item.observed === true || now - item.updatedAt < LAST_GOOD_MAX_AGE_MS)
3226
251
  && isProviderQuotaReportCurrent(item));
3227
- if (!forceRefresh && cacheFresh) return cache!.response;
252
+ if (!forceRefresh && cacheFresh) return currentCache!.response;
3228
253
  const joinable = inflight.get(key);
3229
254
  if (!forceRefresh && joinable && joinable.epoch === invalidationEpoch) return joinable.promise;
3230
255
  // A forced probe takes commit authority: older in-flight probes must not overwrite its result.
3231
- if (forceRefresh) invalidationEpoch += 1;
256
+ if (forceRefresh) bumpProviderQuotaInvalidationEpoch();
3232
257
  const epoch = invalidationEpoch;
3233
258
 
3234
259
  const promise = (async (): Promise<ProviderQuotaResponse> => {
3235
- const previous = cache && cache.key === key ? cache.response.reports : [];
260
+ const previousCache = getProviderQuotaReportCache();
261
+ const previous = previousCache && previousCache.key === key ? previousCache.response.reports : [];
3236
262
  const probeResults = await Promise.all(
3237
263
  Object.entries(config.providers).map(([name, provider]) => (
3238
264
  maybeFetchProviderQuota(name, provider, config, forceRefresh, prefetchedCodexSnapshot)
@@ -3296,7 +322,7 @@ export async function fetchProviderQuotaReports(config: OcxConfig, forceRefresh
3296
322
  && generationMismatchedProviders.size === 0
3297
323
  ) {
3298
324
  const reports = response.reports.filter(item => mayCommitProviderQuotaKey(item.provider, writerGeneration));
3299
- cache = { key, ts: Date.now(), response: { ...response, reports } };
325
+ setProviderQuotaReportCache({ key, ts: Date.now(), response: { ...response, reports } });
3300
326
  replaceCachedProviderQuotas(reports, routingEvidence);
3301
327
  notifyProviderQuotaSnapshot(reports, config);
3302
328
  }
@@ -3311,3 +337,222 @@ export async function fetchProviderQuotaReports(config: OcxConfig, forceRefresh
3311
337
  if (inflight.get(key) === entry) inflight.delete(key);
3312
338
  }
3313
339
  }
340
+
341
+ async function readExplicitAccountQuota(provider: string, accountId: string, configured?: OcxProviderConfig): Promise<{
342
+ result: ProviderQuotaProbeResult;
343
+ identity: string | undefined;
344
+ isCurrent: () => boolean;
345
+ } | null> {
346
+ const target = explicitQuotaConfig(provider, configured);
347
+ if (!target || !explicitQuotaDestination(provider, target)) return null;
348
+ const config = { ...target };
349
+ const epoch = explicitAccountEpoch;
350
+ const accessToken = await getTokenForAccountQuotaProbe(provider, accountId);
351
+ const credential = getAccountCredential(provider, accountId);
352
+ if (!credential || credential.access !== accessToken) return null;
353
+ // Pair the post-renewal credential with the destination captured before renewal.
354
+ const identity = explicitQuotaIdentity(provider, accountId, config);
355
+ const isCurrent = () => epoch === explicitAccountEpoch
356
+ && identity === explicitQuotaIdentity(provider, accountId, configured);
357
+ if (!isCurrent()) return null;
358
+ let result: ProviderQuotaProbeResult;
359
+ switch (provider) {
360
+ case "xai": result = await fetchXaiQuota(provider, { accessToken, upstreamAccountId: credential.accountId }); break;
361
+ case "cursor": result = await fetchCursorQuota(provider, accessToken); break;
362
+ case "kimi": result = await fetchKimiQuota(provider, config, accessToken); break;
363
+ case "command-code": result = await fetchCommandCodeQuota(provider, config, accessToken); break;
364
+ default: return null;
365
+ }
366
+ return { result, identity, isCurrent };
367
+ }
368
+
369
+ async function fetchExplicitAccountQuota(provider: string, accountId: string, force: boolean, configured?: OcxProviderConfig): Promise<AccountQuotaCacheEntry> {
370
+ const key = accountCacheKey(provider, accountId);
371
+ const identity = explicitQuotaIdentity(provider, accountId, configured);
372
+ const previous = accountQuotaCache.get(key);
373
+ const cached = identity && previous?.identity === identity && previous.isCurrent?.() ? previous : undefined;
374
+ if (!force && cached && Date.now() - cached.ts < ACCOUNT_QUOTA_TTL_MS
375
+ && (!cached.quota || Date.now() - cached.quota.updatedAt < LAST_GOOD_MAX_AGE_MS)) return cached;
376
+ const flightKey = `${key}\u0000${identity ?? "missing"}`;
377
+ const running = accountQuotaInflight.get(flightKey);
378
+ if (running) return running;
379
+ const epoch = explicitAccountEpoch;
380
+ const lastGood = cached?.quota && Date.now() - cached.quota.updatedAt < LAST_GOOD_MAX_AGE_MS ? cached.quota : null;
381
+ const flight = (async (): Promise<AccountQuotaCacheEntry> => {
382
+ let read: Awaited<ReturnType<typeof readExplicitAccountQuota>> = null;
383
+ try { read = await readExplicitAccountQuota(provider, accountId, configured); } catch { /* unavailable */ }
384
+ const isCurrent = read?.isCurrent ?? (() => epoch === explicitAccountEpoch && !!identity
385
+ && identity === explicitQuotaIdentity(provider, accountId, configured));
386
+ const result = read?.result;
387
+ const current = epoch === explicitAccountEpoch && isCurrent();
388
+ const quota = current && result && typeof result !== "symbol" ? result.quota : null;
389
+ const empty = result === AUTHORITATIVE_EMPTY_QUOTA;
390
+ const entry: AccountQuotaCacheEntry = {
391
+ ts: Date.now(),
392
+ quota: quota ?? (current && result !== TERMINAL_QUOTA_FAILURE && !empty
393
+ && lastGood && Date.now() - lastGood.updatedAt < LAST_GOOD_MAX_AGE_MS ? lastGood : null),
394
+ ...(!current || (!quota && !empty) ? { unavailable: true as const } : {}),
395
+ identity: read?.identity ?? identity,
396
+ isCurrent: () => epoch === explicitAccountEpoch && isCurrent(),
397
+ };
398
+ if (entry.isCurrent?.()) accountQuotaCache.set(key, entry);
399
+ return entry;
400
+ })().finally(() => { if (accountQuotaInflight.get(flightKey) === flight) accountQuotaInflight.delete(flightKey); });
401
+ accountQuotaInflight.set(flightKey, flight);
402
+ return flight;
403
+ }
404
+
405
+ async function fetchExplicitCurrentQuota(provider: string, config: OcxProviderConfig, liveConfig: OcxConfig): Promise<ProviderQuotaProbeResult> {
406
+ const id = getAccountSet(provider)?.activeAccountId;
407
+ if (!id) return null;
408
+ const read = await readExplicitAccountQuota(provider, id, config);
409
+ if (!read) return null;
410
+ const isCurrent = () => liveConfig.providers[provider] === config
411
+ && read.isCurrent() && getAccountSet(provider)?.activeAccountId === id;
412
+ if (!isCurrent()) return TERMINAL_QUOTA_FAILURE;
413
+ if (read.result && typeof read.result !== "symbol") accountReportCurrent.set(read.result, isCurrent);
414
+ return read.result;
415
+ }
416
+
417
+
418
+ async function fetchAccountQuota(
419
+ provider: string,
420
+ accountId: string,
421
+ forceRefresh: boolean,
422
+ providerConfig?: OcxProviderConfig,
423
+ ): Promise<AccountQuotaCacheEntry> {
424
+ if (!supportsPerAccountQuota(provider)) return { ts: Date.now(), quota: null, unavailable: true };
425
+ if (explicitAccountReader(provider)) return fetchExplicitAccountQuota(provider, accountId, forceRefresh, providerConfig);
426
+ if (provider === "anthropic") hydrateAccountQuotaCache();
427
+ const key = accountCacheKey(provider, accountId);
428
+ const writerGeneration = captureConfigGeneration();
429
+ const cached = accountQuotaCache.get(key);
430
+ if (!forceRefresh && cached && Date.now() - cached.ts < ACCOUNT_QUOTA_TTL_MS) {
431
+ if (provider === "google-antigravity" && cached.quotaFailure && cached.quotaFailureIsCurrent?.() !== true) return { ...cached, quotaFailure: undefined };
432
+ return provider === "anthropic" ? { ...cached, quota: normalizeAnthropicQuota(cached.quota, Date.now()) } : cached;
433
+ }
434
+ const joinable = accountQuotaInflight.get(key);
435
+ if (joinable) return joinable;
436
+
437
+ const epoch = explicitAccountEpoch;
438
+ const probe = (async (): Promise<AccountQuotaCacheEntry> => {
439
+ let diagnosticIdentity: string | undefined;
440
+ let quotaFailure: QuotaFailureCode | undefined;
441
+ const quotaFailureIsCurrent = () => {
442
+ try { return epoch === explicitAccountEpoch && diagnosticIdentity !== undefined && diagnosticIdentity === antigravityQuotaDiagnosticIdentity(accountId); }
443
+ catch { return false; }
444
+ };
445
+ const diagnosticFields = () => quotaFailure && quotaFailureIsCurrent() ? { quotaFailure, quotaFailureIsCurrent } : {};
446
+ try {
447
+ if (provider === "google-antigravity") diagnosticIdentity = antigravityQuotaDiagnosticIdentity(accountId);
448
+ let quota: ProviderQuota | null;
449
+ let kiroSnapshot: KiroUsageSnapshot | null = null;
450
+ if (provider === "kiro") {
451
+ // Kiro resolves the bearer and its routing metadata from ONE account-scoped
452
+ // snapshot. It deliberately does not use getTokenForAccountQuotaProbe: that
453
+ // helper refuses to refresh a background `local-cli` slot because Anthropic's
454
+ // lock can adopt a mismatched Claude CLI identity, but Kiro marks every
455
+ // CLI-imported credential `local-cli`, so the same rule would blank the quota of
456
+ // every inactive pool account the moment its token expired.
457
+ kiroSnapshot = await fetchKiroUsageSnapshot(await kiroUsageContextForAccount(accountId));
458
+ quota = kiroSnapshot?.quota ?? null;
459
+ } else {
460
+ const token = await getTokenForAccountQuotaProbe(provider, accountId);
461
+ if (provider === "google-antigravity") {
462
+ // Per-account Gem/Cla windows (#1082). The project id is part of the stored
463
+ // credential; without it the probe cannot be made, and that is "unavailable",
464
+ // never 0%.
465
+ const credential = getAccountCredential(provider, accountId);
466
+ diagnosticIdentity = credential?.access === token ? antigravityQuotaDiagnosticIdentity(accountId, credential) : undefined;
467
+ if (!diagnosticIdentity || !credential?.projectId) throw new Error("antigravity account unavailable");
468
+ const result = await probeAntigravityUsageQuota(token, credential.projectId);
469
+ quota = result.kind === "available" ? result.quota : null;
470
+ if (result.kind === "unavailable") quotaFailure = result.failure;
471
+ } else if (provider === "anthropic") {
472
+ quota = await fetchAnthropicUsageQuota(token);
473
+ } else {
474
+ return { ts: Date.now(), quota: null, unavailable: true };
475
+ }
476
+ }
477
+ if (!quota) {
478
+ // Preserve last-good bars and mark unavailable; advance TTL so failures
479
+ // negative-cache instead of re-probing on every GUI poll.
480
+ const entry: AccountQuotaCacheEntry = {
481
+ ts: Date.now(),
482
+ // Settle once for all joiners against observations committed during the probe.
483
+ quota: provider === "anthropic"
484
+ ? normalizeAnthropicQuota(accountQuotaCache.get(key)?.quota, Date.now()) : cached?.quota ?? null,
485
+ unavailable: true,
486
+ ...diagnosticFields(),
487
+ };
488
+ if (mayCommitAccountQuotaKey(key, writerGeneration)) {
489
+ accountQuotaCache.set(key, entry);
490
+ if (provider === "kiro") commitKiroAccountUsageState(key, null);
491
+ sweepExpiredOnWrite(entry.ts);
492
+ }
493
+ return entry;
494
+ }
495
+ const entry: AccountQuotaCacheEntry = {
496
+ ts: Date.now(), quota: provider === "anthropic" ? normalizeAnthropicQuota(quota, Date.now()) : quota,
497
+ };
498
+ if (mayCommitAccountQuotaKey(key, writerGeneration)) {
499
+ accountQuotaCache.set(key, entry);
500
+ // Exhaustion state rides the SAME commit guard as the quota row: a probe from a
501
+ // superseded config generation must not publish either half.
502
+ if (provider === "kiro") commitKiroAccountUsageState(key, kiroSnapshot);
503
+ sweepExpiredOnWrite(entry.ts);
504
+ }
505
+ return entry;
506
+ } catch {
507
+ if (provider === "google-antigravity") quotaFailure = "account_unavailable";
508
+ const entry: AccountQuotaCacheEntry = {
509
+ ts: Date.now(),
510
+ quota: provider === "anthropic"
511
+ ? normalizeAnthropicQuota(accountQuotaCache.get(key)?.quota, Date.now()) : cached?.quota ?? null,
512
+ unavailable: true,
513
+ ...diagnosticFields(),
514
+ };
515
+ if (mayCommitAccountQuotaKey(key, writerGeneration)) {
516
+ accountQuotaCache.set(key, entry);
517
+ sweepExpiredOnWrite(entry.ts);
518
+ }
519
+ return entry;
520
+ }
521
+ })().finally(() => {
522
+ if (accountQuotaInflight.get(key) === probe) accountQuotaInflight.delete(key);
523
+ });
524
+ accountQuotaInflight.set(key, probe);
525
+ return probe;
526
+ }
527
+
528
+ /**
529
+ * Per-account quota rows for a provider's logged-in accounts. Probes run in parallel; a
530
+ * single failing account never blocks the others.
531
+ */
532
+ export async function fetchProviderAccountQuotas(
533
+ provider: string,
534
+ forceRefresh = false,
535
+ providerConfig?: OcxProviderConfig,
536
+ ): Promise<ProviderAccountQuota[]> {
537
+ if (!supportsPerAccountQuota(provider)) return [];
538
+ const set = getAccountSet(provider);
539
+ if (!set) return [];
540
+ return mapQuotaRoster(set.accounts, async account => {
541
+ const entry = await fetchAccountQuota(provider, account.id, forceRefresh, providerConfig);
542
+ const result: ProviderAccountQuota = {
543
+ accountId: account.id,
544
+ quota: provider === "anthropic" ? normalizeAnthropicQuota(entry.quota, Date.now()) : entry.quota,
545
+ ...(entry.unavailable ? { unavailable: true as const } : {}),
546
+ ...(entry.unavailable && entry.quotaFailure && entry.quotaFailureIsCurrent?.() === true ? { quotaFailure: entry.quotaFailure } : {}),
547
+ };
548
+ if (entry.quotaFailureIsCurrent) Object.defineProperty(result, "quotaFailureIsCurrent", { value: entry.quotaFailureIsCurrent });
549
+ if (!explicitAccountReader(provider)) return result;
550
+ const identity = entry.identity;
551
+ Object.defineProperty(result, "isCurrent", { value: () => {
552
+ if (entry.isCurrent) return entry.isCurrent();
553
+ const credential = getAccountCredential(provider, account.id);
554
+ return !!credential && (!identity || explicitQuotaIdentity(provider, account.id, providerConfig) === identity);
555
+ } });
556
+ return result;
557
+ });
558
+ }