@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.
Files changed (97) hide show
  1. package/README.md +4 -0
  2. package/gui/dist/assets/index-BU1tE0sr.js +112 -0
  3. package/gui/dist/assets/index-DL9-iS6J.css +1 -0
  4. package/gui/dist/index.html +2 -2
  5. package/gui/dist/provider-icons/meta.svg +1 -0
  6. package/package.json +4 -3
  7. package/src/adapters/cursor/catalog.ts +71 -29
  8. package/src/adapters/cursor/claude-id.ts +76 -0
  9. package/src/adapters/cursor/discovery.ts +16 -3
  10. package/src/adapters/cursor/effort-map.ts +27 -12
  11. package/src/adapters/cursor/protobuf-request.ts +41 -21
  12. package/src/adapters/google.ts +39 -2
  13. package/src/adapters/identity.ts +8 -2
  14. package/src/adapters/openai-responses.ts +57 -4
  15. package/src/bridge.ts +25 -3
  16. package/src/cli/account-auth.ts +28 -3
  17. package/src/cli/account-extended.ts +7 -1
  18. package/src/cli/capabilities.ts +2 -2
  19. package/src/cli/claude.ts +11 -2
  20. package/src/cli/connect.ts +7 -1
  21. package/src/cli/observe.ts +3 -1
  22. package/src/cli/registry.ts +1 -1
  23. package/src/cli/status.ts +19 -4
  24. package/src/client/connect.ts +5 -1
  25. package/src/client/hub-client.ts +29 -5
  26. package/src/clients/config-export.ts +12 -2
  27. package/src/codex/auth-api.ts +102 -9
  28. package/src/codex/catalog/aggregation.ts +8 -0
  29. package/src/codex/catalog/effort.ts +15 -2
  30. package/src/codex/catalog/metadata.ts +119 -9
  31. package/src/codex/catalog/native-models.ts +71 -0
  32. package/src/codex/catalog/parsing.ts +5 -3
  33. package/src/codex/catalog/provider-fetch.ts +166 -28
  34. package/src/codex/catalog.ts +1 -1
  35. package/src/codex/convergence-types.ts +1 -0
  36. package/src/codex/data/upstream-models.json +169 -0
  37. package/src/codex/desired-state.ts +18 -11
  38. package/src/codex/inject.ts +96 -6
  39. package/src/codex/injected-marker.ts +30 -4
  40. package/src/codex/journal.ts +14 -0
  41. package/src/combos/failover.ts +185 -6
  42. package/src/combos/index.ts +6 -0
  43. package/src/combos/resolve.ts +43 -6
  44. package/src/config.ts +5 -1
  45. package/src/generated/compatibility-version.json +115 -83
  46. package/src/generated/model-metadata.ts +1 -1
  47. package/src/grok/sync.ts +10 -2
  48. package/src/integrations/cursor-effort-table.ts +143 -0
  49. package/src/integrations/state.ts +1 -1
  50. package/src/integrations/writer.ts +2 -2
  51. package/src/lib/app-owned-memory-stores.ts +27 -8
  52. package/src/lib/bounded-body.ts +16 -1
  53. package/src/oauth/account-quota-rank.ts +40 -1
  54. package/src/oauth/chatgpt-device.ts +187 -0
  55. package/src/oauth/chatgpt.ts +31 -4
  56. package/src/oauth/generic-account-failover.ts +2 -2
  57. package/src/oauth/index.ts +24 -3
  58. package/src/oauth/log.ts +3 -0
  59. package/src/oauth/meta-muse.ts +235 -0
  60. package/src/providers/antigravity-models.ts +71 -13
  61. package/src/providers/command-code-efforts.ts +15 -0
  62. package/src/providers/free-directory.ts +4 -1
  63. package/src/providers/muse-subscription-usage.ts +95 -0
  64. package/src/providers/quota.ts +96 -0
  65. package/src/providers/registry.ts +116 -8
  66. package/src/responses/code-mode-helper-compat.ts +4 -1
  67. package/src/responses/state.ts +5 -4
  68. package/src/server/auth-cors.ts +241 -56
  69. package/src/server/chat-completions.ts +11 -2
  70. package/src/server/chat-native.ts +30 -4
  71. package/src/server/claude-messages.ts +17 -3
  72. package/src/server/effort-row.ts +131 -0
  73. package/src/server/index.ts +82 -45
  74. package/src/server/live.ts +18 -4
  75. package/src/server/management/api-key-rotation.ts +2 -1
  76. package/src/server/management/api-key-usage.ts +97 -43
  77. package/src/server/management/context.ts +3 -0
  78. package/src/server/management/cursor-integration-routes.ts +36 -7
  79. package/src/server/management/logs-usage-routes.ts +64 -87
  80. package/src/server/management/oauth-account-routes.ts +10 -3
  81. package/src/server/management/provider-routes.ts +218 -1
  82. package/src/server/management/route-registry.ts +1 -0
  83. package/src/server/management/usage-aggregate-cache.ts +464 -0
  84. package/src/server/management/usage-summary-cache.ts +4 -0
  85. package/src/server/models-capabilities.ts +60 -5
  86. package/src/server/responses/core.ts +95 -7
  87. package/src/server/responses/empty-completion-guard.ts +4 -0
  88. package/src/types/config.ts +10 -1
  89. package/src/types/request.ts +8 -0
  90. package/src/types/tools.ts +12 -9
  91. package/src/usage/expected-prices.ts +43 -7
  92. package/src/usage/ledger-scanner.ts +448 -0
  93. package/src/usage/log.ts +1 -1
  94. package/src/usage/summary.ts +915 -655
  95. package/src/web-search/index.ts +1 -1
  96. package/gui/dist/assets/index-BHe2rl_C.js +0 -112
  97. 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
- * mirror that table (read from the 3.18.25 bundle) and carry no Cursor behavior of their own.
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
- for (const family of CURSOR_EFFORT_FAMILIES) {
52
- if (family.test.test(id)) return family.ladder.length > 0 ? [...family.ladder] : null;
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
- return null;
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
- return floored > 0 ? floored : undefined;
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"],