@bitkyc08/opencodex 2.40.0 → 2.41.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 (72) hide show
  1. package/README.md +4 -0
  2. package/gui/dist/assets/index-B2YjLA-i.css +1 -0
  3. package/gui/dist/assets/{index-BHe2rl_C.js → index-aPup8CKb.js} +20 -20
  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/google.ts +39 -2
  12. package/src/adapters/openai-responses.ts +14 -1
  13. package/src/cli/claude.ts +11 -2
  14. package/src/cli/connect.ts +7 -1
  15. package/src/cli/registry.ts +1 -1
  16. package/src/cli/status.ts +19 -4
  17. package/src/client/connect.ts +5 -1
  18. package/src/client/hub-client.ts +29 -5
  19. package/src/clients/config-export.ts +12 -2
  20. package/src/codex/catalog/aggregation.ts +8 -0
  21. package/src/codex/catalog/metadata.ts +5 -0
  22. package/src/codex/catalog/parsing.ts +2 -0
  23. package/src/codex/catalog/provider-fetch.ts +163 -26
  24. package/src/codex/catalog.ts +1 -1
  25. package/src/codex/convergence-types.ts +1 -0
  26. package/src/codex/desired-state.ts +18 -11
  27. package/src/combos/failover.ts +185 -6
  28. package/src/combos/index.ts +6 -0
  29. package/src/combos/resolve.ts +43 -6
  30. package/src/config.ts +5 -1
  31. package/src/generated/compatibility-version.json +85 -61
  32. package/src/generated/model-metadata.ts +1 -1
  33. package/src/grok/sync.ts +10 -2
  34. package/src/integrations/cursor-effort-table.ts +143 -0
  35. package/src/integrations/state.ts +1 -1
  36. package/src/integrations/writer.ts +2 -2
  37. package/src/lib/app-owned-memory-stores.ts +27 -8
  38. package/src/lib/bounded-body.ts +16 -1
  39. package/src/oauth/generic-account-failover.ts +2 -2
  40. package/src/oauth/index.ts +11 -0
  41. package/src/oauth/meta-muse.ts +235 -0
  42. package/src/providers/antigravity-models.ts +71 -13
  43. package/src/providers/command-code-efforts.ts +15 -0
  44. package/src/providers/free-directory.ts +4 -1
  45. package/src/providers/registry.ts +116 -8
  46. package/src/responses/code-mode-helper-compat.ts +4 -1
  47. package/src/responses/state.ts +5 -4
  48. package/src/server/auth-cors.ts +241 -56
  49. package/src/server/chat-completions.ts +11 -2
  50. package/src/server/chat-native.ts +30 -4
  51. package/src/server/claude-messages.ts +17 -3
  52. package/src/server/effort-row.ts +131 -0
  53. package/src/server/index.ts +67 -38
  54. package/src/server/management/api-key-rotation.ts +2 -1
  55. package/src/server/management/api-key-usage.ts +97 -43
  56. package/src/server/management/context.ts +3 -0
  57. package/src/server/management/cursor-integration-routes.ts +36 -7
  58. package/src/server/management/logs-usage-routes.ts +64 -87
  59. package/src/server/management/provider-routes.ts +218 -1
  60. package/src/server/management/route-registry.ts +1 -0
  61. package/src/server/management/usage-aggregate-cache.ts +464 -0
  62. package/src/server/management/usage-summary-cache.ts +4 -0
  63. package/src/server/models-capabilities.ts +60 -5
  64. package/src/server/responses/core.ts +61 -7
  65. package/src/types/config.ts +10 -1
  66. package/src/types/tools.ts +12 -9
  67. package/src/usage/expected-prices.ts +43 -7
  68. package/src/usage/ledger-scanner.ts +448 -0
  69. package/src/usage/log.ts +1 -1
  70. package/src/usage/summary.ts +915 -655
  71. package/src/web-search/index.ts +1 -1
  72. 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;
@@ -1354,7 +1357,7 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
1354
1357
  }
1355
1358
  throw error;
1356
1359
  }
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");
1360
+ 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
1361
  const { ACCOUNT_GATED_NATIVE_OPENAI_MODELS } = await import("../codex/catalog/native-models");
1359
1362
  const includeNativeOpenAi = shouldIncludeNativeOpenAi(config);
1360
1363
  const includeAccountBoundNativeOpenAi = shouldIncludeAccountBoundNativeOpenAi(config);
@@ -1533,6 +1536,7 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
1533
1536
  // GPT-5.6) so the client can pick per request; without a tier, the effective
1534
1537
  // window is the only value.
1535
1538
  ...nativeContextInput(metadataId),
1539
+ maxOutputTokens: nativeOpenAiMaxOutputTokens(metadataId),
1536
1540
  inputModalities: nativeInputModalities(metadataId),
1537
1541
  }),
1538
1542
  });
@@ -1565,44 +1569,69 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
1565
1569
  return disabledModels.has(id) ? [] : [{ id, metadataId }];
1566
1570
  })
1567
1571
  );
1572
+ // The projection is opt-in. Keep the default path free of Cursor install detection,
1573
+ // and resolve the bundle table once for the whole list rather than once per row.
1574
+ const effortRowsEnabled = config.cursorEffortRows === true;
1575
+ const effortRowKnownIds = effortRowsEnabled ? knownEffortRowIds(config) : undefined;
1576
+ const privateInference = effortRowsEnabled
1577
+ ? detectCursorInstalls().find(install => install.build === "private-inference")
1578
+ : undefined;
1579
+ const cursorEffortTable = effortRowsEnabled
1580
+ ? (deps.managementApi?.loadCursorEffortTable ?? loadCursorEffortTable)(privateInference)
1581
+ : null;
1582
+ const expandedNativeModelRow = (id: string, metadataId = id) => {
1583
+ const reasoningEfforts = nativeReasoningEfforts(metadataId);
1584
+ return expandCursorEffortRow(nativeModelRow(id, metadataId), reasoningEfforts, config, {
1585
+ knownIds: effortRowKnownIds,
1586
+ table: cursorEffortTable,
1587
+ supportsReasoning: reasoningEfforts.length > 0,
1588
+ });
1589
+ };
1590
+ const routedRows = await Promise.all(uniqueCatalogModelsForRawPublicList(goOrdered).map(async m => {
1591
+ // Same rule as the anthropic branch: with the global fast switch on, a client
1592
+ // that has no Fast toggle is offered the fast identity directly. An operator
1593
+ // alias is an explicit decision and still wins.
1594
+ const fastModelId = cursorFastIdForListing?.(m.id, m.provider);
1595
+ const publicId = m.alias ?? `${m.provider}/${fastModelId ?? m.id}`;
1596
+ const isCombo = m.provider === "combo" && exactComboSlugs.has(publicId);
1597
+ const provider = config.providers[m.provider];
1598
+ const effective = provider
1599
+ ? (await import("../providers/default-aliases")).effectiveModelAliases(
1600
+ config,
1601
+ provider,
1602
+ knownModelIdsForProvider(m.provider, provider, config),
1603
+ ).get(m.id)
1604
+ : undefined;
1605
+ const row = {
1606
+ id: publicId,
1607
+ object: "model",
1608
+ created: 0,
1609
+ // This endpoint is an OpenAI-compatible inbound contract. Some clients use
1610
+ // owned_by as an adapter selector, so a virtual combo must name that wire
1611
+ // adapter rather than the internal catalog authority marker.
1612
+ owned_by: isCombo ? "openai" : (m.owned_by ?? m.provider),
1613
+ ...(isCombo ? { is_combo: true } : {}),
1614
+ ...(effective ? { alias_of: `${provider?.alias || m.provider}/${effective.alias}` } : {}),
1615
+ ...grokEffortFields(m.reasoningEfforts ?? [], m.defaultReasoningEffort),
1616
+ ...modelCapabilityFields({
1617
+ reasoningEfforts: m.reasoningEfforts,
1618
+ // contextWindow is already the post-cap effective value; contextCap is the raw
1619
+ // operator knob and over-reports models whose real window sits below it.
1620
+ contextWindow: m.contextWindow,
1621
+ maxOutputTokens: m.maxOutputTokens,
1622
+ inputModalities: m.inputModalities,
1623
+ }),
1624
+ };
1625
+ return expandCursorEffortRow(row, m.reasoningEfforts, config, {
1626
+ knownIds: effortRowKnownIds,
1627
+ table: cursorEffortTable,
1628
+ supportsReasoning: (m.reasoningEfforts ?? []).length > 0,
1629
+ });
1630
+ }));
1568
1631
  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
- })),
1632
+ ...visibleNatives.flatMap(id => expandedNativeModelRow(id)),
1633
+ ...visibleAccountNatives.flatMap(({ id, metadataId }) => expandedNativeModelRow(id, metadataId)),
1634
+ ...routedRows.flat(),
1606
1635
  ];
1607
1636
  return jsonResponse({ object: "list", data }, 200, req, policy);
1608
1637
  }
@@ -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
  };