@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.
- package/gui/dist/assets/{index-VuoiWj9J.js → index-D4zuyIxQ.js} +1 -1
- package/gui/dist/index.html +1 -1
- package/package.json +2 -1
- package/src/adapters/base.ts +21 -0
- package/src/adapters/cursor/transport-retry.ts +46 -1
- package/src/adapters/cursor.ts +4 -0
- package/src/adapters/kiro/adapter.ts +42 -1
- package/src/adapters/kiro-retry.ts +23 -4
- package/src/adapters/openai-chat/errors.ts +116 -0
- package/src/adapters/openai-chat/messages.ts +346 -0
- package/src/adapters/openai-chat/passthrough.ts +146 -0
- package/src/adapters/openai-chat/response-events.ts +117 -0
- package/src/adapters/openai-chat/tool-call-validation.ts +200 -0
- package/src/adapters/openai-chat/tool-schema.ts +477 -0
- package/src/adapters/openai-chat/wire.ts +50 -0
- package/src/adapters/openai-chat.ts +33 -1445
- package/src/adapters/openai-responses/canonical-forward.ts +202 -0
- package/src/adapters/openai-responses/image-gen.ts +406 -0
- package/src/adapters/openai-responses/internal.ts +3 -0
- package/src/adapters/openai-responses/passthrough.ts +611 -0
- package/src/adapters/openai-responses/prompt-cache.ts +83 -0
- package/src/adapters/openai-responses/reasoning.ts +220 -0
- package/src/adapters/openai-responses/request-strips.ts +185 -0
- package/src/adapters/openai-responses/tool-output-recovery.ts +509 -0
- package/src/adapters/openai-responses/tool-schema.ts +293 -0
- package/src/adapters/openai-responses/web-search.ts +156 -0
- package/src/adapters/openai-responses.ts +4 -2625
- package/src/bridge/errors.ts +34 -0
- package/src/bridge/internal.ts +174 -0
- package/src/bridge/response-json.ts +624 -0
- package/src/bridge/sse.ts +1444 -0
- package/src/bridge.ts +5 -2204
- package/src/chat/inbound.ts +12 -1
- package/src/codex/account-lifecycle.ts +3 -0
- package/src/codex/account-store.ts +71 -9
- package/src/codex/auth-api/account-list.ts +507 -0
- package/src/codex/auth-api/http.ts +32 -0
- package/src/codex/auth-api/login-flow.ts +554 -0
- package/src/codex/auth-api/login-state.ts +64 -0
- package/src/codex/auth-api/main-account-probe.ts +331 -0
- package/src/codex/auth-api/pool-mode-gate.ts +274 -0
- package/src/codex/auth-api/pool-quota-probe.ts +512 -0
- package/src/codex/auth-api/reset-credit-service.ts +422 -0
- package/src/codex/auth-api/routes.ts +425 -0
- package/src/codex/auth-api/runtime-config.ts +48 -0
- package/src/codex/auth-api.ts +27 -3118
- package/src/codex/auth-context.ts +95 -28
- package/src/codex/catalog/auto-review.ts +507 -0
- package/src/codex/catalog/build-entries.ts +981 -0
- package/src/codex/catalog/combo-member.ts +375 -0
- package/src/codex/catalog/derive-entry.ts +229 -0
- package/src/codex/catalog/effort.ts +0 -1
- package/src/codex/catalog/gated-native-warn.ts +63 -0
- package/src/codex/catalog/gather-capture.ts +533 -0
- package/src/codex/catalog/model-hints.ts +691 -0
- package/src/codex/catalog/model-visibility.ts +304 -0
- package/src/codex/catalog/provider-fetch.ts +52 -2942
- package/src/codex/catalog/provider-models.ts +685 -0
- package/src/codex/catalog/restore.ts +132 -0
- package/src/codex/catalog/retained-sync.ts +706 -0
- package/src/codex/catalog/routed-gather.ts +858 -0
- package/src/codex/catalog/subagent-roster.ts +176 -0
- package/src/codex/catalog/sync.ts +52 -2698
- package/src/codex/inject/config-toml.ts +563 -0
- package/src/codex/inject/remove.ts +192 -0
- package/src/codex/inject/restore.ts +540 -0
- package/src/codex/inject/routing-classify.ts +109 -0
- package/src/codex/inject/routing-target.ts +125 -0
- package/src/codex/inject.ts +81 -1436
- package/src/codex/lineage.ts +458 -0
- package/src/codex/pool-refresh-backoff.ts +152 -0
- package/src/codex/routing/active-account.ts +194 -0
- package/src/codex/routing/cooldown-math.ts +275 -0
- package/src/codex/routing/health-store.ts +402 -0
- package/src/codex/routing/probe-lease.ts +358 -0
- package/src/codex/routing/selection.ts +703 -0
- package/src/codex/routing/thread-affinity.ts +538 -0
- package/src/codex/routing.ts +353 -2234
- package/src/codex/shim-fingerprint.ts +223 -0
- package/src/codex/shim-inspect.ts +175 -0
- package/src/codex/shim-probe.ts +367 -0
- package/src/codex/shim-restore-lock.ts +169 -0
- package/src/codex/shim-state-file.ts +151 -0
- package/src/codex/shim-templates.ts +265 -0
- package/src/codex/shim.ts +48 -1268
- package/src/config/diagnostics.ts +705 -0
- package/src/config/feature-flags.ts +55 -0
- package/src/config/live-reconcile.ts +403 -0
- package/src/config/load-degrade.ts +880 -0
- package/src/config/mutation-lock.ts +244 -0
- package/src/config/openai-tier-backup.ts +268 -0
- package/src/config/persist-unlocked.ts +92 -0
- package/src/config/proxy-env.ts +188 -0
- package/src/config/salvage.ts +244 -0
- package/src/config/schema/config-schema.ts +640 -0
- package/src/config/schema/leaf-validators.ts +855 -0
- package/src/config/warn-memo.ts +28 -0
- package/src/config.ts +234 -4481
- package/src/generated/compatibility-version.json +539 -39
- package/src/lib/request-execution-budget.ts +69 -20
- package/src/lib/spend-reservation-ledger.ts +940 -0
- package/src/lib/upstream-retry.ts +55 -11
- package/src/lib/workflow-budget.ts +553 -30
- package/src/providers/quota/account-cache.ts +441 -0
- package/src/providers/quota/antigravity.ts +295 -0
- package/src/providers/quota/report-cache.ts +320 -0
- package/src/providers/quota/vendor-probes-key.ts +1243 -0
- package/src/providers/quota/vendor-probes-oauth.ts +590 -0
- package/src/providers/quota.ts +324 -3079
- package/src/providers/registry/entries-core.ts +1221 -0
- package/src/providers/registry/entries-extended.ts +1204 -0
- package/src/providers/registry/model-seeds.ts +908 -0
- package/src/providers/registry/types.ts +352 -0
- package/src/providers/registry.ts +24 -3536
- package/src/responses/continuation-ownership.ts +29 -0
- package/src/responses/state/replay-fingerprint.ts +80 -0
- package/src/responses/state/snapshot-codec.ts +104 -0
- package/src/responses/state/spill-failure.ts +118 -0
- package/src/responses/state/spill-queue.ts +665 -0
- package/src/responses/state/temp-recovery.ts +257 -0
- package/src/responses/state.ts +82 -1143
- package/src/routing/identity-domains.ts +449 -0
- package/src/routing/probe-lease.ts +511 -0
- package/src/server/index/bounded-request.ts +88 -0
- package/src/server/index/live-sideband.ts +565 -0
- package/src/server/index/serve-options.ts +1766 -0
- package/src/server/index/startup-warnings.ts +213 -0
- package/src/server/index/websocket-handler.ts +335 -0
- package/src/server/index.ts +40 -2547
- package/src/server/management/route-registry.ts +26 -23
- package/src/server/management/shared.ts +8 -5
- package/src/server/management/workflow-budget-routes.ts +133 -0
- package/src/server/management-api.ts +12 -0
- package/src/server/request-log-conversation.ts +9 -7
- package/src/server/request-log.ts +245 -1
- package/src/server/responses/account-change-state.ts +233 -0
- package/src/server/responses/adapter-continuation.ts +514 -0
- package/src/server/responses/adapter-delivery.ts +214 -0
- package/src/server/responses/adapter-dispatch.ts +971 -0
- package/src/server/responses/compact.ts +59 -4
- package/src/server/responses/completion-policy.ts +33 -0
- package/src/server/responses/core-auth.ts +527 -0
- package/src/server/responses/core-codex-account.ts +859 -0
- package/src/server/responses/core-combo-failure.ts +210 -0
- package/src/server/responses/core-combo.ts +707 -0
- package/src/server/responses/core-errors.ts +152 -0
- package/src/server/responses/core-lifetime.ts +95 -0
- package/src/server/responses/core-normalize.ts +350 -0
- package/src/server/responses/core-opaque-recovery.ts +380 -0
- package/src/server/responses/core-options.ts +159 -0
- package/src/server/responses/core-replay.ts +225 -0
- package/src/server/responses/core.ts +192 -8893
- package/src/server/responses/passthrough-delivery.ts +856 -0
- package/src/server/responses/passthrough-dispatch.ts +1476 -0
- package/src/server/responses/passthrough-execution.ts +54 -0
- package/src/server/responses/request-prepare.ts +970 -0
- package/src/server/responses/request-send-budget.ts +164 -0
- package/src/server/responses/request-sidecar-auth.ts +149 -0
- package/src/server/responses/request-transport.ts +744 -0
- package/src/server/responses/response-effects.ts +157 -0
- package/src/server/responses/run-turn-execution.ts +448 -0
- package/src/server/responses/sidecar-execution.ts +469 -0
- package/src/server/responses-image-gen-repair.ts +1 -1
- package/src/server/workflow-refusal.ts +84 -0
- package/src/types/config.ts +30 -0
- package/src/usage/log.ts +146 -0
- package/src/usage/summary.ts +171 -21
|
@@ -0,0 +1,590 @@
|
|
|
1
|
+
import { effectiveCodexAuthAccountId, fetchMainAccountInfoSnapshot, listCodexAuthAccountsSnapshot } from "../../codex/auth-api";
|
|
2
|
+
import { MAIN_CODEX_ACCOUNT_ID } from "../../codex/main-account";
|
|
3
|
+
import { getValidAccessToken } from "../../oauth";
|
|
4
|
+
import { getAccountCredential, getAccountSet } from "../../oauth/store";
|
|
5
|
+
import { fetchMuseKeyQuotaSnapshot } from "../muse-key-quota";
|
|
6
|
+
import { XAI_GROK_CLIENT_VERSION, XAI_GROK_COMPATIBILITY } from "../xai-transport";
|
|
7
|
+
import {
|
|
8
|
+
commitKiroAccountUsageState,
|
|
9
|
+
fetchKiroUsageSnapshot,
|
|
10
|
+
type KiroUsageSnapshot,
|
|
11
|
+
kiroUsageContextForAccount,
|
|
12
|
+
} from "../kiro-usage";
|
|
13
|
+
import { captureConfigGeneration } from "../../lib/state-store-sweeper";
|
|
14
|
+
import { aggregateCodexPoolCapacity, CODEX_CAPACITY_MAX_QUOTA_AGE_MS, type CodexCapacityQuota } from "../codex-capacity";
|
|
15
|
+
import { asRecord, normalizePercent, normalizeResetAt, readQuotaJson, REQUEST_TIMEOUT_MS, toFiniteNumber } from "../quota-wire";
|
|
16
|
+
import { providerCodexAccountMode } from "../registry";
|
|
17
|
+
import {
|
|
18
|
+
hasQuotaRows,
|
|
19
|
+
providerLabel,
|
|
20
|
+
providerQuotaFromCodexQuota,
|
|
21
|
+
publicCapacityAggregation,
|
|
22
|
+
report,
|
|
23
|
+
tagNativeMainReport,
|
|
24
|
+
type CodexAuthAccountsSnapshotPromise,
|
|
25
|
+
type ProviderQuotaReport,
|
|
26
|
+
} from "./report-cache";
|
|
27
|
+
import {
|
|
28
|
+
accountCacheKey,
|
|
29
|
+
accountQuotaCache,
|
|
30
|
+
hydrateAccountQuotaCache,
|
|
31
|
+
mayCommitAccountQuotaKey,
|
|
32
|
+
persistAccountQuotaCache,
|
|
33
|
+
} from "./account-cache";
|
|
34
|
+
import type { OcxConfig, OcxProviderConfig } from "../../types";
|
|
35
|
+
import type { ProviderQuota, ProviderQuotaWindow } from "../quota-types";
|
|
36
|
+
|
|
37
|
+
const XAI_BILLING_URL = "https://cli-chat-proxy.grok.com/v1/billing";
|
|
38
|
+
const XAI_CREDITS_URL = `${XAI_BILLING_URL}?format=credits`;
|
|
39
|
+
|
|
40
|
+
export async function fetchChatGptForwardQuota(
|
|
41
|
+
config: OcxConfig,
|
|
42
|
+
provider: string,
|
|
43
|
+
providerConfig: OcxProviderConfig,
|
|
44
|
+
forceRefresh: boolean,
|
|
45
|
+
prefetchedSnapshot?: CodexAuthAccountsSnapshotPromise,
|
|
46
|
+
): Promise<ProviderQuotaReport | null> {
|
|
47
|
+
if (providerCodexAccountMode(provider, providerConfig) === "direct") {
|
|
48
|
+
const snapshot = await fetchMainAccountInfoSnapshot(forceRefresh);
|
|
49
|
+
const quota = providerQuotaFromCodexQuota(snapshot.info.quota);
|
|
50
|
+
if (quota) quota.updatedAt = Date.now();
|
|
51
|
+
return quota
|
|
52
|
+
? tagNativeMainReport(report(provider, "chatgpt:wham", quota), snapshot.mainIdentityGeneration)
|
|
53
|
+
: null;
|
|
54
|
+
}
|
|
55
|
+
const snapshot = await (prefetchedSnapshot ?? listCodexAuthAccountsSnapshot(config, forceRefresh));
|
|
56
|
+
const accounts = snapshot.accounts;
|
|
57
|
+
const activeId = effectiveCodexAuthAccountId(config);
|
|
58
|
+
const capacityAccounts = accounts.map(account => ({
|
|
59
|
+
...account,
|
|
60
|
+
active: account.id === activeId,
|
|
61
|
+
quota: providerQuotaFromCodexQuota(account.quota),
|
|
62
|
+
}));
|
|
63
|
+
const active = capacityAccounts.find(account => account.active)
|
|
64
|
+
?? capacityAccounts.find(account => account.id === MAIN_CODEX_ACCOUNT_ID)
|
|
65
|
+
?? capacityAccounts[0];
|
|
66
|
+
const now = Date.now();
|
|
67
|
+
const capacity = aggregateCodexPoolCapacity(capacityAccounts, now);
|
|
68
|
+
if (capacity.aggregation && capacity.quota) {
|
|
69
|
+
return tagNativeMainReport(
|
|
70
|
+
report(
|
|
71
|
+
provider,
|
|
72
|
+
"chatgpt:wham",
|
|
73
|
+
capacity.quota as ProviderQuota,
|
|
74
|
+
publicCapacityAggregation(capacity.aggregation, "aggregate"),
|
|
75
|
+
),
|
|
76
|
+
snapshot.mainIdentityGeneration,
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
const activeUsable = !!active && !active.paused && active.needsReauth !== true;
|
|
80
|
+
const quota = activeUsable && active?.quota
|
|
81
|
+
? { ...active.quota, updatedAt: active.quota.updatedAt ?? Date.now() } as CodexCapacityQuota
|
|
82
|
+
: null;
|
|
83
|
+
const quotaFresh = !!quota
|
|
84
|
+
&& Number.isFinite(quota.updatedAt)
|
|
85
|
+
&& now - quota.updatedAt < CODEX_CAPACITY_MAX_QUOTA_AGE_MS;
|
|
86
|
+
if (quota && quotaFresh) {
|
|
87
|
+
const fallback = report(
|
|
88
|
+
provider,
|
|
89
|
+
"chatgpt:wham",
|
|
90
|
+
quota as ProviderQuota,
|
|
91
|
+
capacity.aggregation
|
|
92
|
+
? publicCapacityAggregation(capacity.aggregation, "effective-account-fallback")
|
|
93
|
+
: undefined,
|
|
94
|
+
);
|
|
95
|
+
return tagNativeMainReport(fallback, snapshot.mainIdentityGeneration);
|
|
96
|
+
}
|
|
97
|
+
if (capacity.aggregation) {
|
|
98
|
+
const updatedAt = Date.now();
|
|
99
|
+
return tagNativeMainReport(
|
|
100
|
+
{
|
|
101
|
+
provider,
|
|
102
|
+
label: providerLabel(provider),
|
|
103
|
+
source: "chatgpt:wham",
|
|
104
|
+
quota: { updatedAt },
|
|
105
|
+
updatedAt,
|
|
106
|
+
aggregation: publicCapacityAggregation(capacity.aggregation, "coverage-only"),
|
|
107
|
+
},
|
|
108
|
+
snapshot.mainIdentityGeneration,
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function centsValue(value: unknown): number | undefined {
|
|
115
|
+
const rec = asRecord(value);
|
|
116
|
+
return rec ? toFiniteNumber(rec.val) : undefined;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** Decode JWT payload `sub` for xAI weekly credits when the stored credential lacks accountId. */
|
|
120
|
+
function xaiUserIdFromAccessToken(accessToken: string): string | undefined {
|
|
121
|
+
const parts = accessToken.split(".");
|
|
122
|
+
if (parts.length < 2 || !parts[1]) return undefined;
|
|
123
|
+
try {
|
|
124
|
+
const payload = JSON.parse(Buffer.from(parts[1], "base64url").toString("utf8")) as { sub?: unknown };
|
|
125
|
+
return typeof payload.sub === "string" && payload.sub.trim() ? payload.sub.trim() : undefined;
|
|
126
|
+
} catch {
|
|
127
|
+
return undefined;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Grok Build weekly credits envelope:
|
|
133
|
+
* `{ config: { creditUsagePercent?, currentPeriod: { type: "USAGE_PERIOD_TYPE_WEEKLY", end } } }`.
|
|
134
|
+
* Omitted percent is treated as 0 (proto3 default).
|
|
135
|
+
*/
|
|
136
|
+
export function parseXaiCreditsResponse(value: unknown): { percent: number; resetAt?: number } | null {
|
|
137
|
+
const body = asRecord(value);
|
|
138
|
+
const config = asRecord(body?.config);
|
|
139
|
+
if (!config) return null;
|
|
140
|
+
const period = asRecord(config.currentPeriod);
|
|
141
|
+
if (!period || period.type !== "USAGE_PERIOD_TYPE_WEEKLY") return null;
|
|
142
|
+
let percent = 0;
|
|
143
|
+
if (config.creditUsagePercent !== undefined) {
|
|
144
|
+
const normalized = normalizePercent(config.creditUsagePercent);
|
|
145
|
+
if (normalized === undefined) return null;
|
|
146
|
+
percent = normalized;
|
|
147
|
+
}
|
|
148
|
+
const resetAt = normalizeResetAt(period.end);
|
|
149
|
+
return {
|
|
150
|
+
percent,
|
|
151
|
+
...(resetAt !== undefined ? { resetAt } : {}),
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
async function fetchXaiWeeklyCredits(accessToken: string, userId: string): Promise<ProviderQuota | null> {
|
|
156
|
+
try {
|
|
157
|
+
const response = await fetch(XAI_CREDITS_URL, {
|
|
158
|
+
redirect: "error",
|
|
159
|
+
headers: {
|
|
160
|
+
Accept: "application/json",
|
|
161
|
+
Authorization: `Bearer ${accessToken}`,
|
|
162
|
+
[XAI_GROK_COMPATIBILITY.headers.tokenAuth]: "xai-grok-cli",
|
|
163
|
+
[XAI_GROK_COMPATIBILITY.headers.authenticateResponse]: "authenticate-response",
|
|
164
|
+
"x-userid": userId,
|
|
165
|
+
[XAI_GROK_COMPATIBILITY.headers.clientVersion]: XAI_GROK_CLIENT_VERSION,
|
|
166
|
+
},
|
|
167
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
168
|
+
});
|
|
169
|
+
if (!response.ok) return null;
|
|
170
|
+
const parsed = parseXaiCreditsResponse(await readQuotaJson(response));
|
|
171
|
+
if (!parsed) return null;
|
|
172
|
+
return {
|
|
173
|
+
weeklyPercent: parsed.percent,
|
|
174
|
+
...(parsed.resetAt !== undefined ? { weeklyResetAt: parsed.resetAt } : {}),
|
|
175
|
+
updatedAt: Date.now(),
|
|
176
|
+
};
|
|
177
|
+
} catch {
|
|
178
|
+
return null;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export async function fetchXaiQuota(provider: string, context: { accessToken: string; upstreamAccountId?: string }): Promise<ProviderQuotaReport | null> {
|
|
183
|
+
const { accessToken } = context;
|
|
184
|
+
|
|
185
|
+
// Prefer the SuperGrok weekly credits window that actually gates prompting (#1283).
|
|
186
|
+
const userId = context.upstreamAccountId?.trim() || xaiUserIdFromAccessToken(accessToken);
|
|
187
|
+
if (userId) {
|
|
188
|
+
const weekly = await fetchXaiWeeklyCredits(accessToken, userId);
|
|
189
|
+
if (weekly) return report(provider, "xai:grok-billing-credits", weekly);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Legacy monthly dollar pool — retained when weekly is unavailable.
|
|
193
|
+
try {
|
|
194
|
+
const response = await fetch(XAI_BILLING_URL, {
|
|
195
|
+
redirect: "error",
|
|
196
|
+
headers: { Accept: "application/json", Authorization: `Bearer ${accessToken}` },
|
|
197
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
198
|
+
});
|
|
199
|
+
if (!response.ok) return null;
|
|
200
|
+
const body = asRecord(await readQuotaJson(response));
|
|
201
|
+
const config = asRecord(body?.config);
|
|
202
|
+
if (!config) return null;
|
|
203
|
+
const limitCents = centsValue(config.monthlyLimit);
|
|
204
|
+
const usedCents = centsValue(config.used);
|
|
205
|
+
if (limitCents === undefined || usedCents === undefined || limitCents <= 0) return null;
|
|
206
|
+
const percent = normalizePercent((usedCents / limitCents) * 100);
|
|
207
|
+
if (percent === undefined) return null;
|
|
208
|
+
return report(provider, "xai:grok-billing", {
|
|
209
|
+
monthlyPercent: percent,
|
|
210
|
+
monthlyResetAt: normalizeResetAt(config.billingPeriodEnd),
|
|
211
|
+
updatedAt: Date.now(),
|
|
212
|
+
});
|
|
213
|
+
} catch {
|
|
214
|
+
return null;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function parseClaudeBucket(value: unknown): { percent?: number; resetAt?: number } | null {
|
|
219
|
+
const rec = asRecord(value);
|
|
220
|
+
if (!rec) return null;
|
|
221
|
+
const percent = normalizePercent(rec.utilization);
|
|
222
|
+
const resetAt = normalizeResetAt(rec.resets_at);
|
|
223
|
+
if (percent === undefined && resetAt === undefined) return null;
|
|
224
|
+
return { percent, resetAt };
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function parseClaudeLimit(value: unknown): { label: string; percent: number; resetAt?: number } | null {
|
|
228
|
+
const rec = asRecord(value);
|
|
229
|
+
if (!rec) return null;
|
|
230
|
+
const percent = normalizePercent(rec.percent);
|
|
231
|
+
if (percent === undefined) return null;
|
|
232
|
+
const scope = asRecord(rec.scope);
|
|
233
|
+
const model = asRecord(scope?.model);
|
|
234
|
+
const rawLabel = String(model?.display_name ?? "").trim();
|
|
235
|
+
if (!rawLabel) return null;
|
|
236
|
+
const lowerLabel = rawLabel.toLowerCase();
|
|
237
|
+
const label = lowerLabel.includes("fable") ? "Fable"
|
|
238
|
+
: lowerLabel.includes("opus") ? "Opus"
|
|
239
|
+
: lowerLabel.includes("sonnet") ? "Sonnet"
|
|
240
|
+
: rawLabel;
|
|
241
|
+
const resetAt = normalizeResetAt(rec.resets_at);
|
|
242
|
+
return { label, percent, ...(resetAt !== undefined ? { resetAt } : {}) };
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/** Claude's OAuth usage endpoint, probed with ONE account's own bearer token. */
|
|
246
|
+
const anthropicUsageInflight = new Map<string, Promise<ProviderQuota | null>>();
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Anthropic per-credential usage.
|
|
250
|
+
*
|
|
251
|
+
* This endpoint reports quota only. Its body carries `five_hour`, `seven_day`, the
|
|
252
|
+
* model-scoped weekly buckets (`seven_day_fable`/`_opus`/`_sonnet`) and a `limits` array,
|
|
253
|
+
* and **no subscription or tier field** — nor does the OAuth token response, which yields only
|
|
254
|
+
* `account.uuid` and `account.email_address` (`src/oauth/anthropic.ts`). That is why
|
|
255
|
+
* `OAuthAccountSummary.plan` is `null` for Anthropic rather than populated here (#3777); it is
|
|
256
|
+
* a missing upstream field, not an unfinished mapping.
|
|
257
|
+
*
|
|
258
|
+
* A tier must not be inferred from what is here. Percentages are normalized per account, so a
|
|
259
|
+
* Max x5 seat at 50% is byte-identical to a Max x20 seat at 50%, and the presence of a
|
|
260
|
+
* model-scoped window tracks entitlement rather than seat size. Populate `plan` only when
|
|
261
|
+
* upstream returns the tier itself.
|
|
262
|
+
*/
|
|
263
|
+
export async function fetchAnthropicUsageQuota(accessToken: string): Promise<ProviderQuota | null> {
|
|
264
|
+
const joinable = anthropicUsageInflight.get(accessToken);
|
|
265
|
+
if (joinable) return joinable;
|
|
266
|
+
|
|
267
|
+
const probe = (async (): Promise<ProviderQuota | null> => {
|
|
268
|
+
const response = await fetch("https://api.anthropic.com/api/oauth/usage", {
|
|
269
|
+
headers: {
|
|
270
|
+
Accept: "application/json, text/plain, */*",
|
|
271
|
+
"Content-Type": "application/json",
|
|
272
|
+
"User-Agent": "claude-cli/2.1.63 (external, cli)",
|
|
273
|
+
"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",
|
|
274
|
+
Authorization: `Bearer ${accessToken}`,
|
|
275
|
+
},
|
|
276
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
277
|
+
});
|
|
278
|
+
if (!response.ok) return null;
|
|
279
|
+
const body = asRecord(await readQuotaJson(response));
|
|
280
|
+
if (!body) return null;
|
|
281
|
+
const fiveHour = parseClaudeBucket(body.five_hour);
|
|
282
|
+
const sevenDay = parseClaudeBucket(body.seven_day);
|
|
283
|
+
const fable = parseClaudeBucket(body.seven_day_fable);
|
|
284
|
+
const opus = parseClaudeBucket(body.seven_day_opus);
|
|
285
|
+
const sonnet = parseClaudeBucket(body.seven_day_sonnet);
|
|
286
|
+
const customWindows: ProviderQuotaWindow[] = [];
|
|
287
|
+
if (fable?.percent !== undefined) customWindows.push({ label: "Fable", percent: fable.percent, ...(fable.resetAt !== undefined ? { resetAt: fable.resetAt } : {}) });
|
|
288
|
+
if (opus?.percent !== undefined) customWindows.push({ label: "Opus", percent: opus.percent, ...(opus.resetAt !== undefined ? { resetAt: opus.resetAt } : {}) });
|
|
289
|
+
if (sonnet?.percent !== undefined) customWindows.push({ label: "Sonnet", percent: sonnet.percent, ...(sonnet.resetAt !== undefined ? { resetAt: sonnet.resetAt } : {}) });
|
|
290
|
+
const knownLabels = new Set(customWindows.map(window => window.label.toLowerCase()));
|
|
291
|
+
const limits = Array.isArray(body.limits) ? body.limits : [];
|
|
292
|
+
for (const rawLimit of limits) {
|
|
293
|
+
const limitRecord = asRecord(rawLimit);
|
|
294
|
+
// `session` and `weekly_all` mirror the canonical five-hour and weekly
|
|
295
|
+
// buckets above; only model-scoped weekly limits add a third window.
|
|
296
|
+
if (String(limitRecord?.kind ?? "").trim().toLowerCase() !== "weekly_scoped") continue;
|
|
297
|
+
const limit = parseClaudeLimit(rawLimit);
|
|
298
|
+
if (!limit || knownLabels.has(limit.label.toLowerCase())) continue;
|
|
299
|
+
knownLabels.add(limit.label.toLowerCase());
|
|
300
|
+
customWindows.push(limit);
|
|
301
|
+
}
|
|
302
|
+
const quota: ProviderQuota = {
|
|
303
|
+
// Claude's 5-hour window is a first-class rate limit, same as the Codex login 5h/weekly
|
|
304
|
+
// rows: report it in the canonical fields so the dashboard renders it with the standard
|
|
305
|
+
// "5-hour limit" label and ordering instead of as a generic extra window.
|
|
306
|
+
...(fiveHour?.percent !== undefined ? { fiveHourPercent: fiveHour.percent } : {}),
|
|
307
|
+
...(fiveHour?.resetAt !== undefined ? { fiveHourResetAt: fiveHour.resetAt } : {}),
|
|
308
|
+
...(sevenDay?.percent !== undefined ? { weeklyPercent: sevenDay.percent } : {}),
|
|
309
|
+
...(sevenDay?.resetAt !== undefined ? { weeklyResetAt: sevenDay.resetAt } : {}),
|
|
310
|
+
...(customWindows.length > 0 ? { customWindows } : {}),
|
|
311
|
+
updatedAt: Date.now(),
|
|
312
|
+
};
|
|
313
|
+
// Empty / schema-changed payloads must not cache as "success with no bars".
|
|
314
|
+
return hasQuotaRows(quota) ? quota : null;
|
|
315
|
+
})().finally(() => {
|
|
316
|
+
if (anthropicUsageInflight.get(accessToken) === probe) anthropicUsageInflight.delete(accessToken);
|
|
317
|
+
});
|
|
318
|
+
anthropicUsageInflight.set(accessToken, probe);
|
|
319
|
+
return probe;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
export async function fetchAnthropicQuota(provider: string): Promise<ProviderQuotaReport | null> {
|
|
323
|
+
// Capture the account we intend to probe before awaiting — a mid-flight active
|
|
324
|
+
// switch must not seed the wrong account's cache with this response.
|
|
325
|
+
const probedAccountId = getAccountSet("anthropic")?.activeAccountId;
|
|
326
|
+
const probedAccountKey = probedAccountId ? accountCacheKey("anthropic", probedAccountId) : null;
|
|
327
|
+
const writerGeneration = captureConfigGeneration();
|
|
328
|
+
let accessToken: string;
|
|
329
|
+
try {
|
|
330
|
+
accessToken = await getValidAccessToken("anthropic");
|
|
331
|
+
} catch {
|
|
332
|
+
return null;
|
|
333
|
+
}
|
|
334
|
+
const quota = await fetchAnthropicUsageQuota(accessToken);
|
|
335
|
+
if (!quota) return null;
|
|
336
|
+
// Share the active-account probe with the per-account cache so Providers-page
|
|
337
|
+
// loads do not double-hit Anthropic's rate-limited usage endpoint.
|
|
338
|
+
if (probedAccountId && probedAccountKey) {
|
|
339
|
+
const stillOwnsToken = getAccountCredential("anthropic", probedAccountId)?.access === accessToken;
|
|
340
|
+
if (stillOwnsToken && mayCommitAccountQuotaKey(probedAccountKey, writerGeneration)) {
|
|
341
|
+
accountQuotaCache.set(probedAccountKey, { ts: Date.now(), quota });
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
return report(provider, "anthropic:oauth-usage", quota);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* Provider-level Kiro row: the active account's usage, shown on the Providers page.
|
|
349
|
+
*
|
|
350
|
+
* The per-account cache is seeded from the same probe so opening that page does not read
|
|
351
|
+
* the active account twice, and the account id is captured before the await so a
|
|
352
|
+
* concurrent account switch cannot file this answer under the wrong account.
|
|
353
|
+
*/
|
|
354
|
+
export async function fetchKiroQuota(provider: string): Promise<ProviderQuotaReport | null> {
|
|
355
|
+
const probedAccountId = getAccountSet("kiro")?.activeAccountId;
|
|
356
|
+
if (!probedAccountId) return null;
|
|
357
|
+
const probedAccountKey = accountCacheKey("kiro", probedAccountId);
|
|
358
|
+
const writerGeneration = captureConfigGeneration();
|
|
359
|
+
let snapshot: KiroUsageSnapshot | null;
|
|
360
|
+
try {
|
|
361
|
+
snapshot = await fetchKiroUsageSnapshot(await kiroUsageContextForAccount(probedAccountId));
|
|
362
|
+
} catch {
|
|
363
|
+
return null;
|
|
364
|
+
}
|
|
365
|
+
if (!snapshot) return null;
|
|
366
|
+
if (mayCommitAccountQuotaKey(probedAccountKey, writerGeneration)) {
|
|
367
|
+
accountQuotaCache.set(probedAccountKey, { ts: Date.now(), quota: snapshot.quota });
|
|
368
|
+
commitKiroAccountUsageState(probedAccountKey, snapshot);
|
|
369
|
+
}
|
|
370
|
+
return report(provider, "kiro:usage-limits", snapshot.quota);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* Provider-level row probed from the key endpoint, for an account that CAN be probed.
|
|
375
|
+
*
|
|
376
|
+
* Written through the same account cache the passive path reads, so the measurement
|
|
377
|
+
* survives a restart and the per-account rows at oauth-account-routes.ts:313 pick it up
|
|
378
|
+
* with no mode change. Deliberately does not flip providerOAuthAccountQuotaMode: that
|
|
379
|
+
* mode selects readPassiveProviderAccountQuotas, and the probed per-account path it would
|
|
380
|
+
* switch to is gated on supportsPerAccountQuota, which has no meta-muse reader, so the
|
|
381
|
+
* GUI account list would go from showing observations to showing nothing.
|
|
382
|
+
*/
|
|
383
|
+
export async function fetchMuseKeyQuota(provider: string): Promise<ProviderQuotaReport | null> {
|
|
384
|
+
const probedAccountId = getAccountSet(provider)?.activeAccountId;
|
|
385
|
+
if (!probedAccountId) return null;
|
|
386
|
+
const oauthAccessToken = getAccountCredential(provider, probedAccountId)?.muse?.oauthAccessToken;
|
|
387
|
+
// An imported or pasted credential has no account token and never will: it is
|
|
388
|
+
// capability, not provider id, that decides whether a probe is possible.
|
|
389
|
+
if (!oauthAccessToken) return null;
|
|
390
|
+
const probedAccountKey = accountCacheKey(provider, probedAccountId);
|
|
391
|
+
const writerGeneration = captureConfigGeneration();
|
|
392
|
+
const quota = await fetchMuseKeyQuotaSnapshot(probedAccountId, oauthAccessToken);
|
|
393
|
+
if (!quota) return null;
|
|
394
|
+
if (mayCommitAccountQuotaKey(probedAccountKey, writerGeneration)) {
|
|
395
|
+
// Hydrate before writing, for the same reason recordPassiveAccountQuota does:
|
|
396
|
+
// persistAccountQuotaCache serializes the whole in-memory map.
|
|
397
|
+
hydrateAccountQuotaCache();
|
|
398
|
+
accountQuotaCache.set(probedAccountKey, { ts: Date.now(), quota });
|
|
399
|
+
persistAccountQuotaCache();
|
|
400
|
+
}
|
|
401
|
+
return report(provider, `${provider}:key-endpoint`, quota);
|
|
402
|
+
}
|
|
403
|
+
/**
|
|
404
|
+
* Provider-level row for a passive provider: the ACTIVE account's last observed
|
|
405
|
+
* subscription windows, the same shape `fetchAnthropicQuota` and `fetchKiroQuota`
|
|
406
|
+
* return.
|
|
407
|
+
*
|
|
408
|
+
* Cache-only. A dashboard load or `ocx account refresh` must never spend an inference
|
|
409
|
+
* turn, so `forceRefresh` does not exist on this path — there is nothing to refresh.
|
|
410
|
+
* `report.updatedAt` is the observation time, which is what both GUI surfaces render
|
|
411
|
+
* as the relative age of the row.
|
|
412
|
+
*/
|
|
413
|
+
export async function fetchPassiveProviderQuota(provider: string): Promise<ProviderQuotaReport | null> {
|
|
414
|
+
const activeId = getAccountSet(provider)?.activeAccountId;
|
|
415
|
+
if (!activeId) return null;
|
|
416
|
+
// Idempotent; without it a proxy restart shows nothing until the next streaming turn
|
|
417
|
+
// even though the last observation is on disk.
|
|
418
|
+
hydrateAccountQuotaCache();
|
|
419
|
+
const entry = accountQuotaCache.get(accountCacheKey(provider, activeId));
|
|
420
|
+
if (!entry?.quota) return null;
|
|
421
|
+
const built = report(provider, `${provider}:subscription-observation`, entry.quota);
|
|
422
|
+
// Tagged here rather than inside report(), which every probed path shares.
|
|
423
|
+
return built ? { ...built, observed: true } : null;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// ---------------------------------------------------------------------------
|
|
427
|
+
// Per-account quota (multiauth)
|
|
428
|
+
// ---------------------------------------------------------------------------
|
|
429
|
+
|
|
430
|
+
|
|
431
|
+
/** Cursor included usage via api2.cursor.sh (Bearer from OAuth) — unofficial, may change. */
|
|
432
|
+
export async function fetchCursorQuota(provider: string, accessToken: string): Promise<ProviderQuotaReport | null> {
|
|
433
|
+
|
|
434
|
+
const authHeaders = {
|
|
435
|
+
Accept: "application/json",
|
|
436
|
+
Authorization: `Bearer ${accessToken}`,
|
|
437
|
+
"User-Agent": "opencodex-quota",
|
|
438
|
+
} as const;
|
|
439
|
+
|
|
440
|
+
// Prefer dashboard period usage (Pro/Team/Ultra spend allowance in USD cents).
|
|
441
|
+
// Field names follow Cursor's Connect RPC shape (limit/remaining/includedSpend), not usedCents.
|
|
442
|
+
try {
|
|
443
|
+
const periodRes = await fetch("https://api2.cursor.sh/aiserver.v1.DashboardService/GetCurrentPeriodUsage", {
|
|
444
|
+
method: "POST",
|
|
445
|
+
redirect: "error",
|
|
446
|
+
headers: {
|
|
447
|
+
...authHeaders,
|
|
448
|
+
"Content-Type": "application/json",
|
|
449
|
+
"Connect-Protocol-Version": "1",
|
|
450
|
+
},
|
|
451
|
+
body: "{}",
|
|
452
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
453
|
+
});
|
|
454
|
+
if (periodRes.ok) {
|
|
455
|
+
const body = asRecord(await readQuotaJson(periodRes));
|
|
456
|
+
const planUsage = asRecord(body?.planUsage);
|
|
457
|
+
if (planUsage) {
|
|
458
|
+
const resetAt = normalizeResetAt(body?.billingCycleEnd ?? planUsage.billingCycleEnd ?? body?.periodEnd);
|
|
459
|
+
|
|
460
|
+
// Primary meter: overall included allowance (Cursor Settings → Usage total %).
|
|
461
|
+
// autoPercentUsed / apiPercentUsed are secondary pools and must not replace the total.
|
|
462
|
+
const limit = toFiniteNumber(planUsage.limit ?? planUsage.limitCents ?? planUsage.totalLimitCents);
|
|
463
|
+
const remaining = toFiniteNumber(planUsage.remaining ?? planUsage.remainingCents);
|
|
464
|
+
const includedSpend = toFiniteNumber(planUsage.includedSpend ?? planUsage.usedCents ?? planUsage.used);
|
|
465
|
+
const totalSpend = toFiniteNumber(planUsage.totalSpend);
|
|
466
|
+
let used: number | undefined;
|
|
467
|
+
if (includedSpend !== undefined) used = includedSpend;
|
|
468
|
+
else if (limit !== undefined && remaining !== undefined) used = Math.max(0, limit - remaining);
|
|
469
|
+
else if (totalSpend !== undefined) used = totalSpend;
|
|
470
|
+
const totalPercent = normalizePercent(planUsage.totalPercentUsed ?? planUsage.percentUsed)
|
|
471
|
+
?? (limit !== undefined && limit > 0 && used !== undefined
|
|
472
|
+
? normalizePercent((used / limit) * 100)
|
|
473
|
+
: undefined);
|
|
474
|
+
|
|
475
|
+
const autoPercent = normalizePercent(planUsage.autoPercentUsed);
|
|
476
|
+
const apiPercent = normalizePercent(planUsage.apiPercentUsed);
|
|
477
|
+
const customWindows: ProviderQuotaWindow[] = [];
|
|
478
|
+
if (autoPercent !== undefined) {
|
|
479
|
+
customWindows.push({
|
|
480
|
+
label: "First-party models",
|
|
481
|
+
percent: autoPercent,
|
|
482
|
+
...(resetAt !== undefined ? { resetAt } : {}),
|
|
483
|
+
});
|
|
484
|
+
}
|
|
485
|
+
if (apiPercent !== undefined) {
|
|
486
|
+
customWindows.push({
|
|
487
|
+
label: "API usage",
|
|
488
|
+
percent: apiPercent,
|
|
489
|
+
...(resetAt !== undefined ? { resetAt } : {}),
|
|
490
|
+
});
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
if (totalPercent !== undefined || customWindows.length > 0) {
|
|
494
|
+
const built = report(provider, "cursor:period-usage", {
|
|
495
|
+
...(totalPercent !== undefined ? {
|
|
496
|
+
monthlyPercent: totalPercent,
|
|
497
|
+
...(resetAt !== undefined ? { monthlyResetAt: resetAt } : {}),
|
|
498
|
+
} : {}),
|
|
499
|
+
...(customWindows.length > 0 ? { customWindows } : {}),
|
|
500
|
+
updatedAt: Date.now(),
|
|
501
|
+
});
|
|
502
|
+
if (built) return { ...built, reverseEngineered: true };
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
} catch {
|
|
507
|
+
/* fall through */
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
// /api/usage/summary — same host, sometimes richer than /auth/usage for Team plans.
|
|
511
|
+
try {
|
|
512
|
+
const summaryRes = await fetch("https://api2.cursor.sh/api/usage/summary", {
|
|
513
|
+
headers: authHeaders,
|
|
514
|
+
redirect: "error",
|
|
515
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
516
|
+
});
|
|
517
|
+
if (summaryRes.ok) {
|
|
518
|
+
const body = asRecord(await readQuotaJson(summaryRes));
|
|
519
|
+
const individual = asRecord(body?.individualUsage);
|
|
520
|
+
const plan = asRecord(individual?.plan);
|
|
521
|
+
if (plan) {
|
|
522
|
+
const used = toFiniteNumber(plan.used);
|
|
523
|
+
const limit = toFiniteNumber(plan.limit);
|
|
524
|
+
const percent = normalizePercent(plan.totalPercentUsed)
|
|
525
|
+
?? (used !== undefined && limit !== undefined && limit > 0
|
|
526
|
+
? normalizePercent((used / limit) * 100)
|
|
527
|
+
: undefined);
|
|
528
|
+
if (percent !== undefined) {
|
|
529
|
+
const built = report(provider, "cursor:usage-summary", {
|
|
530
|
+
monthlyPercent: percent,
|
|
531
|
+
monthlyResetAt: normalizeResetAt(body?.billingCycleEnd),
|
|
532
|
+
updatedAt: Date.now(),
|
|
533
|
+
});
|
|
534
|
+
if (built) return { ...built, reverseEngineered: true };
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
} catch {
|
|
539
|
+
/* fall through to /auth/usage */
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
const response = await fetch("https://api2.cursor.sh/auth/usage", {
|
|
543
|
+
headers: authHeaders,
|
|
544
|
+
redirect: "error",
|
|
545
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
546
|
+
});
|
|
547
|
+
if (!response.ok) return null;
|
|
548
|
+
const body = asRecord(await readQuotaJson(response));
|
|
549
|
+
if (!body) return null;
|
|
550
|
+
|
|
551
|
+
// Prefer the gpt-4 bucket (historical "fast requests"); else first model with used+limit.
|
|
552
|
+
let used: number | undefined;
|
|
553
|
+
let limit: number | undefined;
|
|
554
|
+
const gpt4 = asRecord(body["gpt-4"]);
|
|
555
|
+
if (gpt4) {
|
|
556
|
+
used = toFiniteNumber(gpt4.numRequests ?? gpt4.used);
|
|
557
|
+
limit = toFiniteNumber(gpt4.maxRequestUsage ?? gpt4.limit ?? gpt4.maxRequests);
|
|
558
|
+
}
|
|
559
|
+
if (used === undefined || limit === undefined || limit <= 0) {
|
|
560
|
+
for (const [key, value] of Object.entries(body)) {
|
|
561
|
+
if (key === "startOfMonth" || key === "billingCycleStart") continue;
|
|
562
|
+
const bucket = asRecord(value);
|
|
563
|
+
if (!bucket) continue;
|
|
564
|
+
const bucketUsed = toFiniteNumber(bucket.numRequests ?? bucket.used);
|
|
565
|
+
const bucketLimit = toFiniteNumber(bucket.maxRequestUsage ?? bucket.limit ?? bucket.maxRequests);
|
|
566
|
+
if (bucketUsed !== undefined && bucketLimit !== undefined && bucketLimit > 0) {
|
|
567
|
+
used = bucketUsed;
|
|
568
|
+
limit = bucketLimit;
|
|
569
|
+
break;
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
if (used === undefined || limit === undefined || limit <= 0) return null;
|
|
574
|
+
const percent = normalizePercent((used / limit) * 100);
|
|
575
|
+
if (percent === undefined) return null;
|
|
576
|
+
const startOfMonth = normalizeResetAt(body.startOfMonth ?? body.billingCycleStart);
|
|
577
|
+
// Next reset = same day next month, computed in UTC to avoid timezone-shifted rollover.
|
|
578
|
+
const monthlyResetAt = startOfMonth !== undefined
|
|
579
|
+
? (() => {
|
|
580
|
+
const start = new Date(startOfMonth);
|
|
581
|
+
return Date.UTC(start.getUTCFullYear(), start.getUTCMonth() + 1, start.getUTCDate());
|
|
582
|
+
})()
|
|
583
|
+
: undefined;
|
|
584
|
+
const built = report(provider, "cursor:auth-usage", {
|
|
585
|
+
monthlyPercent: percent,
|
|
586
|
+
...(monthlyResetAt !== undefined ? { monthlyResetAt } : {}),
|
|
587
|
+
updatedAt: Date.now(),
|
|
588
|
+
});
|
|
589
|
+
return built ? { ...built, reverseEngineered: true } : null;
|
|
590
|
+
}
|