@bitkyc08/opencodex 2.39.0 → 2.40.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 +49 -21
- package/gui/dist/assets/index-BHe2rl_C.js +112 -0
- package/gui/dist/assets/index-CJSb3HPe.css +1 -0
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/adapters/cursor/catalog.ts +140 -7
- package/src/adapters/cursor/discovery.ts +79 -87
- package/src/adapters/cursor/effort-map.ts +11 -0
- package/src/adapters/cursor/request-builder.ts +31 -4
- package/src/adapters/cursor.ts +17 -0
- package/src/adapters/openai-chat.ts +95 -9
- package/src/adapters/openai-responses.ts +73 -9
- package/src/bridge.ts +58 -13
- package/src/claude/auth-mode.ts +6 -9
- package/src/claude/gateway-cache.ts +23 -3
- package/src/claude/model-info.ts +13 -2
- package/src/cli/access.ts +30 -0
- package/src/cli/account-extended.ts +19 -10
- package/src/cli/capabilities.ts +40 -1
- package/src/cli/claude-agent-startup-sync.ts +3 -0
- package/src/cli/claude.ts +134 -36
- package/src/cli/connect.ts +226 -0
- package/src/cli/dispatch.ts +126 -21
- package/src/cli/gui-pair-client.ts +170 -0
- package/src/cli/gui.ts +87 -0
- package/src/cli/help.ts +4 -1
- package/src/cli/index.ts +76 -22
- package/src/cli/models-runtime-subcommands.ts +34 -0
- package/src/cli/models-runtime.ts +4 -0
- package/src/cli/models.ts +2 -1
- package/src/cli/opencode.ts +2 -1
- package/src/cli/provider-runtime.ts +31 -1
- package/src/cli/registry.ts +32 -2
- package/src/cli/runtime-api.ts +10 -1
- package/src/cli/status.ts +26 -0
- package/src/cli/system-command.ts +9 -3
- package/src/client/connect.ts +650 -0
- package/src/client/hub-client.ts +457 -0
- package/src/client/hub-relay.ts +288 -0
- package/src/client/machine-api.ts +139 -0
- package/src/client/machine-auth.ts +54 -0
- package/src/client/machine-listener.ts +143 -0
- package/src/client/runtime.ts +93 -0
- package/src/client/state.ts +175 -0
- package/src/clients/config-export.ts +16 -5
- package/src/codex/auth-api.ts +169 -15
- package/src/codex/auth-context.ts +61 -6
- package/src/codex/catalog/aggregation.ts +17 -13
- package/src/codex/catalog/metadata.ts +28 -0
- package/src/codex/catalog/provider-fetch.ts +38 -5
- package/src/codex/catalog.ts +1 -1
- package/src/codex/desired-state.ts +13 -3
- package/src/codex/inject.ts +212 -31
- package/src/codex/journal.ts +54 -6
- package/src/codex/main-account.ts +105 -33
- package/src/codex/native-main-claim.ts +23 -1
- package/src/codex/native-residue.ts +7 -0
- package/src/codex/reset-credit-auto-redeem.ts +237 -0
- package/src/combos/request.ts +15 -3
- package/src/combos/types.ts +12 -0
- package/src/config/paths.ts +15 -1
- package/src/config/provider-validation.ts +40 -0
- package/src/config.ts +447 -3
- package/src/generated/compatibility-version.json +224 -108
- package/src/images/artifacts.ts +73 -37
- package/src/images/fulfill.ts +6 -1
- package/src/images/index.ts +1 -1
- package/src/images/plan.ts +16 -2
- package/src/images/synthetic-tool.ts +5 -0
- package/src/images/xai-client.ts +34 -1
- package/src/integrations/cursor-detect.ts +133 -0
- package/src/integrations/cursor-seen.ts +31 -0
- package/src/lib/errors.ts +18 -0
- package/src/lib/gui-pair-capability.ts +104 -0
- package/src/lib/lab-live-route-production.ts +2 -1
- package/src/lib/process-control.ts +4 -3
- package/src/lib/service-secrets.ts +161 -1
- package/src/lib/windows-system-proxy.ts +115 -0
- package/src/oauth/index.ts +3 -2
- package/src/oauth/pool-settings-capability.ts +55 -0
- package/src/providers/api-keys.ts +3 -1
- package/src/providers/codex-capacity.ts +34 -4
- package/src/providers/derive.ts +10 -0
- package/src/providers/fastwire.ts +10 -2
- package/src/providers/key-store.ts +197 -0
- package/src/providers/model-rename-migration.ts +3 -0
- package/src/providers/openai-sidecar.ts +3 -3
- package/src/providers/quota.ts +91 -42
- package/src/providers/registry.ts +33 -3
- package/src/reasoning-effort.ts +32 -0
- package/src/remote/protocol.ts +109 -0
- package/src/responses/citation-markers.ts +101 -0
- package/src/responses/parser.ts +43 -3
- package/src/responses/spill-store.ts +9 -4
- package/src/router.ts +72 -8
- package/src/server/auth-cors.ts +37 -1
- package/src/server/gui-session.ts +449 -0
- package/src/server/gui-static.ts +24 -7
- package/src/server/images.ts +237 -10
- package/src/server/index.ts +356 -30
- package/src/server/management/agent-settings-routes.ts +7 -1
- package/src/server/management/api-key-rotation.ts +74 -0
- package/src/server/management/combo-routes.ts +18 -6
- package/src/server/management/config-routes.ts +22 -3
- package/src/server/management/context.ts +5 -1
- package/src/server/management/cursor-integration-routes.ts +98 -0
- package/src/server/management/logs-usage-routes.ts +2 -1
- package/src/server/management/model-routes.ts +79 -1
- package/src/server/management/model-rows.ts +23 -1
- package/src/server/management/oauth-account-routes.ts +135 -2
- package/src/server/management/provider-routes.ts +143 -9
- package/src/server/management/route-registry.ts +10 -0
- package/src/server/management/session-routes.ts +13 -0
- package/src/server/management/system-routes.ts +13 -1
- package/src/server/management-api.ts +8 -3
- package/src/server/management-auth.ts +124 -82
- package/src/server/models-capabilities.ts +124 -0
- package/src/server/ports.ts +7 -0
- package/src/server/proxy-liveness.ts +7 -0
- package/src/server/request-log.ts +11 -5
- package/src/server/responses/agent-task-recovery.ts +1 -0
- package/src/server/responses/codex-auth-error.ts +3 -1
- package/src/server/responses/collaboration.ts +26 -1
- package/src/server/responses/combo-stream-preflight.ts +25 -6
- package/src/server/responses/compact.ts +32 -12
- package/src/server/responses/context-overflow.ts +49 -0
- package/src/server/responses/core.ts +196 -12
- package/src/server/responses/fetch-helpers.ts +2 -1
- package/src/server/responses/outbound-body-guard.ts +110 -0
- package/src/server/responses/ws-upstream.ts +87 -6
- package/src/server/responses-self-named-namespace-scrub.ts +181 -0
- package/src/server/system-env.ts +78 -25
- package/src/service.ts +218 -42
- package/src/types/config.ts +134 -0
- package/src/types/provider.ts +54 -1
- package/src/types/request.ts +2 -0
- package/src/types.ts +5 -0
- package/src/update/job.ts +33 -5
- package/src/usage/cost.ts +32 -1
- package/src/usage/expected-prices.ts +16 -0
- package/src/usage/log.ts +6 -2
- package/src/usage/summary.ts +48 -6
- package/gui/dist/assets/index-D-lchsPw.js +0 -112
- package/gui/dist/assets/index-uvENYLin.css +0 -1
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { randomBytes, randomUUID, timingSafeEqual } from "node:crypto";
|
|
2
|
+
import type { OcxConfig } from "../../types";
|
|
3
|
+
|
|
4
|
+
export const API_KEY_ROTATION_TTL_MS = 10 * 60_000;
|
|
5
|
+
|
|
6
|
+
export type ApiKeyRotationStart = {
|
|
7
|
+
id: string;
|
|
8
|
+
name: string;
|
|
9
|
+
key: string;
|
|
10
|
+
rotationId: string;
|
|
11
|
+
expiresAt: string;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
function equalOpaqueId(left: string, right: string): boolean {
|
|
15
|
+
const encoder = new TextEncoder();
|
|
16
|
+
const a = encoder.encode(left);
|
|
17
|
+
const b = encoder.encode(right);
|
|
18
|
+
return a.length === b.length && timingSafeEqual(a, b);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function removeExpiredApiKeyRotations(config: OcxConfig, now = Date.now()): boolean {
|
|
22
|
+
let changed = false;
|
|
23
|
+
for (const entry of config.apiKeys ?? []) {
|
|
24
|
+
if (entry.pendingRotation && Date.parse(entry.pendingRotation.expiresAt) <= now) {
|
|
25
|
+
delete entry.pendingRotation;
|
|
26
|
+
changed = true;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return changed;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function startApiKeyRotation(
|
|
33
|
+
config: OcxConfig,
|
|
34
|
+
keyId: string,
|
|
35
|
+
now = Date.now(),
|
|
36
|
+
): ApiKeyRotationStart | { error: "not-found" | "already-pending" } {
|
|
37
|
+
const entry = (config.apiKeys ?? []).find(candidate => candidate.id === keyId);
|
|
38
|
+
if (!entry) return { error: "not-found" };
|
|
39
|
+
if (entry.pendingRotation && Date.parse(entry.pendingRotation.expiresAt) > now) {
|
|
40
|
+
return { error: "already-pending" };
|
|
41
|
+
}
|
|
42
|
+
const key = `ocx_data_${randomBytes(20).toString("hex")}`;
|
|
43
|
+
const rotationId = randomUUID();
|
|
44
|
+
const createdAt = new Date(now).toISOString();
|
|
45
|
+
const expiresAt = new Date(now + API_KEY_ROTATION_TTL_MS).toISOString();
|
|
46
|
+
entry.pendingRotation = { id: rotationId, key, createdAt, expiresAt };
|
|
47
|
+
return { id: entry.id, name: entry.name, key, rotationId, expiresAt };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function commitApiKeyRotation(
|
|
51
|
+
config: OcxConfig,
|
|
52
|
+
keyId: string,
|
|
53
|
+
rotationId: string,
|
|
54
|
+
now = Date.now(),
|
|
55
|
+
): { ok: true } | { error: "not-found" | "expired" | "mismatch" } {
|
|
56
|
+
const entry = (config.apiKeys ?? []).find(candidate => candidate.id === keyId);
|
|
57
|
+
if (!entry?.pendingRotation) return { error: "not-found" };
|
|
58
|
+
const pending = entry.pendingRotation;
|
|
59
|
+
if (Date.parse(pending.expiresAt) <= now) {
|
|
60
|
+
delete entry.pendingRotation;
|
|
61
|
+
return { error: "expired" };
|
|
62
|
+
}
|
|
63
|
+
if (!equalOpaqueId(pending.id, rotationId)) return { error: "mismatch" };
|
|
64
|
+
entry.key = pending.key;
|
|
65
|
+
delete entry.pendingRotation;
|
|
66
|
+
return { ok: true };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function abortApiKeyRotation(config: OcxConfig, keyId: string, rotationId: string): boolean {
|
|
70
|
+
const entry = (config.apiKeys ?? []).find(candidate => candidate.id === keyId);
|
|
71
|
+
if (!entry?.pendingRotation || !equalOpaqueId(entry.pendingRotation.id, rotationId)) return false;
|
|
72
|
+
delete entry.pendingRotation;
|
|
73
|
+
return true;
|
|
74
|
+
}
|
|
@@ -68,12 +68,24 @@ import { readManagementJsonBody, rethrowManagementBodyTooLarge } from "./body";
|
|
|
68
68
|
import { shadowCallTargetError } from "./shadow-call-validation";
|
|
69
69
|
|
|
70
70
|
|
|
71
|
-
/**
|
|
72
|
-
|
|
73
|
-
|
|
71
|
+
/**
|
|
72
|
+
* Management wire shape: omit fields whose value is the default, so GET responses and
|
|
73
|
+
* persisted config stay sparse. A default echoed here would be written straight back by
|
|
74
|
+
* any client that round-trips GET into PUT, which is how an unset option ends up
|
|
75
|
+
* materialized in every user's config.json.
|
|
76
|
+
*/
|
|
77
|
+
function sparseComboConfig<T extends {
|
|
78
|
+
imageInput?: "auto" | "disabled";
|
|
79
|
+
reasoningEffortMode?: "strict" | "adaptive";
|
|
80
|
+
}>(combo: T): Omit<T, "imageInput" | "reasoningEffortMode"> & {
|
|
81
|
+
imageInput?: "disabled";
|
|
82
|
+
reasoningEffortMode?: "adaptive";
|
|
83
|
+
} {
|
|
84
|
+
const { imageInput, reasoningEffortMode, ...rest } = combo;
|
|
74
85
|
return {
|
|
75
86
|
...rest,
|
|
76
87
|
...(imageInput === "disabled" ? { imageInput: "disabled" as const } : {}),
|
|
88
|
+
...(reasoningEffortMode === "adaptive" ? { reasoningEffortMode: "adaptive" as const } : {}),
|
|
77
89
|
};
|
|
78
90
|
}
|
|
79
91
|
|
|
@@ -137,19 +149,19 @@ export async function handleComboRoutes(ctx: ManagementContext): Promise<Respons
|
|
|
137
149
|
if (error) return jsonResponse({ error }, 400);
|
|
138
150
|
const normalized = normalizeComboConfig(body.combo as import("../../types").OcxComboConfig);
|
|
139
151
|
// Persist only non-default identity/capability fields so config stays sparse.
|
|
152
|
+
// Capability defaults (`imageInput`, `reasoningEffortMode`) go through the same
|
|
153
|
+
// helper the GET/PUT responses use, so the wire shape and the stored shape cannot drift.
|
|
140
154
|
const {
|
|
141
155
|
alias: normalizedAlias,
|
|
142
156
|
nativeAlias: normalizedNativeAlias,
|
|
143
157
|
displayName: normalizedDisplayName,
|
|
144
|
-
imageInput: normalizedImageInput,
|
|
145
158
|
...normalizedBase
|
|
146
|
-
} = normalized;
|
|
159
|
+
} = sparseComboConfig(normalized);
|
|
147
160
|
const stored: import("../../types").OcxComboConfig = {
|
|
148
161
|
...normalizedBase,
|
|
149
162
|
...(normalizedAlias ? { alias: normalizedAlias } : {}),
|
|
150
163
|
...(normalizedNativeAlias ? { nativeAlias: true } : {}),
|
|
151
164
|
...(normalizedDisplayName ? { displayName: normalizedDisplayName } : {}),
|
|
152
|
-
...(normalizedImageInput === "disabled" ? { imageInput: "disabled" as const } : {}),
|
|
153
165
|
};
|
|
154
166
|
const sourceId = renameFrom ?? id;
|
|
155
167
|
const previous = config.combos?.[sourceId];
|
|
@@ -306,6 +306,8 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
|
|
|
306
306
|
// Absent means the historical auto-open, so the GUI can render the toggle
|
|
307
307
|
// without having to know that `undefined` and `true` mean the same thing.
|
|
308
308
|
oauthOpenBrowser: config.oauthOpenBrowser !== false,
|
|
309
|
+
// Absent means off (today's Design B injection), so the GUI/CLI render a plain switch.
|
|
310
|
+
codexDesktopAuthless: config.codexDesktopAuthless === true,
|
|
309
311
|
startupHealth: await readStartupHealth(config),
|
|
310
312
|
codexRuntime: {
|
|
311
313
|
path: displayCodexRuntimePath(resolved.runtime.command),
|
|
@@ -392,14 +394,16 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
|
|
|
392
394
|
codexAccountPickerEnabled?: unknown;
|
|
393
395
|
oauthOpenBrowser?: unknown;
|
|
394
396
|
showCodexSparkQuota?: unknown;
|
|
397
|
+
codexDesktopAuthless?: unknown;
|
|
395
398
|
};
|
|
396
399
|
if (body.codexAutoStart === undefined
|
|
397
400
|
&& body.streamMode === undefined
|
|
398
401
|
&& body.appOwnedMemoryBudgetMb === undefined
|
|
399
402
|
&& body.codexAccountPickerEnabled === undefined
|
|
400
403
|
&& body.oauthOpenBrowser === undefined
|
|
401
|
-
&& body.showCodexSparkQuota === undefined
|
|
402
|
-
|
|
404
|
+
&& body.showCodexSparkQuota === undefined
|
|
405
|
+
&& body.codexDesktopAuthless === undefined) {
|
|
406
|
+
return jsonResponse({ error: "provide codexAutoStart, streamMode, appOwnedMemoryBudgetMb, codexAccountPickerEnabled, oauthOpenBrowser, showCodexSparkQuota, or codexDesktopAuthless" }, 400);
|
|
403
407
|
}
|
|
404
408
|
if (body.codexAutoStart !== undefined && typeof body.codexAutoStart !== "boolean") {
|
|
405
409
|
return jsonResponse({ error: "codexAutoStart boolean is required" }, 400);
|
|
@@ -417,6 +421,9 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
|
|
|
417
421
|
if (body.showCodexSparkQuota !== undefined && typeof body.showCodexSparkQuota !== "boolean") {
|
|
418
422
|
return jsonResponse({ error: "showCodexSparkQuota boolean is required" }, 400);
|
|
419
423
|
}
|
|
424
|
+
if (body.codexDesktopAuthless !== undefined && typeof body.codexDesktopAuthless !== "boolean") {
|
|
425
|
+
return jsonResponse({ error: "codexDesktopAuthless boolean is required" }, 400);
|
|
426
|
+
}
|
|
420
427
|
if (body.appOwnedMemoryBudgetMb !== undefined && (
|
|
421
428
|
typeof body.appOwnedMemoryBudgetMb !== "number"
|
|
422
429
|
|| !Number.isInteger(body.appOwnedMemoryBudgetMb)
|
|
@@ -440,9 +447,12 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
|
|
|
440
447
|
hasOauthOpenBrowser: Object.hasOwn(config, "oauthOpenBrowser"),
|
|
441
448
|
showCodexSparkQuota: config.showCodexSparkQuota,
|
|
442
449
|
hasShowCodexSparkQuota: Object.hasOwn(config, "showCodexSparkQuota"),
|
|
450
|
+
codexDesktopAuthless: config.codexDesktopAuthless,
|
|
451
|
+
hasCodexDesktopAuthless: Object.hasOwn(config, "codexDesktopAuthless"),
|
|
443
452
|
};
|
|
444
453
|
const pickerWasEnabled = codexAccountPickerEnabled(config);
|
|
445
454
|
let pickerIsEnabled = pickerWasEnabled;
|
|
455
|
+
const authlessWasEnabled = config.codexDesktopAuthless === true;
|
|
446
456
|
try {
|
|
447
457
|
if (typeof body.codexAutoStart === "boolean") {
|
|
448
458
|
config.codexAutoStart = body.codexAutoStart;
|
|
@@ -469,6 +479,8 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
|
|
|
469
479
|
if (typeof body.showCodexSparkQuota === "boolean") {
|
|
470
480
|
config.showCodexSparkQuota = body.showCodexSparkQuota;
|
|
471
481
|
}
|
|
482
|
+
if (body.codexDesktopAuthless === true) config.codexDesktopAuthless = true;
|
|
483
|
+
else if (body.codexDesktopAuthless === false) deleteConfigTopLevelKey(config, "codexDesktopAuthless");
|
|
472
484
|
pickerIsEnabled = codexAccountPickerEnabled(config);
|
|
473
485
|
(deps.saveConfigPreservingClaudeCode ?? saveConfigPreservingClaudeCode)(config);
|
|
474
486
|
} catch (error) {
|
|
@@ -491,13 +503,19 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
|
|
|
491
503
|
if (previousSettings.hasShowCodexSparkQuota) {
|
|
492
504
|
config.showCodexSparkQuota = previousSettings.showCodexSparkQuota;
|
|
493
505
|
} else deleteConfigTopLevelKey(config, "showCodexSparkQuota");
|
|
506
|
+
if (previousSettings.hasCodexDesktopAuthless) {
|
|
507
|
+
config.codexDesktopAuthless = previousSettings.codexDesktopAuthless;
|
|
508
|
+
} else deleteConfigTopLevelKey(config, "codexDesktopAuthless");
|
|
494
509
|
throw error;
|
|
495
510
|
}
|
|
496
511
|
if (typeof body.appOwnedMemoryBudgetMb === "number") {
|
|
497
512
|
configureAppOwnedMemoryBudget(resolveAppOwnedMemoryBudgetBytes(body.appOwnedMemoryBudgetMb));
|
|
498
513
|
enforceAppOwnedMemoryBudget();
|
|
499
514
|
}
|
|
500
|
-
|
|
515
|
+
// The authless switch changes the injected config.toml shape, so converge now rather than
|
|
516
|
+
// waiting for the next start; the injector re-reads config and rewrites the form.
|
|
517
|
+
const authlessIsEnabled = config.codexDesktopAuthless === true;
|
|
518
|
+
const catalogRefresh = pickerWasEnabled !== pickerIsEnabled || authlessWasEnabled !== authlessIsEnabled
|
|
501
519
|
? await convergeCodexCatalog()
|
|
502
520
|
: undefined;
|
|
503
521
|
const catalogRefreshPending = catalogRefresh
|
|
@@ -513,6 +531,7 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
|
|
|
513
531
|
oauthOpenBrowser: config.oauthOpenBrowser !== false,
|
|
514
532
|
catalogRefreshPending,
|
|
515
533
|
showCodexSparkQuota: config.showCodexSparkQuota === true,
|
|
534
|
+
codexDesktopAuthless: authlessIsEnabled,
|
|
516
535
|
startupHealth: await readStartupHealth(config),
|
|
517
536
|
});
|
|
518
537
|
}
|
|
@@ -4,7 +4,7 @@ import type { CodexLogGuardProtectionDeps } from "../../codex/log-guard/protecti
|
|
|
4
4
|
import type { CodexLogGuardMaintenanceDeps } from "../../codex/log-guard/maintenance";
|
|
5
5
|
import type { StartupHealth } from "../../codex/autostart-health";
|
|
6
6
|
import type { StartupInstallAction } from "../startup-action-control";
|
|
7
|
-
import type { ManagementPrincipal } from "../management-auth";
|
|
7
|
+
import type { ManagementPrincipal, ManagementSessionControl } from "../management-auth";
|
|
8
8
|
import type { CatalogModel } from "../../codex/catalog";
|
|
9
9
|
import type { Paths as CodexPromptPaths } from "../../codex/prompt-layers";
|
|
10
10
|
import type { injectGrokConfig } from "../../grok/inject";
|
|
@@ -109,6 +109,8 @@ export interface ManagementContext {
|
|
|
109
109
|
url: URL;
|
|
110
110
|
config: OcxConfig;
|
|
111
111
|
deps: ManagementApiDeps;
|
|
112
|
+
/** Installed package version projected through bounded system identity routes. */
|
|
113
|
+
version: string;
|
|
112
114
|
/**
|
|
113
115
|
* Which credential authorized this request, resolved by the auth gate before
|
|
114
116
|
* dispatch. Routes that spend the USER's identity (not just the proxy's) must
|
|
@@ -118,6 +120,8 @@ export interface ManagementContext {
|
|
|
118
120
|
* tests, which are treated as the untrusted `admin-token` case.
|
|
119
121
|
*/
|
|
120
122
|
principal?: ManagementPrincipal;
|
|
123
|
+
/** Narrow current-session revocation seam; contains neither the token nor session map. */
|
|
124
|
+
sessionControl?: ManagementSessionControl;
|
|
121
125
|
convergeCodexCatalog: () => Promise<CatalogDisposition>;
|
|
122
126
|
syncClaudeAgentDefsBestEffort: () => Promise<void>;
|
|
123
127
|
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Read-only status for the Cursor integration card.
|
|
3
|
+
*
|
|
4
|
+
* Cursor Private Inference is configured inside Cursor (Settings > Models > Gateway), not by
|
|
5
|
+
* this proxy: its settings live in a SQLite database the running app rewrites and its API key
|
|
6
|
+
* in the OS keychain, both out of bounds for opencodex. So this route only answers the three
|
|
7
|
+
* questions the dashboard needs — which Cursor builds are installed, what to paste into the
|
|
8
|
+
* gateway form, and whether a Cursor client has actually called `/v1/models` since the proxy
|
|
9
|
+
* started — plus which active models will show Cursor's Reasoning and Context controls.
|
|
10
|
+
*/
|
|
11
|
+
import { readRuntimePort } from "../../config/process-state";
|
|
12
|
+
import { filterCatalogVisibleModels, nativeContextLimits, nativeOpenAiContextTier, uniqueCatalogModelsForRawPublicList, visibleNativeSlugs } from "../../codex/catalog";
|
|
13
|
+
import { cursorLastSeen, type CursorSeen } from "../../integrations/cursor-seen";
|
|
14
|
+
import { detectCursorInstalls, type CursorInstall } from "../../integrations/cursor-detect";
|
|
15
|
+
import { configuredApiAuthToken, isApiAuthRequired, jsonResponse } from "../auth-cors";
|
|
16
|
+
import { fetchAllModels } from "../management-api";
|
|
17
|
+
import { cursorEffortFamily } from "../models-capabilities";
|
|
18
|
+
import type { ManagementContext } from "./context";
|
|
19
|
+
|
|
20
|
+
export const CURSOR_GATEWAY_PLACEHOLDER_KEY = "opencodex-loopback";
|
|
21
|
+
export const CURSOR_GUIDE_URL = "https://lidge-jun.github.io/opencodex/guides/cursor-private-inference/";
|
|
22
|
+
|
|
23
|
+
export interface CursorIntegrationStatus {
|
|
24
|
+
privateInference: { installed: boolean; path: string | null; version: string | null };
|
|
25
|
+
regularCursor: { installed: boolean; path: string | null };
|
|
26
|
+
gateway: { baseUrl: string; apiKeyMode: "credential" | "placeholder"; placeholder: string };
|
|
27
|
+
lastSeen: CursorSeen | null;
|
|
28
|
+
models: Array<{
|
|
29
|
+
id: string;
|
|
30
|
+
reasoning: string[] | null;
|
|
31
|
+
context: { defaultWindow: number; longWindow: number } | null;
|
|
32
|
+
}>;
|
|
33
|
+
guideUrl: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function pick(installs: CursorInstall[], build: CursorInstall["build"]): CursorInstall | undefined {
|
|
37
|
+
return installs.find(install => install.build === build);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export async function buildCursorIntegrationStatus(
|
|
41
|
+
ctx: Pick<ManagementContext, "config" | "deps"> & { url?: URL },
|
|
42
|
+
installs: CursorInstall[] = detectCursorInstalls(),
|
|
43
|
+
): Promise<CursorIntegrationStatus> {
|
|
44
|
+
const { config, deps } = ctx;
|
|
45
|
+
const privateInference = pick(installs, "private-inference");
|
|
46
|
+
const regular = pick(installs, "regular");
|
|
47
|
+
const runtime = (deps.readRuntimePort ?? readRuntimePort)(process.pid);
|
|
48
|
+
// The port the browser reached is the one Cursor on the same machine will reach too; the
|
|
49
|
+
// runtime record and config.port are fallbacks for a request that carries no port.
|
|
50
|
+
const port = runtime?.port ?? (Number(ctx.url?.port) || config.port);
|
|
51
|
+
// Describes the public bind. A second unauthenticated loopback listener may exist, but the
|
|
52
|
+
// value a user pastes into Cursor must work against the bind they will actually reach.
|
|
53
|
+
const credentialConfigured = !!configuredApiAuthToken(config)
|
|
54
|
+
|| (config.apiKeys ?? []).some(entry => !!entry.key.trim());
|
|
55
|
+
const apiKeyMode = isApiAuthRequired(config) || credentialConfigured ? "credential" : "placeholder";
|
|
56
|
+
|
|
57
|
+
const limits = nativeContextLimits(config);
|
|
58
|
+
// Same visibility rules as the raw /v1/models list Cursor will read: disabled models and
|
|
59
|
+
// provider allowlists drop out here too, or the prediction shows rows Cursor never gets.
|
|
60
|
+
const goModels = filterCatalogVisibleModels(await fetchAllModels(config), config);
|
|
61
|
+
const ids = [
|
|
62
|
+
...visibleNativeSlugs(config),
|
|
63
|
+
...uniqueCatalogModelsForRawPublicList(goModels).map(model => model.alias ?? `${model.provider}/${model.id}`),
|
|
64
|
+
];
|
|
65
|
+
const models = ids.map(id => {
|
|
66
|
+
const tier = nativeOpenAiContextTier(id, limits);
|
|
67
|
+
return {
|
|
68
|
+
id,
|
|
69
|
+
reasoning: cursorEffortFamily(id),
|
|
70
|
+
context: tier ? { defaultWindow: tier.defaultWindow, longWindow: tier.longWindow } : null,
|
|
71
|
+
};
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
privateInference: {
|
|
76
|
+
installed: privateInference !== undefined,
|
|
77
|
+
path: privateInference?.path ?? null,
|
|
78
|
+
version: privateInference?.version ?? null,
|
|
79
|
+
},
|
|
80
|
+
regularCursor: { installed: regular !== undefined, path: regular?.path ?? null },
|
|
81
|
+
gateway: {
|
|
82
|
+
baseUrl: `http://127.0.0.1:${port}/v1`,
|
|
83
|
+
apiKeyMode,
|
|
84
|
+
placeholder: CURSOR_GATEWAY_PLACEHOLDER_KEY,
|
|
85
|
+
},
|
|
86
|
+
lastSeen: cursorLastSeen(),
|
|
87
|
+
models,
|
|
88
|
+
guideUrl: CURSOR_GUIDE_URL,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export async function handleCursorIntegrationRoutes(ctx: ManagementContext): Promise<Response | null> {
|
|
93
|
+
const { req, url } = ctx;
|
|
94
|
+
if (url.pathname === "/api/native-integrations/cursor" && req.method === "GET") {
|
|
95
|
+
return jsonResponse(await buildCursorIntegrationStatus(ctx));
|
|
96
|
+
}
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
@@ -202,10 +202,11 @@ export async function handleLogsUsageRoutes(ctx: ManagementContext): Promise<Res
|
|
|
202
202
|
const filter = {
|
|
203
203
|
provider: url.searchParams.get("provider"),
|
|
204
204
|
model: url.searchParams.get("model"),
|
|
205
|
+
apiKeyId: url.searchParams.get("apiKeyId"),
|
|
205
206
|
};
|
|
206
207
|
const project = <T extends UsageSummary>(summary: T, entries?: PersistedUsageEntry[]) =>
|
|
207
208
|
projectUsageSummary(summary, filter, entries);
|
|
208
|
-
const filterRequested = Boolean(filter.provider ?? filter.model);
|
|
209
|
+
const filterRequested = Boolean(filter.provider ?? filter.model ?? filter.apiKeyId);
|
|
209
210
|
const now = Date.now();
|
|
210
211
|
try {
|
|
211
212
|
const cacheKey = `${range}:${surface}`;
|
|
@@ -72,13 +72,15 @@ function readDefaultReasoningEffort(raw: unknown, efforts: string[] | undefined)
|
|
|
72
72
|
import type { CatalogModel } from "../../codex/catalog";
|
|
73
73
|
import { accountBoundNativeOpenAiSlugsBySelector, catalogModelSlug, configuredNativeAliasSlugs, disabledNativeSlugs, invalidateCodexModelsCache, nativeModelRows, shouldIncludeAccountBoundNativeOpenAi, uniqueCatalogModelsForPublicList } from "../../codex/catalog";
|
|
74
74
|
import { CatalogGatherBusyError } from "../../codex/catalog/provider-fetch";
|
|
75
|
+
import { clearModelCache, getProviderLiveModelCount } from "../../codex/model-cache";
|
|
75
76
|
import { NATIVE_OPENAI_MODELS } from "../../codex/catalog/native-models";
|
|
76
|
-
|
|
77
|
+
|
|
77
78
|
import {
|
|
78
79
|
DEFAULT_SUBAGENT_MODELS,
|
|
79
80
|
codexAutoStartEnabled,
|
|
80
81
|
hasOwnProvider,
|
|
81
82
|
isValidProviderName,
|
|
83
|
+
modelDisplayNamesConfigError,
|
|
82
84
|
multiAgentGuidanceEnabled,
|
|
83
85
|
providerBaseUrlConfigError,
|
|
84
86
|
providerHeadersConfigError,
|
|
@@ -101,6 +103,7 @@ import { providerCodexAccountMode } from "../../providers/registry";
|
|
|
101
103
|
import { encodedModelIdCollides, routedSlug, slugEquals } from "../../providers/slug-codec";
|
|
102
104
|
import { knownModelIdsForProvider } from "../../router";
|
|
103
105
|
import { effectiveModelAliases, MODEL_ALIAS_PATTERN } from "../../providers/default-aliases";
|
|
106
|
+
import { isValidModelDiscoveryModelId } from "../../providers/model-discovery-limits";
|
|
104
107
|
import { comboPublicModelId } from "../../combos/types";
|
|
105
108
|
import { COMBO_NAMESPACE, comboDisabledModelSelectors, comboModelId, preservesPhysicalComboProvider } from "../../combos";
|
|
106
109
|
import { clearProviderQuotaCache, fetchProviderQuotaReports } from "../../providers/quota";
|
|
@@ -353,6 +356,81 @@ export async function handleModelRoutes(ctx: ManagementContext): Promise<Respons
|
|
|
353
356
|
return jsonResponse(await listManagementModelRows(config));
|
|
354
357
|
}
|
|
355
358
|
|
|
359
|
+
const displayNameMatch = url.pathname.match(/^\/api\/providers\/([^/]+)\/model-display-names$/);
|
|
360
|
+
if (displayNameMatch && req.method === "PUT") {
|
|
361
|
+
let name: string;
|
|
362
|
+
try { name = decodeURIComponent(displayNameMatch[1]!); } catch { return jsonResponse({ error: "invalid provider encoding" }, 400); }
|
|
363
|
+
if (name === "keys") return null;
|
|
364
|
+
if (!hasOwnProvider(config.providers, name)) {
|
|
365
|
+
return jsonResponse({ error: `provider '${name}' not found` }, 404, req, config);
|
|
366
|
+
}
|
|
367
|
+
let body: unknown;
|
|
368
|
+
try { body = await readManagementJsonBody(req); } catch (error) { rethrowManagementBodyTooLarge(error); return jsonResponse({ error: "invalid JSON body" }, 400); }
|
|
369
|
+
if (!isPlainRecord(body) || typeof body.modelId !== "string"
|
|
370
|
+
|| (body.displayName !== null && typeof body.displayName !== "string")) {
|
|
371
|
+
return jsonResponse({ error: "modelId and displayName string or null are required" }, 400, req, config);
|
|
372
|
+
}
|
|
373
|
+
const modelId = body.modelId.trim();
|
|
374
|
+
const displayName = typeof body.displayName === "string" ? body.displayName.trim() : null;
|
|
375
|
+
if (!isValidModelDiscoveryModelId(modelId)) {
|
|
376
|
+
return jsonResponse({ error: "modelId must be a valid model id" }, 400, req, config);
|
|
377
|
+
}
|
|
378
|
+
const validationError = displayName === null
|
|
379
|
+
? null
|
|
380
|
+
: modelDisplayNamesConfigError({ [modelId]: displayName });
|
|
381
|
+
if (validationError) return jsonResponse({ error: validationError }, 400, req, config);
|
|
382
|
+
|
|
383
|
+
const provider = config.providers[name]!;
|
|
384
|
+
const hadDisplayNames = Object.hasOwn(provider, "modelDisplayNames");
|
|
385
|
+
const previousDisplayNames = provider.modelDisplayNames;
|
|
386
|
+
const nextDisplayNames = Object.assign(
|
|
387
|
+
Object.create(null) as Record<string, string>,
|
|
388
|
+
previousDisplayNames ?? {},
|
|
389
|
+
);
|
|
390
|
+
if (displayName === null) delete nextDisplayNames[modelId];
|
|
391
|
+
else nextDisplayNames[modelId] = displayName;
|
|
392
|
+
const mergedValidationError = modelDisplayNamesConfigError(nextDisplayNames);
|
|
393
|
+
if (mergedValidationError) return jsonResponse({ error: mergedValidationError }, 400, req, config);
|
|
394
|
+
if (Object.keys(nextDisplayNames).length > 0) provider.modelDisplayNames = nextDisplayNames;
|
|
395
|
+
else delete provider.modelDisplayNames;
|
|
396
|
+
|
|
397
|
+
try {
|
|
398
|
+
persistConfig(config);
|
|
399
|
+
} catch (error) {
|
|
400
|
+
if (hadDisplayNames) provider.modelDisplayNames = previousDisplayNames;
|
|
401
|
+
else delete provider.modelDisplayNames;
|
|
402
|
+
throw error;
|
|
403
|
+
}
|
|
404
|
+
clearModelCache(name);
|
|
405
|
+
const catalogRefresh = await convergeCodexCatalog();
|
|
406
|
+
const storedDisplayName = provider.modelDisplayNames?.[modelId] ?? null;
|
|
407
|
+
if (catalogRefresh.status === "failed") {
|
|
408
|
+
return jsonResponse({
|
|
409
|
+
error: "model display name saved but catalog refresh failed",
|
|
410
|
+
saved: true,
|
|
411
|
+
provider: name,
|
|
412
|
+
modelId,
|
|
413
|
+
displayNameOverride: storedDisplayName,
|
|
414
|
+
catalogRefresh,
|
|
415
|
+
}, 503, req, config);
|
|
416
|
+
}
|
|
417
|
+
const row = (await listManagementModelRows(config)).find(candidate => (
|
|
418
|
+
candidate.native !== true
|
|
419
|
+
&& candidate.custom !== true
|
|
420
|
+
&& candidate.provider === name
|
|
421
|
+
&& candidate.id === modelId
|
|
422
|
+
));
|
|
423
|
+
return jsonResponse({
|
|
424
|
+
ok: true,
|
|
425
|
+
provider: name,
|
|
426
|
+
modelId,
|
|
427
|
+
displayName: row?.displayName ?? storedDisplayName ?? routedSlug(name, modelId),
|
|
428
|
+
displayNameOverride: storedDisplayName,
|
|
429
|
+
displayNameSource: row?.displayNameSource ?? (storedDisplayName ? "operator" : "fallback"),
|
|
430
|
+
catalogRefresh,
|
|
431
|
+
});
|
|
432
|
+
}
|
|
433
|
+
|
|
356
434
|
/**
|
|
357
435
|
* Client config document for OpenCode / Pi, built from the SAME function `ocx export`
|
|
358
436
|
* calls, so the bytes a user downloads here and the bytes they pipe from the CLI cannot
|
|
@@ -41,8 +41,28 @@ export type ManagementModelRow = Partial<CatalogModel> & {
|
|
|
41
41
|
native?: boolean;
|
|
42
42
|
custom?: boolean;
|
|
43
43
|
customId?: string;
|
|
44
|
+
displayNameOverride?: string;
|
|
45
|
+
displayNameSource?: "operator" | "provider" | "fallback";
|
|
44
46
|
};
|
|
45
47
|
|
|
48
|
+
/** Resolve the exact text and source shown for one routed discovered model. */
|
|
49
|
+
export function effectiveManagementDisplayName(
|
|
50
|
+
config: Pick<OcxConfig, "providers">,
|
|
51
|
+
model: CatalogModel,
|
|
52
|
+
): Pick<ManagementModelRow, "displayName" | "displayNameOverride" | "displayNameSource"> {
|
|
53
|
+
const provider = config.providers[model.provider];
|
|
54
|
+
const configured = provider?.modelDisplayNames;
|
|
55
|
+
if (configured && Object.hasOwn(configured, model.id)) {
|
|
56
|
+
const displayName = configured[model.id]?.trim();
|
|
57
|
+
if (displayName) {
|
|
58
|
+
return { displayName, displayNameOverride: displayName, displayNameSource: "operator" };
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
const providerDisplayName = model.displayName?.trim();
|
|
62
|
+
if (providerDisplayName) return { displayName: providerDisplayName, displayNameSource: "provider" };
|
|
63
|
+
return { displayName: catalogModelSlug(model), displayNameSource: "fallback" };
|
|
64
|
+
}
|
|
65
|
+
|
|
46
66
|
/**
|
|
47
67
|
* The exact row list `/api/models` returns. Extracted so `/api/client-config` exports the
|
|
48
68
|
* models the GUI's Models tab shows — including this function's `disabled` computation,
|
|
@@ -133,8 +153,10 @@ export async function listManagementModelRows(
|
|
|
133
153
|
if (m.provider !== "combo" && customNamespaced.has(namespaced)) return null;
|
|
134
154
|
const contextCap = providerContextCap(config, m.provider);
|
|
135
155
|
const nativeAlias = m.provider === "combo" && m.nativeAlias === true;
|
|
156
|
+
const displayName = effectiveManagementDisplayName(config, m);
|
|
136
157
|
return {
|
|
137
158
|
...m,
|
|
159
|
+
...displayName,
|
|
138
160
|
namespaced,
|
|
139
161
|
disabled: [...disabled].some(stored => (
|
|
140
162
|
(!nativeAlias && stored === namespaced) || slugEquals(stored, m.provider, m.id)
|
|
@@ -152,7 +174,7 @@ export function toExportModel(row: ManagementModelRow): ExportModel {
|
|
|
152
174
|
provider: row.provider,
|
|
153
175
|
id: row.id,
|
|
154
176
|
...(row.native ? { native: true } : {}),
|
|
155
|
-
...(row.displayName ? { displayName: row.displayName } : {}),
|
|
177
|
+
...(row.displayName && row.displayNameSource !== "fallback" ? { displayName: row.displayName } : {}),
|
|
156
178
|
...(row.contextWindow !== undefined ? { contextWindow: row.contextWindow } : {}),
|
|
157
179
|
...(row.inputModalities ? { inputModalities: row.inputModalities } : {}),
|
|
158
180
|
...(row.reasoningEfforts ? { reasoningEfforts: row.reasoningEfforts } : {}),
|