@bitkyc08/opencodex 2.42.0 → 2.43.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 (246) hide show
  1. package/AGENTS_INSTALL.md +2 -2
  2. package/README.md +31 -0
  3. package/bin/ocx.mjs +10 -1
  4. package/gui/dist/assets/index-DS1NE4Jn.css +1 -0
  5. package/gui/dist/assets/index-Djowl68T.js +112 -0
  6. package/gui/dist/index.html +2 -2
  7. package/package.json +1 -1
  8. package/src/adapters/anthropic-image-codec.ts +304 -0
  9. package/src/adapters/anthropic-image-normalize.ts +8 -298
  10. package/src/adapters/anthropic.ts +30 -7
  11. package/src/adapters/command-code.ts +7 -5
  12. package/src/adapters/cursor/desktop-executor-contract.ts +15 -0
  13. package/src/adapters/cursor/images.ts +36 -6
  14. package/src/adapters/cursor/live-transport.ts +7 -2
  15. package/src/adapters/cursor/native-exec-desktop.ts +2 -15
  16. package/src/adapters/cursor/protobuf-request.ts +13 -3
  17. package/src/adapters/cursor/tool-definitions.ts +5 -670
  18. package/src/adapters/cursor/tool-guidance.ts +236 -0
  19. package/src/adapters/cursor/tool-naming.ts +252 -0
  20. package/src/adapters/cursor/tool-schemas.ts +195 -0
  21. package/src/adapters/cursor/types.ts +6 -3
  22. package/src/adapters/exec-tool-result-normalize.ts +1 -1
  23. package/src/adapters/google-errors.ts +9 -1
  24. package/src/adapters/google.ts +1 -0
  25. package/src/adapters/kiro-calibration.ts +181 -0
  26. package/src/adapters/kiro.ts +135 -3
  27. package/src/adapters/openai-responses.ts +216 -26
  28. package/src/adapters/responses-code-mode.ts +59 -0
  29. package/src/adapters/tool-catalog-nudge.ts +1 -1
  30. package/src/adapters/xai-schema-analysis.ts +86 -0
  31. package/src/adapters/xai-tool-schema.ts +2 -87
  32. package/src/adapters/xai-web-search.ts +1 -1
  33. package/src/bridge.ts +22 -10
  34. package/src/chat/inbound.ts +11 -3
  35. package/src/claude/inbound-content-options.ts +60 -0
  36. package/src/claude/inbound-model-options.ts +142 -0
  37. package/src/claude/inbound-records.ts +7 -0
  38. package/src/claude/inbound.ts +10 -202
  39. package/src/claude/model-info.ts +45 -0
  40. package/src/cli/account-auth.ts +21 -6
  41. package/src/cli/capabilities.ts +13 -2
  42. package/src/cli/claude.ts +232 -39
  43. package/src/cli/config-command.ts +9 -1
  44. package/src/cli/dispatch.ts +5 -1
  45. package/src/cli/doctor.ts +10 -0
  46. package/src/cli/effort.ts +372 -0
  47. package/src/cli/export-command.ts +3 -9
  48. package/src/cli/help.ts +1 -0
  49. package/src/cli/index.ts +13 -0
  50. package/src/cli/init.ts +4 -0
  51. package/src/cli/model-selection-guidance.ts +30 -0
  52. package/src/cli/models-runtime.ts +3 -2
  53. package/src/cli/models.ts +8 -3
  54. package/src/cli/opencode.ts +4 -1
  55. package/src/cli/provider-runtime.ts +65 -0
  56. package/src/cli/provider.ts +8 -0
  57. package/src/cli/registry.ts +16 -2
  58. package/src/cli/runtime-api.ts +3 -1
  59. package/src/cli/star-prompt.ts +22 -6
  60. package/src/cli/status-probes.ts +168 -0
  61. package/src/cli/status.ts +5 -168
  62. package/src/clients/config-export/constants.ts +69 -0
  63. package/src/clients/config-export/contracts.ts +154 -0
  64. package/src/clients/config-export/dsh.ts +132 -0
  65. package/src/clients/config-export/fast-models.ts +29 -0
  66. package/src/clients/config-export/mcode.ts +83 -0
  67. package/src/clients/config-export/model-metadata.ts +108 -0
  68. package/src/clients/config-export/omp.ts +104 -0
  69. package/src/clients/config-export/zcode.ts +92 -0
  70. package/src/clients/config-export.ts +18 -710
  71. package/src/codex/account-lifecycle.ts +20 -3
  72. package/src/codex/account-usability.ts +2 -0
  73. package/src/codex/auth-api.ts +170 -23
  74. package/src/codex/auth-context.ts +328 -24
  75. package/src/codex/catalog/effort.ts +30 -4
  76. package/src/codex/catalog/metadata.ts +39 -9
  77. package/src/codex/catalog/native-models.ts +53 -12
  78. package/src/codex/catalog/parsing.ts +119 -5
  79. package/src/codex/catalog/provider-fetch.ts +150 -20
  80. package/src/codex/catalog/reserve.ts +52 -0
  81. package/src/codex/catalog/sync.ts +89 -16
  82. package/src/codex/convergence-types.ts +1 -0
  83. package/src/codex/convergence.ts +2 -0
  84. package/src/codex/data/upstream-models.json +1 -1
  85. package/src/codex/forward-transport-headers.ts +25 -0
  86. package/src/codex/inject.ts +3 -28
  87. package/src/codex/legacy-config-keys.ts +68 -0
  88. package/src/codex/log-guard/inspect-schema.ts +137 -0
  89. package/src/codex/log-guard/inspect.ts +2 -134
  90. package/src/codex/loopback-target.ts +54 -0
  91. package/src/codex/main-account-cache.ts +63 -1
  92. package/src/codex/main-account-hard-lock.ts +52 -0
  93. package/src/codex/main-account.ts +3 -1
  94. package/src/codex/management-convergence.ts +3 -0
  95. package/src/codex/model-entitlements.ts +54 -4
  96. package/src/codex/project-config-warnings.ts +92 -2
  97. package/src/codex/prompt-layers/encoding.ts +80 -0
  98. package/src/codex/prompt-layers/paths.ts +54 -0
  99. package/src/codex/prompt-layers/revision.ts +55 -0
  100. package/src/codex/prompt-layers/toml-edit.ts +163 -0
  101. package/src/codex/prompt-layers/toml-read.ts +181 -0
  102. package/src/codex/prompt-layers.ts +14 -520
  103. package/src/codex/quota-auto-refresh-state.ts +16 -0
  104. package/src/codex/quota-auto-refresh.ts +219 -0
  105. package/src/codex/quota-types.ts +51 -0
  106. package/src/codex/quota.ts +252 -93
  107. package/src/codex/reserve-availability.ts +177 -0
  108. package/src/codex/routing.ts +28 -9
  109. package/src/codex/shim.ts +53 -11
  110. package/src/codex/subagent-model-fallback.ts +23 -3
  111. package/src/combos/failover.ts +125 -7
  112. package/src/combos/identifiers.ts +89 -0
  113. package/src/combos/index.ts +4 -0
  114. package/src/combos/resolve.ts +80 -9
  115. package/src/combos/types.ts +20 -93
  116. package/src/config/subagent-models.ts +24 -0
  117. package/src/config.ts +156 -13
  118. package/src/generated/compatibility-version.json +454 -166
  119. package/src/generated/model-metadata.ts +1 -1
  120. package/src/integrations/journal.ts +65 -4
  121. package/src/integrations/store.ts +5 -0
  122. package/src/lab/events/limits.ts +4 -0
  123. package/src/lib/destination-policy.ts +31 -2
  124. package/src/lib/errors.ts +39 -0
  125. package/src/lib/provider-outbound.ts +69 -3
  126. package/src/lib/proxy-env.ts +22 -0
  127. package/src/lib/redact-folding.ts +176 -0
  128. package/src/lib/redact.ts +2 -175
  129. package/src/lib/state-store-sweeper.ts +20 -6
  130. package/src/lib/token-estimate.ts +94 -27
  131. package/src/lib/windows-user-principal.ts +53 -5
  132. package/src/oauth/anthropic-routing.ts +99 -3
  133. package/src/oauth/generic-account-failover.ts +36 -13
  134. package/src/oauth/index.ts +127 -26
  135. package/src/oauth/login-cli.ts +5 -0
  136. package/src/oauth/meta-muse.ts +117 -15
  137. package/src/oauth/pool-settings-capability.ts +15 -4
  138. package/src/providers/api-keys.ts +8 -10
  139. package/src/providers/default-aliases.ts +39 -0
  140. package/src/providers/derive.ts +10 -2
  141. package/src/providers/fastwire.ts +36 -7
  142. package/src/providers/initial-model-selection-runtime.ts +90 -0
  143. package/src/providers/initial-model-selection.ts +120 -0
  144. package/src/providers/key-failover.ts +134 -54
  145. package/src/providers/key-store.ts +11 -1
  146. package/src/providers/label.ts +1 -1
  147. package/src/providers/model-discovery.ts +76 -0
  148. package/src/providers/model-rename-startup.ts +72 -8
  149. package/src/providers/openai-sidecar.ts +17 -5
  150. package/src/providers/openai-tiers-destination.ts +102 -0
  151. package/src/providers/openai-tiers.ts +2 -99
  152. package/src/providers/opencode-go-transport.ts +41 -0
  153. package/src/providers/quota-key-accounts.ts +141 -0
  154. package/src/providers/quota-types.ts +9 -0
  155. package/src/providers/quota.ts +535 -104
  156. package/src/providers/registry.ts +60 -17
  157. package/src/providers/xai-responses-opt-in.ts +31 -5
  158. package/src/quota/reset-activation.ts +81 -0
  159. package/src/quota/reset-detector.ts +305 -0
  160. package/src/quota/reset-notify-config.ts +162 -0
  161. package/src/quota/reset-observer.ts +125 -0
  162. package/src/quota/reset-poller.ts +160 -0
  163. package/src/quota/reset-seen-store.ts +385 -0
  164. package/src/quota/reset-sinks.ts +199 -0
  165. package/src/quota/window-mapping.ts +106 -0
  166. package/src/responses/apply-patch-envelope.ts +46 -0
  167. package/src/responses/code-mode-helper-compat.ts +39 -1
  168. package/src/responses/custom-tool-compat.ts +10 -4
  169. package/src/responses/hosted-tool-policy.ts +12 -4
  170. package/src/responses/parser-content.ts +133 -0
  171. package/src/responses/parser-text-format.ts +24 -0
  172. package/src/responses/parser-tools.ts +188 -0
  173. package/src/responses/parser.ts +3 -326
  174. package/src/responses/state.ts +124 -28
  175. package/src/router.ts +48 -13
  176. package/src/routing/analytics.ts +1 -0
  177. package/src/routing/capability.ts +17 -4
  178. package/src/server/auth-cors.ts +7 -1
  179. package/src/server/background-lifecycle.ts +23 -1
  180. package/src/server/chat-completions.ts +25 -3
  181. package/src/server/claude-messages.ts +62 -5
  182. package/src/server/effort-row.ts +1 -1
  183. package/src/server/fast-row.ts +295 -0
  184. package/src/server/gui-static.ts +30 -4
  185. package/src/server/index.ts +107 -21
  186. package/src/server/management/agent-settings-routes.ts +2 -2
  187. package/src/server/management/combo-routes.ts +37 -9
  188. package/src/server/management/config-routes.ts +93 -2
  189. package/src/server/management/integration-routes.ts +108 -0
  190. package/src/server/management/model-routes.ts +13 -3
  191. package/src/server/management/model-rows.ts +20 -1
  192. package/src/server/management/native-integration-routes.ts +4 -1
  193. package/src/server/management/oauth-account-routes.ts +38 -10
  194. package/src/server/management/provider-routes.ts +34 -3
  195. package/src/server/management/quota-reset-routes.ts +57 -0
  196. package/src/server/management/route-registry.ts +7 -4
  197. package/src/server/management/shared.ts +19 -5
  198. package/src/server/management/system-routes.ts +3 -2
  199. package/src/server/management-api.ts +14 -2
  200. package/src/server/ports.ts +12 -2
  201. package/src/server/relay-eager.ts +38 -23
  202. package/src/server/relay.ts +4 -0
  203. package/src/server/request-log.ts +6 -0
  204. package/src/server/responses/codex-ws-correlation.ts +65 -0
  205. package/src/server/responses/codex-ws-exchange.ts +261 -0
  206. package/src/server/responses/codex-ws-metadata.ts +134 -0
  207. package/src/server/responses/codex-ws-pool.ts +162 -0
  208. package/src/server/responses/codex-ws-request.ts +87 -0
  209. package/src/server/responses/codex-ws-session.ts +93 -0
  210. package/src/server/responses/codex-ws-wire.ts +144 -0
  211. package/src/server/responses/collaboration.ts +41 -1
  212. package/src/server/responses/compact.ts +105 -12
  213. package/src/server/responses/core.ts +476 -57
  214. package/src/server/responses/fetch-helpers.ts +10 -3
  215. package/src/server/responses/input-admission.ts +16 -9
  216. package/src/server/responses/responses-field-backfill.ts +1 -1
  217. package/src/server/responses/ws-upstream.ts +34 -318
  218. package/src/server/responses-custom-tool-repair.ts +20 -4
  219. package/src/server/responses-undeclared-tool-guard.ts +100 -8
  220. package/src/server/safe-response-headers.ts +23 -0
  221. package/src/server/search.ts +9 -0
  222. package/src/server/subagent-models-startup.ts +27 -0
  223. package/src/server/system-env-shell.ts +238 -0
  224. package/src/server/system-env.ts +7 -234
  225. package/src/server/ws-bridge.ts +3 -25
  226. package/src/server/xai-responses-startup.ts +21 -0
  227. package/src/service-manager-probe.ts +1 -1
  228. package/src/service.ts +55 -16
  229. package/src/types/config.ts +108 -12
  230. package/src/types/provider.ts +36 -7
  231. package/src/types/tools.ts +26 -1
  232. package/src/types.ts +2 -0
  233. package/src/update/notify.ts +8 -2
  234. package/src/usage/cost.ts +38 -28
  235. package/src/usage/expected-prices.ts +34 -15
  236. package/src/usage/log.ts +2 -0
  237. package/src/usage/model-identity.ts +26 -0
  238. package/src/usage/summary.ts +15 -1
  239. package/src/vision/describe.ts +6 -0
  240. package/src/vision/image-rewrite.ts +108 -0
  241. package/src/vision/index.ts +19 -306
  242. package/src/vision/plan.ts +205 -0
  243. package/src/web-search/executor.ts +6 -0
  244. package/src/web-search/index.ts +8 -1
  245. package/gui/dist/assets/index-BU1tE0sr.js +0 -112
  246. package/gui/dist/assets/index-DL9-iS6J.css +0 -1
@@ -9,10 +9,12 @@ import type { StoredAccountQuota } from "../codex/quota";
9
9
  import { isMainAccountIdentityGenerationLive } from "../codex/main-account-cache";
10
10
  import { MAIN_CODEX_ACCOUNT_ID } from "../codex/main-account";
11
11
  import { codexPlanKey } from "../codex/plan";
12
+ import { resolveEnvValue } from "../config";
12
13
  import { resolveProviderApiKey } from "./key-store";
13
14
  import { getValidAccessToken, getValidAccessTokenForAccount } from "../oauth";
14
15
  import { getAccountCredential, getAccountSet, getCredential } from "../oauth/store";
15
16
  import { antigravityUserAgent } from "../adapters/client-fingerprint";
17
+ import { isCanonicalOllamaCloudUrl } from "../adapters/ollama-native-url";
16
18
  import { providerOutboundPost, providerRedirectError, type ProviderOutboundDependencies } from "../lib/provider-outbound";
17
19
  import { apiKeyPoolEntryId } from "./api-keys";
18
20
  import { XAI_GROK_CLIENT_VERSION, XAI_GROK_COMPATIBILITY } from "./xai-transport";
@@ -46,6 +48,7 @@ import {
46
48
  type CodexCapacityQuota,
47
49
  } from "./codex-capacity";
48
50
  import type {
51
+ AccountQuotaMode,
49
52
  ProviderQuota,
50
53
  ProviderQuotaCreditsUsd,
51
54
  ProviderQuotaWindow,
@@ -63,6 +66,7 @@ import {
63
66
  readPersistedAccountQuotas,
64
67
  schedulePersistAccountQuotas,
65
68
  } from "./account-quota-disk";
69
+ import { clearProviderApiKeyQuotaCache, mapQuotaRoster, readProviderApiKeyQuotas, type ProviderApiKeyQuota } from "./quota-key-accounts";
66
70
 
67
71
  export type { ProviderQuota, ProviderQuotaCreditsUsd, ProviderQuotaWindow } from "./quota-types";
68
72
 
@@ -83,6 +87,8 @@ const OPENCODE_GO_USAGE_URL = `${OPENCODE_GO_BASE_URL}/usage`;
83
87
  const OPENROUTER_BASE_URL = "https://openrouter.ai/api/v1";
84
88
  const DEEPSEEK_BASE_URL = "https://api.deepseek.com";
85
89
  const CLINE_BASE_URL = "https://api.cline.bot";
90
+ const OLLAMA_CLOUD_BASE_URL = "https://ollama.com";
91
+ const OLLAMA_CLOUD_USAGE_URL = `${OLLAMA_CLOUD_BASE_URL}/api/usage`;
86
92
  const ZAI_BASE_URL = "https://api.z.ai";
87
93
  const ZAI_CN_BASE_URL = "https://open.bigmodel.cn";
88
94
  const MINIMAX_REMAINS_URL = "https://www.minimax.io/v1/token_plan/remains";
@@ -96,6 +102,7 @@ const XAI_CREDITS_URL = `${XAI_BILLING_URL}?format=credits`;
96
102
  /** Keep a failed probe's previous row at most this long before dropping it. */
97
103
  const LAST_GOOD_MAX_AGE_MS = CODEX_CAPACITY_MAX_QUOTA_AGE_MS;
98
104
  const nativeMainReportGenerations = new WeakMap<ProviderQuotaReport, number>();
105
+ const accountReportCurrent = new WeakMap<ProviderQuotaReport, () => boolean>();
99
106
  let providerQuotaBeforePublishForTests: (() => void | Promise<void>) | null = null;
100
107
 
101
108
  /** Test-only seam for identity/config invalidation after probes but before publication. */
@@ -130,6 +137,18 @@ export interface ProviderQuotaReport {
130
137
  quota: ProviderQuota;
131
138
  updatedAt: number;
132
139
  reverseEngineered?: boolean;
140
+ /**
141
+ * The row was OBSERVED in-band on a streaming turn rather than probed.
142
+ *
143
+ * Age means something different for these. A probed provider re-reads on its own TTL,
144
+ * so a row older than the last-good bound means the probe is failing and showing it
145
+ * would misrepresent a live number. A passive provider publishes no endpoint at all
146
+ * (`hasPassiveAccountQuota`), so its last observation is not a stale reading of
147
+ * something fresher — it is the only measurement that exists, and dropping it leaves
148
+ * the operator with nothing. Consumers that enforce a freshness bound must exempt
149
+ * these and state the observation age instead.
150
+ */
151
+ observed?: boolean;
133
152
  aggregation?: CodexCapacityAggregation;
134
153
  }
135
154
 
@@ -147,6 +166,7 @@ let invalidationEpoch = 0;
147
166
  export function clearProviderQuotaCache(): void {
148
167
  cache = null;
149
168
  clearCachedProviderQuotas();
169
+ clearProviderApiKeyQuotaCache();
150
170
  invalidationEpoch += 1;
151
171
  }
152
172
 
@@ -316,6 +336,15 @@ function isCanonicalClineBaseUrl(baseUrl: string): boolean {
316
336
  return normalized === CLINE_BASE_URL || normalized === `${CLINE_BASE_URL}/api/v1`;
317
337
  }
318
338
 
339
+ function isCanonicalOllamaCloudBaseUrl(baseUrl?: string): boolean {
340
+ if (!baseUrl) return false;
341
+ try {
342
+ return isCanonicalOllamaCloudUrl(baseUrl);
343
+ } catch {
344
+ return false;
345
+ }
346
+ }
347
+
319
348
  function isCanonicalZaiBaseUrl(baseUrl: string): boolean {
320
349
  const normalized = normalizedBaseUrl(baseUrl);
321
350
  return normalized === ZAI_BASE_URL
@@ -647,6 +676,78 @@ async function fetchClineQuota(provider: string, config: OcxProviderConfig): Pro
647
676
  return windows > 0 ? report(provider, "cline:plan-usage-limits", quota) : null;
648
677
  }
649
678
 
679
+ /**
680
+ * Ollama Cloud `GET https://ollama.com/api/usage` — returns account usage.
681
+ * Legacy plans report rolling 5-hour `limits.session.usage` and 7-day
682
+ * `limits.weekly.usage`. Migrated monthly-credit plans report
683
+ * `limits.monthly.usage`. `usage` values are normalized fractions (0..1).
684
+ */
685
+ function parseOllamaPercent(usageValue: unknown): number | undefined {
686
+ const usage = toFiniteNumber(usageValue);
687
+ if (usage === undefined || usage < 0) return undefined;
688
+ const percent = Math.round(usage * 10000) / 100;
689
+ return normalizePercent(percent);
690
+ }
691
+
692
+ export function parseOllamaCloudQuota(body: Record<string, unknown> | null): ProviderQuota | null {
693
+ if (!body) return null;
694
+ const limits = asRecord(body.limits);
695
+ if (!limits) return null;
696
+
697
+ const quota: ProviderQuota = { updatedAt: Date.now() };
698
+ let windows = 0;
699
+
700
+ const session = asRecord(limits.session);
701
+ if (session) {
702
+ const percent = parseOllamaPercent(session.usage);
703
+ if (percent !== undefined) {
704
+ quota.fiveHourPercent = percent;
705
+ windows += 1;
706
+ }
707
+ }
708
+
709
+ const weekly = asRecord(limits.weekly);
710
+ if (weekly) {
711
+ const percent = parseOllamaPercent(weekly.usage);
712
+ if (percent !== undefined) {
713
+ quota.weeklyPercent = percent;
714
+ windows += 1;
715
+ }
716
+ }
717
+
718
+ const monthly = asRecord(limits.monthly);
719
+ if (monthly) {
720
+ const percent = parseOllamaPercent(monthly.usage);
721
+ if (percent !== undefined) {
722
+ quota.monthlyPercent = percent;
723
+ windows += 1;
724
+ }
725
+ }
726
+
727
+ return windows > 0 ? quota : null;
728
+ }
729
+
730
+ async function fetchOllamaCloudQuota(provider: string, config: OcxProviderConfig): Promise<ProviderQuotaProbeResult> {
731
+ const effectiveBaseUrl = config.baseUrl ?? getProviderRegistryEntry(provider)?.baseUrl ?? "";
732
+ if (!isCanonicalOllamaCloudBaseUrl(effectiveBaseUrl)) return null;
733
+ const apiKey = resolveProviderApiKey(config.apiKey)?.trim();
734
+ if (!apiKey) return null;
735
+ const response = await fetch(OLLAMA_CLOUD_USAGE_URL, {
736
+ headers: { Accept: "application/json", Authorization: `Bearer ${apiKey}` },
737
+ redirect: "error",
738
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
739
+ });
740
+ if (!response.ok) {
741
+ if (response.status === 404) return null;
742
+ return response.status >= 400 && response.status < 500 && response.status !== 408 && response.status !== 429
743
+ ? TERMINAL_QUOTA_FAILURE
744
+ : null;
745
+ }
746
+ const body = asRecord(await readQuotaJson(response));
747
+ const quota = parseOllamaCloudQuota(body);
748
+ return quota ? report(provider, "ollama-cloud:usage", quota) : null;
749
+ }
750
+
650
751
  /**
651
752
  * Z.AI GLM Coding Plan `GET /api/monitor/usage/quota/limit` — the coding-plan
652
753
  * limits arrive as a `limits` array of `TOKENS_LIMIT` (newer plans call the
@@ -1085,7 +1186,8 @@ function tagNativeMainReport(
1085
1186
 
1086
1187
  function isProviderQuotaReportCurrent(value: ProviderQuotaReport): boolean {
1087
1188
  const generation = nativeMainReportGenerations.get(value);
1088
- return generation === undefined || isMainAccountIdentityGenerationLive(generation);
1189
+ return (generation === undefined || isMainAccountIdentityGenerationLive(generation))
1190
+ && (accountReportCurrent.get(value)?.() ?? true);
1089
1191
  }
1090
1192
 
1091
1193
  async function fetchChatGptForwardQuota(
@@ -1206,6 +1308,7 @@ export function parseXaiCreditsResponse(value: unknown): { percent: number; rese
1206
1308
  async function fetchXaiWeeklyCredits(accessToken: string, userId: string): Promise<ProviderQuota | null> {
1207
1309
  try {
1208
1310
  const response = await fetch(XAI_CREDITS_URL, {
1311
+ redirect: "error",
1209
1312
  headers: {
1210
1313
  Accept: "application/json",
1211
1314
  Authorization: `Bearer ${accessToken}`,
@@ -1229,16 +1332,11 @@ async function fetchXaiWeeklyCredits(accessToken: string, userId: string): Promi
1229
1332
  }
1230
1333
  }
1231
1334
 
1232
- async function fetchXaiQuota(provider: string): Promise<ProviderQuotaReport | null> {
1233
- let accessToken: string;
1234
- try {
1235
- accessToken = await getValidAccessToken("xai");
1236
- } catch {
1237
- return null;
1238
- }
1335
+ async function fetchXaiQuota(provider: string, context: { accessToken: string; upstreamAccountId?: string }): Promise<ProviderQuotaReport | null> {
1336
+ const { accessToken } = context;
1239
1337
 
1240
1338
  // Prefer the SuperGrok weekly credits window that actually gates prompting (#1283).
1241
- const userId = getCredential("xai")?.accountId?.trim() || xaiUserIdFromAccessToken(accessToken);
1339
+ const userId = context.upstreamAccountId?.trim() || xaiUserIdFromAccessToken(accessToken);
1242
1340
  if (userId) {
1243
1341
  const weekly = await fetchXaiWeeklyCredits(accessToken, userId);
1244
1342
  if (weekly) return report(provider, "xai:grok-billing-credits", weekly);
@@ -1247,6 +1345,7 @@ async function fetchXaiQuota(provider: string): Promise<ProviderQuotaReport | nu
1247
1345
  // Legacy monthly dollar pool — retained when weekly is unavailable.
1248
1346
  try {
1249
1347
  const response = await fetch(XAI_BILLING_URL, {
1348
+ redirect: "error",
1250
1349
  headers: { Accept: "application/json", Authorization: `Bearer ${accessToken}` },
1251
1350
  signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
1252
1351
  });
@@ -1427,7 +1526,9 @@ async function fetchPassiveProviderQuota(provider: string): Promise<ProviderQuot
1427
1526
  hydrateAccountQuotaCache();
1428
1527
  const entry = accountQuotaCache.get(accountCacheKey(provider, activeId));
1429
1528
  if (!entry?.quota) return null;
1430
- return report(provider, `${provider}:subscription-observation`, entry.quota);
1529
+ const built = report(provider, `${provider}:subscription-observation`, entry.quota);
1530
+ // Tagged here rather than inside report(), which every probed path shares.
1531
+ return built ? { ...built, observed: true } : null;
1431
1532
  }
1432
1533
 
1433
1534
  // ---------------------------------------------------------------------------
@@ -1447,8 +1548,12 @@ type AccountQuotaCacheEntry = {
1447
1548
  quota: ProviderQuota | null;
1448
1549
  /** Last probe failed (429 / network / expired login); still may hold last-good quota. */
1449
1550
  unavailable?: true;
1551
+ /** Private new-reader identity; never persisted or serialized. */
1552
+ identity?: string;
1553
+ isCurrent?: () => boolean;
1450
1554
  };
1451
1555
  const accountQuotaCache = new Map<string, AccountQuotaCacheEntry>();
1556
+ let explicitAccountEpoch = 0;
1452
1557
 
1453
1558
  /**
1454
1559
  * Seed the cache from the last run, once.
@@ -1492,11 +1597,21 @@ export interface ProviderAccountQuota {
1492
1597
  quota: ProviderQuota | null;
1493
1598
  /** Set when the probe could not reach upstream (expired login, 429, network). */
1494
1599
  unavailable?: true;
1600
+ isCurrent?: () => boolean;
1495
1601
  }
1496
1602
 
1497
1603
  /** Providers whose per-account quota can be probed. Extend as other OAuth APIs are covered. */
1498
1604
  export function supportsPerAccountQuota(provider: string): boolean {
1499
- return provider === "anthropic" || provider === "kiro" || provider === "google-antigravity";
1605
+ return provider === "anthropic" || provider === "kiro" || provider === "google-antigravity"
1606
+ || explicitAccountReader(provider);
1607
+ }
1608
+
1609
+ function explicitAccountReader(provider: string): boolean {
1610
+ return provider === "xai" || provider === "cursor" || provider === "kimi" || provider === "command-code";
1611
+ }
1612
+
1613
+ export function providerOAuthAccountQuotaMode(provider: string): AccountQuotaMode {
1614
+ return hasPassiveAccountQuota(provider) ? "passive" : supportsPerAccountQuota(provider) ? "probe" : "unsupported";
1500
1615
  }
1501
1616
 
1502
1617
  function accountCacheKey(provider: string, accountId: string): string {
@@ -1509,6 +1624,7 @@ function accountCacheKey(provider: string, accountId: string): string {
1509
1624
  */
1510
1625
  export function getCachedProviderAccountQuota(provider: string, accountId: string): ProviderQuota | null {
1511
1626
  const entry = accountQuotaCache.get(accountCacheKey(provider, accountId));
1627
+ if (entry?.isCurrent && !entry.isCurrent()) return null;
1512
1628
  return entry?.quota ?? null;
1513
1629
  }
1514
1630
 
@@ -1530,11 +1646,8 @@ export function setCachedProviderAccountQuotaForTests(
1530
1646
  * Providers whose per-account quota is OBSERVED in-band, never probed.
1531
1647
  *
1532
1648
  * Deliberately separate from `supportsPerAccountQuota` rather than folded into it. That
1533
- * predicate gates `fetchAccountQuota`, whose fallback branch sends any
1534
- * non-Kiro/non-Antigravity bearer to Anthropic's usage endpoint so adding `meta-muse`
1535
- * there without a dedicated branch would ship a Meta credential to Anthropic. And even
1536
- * with a branch it would be the wrong predicate: it means "this provider can be probed",
1537
- * and Meta publishes no quota endpoint to probe.
1649
+ * predicate gates explicit upstream readers. Meta publishes no quota endpoint, so it
1650
+ * remains a cache-only observation even when every probe reader is account-scoped.
1538
1651
  */
1539
1652
  export function hasPassiveAccountQuota(provider: string): boolean {
1540
1653
  return provider === "meta-muse";
@@ -1640,6 +1753,7 @@ export function resetProviderQuotaReconcileStateForTests(): void {
1640
1753
 
1641
1754
  /** Drop cached per-account rows (all, or just one provider's). */
1642
1755
  export function clearAccountQuotaCache(provider?: string): void {
1756
+ explicitAccountEpoch += 1;
1643
1757
  if (!provider) {
1644
1758
  accountQuotaCache.clear();
1645
1759
  accountQuotaInflight.clear();
@@ -1684,11 +1798,115 @@ async function getTokenForAccountQuotaProbe(provider: string, accountId: string)
1684
1798
  return getValidAccessTokenForAccount(provider, accountId);
1685
1799
  }
1686
1800
 
1801
+ function explicitQuotaConfig(provider: string, configured?: OcxProviderConfig): OcxProviderConfig | undefined {
1802
+ if (configured) return configured;
1803
+ const entry = getProviderRegistryEntry(provider);
1804
+ return entry ? { adapter: entry.adapter, baseUrl: entry.baseUrl, authMode: "oauth" } : undefined;
1805
+ }
1806
+
1807
+ function explicitQuotaIdentity(provider: string, accountId: string, configured?: OcxProviderConfig): string | undefined {
1808
+ const credential = getAccountCredential(provider, accountId);
1809
+ const target = explicitQuotaConfig(provider, configured);
1810
+ if (!credential || !target) return undefined;
1811
+ return createHash("sha256").update(JSON.stringify([
1812
+ provider, accountId, credential.access, credential.refresh, credential.expires,
1813
+ credential.accountId, credential.projectId, credential.source,
1814
+ target.adapter, target.baseUrl, target.authMode, target.disabled === true,
1815
+ ])).digest("hex");
1816
+ }
1817
+
1818
+ function explicitQuotaDestination(provider: string, config: OcxProviderConfig): boolean {
1819
+ if (config.disabled === true || config.authMode !== "oauth") return false;
1820
+ if (provider === "kimi") return isCanonicalKimiCodeBaseUrl(config.baseUrl);
1821
+ if (provider === "command-code") return isCanonicalCommandCodeBaseUrl(config.baseUrl);
1822
+ // These readers use fixed canonical billing origins, never config.baseUrl.
1823
+ return provider === "xai" || provider === "cursor";
1824
+ }
1825
+
1826
+ async function readExplicitAccountQuota(provider: string, accountId: string, configured?: OcxProviderConfig): Promise<{
1827
+ result: ProviderQuotaProbeResult;
1828
+ identity: string | undefined;
1829
+ isCurrent: () => boolean;
1830
+ } | null> {
1831
+ const target = explicitQuotaConfig(provider, configured);
1832
+ if (!target || !explicitQuotaDestination(provider, target)) return null;
1833
+ const config = { ...target };
1834
+ const epoch = explicitAccountEpoch;
1835
+ const accessToken = await getTokenForAccountQuotaProbe(provider, accountId);
1836
+ const credential = getAccountCredential(provider, accountId);
1837
+ if (!credential || credential.access !== accessToken) return null;
1838
+ // Pair the post-renewal credential with the destination captured before renewal.
1839
+ const identity = explicitQuotaIdentity(provider, accountId, config);
1840
+ const isCurrent = () => epoch === explicitAccountEpoch
1841
+ && identity === explicitQuotaIdentity(provider, accountId, configured);
1842
+ if (!isCurrent()) return null;
1843
+ let result: ProviderQuotaProbeResult;
1844
+ switch (provider) {
1845
+ case "xai": result = await fetchXaiQuota(provider, { accessToken, upstreamAccountId: credential.accountId }); break;
1846
+ case "cursor": result = await fetchCursorQuota(provider, accessToken); break;
1847
+ case "kimi": result = await fetchKimiQuota(provider, config, accessToken); break;
1848
+ case "command-code": result = await fetchCommandCodeQuota(provider, config, accessToken); break;
1849
+ default: return null;
1850
+ }
1851
+ return { result, identity, isCurrent };
1852
+ }
1853
+
1854
+ async function fetchExplicitAccountQuota(provider: string, accountId: string, force: boolean, configured?: OcxProviderConfig): Promise<AccountQuotaCacheEntry> {
1855
+ const key = accountCacheKey(provider, accountId);
1856
+ const identity = explicitQuotaIdentity(provider, accountId, configured);
1857
+ const previous = accountQuotaCache.get(key);
1858
+ const cached = identity && previous?.identity === identity && previous.isCurrent?.() ? previous : undefined;
1859
+ if (!force && cached && Date.now() - cached.ts < ACCOUNT_QUOTA_TTL_MS
1860
+ && (!cached.quota || Date.now() - cached.quota.updatedAt < LAST_GOOD_MAX_AGE_MS)) return cached;
1861
+ const flightKey = `${key}\u0000${identity ?? "missing"}`;
1862
+ const running = accountQuotaInflight.get(flightKey);
1863
+ if (running) return running;
1864
+ const epoch = explicitAccountEpoch;
1865
+ const lastGood = cached?.quota && Date.now() - cached.quota.updatedAt < LAST_GOOD_MAX_AGE_MS ? cached.quota : null;
1866
+ const flight = (async (): Promise<AccountQuotaCacheEntry> => {
1867
+ let read: Awaited<ReturnType<typeof readExplicitAccountQuota>> = null;
1868
+ try { read = await readExplicitAccountQuota(provider, accountId, configured); } catch { /* unavailable */ }
1869
+ const isCurrent = read?.isCurrent ?? (() => epoch === explicitAccountEpoch && !!identity
1870
+ && identity === explicitQuotaIdentity(provider, accountId, configured));
1871
+ const result = read?.result;
1872
+ const current = epoch === explicitAccountEpoch && isCurrent();
1873
+ const quota = current && result && typeof result !== "symbol" ? result.quota : null;
1874
+ const empty = result === AUTHORITATIVE_EMPTY_QUOTA;
1875
+ const entry: AccountQuotaCacheEntry = {
1876
+ ts: Date.now(),
1877
+ quota: quota ?? (current && result !== TERMINAL_QUOTA_FAILURE && !empty
1878
+ && lastGood && Date.now() - lastGood.updatedAt < LAST_GOOD_MAX_AGE_MS ? lastGood : null),
1879
+ ...(!current || (!quota && !empty) ? { unavailable: true as const } : {}),
1880
+ identity: read?.identity ?? identity,
1881
+ isCurrent: () => epoch === explicitAccountEpoch && isCurrent(),
1882
+ };
1883
+ if (entry.isCurrent?.()) accountQuotaCache.set(key, entry);
1884
+ return entry;
1885
+ })().finally(() => { if (accountQuotaInflight.get(flightKey) === flight) accountQuotaInflight.delete(flightKey); });
1886
+ accountQuotaInflight.set(flightKey, flight);
1887
+ return flight;
1888
+ }
1889
+
1890
+ async function fetchExplicitCurrentQuota(provider: string, config: OcxProviderConfig, liveConfig: OcxConfig): Promise<ProviderQuotaProbeResult> {
1891
+ const id = getAccountSet(provider)?.activeAccountId;
1892
+ if (!id) return null;
1893
+ const read = await readExplicitAccountQuota(provider, id, config);
1894
+ if (!read) return null;
1895
+ const isCurrent = () => liveConfig.providers[provider] === config
1896
+ && read.isCurrent() && getAccountSet(provider)?.activeAccountId === id;
1897
+ if (!isCurrent()) return TERMINAL_QUOTA_FAILURE;
1898
+ if (read.result && typeof read.result !== "symbol") accountReportCurrent.set(read.result, isCurrent);
1899
+ return read.result;
1900
+ }
1901
+
1687
1902
  async function fetchAccountQuota(
1688
1903
  provider: string,
1689
1904
  accountId: string,
1690
1905
  forceRefresh: boolean,
1906
+ providerConfig?: OcxProviderConfig,
1691
1907
  ): Promise<AccountQuotaCacheEntry> {
1908
+ if (!supportsPerAccountQuota(provider)) return { ts: Date.now(), quota: null, unavailable: true };
1909
+ if (explicitAccountReader(provider)) return fetchExplicitAccountQuota(provider, accountId, forceRefresh, providerConfig);
1692
1910
  const key = accountCacheKey(provider, accountId);
1693
1911
  const writerGeneration = captureConfigGeneration();
1694
1912
  const cached = accountQuotaCache.get(key);
@@ -1718,8 +1936,10 @@ async function fetchAccountQuota(
1718
1936
  const projectId = getAccountCredential(provider, accountId)?.projectId;
1719
1937
  if (!projectId) throw new Error("antigravity account has no project id");
1720
1938
  quota = await fetchAntigravityUsageQuota(token, projectId);
1721
- } else {
1939
+ } else if (provider === "anthropic") {
1722
1940
  quota = await fetchAnthropicUsageQuota(token);
1941
+ } else {
1942
+ return { ts: Date.now(), quota: null, unavailable: true };
1723
1943
  }
1724
1944
  }
1725
1945
  if (!quota) {
@@ -1772,18 +1992,27 @@ async function fetchAccountQuota(
1772
1992
  export async function fetchProviderAccountQuotas(
1773
1993
  provider: string,
1774
1994
  forceRefresh = false,
1995
+ providerConfig?: OcxProviderConfig,
1775
1996
  ): Promise<ProviderAccountQuota[]> {
1776
1997
  if (!supportsPerAccountQuota(provider)) return [];
1777
1998
  const set = getAccountSet(provider);
1778
1999
  if (!set) return [];
1779
- return await Promise.all(set.accounts.map(async account => {
1780
- const entry = await fetchAccountQuota(provider, account.id, forceRefresh);
1781
- return {
2000
+ return mapQuotaRoster(set.accounts, async account => {
2001
+ const entry = await fetchAccountQuota(provider, account.id, forceRefresh, providerConfig);
2002
+ const result: ProviderAccountQuota = {
1782
2003
  accountId: account.id,
1783
2004
  quota: entry.quota,
1784
2005
  ...(entry.unavailable ? { unavailable: true as const } : {}),
1785
2006
  };
1786
- }));
2007
+ if (!explicitAccountReader(provider)) return result;
2008
+ const identity = entry.identity;
2009
+ Object.defineProperty(result, "isCurrent", { value: () => {
2010
+ if (entry.isCurrent) return entry.isCurrent();
2011
+ const credential = getAccountCredential(provider, account.id);
2012
+ return !!credential && (!identity || explicitQuotaIdentity(provider, account.id, providerConfig) === identity);
2013
+ } });
2014
+ return result;
2015
+ });
1787
2016
  }
1788
2017
 
1789
2018
  function normalizedBaseUrl(value: string): string | null {
@@ -1902,10 +2131,10 @@ function parseKimiQuotaPayload(value: unknown): ProviderQuota | null {
1902
2131
  return hasQuotaRows(quota) ? quota : null;
1903
2132
  }
1904
2133
 
1905
- async function resolveKimiQuotaBearer(config: OcxProviderConfig): Promise<string | null> {
2134
+ async function resolveKimiQuotaBearer(config: OcxProviderConfig, accountId?: string): Promise<string | null> {
1906
2135
  if (config.authMode === "oauth") {
1907
2136
  try {
1908
- return await getValidAccessToken("kimi");
2137
+ return accountId ? await getTokenForAccountQuotaProbe("kimi", accountId) : null;
1909
2138
  } catch {
1910
2139
  return null;
1911
2140
  }
@@ -1917,13 +2146,13 @@ async function resolveKimiQuotaBearer(config: OcxProviderConfig): Promise<string
1917
2146
  return primary || null;
1918
2147
  }
1919
2148
 
1920
- async function fetchKimiQuota(provider: string, config: OcxProviderConfig): Promise<ProviderQuotaReport | null> {
2149
+ async function fetchKimiQuota(provider: string, config: OcxProviderConfig, accessToken: string): Promise<ProviderQuotaReport | null> {
1921
2150
  // Never release credentials to a user-edited or lookalike provider host.
1922
2151
  if (!isCanonicalKimiCodeBaseUrl(config.baseUrl)) return null;
1923
- const accessToken = await resolveKimiQuotaBearer(config);
1924
2152
  if (!accessToken) return null;
1925
2153
  const response = await fetch(KIMI_CODE_USAGE_URL, {
1926
2154
  headers: { Accept: "application/json", Authorization: `Bearer ${accessToken}` },
2155
+ redirect: "error",
1927
2156
  signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
1928
2157
  });
1929
2158
  if (!response.ok) return null;
@@ -2010,10 +2239,10 @@ async function fetchCommandCodeSpend(
2010
2239
  }
2011
2240
 
2012
2241
  /** OAuth access token or ACTIVE Provider-API key for the Command Code quota probe. */
2013
- async function resolveCommandCodeQuotaBearer(config: OcxProviderConfig): Promise<string | null> {
2242
+ async function resolveCommandCodeQuotaBearer(config: OcxProviderConfig, accountId?: string): Promise<string | null> {
2014
2243
  if (config.authMode === "oauth") {
2015
2244
  try {
2016
- return await getValidAccessToken("command-code");
2245
+ return accountId ? await getTokenForAccountQuotaProbe("command-code", accountId) : null;
2017
2246
  } catch {
2018
2247
  return null;
2019
2248
  }
@@ -2028,10 +2257,9 @@ async function resolveCommandCodeQuotaBearer(config: OcxProviderConfig): Promise
2028
2257
  * usage view uses (windowLimits.fiveHour / windowLimits.weekly), plus soft
2029
2258
  * whoami (team orgId scoping) and subscription-scoped spend for creditsUsd.
2030
2259
  */
2031
- async function fetchCommandCodeQuota(provider: string, config: OcxProviderConfig): Promise<ProviderQuotaProbeResult> {
2260
+ async function fetchCommandCodeQuota(provider: string, config: OcxProviderConfig, bearer: string): Promise<ProviderQuotaProbeResult> {
2032
2261
  // Never release credentials to a user-edited or lookalike provider host.
2033
2262
  if (!isCanonicalCommandCodeBaseUrl(config.baseUrl)) return null;
2034
- const bearer = await resolveCommandCodeQuotaBearer(config);
2035
2263
  if (!bearer) return null;
2036
2264
  const whoamiBody = await fetchCommandCodeJson(COMMAND_CODE_WHOAMI_URL, bearer);
2037
2265
  const whoami = asRecord(whoamiBody?.data) ?? whoamiBody;
@@ -2071,13 +2299,7 @@ async function fetchCommandCodeQuota(provider: string, config: OcxProviderConfig
2071
2299
  }
2072
2300
 
2073
2301
  /** Cursor included usage via api2.cursor.sh (Bearer from OAuth) — unofficial, may change. */
2074
- async function fetchCursorQuota(provider: string): Promise<ProviderQuotaReport | null> {
2075
- let accessToken: string;
2076
- try {
2077
- accessToken = await getValidAccessToken("cursor");
2078
- } catch {
2079
- return null;
2080
- }
2302
+ async function fetchCursorQuota(provider: string, accessToken: string): Promise<ProviderQuotaReport | null> {
2081
2303
 
2082
2304
  const authHeaders = {
2083
2305
  Accept: "application/json",
@@ -2090,6 +2312,7 @@ async function fetchCursorQuota(provider: string): Promise<ProviderQuotaReport |
2090
2312
  try {
2091
2313
  const periodRes = await fetch("https://api2.cursor.sh/aiserver.v1.DashboardService/GetCurrentPeriodUsage", {
2092
2314
  method: "POST",
2315
+ redirect: "error",
2093
2316
  headers: {
2094
2317
  ...authHeaders,
2095
2318
  "Content-Type": "application/json",
@@ -2158,6 +2381,7 @@ async function fetchCursorQuota(provider: string): Promise<ProviderQuotaReport |
2158
2381
  try {
2159
2382
  const summaryRes = await fetch("https://api2.cursor.sh/api/usage/summary", {
2160
2383
  headers: authHeaders,
2384
+ redirect: "error",
2161
2385
  signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
2162
2386
  });
2163
2387
  if (summaryRes.ok) {
@@ -2187,6 +2411,7 @@ async function fetchCursorQuota(provider: string): Promise<ProviderQuotaReport |
2187
2411
 
2188
2412
  const response = await fetch("https://api2.cursor.sh/auth/usage", {
2189
2413
  headers: authHeaders,
2414
+ redirect: "error",
2190
2415
  signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
2191
2416
  });
2192
2417
  if (!response.ok) return null;
@@ -2273,10 +2498,11 @@ function classifyAntigravityFamily(modelId: string, modelInfo: Record<string, un
2273
2498
  }
2274
2499
 
2275
2500
  function antigravityUsedPercent(quotaInfo: Record<string, unknown>): number | undefined {
2276
- const remaining = normalizePercent(toFiniteNumber(quotaInfo.remainingFraction) !== undefined
2277
- ? toFiniteNumber(quotaInfo.remainingFraction)! * 100
2278
- : toFiniteNumber(quotaInfo.remainingPercentage) !== undefined
2279
- ? toFiniteNumber(quotaInfo.remainingPercentage)! * 100
2501
+ const target = asRecord(quotaInfo.remaining) ?? quotaInfo;
2502
+ const remaining = normalizePercent(toFiniteNumber(target.remainingFraction) !== undefined
2503
+ ? toFiniteNumber(target.remainingFraction)! * 100
2504
+ : toFiniteNumber(target.remainingPercentage) !== undefined
2505
+ ? toFiniteNumber(target.remainingPercentage)! * 100
2280
2506
  : undefined);
2281
2507
  if (remaining === undefined) return undefined;
2282
2508
  return normalizePercent(100 - remaining);
@@ -2311,6 +2537,75 @@ function antigravityWindowsFromModels(body: Record<string, unknown> | null): Pro
2311
2537
  return customWindows;
2312
2538
  }
2313
2539
 
2540
+ /**
2541
+ * Parse Google Antigravity quota from `v1internal:retrieveUserQuotaSummary`.
2542
+ * Groups contain Gemini models and Claude/3P models, each with 5h and weekly limit buckets.
2543
+ */
2544
+ function parseAntigravityQuotaSummary(body: Record<string, unknown> | null): ProviderQuota | null {
2545
+ const groups = Array.isArray(body?.groups) ? (body.groups as unknown[]) : [];
2546
+ if (groups.length === 0) return null;
2547
+
2548
+ const customWindowsMap = new Map<string, ProviderQuotaWindow>();
2549
+
2550
+ for (const rawGroup of groups) {
2551
+ const group = asRecord(rawGroup);
2552
+ if (!group) continue;
2553
+ const groupName = `${typeof group.displayName === "string" ? group.displayName : ""} ${typeof group.description === "string" ? group.description : ""}`.toLowerCase();
2554
+ const isGemini = groupName.includes("gemini");
2555
+ const isClaude = groupName.includes("claude") || groupName.includes("3p") || groupName.includes("gpt");
2556
+
2557
+ const buckets = Array.isArray(group.buckets) ? (group.buckets as unknown[]) : [];
2558
+ for (const rawBucket of buckets) {
2559
+ const bucket = asRecord(rawBucket);
2560
+ if (!bucket) continue;
2561
+ const windowStr = `${typeof bucket.window === "string" ? bucket.window : ""} ${typeof bucket.bucketId === "string" ? bucket.bucketId : ""} ${typeof bucket.displayName === "string" ? bucket.displayName : ""}`.toLowerCase();
2562
+ const percent = antigravityUsedPercent(bucket);
2563
+ if (percent === undefined) continue;
2564
+ const resetAt = normalizeResetAt(bucket.resetTime);
2565
+
2566
+ const isWeekly = windowStr.includes("week");
2567
+ const is5h = windowStr.includes("5h") || windowStr.includes("five");
2568
+
2569
+ if (isGemini) {
2570
+ const label = is5h ? "Gem" : isWeekly ? "Gem (Weekly)" : "";
2571
+ if (label && !customWindowsMap.has(label)) {
2572
+ customWindowsMap.set(label, { label, percent, ...(resetAt !== undefined ? { resetAt } : {}) });
2573
+ }
2574
+ } else if (isClaude) {
2575
+ const label = is5h ? "Cla" : isWeekly ? "Cla (Weekly)" : "";
2576
+ if (label && !customWindowsMap.has(label)) {
2577
+ customWindowsMap.set(label, { label, percent, ...(resetAt !== undefined ? { resetAt } : {}) });
2578
+ }
2579
+ } else {
2580
+ const baseLabel = typeof group.displayName === "string" ? group.displayName : "Other";
2581
+ const label = isWeekly ? `${baseLabel} (Weekly)` : baseLabel;
2582
+ if (!customWindowsMap.has(label)) {
2583
+ customWindowsMap.set(label, { label, percent, ...(resetAt !== undefined ? { resetAt } : {}) });
2584
+ }
2585
+ }
2586
+ }
2587
+ }
2588
+
2589
+ const PREFERRED_ORDER = ["Gem", "Gem (Weekly)", "Cla", "Cla (Weekly)"];
2590
+ const customWindows = Array.from(customWindowsMap.values()).sort((a, b) => {
2591
+ const ia = PREFERRED_ORDER.indexOf(a.label);
2592
+ const ib = PREFERRED_ORDER.indexOf(b.label);
2593
+ if (ia !== -1 && ib !== -1) return ia - ib;
2594
+ if (ia !== -1) return -1;
2595
+ if (ib !== -1) return 1;
2596
+ return a.label.localeCompare(b.label);
2597
+ });
2598
+
2599
+ if (customWindows.length === 0) {
2600
+ return null;
2601
+ }
2602
+
2603
+ return {
2604
+ customWindows,
2605
+ updatedAt: Date.now(),
2606
+ };
2607
+ }
2608
+
2314
2609
  const ANTIGRAVITY_ACCOUNT_QUOTA_BASE = "https://daily-cloudcode-pa.googleapis.com";
2315
2610
  let antigravityOutboundDependencies: ProviderOutboundDependencies = {};
2316
2611
 
@@ -2327,6 +2622,28 @@ export function setAntigravityAccountQuotaTransportForTests(dependencies: Provid
2327
2622
  * A redirect or non-2xx yields null (unavailable), never a partial row.
2328
2623
  */
2329
2624
  export async function fetchAntigravityUsageQuota(accessToken: string, projectId: string): Promise<ProviderQuota | null> {
2625
+ const summaryUrl = `${ANTIGRAVITY_ACCOUNT_QUOTA_BASE}/v1internal:retrieveUserQuotaSummary`;
2626
+ try {
2627
+ const summaryResponse = await providerOutboundPost("google-antigravity", { baseUrl: ANTIGRAVITY_ACCOUNT_QUOTA_BASE }, summaryUrl, {
2628
+ headers: {
2629
+ Accept: "application/json",
2630
+ "Content-Type": "application/json",
2631
+ "User-Agent": antigravityUserAgent(),
2632
+ Authorization: `Bearer ${accessToken}`,
2633
+ },
2634
+ body: JSON.stringify({ project: projectId }),
2635
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
2636
+ }, antigravityOutboundDependencies);
2637
+ if (await providerRedirectError(summaryResponse, summaryUrl)) return null;
2638
+ if (summaryResponse.status === 401 || summaryResponse.status === 403) return null;
2639
+ if (summaryResponse.ok) {
2640
+ const quota = parseAntigravityQuotaSummary(asRecord(await readQuotaJson(summaryResponse)));
2641
+ if (quota) return quota;
2642
+ }
2643
+ } catch {
2644
+ // Fallback to fetchAvailableModels on error
2645
+ }
2646
+
2330
2647
  const url = `${ANTIGRAVITY_ACCOUNT_QUOTA_BASE}/v1internal:fetchAvailableModels`;
2331
2648
  const response = await providerOutboundPost("google-antigravity", { baseUrl: ANTIGRAVITY_ACCOUNT_QUOTA_BASE }, url, {
2332
2649
  headers: {
@@ -2355,6 +2672,35 @@ async function fetchAntigravityQuota(provider: string, config: OcxProviderConfig
2355
2672
  return null;
2356
2673
  }
2357
2674
  const baseUrl = (config.baseUrl || ANTIGRAVITY_ACCOUNT_QUOTA_BASE).replace(/\/+$/, "");
2675
+
2676
+ // The summary probe is pinned to Google's own host through the provider-outbound
2677
+ // transport, mirroring `fetchAntigravityUsageQuota` above: a configured `baseUrl` is a
2678
+ // routing choice for requests, not a second source of Google's accounting, and this
2679
+ // request carries the account bearer.
2680
+ const summaryUrl = `${ANTIGRAVITY_ACCOUNT_QUOTA_BASE}/v1internal:retrieveUserQuotaSummary`;
2681
+ try {
2682
+ const summaryResponse = await providerOutboundPost("google-antigravity", { baseUrl: ANTIGRAVITY_ACCOUNT_QUOTA_BASE }, summaryUrl, {
2683
+ headers: {
2684
+ Accept: "application/json",
2685
+ "Content-Type": "application/json",
2686
+ "User-Agent": antigravityUserAgent(),
2687
+ Authorization: `Bearer ${accessToken}`,
2688
+ },
2689
+ body: JSON.stringify({ project: credential.projectId }),
2690
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
2691
+ }, antigravityOutboundDependencies);
2692
+ if (await providerRedirectError(summaryResponse, summaryUrl)) return null;
2693
+ if (summaryResponse.status === 401 || summaryResponse.status === 403) return null;
2694
+ if (summaryResponse.ok) {
2695
+ const quota = parseAntigravityQuotaSummary(asRecord(await readQuotaJson(summaryResponse)));
2696
+ if (quota) {
2697
+ return report(provider, "google-antigravity:retrieveUserQuotaSummary", quota);
2698
+ }
2699
+ }
2700
+ } catch {
2701
+ // Fallback on network/fetch error
2702
+ }
2703
+
2358
2704
  const response = await fetch(`${baseUrl}/v1internal:fetchAvailableModels`, {
2359
2705
  method: "POST",
2360
2706
  headers: {
@@ -2375,6 +2721,54 @@ async function fetchAntigravityQuota(provider: string, config: OcxProviderConfig
2375
2721
  });
2376
2722
  }
2377
2723
 
2724
+ type KeyQuotaReader = (name: string, provider: OcxProviderConfig) => Promise<ProviderQuotaProbeResult>;
2725
+
2726
+ /** Same selector drives cheap capabilities and uncached reads; never resolves credentials. */
2727
+ function keyQuotaReaderForProvider(name: string, provider: OcxProviderConfig): KeyQuotaReader | null {
2728
+ if (provider.disabled === true || (provider.authMode ?? "key") !== "key") return null;
2729
+ if (isCanonicalKimiCodeBaseUrl(provider.baseUrl)) {
2730
+ return async (id, config) => {
2731
+ const bearer = await resolveKimiQuotaBearer(config);
2732
+ return bearer ? fetchKimiQuota(id, config, bearer) : null;
2733
+ };
2734
+ }
2735
+ if (name === "commandcode" && isCanonicalCommandCodeBaseUrl(provider.baseUrl)) {
2736
+ return async (id, config) => {
2737
+ const bearer = await resolveCommandCodeQuotaBearer(config);
2738
+ return bearer ? fetchCommandCodeQuota(id, config, bearer) : null;
2739
+ };
2740
+ }
2741
+ if (registryEntryForProviderDestination(provider)?.id === "opencode-go") return fetchOpenCodeGoQuota;
2742
+ if (isCanonicalA6apiBaseUrl(provider.baseUrl)) return fetchA6apiQuota;
2743
+ if (name === "openrouter" && isCanonicalOpenRouterBaseUrl(provider.baseUrl)) return fetchOpenRouterQuota;
2744
+ if (name === "deepseek" && isCanonicalDeepSeekBaseUrl(provider.baseUrl)) return fetchDeepSeekQuota;
2745
+ if (name === "cline-pass" && isCanonicalClineBaseUrl(provider.baseUrl)) return fetchClineQuota;
2746
+ if (isCanonicalOllamaCloudBaseUrl(provider.baseUrl ?? getProviderRegistryEntry(name)?.baseUrl)) return fetchOllamaCloudQuota;
2747
+ if (["zai", "glm", "glm-cn", "zhipu-bigmodel-coding"].includes(name) && isCanonicalZaiBaseUrl(provider.baseUrl)) return fetchZaiQuota;
2748
+ if (["minimax", "minimax-cn"].includes(name) && isCanonicalMinimaxBaseUrl(provider.baseUrl)) return fetchMinimaxQuota;
2749
+ if (name === "moonshot" && isCanonicalMoonshotBaseUrl(provider.baseUrl)) return fetchMoonshotQuota;
2750
+ if (name === "venice" && isCanonicalVeniceBaseUrl(provider.baseUrl)) return fetchVeniceQuota;
2751
+ if (name === "synthetic" && isCanonicalSyntheticBaseUrl(provider.baseUrl)) return fetchSyntheticQuota;
2752
+ if (name === "deepinfra" && isCanonicalDeepInfraBaseUrl(provider.baseUrl)) return fetchDeepInfraQuota;
2753
+ if (name === "neuralwatt" && isCanonicalNeuralwattBaseUrl(provider.baseUrl)) return fetchNeuralwattQuota;
2754
+ return null;
2755
+ }
2756
+
2757
+ export function providerApiKeyQuotaMode(name: string, provider: OcxProviderConfig): AccountQuotaMode {
2758
+ return keyQuotaReaderForProvider(name, provider) ? "probe" : "unsupported";
2759
+ }
2760
+
2761
+ export async function fetchProviderApiKeyQuotas(config: OcxConfig, name: string, forceRefresh = false): Promise<ProviderApiKeyQuota[]> {
2762
+ const provider = config.providers[name];
2763
+ if (!provider || !keyQuotaReaderForProvider(name, provider)) return [];
2764
+ return readProviderApiKeyQuotas(config, name, forceRefresh, async (isolatedProvider, isolatedConfig) => {
2765
+ const result = await maybeFetchProviderQuota(name, isolatedProvider, isolatedConfig, false);
2766
+ if (result === TERMINAL_QUOTA_FAILURE) return { kind: "terminal" };
2767
+ if (result === AUTHORITATIVE_EMPTY_QUOTA) return { kind: "empty" };
2768
+ return result ? { kind: "quota", quota: result.quota } : { kind: "unavailable" };
2769
+ });
2770
+ }
2771
+
2378
2772
  async function maybeFetchProviderQuota(
2379
2773
  name: string,
2380
2774
  provider: OcxProviderConfig,
@@ -2387,79 +2781,108 @@ async function maybeFetchProviderQuota(
2387
2781
  if (isBuiltInChatGptForwardProvider(name, provider)) {
2388
2782
  return fetchChatGptForwardQuota(config, name, provider, forceRefresh, prefetchedCodexSnapshot);
2389
2783
  }
2390
- if (provider.authMode === "oauth" && name === "xai") return fetchXaiQuota(name);
2784
+ if (provider.authMode === "oauth" && explicitAccountReader(name)) return await fetchExplicitCurrentQuota(name, provider, config);
2391
2785
  if (provider.authMode === "oauth" && name === "anthropic") return fetchAnthropicQuota(name);
2392
- if (provider.authMode === "oauth" && name === "cursor") return fetchCursorQuota(name);
2393
2786
  if (provider.authMode === "oauth" && name === "google-antigravity") return fetchAntigravityQuota(name, provider);
2394
2787
  if (provider.authMode === "oauth" && name === "kiro") return fetchKiroQuota(name);
2395
2788
  // Passive providers (meta-muse): Meta publishes no quota endpoint, so there is no
2396
2789
  // probe to run — the row is the active account's last in-band observation.
2397
2790
  if (provider.authMode === "oauth" && hasPassiveAccountQuota(name)) return fetchPassiveProviderQuota(name);
2398
- // Kimi Code `/usages` accepts OAuth or coding-plan API keys, but only on the canonical
2399
- // host and only for real key auth — forward/local modes carry no credential of ours.
2400
- if (provider.authMode === "oauth" && name === "kimi") return fetchKimiQuota(name, provider);
2401
- if (provider.authMode === "key" && isCanonicalKimiCodeBaseUrl(provider.baseUrl)) {
2402
- return fetchKimiQuota(name, provider);
2403
- }
2404
- // OAuth account login or Provider-API key only; forward/local modes carry no
2405
- // credential of ours on the canonical host.
2406
- if (provider.authMode === "oauth" && name === "command-code") {
2407
- return fetchCommandCodeQuota(name, provider);
2408
- }
2409
- if ((provider.authMode ?? "key") === "key" && name === "commandcode"
2410
- && isCanonicalCommandCodeBaseUrl(provider.baseUrl)) {
2411
- return fetchCommandCodeQuota(name, provider);
2412
- }
2413
- // Identify OpenCode Go by where it routes, not by what the row is called. Multi-account
2414
- // setups keep the same destination under names like `opencode-go-2` (#1924), and those rows
2415
- // silently had no quota panel and no `ocx provider quota --json` report while the literal
2416
- // name was the gate. `registryEntryForProviderDestination` is the existing predicate for
2417
- // exactly this question: normalized endpoint + adapter + key auth, so a canonical URL behind
2418
- // a different adapter is still not OpenCode Go. The defensive URL check inside
2419
- // `fetchOpenCodeGoQuota` stays — sending a key anywhere must not depend on this gate.
2420
- if ((provider.authMode ?? "key") === "key" && registryEntryForProviderDestination(provider)?.id === "opencode-go") {
2421
- return fetchOpenCodeGoQuota(name, provider);
2422
- }
2423
- if ((provider.authMode ?? "key") === "key" && isCanonicalA6apiBaseUrl(provider.baseUrl)) {
2424
- return fetchA6apiQuota(name, provider);
2425
- }
2426
- if ((provider.authMode ?? "key") === "key" && name === "openrouter") {
2427
- return fetchOpenRouterQuota(name, provider);
2428
- }
2429
- if ((provider.authMode ?? "key") === "key" && name === "deepseek") {
2430
- return fetchDeepSeekQuota(name, provider);
2431
- }
2432
- if ((provider.authMode ?? "key") === "key" && name === "cline-pass") {
2433
- return fetchClineQuota(name, provider);
2434
- }
2435
- if ((provider.authMode ?? "key") === "key"
2436
- && (name === "zai" || name === "glm" || name === "glm-cn" || name === "zhipu-bigmodel-coding")) {
2437
- return fetchZaiQuota(name, provider);
2438
- }
2439
- if ((provider.authMode ?? "key") === "key" && (name === "minimax" || name === "minimax-cn")) {
2440
- return fetchMinimaxQuota(name, provider);
2441
- }
2442
- if ((provider.authMode ?? "key") === "key" && name === "moonshot") {
2443
- return fetchMoonshotQuota(name, provider);
2444
- }
2445
- if ((provider.authMode ?? "key") === "key" && name === "venice") {
2446
- return fetchVeniceQuota(name, provider);
2447
- }
2448
- if ((provider.authMode ?? "key") === "key" && name === "synthetic") {
2449
- return fetchSyntheticQuota(name, provider);
2450
- }
2451
- if ((provider.authMode ?? "key") === "key" && name === "deepinfra") {
2452
- return fetchDeepInfraQuota(name, provider);
2453
- }
2454
- if ((provider.authMode ?? "key") === "key" && name === "neuralwatt") {
2455
- return fetchNeuralwattQuota(name, provider);
2456
- }
2457
- return null;
2791
+ const reader = keyQuotaReaderForProvider(name, provider);
2792
+ return reader ? reader(name, provider) : null;
2458
2793
  } catch {
2459
2794
  return null;
2460
2795
  }
2461
2796
  }
2462
2797
 
2798
+ /**
2799
+ * Hand freshly committed provider reports to the optional quota-reset observer.
2800
+ *
2801
+ * Lazy import on purpose: this module is statically reachable from
2802
+ * src/server/responses/core.ts (via oauth/anthropic-routing.ts), so a static edge would put
2803
+ * the observer and its sink registry on every install's request path.
2804
+ *
2805
+ * No previous snapshot is passed. The observer keeps its own persisted last-seen map,
2806
+ * because `previous` here is bound only when `cache.key === key` and the key digest at
2807
+ * cacheKeyWithAggregationState includes quota values and updatedAt — so it is empty exactly
2808
+ * when a reset happened.
2809
+ *
2810
+ * Account identity is resolved SYNCHRONOUSLY, before any await. Two reasons, both observed:
2811
+ * pool failover rewrites `activeAccountId` during request routing
2812
+ * (promoteAnthropicActiveAccount -> setActiveAccount), so a 429 between this commit and a
2813
+ * later async read would attribute this report to a different account and overwrite that
2814
+ * account's baseline with these numbers. fetchAnthropicQuota already captures
2815
+ * `probedAccountId` before awaiting for exactly this reason; the observer must not be
2816
+ * sloppier than the cache it observes.
2817
+ *
2818
+ * Observations are serialized through a module-level promise chain for the same reason as
2819
+ * the codex seam: Bun does not resolve concurrent import() calls in call order, and an
2820
+ * out-of-order baseline swap manufactures false resets that then burn the durable
2821
+ * idempotence key.
2822
+ */
2823
+ let pendingProviderObservation: Promise<void> = Promise.resolve();
2824
+
2825
+ /**
2826
+ * Stable per-account observation key for one provider report.
2827
+ *
2828
+ * OAuth providers key by active account id. Key-auth providers have NO account set, so
2829
+ * every key in `apiKeyPool` would collapse onto "default" and rotating from a spent key to
2830
+ * a fresh one would read as a reset (measured: 97% -> 12% fired a false surprise). The
2831
+ * cache key already discriminates these at cacheKey() via apiKeyPoolEntryId, so this
2832
+ * mirrors that discriminator instead of inventing a second notion of identity.
2833
+ */
2834
+ function providerObservationAccountKey(provider: string, config: OcxConfig): string {
2835
+ const oauthAccountId = getAccountSet(provider)?.activeAccountId;
2836
+ if (oauthAccountId !== undefined) return `${provider}\u0000${oauthAccountId}`;
2837
+ const providerConfig = config.providers[provider];
2838
+ const resolvedKey = typeof providerConfig?.apiKey === "string"
2839
+ ? resolveEnvValue(providerConfig.apiKey)?.trim()
2840
+ : undefined;
2841
+ const keyId = resolvedKey ? apiKeyPoolEntryId(resolvedKey) : "default";
2842
+ return `${provider}\u0000key:${keyId}`;
2843
+ }
2844
+
2845
+ /** Test-only view of the observation account key. */
2846
+ export function providerObservationAccountKeyForTests(provider: string, config: OcxConfig): string {
2847
+ return providerObservationAccountKey(provider, config);
2848
+ }
2849
+
2850
+ function notifyProviderQuotaSnapshot(
2851
+ reports: ReadonlyArray<ProviderQuotaReport>,
2852
+ config: OcxConfig,
2853
+ ): void {
2854
+ if (reports.length === 0) return;
2855
+ // Resolved here, synchronously, while the identity is still the one that produced these
2856
+ // reports.
2857
+ const observations = reports.map(report => ({
2858
+ scope: report.provider,
2859
+ accountKey: providerObservationAccountKey(report.provider, config),
2860
+ quota: report.quota,
2861
+ }));
2862
+ pendingProviderObservation = pendingProviderObservation
2863
+ .then(async () => {
2864
+ const observer = await import("../quota/reset-observer");
2865
+ if (!observer.hasQuotaResetSink()) return;
2866
+ const { providerWindowObservations } = await import("../quota/window-mapping");
2867
+ for (const observation of observations) {
2868
+ observer.observeQuotaSnapshot({
2869
+ scope: observation.scope,
2870
+ accountKey: observation.accountKey,
2871
+ windows: providerWindowObservations(observation.quota),
2872
+ });
2873
+ }
2874
+ })
2875
+ .catch(() => {
2876
+ // Detection is best-effort: a quota refresh must never fail because of it. Swallowing
2877
+ // here also keeps the chain alive — a rejected link would poison every later one.
2878
+ });
2879
+ }
2880
+
2881
+ /** Await the observation chain. Tests only: production never needs to join it. */
2882
+ export function flushProviderQuotaObservationsForTests(): Promise<void> {
2883
+ return pendingProviderObservation;
2884
+ }
2885
+
2463
2886
  export async function fetchProviderQuotaReports(config: OcxConfig, forceRefresh = false): Promise<ProviderQuotaResponse> {
2464
2887
  // A Pool report's cache signature and provider fetch must share one account snapshot.
2465
2888
  // Preserve force semantics when deciding whether that snapshot refreshes upstream data.
@@ -2472,9 +2895,14 @@ export async function fetchProviderQuotaReports(config: OcxConfig, forceRefresh
2472
2895
  const now = Date.now();
2473
2896
  // The cache fast path must not extend a preserved last-good row past its 30-minute bound:
2474
2897
  // a row preserved at age 29:59 plus a full 5-minute TTL would otherwise serve until ~35min.
2898
+ // An OBSERVED row is exempt: it carries the observation time, which is older than the bound
2899
+ // by construction and never becomes fresher on its own. Without the exemption a single
2900
+ // configured passive provider makes this predicate permanently false, so every dashboard
2901
+ // poll re-probes every OTHER provider upstream instead of serving the 5-minute cache.
2475
2902
  const cacheFresh = cache && cache.key === key && now - cache.ts < CACHE_TTL_MS
2476
2903
  && cache.response.reports.every(item =>
2477
- now - item.updatedAt < LAST_GOOD_MAX_AGE_MS && isProviderQuotaReportCurrent(item));
2904
+ (item.observed === true || now - item.updatedAt < LAST_GOOD_MAX_AGE_MS)
2905
+ && isProviderQuotaReportCurrent(item));
2478
2906
  if (!forceRefresh && cacheFresh) return cache!.response;
2479
2907
  const joinable = inflight.get(key);
2480
2908
  if (!forceRefresh && joinable && joinable.epoch === invalidationEpoch) return joinable.promise;
@@ -2518,7 +2946,9 @@ export async function fetchProviderQuotaReports(config: OcxConfig, forceRefresh
2518
2946
  const byProvider = new Map<string, ProviderQuotaReport>();
2519
2947
  const generationMismatchedProviders = new Set<string>();
2520
2948
  for (const item of previous) {
2521
- if (item.updatedAt < cutoff) continue;
2949
+ // Same exemption as the fast path. A passive row reaching `previous` is not a probe
2950
+ // that went quiet — there is no probe — so age cannot condemn it.
2951
+ if (item.observed !== true && item.updatedAt < cutoff) continue;
2522
2952
  if (isProviderQuotaReportCurrent(item)) byProvider.set(item.provider, item);
2523
2953
  else generationMismatchedProviders.add(item.provider);
2524
2954
  }
@@ -2547,6 +2977,7 @@ export async function fetchProviderQuotaReports(config: OcxConfig, forceRefresh
2547
2977
  const reports = response.reports.filter(item => mayCommitProviderQuotaKey(item.provider, writerGeneration));
2548
2978
  cache = { key, ts: Date.now(), response: { ...response, reports } };
2549
2979
  replaceCachedProviderQuotas(reports);
2980
+ notifyProviderQuotaSnapshot(reports, config);
2550
2981
  }
2551
2982
  return response;
2552
2983
  })();