@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
package/src/server/index.ts
CHANGED
|
@@ -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
|
-
//
|
|
813
|
-
//
|
|
814
|
-
//
|
|
815
|
-
//
|
|
816
|
-
|
|
817
|
-
|
|
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.
|
|
1570
|
-
...visibleAccountNatives.
|
|
1571
|
-
...
|
|
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
|
}
|
package/src/server/live.ts
CHANGED
|
@@ -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 =
|
|
242
|
-
if (!
|
|
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 =
|
|
252
|
-
if (!
|
|
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
|
-
|
|
102
|
-
|
|
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
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
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 {
|
|
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
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
|
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:
|
|
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
|
};
|