@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
@@ -227,6 +227,9 @@ import { detectInstall } from "../update/index";
227
227
  import { readyProtocolMetadata } from "../remote/protocol";
228
228
  import { modelCapabilityFields } from "./models-capabilities";
229
229
  import { recordCursorSeen } from "../integrations/cursor-seen";
230
+ import { detectCursorInstalls } from "../integrations/cursor-detect";
231
+ import { loadCursorEffortTable } from "../integrations/cursor-effort-table";
232
+ import { expandCursorEffortRow, knownEffortRowIds } from "./effort-row";
230
233
 
231
234
  export const MAX_WS_FRAME_BYTES = 50 * 1024 * 1024;
232
235
  const WEBSOCKET_IDLE_TIMEOUT_SECONDS = 0;
@@ -809,13 +812,21 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
809
812
  if (path === "/v1/responses/compact") return req.method === "POST";
810
813
  if (path === "/v1/alpha/search") return req.method === "POST";
811
814
  if (path === "/v1/models") return req.method === "GET";
812
- // Standalone realtime voice sessions (codex-rs thread/realtime/start, WebSocket
813
- // transport) a directly-spawned `codex app-server` needs these for desktop
814
- // voice the same way it needs /v1/responses. WebSocket upgrades only; plain
815
- // HTTP on these paths stays rejected.
816
- if (path === "/v1/realtime" || path === "/v1/live") {
817
- return req.headers.get("upgrade")?.toLowerCase() === "websocket";
818
- }
815
+ // Realtime voice a directly-spawned `codex app-server` needs these for desktop voice
816
+ // the same way it needs /v1/responses. Two shapes, same trust model as /v1/responses:
817
+ // - standalone sessions (codex-rs thread/realtime/start, WebSocket transport):
818
+ // WebSocket upgrades on the bare /v1/realtime and /v1/live paths only;
819
+ // - WebRTC calls (desktop v3 voice): POST call-create on /v1/live or
820
+ // /v1/realtime/calls, then the sideband join as a WebSocket upgrade on the keyed
821
+ // /v1/live/{callId}, /v1/realtime/calls/{callId}, or /v1/realtime?call_id= form
822
+ // (the join reaches this listener through the injected
823
+ // experimental_realtime_ws_base_url; openai/codex #35830).
824
+ // Plain HTTP on the upgrade paths stays rejected.
825
+ const isWebSocketUpgrade = req.headers.get("upgrade")?.toLowerCase() === "websocket";
826
+ if (path === "/v1/realtime") return isWebSocketUpgrade;
827
+ if (path === "/v1/live") return isWebSocketUpgrade || req.method === "POST";
828
+ if (path === "/v1/realtime/calls") return req.method === "POST";
829
+ if (/^\/v1\/(?:live|realtime\/calls)\/[^/]+\/?$/.test(path)) return isWebSocketUpgrade;
819
830
  return false;
820
831
  }
821
832
 
@@ -1354,7 +1365,7 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
1354
1365
  }
1355
1366
  throw error;
1356
1367
  }
1357
- const { accountBoundNativeOpenAiSlugsBySelector, applyNativeVisibility, buildCatalogEntries, configuredNativeAliasSlugs, desktopAllowlistSuppressedNativeSlugs, disabledNativeSlugs, exactComboCatalogSlugs, loadCatalogTemplate, NATIVE_OPENAI_MODELS, nativeContextLimits, nativeInputModalities, nativeOpenAiContextWindow, nativeOpenAiContextTier, nativeOpenAiSlugs, nativeReasoningEfforts, nativeDefaultReasoningEffort, orderForSubagents, filterCatalogVisibleModels, shouldIncludeAccountBoundNativeOpenAi, shouldIncludeNativeOpenAi, uniqueCatalogModelsForRawPublicList, visibleCodexAccountSelectors, visibleNativeSlugs, desktopVisibleNativeSlugs } = await import("../codex/catalog");
1368
+ const { accountBoundNativeOpenAiSlugsBySelector, applyNativeVisibility, buildCatalogEntries, configuredNativeAliasSlugs, desktopAllowlistSuppressedNativeSlugs, disabledNativeSlugs, exactComboCatalogSlugs, loadCatalogTemplate, NATIVE_OPENAI_MODELS, nativeContextLimits, nativeInputModalities, nativeOpenAiContextWindow, nativeOpenAiMaxOutputTokens, nativeOpenAiContextTier, nativeOpenAiSlugs, nativeReasoningEfforts, nativeDefaultReasoningEffort, orderForSubagents, filterCatalogVisibleModels, shouldIncludeAccountBoundNativeOpenAi, shouldIncludeNativeOpenAi, uniqueCatalogModelsForRawPublicList, visibleCodexAccountSelectors, visibleNativeSlugs, desktopVisibleNativeSlugs } = await import("../codex/catalog");
1358
1369
  const { ACCOUNT_GATED_NATIVE_OPENAI_MODELS } = await import("../codex/catalog/native-models");
1359
1370
  const includeNativeOpenAi = shouldIncludeNativeOpenAi(config);
1360
1371
  const includeAccountBoundNativeOpenAi = shouldIncludeAccountBoundNativeOpenAi(config);
@@ -1533,6 +1544,7 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
1533
1544
  // GPT-5.6) so the client can pick per request; without a tier, the effective
1534
1545
  // window is the only value.
1535
1546
  ...nativeContextInput(metadataId),
1547
+ maxOutputTokens: nativeOpenAiMaxOutputTokens(metadataId),
1536
1548
  inputModalities: nativeInputModalities(metadataId),
1537
1549
  }),
1538
1550
  });
@@ -1565,44 +1577,69 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
1565
1577
  return disabledModels.has(id) ? [] : [{ id, metadataId }];
1566
1578
  })
1567
1579
  );
1580
+ // The projection is opt-in. Keep the default path free of Cursor install detection,
1581
+ // and resolve the bundle table once for the whole list rather than once per row.
1582
+ const effortRowsEnabled = config.cursorEffortRows === true;
1583
+ const effortRowKnownIds = effortRowsEnabled ? knownEffortRowIds(config) : undefined;
1584
+ const privateInference = effortRowsEnabled
1585
+ ? detectCursorInstalls().find(install => install.build === "private-inference")
1586
+ : undefined;
1587
+ const cursorEffortTable = effortRowsEnabled
1588
+ ? (deps.managementApi?.loadCursorEffortTable ?? loadCursorEffortTable)(privateInference)
1589
+ : null;
1590
+ const expandedNativeModelRow = (id: string, metadataId = id) => {
1591
+ const reasoningEfforts = nativeReasoningEfforts(metadataId);
1592
+ return expandCursorEffortRow(nativeModelRow(id, metadataId), reasoningEfforts, config, {
1593
+ knownIds: effortRowKnownIds,
1594
+ table: cursorEffortTable,
1595
+ supportsReasoning: reasoningEfforts.length > 0,
1596
+ });
1597
+ };
1598
+ const routedRows = await Promise.all(uniqueCatalogModelsForRawPublicList(goOrdered).map(async m => {
1599
+ // Same rule as the anthropic branch: with the global fast switch on, a client
1600
+ // that has no Fast toggle is offered the fast identity directly. An operator
1601
+ // alias is an explicit decision and still wins.
1602
+ const fastModelId = cursorFastIdForListing?.(m.id, m.provider);
1603
+ const publicId = m.alias ?? `${m.provider}/${fastModelId ?? m.id}`;
1604
+ const isCombo = m.provider === "combo" && exactComboSlugs.has(publicId);
1605
+ const provider = config.providers[m.provider];
1606
+ const effective = provider
1607
+ ? (await import("../providers/default-aliases")).effectiveModelAliases(
1608
+ config,
1609
+ provider,
1610
+ knownModelIdsForProvider(m.provider, provider, config),
1611
+ ).get(m.id)
1612
+ : undefined;
1613
+ const row = {
1614
+ id: publicId,
1615
+ object: "model",
1616
+ created: 0,
1617
+ // This endpoint is an OpenAI-compatible inbound contract. Some clients use
1618
+ // owned_by as an adapter selector, so a virtual combo must name that wire
1619
+ // adapter rather than the internal catalog authority marker.
1620
+ owned_by: isCombo ? "openai" : (m.owned_by ?? m.provider),
1621
+ ...(isCombo ? { is_combo: true } : {}),
1622
+ ...(effective ? { alias_of: `${provider?.alias || m.provider}/${effective.alias}` } : {}),
1623
+ ...grokEffortFields(m.reasoningEfforts ?? [], m.defaultReasoningEffort),
1624
+ ...modelCapabilityFields({
1625
+ reasoningEfforts: m.reasoningEfforts,
1626
+ // contextWindow is already the post-cap effective value; contextCap is the raw
1627
+ // operator knob and over-reports models whose real window sits below it.
1628
+ contextWindow: m.contextWindow,
1629
+ maxOutputTokens: m.maxOutputTokens,
1630
+ inputModalities: m.inputModalities,
1631
+ }),
1632
+ };
1633
+ return expandCursorEffortRow(row, m.reasoningEfforts, config, {
1634
+ knownIds: effortRowKnownIds,
1635
+ table: cursorEffortTable,
1636
+ supportsReasoning: (m.reasoningEfforts ?? []).length > 0,
1637
+ });
1638
+ }));
1568
1639
  const data = [
1569
- ...visibleNatives.map(id => nativeModelRow(id)),
1570
- ...visibleAccountNatives.map(({ id, metadataId }) => nativeModelRow(id, metadataId)),
1571
- ...await Promise.all(uniqueCatalogModelsForRawPublicList(goOrdered).map(async m => {
1572
- // Same rule as the anthropic branch: with the global fast switch on, a client
1573
- // that has no Fast toggle is offered the fast identity directly. An operator
1574
- // alias is an explicit decision and still wins.
1575
- const fastModelId = cursorFastIdForListing?.(m.id, m.provider);
1576
- const publicId = m.alias ?? `${m.provider}/${fastModelId ?? m.id}`;
1577
- const isCombo = m.provider === "combo" && exactComboSlugs.has(publicId);
1578
- const provider = config.providers[m.provider];
1579
- const effective = provider
1580
- ? (await import("../providers/default-aliases")).effectiveModelAliases(
1581
- config,
1582
- provider,
1583
- knownModelIdsForProvider(m.provider, provider, config),
1584
- ).get(m.id)
1585
- : undefined;
1586
- return {
1587
- id: publicId,
1588
- object: "model",
1589
- created: 0,
1590
- // This endpoint is an OpenAI-compatible inbound contract. Some clients use
1591
- // owned_by as an adapter selector, so a virtual combo must name that wire
1592
- // adapter rather than the internal catalog authority marker.
1593
- owned_by: isCombo ? "openai" : (m.owned_by ?? m.provider),
1594
- ...(isCombo ? { is_combo: true } : {}),
1595
- ...(effective ? { alias_of: `${provider?.alias || m.provider}/${effective.alias}` } : {}),
1596
- ...grokEffortFields(m.reasoningEfforts ?? [], m.defaultReasoningEffort),
1597
- ...modelCapabilityFields({
1598
- reasoningEfforts: m.reasoningEfforts,
1599
- // contextWindow is already the post-cap effective value; contextCap is the raw
1600
- // operator knob and over-reports models whose real window sits below it.
1601
- contextWindow: m.contextWindow,
1602
- inputModalities: m.inputModalities,
1603
- }),
1604
- };
1605
- })),
1640
+ ...visibleNatives.flatMap(id => expandedNativeModelRow(id)),
1641
+ ...visibleAccountNatives.flatMap(({ id, metadataId }) => expandedNativeModelRow(id, metadataId)),
1642
+ ...routedRows.flat(),
1606
1643
  ];
1607
1644
  return jsonResponse({ object: "list", data }, 200, req, policy);
1608
1645
  }
@@ -147,6 +147,20 @@ function clientProtocolHeaders(reqHeaders: Headers): Record<string, string> {
147
147
 
148
148
  const LIVE_CALL_ID_RE = /^[A-Za-z0-9_-]{1,128}$/;
149
149
 
150
+ /**
151
+ * Decode one path-segment call id. A malformed percent escape (`%ZZ`) makes
152
+ * `decodeURIComponent` throw; that must read as "not a sideband target" (JSON 404),
153
+ * never escape the router as a 500.
154
+ */
155
+ function decodeLiveCallId(segment: string): string | null {
156
+ try {
157
+ const callId = decodeURIComponent(segment);
158
+ return LIVE_CALL_ID_RE.test(callId) ? callId : null;
159
+ } catch {
160
+ return null;
161
+ }
162
+ }
163
+
150
164
  /**
151
165
  * Credential-shaped query keys never forwarded upstream on a standalone realtime
152
166
  * relay. Auth on the upstream socket is proxy-owned (headers resolved by
@@ -238,8 +252,8 @@ function httpsToWss(httpUrl: string): string {
238
252
  export function parseLiveSidebandTarget(pathname: string, searchParams: URLSearchParams, rawQuery = ""): LiveSidebandTarget | null {
239
253
  const liveMatch = pathname.match(/^\/v1\/live\/([^/]+)\/?$/);
240
254
  if (liveMatch) {
241
- const callId = decodeURIComponent(liveMatch[1]!);
242
- if (!LIVE_CALL_ID_RE.test(callId)) return null;
255
+ const callId = decodeLiveCallId(liveMatch[1]!);
256
+ if (!callId) return null;
243
257
  return { style: "frameless-path", callId };
244
258
  }
245
259
  // Standalone Frameless session (no call-create): `GET /v1/live?model=`.
@@ -248,8 +262,8 @@ export function parseLiveSidebandTarget(pathname: string, searchParams: URLSearc
248
262
  }
249
263
  const callsMatch = pathname.match(/^\/v1\/realtime\/calls\/([^/]+)\/?$/);
250
264
  if (callsMatch) {
251
- const callId = decodeURIComponent(callsMatch[1]!);
252
- if (!LIVE_CALL_ID_RE.test(callId)) return null;
265
+ const callId = decodeLiveCallId(callsMatch[1]!);
266
+ if (!callId) return null;
253
267
  return { style: "realtime-calls-path", callId };
254
268
  }
255
269
  if (pathname === "/v1/realtime" || pathname === "/v1/realtime/") {
@@ -7,6 +7,7 @@ export type ApiKeyRotationStart = {
7
7
  id: string;
8
8
  name: string;
9
9
  key: string;
10
+ createdAt: string;
10
11
  rotationId: string;
11
12
  expiresAt: string;
12
13
  };
@@ -44,7 +45,7 @@ export function startApiKeyRotation(
44
45
  const createdAt = new Date(now).toISOString();
45
46
  const expiresAt = new Date(now + API_KEY_ROTATION_TTL_MS).toISOString();
46
47
  entry.pendingRotation = { id: rotationId, key, createdAt, expiresAt };
47
- return { id: entry.id, name: entry.name, key, rotationId, expiresAt };
48
+ return { id: entry.id, name: entry.name, key, createdAt, rotationId, expiresAt };
48
49
  }
49
50
 
50
51
  export function commitApiKeyRotation(
@@ -1,9 +1,9 @@
1
1
  import {
2
2
  currentUsageLogRevision,
3
- readUsageSnapshotForManagement,
4
3
  usageLogIdentityKey,
5
4
  type PersistedUsageEntry,
6
5
  } from "../../usage/log";
6
+ import { scanUsageLedgerCooperatively } from "../../usage/ledger-scanner";
7
7
 
8
8
  /**
9
9
  * Per-key usage as the API tab renders it.
@@ -29,6 +29,11 @@ export interface ApiKeyUsageSnapshot {
29
29
  attributionSince?: string;
30
30
  }
31
31
 
32
+ export interface ApiKeyUsageAccumulator {
33
+ add(entry: PersistedUsageEntry): void;
34
+ snapshot(): ApiKeyUsageSnapshot;
35
+ }
36
+
32
37
  const SEVEN_DAYS_MS = 7 * 24 * 60 * 60 * 1000;
33
38
 
34
39
  /**
@@ -58,6 +63,21 @@ export function rollupApiKeyUsage(
58
63
  configuredIds: string[],
59
64
  now: number = Date.now(),
60
65
  ): ApiKeyUsageSnapshot {
66
+ const accumulator = createApiKeyUsageAccumulator(configuredIds, now);
67
+ for (const entry of entries) accumulator.add(entry);
68
+ return accumulator.snapshot();
69
+ }
70
+
71
+ /**
72
+ * Constant-memory fold for API-key attribution while the usage ledger streams.
73
+ *
74
+ * Only configured IDs are retained, so a hand-edited ledger containing an
75
+ * unbounded set of arbitrary `apiKeyId` values cannot grow this accumulator.
76
+ */
77
+ export function createApiKeyUsageAccumulator(
78
+ configuredIds: string[],
79
+ now: number = Date.now(),
80
+ ): ApiKeyUsageAccumulator {
61
81
  const duplicated = new Set<string>();
62
82
  const seen = new Set<string>();
63
83
  for (const id of configuredIds) {
@@ -69,37 +89,40 @@ export function rollupApiKeyUsage(
69
89
  let attributionSince: number | undefined;
70
90
  const cutoff = now - SEVEN_DAYS_MS;
71
91
 
72
- for (const entry of entries) {
73
- if (!entry.admissionKind) continue;
74
- const timestamp = usableTimestamp(entry.timestamp);
75
- if (timestamp !== null && (attributionSince === undefined || timestamp < attributionSince)) {
76
- attributionSince = timestamp;
77
- }
78
- if (entry.admissionKind !== "configured" || !entry.apiKeyId) continue;
79
-
80
- const bucket = totals.get(entry.apiKeyId) ?? { requests7d: 0, totalRequests: 0 };
81
- // The request happened even if its clock reading is unusable, so it still
82
- // counts toward the total; only the time-based fields are skipped.
83
- bucket.totalRequests += 1;
84
- if (timestamp !== null) {
85
- if (timestamp >= cutoff) bucket.requests7d += 1;
86
- const iso = new Date(timestamp).toISOString();
87
- if (!bucket.lastUsedAt || iso > bucket.lastUsedAt) bucket.lastUsedAt = iso;
88
- }
89
- totals.set(entry.apiKeyId, bucket);
90
- }
91
-
92
- const rollup = new Map<string, ApiKeyUsage>();
93
- for (const id of configuredIds) {
94
- if (duplicated.has(id)) {
95
- rollup.set(id, { ambiguous: true });
96
- continue;
97
- }
98
- rollup.set(id, totals.get(id) ?? { requests7d: 0, totalRequests: 0 });
99
- }
100
92
  return {
101
- rollup,
102
- ...(attributionSince !== undefined ? { attributionSince: new Date(attributionSince).toISOString() } : {}),
93
+ add(entry) {
94
+ if (!entry.admissionKind) return;
95
+ const timestamp = usableTimestamp(entry.timestamp);
96
+ if (timestamp !== null && (attributionSince === undefined || timestamp < attributionSince)) {
97
+ attributionSince = timestamp;
98
+ }
99
+ if (entry.admissionKind !== "configured" || !entry.apiKeyId || !seen.has(entry.apiKeyId)) return;
100
+
101
+ const bucket = totals.get(entry.apiKeyId) ?? { requests7d: 0, totalRequests: 0 };
102
+ // The request happened even if its clock reading is unusable, so it still
103
+ // counts toward the total; only the time-based fields are skipped.
104
+ bucket.totalRequests += 1;
105
+ if (timestamp !== null) {
106
+ if (timestamp >= cutoff) bucket.requests7d += 1;
107
+ const iso = new Date(timestamp).toISOString();
108
+ if (!bucket.lastUsedAt || iso > bucket.lastUsedAt) bucket.lastUsedAt = iso;
109
+ }
110
+ totals.set(entry.apiKeyId, bucket);
111
+ },
112
+ snapshot() {
113
+ const rollup = new Map<string, ApiKeyUsage>();
114
+ for (const id of configuredIds) {
115
+ if (duplicated.has(id)) {
116
+ rollup.set(id, { ambiguous: true });
117
+ continue;
118
+ }
119
+ rollup.set(id, totals.get(id) ?? { requests7d: 0, totalRequests: 0 });
120
+ }
121
+ return {
122
+ rollup,
123
+ ...(attributionSince !== undefined ? { attributionSince: new Date(attributionSince).toISOString() } : {}),
124
+ };
125
+ },
103
126
  };
104
127
  }
105
128
 
@@ -111,6 +134,7 @@ export function rollupApiKeyUsage(
111
134
  * caching it costs nothing; a new row changes the revision and invalidates it.
112
135
  */
113
136
  let rollupCache: { revisionKey: string; expiresAt: number; lastSeenSize?: number; snapshot: ApiKeyUsageSnapshot } | null = null;
137
+ const rollupFlights = new Map<string, Promise<ApiKeyUsageSnapshot>>();
114
138
 
115
139
  /**
116
140
  * The rollup is a function of the log AND of the clock: a request ages out of
@@ -127,6 +151,7 @@ const ROLLUP_CACHE_TTL_MS = 60_000;
127
151
  /** Test seam: the cache is module state and would otherwise leak between cases. */
128
152
  export function clearApiKeyUsageCacheForTests(): void {
129
153
  rollupCache = null;
154
+ rollupFlights.clear();
130
155
  }
131
156
 
132
157
  /**
@@ -159,6 +184,25 @@ export function cacheApiKeyUsageFromSnapshot(
159
184
  return rolled;
160
185
  }
161
186
 
187
+ /** Seed the API-key cache from the accumulator already fed by `/api/usage`. */
188
+ export function cacheApiKeyUsageFromRollup(
189
+ snapshot: ApiKeyUsageSnapshot,
190
+ configuredIds: string[],
191
+ identityKey: string,
192
+ lastSeenSize: number,
193
+ maxReadBytes: number | undefined,
194
+ now: number = Date.now(),
195
+ ): ApiKeyUsageSnapshot {
196
+ const idsKey = JSON.stringify([configuredIds, maxReadBytes]);
197
+ rollupCache = {
198
+ revisionKey: `${identityKey}|${idsKey}`,
199
+ expiresAt: now + ROLLUP_CACHE_TTL_MS,
200
+ lastSeenSize,
201
+ snapshot,
202
+ };
203
+ return snapshot;
204
+ }
205
+
162
206
  export async function readApiKeyUsageRollup(configuredIds: string[], maxReadBytes?: number): Promise<ApiKeyUsageSnapshot> {
163
207
  // JSON rather than a joined string: ids are only validated as non-empty
164
208
  // strings, so `["a\0b","c"]` and `["a","b\0c"]` join to the same value and one
@@ -173,18 +217,28 @@ export async function readApiKeyUsageRollup(configuredIds: string[], maxReadByte
173
217
  return rollupCache.snapshot;
174
218
  }
175
219
 
176
- const snapshot = await readUsageSnapshotForManagement(maxReadBytes);
177
- const rolled = {
178
- ...rollupApiKeyUsage(snapshot.entries, configuredIds, now),
179
- ...(snapshot.truncatedPrefixBytes > 0 || snapshot.entriesTruncated ? { historyTruncated: true as const } : {}),
180
- };
181
- rollupCache = {
182
- revisionKey: `${usageLogIdentityKey(snapshot.revision)}|${idsKey}`,
183
- expiresAt: now + ROLLUP_CACHE_TTL_MS,
184
- lastSeenSize: snapshot.revision?.size ?? 0,
185
- snapshot: rolled,
186
- };
187
- return rolled;
220
+ const existing = rollupFlights.get(idsKey);
221
+ if (existing) return await existing;
222
+
223
+ const flight = (async (): Promise<ApiKeyUsageSnapshot> => {
224
+ const accumulator = createApiKeyUsageAccumulator(configuredIds, now);
225
+ const scan = await scanUsageLedgerCooperatively({ onEntry: entry => accumulator.add(entry) });
226
+ if (scan.oversizedRows > 0) throw new Error("usage ledger contains an oversized row");
227
+ return cacheApiKeyUsageFromRollup(
228
+ accumulator.snapshot(),
229
+ configuredIds,
230
+ usageLogIdentityKey(scan.revision),
231
+ scan.revision?.size ?? 0,
232
+ maxReadBytes,
233
+ now,
234
+ );
235
+ })();
236
+ rollupFlights.set(idsKey, flight);
237
+ try {
238
+ return await flight;
239
+ } finally {
240
+ if (rollupFlights.get(idsKey) === flight) rollupFlights.delete(idsKey);
241
+ }
188
242
  } catch {
189
243
  const rollup = new Map<string, ApiKeyUsage>();
190
244
  for (const id of configuredIds) rollup.set(id, { requests7d: 0, totalRequests: 0 });
@@ -11,6 +11,8 @@ import type { injectGrokConfig } from "../../grok/inject";
11
11
  import type { removeDesktop3pStandardPivot, writeDesktop3pConfig } from "../../claude/desktop-3p";
12
12
  import type { probeClaudeDesktopPolicy } from "../../claude/desktop-policy";
13
13
  import type { RuntimePortState } from "../../config/process-state";
14
+ import type { CursorInstall } from "../../integrations/cursor-detect";
15
+ import type { CursorEffortTable } from "../../integrations/cursor-effort-table";
14
16
  import type { CatalogDisposition, ConvergeCodex } from "../../codex/convergence-types";
15
17
  import type {
16
18
  performCodexRestart,
@@ -58,6 +60,7 @@ export interface ManagementApiDeps {
58
60
  * on the developer's real runtime state file.
59
61
  */
60
62
  readRuntimePort?: (pid: number) => RuntimePortState | null;
63
+ loadCursorEffortTable?: (install: CursorInstall | undefined) => CursorEffortTable | null;
61
64
  clearThreadAccountMap?: () => void;
62
65
  clearProviderQuotaCache?: () => void;
63
66
  primeCodexPoolQuotas?: (config: OcxConfig, reason: string) => Promise<void> | void;
@@ -9,12 +9,14 @@
9
9
  * started — plus which active models will show Cursor's Reasoning and Context controls.
10
10
  */
11
11
  import { readRuntimePort } from "../../config/process-state";
12
- import { filterCatalogVisibleModels, nativeContextLimits, nativeOpenAiContextTier, uniqueCatalogModelsForRawPublicList, visibleNativeSlugs } from "../../codex/catalog";
12
+ import { filterCatalogVisibleModels, nativeContextLimits, nativeOpenAiContextTier, nativeReasoningEfforts, uniqueCatalogModelsForRawPublicList, visibleNativeSlugs } from "../../codex/catalog";
13
13
  import { cursorLastSeen, type CursorSeen } from "../../integrations/cursor-seen";
14
14
  import { detectCursorInstalls, type CursorInstall } from "../../integrations/cursor-detect";
15
+ import { loadCursorEffortTable } from "../../integrations/cursor-effort-table";
15
16
  import { configuredApiAuthToken, isApiAuthRequired, jsonResponse } from "../auth-cors";
16
17
  import { fetchAllModels } from "../management-api";
17
- import { cursorEffortFamily } from "../models-capabilities";
18
+ import { predictCursorEffort } from "../models-capabilities";
19
+ import { expandCursorEffortRow, knownEffortRowIds } from "../effort-row";
18
20
  import type { ManagementContext } from "./context";
19
21
 
20
22
  export const CURSOR_GATEWAY_PLACEHOLDER_KEY = "opencodex-loopback";
@@ -25,9 +27,13 @@ export interface CursorIntegrationStatus {
25
27
  regularCursor: { installed: boolean; path: string | null };
26
28
  gateway: { baseUrl: string; apiKeyMode: "credential" | "placeholder"; placeholder: string };
27
29
  lastSeen: CursorSeen | null;
30
+ effortTable: { source: "bundle" | "static"; version: string | null; families: number | null };
28
31
  models: Array<{
29
32
  id: string;
30
33
  reasoning: string[] | null;
34
+ family: string | null;
35
+ tableLess: boolean;
36
+ effortRows: string[];
31
37
  context: { defaultWindow: number; longWindow: number } | null;
32
38
  }>;
33
39
  guideUrl: string;
@@ -58,18 +64,40 @@ export async function buildCursorIntegrationStatus(
58
64
  // Same visibility rules as the raw /v1/models list Cursor will read: disabled models and
59
65
  // provider allowlists drop out here too, or the prediction shows rows Cursor never gets.
60
66
  const goModels = filterCatalogVisibleModels(await fetchAllModels(config), config);
61
- const ids = [
62
- ...visibleNativeSlugs(config),
63
- ...uniqueCatalogModelsForRawPublicList(goModels).map(model => model.alias ?? `${model.provider}/${model.id}`),
67
+ // supportsReasoning mirrors what the /v1/models row advertises (a non-empty ladder); the
68
+ // gemini family withholds its control when it is false.
69
+ const ids: Array<{ id: string; supportsReasoning: boolean; reasoningEfforts: readonly string[] }> = [
70
+ ...visibleNativeSlugs(config).map(id => {
71
+ const reasoningEfforts = nativeReasoningEfforts(id);
72
+ return { id, supportsReasoning: reasoningEfforts.length > 0, reasoningEfforts };
73
+ }),
74
+ ...uniqueCatalogModelsForRawPublicList(goModels).map(model => ({
75
+ id: model.alias ?? `${model.provider}/${model.id}`,
76
+ supportsReasoning: (model.reasoningEfforts ?? []).length > 0,
77
+ reasoningEfforts: model.reasoningEfforts ?? [],
78
+ })),
64
79
  ];
65
- const models = ids.map(id => {
80
+ const table = (deps.loadCursorEffortTable ?? loadCursorEffortTable)(privateInference);
81
+ const effortRowKnownIds = config.cursorEffortRows === true ? knownEffortRowIds(config) : undefined;
82
+ const models = ids.map(({ id, supportsReasoning, reasoningEfforts }) => {
66
83
  const tier = nativeOpenAiContextTier(id, limits);
84
+ const predicted = predictCursorEffort(id, table, supportsReasoning);
67
85
  return {
68
86
  id,
69
- reasoning: cursorEffortFamily(id),
87
+ reasoning: predicted.ladder,
88
+ family: predicted.family,
89
+ tableLess: predicted.ladder === null,
90
+ effortRows: expandCursorEffortRow({ id }, reasoningEfforts, config, {
91
+ knownIds: effortRowKnownIds,
92
+ table,
93
+ supportsReasoning,
94
+ }).slice(1).map(row => row.id),
70
95
  context: tier ? { defaultWindow: tier.defaultWindow, longWindow: tier.longWindow } : null,
71
96
  };
72
97
  });
98
+ const effortTable = table
99
+ ? { source: "bundle" as const, version: table.version, families: table.families.length }
100
+ : { source: "static" as const, version: null, families: null };
73
101
 
74
102
  return {
75
103
  privateInference: {
@@ -84,6 +112,7 @@ export async function buildCursorIntegrationStatus(
84
112
  placeholder: CURSOR_GATEWAY_PLACEHOLDER_KEY,
85
113
  },
86
114
  lastSeen: cursorLastSeen(),
115
+ effortTable,
87
116
  models,
88
117
  guideUrl: CURSOR_GUIDE_URL,
89
118
  };