@bitkyc08/opencodex 2.40.0 → 2.42.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/README.md +4 -0
- package/gui/dist/assets/index-BU1tE0sr.js +112 -0
- package/gui/dist/assets/index-DL9-iS6J.css +1 -0
- package/gui/dist/index.html +2 -2
- package/gui/dist/provider-icons/meta.svg +1 -0
- package/package.json +4 -3
- package/src/adapters/cursor/catalog.ts +71 -29
- package/src/adapters/cursor/claude-id.ts +76 -0
- package/src/adapters/cursor/discovery.ts +16 -3
- package/src/adapters/cursor/effort-map.ts +27 -12
- package/src/adapters/cursor/protobuf-request.ts +41 -21
- package/src/adapters/google.ts +39 -2
- package/src/adapters/identity.ts +8 -2
- package/src/adapters/openai-responses.ts +57 -4
- package/src/bridge.ts +25 -3
- package/src/cli/account-auth.ts +28 -3
- package/src/cli/account-extended.ts +7 -1
- package/src/cli/capabilities.ts +2 -2
- package/src/cli/claude.ts +11 -2
- package/src/cli/connect.ts +7 -1
- package/src/cli/observe.ts +3 -1
- package/src/cli/registry.ts +1 -1
- package/src/cli/status.ts +19 -4
- package/src/client/connect.ts +5 -1
- package/src/client/hub-client.ts +29 -5
- package/src/clients/config-export.ts +12 -2
- package/src/codex/auth-api.ts +102 -9
- package/src/codex/catalog/aggregation.ts +8 -0
- package/src/codex/catalog/effort.ts +15 -2
- package/src/codex/catalog/metadata.ts +119 -9
- package/src/codex/catalog/native-models.ts +71 -0
- package/src/codex/catalog/parsing.ts +5 -3
- package/src/codex/catalog/provider-fetch.ts +166 -28
- package/src/codex/catalog.ts +1 -1
- package/src/codex/convergence-types.ts +1 -0
- package/src/codex/data/upstream-models.json +169 -0
- package/src/codex/desired-state.ts +18 -11
- package/src/codex/inject.ts +96 -6
- package/src/codex/injected-marker.ts +30 -4
- package/src/codex/journal.ts +14 -0
- package/src/combos/failover.ts +185 -6
- package/src/combos/index.ts +6 -0
- package/src/combos/resolve.ts +43 -6
- package/src/config.ts +5 -1
- package/src/generated/compatibility-version.json +115 -83
- package/src/generated/model-metadata.ts +1 -1
- package/src/grok/sync.ts +10 -2
- package/src/integrations/cursor-effort-table.ts +143 -0
- package/src/integrations/state.ts +1 -1
- package/src/integrations/writer.ts +2 -2
- package/src/lib/app-owned-memory-stores.ts +27 -8
- package/src/lib/bounded-body.ts +16 -1
- package/src/oauth/account-quota-rank.ts +40 -1
- package/src/oauth/chatgpt-device.ts +187 -0
- package/src/oauth/chatgpt.ts +31 -4
- package/src/oauth/generic-account-failover.ts +2 -2
- package/src/oauth/index.ts +24 -3
- package/src/oauth/log.ts +3 -0
- package/src/oauth/meta-muse.ts +235 -0
- package/src/providers/antigravity-models.ts +71 -13
- package/src/providers/command-code-efforts.ts +15 -0
- package/src/providers/free-directory.ts +4 -1
- package/src/providers/muse-subscription-usage.ts +95 -0
- package/src/providers/quota.ts +96 -0
- package/src/providers/registry.ts +116 -8
- package/src/responses/code-mode-helper-compat.ts +4 -1
- package/src/responses/state.ts +5 -4
- package/src/server/auth-cors.ts +241 -56
- package/src/server/chat-completions.ts +11 -2
- package/src/server/chat-native.ts +30 -4
- package/src/server/claude-messages.ts +17 -3
- package/src/server/effort-row.ts +131 -0
- package/src/server/index.ts +82 -45
- package/src/server/live.ts +18 -4
- package/src/server/management/api-key-rotation.ts +2 -1
- package/src/server/management/api-key-usage.ts +97 -43
- package/src/server/management/context.ts +3 -0
- package/src/server/management/cursor-integration-routes.ts +36 -7
- package/src/server/management/logs-usage-routes.ts +64 -87
- package/src/server/management/oauth-account-routes.ts +10 -3
- package/src/server/management/provider-routes.ts +218 -1
- package/src/server/management/route-registry.ts +1 -0
- package/src/server/management/usage-aggregate-cache.ts +464 -0
- package/src/server/management/usage-summary-cache.ts +4 -0
- package/src/server/models-capabilities.ts +60 -5
- package/src/server/responses/core.ts +95 -7
- package/src/server/responses/empty-completion-guard.ts +4 -0
- package/src/types/config.ts +10 -1
- package/src/types/request.ts +8 -0
- package/src/types/tools.ts +12 -9
- package/src/usage/expected-prices.ts +43 -7
- package/src/usage/ledger-scanner.ts +448 -0
- package/src/usage/log.ts +1 -1
- package/src/usage/summary.ts +915 -655
- package/src/web-search/index.ts +1 -1
- package/gui/dist/assets/index-BHe2rl_C.js +0 -112
- package/gui/dist/assets/index-CJSb3HPe.css +0 -1
|
@@ -0,0 +1,464 @@
|
|
|
1
|
+
import { enforceAppOwnedMemoryBudget } from "../../lib/app-owned-memory";
|
|
2
|
+
import {
|
|
3
|
+
currentUsageLogRevision,
|
|
4
|
+
usageLogIdentityKey,
|
|
5
|
+
usageLogRevisionKey,
|
|
6
|
+
type UsageLogRevision,
|
|
7
|
+
} from "../../usage/log";
|
|
8
|
+
import {
|
|
9
|
+
scanUsageLedgerCooperatively,
|
|
10
|
+
UsageLedgerRebuildRequiredError,
|
|
11
|
+
} from "../../usage/ledger-scanner";
|
|
12
|
+
import {
|
|
13
|
+
createUsageSummaryAccumulator,
|
|
14
|
+
type UsageSummaryAccumulator,
|
|
15
|
+
} from "../../usage/summary";
|
|
16
|
+
import { userCostOverlayVersion } from "../../usage/user-cost-overlays";
|
|
17
|
+
|
|
18
|
+
import {
|
|
19
|
+
cacheApiKeyUsageFromRollup,
|
|
20
|
+
createApiKeyUsageAccumulator,
|
|
21
|
+
} from "./api-key-usage";
|
|
22
|
+
|
|
23
|
+
interface RetainedUsageAggregate {
|
|
24
|
+
accumulator: UsageSummaryAccumulator;
|
|
25
|
+
revision: UsageLogRevision | null;
|
|
26
|
+
identityKey: string;
|
|
27
|
+
revisionKey: string;
|
|
28
|
+
processedThroughBytes: number;
|
|
29
|
+
processedThroughDigest: string;
|
|
30
|
+
overlayVersion: number;
|
|
31
|
+
timeZone: string;
|
|
32
|
+
retainedAt: number;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface UsageAggregateResult {
|
|
36
|
+
accumulator: UsageSummaryAccumulator;
|
|
37
|
+
revision: UsageLogRevision | null;
|
|
38
|
+
processedThroughBytes: number;
|
|
39
|
+
overlayVersion: number;
|
|
40
|
+
timeZone: string;
|
|
41
|
+
update: "unchanged" | "append" | "rebuild";
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface UsageAggregateOptions {
|
|
45
|
+
now?: number;
|
|
46
|
+
configuredApiKeyIds?: string[];
|
|
47
|
+
managementUsageMaxReadBytes?: number;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface UsageAggregateRetainedStats {
|
|
51
|
+
count: number;
|
|
52
|
+
bytes: number;
|
|
53
|
+
evictableBytes: number;
|
|
54
|
+
pinnedBytes: number;
|
|
55
|
+
oldestAt: number | null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const MAX_REBUILD_ATTEMPTS = 2;
|
|
59
|
+
const MAX_RETAINED_FILTERED_AGGREGATES = 4;
|
|
60
|
+
|
|
61
|
+
let retainedAggregate: RetainedUsageAggregate | null = null;
|
|
62
|
+
const pinnedAggregates = new Set<RetainedUsageAggregate>();
|
|
63
|
+
let baseFlight: Promise<UsageAggregateResult> | null = null;
|
|
64
|
+
const filteredFlights = new Map<string, Promise<UsageAggregateResult>>();
|
|
65
|
+
const retainedFilteredAggregates = new Map<string, RetainedUsageAggregate>();
|
|
66
|
+
|
|
67
|
+
function currentTimeZone(): string {
|
|
68
|
+
return Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function resultFrom(
|
|
72
|
+
state: RetainedUsageAggregate,
|
|
73
|
+
update: UsageAggregateResult["update"],
|
|
74
|
+
): UsageAggregateResult {
|
|
75
|
+
return {
|
|
76
|
+
accumulator: state.accumulator,
|
|
77
|
+
revision: state.revision,
|
|
78
|
+
processedThroughBytes: state.processedThroughBytes,
|
|
79
|
+
overlayVersion: state.overlayVersion,
|
|
80
|
+
timeZone: state.timeZone,
|
|
81
|
+
update,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function publishRetainedAggregate(state: RetainedUsageAggregate): UsageAggregateResult {
|
|
86
|
+
retainedAggregate = state;
|
|
87
|
+
// The budget may evict the state immediately. The request that built it still
|
|
88
|
+
// owns the returned accumulator and can finish this response safely.
|
|
89
|
+
enforceAppOwnedMemoryBudget();
|
|
90
|
+
return resultFrom(state, "rebuild");
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function makeRetainedAggregate(
|
|
94
|
+
accumulator: UsageSummaryAccumulator,
|
|
95
|
+
scan: Awaited<ReturnType<typeof scanUsageLedgerCooperatively>>,
|
|
96
|
+
overlayVersion: number,
|
|
97
|
+
timeZone: string,
|
|
98
|
+
): RetainedUsageAggregate {
|
|
99
|
+
return {
|
|
100
|
+
accumulator,
|
|
101
|
+
revision: scan.revision,
|
|
102
|
+
identityKey: usageLogIdentityKey(scan.revision),
|
|
103
|
+
revisionKey: usageLogRevisionKey(scan.revision),
|
|
104
|
+
processedThroughBytes: scan.processedThroughBytes,
|
|
105
|
+
processedThroughDigest: scan.processedThroughDigest,
|
|
106
|
+
overlayVersion,
|
|
107
|
+
timeZone,
|
|
108
|
+
retainedAt: Date.now(),
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async function rebuildAggregate(options: UsageAggregateOptions): Promise<UsageAggregateResult> {
|
|
113
|
+
let lastError: unknown;
|
|
114
|
+
for (let attempt = 0; attempt < MAX_REBUILD_ATTEMPTS; attempt += 1) {
|
|
115
|
+
const overlayVersion = userCostOverlayVersion();
|
|
116
|
+
const timeZone = currentTimeZone();
|
|
117
|
+
const accumulator = createUsageSummaryAccumulator({ mode: "row-unique" });
|
|
118
|
+
const apiKeyAccumulator = options.configuredApiKeyIds
|
|
119
|
+
? createApiKeyUsageAccumulator(options.configuredApiKeyIds, options.now)
|
|
120
|
+
: null;
|
|
121
|
+
try {
|
|
122
|
+
const scan = await scanUsageLedgerCooperatively({
|
|
123
|
+
onEntry(entry) {
|
|
124
|
+
accumulator.add(entry);
|
|
125
|
+
apiKeyAccumulator?.add(entry);
|
|
126
|
+
},
|
|
127
|
+
});
|
|
128
|
+
if (scan.oversizedRows > 0) {
|
|
129
|
+
throw new Error("usage ledger contains an oversized row");
|
|
130
|
+
}
|
|
131
|
+
if (userCostOverlayVersion() !== overlayVersion || currentTimeZone() !== timeZone) {
|
|
132
|
+
lastError = new Error("usage aggregation inputs changed during rebuild");
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const state = makeRetainedAggregate(accumulator, scan, overlayVersion, timeZone);
|
|
137
|
+
const result = publishRetainedAggregate(state);
|
|
138
|
+
if (apiKeyAccumulator && options.configuredApiKeyIds) {
|
|
139
|
+
cacheApiKeyUsageFromRollup(
|
|
140
|
+
apiKeyAccumulator.snapshot(),
|
|
141
|
+
options.configuredApiKeyIds,
|
|
142
|
+
state.identityKey,
|
|
143
|
+
state.revision?.size ?? 0,
|
|
144
|
+
options.managementUsageMaxReadBytes,
|
|
145
|
+
options.now,
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
return result;
|
|
149
|
+
} catch (error) {
|
|
150
|
+
lastError = error;
|
|
151
|
+
if (!(error instanceof UsageLedgerRebuildRequiredError) || attempt + 1 >= MAX_REBUILD_ATTEMPTS) {
|
|
152
|
+
throw error;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
throw lastError ?? new Error("usage aggregate rebuild did not settle");
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function requiresRebuild(
|
|
160
|
+
state: RetainedUsageAggregate,
|
|
161
|
+
observed: UsageLogRevision | null,
|
|
162
|
+
overlayVersion: number,
|
|
163
|
+
timeZone: string,
|
|
164
|
+
): boolean {
|
|
165
|
+
if (state.overlayVersion !== overlayVersion || state.timeZone !== timeZone) return true;
|
|
166
|
+
if (state.identityKey !== usageLogIdentityKey(observed)) return true;
|
|
167
|
+
if (!state.revision || !observed) return state.revision !== observed;
|
|
168
|
+
if (observed.size < state.revision.size) return true;
|
|
169
|
+
// At the same size, metadata movement cannot be an append. Rebuild so a
|
|
170
|
+
// detectable same-inode replacement/edit never extends stale counters.
|
|
171
|
+
return observed.size === state.revision.size && usageLogRevisionKey(observed) !== state.revisionKey;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
async function appendAggregate(
|
|
175
|
+
state: RetainedUsageAggregate,
|
|
176
|
+
options: UsageAggregateOptions,
|
|
177
|
+
): Promise<UsageAggregateResult> {
|
|
178
|
+
pinnedAggregates.add(state);
|
|
179
|
+
let rebuildAfterUnpin = false;
|
|
180
|
+
try {
|
|
181
|
+
// Clone first and publish only after the scanner verifies the captured
|
|
182
|
+
// suffix. A callback error, mutation, or oversized row leaves retained
|
|
183
|
+
// state byte-for-byte untouched.
|
|
184
|
+
const candidate = state.accumulator.clone();
|
|
185
|
+
const scan = await scanUsageLedgerCooperatively({
|
|
186
|
+
startAtBytes: state.processedThroughBytes,
|
|
187
|
+
expectedIdentityKey: state.identityKey,
|
|
188
|
+
expectedProcessedThroughDigest: state.processedThroughDigest,
|
|
189
|
+
onEntry: entry => candidate.add(entry),
|
|
190
|
+
});
|
|
191
|
+
if (scan.oversizedRows > 0) {
|
|
192
|
+
if (retainedAggregate === state) retainedAggregate = null;
|
|
193
|
+
throw new Error("usage ledger contains an oversized row");
|
|
194
|
+
}
|
|
195
|
+
if (userCostOverlayVersion() !== state.overlayVersion || currentTimeZone() !== state.timeZone) {
|
|
196
|
+
if (retainedAggregate === state) retainedAggregate = null;
|
|
197
|
+
rebuildAfterUnpin = true;
|
|
198
|
+
} else {
|
|
199
|
+
const next: RetainedUsageAggregate = {
|
|
200
|
+
...state,
|
|
201
|
+
accumulator: candidate,
|
|
202
|
+
revision: scan.revision,
|
|
203
|
+
identityKey: usageLogIdentityKey(scan.revision),
|
|
204
|
+
revisionKey: usageLogRevisionKey(scan.revision),
|
|
205
|
+
processedThroughBytes: scan.processedThroughBytes,
|
|
206
|
+
processedThroughDigest: scan.processedThroughDigest,
|
|
207
|
+
retainedAt: Date.now(),
|
|
208
|
+
};
|
|
209
|
+
retainedAggregate = next;
|
|
210
|
+
enforceAppOwnedMemoryBudget();
|
|
211
|
+
return resultFrom(next, "append");
|
|
212
|
+
}
|
|
213
|
+
} catch (error) {
|
|
214
|
+
if (retainedAggregate === state) retainedAggregate = null;
|
|
215
|
+
if (error instanceof UsageLedgerRebuildRequiredError) rebuildAfterUnpin = true;
|
|
216
|
+
else throw error;
|
|
217
|
+
} finally {
|
|
218
|
+
pinnedAggregates.delete(state);
|
|
219
|
+
}
|
|
220
|
+
if (rebuildAfterUnpin) return rebuildAggregate(options);
|
|
221
|
+
throw new Error("usage aggregate append did not settle");
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
async function refreshAggregate(options: UsageAggregateOptions): Promise<UsageAggregateResult> {
|
|
225
|
+
const state = retainedAggregate;
|
|
226
|
+
if (!state) return rebuildAggregate(options);
|
|
227
|
+
|
|
228
|
+
const observed = currentUsageLogRevision();
|
|
229
|
+
const overlayVersion = userCostOverlayVersion();
|
|
230
|
+
const timeZone = currentTimeZone();
|
|
231
|
+
if (requiresRebuild(state, observed, overlayVersion, timeZone)) {
|
|
232
|
+
retainedAggregate = null;
|
|
233
|
+
return rebuildAggregate(options);
|
|
234
|
+
}
|
|
235
|
+
if (usageLogRevisionKey(observed) === state.revisionKey) return resultFrom(state, "unchanged");
|
|
236
|
+
return appendAggregate(state, options);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
export async function getUsageAggregate(
|
|
240
|
+
options: UsageAggregateOptions = {},
|
|
241
|
+
): Promise<UsageAggregateResult> {
|
|
242
|
+
if (baseFlight) return baseFlight;
|
|
243
|
+
const flight = refreshAggregate(options);
|
|
244
|
+
baseFlight = flight;
|
|
245
|
+
try {
|
|
246
|
+
return await flight;
|
|
247
|
+
} finally {
|
|
248
|
+
if (baseFlight === flight) baseFlight = null;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function normalizeFilterValue(value: string | null | undefined): string | null {
|
|
253
|
+
const normalized = typeof value === "string" ? value.trim().toLowerCase() : "";
|
|
254
|
+
return normalized || null;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function normalizeExactFilterValue(value: string | null | undefined): string | null {
|
|
258
|
+
const normalized = typeof value === "string" ? value.trim() : "";
|
|
259
|
+
return normalized || null;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
export async function getFilteredUsageAggregate(filter: {
|
|
263
|
+
provider?: string | null;
|
|
264
|
+
model?: string | null;
|
|
265
|
+
apiKeyId?: string | null;
|
|
266
|
+
}): Promise<UsageAggregateResult> {
|
|
267
|
+
const normalizedFilter = {
|
|
268
|
+
provider: normalizeFilterValue(filter.provider),
|
|
269
|
+
model: normalizeFilterValue(filter.model),
|
|
270
|
+
apiKeyId: normalizeExactFilterValue(filter.apiKeyId),
|
|
271
|
+
};
|
|
272
|
+
const key = JSON.stringify([
|
|
273
|
+
normalizedFilter.provider,
|
|
274
|
+
normalizedFilter.model,
|
|
275
|
+
normalizedFilter.apiKeyId,
|
|
276
|
+
]);
|
|
277
|
+
const existing = filteredFlights.get(key);
|
|
278
|
+
if (existing) return existing;
|
|
279
|
+
|
|
280
|
+
const flight = refreshFilteredAggregate(key, normalizedFilter);
|
|
281
|
+
filteredFlights.set(key, flight);
|
|
282
|
+
try {
|
|
283
|
+
return await flight;
|
|
284
|
+
} finally {
|
|
285
|
+
if (filteredFlights.get(key) === flight) filteredFlights.delete(key);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
type NormalizedUsageFilter = {
|
|
290
|
+
provider: string | null;
|
|
291
|
+
model: string | null;
|
|
292
|
+
apiKeyId: string | null;
|
|
293
|
+
};
|
|
294
|
+
|
|
295
|
+
function trimRetainedFilteredAggregates(): void {
|
|
296
|
+
while (retainedFilteredAggregates.size > MAX_RETAINED_FILTERED_AGGREGATES) {
|
|
297
|
+
const oldest = [...retainedFilteredAggregates]
|
|
298
|
+
.filter(([, state]) => !pinnedAggregates.has(state))
|
|
299
|
+
.sort(([, left], [, right]) => left.retainedAt - right.retainedAt)[0];
|
|
300
|
+
if (!oldest) return;
|
|
301
|
+
retainedFilteredAggregates.delete(oldest[0]);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function publishFilteredAggregate(
|
|
306
|
+
key: string,
|
|
307
|
+
state: RetainedUsageAggregate,
|
|
308
|
+
update: UsageAggregateResult["update"],
|
|
309
|
+
): UsageAggregateResult {
|
|
310
|
+
retainedFilteredAggregates.set(key, state);
|
|
311
|
+
trimRetainedFilteredAggregates();
|
|
312
|
+
enforceAppOwnedMemoryBudget();
|
|
313
|
+
return resultFrom(state, update);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
async function rebuildFilteredAggregate(
|
|
317
|
+
key: string,
|
|
318
|
+
filter: NormalizedUsageFilter,
|
|
319
|
+
): Promise<UsageAggregateResult> {
|
|
320
|
+
let lastError: unknown;
|
|
321
|
+
for (let attempt = 0; attempt < MAX_REBUILD_ATTEMPTS; attempt += 1) {
|
|
322
|
+
const overlayVersion = userCostOverlayVersion();
|
|
323
|
+
const timeZone = currentTimeZone();
|
|
324
|
+
const accumulator = createUsageSummaryAccumulator({ filter, mode: "row-unique" });
|
|
325
|
+
try {
|
|
326
|
+
const scan = await scanUsageLedgerCooperatively({ onEntry: entry => accumulator.add(entry) });
|
|
327
|
+
if (scan.oversizedRows > 0) throw new Error("usage ledger contains an oversized row");
|
|
328
|
+
if (userCostOverlayVersion() !== overlayVersion || currentTimeZone() !== timeZone) {
|
|
329
|
+
lastError = new Error("usage aggregation inputs changed during filtered scan");
|
|
330
|
+
continue;
|
|
331
|
+
}
|
|
332
|
+
const state = makeRetainedAggregate(accumulator, scan, overlayVersion, timeZone);
|
|
333
|
+
return publishFilteredAggregate(key, state, "rebuild");
|
|
334
|
+
} catch (error) {
|
|
335
|
+
lastError = error;
|
|
336
|
+
if (!(error instanceof UsageLedgerRebuildRequiredError) || attempt + 1 >= MAX_REBUILD_ATTEMPTS) {
|
|
337
|
+
throw error;
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
throw lastError ?? new Error("filtered usage scan did not settle");
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
async function appendFilteredAggregate(
|
|
345
|
+
key: string,
|
|
346
|
+
state: RetainedUsageAggregate,
|
|
347
|
+
filter: NormalizedUsageFilter,
|
|
348
|
+
): Promise<UsageAggregateResult> {
|
|
349
|
+
pinnedAggregates.add(state);
|
|
350
|
+
let rebuildAfterUnpin = false;
|
|
351
|
+
try {
|
|
352
|
+
const candidate = state.accumulator.clone();
|
|
353
|
+
const scan = await scanUsageLedgerCooperatively({
|
|
354
|
+
startAtBytes: state.processedThroughBytes,
|
|
355
|
+
expectedIdentityKey: state.identityKey,
|
|
356
|
+
expectedProcessedThroughDigest: state.processedThroughDigest,
|
|
357
|
+
onEntry: entry => candidate.add(entry),
|
|
358
|
+
});
|
|
359
|
+
if (scan.oversizedRows > 0) {
|
|
360
|
+
if (retainedFilteredAggregates.get(key) === state) retainedFilteredAggregates.delete(key);
|
|
361
|
+
throw new Error("usage ledger contains an oversized row");
|
|
362
|
+
}
|
|
363
|
+
if (userCostOverlayVersion() !== state.overlayVersion || currentTimeZone() !== state.timeZone) {
|
|
364
|
+
if (retainedFilteredAggregates.get(key) === state) retainedFilteredAggregates.delete(key);
|
|
365
|
+
rebuildAfterUnpin = true;
|
|
366
|
+
} else {
|
|
367
|
+
const next: RetainedUsageAggregate = {
|
|
368
|
+
...state,
|
|
369
|
+
accumulator: candidate,
|
|
370
|
+
revision: scan.revision,
|
|
371
|
+
identityKey: usageLogIdentityKey(scan.revision),
|
|
372
|
+
revisionKey: usageLogRevisionKey(scan.revision),
|
|
373
|
+
processedThroughBytes: scan.processedThroughBytes,
|
|
374
|
+
processedThroughDigest: scan.processedThroughDigest,
|
|
375
|
+
retainedAt: Date.now(),
|
|
376
|
+
};
|
|
377
|
+
return publishFilteredAggregate(key, next, "append");
|
|
378
|
+
}
|
|
379
|
+
} catch (error) {
|
|
380
|
+
if (retainedFilteredAggregates.get(key) === state) retainedFilteredAggregates.delete(key);
|
|
381
|
+
if (error instanceof UsageLedgerRebuildRequiredError) rebuildAfterUnpin = true;
|
|
382
|
+
else throw error;
|
|
383
|
+
} finally {
|
|
384
|
+
pinnedAggregates.delete(state);
|
|
385
|
+
trimRetainedFilteredAggregates();
|
|
386
|
+
}
|
|
387
|
+
if (rebuildAfterUnpin) return rebuildFilteredAggregate(key, filter);
|
|
388
|
+
throw new Error("filtered usage append did not settle");
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
async function refreshFilteredAggregate(
|
|
392
|
+
key: string,
|
|
393
|
+
filter: NormalizedUsageFilter,
|
|
394
|
+
): Promise<UsageAggregateResult> {
|
|
395
|
+
const state = retainedFilteredAggregates.get(key);
|
|
396
|
+
if (!state) return rebuildFilteredAggregate(key, filter);
|
|
397
|
+
const observed = currentUsageLogRevision();
|
|
398
|
+
const overlayVersion = userCostOverlayVersion();
|
|
399
|
+
const timeZone = currentTimeZone();
|
|
400
|
+
if (requiresRebuild(state, observed, overlayVersion, timeZone)) {
|
|
401
|
+
retainedFilteredAggregates.delete(key);
|
|
402
|
+
return rebuildFilteredAggregate(key, filter);
|
|
403
|
+
}
|
|
404
|
+
if (state.revisionKey === usageLogRevisionKey(observed)) {
|
|
405
|
+
state.retainedAt = Date.now();
|
|
406
|
+
return resultFrom(state, "unchanged");
|
|
407
|
+
}
|
|
408
|
+
return appendFilteredAggregate(key, state, filter);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
export function usageAggregateRetainedStats(): UsageAggregateRetainedStats {
|
|
412
|
+
const states = [
|
|
413
|
+
...(retainedAggregate ? [retainedAggregate] : []),
|
|
414
|
+
...retainedFilteredAggregates.values(),
|
|
415
|
+
];
|
|
416
|
+
if (states.length === 0) {
|
|
417
|
+
return { count: 0, bytes: 0, evictableBytes: 0, pinnedBytes: 0, oldestAt: null };
|
|
418
|
+
}
|
|
419
|
+
let bytes = 0;
|
|
420
|
+
let evictableBytes = 0;
|
|
421
|
+
let pinnedBytes = 0;
|
|
422
|
+
let oldestAt: number | null = null;
|
|
423
|
+
for (const state of states) {
|
|
424
|
+
const stateBytes = state.accumulator.estimatedBytes;
|
|
425
|
+
bytes += stateBytes;
|
|
426
|
+
if (pinnedAggregates.has(state)) pinnedBytes += stateBytes;
|
|
427
|
+
else {
|
|
428
|
+
evictableBytes += stateBytes;
|
|
429
|
+
oldestAt = oldestAt === null ? state.retainedAt : Math.min(oldestAt, state.retainedAt);
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
return {
|
|
433
|
+
count: states.length,
|
|
434
|
+
bytes,
|
|
435
|
+
evictableBytes,
|
|
436
|
+
pinnedBytes,
|
|
437
|
+
oldestAt,
|
|
438
|
+
};
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
export function discardRetainedUsageAggregate(): number {
|
|
442
|
+
const candidates: Array<{ key: string | null; state: RetainedUsageAggregate }> = [
|
|
443
|
+
...(retainedAggregate && !pinnedAggregates.has(retainedAggregate)
|
|
444
|
+
? [{ key: null, state: retainedAggregate }]
|
|
445
|
+
: []),
|
|
446
|
+
...[...retainedFilteredAggregates]
|
|
447
|
+
.filter(([, state]) => !pinnedAggregates.has(state))
|
|
448
|
+
.map(([key, state]) => ({ key, state })),
|
|
449
|
+
];
|
|
450
|
+
const oldest = candidates.sort((left, right) => left.state.retainedAt - right.state.retainedAt)[0];
|
|
451
|
+
if (!oldest) return 0;
|
|
452
|
+
const released = oldest.state.accumulator.estimatedBytes;
|
|
453
|
+
if (oldest.key === null) retainedAggregate = null;
|
|
454
|
+
else retainedFilteredAggregates.delete(oldest.key);
|
|
455
|
+
return released;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
export function resetUsageAggregateCacheForTests(): void {
|
|
459
|
+
retainedAggregate = null;
|
|
460
|
+
pinnedAggregates.clear();
|
|
461
|
+
baseFlight = null;
|
|
462
|
+
filteredFlights.clear();
|
|
463
|
+
retainedFilteredAggregates.clear();
|
|
464
|
+
}
|
|
@@ -6,6 +6,8 @@ export type CachedUsageSummary = UsageSummary & {
|
|
|
6
6
|
truncatedPrefixBytes: number;
|
|
7
7
|
entriesTruncated: boolean;
|
|
8
8
|
entriesDropped: number;
|
|
9
|
+
snapshotWindowStart: number | null;
|
|
10
|
+
snapshotWindowEnd: number | null;
|
|
9
11
|
};
|
|
10
12
|
|
|
11
13
|
export interface UsageSummaryCacheEntry {
|
|
@@ -15,6 +17,8 @@ export interface UsageSummaryCacheEntry {
|
|
|
15
17
|
maxReadBytes: number;
|
|
16
18
|
/** userCostOverlayVersion() when the summary was computed; overlay edits invalidate the entry. */
|
|
17
19
|
overlayVersion: number;
|
|
20
|
+
/** Local calendar zone used to build day/range buckets. */
|
|
21
|
+
timeZone: string;
|
|
18
22
|
expiresAt: number;
|
|
19
23
|
/** Generation freshness: ignore size/mtime until this instant. */
|
|
20
24
|
freshUntil: number;
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { CursorEffortTable } from "../integrations/cursor-effort-table";
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* Extended capability advertisement for the OpenAI-shape `GET /v1/models` list.
|
|
3
5
|
*
|
|
@@ -23,7 +25,8 @@ export const OPENAI_FAMILY_API_TYPES: ReadonlySet<string> = new Set(["chat_compl
|
|
|
23
25
|
* The reasoning-effort ladder Cursor's local-agent runtime attaches to a model, keyed by the
|
|
24
26
|
* model id after its last `/`. Cursor decides this from its own table rather than from the
|
|
25
27
|
* gateway's `reasoning_effort` list, so the dashboard can only PREDICT it; the values here
|
|
26
|
-
*
|
|
28
|
+
* form the fallback mirror of the 3.18.25 table; the live table is read by
|
|
29
|
+
* `src/integrations/cursor-effort-table.ts`. These values carry no Cursor behavior of their own.
|
|
27
30
|
* Null means Cursor shows no Reasoning control for the id. Distinct from
|
|
28
31
|
* `src/adapters/cursor/effort-map.ts`, which maps opencodex efforts onto Cursor's *backend*
|
|
29
32
|
* tiers for the outbound provider; this is what Cursor's *local* picker renders.
|
|
@@ -43,15 +46,62 @@ const CURSOR_EFFORT_FAMILIES: ReadonlyArray<{ test: RegExp; ladder: readonly str
|
|
|
43
46
|
];
|
|
44
47
|
|
|
45
48
|
export function cursorEffortFamily(modelId: string): string[] | null {
|
|
49
|
+
const id = normalizeCursorPickerId(modelId);
|
|
50
|
+
for (const family of CURSOR_EFFORT_FAMILIES) {
|
|
51
|
+
if (family.test.test(id)) return family.ladder.length > 0 ? [...family.ladder] : null;
|
|
52
|
+
}
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface CursorEffortPrediction {
|
|
57
|
+
ladder: string[] | null;
|
|
58
|
+
source: "bundle" | "static";
|
|
59
|
+
/** Bundle family id when one matched (e.g. "anthropic-opus-5"); null otherwise. */
|
|
60
|
+
family: string | null;
|
|
61
|
+
outputCap?: number;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function normalizeCursorPickerId(modelId: string): string {
|
|
46
65
|
let id = modelId.trim().toLowerCase();
|
|
47
66
|
const slash = id.lastIndexOf("/");
|
|
48
67
|
if (slash !== -1) id = id.slice(slash + 1);
|
|
49
68
|
const at = id.indexOf("@");
|
|
50
69
|
if (at !== -1) id = id.slice(0, at);
|
|
51
|
-
|
|
52
|
-
|
|
70
|
+
return id;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* `supportsReasoning` is what the gateway row will advertise in
|
|
75
|
+
* `capabilities.supports_reasoning`; Cursor's gemini family withholds its control when that is
|
|
76
|
+
* false (`effortRequiresReasoningCapability`). Callers that do not know the row pass nothing
|
|
77
|
+
* and get the id-only prediction.
|
|
78
|
+
*/
|
|
79
|
+
export function predictCursorEffort(
|
|
80
|
+
modelId: string,
|
|
81
|
+
table: CursorEffortTable | null,
|
|
82
|
+
supportsReasoning?: boolean,
|
|
83
|
+
): CursorEffortPrediction {
|
|
84
|
+
const id = normalizeCursorPickerId(modelId);
|
|
85
|
+
if (table) {
|
|
86
|
+
for (const family of table.families) {
|
|
87
|
+
if (family.pattern.test(id)) {
|
|
88
|
+
if (family.requiresReasoningCapability && supportsReasoning === false) {
|
|
89
|
+
return { ladder: null, source: "bundle", family: family.id };
|
|
90
|
+
}
|
|
91
|
+
return {
|
|
92
|
+
ladder: family.ladder.length > 0 ? [...family.ladder] : null,
|
|
93
|
+
source: "bundle",
|
|
94
|
+
family: family.id,
|
|
95
|
+
...(family.outputCap !== undefined ? { outputCap: family.outputCap } : {}),
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
if (table.bareGpt5?.pattern.test(id)) return { ladder: [...table.bareGpt5.ladder], source: "bundle", family: "gpt-5" };
|
|
100
|
+
return { ladder: null, source: "bundle", family: null };
|
|
53
101
|
}
|
|
54
|
-
|
|
102
|
+
const staticLadder = cursorEffortFamily(modelId);
|
|
103
|
+
const gated = supportsReasoning === false && id.startsWith("gemini-") ? null : staticLadder;
|
|
104
|
+
return { ladder: gated, source: "static", family: null };
|
|
55
105
|
}
|
|
56
106
|
|
|
57
107
|
export interface ModelCapabilityInput {
|
|
@@ -64,6 +114,7 @@ export interface ModelCapabilityInput {
|
|
|
64
114
|
* as costing more).
|
|
65
115
|
*/
|
|
66
116
|
longContextWindow?: number;
|
|
117
|
+
maxOutputTokens?: number;
|
|
67
118
|
inputModalities?: readonly string[];
|
|
68
119
|
}
|
|
69
120
|
|
|
@@ -71,6 +122,7 @@ export interface ModelCapabilityFields {
|
|
|
71
122
|
api_types: readonly string[];
|
|
72
123
|
capabilities: {
|
|
73
124
|
context_length?: number;
|
|
125
|
+
max_output_tokens?: number;
|
|
74
126
|
/** Cursor's extended-row filter REQUIRES this to contain "text"; every route emits text. */
|
|
75
127
|
output_modalities: string[];
|
|
76
128
|
input_modalities?: string[];
|
|
@@ -91,13 +143,15 @@ export interface ModelCapabilityFields {
|
|
|
91
143
|
function positiveInt(value: unknown): number | undefined {
|
|
92
144
|
if (typeof value !== "number" || !Number.isFinite(value)) return undefined;
|
|
93
145
|
const floored = Math.floor(value);
|
|
94
|
-
|
|
146
|
+
// Catalog limits are safe integers everywhere else; an unsafe finite value is a bad row.
|
|
147
|
+
return floored > 0 && Number.isSafeInteger(floored) ? floored : undefined;
|
|
95
148
|
}
|
|
96
149
|
|
|
97
150
|
export function modelCapabilityFields(input: ModelCapabilityInput): ModelCapabilityFields {
|
|
98
151
|
const efforts = (input.reasoningEfforts ?? []).filter(effort => typeof effort === "string" && effort.length > 0);
|
|
99
152
|
const contextLength = positiveInt(input.contextWindow);
|
|
100
153
|
const longContextLength = positiveInt(input.longContextWindow);
|
|
154
|
+
const maxOutputTokens = positiveInt(input.maxOutputTokens);
|
|
101
155
|
const hasLongTier = contextLength !== undefined && longContextLength !== undefined && longContextLength > contextLength;
|
|
102
156
|
const modalities = Array.isArray(input.inputModalities)
|
|
103
157
|
? input.inputModalities.filter(modality => typeof modality === "string" && modality.length > 0)
|
|
@@ -109,6 +163,7 @@ export function modelCapabilityFields(input: ModelCapabilityInput): ModelCapabil
|
|
|
109
163
|
...(hasLongTier
|
|
110
164
|
? { context_length: longContextLength }
|
|
111
165
|
: contextLength !== undefined ? { context_length: contextLength } : {}),
|
|
166
|
+
...(maxOutputTokens !== undefined ? { max_output_tokens: maxOutputTokens } : {}),
|
|
112
167
|
// Once a gateway advertises api_types, Cursor keeps only rows whose output_modalities
|
|
113
168
|
// include "text"; omitting the key drops the row from the extended catalog.
|
|
114
169
|
output_modalities: ["text"],
|